- 缓存热点批量流程(有采集缓存不触发 Google) - 简报不足直接从采集缓存生成(轻量补齐) - 三图合成(模特/印花/底图)+ 底图压缩 <2MB - 热点去重→风格去重自动切换 + 不适合类目 review 兜底 - 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述) - 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
125 lines
5.4 KiB
Python
125 lines
5.4 KiB
Python
"""节点 9/9:商品上传模板导出(template_export)——在 seed_shot 之后。
|
|
|
|
把最终结果导入模板:
|
|
- SPU货号 / SKU货号 = 设计货号(oss_code,前缀+3位计数)
|
|
- 商品名称 = cn_title(多模态标题生成)
|
|
- 英文名称 = en_title
|
|
- 商品轮播图1:SKU 行按颜色路由(该颜色三合一链接),SPU 行随机一张
|
|
- 详情图文(SPU 行):全部三合一主图链接 + 种草图链接,| 分割
|
|
|
|
需在 oss_upload / seed_shot 之后运行(图床链接与货号已生成)。
|
|
"""
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List
|
|
|
|
from graph.paths import project_root, runtime_root
|
|
from graph.validate import with_fallback
|
|
|
|
|
|
def _template_out_path(prod_dir: Path, tpl_name: str) -> Path:
|
|
"""模板输出路径:默认 {tpl_name}_已填写.xlsx;已存在/被占用则自动换名加序号(同款号多产品不互相覆盖)。"""
|
|
base = prod_dir / f"{tpl_name}_已填写.xlsx"
|
|
try:
|
|
with open(base, "ab"):
|
|
pass
|
|
except OSError:
|
|
pass
|
|
else:
|
|
if not base.exists():
|
|
return base
|
|
for i in range(2, 100):
|
|
cand = prod_dir / f"{tpl_name}_已填写_{i}.xlsx"
|
|
if not cand.exists():
|
|
return cand
|
|
return prod_dir / f"{tpl_name}_已填写_{int(time.time())}.xlsx"
|
|
|
|
|
|
@with_fallback("template_export")
|
|
def template_export_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
|
products: List[Dict[str, Any]] = state.get("product") or []
|
|
config = state["config"] or {}
|
|
pcfg = config.get("product") or {}
|
|
output_dir = Path(state["output_dir"])
|
|
errors = list(state.get("errors") or [])
|
|
stats = dict(state.get("stats") or {})
|
|
|
|
# 模板写入时机:所有集合/产品(含种草图、OSS)全部完成后才执行本节点
|
|
print(f"[template] 全部集合({len(products)} 个产品)处理完成,开始统一写入模板…")
|
|
|
|
tp = (pcfg.get("template_path") or "").strip()
|
|
if not tp:
|
|
print("[template] 未配置 product.template_path,跳过模板导出")
|
|
return {"stats": stats, "errors": errors}
|
|
if not Path(tp).exists():
|
|
cand = None
|
|
for root in (runtime_root(), project_root()):
|
|
c = root / tp
|
|
if c.exists():
|
|
cand = str(c)
|
|
break
|
|
if cand:
|
|
tp = cand
|
|
else:
|
|
print(f"[template] 模板文件不存在: {tp}")
|
|
return {"stats": stats, "errors": errors}
|
|
|
|
# db 路径
|
|
dbp = (pcfg.get("db_path") or "db/spu_sku.db")
|
|
db_path = Path(dbp)
|
|
if not db_path.is_absolute():
|
|
for root in (runtime_root(), project_root()):
|
|
if (root / db_path).exists():
|
|
db_path = root / db_path
|
|
break
|
|
|
|
from graph.template_export import export_product
|
|
tdir = (pcfg.get("template_dir") or "").strip() or str(Path(tp).parent)
|
|
prod_dir = output_dir / "product"
|
|
prod_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
exported: List[str] = []
|
|
skipped = 0
|
|
merged_out: Optional[str] = None # 合并模式:一次任务所有产品填同一个模板
|
|
is_first = True
|
|
for r in products:
|
|
# 失败跳过:合成图(composite/printed)与标题都失败的产品不写进模板
|
|
has_img = bool(r.get("composite_path") or r.get("printed_path"))
|
|
has_title = bool((r.get("cn_title") or "").strip())
|
|
if not (has_img and has_title):
|
|
skipped += 1
|
|
print(f"[template] 跳过失败产品 {r.get('spu_code')}/{r.get('img_code','')}: "
|
|
f"合成图={'有' if has_img else '无'} 标题={'有' if has_title else '无'}(不写入模板)")
|
|
continue
|
|
sku_codes = [cc.get("sku_code") for cc in (r.get("color_composites") or [])]
|
|
if not sku_codes:
|
|
sku_codes = [r.get("sku_code") or ""]
|
|
try:
|
|
if is_first:
|
|
merged_out = str(_template_out_path(prod_dir, "商品上传"))
|
|
out = export_product(
|
|
db_path, r.get("spu_code", ""), sku_codes, tdir, tp,
|
|
merged_out,
|
|
images=[],
|
|
spu_per_color=True, # 每颜色一个独立 SPU 块(单色多 SPU)
|
|
oss_code=r.get("oss_code") or (r.get("color_composites") or [{}])[0].get("code", ""),
|
|
cn_title=r.get("cn_title", ""),
|
|
en_title=r.get("en_title", ""),
|
|
ja_title=r.get("ja_title", ""),
|
|
composite_urls=r.get("color_composites") or [],
|
|
seed_shot_urls=r.get("seed_shot_urls") or [],
|
|
append_to="" if is_first else merged_out, # 首个产品从模板创建,后续追加合并
|
|
markup_percent=float(pcfg.get("markup_percent") or 0),
|
|
)
|
|
r["template_path"] = str(out)
|
|
exported.append(str(out))
|
|
print(f"[template] 商品上传模板已生成({len(exported)}/{len(products)} 合并): {out}")
|
|
except Exception as e: # noqa: BLE001
|
|
errors.append({"node": "template_export", "type": type(e).__name__,
|
|
"message": f"模板导出失败 {r.get('spu_code')}: {e}", "trace": ""})
|
|
print(f"[template] 模板导出失败 {r.get('spu_code')}: {e}")
|
|
is_first = False
|
|
|
|
stats["template_export"] = {"exported": len(exported)}
|
|
return {"product": products, "errors": errors, "stats": stats}
|