模板导出增强 + 模特性别分组 + 三合一提示词精简
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)
This commit is contained in:
+258
-125
@@ -20,6 +20,11 @@ from graph.product import _connect
|
||||
# 商品轮播图列名关键词(模板存在中/英/日变体,如 商品轮播图1 / Product Carousel Image 1 / 商品カルーセル画像1)
|
||||
_CAROUSEL_KW = ("轮播", "carousel", "カルーセル")
|
||||
|
||||
# 商品产地:国家简称 → 正式名称(模版要求,如「沙特站」提取为「沙特」但需填「沙特阿拉伯」)
|
||||
_COUNTRY_NAME_MAP = {
|
||||
"沙特": "沙特阿拉伯",
|
||||
}
|
||||
|
||||
|
||||
def _size_rank(size) -> tuple:
|
||||
"""把尺码字符串转成可排序 rank(从小到大)。
|
||||
@@ -99,15 +104,29 @@ def _ja_col(router) -> Optional[int]:
|
||||
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, composite_by_sku: Dict[str, Any],
|
||||
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货号=设计货号、商品名称=cn_title、英文名称=en_title、
|
||||
日语名称=ja_title、商品轮播图1=随机一张三合一主图、详情图文=全部主图+种草图链接 | 分割
|
||||
- SPU 行:SPU货号=设计货号、SKU货号=设计货号、商品名称=en_title、英文名称=en_title、
|
||||
日语名称=ja_title、西语名称=es_title、商品轮播图1=随机一张三合一主图、
|
||||
详情图文=全部主图+种草图 链接 | 分割(不含 img_url_2)
|
||||
- SKU 行:SPU货号=设计货号、SKU货号=该颜色货号、商品轮播图1=该颜色三合一链接、
|
||||
商品名称/英文名称/日语名称 与 SPU 一致
|
||||
商品名称/英文名称/日语名称/西语名称 与 SPU 一致
|
||||
only_rows:合并模式下只填充本产品块的行(None=该款全部行)
|
||||
"""
|
||||
import random
|
||||
@@ -131,6 +150,10 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_
|
||||
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:
|
||||
@@ -148,12 +171,14 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_
|
||||
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 cn_title:
|
||||
router.ws.cell(row, name_col, cn_title)
|
||||
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:
|
||||
@@ -163,13 +188,15 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_
|
||||
else:
|
||||
if spu_col and oss_code:
|
||||
router.ws.cell(row, spu_col, oss_code)
|
||||
# SKU 行与 SPU 一致:商品名称/英文名称/日语名称
|
||||
if name_col and cn_title:
|
||||
router.ws.cell(row, name_col, cn_title)
|
||||
# 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货号(同一货号)
|
||||
@@ -185,6 +212,7 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str,
|
||||
"SKC货号": spu_code, # code 路由为 SKC货号(用户要求)
|
||||
"风格": "休闲", # style 路由为"休闲"(用户要求)
|
||||
"商品产地": origin_province, # 产地省份不用填,经营站点填到「商品产地」
|
||||
"款式来源": "现货款", # SPU商品属性-款式来源 统一填「现货款」(用户要求)
|
||||
}
|
||||
if color:
|
||||
row["色值(主规格)"] = color
|
||||
@@ -195,19 +223,26 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str,
|
||||
return row
|
||||
|
||||
|
||||
def _find_price_header(router) -> str:
|
||||
"""定位价格列表头:任意含「申报价格」的列(美站/日站/英站…模糊匹配);找不到回退默认。"""
|
||||
for k in router.column_map:
|
||||
if "申报价格" in str(k):
|
||||
return str(k)
|
||||
return "申报价格-日本站"
|
||||
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 = "申报价格-日本站") -> Dict[str, Any]:
|
||||
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_header 列,如 申报价格-美国站/日本站,模糊匹配)= SKU.price × (1+markup/100),预先填好。
|
||||
价格(price_headers 列,如 申报价格-美国站/日本站,模糊匹配到多个时全部填)= SKU.price × (1+markup/100),
|
||||
预先填好。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。
|
||||
规格类型2 统一填「尺码」两个字(不是 size 参数值)。"""
|
||||
row: Dict[str, Any] = {
|
||||
"基础信息-商品层级": "sku",
|
||||
@@ -217,17 +252,30 @@ def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, 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
|
||||
if dbk == "price":
|
||||
header = price_header # 模糊匹配的实际价格列(申报价格-美站/日站/英站…)
|
||||
v = sk.get(dbk)
|
||||
if dbk == "price" and v not in (None, ""):
|
||||
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
|
||||
@@ -325,6 +373,7 @@ def _read_meta(router) -> tuple:
|
||||
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
|
||||
|
||||
@@ -342,42 +391,36 @@ def _read_skus(db_path, spu_code: str, sku_code: str) -> List[Dict[str, Any]]:
|
||||
return skus
|
||||
|
||||
|
||||
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,
|
||||
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 = "",
|
||||
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:
|
||||
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
|
||||
bust_headers: Optional[List[str]] = None,
|
||||
) -> List[int]:
|
||||
"""在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。
|
||||
|
||||
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 模板生成)
|
||||
composite_urls:[{"sku_code","color","url","code"}] 每色三合一主图(含图床链接与货号)
|
||||
seed_shot_urls :种草图图床链接列表(详情图文 | 拼接用)
|
||||
append_to :已有输出文件路径;提供则在其基础上追加本产品块(一次任务多产品合并一个模板)
|
||||
markup_percent :加价百分比,申报价格 = SKU.price × (1+markup/100) 预填
|
||||
返回输出文件路径。
|
||||
供单产品 export_product 与批量 export_products 复用(批量时只打开/保存一次)。
|
||||
"""
|
||||
# 1) 读 db(支持单/多颜色)
|
||||
spu = _read_spu(db_path, spu_code)
|
||||
if spu is None:
|
||||
raise ValueError(f"SPU {spu_code} 不存在于 db")
|
||||
@@ -395,90 +438,180 @@ def export_product(
|
||||
skus_by_color.append((sc, skus))
|
||||
images = [str(i) for i in (images or []) if i]
|
||||
|
||||
# 2) import template_router(优先 config 的 template_dir;打包后回退 _MEIPASS/model)
|
||||
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))
|
||||
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
|
||||
|
||||
# 该颜色全部尺码 SKU(SKU 行 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_header = _find_price_header(router) # 申报价格列(美站/日站/英站…模糊匹配)
|
||||
multi = len(skus_by_color) > 1
|
||||
color_col = router.resolve_col("色值(主规格)")
|
||||
block_rows: List[int] = [] # 本产品块插入的所有行号(_fill_design_fields 只填这些行)
|
||||
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
|
||||
|
||||
if spu_per_color:
|
||||
# 3) 单 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
|
||||
|
||||
# 该颜色全部尺码 SKU(SKU 行 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,
|
||||
price_header=price_header),
|
||||
spu_code=spu_code, match="exact",
|
||||
))
|
||||
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:
|
||||
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
|
||||
|
||||
# 3.3) 轮播图:首色 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:
|
||||
# 4) 单 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,
|
||||
price_header=price_header),
|
||||
))
|
||||
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)
|
||||
# 无 SPU 行:首图(轮播图1)由 _fill_design_fields 按 SKU 行填充
|
||||
products 每项字段:spu_code / sku_codes(str 或 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
|
||||
|
||||
# 5) 设计联动字段:货号/标题/轮播图路由/详情图文(图床链接,| 分割)
|
||||
if oss_code or cn_title or en_title or ja_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,
|
||||
by_sku, all_urls, seed_shot_urls or [], only_rows=block_rows)
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user