feat(product-commerce): 款式主键统一 style_id 并下线评价采集
采集端与 data-hub 统一用 style_id (= dim_style.id) 关联款式,style_name 降级为显示冗余。
款式主键:
- 14 张采集表加 style_id BIGINT + style_match_method + *_style_id_idx 索引
- 一律逻辑外键:不建 FOREIGN KEY,早期 10 个 *_style_id_fk 在 schema.sql 里幂等 DROP
- loader 下发 dim_style.id;db 层 attach_style_ids / resolve_style_ids_by_product 兜底
- 新增 backfill_style_id.py 三段回填(款名 / 日报按商品 ID / 广告表按 spu_id)
- 新增视图 v_style_id_coverage、v_style_unmatched_product、v_style_unmatched_source
- 实库补录 199 个商品 ID 到 67 款 spus;日报 style_id 覆盖 49.1% -> 79.0%,
tm 广告 99.4% / jd 92.4% / dy 83.2%,其余 11 张 100%
评价采集下线:
- product_reviews 归档为 product_reviews_archived_20261101
- 差评改由 shop.{tmall,jd,dy}_negative_reviews.daily 采集,消费方读 v_negative_reviews
- 删除 import_product_reviews.py / orchestrate_review_collection.py 及相关命令与绑定
- dy_product_scraping 默认流程摘除评论采集,保留 --reviews-only
天猫广告:
- 迁移到独立表 tm_product_ad_daily_metrics,不再写 product_daily_metrics.raw_data
同批附带:
- 光合上传支持天猫新店 new_bag(仅黑曜石系列)
- 京东商智双月日历排除 -diff-month 补位格
- 万相主体搜索词覆盖 STYLE_SEARCH_KEYWORDS
- ERP 部分编码失败时用已成功编码销量兜底,避免整款归零
This commit is contained in:
1 parent
622a609800
commit
6f1c193f4b
50 files changed
+3301
-1150
No files matched your search
+128
-11
@@ -43,7 +43,7 @@ uv run gyxx scripts run <command_id> --date 2026-08-01 --execute
|
||||
| 京东自营品牌单日回采 | `shop.jd_self_operated.collect_brand` | `--date` 自动成为起止日期 |
|
||||
| 商品历史补采 | `product.backfill.run` | `--date` 自动成为 `--from/--to` |
|
||||
| 商品评价补采 | `product.review.orchestrate` | `--date` 自动成为目标日期 |
|
||||
| 天猫旗舰店和箱包店视频上传 | `product.video_upload.run` | 业务日期用于运行审计;默认顺序扫描两店,由 PostgreSQL 附件级账本防重 |
|
||||
| 天猫旗舰店、箱包店与天猫新店视频上传 | `product.video_upload.run` | 业务日期用于运行审计;`--store all` 顺序扫描三家,由 PostgreSQL 附件级账本防重 |
|
||||
| 京东旗舰店视频上传 | `product.jd_video_upload.run` | 业务日期只用于运行审计;从固定凌云air记录锚点按飞书视图顺序扫描,日期可为空 |
|
||||
| 采购单更新 | `supply.workflow.run` | 自动选择 `mcp-run purchase-order-update` |
|
||||
|
||||
@@ -51,16 +51,16 @@ uv run gyxx scripts run <command_id> --date 2026-08-01 --execute
|
||||
|
||||
### 天猫视频上传工作流
|
||||
|
||||
天猫视频上传注册为幂等定时工作流 `product.video_upload`,按 `config/schedules.json` 以 2026-09-04 为起始日每 3 天 14:00(Asia/Shanghai)执行;前端仍可手动选择“光影行星旗舰店 / 光影行星鑫华达专卖店:龙虾仔”并立即运行,也可填写天猫话题完整名称或关键词。正式执行只处理所选店铺,话题搜索结果按包含关系选中。默认 dry-run 只生成运行记录并跳过浏览器节点;确认飞书待传记录、对应淘宝光合店铺的登录态和 PostgreSQL 后,再显式正式执行:
|
||||
天猫视频上传注册为幂等定时工作流 `product.video_upload`,按 `config/schedules.json` 以 2026-09-04 为起始日每 3 天 14:00(Asia/Shanghai)执行;前端仍可手动选择“光影行星旗舰店 / 光影行星鑫华达专卖店:龙虾仔 / 天猫新店(gyxx箱包旗舰店:龙虾仔)”并立即运行,也可填写天猫话题完整名称或关键词。正式执行只处理所选店铺,话题搜索结果按包含关系选中。默认 dry-run 只生成运行记录并跳过浏览器节点;确认飞书待传记录、对应淘宝光合店铺的登录态和 PostgreSQL 后,再显式正式执行:
|
||||
|
||||
```bash
|
||||
uv run gyxx run product.video_upload --date 2026-08-07
|
||||
uv run gyxx run product.video_upload --date 2026-08-07 --execute
|
||||
```
|
||||
|
||||
前端把店铺选择作为受限运行参数传给脚本,只允许 `flagship` 或 `luggage`。脚本先读取飞书并应用所选店铺路由,再写入/读取附件级账本;只有存在可领取的未上传附件时才启动有头 Chrome、登录对应账号并上传。旗舰店只选择“天猫上传”未勾选且有附件的光影行星男/女记录;箱包店只选择女性、全部款式命中箱包店白名单且有附件的记录。两店分别使用 `GUANGHE_USERNAME` / `GUANGHE_PASSWORD` 和 `GUANGHE_LUGGAGE_USERNAME` / `GUANGHE_LUGGAGE_PASSWORD`,箱包店当前账号为 `光影行星鑫华达专卖店:龙虾仔`,密码配置保持不变,并保留独立浏览器 Profile。发生登录或页面错误时保留窗口供人工定位,排查完成后手动关闭。
|
||||
前端把店铺选择作为受限运行参数传给脚本,只允许 `flagship`、`luggage` 或 `new_bag`。脚本先读取飞书并应用所选店铺路由,再写入/读取附件级账本;只有存在可领取的未上传附件时才启动有头 Chrome、登录对应账号并上传。旗舰店只选择“天猫上传”未勾选且有附件的光影行星男/女记录;箱包店只选择女性、全部款式命中箱包店白名单且有附件的记录;天猫新店不区分性别,只要求一条记录的**全部**「代拍产品」款式都属黑曜石系列(关键词 `黑曜石`,如黑曜石托特、黑曜石吐司、黑曜石电脑包、黑曜石饺子),组合款里掺了其它系列整条记录都不进新店,视频源与其它店铺为同一张飞书表。三店分别使用 `GUANGHE_USERNAME` / `GUANGHE_PASSWORD`、`GUANGHE_LUGGAGE_USERNAME` / `GUANGHE_LUGGAGE_PASSWORD` 和 `GUANGHE_NEW_BAG_USERNAME` / `GUANGHE_NEW_BAG_PASSWORD`,箱包店当前账号为 `光影行星鑫华达专卖店:龙虾仔`,天猫新店当前账号为 `gyxx箱包旗舰店:龙虾仔`,密码配置保持不变,各自保留独立浏览器 Profile(`-luggage` / `-new-bag`)。发生登录或页面错误时保留窗口供人工定位,排查完成后手动关闭。
|
||||
|
||||
每次正式扫描都会把视频附件写入 PostgreSQL 附件级发布账本,防重身份由视频 `file_token + 目标账号 + 飞书记录` 构成。账本状态为 `published` 的附件在下次执行时自动跳过,因此同一业务日期可以安全重跑并发现新增附件。旗舰店仍只在一条飞书记录的全部附件发布成功后回写“天猫上传”;箱包店不借用该勾选状态,是否已上传完全以自己的目标账号账本为准。
|
||||
每次正式扫描都会把视频附件写入 PostgreSQL 附件级发布账本,防重身份由视频 `file_token + 目标账号 + 飞书记录` 构成。账本状态为 `published` 的附件在下次执行时自动跳过,因此同一业务日期可以安全重跑并发现新增附件。旗舰店仍只在一条飞书记录的全部附件发布成功后回写“天猫上传”;箱包店和天猫新店不借用该勾选状态,是否已上传完全以自己的目标账号账本(`guanghe-luggage` / `guanghe-new-bag`)为准,同一视频可以分别发到多家店铺。
|
||||
|
||||
首次正式执行会读取 `config/tmall-video-publication-backfill.json`,将 2026-07-26 箱包店旧日志中可按 `record_id + file_token + 文件名` 再次核验的 25 个明确成功附件回填为 `published`。配置与当前飞书任一身份不一致都会停止,不会静默猜测。该回填不依赖部署机保留旧 `var/` 日志文件;证据路径与成功行号已固化在配置中供审计。
|
||||
|
||||
@@ -103,11 +103,17 @@ uv run gyxx scripts run product.jd_video_upload.run --date 2026-08-07 --arg=--re
|
||||
|
||||
唯一工作流 ID 是 `product.ecommerce_costs.daily`,每天 10:00 由项目内 Python 调度器运行,
|
||||
业务日期为前一天;天猫、京东、抖音三个下载分支并行启动,各自完成下载后再导入。天猫流程先进入万相台商品报表选择昨日,提交下载任务;任务在下载列表显示“生成成功”
|
||||
后下载 ZIP、做路径安全校验并解压 CSV,随后将商品级广告指标幂等写入
|
||||
`product_daily_metrics.raw_data.tm_ad_metrics`。商品款式先按 `dim_style.tm_spus` 的商品 ID 匹配,
|
||||
匹配不到再按商品名称做精确包含匹配,并在节点内保留 `style_name` 与
|
||||
`style_match_method`(`product_id`/`product_name`/`ambiguous`/`unmatched`)。它不会覆盖同一商品日期
|
||||
已有的访客、成交等经营日报字段;data-hub 产品生命进程从该节点读取广告成交金额和花费并计算投产比、电商费比。
|
||||
后下载 ZIP、做路径安全校验并解压 CSV,随后将商品级广告指标幂等写入独立表
|
||||
`tm_product_ad_daily_metrics`(每日每商品一行,主键 `(stat_date, product_id)`)。花费、直接/间接/归因
|
||||
成交金额和投产比走标量列,报表其余指标(展现、点击、点击率、转化率、预售成交等)原样存在 `metrics`
|
||||
JSONB。商品款式先按 `dim_style.tm_spus` 的商品 ID 匹配,匹配不到再按商品名称做精确包含匹配,结果
|
||||
写进 `style_name` 与 `style_match_method`(`product_id`/`product_name`/`ambiguous`/`unmatched`)。
|
||||
天猫广告数据**不再写入** `product_daily_metrics.raw_data.tm_ad_metrics`,投放与经营日报分表,经营数据
|
||||
重新同步不会覆盖投放数据;data-hub 产品生命进程从新表读取花费和广告归因成交金额并计算投产比、电商费比。
|
||||
|
||||
历史数据迁移(2026-11-01 一次性执行):`src/gyxx_flow/modules/product_commerce/scripts/
|
||||
migrate_tm_ad_metrics_to_table.py`,默认 dry-run,`--execute` 先备份旧节点到运行时 TMP_ROOT 再回填,
|
||||
校验行数与花费合计一致后才允许 `--clear-source` 清除 `raw_data` 里的旧节点。
|
||||
|
||||
正式服务必须注入 `WANXIANG_ACCOUNT`、`WANXIANG_PASSWORD`、`GYXX_DATA_ROOT` 和
|
||||
`GYXX_POSTGRES_DSN`。默认浏览器 Profile 是
|
||||
@@ -139,8 +145,9 @@ uv run gyxx scripts run product.import.tmall_ads --date 2026-08-17 --execute
|
||||
1. 检查 `gyxx schedule status`、服务日志和对应 `run_id`,确认失败发生在申请、下载、解压还是入库。
|
||||
2. 检查同一 Profile 是否仍能打开万相台商品报表;登录失效时在有头命令中人工完成验证。
|
||||
3. 检查日期目录中的 `manifest.json`、CSV 表头和 `日期/主体ID/主体名称`;ZIP 存在不代表数据库已导入。
|
||||
4. 检查 PostgreSQL 的 `product_daily_metrics` 是否存在 `platform='tm'`、目标日期和商品 ID,且
|
||||
`raw_data->'tm_ad_metrics'` 已更新;确认经营日报其他 JSON 节点仍保留。
|
||||
4. 检查 PostgreSQL 的 `tm_product_ad_daily_metrics` 是否存在目标日期与商品 ID 的行,`spend`、
|
||||
`attributed_deal_amount` 是否为目标值,`style_name` 是否已匹配;确认 `product_daily_metrics`
|
||||
同日期经营数据(访客、成交)仍完整。
|
||||
5. 只有在外部状态核对清楚后才按同一业务日期重跑,不能用换日期或重复申请掩盖下载结果不明。
|
||||
|
||||
## 调度操作
|
||||
@@ -385,6 +392,116 @@ ORDER BY style_name;
|
||||
必须把它拆成独立系列(单独的 `series_name` + 锚点标记)并把地址写在自己那行;只写地址
|
||||
而不拆系列的话,聚合会把它并入原系列,只在日志里留一条告警,不会真的生效。
|
||||
|
||||
## 款式主键统一(style_id = dim_style.id,2026-11-01)
|
||||
|
||||
采集端不再只用 `style_name` 关联款式。**唯一关联键是 `style_id`**(= `dim_style.id`,
|
||||
与 data-hub 的 22 张表同一套主键),`style_name` 降级为显示冗余。这样在 dim_style
|
||||
改款名时,历史指标表不会变成孤儿。
|
||||
|
||||
### 数据从哪里来
|
||||
|
||||
| 环节 | 机制 |
|
||||
|---|---|
|
||||
| loader 下发 | `merge_lifecycle_style_records()` 输出 `style_id` + `style_match_method`,经 `style_config_loader` 进入每条款式配置 |
|
||||
| db 层兜底 | `db.attach_style_ids()`(按款名)/ `db.resolve_style_ids_by_product()`(按平台商品 ID)在 upsert 内补齐 |
|
||||
| 历史回填 | `backfill_style_id.py` |
|
||||
|
||||
### 回填与验收
|
||||
|
||||
```powershell
|
||||
# 只报数,不写库
|
||||
.venv\Scripts\python.exe backfill_style_id.py --dry-run
|
||||
# 执行(幂等,只补 style_id IS NULL 的行)
|
||||
.venv\Scripts\python.exe backfill_style_id.py
|
||||
# 只补款名表,跳过按商品 ID 的两步
|
||||
.venv\Scripts\python.exe backfill_style_id.py --skip-product-table --skip-ad-tables
|
||||
```
|
||||
|
||||
回填分三段,缺一段覆盖率就上不去:
|
||||
|
||||
| 段 | 表 | 依据 |
|
||||
|---|---|---|
|
||||
| 按款名 | 13 张(`daily_style_metrics` / `weekly_style_metrics` / `erp_all_shop_style_daily_metrics` 等) | `t.style_name = dim_style.style_name` |
|
||||
| 按商品 ID | `product_daily_metrics` | `platform + product_id` 反查 `*_spus` |
|
||||
| 按商品 ID(二次) | 三张广告表 | 同上;**顺带把空的 `style_name` 补成 dim_style 的权威款名** |
|
||||
|
||||
广告表必须走第三段:平台的报表不给款式名,`jd_product_ad_daily_metrics` 有 27%、
|
||||
`dy_product_ad_daily_metrics` 有 40% 的行 `style_name` 为空,只按款名回填完全无效。
|
||||
补完后 data-hub 现有按款名的 join 立刻多认回一批行,不必等主键切换。
|
||||
|
||||
```sql
|
||||
-- 各表回填进度;除 product_daily_metrics 与三张广告表外都应 100%
|
||||
SELECT * FROM v_style_id_coverage ORDER BY tbl;
|
||||
-- 未建档的平台商品 ID,仅统计经营日报口径
|
||||
SELECT * FROM v_style_unmatched_product ORDER BY platform, rows DESC;
|
||||
-- 同上,但把三张广告表也算进来(纯投流商品只在这里出现)
|
||||
SELECT * FROM v_style_unmatched_source ORDER BY platform, rows DESC;
|
||||
```
|
||||
|
||||
`style_match_method` 取值:`dim_style`(按款名精确)、`product_id`(按平台商品 ID)、
|
||||
`product_name`(按商品名包含)、`unmatched`(确实没建档)。
|
||||
|
||||
### 补 spus 的正确做法
|
||||
|
||||
`product_daily_metrics.product_no` 存的是**款名**(不是 ERP 编码),天猫填充率 100%、
|
||||
京东 97.8%、抖音 0%。补 spus 时按这个优先级,命中率远高于猜商品标题:
|
||||
|
||||
1. `product_no` 精确等于 `dim_style.style_name`;
|
||||
2. `product_no` 与款名互为前后缀(长度差 ≤ 3,如「蓝鹊pro」→「蓝鹊」、「知更大号」→「知更」);
|
||||
3. 商品标题包含完整款名(抖音只能用这一条),并用「命中位置后的版本后缀」排除串款
|
||||
—— 例如标题「盖亚斜挎包**3**」不能归到「盖亚斜挎」;
|
||||
4. 编码与标题互相矛盾时**不要自动写**,列出来人工定(实测有天猫单品编码写「理岩」、
|
||||
标题写「阿波罗 air」,支付额 50 万,猜错代价极大)。
|
||||
|
||||
物料类(数据线、内胆包、补邮费、配件包、礼品袋、定制服务)不该进 `spus`,应留
|
||||
`unmatched`。`dim_style` 里本身建成款的物料(`配件包`、`飞机盒`、`电泵` 等)也不写。
|
||||
|
||||
### 必须知道的边界
|
||||
|
||||
- `product_daily_metrics` 表里**没有款名列**,只能拿 `platform + product_id` 去
|
||||
`tm_spus / jd_spus / dy_spus` 反查。未建档的商品 ID 回填不了,要靠建档页补 `spus`
|
||||
才能涨,不是脚本 bug。
|
||||
- 2026-11-01 补过一轮:`product_daily_metrics` 49.1% → **86.0%(jd)/ 77.3%(tm)/
|
||||
76.7%(dy)**,整体 79.0%;广告表 tm 99.4% / jd 92.4% / dy 83.2%。
|
||||
剩余 259 个商品 ID(dy 107 / jd 52 / tm 100)未建档,清单见 `v_style_unmatched_source`。
|
||||
- **不建物理外键**:14 张表的 `style_id` 都只做**逻辑外键**(与 data-hub 侧一致)。
|
||||
库里没有任何指向 `dim_style(id)` 的 FK 约束,完整性靠写入端(loader 下发 +
|
||||
`attach_style_ids` 兜底)、`backfill_style_id.py --dry-run`、`v_style_id_coverage`
|
||||
三道保证。理由:采集语义是「宁可留 NULL 待回填,也不要整批写入失败」,
|
||||
物理外键会在传错 id 时直接中断整天采集;此外拆系列/合并同款要改数据时,
|
||||
还得先删约束。早期 P4 建过的 10 个 `<表名>_style_id_fk` 已在 schema.sql 里
|
||||
幂等 `DROP CONSTRAINT IF EXISTS` 清掉,老库重跑 schema 即自动回到无 FK 状态。
|
||||
- 主键一律没动:`daily_style_metrics` 仍是 `(metric_date, style_name, platform)`。
|
||||
data-hub 有 6 处按 `style_name` join,改主键收益小、回归面大。
|
||||
|
||||
## 商品评价采集下线(product_reviews,2026-11-01)
|
||||
|
||||
`product_reviews` 已**停止写入并归档**为 `product_reviews_archived_20261101`。
|
||||
原因是 JD 侧 `style_name` 全靠 `sku_id` 反查,而映射表不存在,214 行全部为 NULL,
|
||||
两个消费方都是 `WHERE style_name = ?`,等于入库了读不到。
|
||||
|
||||
差评统一由三个独立工作流采集:`shop.tmall_negative_reviews.daily`、
|
||||
`shop.jd_negative_reviews.daily`、`shop.dy_negative_reviews.daily`。
|
||||
|
||||
### 消费方改读视图
|
||||
|
||||
```sql
|
||||
SELECT platform, comment_date, rating, content, style_id, style_name, style_match_method
|
||||
FROM v_negative_reviews; -- tm + jd + dy 三张差评表 UNION,款式按 ID 优先、商品名兜底
|
||||
```
|
||||
|
||||
改动点:`analyze_style.py:pull_reviews`、`content_marketing/daily_marketing_report.py:_review_context`。
|
||||
|
||||
### 口径变化(必须知道)
|
||||
|
||||
- 数据源**只有差评一档**,不再有好评/中评。风格分析的「评价等级分布」因此只剩差评。
|
||||
- 营销日报的 `sample_size` = 差评条数,`negative_rate` 恒为 1.0;门槛从「至少 5 条评价」
|
||||
改成「至少 1 条差评」(旧门槛在纯差评源上会导致永不产出)。
|
||||
- 覆盖面反而更全:天猫差评 97 条(旧表天猫为 0)、京东 32 条(旧表 8 月断更)。
|
||||
- 若产品侧确实需要好评样本,应把差评采集器扩成「按等级采集」,**不要恢复 product_reviews**。
|
||||
- `dy_product_scraping.py` 默认流程里的评论采集已摘除(只产出无人读取的本地 JSON);
|
||||
需要历史样本时用 `--reviews-only` 显式触发。
|
||||
|
||||
## 业务源码同步
|
||||
|
||||
外部业务源码目录只用于发现更新,GYXX Flow 运行时不导入或挂载这些目录。只读检查:
|
||||
|
||||
Reference in new issue
Block a user