模板导出增强 + 模特性别分组 + 三合一提示词精简

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:
2026-08-26 18:05:11 +08:00
parent e317547b8b
commit b7f429db89
93 changed files with 2708 additions and 583 deletions
+258 -125
View File
@@ -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
# 该颜色全部尺码 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_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
# 该颜色全部尺码 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,
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_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
# 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: