修复种草图生成:以三合一主图为参考 img2img 生成,按颜色分配(每色优先、超出随机补足);新增 BR/CA/DE/ES/IT/PL/SA 七国配置与提示词;删除验证用测试脚本
This commit is contained in:
+113
-39
@@ -93,7 +93,13 @@ class TemplateRouter:
|
||||
raise ValueError(f"工作簿中不存在工作表 {sheet!r},现有: {self.wb.sheetnames}")
|
||||
self.ws = self.wb[sheet]
|
||||
|
||||
# 完整表头 -> 列号 (如 "基础信息-商品层级" -> 1)
|
||||
# 布局行号(自动探测,失败回退模块级常量,兼容旧版模版)
|
||||
self.group_row = GROUP_ROW
|
||||
self.header_row = HEADER_ROW
|
||||
self.data_start = DATA_START
|
||||
self._detect_layout()
|
||||
|
||||
# 完整表头 -> 列号 (如 "基础信息-商品层级" -> 1,含括号全名别名)
|
||||
self.header_map: dict[str, int] = {}
|
||||
# 列名(去分组) -> 列号 (如 "商品层级" -> 1)
|
||||
self.column_map: dict[str, int] = {}
|
||||
@@ -101,21 +107,82 @@ class TemplateRouter:
|
||||
self.col_headers: dict[int, str] = {}
|
||||
self._build_headers()
|
||||
|
||||
# 关键列按列名解析(不依赖固定列号,列名缺失时回退模块级常量)
|
||||
self.level_col = self.column_map.get("商品层级", LEVEL_COL)
|
||||
self.spu_code_col = self.column_map.get("SPU货号", SPU_CODE_COL)
|
||||
|
||||
self._used_rows: set[int] | None = None # 已占用行缓存,见 _refresh()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 布局自动探测
|
||||
# ------------------------------------------------------------------
|
||||
def _detect_layout(self) -> None:
|
||||
"""按固定分组名定位布局(不依赖固定行列)。
|
||||
|
||||
分组名(如「基础信息(SKU/SPU均必填)」「SPU商品属性」)是模版固定不变的,
|
||||
以其为锚点:
|
||||
1. 分组行 = 含「基础信息」的行(去括号说明后精确匹配,兼容含括号全名);
|
||||
2. 列名行 = 分组行正下方的列名行(校验含「商品层级」+「SPU货号」);
|
||||
3. 数据起始行 = 列名行之后第一个整行空行(跳过说明行)。
|
||||
探测失败时保持模块级常量默认值。
|
||||
"""
|
||||
max_col = min(self.ws.max_column, 30)
|
||||
|
||||
def row_texts(row: int) -> list[str]:
|
||||
return [str(self.ws.cell(row, c).value or "").strip()
|
||||
for c in range(1, max_col + 1)]
|
||||
|
||||
# 1) 分组行:按固定分组名「基础信息」匹配
|
||||
group_row = None
|
||||
for row in range(1, min(self.ws.max_row, 30) + 1):
|
||||
for v in row_texts(row):
|
||||
if _GROUP_BRACKET.sub("", v).strip() == "基础信息" or "基础信息" in v:
|
||||
group_row = row
|
||||
break
|
||||
if group_row:
|
||||
break
|
||||
|
||||
# 2) 列名行:固定位于分组行正下方(校验含「商品层级」+「SPU货号」;
|
||||
# 列名可能变化,校验失败时直接用分组行正下方一行)
|
||||
header_row = None
|
||||
if group_row:
|
||||
for row in range(group_row + 1, min(self.ws.max_row, group_row + 4) + 1):
|
||||
vals = row_texts(row)
|
||||
if any("商品层级" in v for v in vals) and any("SPU货号" in v for v in vals):
|
||||
header_row = row
|
||||
break
|
||||
if header_row is None:
|
||||
header_row = group_row + 1
|
||||
|
||||
# 3) 数据起始行:列名行正下方第2行(列名行→说明行→数据起始行)。
|
||||
# 不用「第一个空行」——已填写的模版首个空行在数据之后,会漏扫已有数据。
|
||||
if header_row:
|
||||
data_start = header_row + 2
|
||||
|
||||
if group_row:
|
||||
self.group_row = group_row
|
||||
if header_row:
|
||||
self.header_row = header_row
|
||||
if data_start:
|
||||
self.data_start = data_start
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 表头解析
|
||||
# ------------------------------------------------------------------
|
||||
def _build_headers(self) -> None:
|
||||
for col in range(1, self.ws.max_column + 1):
|
||||
raw_group = self.ws.cell(GROUP_ROW, col).value
|
||||
raw_name = self.ws.cell(HEADER_ROW, col).value
|
||||
group = _GROUP_BRACKET.sub("", str(raw_group)).strip() if raw_group else ""
|
||||
raw_group = self.ws.cell(self.group_row, col).value
|
||||
raw_name = self.ws.cell(self.header_row, col).value
|
||||
group_full = str(raw_group).strip() if raw_group else ""
|
||||
group = _GROUP_BRACKET.sub("", group_full).strip()
|
||||
name = str(raw_name).strip() if raw_name else ""
|
||||
if not name:
|
||||
continue
|
||||
header = f"{group}-{name}" if group else name
|
||||
self.header_map[header] = col
|
||||
if group_full and group_full != group:
|
||||
# 完整分组名(含括号说明)别名,如「基础信息(SKU/SPU均必填)-商品层级」
|
||||
self.header_map[f"{group_full}-{name}"] = col
|
||||
self.column_map[name] = col
|
||||
self.col_headers[col] = header
|
||||
|
||||
@@ -134,19 +201,19 @@ class TemplateRouter:
|
||||
return m
|
||||
|
||||
def _refresh_used_rows(self) -> None:
|
||||
"""扫描数据区,收集 A 列(商品层级)非空的行号集合。"""
|
||||
"""扫描数据区,收集商品层级列(按列名解析)非空的行号集合。"""
|
||||
used: set[int] = set()
|
||||
for row in range(DATA_START, self.ws.max_row + 1):
|
||||
v = self.ws.cell(row, LEVEL_COL).value
|
||||
for row in range(self.data_start, self.ws.max_row + 1):
|
||||
v = self.ws.cell(row, self.level_col).value
|
||||
if v is not None and str(v).strip():
|
||||
used.add(row)
|
||||
self._used_rows = used
|
||||
|
||||
def first_empty_row(self, start: int = DATA_START) -> int:
|
||||
def first_empty_row(self, start: int | None = None) -> int:
|
||||
"""返回数据区第一个空行(A 列为空)。"""
|
||||
if self._used_rows is None:
|
||||
self._refresh_used_rows()
|
||||
row = max(start, DATA_START)
|
||||
row = max(start or self.data_start, self.data_start)
|
||||
while row in self._used_rows:
|
||||
row += 1
|
||||
return row
|
||||
@@ -161,8 +228,8 @@ class TemplateRouter:
|
||||
spu_code = str(spu_code).strip().lower()
|
||||
match = self._normalize_match(match)
|
||||
hits = []
|
||||
for row in range(DATA_START, self.ws.max_row + 1):
|
||||
v = self.ws.cell(row, SPU_CODE_COL).value
|
||||
for row in range(self.data_start, self.ws.max_row + 1):
|
||||
v = self.ws.cell(row, self.spu_code_col).value
|
||||
if v is None:
|
||||
continue
|
||||
v = str(v).strip().lower()
|
||||
@@ -171,15 +238,15 @@ class TemplateRouter:
|
||||
return hits
|
||||
|
||||
def find_sku_rows(self, spu_code: str, match: str = "exact") -> list[int]:
|
||||
"""按 SPU货号(B列)+ 层级=sku(A列)查找所有 SKU 行号(单 SPU 多色时无 SPU 行,SKU 行即全部)。"""
|
||||
"""按 SPU货号(按列名解析)+ 层级=sku(商品层级列)查找所有 SKU 行号(单 SPU 多色时无 SPU 行,SKU 行即全部)。"""
|
||||
spu_code = str(spu_code).strip().lower()
|
||||
match = self._normalize_match(match)
|
||||
hits = []
|
||||
for row in range(DATA_START, self.ws.max_row + 1):
|
||||
lv = self.ws.cell(row, LEVEL_COL).value
|
||||
for row in range(self.data_start, self.ws.max_row + 1):
|
||||
lv = self.ws.cell(row, self.level_col).value
|
||||
if lv is None or str(lv).strip().lower() != "sku":
|
||||
continue
|
||||
v = self.ws.cell(row, SPU_CODE_COL).value
|
||||
v = self.ws.cell(row, self.spu_code_col).value
|
||||
if v is None:
|
||||
continue
|
||||
v = str(v).strip().lower()
|
||||
@@ -189,7 +256,7 @@ class TemplateRouter:
|
||||
|
||||
def find_level_rows(self, level: str, match: str = "exact") -> list[int]:
|
||||
"""
|
||||
按 商品层级(A列)查找所有行号。
|
||||
按 商品层级(按列名解析)查找所有行号。
|
||||
|
||||
match="exact"(默认):精准匹配,去首尾空格后完全相等(不区分大小写);
|
||||
match="fuzzy" :模糊匹配,层级包含关键字即命中(不区分大小写)。
|
||||
@@ -197,8 +264,8 @@ class TemplateRouter:
|
||||
level = str(level).strip().lower()
|
||||
match = self._normalize_match(match)
|
||||
hits = []
|
||||
for row in range(DATA_START, self.ws.max_row + 1):
|
||||
v = self.ws.cell(row, LEVEL_COL).value
|
||||
for row in range(self.data_start, self.ws.max_row + 1):
|
||||
v = self.ws.cell(row, self.level_col).value
|
||||
if v is None:
|
||||
continue
|
||||
v = str(v).strip().lower()
|
||||
@@ -278,13 +345,27 @@ class TemplateRouter:
|
||||
return column_index_from_string(key_s.upper())
|
||||
raise KeyError(f"无法识别的表头: {key!r}(可用完整表头/列名/列号/列字母)")
|
||||
|
||||
def _value_by_col(self, data: dict, name: str):
|
||||
"""从 data 中按列名取第一个匹配的值(键可为完整表头/列名/列号/列字母)。"""
|
||||
col = self.column_map.get(name)
|
||||
if col is None:
|
||||
return None
|
||||
for k, v in data.items():
|
||||
try:
|
||||
if self.resolve_col(k) == col:
|
||||
return v
|
||||
except KeyError:
|
||||
continue
|
||||
return None
|
||||
|
||||
def insert(self, data: dict, spu_code: str | None = None,
|
||||
level: str | None = None, update: bool = False,
|
||||
match: str = "exact") -> int:
|
||||
"""
|
||||
插入一行数据到模版表。data 键支持:完整表头(推荐)、列名、列号、列字母。
|
||||
|
||||
- 若 data 中带「商品层级/基础信息-商品层级」则自动取其值路由;
|
||||
- 若 data 中带「商品层级」(任意键形式,如「基础信息(SKU/SPU均必填)-商品层级」/
|
||||
「基础信息-商品层级」/「商品层级」)则自动取其值路由;
|
||||
- 传 spu_code 会把该行路由到对应 SPU 行之后(SKU 关联);
|
||||
- update=True 时覆盖已存在的目标行,否则写入新空行;
|
||||
- match 控制货号/层级的匹配模式:exact(精准,默认) / fuzzy(模糊),见 route()。
|
||||
@@ -293,27 +374,20 @@ class TemplateRouter:
|
||||
"""
|
||||
match = self._normalize_match(match)
|
||||
|
||||
# 1) 解析层级与 spu_code
|
||||
lv = level
|
||||
if lv is None:
|
||||
for k in ("基础信息-商品层级", "商品层级", LEVEL_COL):
|
||||
if k in data:
|
||||
lv = data[k]
|
||||
break
|
||||
# 1) 解析层级与 spu_code(按列名匹配,不依赖固定键名/列号)
|
||||
lv = level if level is not None else self._value_by_col(data, "商品层级")
|
||||
lv_s = str(lv).strip().lower() if lv is not None else ""
|
||||
|
||||
# 只有「SKU 层」才需要按 SPU货号 关联路由;SPU/单sku商品 层走新行路由
|
||||
use_spu_route = spu_code is not None
|
||||
if spu_code is None:
|
||||
if lv is not None and str(lv).strip().lower() == "sku" and "SPU货号" in data:
|
||||
use_spu_route = True # sku 层 -> 插到所属 SPU 行之后
|
||||
elif lv is None and "SPU货号" in data and self.find_spu_rows(data["SPU货号"], match):
|
||||
use_spu_route = True # 无层级但货号已存在 -> 视为该 SPU 的变体行
|
||||
spu = spu_code if use_spu_route else None
|
||||
if use_spu_route and spu is None:
|
||||
spu = data["SPU货号"]
|
||||
if not self.find_spu_rows(spu, match):
|
||||
use_spu_route = False # 无 SPU 行(单 SPU 多色,不填 SPU 行)→ 按层级末尾追加
|
||||
spu = None
|
||||
# 只有「SKU 层」或未声明层级时才按 SPU货号 关联路由;SPU/单sku商品 层走新行路由
|
||||
spu = spu_code
|
||||
use_spu_route = spu is not None
|
||||
if spu is None and (lv_s == "sku" or lv is None):
|
||||
spu = self._value_by_col(data, "SPU货号")
|
||||
if spu is not None:
|
||||
use_spu_route = True
|
||||
if use_spu_route and spu is not None and not self.find_spu_rows(spu, match):
|
||||
use_spu_route = False # 无 SPU 行(单 SPU 多色,不填 SPU 行)→ 按层级末尾追加
|
||||
spu = None
|
||||
|
||||
# 2) 路由到目标行
|
||||
row = self.route(lv, spu, update=update, match=match)
|
||||
|
||||
Reference in New Issue
Block a user