v88 功能增强:产品落盘持久化 + 生图网关适配 + 模板导出优化
- 产品持久化:每完成一个产品立即追加写入 products_pending.jsonl,崩溃不丢已完成产品,finish 读盘合并后统一写模板 - 503 致命错误提前终止:compose/product/seed_shot 端到端识别,提前终止搜索分析,丢弃未完成简报,保留已完成落盘产品直接合成模板 - 模特分配:material_library 合格模特图按任务序号独立随机,同 SPU 多款不再共用同一模特 - 图像网关适配:execution_mode/background 默认不再传入 yunfei 等标准网关,base_url 需带 /v1;429/5xx/空响应退避重试 - Pinterest 分析:删除 term 注入与纯文本降级,失败直接放弃;图片上传前 PIL 完整性校验;suitable_for_print=False 过滤丢弃 - 模板导出:不再产生空白 xlsx,文件名=模板原文件名_已填写;写入前按货号末 3 位升序排序 - 删除对接文档.md,更新 README,gitignore 排除测试产物
This commit is contained in:
+110
-9
@@ -25,6 +25,34 @@ _COUNTRY_NAME_MAP = {
|
||||
"沙特": "沙特阿拉伯",
|
||||
}
|
||||
|
||||
# 成分值字典映射:db 成分值 → 女装模板下拉框选项(男装模板选项与 db 值一致,直接保留)。
|
||||
# 女装模板(如 SatVoy 沙特)成分下拉框是「中文+英文」格式(棉Cotton),db 存中文(棉),需映射。
|
||||
_COMPONENT_FEMALE_MAP = {
|
||||
"棉": "棉Cotton",
|
||||
"聚酯纤维": "聚酯纤维(涤纶)Polyester",
|
||||
"氨纶": "氨纶Elastane",
|
||||
"锦纶": "锦纶(尼龙)Polyamide",
|
||||
"再生聚酯纤维": "聚酯纤维(涤纶)Polyester", # 女装无再生聚酯纤维,归入聚酯纤维
|
||||
"棉Cotton": "棉Cotton", # 已是女装格式,保持
|
||||
}
|
||||
|
||||
|
||||
def _map_component(value: Any, gender: Optional[str]) -> Any:
|
||||
"""成分值按性别映射:male/None 保留原值;female 查女装字典(找不到保留原值)。"""
|
||||
if gender != "female" or value in (None, ""):
|
||||
return value
|
||||
return _COMPONENT_FEMALE_MAP.get(value, value)
|
||||
|
||||
|
||||
def _template_gender(template_path: str) -> Optional[str]:
|
||||
"""通过表头解析模版「类目」判断男装/女装:类目含「男」→male;含「女」→female;都不含→None。
|
||||
用于成分值映射(女装模板下拉框是「中文+英文」格式,需字典映射;男装保留 db 值)。"""
|
||||
try:
|
||||
from graph.seed_shot import gender_from_category, read_template_category
|
||||
return gender_from_category(read_template_category(template_path))
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
|
||||
|
||||
def _size_rank(size) -> tuple:
|
||||
"""把尺码字符串转成可排序 rank(从小到大)。
|
||||
@@ -206,9 +234,11 @@ 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, origin_province: str,
|
||||
color: Optional[str] = None,
|
||||
fabric_headers: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
fabric_headers: Optional[List[str]] = None,
|
||||
gender: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""构造一行 SPU(固定字段:SKC货号=code、风格=休闲、商品产地=经营站点;多颜色时用色值列区分)。
|
||||
fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。"""
|
||||
fabric 填「面料弹性」列(fabric_headers,如 SPU商品属性-面料弹性,检测到才填 spu.fabric)。
|
||||
component_1/2/3 按性别映射(gender=female 时查 _COMPONENT_FEMALE_MAP,男装/None 保留原值)。"""
|
||||
row: Dict[str, Any] = {
|
||||
"基础信息-商品层级": "spu",
|
||||
"SKC货号": spu_code, # code 路由为 SKC货号(用户要求)
|
||||
@@ -220,6 +250,8 @@ def _build_spu_row(spu: Dict[str, Any], spu_code: str, origin_province: str,
|
||||
row["色值(主规格)"] = color
|
||||
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)
|
||||
if v not in (None, ""):
|
||||
row[header] = v
|
||||
fabric = spu.get("fabric")
|
||||
@@ -251,15 +283,39 @@ def _find_sa_size_headers(router) -> List[str]:
|
||||
return [str(k) for k in router.column_map if "沙特阿拉伯码" in str(k)]
|
||||
|
||||
|
||||
def _find_suggested_price_headers(router) -> List[str]:
|
||||
"""定位「建议售价」列(不含「单位」);匹配到多个时全部返回。"""
|
||||
return [str(k) for k in router.column_map if "建议售价" in str(k) and "单位" not in str(k)]
|
||||
|
||||
|
||||
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 _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,
|
||||
sa_size_headers: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
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) -> 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),检测到才填)。
|
||||
预先填好。建议售价(suggested_price_headers 列,模板「建议售价」必填时才填)= 申报价格 × (1+suggested_price_ratio/100),
|
||||
填了建议售价同时填「建议售价单位」=CNY。bust 填所有「胸围」列(bust_headers,如 基码表-胸围(cm)/胸围全围(cm),检测到才填)。
|
||||
size 填「尺码」列 + 所有「沙特阿拉伯码」列(sa_size_headers,如 尺码表-沙特阿拉伯码,检测到才填)。
|
||||
规格类型2 统一填「尺码」两个字(不是 size 参数值)。"""
|
||||
row: Dict[str, Any] = {
|
||||
@@ -295,6 +351,14 @@ 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
|
||||
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"
|
||||
continue
|
||||
if dbk == "size":
|
||||
if v in (None, ""):
|
||||
@@ -445,6 +509,11 @@ def _insert_product_block(
|
||||
bust_headers: Optional[List[str]] = None,
|
||||
fabric_headers: Optional[List[str]] = None,
|
||||
sa_size_headers: Optional[List[str]] = None,
|
||||
gender: Optional[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,
|
||||
) -> List[int]:
|
||||
"""在已打开的 router 中插入一个产品的 SPU+SKU 块并填充设计字段,返回本块行号。
|
||||
|
||||
@@ -474,7 +543,8 @@ def _insert_product_block(
|
||||
# 单 SPU 多色:1 个 SPU 行(无色值,SPU 级信息由 _fill_design_fields 填充)
|
||||
# + 全部颜色尺码 SKU 行(色值在 SKU 行区分)
|
||||
block_rows.append(router.insert(
|
||||
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers),
|
||||
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers,
|
||||
gender=gender),
|
||||
match="exact",
|
||||
))
|
||||
for ci, (sc, skus) in enumerate(skus_by_color):
|
||||
@@ -488,7 +558,11 @@ def _insert_product_block(
|
||||
_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),
|
||||
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),
|
||||
spu_code=spu_code, match="exact",
|
||||
))
|
||||
|
||||
@@ -504,7 +578,8 @@ def _insert_product_block(
|
||||
else:
|
||||
# 单 SPU + 多颜色变体:1 个 SPU 行(无色值)+ 所有颜色所有尺码 SKU 行(色值区分)
|
||||
block_rows.append(router.insert(
|
||||
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers), match="exact"))
|
||||
_build_spu_row(spu, spu_code, origin_province, fabric_headers=fabric_headers,
|
||||
gender=gender), match="exact"))
|
||||
multi_variant = len(skus_by_color) > 1
|
||||
for ci, (sc, skus) in enumerate(skus_by_color):
|
||||
first = skus[0]
|
||||
@@ -515,7 +590,11 @@ def _insert_product_block(
|
||||
_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),
|
||||
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_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)
|
||||
@@ -554,6 +633,7 @@ def export_product(
|
||||
seed_shot_urls: Optional[List[str]] = None,
|
||||
append_to: str = "",
|
||||
markup_percent: float = 0.0,
|
||||
suggested_price_ratio: float = 0.0,
|
||||
) -> Path:
|
||||
"""生成商品上传"已填写"模板(支持多产品合并到同一文件)。
|
||||
|
||||
@@ -584,6 +664,11 @@ def export_product(
|
||||
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
|
||||
fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填)
|
||||
sa_size_headers = _find_sa_size_headers(router) # 沙特阿拉伯码列(尺码表-沙特阿拉伯码…检测到才填)
|
||||
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} # 各建议售价列是否必填(结合非必填/必填判断)
|
||||
_insert_product_block(router, db_path, spu_code, sku_code,
|
||||
origin_province, warehouses, price_headers,
|
||||
markup_percent=markup_percent, images=images,
|
||||
@@ -594,7 +679,12 @@ def export_product(
|
||||
seed_shot_urls=seed_shot_urls,
|
||||
bust_headers=bust_headers,
|
||||
fabric_headers=fabric_headers,
|
||||
sa_size_headers=sa_size_headers)
|
||||
sa_size_headers=sa_size_headers,
|
||||
gender=gender,
|
||||
suggested_price_ratio=suggested_price_ratio,
|
||||
suggested_price_headers=suggested_price_headers,
|
||||
suggested_required=suggested_required,
|
||||
suggested_unit_headers=suggested_unit_headers)
|
||||
out = router.save(out_path)
|
||||
return Path(out)
|
||||
finally:
|
||||
@@ -611,6 +701,7 @@ def export_products(
|
||||
template_path: str,
|
||||
out_path: str,
|
||||
markup_percent: float = 0.0,
|
||||
suggested_price_ratio: float = 0.0,
|
||||
) -> Path:
|
||||
"""批量合并导出:所有产品一次性写入同一模板,只打开/保存一次。
|
||||
|
||||
@@ -628,6 +719,11 @@ def export_products(
|
||||
bust_headers = _find_bust_headers(router) # 胸围列(基码表-胸围(cm)/胸围全围(cm)…多个全填)
|
||||
fabric_headers = _find_fabric_headers(router) # 面料弹性列(SPU商品属性-面料弹性…检测到才填)
|
||||
sa_size_headers = _find_sa_size_headers(router) # 沙特阿拉伯码列(尺码表-沙特阿拉伯码…检测到才填)
|
||||
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} # 各建议售价列是否必填(结合非必填/必填判断)
|
||||
for r in products:
|
||||
try:
|
||||
_insert_product_block(
|
||||
@@ -647,6 +743,11 @@ def export_products(
|
||||
bust_headers=bust_headers,
|
||||
fabric_headers=fabric_headers,
|
||||
sa_size_headers=sa_size_headers,
|
||||
gender=gender,
|
||||
suggested_price_ratio=suggested_price_ratio,
|
||||
suggested_price_headers=suggested_price_headers,
|
||||
suggested_required=suggested_required,
|
||||
suggested_unit_headers=suggested_unit_headers,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[template_export] 产品 {r.get('spu_code')} 写入失败,跳过: {e}")
|
||||
|
||||
Reference in New Issue
Block a user