feat(product-commerce): 系列聚合去重与采集开关两层语义

配置来源收敛
- 系列平台编码 tm/jd/dy/jd_self 与 9 类飞书地址改为整系列并集去重后挂锚点,
  子款编码保留在 series.platform_ids 备查;多表冲突保留最高优先级并告警
- 写表脚本(当日/天猫主图/京东主图)去掉本地 JSON 回退,地址缺失或读取失败
  直接 fail closed,避免库与表地址不一致被静默掩盖
- 测试注入 records_provider 时禁用配置缓存读写,防止假数据写入生产快照

采集开关(dim_style.collect_enabled)
- 锚点为系列总开关:false 则整系列不采
- 锚点为 true 时,子款自身 false 仅把自己的 ERP 编码排除出聚合,其余子款照常
- 排除结果记录在 series.erp_excluded 并在运行时提示

聚水潭全店铺日报防错
- 列布局 fail closed、行 td 数校验、满页判定 + 总页数兜底、值域交叉校验
- 新增单日总量闸:超近 14 天中位数 8 倍拒绝落库

其他
- 新增电商费用归集配置 ad_cost_rollup.json 与积分兑换周报采集
- 店铺情报、供应链到货同步、通知路由与相关配置/文档同步更新
- 忽略本地 dsh workbench 脚手架(含 node_modules)
This commit is contained in:
wangyunlong committed 2026-10-30 11:49:31 +08:00
1 parent 2f31d5872f
commit 622a609800
66 files changed
+4156 -469

No files matched your search

