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:
+257
-83
@@ -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/3;material 字段不经过任何映射,原样透传。
|
||||
"""
|
||||
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" / "S,M,L")
|
||||
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": "面料克重1(g/m²)",
|
||||
"fabric_weight_unit_1": "面料克重1(g/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(
|
||||
# 该颜色全部尺码 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,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user