v92-v105 多国适配 + 模板导出增强 + 生图可靠性优化

- 新增韩国(KR)适配:countries/pinterest 种子词、K-pop 提示词、UI 国家列表
- 爬虫跨运行持久化已采集 URL(.collected_urls.json),同关键词重复搜索采新图
- 模板导出增强:SPU 商品属性列名前缀剥离匹配、尺码下拉框 INDIRECT 动态引用、
  SPU 字段映射(袖长/门襟/胸垫等)、季节/印花图案女装映射、建议售价统一必填、
  SKU 分类/数量/单位直接填默认值
- 自定义模式:多模态提示词独立(custom_analyze_*)、生图提示词模板可配置
- Pinterest:空选品守卫、连续空分析保护(max_empty_analyze)、flat_prompt 配置覆盖
- 生图可靠性:开始/完成进度日志、异步任务轮询超时 60s→300s
This commit is contained in:
2026-08-31 18:35:51 +08:00
parent 71a48e4ed5
commit 3dc594cf57
29 changed files with 1369 additions and 321 deletions
+257 -83
View File
@@ -22,6 +22,7 @@ _CAROUSEL_KW = ("轮播", "carousel", "カルーセル")
# 成分值字典映射:db 成分值 → 女装模板下拉框选项(男装模板选项与 db 值一致,直接保留)。
# 女装模板(如 SatVoy 沙特)成分下拉框是「中文+英文」格式(棉Cotton),db 存中文(棉),需映射。
# 该字典为「内置默认」,运行时可被 config.yaml 顶层 component_map.female 覆盖(缺失/留空回退这里)。
_COMPONENT_FEMALE_MAP = {
"": "棉Cotton",
"聚酯纤维": "聚酯纤维(涤纶)Polyester",
@@ -32,11 +33,108 @@ _COMPONENT_FEMALE_MAP = {
}
def _map_component(value: Any, gender: Optional[str]) -> Any:
"""成分值按性别映射:male/None 保留原值;female 查女装字典(找不到保留原值)。"""
def _resolve_component_map(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""从 config 读取可配置成分映射(config.component_map.female),缺失/留空回退内置默认。
仅作用于 component_1/2/3material 字段不经过任何映射,原样透传。
"""
if not config:
return dict(_COMPONENT_FEMALE_MAP)
female = None
try:
cm = config.get("component_map") or {}
female = (cm.get("female") or {}) if isinstance(cm, dict) else {}
except Exception: # noqa: BLE001
female = {}
if not isinstance(female, dict) or not female:
return dict(_COMPONENT_FEMALE_MAP)
merged = dict(_COMPONENT_FEMALE_MAP)
merged.update({str(k): v for k, v in female.items() if v not in (None, "")})
return merged
def _map_component(value: Any, gender: Optional[str], cmp_map: Optional[Dict[str, Any]] = None) -> Any:
"""成分值按性别映射(仅 component 字段,不作用于 material):male/None 保留原值;
female 查成分映射表(默认内置、可由 config 覆盖,找不到保留原值)。"""
if gender != "female" or value in (None, ""):
return value
return _COMPONENT_FEMALE_MAP.get(value, value)
mapping = cmp_map if cmp_map is not None else _COMPONENT_FEMALE_MAP
return mapping.get(value, value)
# 季节值字典映射:db 季节值 → 女装模板下拉框选项(男装模板与 db 值一致,直接保留)。
# 女装模板(如 SatVoy 沙特)季节下拉框用「ALL/全球/所有」等英文+中文格式,db 存「四季」,需映射。
# 该字典为「内置默认」,运行时可被 config.yaml 顶层 season_map.female 覆盖(缺失/留空回退这里)。
_SEASON_FEMALE_MAP = {
"四季": "ALL/全球/所有",
}
# 印花图案值字典映射:db pattern 值 → 女装模板下拉框选项(男装模板与 db 值一致,直接保留)。
# 女装模板遇到 pattern="印花" 时映射为「卡通」;该字典为「内置默认」,
# 运行时可被 config.yaml 顶层 pattern_map.female 覆盖(缺失/留空回退这里)。
_PATTERN_FEMALE_MAP = {
"印花": "卡通",
}
def _resolve_pattern_map(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""从 config 读取可配置图案映射(config.pattern_map.female),缺失/留空回退内置默认。
仅作用于 spu.pattern 字段(女装模板时映射,男装/None 保留原值)。
"""
if not config:
return dict(_PATTERN_FEMALE_MAP)
female = None
try:
pm = config.get("pattern_map") or {}
female = (pm.get("female") or {}) if isinstance(pm, dict) else {}
except Exception: # noqa: BLE001
female = {}
if not isinstance(female, dict) or not female:
return dict(_PATTERN_FEMALE_MAP)
merged = dict(_PATTERN_FEMALE_MAP)
merged.update({str(k): v for k, v in female.items() if v not in (None, "")})
return merged
def _map_pattern(value: Any, gender: Optional[str], pattern_map: Optional[Dict[str, Any]] = None) -> Any:
"""图案值按性别映射(仅 spu.pattern 字段):male/None 保留原值;
female 查图案映射表(默认内置、可由 config 覆盖,找不到保留原值),如「印花」→「卡通」。"""
if gender != "female" or value is None:
return value
v = str(value).strip()
mapping = pattern_map if pattern_map is not None else _PATTERN_FEMALE_MAP
return mapping.get(v, value)
def _resolve_season_map(config: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""从 config 读取可配置季节映射(config.season_map.female),缺失/留空回退内置默认。
仅作用于 spu.season 字段(女装模板时映射,男装/None 保留原值)。
"""
if not config:
return dict(_SEASON_FEMALE_MAP)
female = None
try:
sm = config.get("season_map") or {}
female = (sm.get("female") or {}) if isinstance(sm, dict) else {}
except Exception: # noqa: BLE001
female = {}
if not isinstance(female, dict) or not female:
return dict(_SEASON_FEMALE_MAP)
merged = dict(_SEASON_FEMALE_MAP)
merged.update({str(k): v for k, v in female.items() if v not in (None, "")})
return merged
def _map_season(value: Any, gender: Optional[str], season_map: Optional[Dict[str, Any]] = None) -> Any:
"""季节值按性别映射(仅 spu.season 字段):male/None 保留原值;
female 查季节映射表(默认内置、可由 config 覆盖,找不到保留原值)。"""
if gender != "female" or value in (None, ""):
return value
mapping = season_map if season_map is not None else _SEASON_FEMALE_MAP
return mapping.get(str(value), value)
def _template_gender(template_path: str) -> Optional[str]:
@@ -230,13 +328,20 @@ def _fill_design_fields(router, spu_code: str, oss_code: str, cn_title: str, en_
def _build_spu_row(spu: Dict[str, Any], spu_code: str,
color: Optional[str] = None,
fabric_headers: Optional[List[str]] = None,
gender: Optional[str] = None) -> Dict[str, Any]:
"""构造一行 SPU(固定字段:SKC货号=code、风格=休闲、商品产地=中国大陆、产地省份=广东省;多颜色时用色值列区分)。
gender: Optional[str] = None,
component_map: Optional[Dict[str, Any]] = None,
season_map: Optional[Dict[str, Any]] = None,
pattern_map: Optional[Dict[str, Any]] = None,
oss_code: str = "") -> Dict[str, Any]:
"""构造一行 SPU(固定字段:SKC货号=当前生成货号、风格=休闲、商品产地=中国大陆、产地省份=广东省;多颜色时用色值区分)。
fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。
component_1/2/3 按性别映射(gender=female 时查 _COMPONENT_FEMALE_MAP,男装/None 保留原值)。"""
component_1/2/3 按性别映射(gender=female 时查成分映射表 component_map,男装/None 保留原值
material 字段不经过任何映射,原样透传)。
season 按性别映射(gender=female 时查季节映射表 season_map,如「四季」→「ALL/全球/所有」)。
pattern 按性别映射(gender=female 时空值查图案映射表 pattern_map,如空值→「卡通」)。"""
row: Dict[str, Any] = {
"基础信息-商品层级": "spu",
"SKC货号": spu_code, # code 路由为 SKC货号(用户要求
"SKC货号": oss_code or spu_code, # SKC货号 = 当前生成货号 oss_code(无则回退 spu_code
"风格": "休闲", # style 路由为"休闲"(用户要求)
"商品产地": "中国大陆", # 所有国家统一「中国大陆」,不读经营站点/不做字典匹配(用户要求)
"产地省份": "广东省", # 新增:精确匹配「产地省份」列,统一填「广东省」(用户要求)
@@ -247,7 +352,11 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str,
for dbk, header in SPU_MAP.items():
v = spu.get(dbk)
if dbk in ("component_1", "component_2", "component_3"):
v = _map_component(v, gender)
v = _map_component(v, gender, component_map)
elif dbk == "season":
v = _map_season(v, gender, season_map)
elif dbk == "pattern":
v = _map_pattern(v, gender, pattern_map)
if v not in (None, ""):
row[header] = v
fabric = spu.get("fabric")
@@ -289,13 +398,62 @@ def _find_suggested_unit_headers(router) -> List[str]:
return [str(k) for k in router.column_map if "建议售价单位" in str(k)]
def _read_suggested_required(router, col: int) -> bool:
"""读取「建议售价」列下方注意事项,判断是否必填。
结合「非必填/必填」判断(不能只看「必填」两字):含「非必填」→非必填;否则含「必填」→必填。"""
note = str(router.ws.cell(router.header_row + 1, col).value or "")
if "非必填" in note:
return False
return "必填" in note
def _read_range_values(ws, ref: str) -> Optional[set]:
"""读取单元格/区域内的非空值集合;解析失败返回 None。"""
try:
cells = ws[ref]
except Exception: # noqa: BLE001
return None
opts = []
if isinstance(cells, (list, tuple)):
for row in cells:
for cell in row:
if cell.value not in (None, ""):
opts.append(str(cell.value).strip())
else:
if cells.value not in (None, ""):
opts.append(str(cells.value).strip())
return set(opts) if opts else None
def _read_size_options(router) -> Optional[set]:
"""读取「尺码」列的 dataValidation 下拉选项集合;无下拉/非 list/无值 返回 None。
仅作「尺码下拉受限时过滤 SKU 行」用:sku.size 不在下拉选项内则该 SKU 行跳过。
支持下拉直接内联("S,M,L")、引用区域("=Sheet!$A$1:$A$5")、动态引用(INDIRECT(...))。
动态引用无法解析(INDIRECT 指向空单元格/公式/跨表)→ 返回 None(不启用过滤,避免误杀全部 SKU 行)。"""
import re
from openpyxl.utils import get_column_letter
col = router.column_map.get("尺码") or router.column_map.get("商品规格-尺码")
if col is None:
return None
lf = get_column_letter(col)
dvs = getattr(router.ws, "data_validations", None)
if dvs is None:
return None
for dv in dvs.dataValidation:
if getattr(dv, "type", "") != "list":
continue
if lf not in str(getattr(dv, "sqref", "")):
continue
f = (getattr(dv, "formula1", "") or "").strip()
if not f:
continue
# 动态引用 INDIRECT(...):尝试解析引用的单元格/区域内容;无法解析 → 不过滤
m = re.match(r"^=?INDIRECT\((.+)\)$", f, re.IGNORECASE)
if m:
opts = _read_range_values(router.ws, m.group(1).strip())
return opts if opts else None
# 引用区域(=Sheet!$A$1:$A$5 或 Sheet!$A$1:$A$5
ref = f[1:] if f.startswith("=") else f
if ":" in ref:
opts = _read_range_values(router.ws, ref)
return opts if opts else None
# 内联列表("S,M,L" / "S;M;L" / "SML"
sep = "" if "" in f else (";" if ";" in f else ",")
opts = [x.strip() for x in f.split(sep) if x.strip()]
return set(opts) if opts else None
return None
def _find_sku_category_headers(router) -> List[str]:
@@ -313,22 +471,6 @@ def _find_sku_qty_unit_headers(router) -> List[str]:
return [str(k) for k in router.column_map if "SKU数量单位" in str(k)]
def _required_headers(router, headers) -> List[str]:
"""过滤「非必填」列:读列头下一行备注(与建议售价同套路),备注显式写「非必填」才跳过。
SKU分类/SKU数量/SKU数量单位在本模板族属必填项(SKU分类备注「必填」,数量/单位为按规则填写),
因此默认视为需填写,仅在列备注写明「非必填」时跳过。"""
keep: List[str] = []
for h in headers:
col = router.column_map.get(h)
if col is not None:
note = str(router.ws.cell(router.header_row + 1, col).value or "")
if "非必填" in note:
continue
keep.append(h)
return keep
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 = "申报价格-日本站",
@@ -337,12 +479,13 @@ def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color:
sa_size_headers: Optional[List[str]] = None,
suggested_price_ratio: float = 0.0,
suggested_price_headers: Optional[List[str]] = None,
suggested_required: Optional[Dict[str, bool]] = None,
suggested_unit_headers: Optional[List[str]] = None,
sku_category_headers: Optional[List[str]] = None,
sku_qty_headers: Optional[List[str]] = None,
sku_qty_unit_headers: Optional[List[str]] = None) -> Dict[str, Any]:
"""构造一行 SKU(固定字段:SPU货号、SKC货号=sku.code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。
sku_qty_unit_headers: Optional[List[str]] = None,
oss_code: str = "",
size_options: Optional[set] = None) -> Dict[str, Any]:
"""构造一行 SKU(固定字段:SPU货号、SKC货号=当前生成货号 oss_code、规格类型2、币种 CNY、发货仓1~N 及库存 200)。
价格(price_headers 列,如 申报价格-美国站/日本站,模糊匹配到多个时全部填)= SKU.price × (1+markup/100)
预先填好。建议售价(suggested_price_headers 列,模板「建议售价」必填时才填)= 申报价格 × (1+suggested_price_ratio/100)
填了建议售价同时填「建议售价单位」=CNY。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。
@@ -351,7 +494,7 @@ def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color:
row: Dict[str, Any] = {
"基础信息-商品层级": "sku",
"SPU货号": spu_code,
"SKC货号": sk.get("code") or sc, # SKC货号 = SKU 的 code(款号-颜色编码
"SKC货号": oss_code or spu_code, # SKC货号 = 当前生成货号 oss_code(无则回退 spu_code
"色值(主规格)": color,
"规格类型2": "尺码", # 规格类型2 统一填「尺码」(不填 size 值)
"币种": "CNY",
@@ -381,18 +524,19 @@ def _build_sku_row(spu_code: str, sc: str, sk: Dict[str, Any], size: str, color:
v = round(float(v) * (1 + markup_percent / 100), 2) # 申报价格 = price × (1+加价%)
for h in price_headers:
row[h] = v
# 建议售价 = 申报价格 × (1+建议售价比例%);模板「建议售价」必填时才填,填了同时填单位 CNY
# 建议售价 = 申报价格 × (1+建议售价比例%);统一必填,填了同时填单位 CNY
if suggested_price_headers and suggested_price_ratio > 0:
suggested = round(v * (1 + suggested_price_ratio / 100), 2)
for h in suggested_price_headers:
if (suggested_required or {}).get(h, True):
row[h] = suggested
for uh in suggested_unit_headers:
row[uh] = "CNY"
row[h] = suggested
for uh in suggested_unit_headers:
row[uh] = "CNY"
continue
if dbk == "size":
if v in (None, ""):
continue
if size_options is not None and v not in size_options:
return None # 尺码不在模板下拉框选项内,跳过该 SKU 行
row[header] = v
for h in sa_size_headers:
row[h] = v
@@ -443,13 +587,23 @@ SPU_MAP: Dict[str, str] = {
"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": "里料纹理",
"placket_type": "SPU商品属性-门襟类型",
"sleeve_type": "SPU商品属性-袖型",
"sleeve_length_type": "SPU商品属性-袖长",
"breast_pad": "SPU商品属性-胸垫",
"layout": "SPU商品属性-版型",
"silhouette": "SPU商品属性-廓形",
"length_type": "SPU商品属性-长度",
"belt": "SPU商品属性-腰带",
"occasion": "SPU商品属性-场合",
"scene": "SPU商品属性-场景",
"hemline_shape": "SPU商品属性-下摆形状",
}
# db SKU 字段 -> 上传模板列名
@@ -541,9 +695,11 @@ def _insert_product_block(
fabric_headers: Optional[List[str]] = None,
sa_size_headers: Optional[List[str]] = None,
gender: Optional[str] = None,
component_map: Optional[Dict[str, Any]] = None,
season_map: Optional[Dict[str, Any]] = None,
pattern_map: Optional[Dict[str, Any]] = None,
suggested_price_ratio: float = 0.0,
suggested_price_headers: Optional[List[str]] = None,
suggested_required: Optional[Dict[str, bool]] = None,
suggested_unit_headers: Optional[List[str]] = None,
sku_category_headers: Optional[List[str]] = None,
sku_qty_headers: Optional[List[str]] = None,
@@ -572,13 +728,16 @@ def _insert_product_block(
color_col = router.resolve_col("色值(主规格)")
block_rows: List[int] = []
size_options = _read_size_options(router) # 尺码下拉框选项(sku.size 不在其中则跳过该 SKU 行)
if spu_per_color:
# 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
# + 全部颜色尺码 SKU 行(色值在 SKU 行区分)
block_rows.append(router.insert(
_build_spu_row(spu, spu_code, fabric_headers=fabric_headers,
gender=gender),
gender=gender, component_map=component_map,
season_map=season_map, pattern_map=pattern_map,
oss_code=oss_code),
match="exact",
))
for ci, (sc, skus) in enumerate(skus_by_color):
@@ -588,20 +747,21 @@ def _insert_product_block(
# 该颜色全部尺码 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,
sa_size_headers=sa_size_headers,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_required=suggested_required,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,
sku_qty_unit_headers=sku_qty_unit_headers),
spu_code=spu_code, match="exact",
))
row = _build_sku_row(spu_code, sc, sk, size, color, warehouses,
markup_percent=markup_percent, multi=True,
bust_headers=bust_headers, price_headers=price_headers,
sa_size_headers=sa_size_headers,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,
sku_qty_unit_headers=sku_qty_unit_headers,
oss_code=oss_code, size_options=size_options)
if row is None:
continue
block_rows.append(router.insert(row, spu_code=spu_code, match="exact"))
# 轮播图:首色 SKU 行轮播图1 = 生成首图;SKU 行按色值填 db url/生成图
if ci == 0 and images:
@@ -616,26 +776,30 @@ def _insert_product_block(
# 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分)
block_rows.append(router.insert(
_build_spu_row(spu, spu_code, fabric_headers=fabric_headers,
gender=gender), match="exact"))
gender=gender, component_map=component_map,
season_map=season_map, pattern_map=pattern_map,
oss_code=oss_code), 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,
sa_size_headers=sa_size_headers,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_required=suggested_required,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,
sku_qty_unit_headers=sku_qty_unit_headers),
))
row = _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,
sa_size_headers=sa_size_headers,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,
sku_qty_unit_headers=sku_qty_unit_headers,
oss_code=oss_code, size_options=size_options)
if row is None:
continue
block_rows.append(router.insert(row))
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)
@@ -674,6 +838,9 @@ def export_product(
append_to: str = "",
markup_percent: float = 0.0,
suggested_price_ratio: float = 0.0,
component_map: Optional[Dict[str, Any]] = None,
season_map: Optional[Dict[str, Any]] = None,
pattern_map: Optional[Dict[str, Any]] = None,
) -> Path:
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
@@ -707,11 +874,10 @@ def export_product(
gender = _template_gender(template_path) # 类目含「男」→male /「女」→female(成分值映射用)
suggested_price_headers = _find_suggested_price_headers(router) # 建议售价列(不含单位)
suggested_unit_headers = _find_suggested_unit_headers(router) # 建议售价单位列
suggested_required = {h: _read_suggested_required(router, router.column_map[h])
for h in suggested_price_headers} # 各建议售价列是否必填(结合非必填/必填判断
sku_category_headers = _required_headers(router, _find_sku_category_headers(router)) # SKU分类列(非必填才跳过
sku_qty_headers = _required_headers(router, _find_sku_qty_headers(router)) # SKU数量列
sku_qty_unit_headers = _required_headers(router, _find_sku_qty_unit_headers(router)) # SKU数量单位列
sku_category_headers = _find_sku_category_headers(router) # SKU分类列(存在即填默认值「单品」
sku_qty_headers = _find_sku_qty_headers(router) # SKU数量列(存在即填 1
sku_qty_unit_headers = _find_sku_qty_unit_headers(router) # SKU数量单位列(存在即填「件」)
_insert_product_block(router, db_path, spu_code, sku_code,
warehouses, price_headers,
markup_percent=markup_percent, images=images,
@@ -724,9 +890,12 @@ def export_product(
fabric_headers=fabric_headers,
sa_size_headers=sa_size_headers,
gender=gender,
component_map=component_map,
season_map=season_map,
pattern_map=pattern_map,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_required=suggested_required,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,
@@ -748,6 +917,9 @@ def export_products(
out_path: str,
markup_percent: float = 0.0,
suggested_price_ratio: float = 0.0,
component_map: Optional[Dict[str, Any]] = None,
season_map: Optional[Dict[str, Any]] = None,
pattern_map: Optional[Dict[str, Any]] = None,
) -> Path:
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
@@ -768,11 +940,10 @@ def export_products(
gender = _template_gender(template_path) # 类目含「男」→male /「女」→female(成分值映射用)
suggested_price_headers = _find_suggested_price_headers(router) # 建议售价列(不含单位)
suggested_unit_headers = _find_suggested_unit_headers(router) # 建议售价单位列
suggested_required = {h: _read_suggested_required(router, router.column_map[h])
for h in suggested_price_headers} # 各建议售价列是否必填(结合非必填/必填判断
sku_category_headers = _required_headers(router, _find_sku_category_headers(router)) # SKU分类列(非必填才跳过
sku_qty_headers = _required_headers(router, _find_sku_qty_headers(router)) # SKU数量列
sku_qty_unit_headers = _required_headers(router, _find_sku_qty_unit_headers(router)) # SKU数量单位列
sku_category_headers = _find_sku_category_headers(router) # SKU分类列(存在即填默认值「单品」
sku_qty_headers = _find_sku_qty_headers(router) # SKU数量列(存在即填 1
sku_qty_unit_headers = _find_sku_qty_unit_headers(router) # SKU数量单位列(存在即填「件」)
for r in products:
try:
_insert_product_block(
@@ -793,9 +964,12 @@ def export_products(
fabric_headers=fabric_headers,
sa_size_headers=sa_size_headers,
gender=gender,
component_map=component_map,
season_map=season_map,
pattern_map=pattern_map,
suggested_price_ratio=suggested_price_ratio,
suggested_price_headers=suggested_price_headers,
suggested_required=suggested_required,
suggested_unit_headers=suggested_unit_headers,
sku_category_headers=sku_category_headers,
sku_qty_headers=sku_qty_headers,