+137 -3
View File
@@ -161,7 +161,9 @@ def test_normalize_style_config_owns_erp_once_and_platform_ids_per_child() -> No
]
def test_runtime_records_use_lifecycle_identity_and_keep_dynamic_targets() -> None:
def test_runtime_records_use_lifecycle_identity_and_lifecycle_targets() -> None:
"""地址以 dim_style 为准:动态配置里的旧地址不再覆盖建档值。"""
dynamic_records = [
{
"style_name": "款式甲",
@@ -185,6 +187,8 @@ def test_runtime_records_use_lifecycle_identity_and_keep_dynamic_targets() -> No
"jd_self": [],
"dy": [],
},
"sales_bitable_url": "https://example.feishu.cn/base/newsales?table=tblNewSales",
"tm_persona_bitable_url": "tm-persona-new",
}
]
@@ -196,8 +200,138 @@ def test_runtime_records_use_lifecycle_identity_and_keep_dynamic_targets() -> No
assert by_platform["京东自营"]["item_ids"] == []
assert by_platform["抖音"]["item_ids"] == []
assert all(row["erp_codes"] == ["NEW-ERP"] for row in records)
assert by_platform["天猫"]["sales_bitable_url"].endswith("table=tblSales")
assert by_platform["天猫"]["tm_persona_bitable_url"] == "tm-persona"
assert by_platform["天猫"]["sales_bitable_url"].endswith("table=tblNewSales")
assert by_platform["天猫"]["tm_persona_bitable_url"] == "tm-persona-new"
def test_dynamic_targets_are_ignored_when_lifecycle_has_none() -> None:
"""建档地址为空时不再回退旧表:飞书地址只有 dim_style 一个来源。"""
records = merge_lifecycle_style_records(
[
{
"style_name": "款式乙",
"erp_codes": ["ERP-B"],
"platform": "天猫",
"item_ids": ["TM-B"],
"sales_bitable_url": "https://example.feishu.cn/base/legacy?table=tblLegacy",
"main_image_bitable_url": "https://example.feishu.cn/base/legacy?table=tblLegacyMain",
"creator_bitable_url": "https://example.feishu.cn/base/legacy?table=tblLegacyCreator",
}
],
[
{
"id": 9,
"name": "款式乙",
"erp_style_codes": ["ERP-B"],
"platform_product_ids": {"tm": ["TM-B"], "jd": [], "jd_self": [], "dy": []},
}
],
)
row = next(r for r in records if r["platform"] == "天猫")
assert row["sales_bitable_url"] == ""
assert row["main_image_bitable_url"] == ""
assert row["creator_bitable_url"] == ""
def test_series_targets_propagate_from_anchor() -> None:
"""锚点的 9 类地址下发到同系列子款;子款自身的旧地址被覆盖。"""
lifecycle_rows = [
{
"id": 1,
"name": "逐星",
"erp_style_codes": ["E-ANCHOR"],
"series_name": "逐星",
"is_series_anchor": True,
"platform_product_ids": {"tm": ["TM-A"], "jd": [], "jd_self": [], "dy": []},
"sales_bitable_url": "https://example.feishu.cn/base/anchor?table=tblAnchorSales",
"main_image_bitable_url": "https://example.feishu.cn/base/anchor?table=tblAnchorMain",
"creator_bitable_url": "https://example.feishu.cn/base/anchor?table=tblAnchorCreator",
},
{
"id": 2,
"name": "逐星GT",
"erp_style_codes": ["E-CHILD"],
"series_name": "逐星",
"is_series_anchor": False,
"platform_product_ids": {"tm": ["TM-C"], "jd": [], "jd_self": [], "dy": []},
"sales_bitable_url": "https://example.feishu.cn/base/child?table=tblChildSales",
},
]
records = merge_lifecycle_style_records([], lifecycle_rows)
child = next(r for r in records if r["style_name"] == "逐星GT")
assert "anchor" in child["sales_bitable_url"]
assert "anchor" in child["main_image_bitable_url"]
assert "anchor" in child["creator_bitable_url"]
def test_series_targets_aggregate_from_child_when_anchor_empty() -> None:
"""子款误填地址、锚点为空:聚合上浮到锚点并下发整条系列,不丢配置。"""
lifecycle_rows = [
{
"id": 1,
"name": "云栖",
"erp_style_codes": ["E-ANCHOR"],
"series_name": "云栖",
"is_series_anchor": True,
"platform_product_ids": {"tm": ["TM-A"], "jd": [], "jd_self": [], "dy": []},
},
{
"id": 2,
"name": "云栖标准版",
"erp_style_codes": ["E-CHILD"],
"series_name": "云栖",
"is_series_anchor": False,
"platform_product_ids": {"tm": [], "jd": [], "jd_self": [], "dy": []},
"sales_bitable_url": "https://example.feishu.cn/base/child?table=tblChildSales",
"main_image_bitable_url": "https://example.feishu.cn/base/child?table=tblChildMain",
},
]
records = merge_lifecycle_style_records([], lifecycle_rows)
anchor_sales = {r["sales_bitable_url"] for r in records if r["style_name"] == "云栖"}
child_sales = {r["sales_bitable_url"] for r in records if r["style_name"] == "云栖标准版"}
# 子款地址上浮到锚点,并统一下发到整条系列
assert anchor_sales == {"https://example.feishu.cn/base/child?table=tblChildSales"}
assert child_sales == anchor_sales
def test_series_targets_conflict_keeps_anchor_address(capsys) -> None:
"""同一字段多张表:保留锚点那条并告警,避免同系列拆到两个表。"""
lifecycle_rows = [
{
"id": 1,
"name": "冲突款",
"erp_style_codes": ["E-ANCHOR"],
"series_name": "冲突系列",
"is_series_anchor": True,
"platform_product_ids": {"tm": ["TM-A"], "jd": [], "jd_self": [], "dy": []},
"sales_bitable_url": "https://example.feishu.cn/base/anchor?table=tblAnchorSales",
},
{
"id": 2,
"name": "冲突款-2",
"erp_style_codes": ["E-CHILD"],
"series_name": "冲突系列",
"is_series_anchor": False,
"platform_product_ids": {"tm": [], "jd": [], "jd_self": [], "dy": []},
"sales_bitable_url": "https://example.feishu.cn/base/child?table=tblChildSales",
},
]
records = merge_lifecycle_style_records([], lifecycle_rows)
assert {r["sales_bitable_url"] for r in records} == {
"https://example.feishu.cn/base/anchor?table=tblAnchorSales"
}
assert "个不同地址" in capsys.readouterr().out
def test_runtime_records_empty_lifecycle_ids_clear_legacy_platform_ids() -> None: