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

+99
View File
@@ -286,6 +286,105 @@ uv run gyxx effects reconcile <workflow_id> \
浏览器状态异常时,只备份并处理目标脚本的 `state/browser/<module>/<script>`,不要清理其他脚本状态。调度状态异常时先停止唯一调度进程并备份 `state/scheduler`,不要同时启动第二实例。
## 商品日报采集开关(product.daily)
采集开关是 `dim_style.collect_enabled`(2026-10-29 起),**两层语义**(2026-10-30 起):
1. **锚点是系列总开关**:以该系列 `is_series_anchor = TRUE` 那一行为准;系列没有锚点时按款名
排序第一个。锚点 `FALSE` → **整条系列一款都不采**,同系列其余款式跟随锚点。
2. **锚点 `TRUE` 时,子款开关只决定自己的 ERP 编码是否参与聚合**:子款 `TRUE` 的 ERP 款式编码
并入锚点一起去聚水潭取数;子款 `FALSE` 的编码不并入(既不由它自己采,也不让锚点代采)。
被排除的子款列在 `series.erp_excluded`,运行时打
`[INFO] 系列「X」子款 Y 已关闭采集,ERP 编码不纳入「Z」聚合`。平台商品编码仍按整系列聚合,
不受子款开关影响。
旧的独立表 `product_daily_collect_scope` 已迁移并删除,备份在
`var/tmp/collect_scope_before_drop_20261029.json`。
默认值是 `TRUE`(采集),因此产品生命进程在 `dim_style` 新增的款式**无需任何操作**就会进入日报;
配件、包装、耗材、停售款才需要显式关掉。
关掉整条系列(改锚点那一行即可,子款自动跟随):
```sql
-- 单系列
UPDATE dim_style SET collect_enabled = FALSE WHERE style_name = '黑曜石托特包2';
-- 批量:把若干系列的所有行一起关(无锚点的系列也覆盖到)
UPDATE dim_style SET collect_enabled = FALSE
WHERE COALESCE(NULLIF(btrim(series_name), ''), style_name)
IN ('飞机盒', '香片', '雨衣', '鞋袋');
```
恢复采集把 `FALSE` 改成 `TRUE`。改完无需重启控制台或调度器:`StyleConfigLoader.load()`
每次运行都直接读库,下一轮 `product.daily`(每天 08:40,业务日 T-1)即生效。
两点注意:
- **开关落在锚点行上才控制整条系列。** ERP 编码与三平台商品编码都由
`_apply_series_sharing()` 按整系列聚合并去重后挂到锚点(子款误填的编码也会并入,但
`collect_enabled = FALSE` 的子款 ERP 编码除外),锚点是系列唯一的采集单元;子款自己的
编码保留在 `series.own_erp_codes` 与 `series.platform_ids` 备查。
- 关掉只影响**商品日报**(tm/jd/dy 平台段 + ERP 段)。天猫/京东主图周采集、聚水潭全店铺日报、
三平台电商费用**不受此开关影响**。
放行不等于一定会采:采集门仍要求「ERP 款式编码 + 各平台商品编码」齐备;飞书目标表地址
(下一节的 9 类 `bitable_*` 列)只决定能否写飞书,缺地址的款照常采集入库。缺口写在
`var/logs/product_commerce/<date>_skipped_styles.json` 的 `skipped.reason`。
改款式名时要同步改 `series_name`/锚点标记,否则旧开关会变成孤儿行、新名字按默认 TRUE 采集。
## 飞书表地址维护(dim_style)
2026-10-29 起,9 类飞书多维表格地址**全部维护在 `dim_style`**,而且只写在系列锚点行上。
`workflow_dynamic_styles` / `workflow_dynamic_style_platforms` 里的同名字段已废弃,
代码不再读取;旧值备份在 `var/tmp/bitable_urls_before_move_20261029.json`。
| 用途 | `dim_style` 列 | 原动态配置字段 |
|---|---|---|
| 销量表 | `bitable_weekly_url` | `sales_bitable_url` |
| 主图表 | `bitable_main_image_url` | `main_image_bitable_url` |
| 合作达人表 | `bitable_creator_url` | `creator_bitable_url` |
| 自营合作达人表 | `bitable_self_creator_url` | `self_creator_bitable_url` |
| 每周/每月笔记分析 | `bitable_weekly_note_url` | `weekly_note_analysis_url` |
| 平台单品分析 | `bitable_style_analysis_url` | `style_analysis_bitable_url` |
| 人群画像(天猫) | `bitable_persona_tm_url` | `tm_persona_bitable_url` |
| 人群画像(京东) | `bitable_persona_jd_url` | `jd_persona_bitable_url` |
| 人群画像(抖音) | `bitable_persona_dy_url` | `dy_persona_bitable_url` |
子款留空即可:两处代码会把整条系列的地址**聚合去重**后统一下发——
`workflow_config._propagate_series_targets()` 在合并建档数据时按「锚点优先、其余按款名」
收集整系列的非空地址,去重后写回每一行;`style_config_loader._apply_series_sharing()`
在聚合配置时再做一次同样的兜底。因此子款误填的地址不会丢、也不会让同系列出现两套表。
一个表位只能落一张表:同一字段填了多个不同地址时,保留优先级最高的那条,并在日志里打
`[WARN] 系列「X」<字段> 有 N 个不同表`,提示到锚点手动裁决。
```sql
-- 改某个系列的主图表(只改锚点一行,子款自动跟随)
UPDATE dim_style
SET bitable_main_image_url = 'https://xxx.feishu.cn/base/<base>?table=<tbl>'
WHERE style_name = '逐星'; -- 必须是该系列的锚点行
-- 查看哪些锚点还缺地址
SELECT style_name, series_name,
bitable_weekly_url, bitable_main_image_url, bitable_creator_url
FROM dim_style
WHERE is_series_anchor AND COALESCE(bitable_weekly_url, '') = ''
ORDER BY style_name;
```
改完不需要重启:`StyleConfigLoader.load()` 每次运行都读库,下一轮工作流即生效。
若某类地址为空,对应环节会跳过写表但照常采集入库(例如主图缺表地址打 `[NO-TABLE]`)。
**地址读取是 fail-closed 的**(2026-10-30):写表脚本拿不到 `dim_style` 地址时会打
`[FAIL] … 拒绝回退本地 JSON` 并以退出码 1 结束,**不会**改用 `bitable_style_map.json` /
`bitable_main_image_map.json` 里的旧地址。所以看到这类 `[FAIL]` 先去查库里的地址,
不要去改本地 JSON(这两个文件已无读取方)。
新增系列时,地址写在锚点行。若某款业务上确实是独立一套表(历史上 `逐星GT` 这种情形),
必须把它拆成独立系列(单独的 `series_name` + 锚点标记)并把地址写在自己那行;只写地址
而不拆系列的话,聚合会把它并入原系列,只在日志里留一条告警,不会真的生效。
## 业务源码同步
外部业务源码目录只用于发现更新,GYXX Flow 运行时不导入或挂载这些目录。只读检查: