Files
pod_trend_agent/graph/template_export.py
T
3218485270 b7f429db89 模板导出增强 + 模特性别分组 + 三合一提示词精简
1) 模板导出:识别「基码表-胸围」填 sku.bust(多个胸围列都填);申报价格模糊匹配多列统一按加价后价格填写;详情图文不再拼接 img_url_2;SPU 款式来源统一填「现货款」;商品产地国家简称映射(沙特→沙特阿拉伯)
2) 模特性别分组:model_features 按男女分组,按模板类目含男/女固定取对应性别模特(含 Pinterest 模式 pipeline)
3) 三合一提示词:去掉 DESIGN CONTENT 四要素描述(设计已由设计稿提供)
4) 生图尺寸:全部改为读 config 不再硬编码(设计图 compose.design_size / 合成图 compose.size / 种草图 seed_shot.size)
2026-08-26 18:05:11 +08:00

622 lines
28 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""商品上传模板导出:从 db 读 SPU/SKU → 调 template_router 路由填入上传模板 Excel。
流程(product_node 生成产品图后调用):
1. 从 spu_sku.db 读 SPU(款号)+ 该款选定颜色的全部尺码 SKU;
2. 用 model/template_router.py 的 TemplateRouter
- insert 一行 SPUSPU货号 + 商品属性字段)
- 每个尺码 insert 一行 SKU(路由到 SPU 行下方,SKU货号 = 款号-颜色编码-尺码,填尺码表)
- 商品轮播图1~N 填生成的产品图路径(底图/印花/模特/合成)
3. save 输出 <模板名>_已填写.xlsx 到指定目录。
字段映射:db 字段名 → 上传模板列名(见 SPU_MAP / SKU_MAP)。
"""
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from graph.product import _connect
# 商品轮播图列名关键词(模板存在中/英/日变体,如 商品轮播图1 / Product Carousel Image 1 / 商品カルーセル画像1)
_CAROUSEL_KW = ("轮播", "carousel", "カルーセル")
# 商品产地:国家简称 → 正式名称(模版要求,如「沙特站」提取为「沙特」但需填「沙特阿拉伯」)
_COUNTRY_NAME_MAP = {
"沙特": "沙特阿拉伯",
}
def _size_rank(size) -> tuple:
"""把尺码字符串转成可排序 rank(从小到大)。
优先级:数字码(90/100...) < 字母码(XS/S/M/L/XL/XXL/3XL...) < 童装年龄码(0/3M、1-2Y...)
< 尺寸规格(30*40...) < 未知(字典序兜底) < 均码/Onesize(最后)。
"""
s = str(size or "").strip().upper()
if not s:
return (9, 0, "")
if s in ("ONESIZE", "ONE SIZE", "FREESIZE", "FREE SIZE", "均码"):
return (8, 0, s)
if s == "XS":
return (2, 0, s)
if s in ("S", "M", "L"):
return (2, {"S": 1, "M": 2, "L": 3}[s], s)
m = re.fullmatch(r"(X+)(L)", s) # XL/XXL/XXXL/XXXXL/XXXXXL
if m:
return (2, 3 + len(m.group(1)), s)
m = re.fullmatch(r"(\d+)XL", s) # 3XL/4XL/5XL
if m:
return (2, 3 + int(m.group(1)), s)
if s.isdigit(): # 数字码:90/100/110...
return (1, int(s), s)
m = re.fullmatch(r"(\d+)\s*[-/]\s*(\d+)\s*(?:M|Y|YRS|YR)?", s) # 童装年龄码
if m:
return (3, (int(m.group(1)) + int(m.group(2))) / 2.0, s)
m = re.search(r"(\d+(?:\.\d+)?)\s*(?:CM)?\s*[X*]", s) # 尺寸规格:30*40/12CM X 12CM
if m:
return (4, float(m.group(1)), s)
return (5, 0, s) # 未知格式:字典序兜底
def _carousel_col(router, idx: int) -> Optional[int]:
"""定位「商品轮播图{idx}」列号:先精确匹配(中文列名),失败则按中/英/日关键词模糊匹配序号。"""
try:
return router.resolve_col(f"商品轮播图{idx}")
except KeyError:
pass
for name, col in router.column_map.items():
low = str(name).lower().replace(" ", "").replace(" ", "")
if not any(k in low for k in _CAROUSEL_KW):
continue
m = re.search(r"(\d+)$", low)
if m and int(m.group(1)) == idx:
return col
return None
def _detail_col(router) -> Optional[int]:
"""定位「详情图文」列:优先英语(详情图文-英语),回退日语(详情图文-日语),再回退任意详情图文。"""
for name in ("详情图文-英语", "详情图文-英文", "详情图文-EN"):
try:
return router.resolve_col(name)
except KeyError:
pass
try:
return router.resolve_col("详情图文-日语")
except KeyError:
pass
for name, col in router.column_map.items():
if "详情图文" in str(name):
return col
return None
def _ja_col(router) -> Optional[int]:
"""定位「日语名称」列(基础信息组,如 日语名称/日语标题)。"""
try:
return router.resolve_col("日语名称")
except KeyError:
pass
for name, col in router.column_map.items():
low = str(name)
if "日语" in low and "详情图文" not in low and "轮播图" not in low and "名称" in low:
return col
return None
def _es_col(router) -> Optional[int]:
"""定位「西语名称」列(基础信息组,如 西语名称/西班牙语名称)。"""
try:
return router.resolve_col("西语名称")
except KeyError:
pass
for name, col in router.column_map.items():
low = str(name)
if ("西语" in low or "西班牙" in low) and "详情图文" not in low and "轮播图" not in low and "名称" in low:
return col
return None
def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_title: str,
ja_title: str, es_title: str, composite_by_sku: Dict[str, Any],
all_composite_urls: List[str], seed_shot_urls: List[str],
only_rows: Optional[List[int]] = None) -> None:
"""按用户要求填充设计联动字段:
- SPU 行:SPU货号=设计货号、SKU货号=设计货号、商品名称=en_title、英文名称=en_title、
日语名称=ja_title、西语名称=es_title、商品轮播图1=随机一张三合一主图、
详情图文=全部主图+种草图 链接 | 分割(不含 img_url_2
- SKU 行:SPU货号=设计货号、SKU货号=该颜色货号、商品轮播图1=该颜色三合一链接、
商品名称/英文名称/日语名称/西语名称 与 SPU 一致
only_rows:合并模式下只填充本产品块的行(None=该款全部行)
"""
import random
try:
color_col = router.resolve_col("色值(主规格)")
except KeyError:
color_col = None
try:
spu_col = router.resolve_col("SPU货号")
except KeyError:
spu_col = None
try:
name_col = router.resolve_col("商品名称")
except KeyError:
name_col = None
try:
en_col = router.resolve_col("英文名称")
except KeyError:
en_col = None
try:
ja_col = _ja_col(router)
except KeyError:
ja_col = None
try:
es_col = _es_col(router)
except KeyError:
es_col = None
try:
sku_code_col = router.resolve_col("SKU货号")
except KeyError:
sku_code_col = None
car1 = _carousel_col(router, 1)
detail_col = _detail_col(router)
for row in router.find_spu_rows(spu_code):
if only_rows is not None and row not in only_rows:
continue # 合并模式:只填本产品块的行
lvl = str(router.ws.cell(row, router.level_col).value or "").strip().lower()
color = str(router.ws.cell(row, color_col).value or "").strip() if color_col else ""
if lvl == "spu":
if spu_col and oss_code:
router.ws.cell(row, spu_col, oss_code)
if sku_code_col and oss_code:
router.ws.cell(row, sku_code_col, oss_code)
if name_col and en_title:
router.ws.cell(row, name_col, en_title) # 商品名称统一用 en_title
if en_col and en_title:
router.ws.cell(row, en_col, en_title)
if ja_col and ja_title:
router.ws.cell(row, ja_col, ja_title)
if es_col and es_title:
router.ws.cell(row, es_col, es_title)
if car1 is not None and all_composite_urls:
router.ws.cell(row, car1, random.choice(all_composite_urls)) # SPU 轮播图1 随机
if detail_col is not None:
links = [u for u in (all_composite_urls + list(seed_shot_urls or [])) if u]
if links:
router.ws.cell(row, detail_col, "|".join(links)) # 详情图文 | 分割
else:
if spu_col and oss_code:
router.ws.cell(row, spu_col, oss_code)
# SKU 行与 SPU 一致:商品名称/英文名称/日语名称/西语名称
if name_col and en_title:
router.ws.cell(row, name_col, en_title) # 商品名称统一用 en_title
if en_col and en_title:
router.ws.cell(row, en_col, en_title)
if ja_col and ja_title:
router.ws.cell(row, ja_col, ja_title)
if es_col and es_title:
router.ws.cell(row, es_col, es_title)
cc = composite_by_sku.get(color) or composite_by_sku.get("") # 按色值匹配该颜色主图
if sku_code_col and oss_code:
router.ws.cell(row, sku_code_col, oss_code) # SKU货号=SPU货号(同一货号)
if car1 is not None and cc and cc.get("url"):
router.ws.cell(row, car1, cc["url"]) # 该颜色轮播图1
def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str,
color: Optional[str] = None) -> Dict[str, Any]:
"""构造一行 SPU(固定字段:SKC货号=code、风格=休闲、商品产地=经营站点;多颜色时用色值列区分)。"""
row: Dict[str, Any] = {
"基础信息-商品层级": "spu",
"SKC货号": spu_code, # code 路由为 SKC货号(用户要求)
"风格": "休闲", # style 路由为"休闲"(用户要求)
"商品产地": origin_province, # 产地省份不用填,经营站点填到「商品产地」
"款式来源": "现货款", # SPU商品属性-款式来源 统一填「现货款」(用户要求)
}
if color:
row["色值(主规格)"] = color
for dbk, header in SPU_MAP.items():
v = spu.get(dbk)
if v not in (None, ""):
row[header] = v
return row
def _find_price_headers(router) -> List[str]:
"""定位所有「申报价格」列(美站/日站/英站…模糊匹配);匹配到多个时全部返回,统一填加价后价格。"""
hits = [str(k) for k in router.column_map if "申报价格" in str(k)]
return hits or ["申报价格-日本站"]
def _find_bust_headers(router) -> List[str]:
"""定位所有「胸围」列(基码表-胸围(cm)/胸围全围(cm)…模糊匹配);匹配到多个时全部填 sku.bust。"""
hits = [str(k) for k in router.column_map if "胸围" in str(k)]
return hits or ["胸围全围(cm"]
def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color: str,
warehouses: List[str], markup_percent: float = 0.0,
multi: bool = True, price_header: str = "申报价格-日本站",
bust_headers: Optional[List[str]] = None,
price_headers: Optional[List[str]] = None) -> Dict[str, Any]:
"""构造一行 SKU(固定字段:SPU货号、SKC货号=sku.code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。
价格(price_headers 列,如 申报价格-美国站/日本站,模糊匹配到多个时全部填)= SKU.price × (1+markup/100)
预先填好。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。
规格类型2 统一填「尺码」两个字(不是 size 参数值)。"""
row: Dict[str, Any] = {
"基础信息-商品层级": "sku",
"SPU货号": spu_code,
"SKC货号": sk.get("code") or sc, # SKC货号 = SKU 的 code(款号-颜色编码)
"色值(主规格)": color,
"规格类型2": "尺码", # 规格类型2 统一填「尺码」(不填 size 值)
"币种": "CNY",
}
if price_headers is None:
price_headers = [price_header]
if bust_headers is None:
bust_headers = ["胸围全围(cm"]
for j, w in enumerate(warehouses, start=1):
row[f"发货仓{j}"] = w
row[f"发货仓{j}库存"] = 200
for dbk, header in SKU_MAP.items():
if dbk == "color":
continue
v = sk.get(dbk)
if dbk == "bust":
if v in (None, ""):
continue
for h in bust_headers:
row[h] = v
continue
if dbk == "price":
if v in (None, ""):
continue
v = round(float(v) * (1 + markup_percent / 100), 2) # 申报价格 = price × (1+加价%)
for h in price_headers:
row[h] = v
continue
if v not in (None, ""):
row[header] = v
return row
def _fill_sku_carousel(router, spu_code: str, color: str, color_col: int,
first: Dict[str, Any], sku_imgs: List[str]) -> None:
"""SKU 行商品轮播图2~5db img_url_2~5 优先,无则回退生成图;按色值列匹配所属 SKU 行。"""
for j in range(2, 6):
url = first.get(f"img_url_{j}") # db CDN url(该颜色 SKU 的 img_url_2~5
img = url if url not in (None, "") else (sku_imgs[j - 2] if j - 2 < len(sku_imgs) else None)
if not img:
continue
col = _carousel_col(router, j)
if col is None:
continue
for row in router.find_spu_rows(spu_code):
if (str(router.ws.cell(row, router.level_col).value or "").strip().lower() == "sku"
and str(router.ws.cell(row, color_col).value or "").strip() == color):
router.ws.cell(row, col, str(img))
# db SPU 字段 -> 上传模板列名
SPU_MAP: Dict[str, str] = {
"code": "SPU货号",
"material": "材质",
"component_1": "成分1",
"component_proportion_1": "成分1成分比例",
"component_2": "成分2",
"component_proportion_2": "成分2成分比例",
"component_3": "成分3",
"component_proportion_3": "成分3成分比例",
"pattern": "图案",
"details": "细节",
"collar_style": "领型",
"care_Instructions": "护理说明",
"fabric": "面料",
"target_audience": "适用人群",
"season": "季节",
"is_transparent": "是否透明",
"layout": "版型",
"weaving_method": "织造方式",
"printing_type": "印花类型",
"fabric_texture_1": "面料纹理1",
"fabric_weight_1": "面料克重1g/m²)",
"fabric_weight_unit_1": "面料克重1g/m²)单位",
"lining_texture": "里料纹理",
}
# db SKU 字段 -> 上传模板列名
SKU_MAP: Dict[str, str] = {
"color": "色值(主规格)",
"size": "尺码",
"size_group": "尺码组别",
"size_type": "尺码类型",
"shoulder_width": "肩宽(cm",
"bust": "胸围全围(cm",
"clothing_length": "衣长(cm",
"sleeve_length": "袖长(cm",
"longest_side": "最长边(cm",
"secondary_long_side": "次长边(cm",
"shortest_side": "最短边(cm",
"package_weight": "重量(g", # 包装重量:从 db 提取
"price": "申报价格-日本站",
}
def _read_spu(db_path, spu_code: str) -> Optional[Dict[str, Any]]:
conn = _connect(db_path)
row = conn.execute("SELECT * FROM SPU WHERE code = ?", (spu_code,)).fetchone()
conn.close()
return dict(row) if row else None
def _read_meta(router) -> tuple:
"""读模板顶头元信息:经营站点、发货仓(按标签名定位,不依赖固定行列)。
返回 (origin_province, warehouses)
- origin_province:经营站点去掉末尾「站」(如「日本站」→「日本」)
- warehouses:发货仓按「、」分隔的列表(如「名古屋仓、inkreach——东京」→ 2 个)
"""
ws = router.ws
top = max(1, router.group_row - 1) # 元信息区位于分组行之前
def _val(label: str) -> str:
for row in range(1, top + 1):
for col in range(1, min(ws.max_column, 30) + 1):
if str(ws.cell(row, col).value or "").strip() == label:
return str(ws.cell(row + 1, col).value or "").strip()
return ""
site = _val("经营站点")
raw = _val("发货仓")
if not site and not raw:
# 回退:旧版固定位置(第2行第1/2列)
site = str(ws.cell(2, 1).value or "").strip()
raw = str(ws.cell(2, 2).value or "").strip()
origin_province = site[:-1] if site.endswith("站") else site
origin_province = _COUNTRY_NAME_MAP.get(origin_province, origin_province) # 简称→正式名称(如 沙特→沙特阿拉伯)
warehouses = [w.strip() for w in raw.split("、") if w.strip()]
return origin_province, warehouses
def _read_skus(db_path, spu_code: str, sku_code: str) -> List[Dict[str, Any]]:
"""该款该颜色的全部尺码 SKU,按尺码从小到大排序(字母码/数字码/童装码/规格码)。"""
conn = _connect(db_path)
rows = conn.execute(
"""SELECT s.*, p.code AS spu_code FROM SKU s
JOIN SPU p ON s.spu_id = p.id
WHERE p.code = ? AND s.code = ?""", (spu_code, sku_code)).fetchall()
conn.close()
skus = [dict(r) for r in rows]
skus.sort(key=lambda sk: _size_rank(sk.get("size")))
return skus
def _import_router(template_dir: str) -> None:
"""import template_router(优先 config 的 template_dir;打包后回退 _MEIPASS/model;再兜底项目自带 templates/)。"""
tdir = Path(template_dir)
candidates = [tdir]
meipass = getattr(sys, "_MEIPASS", None)
if meipass:
candidates.append(Path(meipass) / "model")
# 用户上传的模板可能在任意目录(无 template_router.py),兜底项目自带 templates/
from graph.paths import project_root as _proj_root
candidates.append(_proj_root() / "templates")
for d in candidates:
if d.exists() and str(d) not in sys.path:
sys.path.insert(0, str(d))
def _insert_product_block(
router, db_path, spu_code, sku_code,
origin_province, warehouses, price_headers,
markup_percent: float = 0.0, images: Optional[List[str]] = None,
spu_per_color: bool = True,
oss_code: str = "", cn_title: str = "", en_title: str = "", ja_title: str = "",
es_title: str = "",
composite_urls: Optional[List[Dict[str, Any]]] = None,
seed_shot_urls: Optional[List[str]] = None,
bust_headers: Optional[List[str]] = None,
) -> List[int]:
"""在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。
供单产品 export_product 与批量 export_products 复用(批量时只打开/保存一次)。
"""
spu = _read_spu(db_path, spu_code)
if spu is None:
raise ValueError(f"SPU {spu_code} 不存在于 db")
if isinstance(sku_code, str) and "," in sku_code:
sku_codes = [s.strip() for s in sku_code.split(",") if s.strip()]
elif isinstance(sku_code, (list, tuple)):
sku_codes = list(sku_code)
else:
sku_codes = [sku_code]
skus_by_color: List[tuple] = []
for sc in sku_codes:
skus = _read_skus(db_path, spu_code, sc)
if not skus:
raise ValueError(f"SKU {sc} 不存在于 db(款号 {spu_code}")
skus_by_color.append((sc, skus))
images = [str(i) for i in (images or []) if i]
color_col = router.resolve_col("色值(主规格)")
block_rows: List[int] = []
if spu_per_color:
# 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
# + 全部颜色尺码 SKU 行(色值在 SKU 行区分)
block_rows.append(router.insert(
_build_spu_row(spu, spu_code, origin_province),
match="exact",
))
for ci, (sc, skus) in enumerate(skus_by_color):
first = skus[0]
color = first.get("color") or sc
# 该颜色全部尺码 SKUSKU 行 SPU货号/SKU货号=spu_code,色值区分)
for i, sk in enumerate(skus):
size = sk.get("size") or f"{i+1}"
block_rows.append(router.insert(
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
markup_percent=markup_percent, multi=True,
bust_headers=bust_headers, price_headers=price_headers),
spu_code=spu_code, match="exact",
))
# 轮播图:首色 SKU 行轮播图1 = 生成首图;SKU 行按色值填 db url/生成图
if ci == 0 and images:
col1 = _carousel_col(router, 1)
if col1 is not None:
sku_rows = router.find_sku_rows(spu_code)
if sku_rows:
router.ws.cell(min(sku_rows), col1, str(images[0]))
sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else []
_fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs)
else:
# 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分)
block_rows.append(router.insert(
_build_spu_row(spu, spu_code, origin_province), match="exact"))
multi_variant = len(skus_by_color) > 1
for ci, (sc, skus) in enumerate(skus_by_color):
first = skus[0]
color = first.get("color") or sc
for i, sk in enumerate(skus):
size = sk.get("size") or f"{i+1}"
block_rows.append(router.insert(
_build_sku_row(spu_code, sc, sk, size, color, warehouses,
markup_percent=markup_percent, multi=multi_variant,
bust_headers=bust_headers, price_headers=price_headers),
))
sku_imgs = [x for x in (images[1:] + images[:1]) if x][:4] if (ci == 0 and images) else []
_fill_sku_carousel(router, spu_code, color, color_col, first, sku_imgs)
# 设计联动字段:货号/标题/轮播图路由/详情图文(图床链接,| 分割)
if oss_code or cn_title or en_title or ja_title or es_title or composite_urls:
by_sku: Dict[str, Any] = {}
all_urls: List[str] = []
for cc in (composite_urls or []):
if cc.get("color"):
by_sku[str(cc["color"]).strip()] = cc
if cc.get("url"):
all_urls.append(str(cc["url"]))
_fill_design_fields(router, spu_code, oss_code, cn_title, en_title, ja_title,
es_title, by_sku, all_urls, seed_shot_urls or [],
only_rows=block_rows)
return block_rows
def export_product(
db_path,
spu_code: str,
sku_code, # str | List[str]:单颜色或多个颜色
template_dir: str,
template_path: str,
out_path: str,
images: Optional[List[str]] = None,
spu_per_color: bool = True,
oss_code: str = "",
cn_title: str = "",
en_title: str = "",
ja_title: str = "",
es_title: str = "",
composite_urls: Optional[List[Dict[str, Any]]] = None,
seed_shot_urls: Optional[List[str]] = None,
append_to: str = "",
markup_percent: float = 0.0,
) -> Path:
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
sku_code SKU 颜色编码,支持单个 str 或多个(list/tuple/逗号分隔字符串)。
spu_per_color True(默认)= 每个颜色导出一个 SPU 块;False = 单 SPU 下挂所有颜色 SKU 变体。
template_dir template_router.py 所在目录(用于 import
template_path :商品上传模版 xlsx 路径
images :生成的产品图路径列表(仅作用于第一个颜色块:SPU 行轮播图1 + SKU 行回退)
oss_code :设计货号(前缀+3位计数),SPU货号/SKU货号 列均填它
cn_title :商品名称(中文标题)
en_title :英文名称(英文标题)
ja_title :日语名称(日语标题,JP 模板生成)
es_title :西语名称(西班牙语标题,ES 模板生成)
composite_urls[{"sku_code","color","url","code"}] 每色三合一主图(含图床链接与货号)
seed_shot_urls :种草图图床链接列表(详情图文 | 拼接用)
append_to :已有输出文件路径;提供则在其基础上追加本产品块(一次任务多产品合并一个模板)
markup_percent :加价百分比,申报价格 = SKU.price × (1+markup/100) 预填
返回输出文件路径。
"""
_import_router(template_dir)
from template_router import TemplateRouter # noqa: E402
# append_to:合并模式从已有输出文件继续追加(一次任务多产品填一个模板)
router = TemplateRouter(append_to if append_to else template_path)
try:
origin_province, warehouses = _read_meta(router)
price_headers = _find_price_headers(router) # 申报价格列(美站/日站/英站…模糊匹配,多个全填)
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
_insert_product_block(router, db_path, spu_code, sku_code,
origin_province, warehouses, price_headers,
markup_percent=markup_percent, images=images,
spu_per_color=spu_per_color,
oss_code=oss_code, cn_title=cn_title, en_title=en_title,
ja_title=ja_title, es_title=es_title,
composite_urls=composite_urls,
seed_shot_urls=seed_shot_urls,
bust_headers=bust_headers)
out = router.save(out_path)
return Path(out)
finally:
try:
router.close()
except Exception:
pass
def export_products(
db_path,
products: List[Dict[str, Any]],
template_dir: str,
template_path: str,
out_path: str,
markup_percent: float = 0.0,
) -> Path:
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
products 每项字段:spu_code / sku_codesstr 或 list/ oss_code / cn_title / en_title /
ja_title / es_title / composite_urls / seed_shot_urls / images / spu_per_color。
相比逐产品调用 export_product(每次全量读写工作簿),批量模式显著提速。
"""
_import_router(template_dir)
from template_router import TemplateRouter # noqa: E402
router = TemplateRouter(template_path)
try:
origin_province, warehouses = _read_meta(router)
price_headers = _find_price_headers(router) # 申报价格列(美站/日站/英站…模糊匹配,多个全填)
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
for r in products:
try:
_insert_product_block(
router, db_path, r.get("spu_code", ""),
r.get("sku_codes") or r.get("sku_code") or "",
origin_province, warehouses, price_headers,
markup_percent=markup_percent,
images=r.get("images"),
spu_per_color=bool(r.get("spu_per_color", True)),
oss_code=r.get("oss_code", ""),
cn_title=r.get("cn_title", ""),
en_title=r.get("en_title", ""),
ja_title=r.get("ja_title", ""),
es_title=r.get("es_title", ""),
composite_urls=r.get("composite_urls"),
seed_shot_urls=r.get("seed_shot_urls"),
bust_headers=bust_headers,
)
except Exception as e: # noqa: BLE001
print(f"[template_export] 产品 {r.get('spu_code')} 写入失败,跳过: {e}")
continue
out = router.save(out_path)
return Path(out)
finally:
try:
router.close()
except Exception:
pass