采集端与 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 部分编码失败时用已成功编码销量兜底,避免整款归零
387 lines
22 KiB
Markdown
387 lines
22 KiB
Markdown
# 款式主键统一 + 评论采集下线 方案
|
||
|
||
> 状态:**已实施(2026-11-01)**,见文末「八、实施记录」
|
||
> 涉及项目:`gyxx-flow`(采集端)、`data-hub`(消费端,共用同一个 PostgreSQL)
|
||
> 编写日期:2026-11-01
|
||
|
||
## 一、三条结论
|
||
|
||
1. **`product_reviews` 可以下线**,但不能直接删——它有两个消费方(风格分析、营销日报)要改成读三张差评表。
|
||
2. **`style_id` 就是 `dim_style.id`**,与 data-hub 现有 `style_id` 同一体系,不会产生第二套 ID。已实测:`erp_product.style_id` 的 135 个值 **135/135** 命中 `dim_style.id`,`own_brand_product.style_id` 31/34 命中(3 条为历史脏数据)。
|
||
3. **改造入口只有一处**:`DIM_STYLE_RUNTIME_SQL` 早已 `SELECT s.id`,`merge_lifecycle_style_records()` 也已经拿到这个 id——只是被编码进 `source_record_id = "lifecycle_style:<id>"` 字符串里,没有作为独立字段下发给写入脚本。把它提成正式字段,全链路就都有了。
|
||
|
||
## 二、现状盘点
|
||
|
||
### 2.1 两套引用方式并存
|
||
|
||
| 侧 | 关联方式 | 表 |
|
||
|---|---|---|
|
||
| data-hub(产品生命、营销中台、客服、内容) | `style_id BIGINT` → `dim_style.id` | `erp_product`、`own_brand_product`、`erp_marketing_*`、`erp_cs_*`、`cmt_*`、`market_product_classification` 等 22 张 |
|
||
| gyxx-flow(采集端) | `style_name TEXT` | 15 张,见 2.3 |
|
||
|
||
### 2.2 实测匹配率(2026-11-01)
|
||
|
||
| 表 | 总行数 | 按 style_name 命中 dim_style |
|
||
|---|---|---|
|
||
| `daily_style_metrics` | 17,352 | 17,352(100%) |
|
||
| `erp_all_shop_style_daily_metrics` | 9,399 | 9,399(100%) |
|
||
| `product_reviews` | 1,936 | 1,434(74%,全部来自抖音;京东 214 行 style_name 全 NULL) |
|
||
| `product_daily_metrics` | 75,568 | **无款式列**,只能靠 `tm_spus/jd_spus/dy_spus @> to_jsonb(product_id)` 现算 |
|
||
|
||
`product_daily_metrics` 按商品 ID 反查的现状:
|
||
|
||
| 平台 | 行数 | 可匹配 | 去重商品 ID | 可匹配 ID |
|
||
|---|---|---|---|---|
|
||
| dy | 28,972 | 13,615(47%) | 253 | 83 |
|
||
| jd | 17,044 | 9,394(55%) | 160 | 61 |
|
||
| tm | 29,552 | 14,089(48%) | 281 | 92 |
|
||
|
||
一对多歧义 **0 条**。约一半行匹配不上,原因是 **458 个商品 ID 从未写进 dim_style 的 spus 数组**(dy 170 / jd 99 / tm 189),不是字段设计问题。
|
||
|
||
### 2.3 采集端 15 张只存 style_name 的表
|
||
|
||
`daily_style_metrics`、`weekly_style_metrics`、`erp_all_shop_style_daily_metrics`、`daily_persona_metrics`、`main_image_creatives`、`fact_style_analysis_reports`、`decline_events`、`fact_alert_history`、`jd_self_weekly_sales`、`persona_screenshots`、`product_reviews`、`jd_product_ad_daily_metrics`、`dy_product_ad_daily_metrics`、`workflow_dynamic_styles`、`product_daily_metrics`(无列,靠 ID 反查)。
|
||
|
||
## 三、目标模型
|
||
|
||
- **唯一关联键 = `style_id`(`dim_style.id`)**,所有采集表落库时必须带上。
|
||
- `style_name` 保留为**显示冗余**,不再作为 join 键;等下游切完 join 后可以只在视图里出现。
|
||
- 匹配不上的行(未建档商品、广告未匹配 SKU、差评)**允许 `style_id IS NULL`**,但必须同时写 `style_match_method`(`product_id` / `product_name` / `anchor` / `unmatched`),便于统计漏维护。
|
||
- **不动现有主键**。`daily_style_metrics` 的 PK 仍是 `(metric_date, style_name, platform)`——data-hub 有 6 处 `metric.style_name = style.name` 的 join,改 PK 收益小、回归面大。加列、加索引、切 join 即可。
|
||
|
||
## 四、实施分期
|
||
|
||
### P0 观测层(只读,不动数据)
|
||
|
||
新增两个视图,先把问题量化出来,方案落地后靠它们验收:
|
||
|
||
```sql
|
||
-- 未建档的平台商品 ID(推动在 dim_style 里补 spus)
|
||
CREATE OR REPLACE VIEW v_style_unmatched_product AS
|
||
SELECT m.platform, m.product_id,
|
||
MAX(m.product_name) AS product_name,
|
||
MIN(m.stat_date) AS first_date, MAX(m.stat_date) AS last_date,
|
||
COUNT(*) AS rows
|
||
FROM product_daily_metrics m
|
||
WHERE NOT EXISTS (
|
||
SELECT 1 FROM dim_style d
|
||
WHERE (m.platform = 'tm' AND d.tm_spus @> to_jsonb(m.product_id))
|
||
OR (m.platform = 'jd' AND d.jd_spus @> to_jsonb(m.product_id))
|
||
OR (m.platform = 'dy' AND d.dy_spus @> to_jsonb(m.product_id)))
|
||
GROUP BY m.platform, m.product_id;
|
||
|
||
-- 各表 style_id 回填进度
|
||
CREATE OR REPLACE VIEW v_style_id_coverage AS
|
||
SELECT 'daily_style_metrics' AS tbl, COUNT(*) AS rows,
|
||
COUNT(style_id) AS with_style_id FROM daily_style_metrics
|
||
UNION ALL SELECT 'erp_all_shop_style_daily_metrics', COUNT(*), COUNT(style_id)
|
||
FROM erp_all_shop_style_daily_metrics
|
||
UNION ALL SELECT 'product_daily_metrics', COUNT(*), COUNT(style_id)
|
||
FROM product_daily_metrics;
|
||
```
|
||
|
||
验收锚点:`v_style_id_coverage` 里前三张表 `with_style_id` 开始上涨到预期比例;`v_style_unmatched_product` 的条数随建档推进下降。
|
||
|
||
### P1 评论采集链路下线
|
||
|
||
**要删的东西**
|
||
|
||
| 位置 | 动作 |
|
||
|---|---|
|
||
| `orchestrate_daily_collection.py:437-452` | 移除 `import_product_reviews` 步骤 |
|
||
| `import_product_reviews.py` | 删除 |
|
||
| `orchestrate_review_collection.py` | 删除 |
|
||
| `config/commands.json:516` `product.review.orchestrate` | 移除命令 |
|
||
| `db/__init__.py` `upsert_product_reviews` | 删除 |
|
||
| `db/schema.sql:813-870` `product_reviews` 建表段 | 移除(库里的表归档不删) |
|
||
|
||
**要补的东西**
|
||
|
||
1. 建统一差评视图,字段对齐两个消费方现在的用法(`platform` / `rating` / `content` / `style_id` / `comment_date`):
|
||
|
||
```sql
|
||
CREATE OR REPLACE VIEW v_negative_reviews AS
|
||
SELECT 'tm' AS platform, n.business_date AS comment_date, n.review_level AS rating,
|
||
n.content, NULL::text AS order_id, NULL::text AS product_id,
|
||
n.product_name, n.review_id, n.review_at, n.raw_review
|
||
FROM tmall_negative_reviews n
|
||
UNION ALL
|
||
SELECT 'jd', n.business_date, n.review_level, n.content, n.order_id, NULL,
|
||
n.product_name, n.review_id, n.review_at, n.raw_review
|
||
FROM jd_negative_reviews n
|
||
UNION ALL
|
||
SELECT 'dy', n.business_date, n.rating_level, n.content, n.order_id, n.product_id,
|
||
n.product_name, n.review_id, n.review_at, n.raw_review
|
||
FROM dy_negative_reviews n;
|
||
```
|
||
|
||
2. 改两个消费方:
|
||
- `analyze_style.py:112 pull_reviews()` → 改查 `v_negative_reviews`(按 `style_id` 或 `product_id` 关联)
|
||
- `content_marketing/daily_marketing_report.py:1020 _review_context()` → 同上
|
||
|
||
3. `product_reviews` 归档而不是删除:
|
||
|
||
```sql
|
||
ALTER TABLE product_reviews RENAME TO product_reviews_archived_20261101;
|
||
```
|
||
|
||
**必须明确的功能退化**:`product_reviews` 存的是好/中/差全量,差评三表只有差评。下线后:
|
||
- 风格分析里的「评价等级分布」从三档变成只有差评一档(差评样本反而更完整:天猫 97 + 京东 32 + 抖音 14,而 `product_reviews` 里天猫是 0、京东 8 月就断了)。
|
||
- 营销日报的「近 7 天差评归因」不受影响,反而多了天猫来源。
|
||
- 若产品侧确实需要好评样本沉淀,需要把三个差评采集器扩成「按等级采集」,不要恢复 `product_reviews`。
|
||
|
||
另外 `dy_product_scraping.py` 主流程里的评论采集动作只产出本地 JSON,`product_reviews` 停写后就没有消费方,建议同步关掉(或保留但不再进 `product.daily`)。
|
||
|
||
### P2 loader 下发 style_id(1 处改动,收益最大)
|
||
|
||
`src/gyxx_flow/adapters/workflow_config.py:322 merge_lifecycle_style_records()`:
|
||
|
||
```python
|
||
output["style_id"] = lifecycle.get("id") # dim_style.id
|
||
output["style_match_method"] = "dim_style"
|
||
```
|
||
|
||
同时 `source_record_id` 保留(控制台/账本还在用),只是不再承担传 ID 的职责。
|
||
|
||
影响面:所有通过 `runtime_workflow_configs()` 取款的脚本自动获得 `style_id`,包括日报聚合、周报、ERP 全店铺、人像、主图、声明式分析。这一步不改表、不改写入逻辑,可以先合入验证 loader 输出。
|
||
|
||
### P3 采集表加列 + 回填 + 写入端填充
|
||
|
||
**统一 DDL 模板**(以 `daily_style_metrics` 为例,其余同构):
|
||
|
||
```sql
|
||
ALTER TABLE daily_style_metrics ADD COLUMN IF NOT EXISTS style_id BIGINT;
|
||
UPDATE daily_style_metrics d SET style_id = s.id
|
||
FROM dim_style s WHERE s.style_name = d.style_name AND d.style_id IS NULL;
|
||
CREATE INDEX IF NOT EXISTS daily_style_metrics_style_date_idx
|
||
ON daily_style_metrics (style_id, metric_date DESC);
|
||
```
|
||
|
||
按表处理(**必须匹配**的加 `NOT NULL`,**允许未匹配**的保持可空):
|
||
|
||
| 表 | 款式来源 | style_id | 写入端改动 |
|
||
|---|---|---|---|
|
||
| `daily_style_metrics` | loader 款式清单 | NOT NULL | `aggregate_daily_final.py` + `db.upsert_daily_metrics` |
|
||
| `weekly_style_metrics` | 从 daily 聚合 | NOT NULL | `weekly_aggregate.py` |
|
||
| `erp_all_shop_style_daily_metrics` | loader ERP 款 | NOT NULL | `backfill_erp_all_shop_daily.py` |
|
||
| `daily_persona_metrics` | loader | NOT NULL | `collect_*_persona_to_bitable.py` |
|
||
| `main_image_creatives` | loader | NOT NULL | `insert_main_image_records.py` / `insert_jd_main_image_records.py` |
|
||
| `fact_style_analysis_reports` | loader | NOT NULL | `analyze_style.py` |
|
||
| `decline_events` / `fact_alert_history` | daily 派生 | NOT NULL | `check_nine_day_decline.py` |
|
||
| `jd_self_weekly_sales` | loader | NOT NULL | `jd_self_inventory_sales_collector.py` |
|
||
| `persona_screenshots` | loader | NOT NULL | `taobao_dmp_item_crowd_insight_screenshots.py` |
|
||
| `product_daily_metrics` | 平台商品 ID 反查 | **可空** | `import_product_daily.py` |
|
||
| `jd_product_ad_daily_metrics` / `dy_product_ad_daily_metrics` | 名称/ID 反查 | **可空**(已有 `match_status`) | 两个 `import_*_ad_*.py` |
|
||
| 差评三表 | 反查 | **可空** | `collectors/*_negative_reviews.py` 入库侧 |
|
||
|
||
**`product_daily_metrics` 的回填要处理系列聚合带来的歧义**(当前 0 条歧义,但要按锚点优先写死优先级):
|
||
|
||
```sql
|
||
UPDATE product_daily_metrics m
|
||
SET style_id = pick.style_id
|
||
FROM LATERAL (
|
||
SELECT d.id AS style_id
|
||
FROM dim_style d
|
||
WHERE (m.platform = 'tm' AND d.tm_spus @> to_jsonb(m.product_id))
|
||
OR (m.platform = 'jd' AND d.jd_spus @> to_jsonb(m.product_id))
|
||
OR (m.platform = 'dy' AND d.dy_spus @> to_jsonb(m.product_id))
|
||
ORDER BY d.is_series_anchor DESC, LENGTH(d.style_name), d.id
|
||
LIMIT 1
|
||
) pick
|
||
WHERE m.style_id IS NULL;
|
||
```
|
||
|
||
(真实脚本里用 Python 侧 `_resolve_style_match()` 复用同一套优先级,SQL 只用于一次性回填。)
|
||
|
||
回填后预期的非空比例 = 2.2 节的实测值:**dy 47% / jd 55% / tm 48%**。这不是 bug——剩下的是 458 个未建档商品 ID,要靠 P5 的人补 spus 才能涨。
|
||
|
||
### P4 约束 + 下游切 join
|
||
|
||
1. ~~对「必须匹配」的表加外键~~ → **改为不建物理外键(2026-11-01 定案)**。
|
||
|
||
原始设想是给 10 张「必须匹配」的表加 `FOREIGN KEY (style_id) REFERENCES dim_style(id) ON UPDATE CASCADE`,
|
||
实施后又按业主意见拆掉了。理由是采集链路的语义与硬约束冲突:
|
||
|
||
- 采集要求 **fail-soft**:一个商品 ID 没建档,正确行为是把该行 `style_id` 留 NULL 并标
|
||
`unmatched`,继续写其它行。物理外键会让整批 INSERT/UPDATE 直接报错,等于「一个商品
|
||
未建档 → 当天数据全丢」,比留 NULL 更糟。
|
||
- 款式会动:拆系列、合并同款、重建 dim_style 行都要先卸约束再动数据,运维成本不小。
|
||
- data-hub 侧 22 张表本来就用 `style_id` 关联、**不带物理外键**,两边保持一致才好维护。
|
||
|
||
改为**逻辑外键**,完整性由三处保证:
|
||
|
||
| 环节 | 手段 |
|
||
|---|---|
|
||
| 写入 | loader 下发 `style_id`(源自 dim_style 本身);缺失时 `attach_style_ids` / `resolve_style_ids_by_product` 批量兜底 |
|
||
| 校验 | `backfill_style_id.py --dry-run` 预演;`v_style_id_coverage` 报每张表覆盖率 |
|
||
| 观测 | `v_style_unmatched_product`(日报口径)/ `v_style_unmatched_source`(全源口径)列未建档清单 |
|
||
|
||
老库里早期建过的 10 个 `<表名>_style_id_fk` 由 `schema.sql` 里的 `DROP CONSTRAINT IF EXISTS`
|
||
幂等清理;只保留 `*_style_id_idx` 索引(join 性能靠索引,不靠约束)。
|
||
|
||
2. data-hub 侧把 `metric.style_name = style.name` 换成 `metric.style_id = style.id`。涉及 `ProductLifecycleMapper.xml` 第 120、155 行(`daily_style_metrics`),以及 `JOIN dim_style dim ON dim.style_name = style.name`(第 15 行)。改完后 text join 变 bigint join,是纯性能收益;`style_name` 列仍在,改错了可以立即回切。
|
||
|
||
3. 加测试:
|
||
- `tests/.../test_daily_style_metrics_style_id.py`:写入必须带 `style_id`,且等于 loader 输出的 id
|
||
- `tests/.../test_style_id_coverage.py`:`v_style_id_coverage` 里「必须匹配」的表 `with_style_id == rows`
|
||
- 现有 `test_import_tmall_product_ads.py` 的风格(断言 SQL 形状)继续用在新增字段上
|
||
|
||
### P5 数据治理(不是代码活,但决定 P3 的收益)
|
||
|
||
`v_style_unmatched_product` 出来的 458 个商品 ID 需要在 dim_style 建档页补 `tm_spus/jd_spus/dy_spus`。分批推进:先补最近 90 天有销量的,历史行允许永久 NULL。
|
||
|
||
## 五、风险与取舍
|
||
|
||
| 风险 | 影响 | 应对 |
|
||
|---|---|---|
|
||
| 款名在 dim_style 被改,历史表 `style_name` 变孤儿 | 现在 join 直接丢数据,这正是要 style_id 的原因 | P3 之后 join 走 id,款名改动不再影响 |
|
||
| 加 `NOT NULL` 时某天有未建档款写入 | 写入失败,工作流中断 | 先在写入端做「未匹配记 NULL + 打 WARN」,P5 补齐后再收紧为 NOT NULL |
|
||
| `product_reviews` 下线后没有好评样本 | 风格分析评价分布退化 | 明确接受;需要时扩差评采集器等级,不恢复旧表 |
|
||
| 改 PK(有人会想顺手做) | data-hub 6 处 join + 飞书脚本全回归 | **本次明确不做**,PK 保持 style_name |
|
||
| 差评表天猫侧没有 product_id | 只能靠商品名反查款式 | 采集器补 product_id(页面有商品链接可解析),或退化为名称匹配并标记 `unmatched` |
|
||
|
||
## 六、验收标准
|
||
|
||
1. `v_style_id_coverage`:`daily_style_metrics`、`erp_all_shop_style_daily_metrics`、`weekly_style_metrics` 的 `with_style_id = rows`(100%)。
|
||
2. `product_daily_metrics` 的 `style_id` 非空比例不低于 2.2 节的实测基线(47%/55%/48%),且逐月上升。
|
||
3. `product_reviews` 无新增行;两个消费方在 `v_negative_reviews` 上跑通,风格分析与营销日报输出结构不变。
|
||
4. data-hub 产品生命页、营销中台预算页的款式指标与改造前同月数值一致(对账 2026-10 全月)。
|
||
|
||
## 七、建议执行顺序
|
||
|
||
P0 → P1(下线评论链路,独立可交付)→ P2(loader 一行改动,验证输出)→ P3(分表灰度,先做 `daily_style_metrics`)→ P4(约束 + 下游切换)→ P5(补建档)。
|
||
|
||
P1 与 P2/P3 没有依赖关系,可以先做 P1 拿到收益(少一条采集链路、少一张表),再从容做款式主键统一。
|
||
|
||
## 八、实施记录(2026-11-01)
|
||
|
||
### 已落地
|
||
|
||
| 阶段 | 成果 |
|
||
|---|---|
|
||
| P0 | `v_style_unmatched_product`、`v_style_id_coverage` 两个观测视图已建并验证 |
|
||
| P1 | 删 `import_product_reviews.py` / `orchestrate_review_collection.py`;移除 `product.review.orchestrate` 命令与 runtime binding;`run_collect` 不再调评论入库;建 `v_negative_reviews`;两个消费方改读视图;`product_reviews` 归档为 `product_reviews_archived_20261101`(1936 行已备份到 `var/tmp/product_reviews_archive_20261101.json`) |
|
||
| P2 | `merge_lifecycle_style_records` 输出 `style_id` + `style_match_method`;loader 透传;实库验证 **204/204 款全部拿到 style_id** |
|
||
| P3 | 14 张采集表加 `style_id`(+ `style_match_method`)+ 索引;db 层 `attach_style_ids` / `resolve_style_ids_by_product` 统一兜底;`backfill_style_id.py` 回填历史 |
|
||
| P4 | ~~10 张「必须匹配」表加 `style_id → dim_style(id)` 物理外键~~ → 已按要求**全部拆除**,改逻辑外键(库里 0 个 FK,只留 14 个 `style_id` 索引);新增 `tests/modules/product_commerce/test_style_id_identity.py`(28 条契约测试) |
|
||
| P5 | 提供 `v_style_unmatched_product` 清单(dy 170 / jd 99 / tm 189),需业务侧补 spus |
|
||
|
||
### 回填结果(实库)
|
||
|
||
| 表 | 行数 | 有 style_id | 比例 |
|
||
|---|---|---|---|
|
||
| daily_style_metrics | 17,352 | 17,352 | 100% |
|
||
| weekly_style_metrics | 532 | 532 | 100% |
|
||
| erp_all_shop_style_daily_metrics | 9,399 | 9,399 | 100% |
|
||
| daily_persona_metrics | 2,357 | 2,357 | 100% |
|
||
| main_image_creatives | 9,094 | 9,094 | 100% |
|
||
| fact_style_analysis_reports | 465 | 465 | 100% |
|
||
| decline_events / fact_alert_history | 14 / 14 | 14 / 14 | 100% |
|
||
| persona_screenshots | 25 | 25 | 100% |
|
||
| jd_self_weekly_sales | 117 | 111 | 94.9%(6 款已不在 dim_style) |
|
||
| tm_product_ad_daily_metrics | 11,727 | 11,108 | 94.7% |
|
||
| jd_product_ad_daily_metrics | 33,808 | 24,707 | 73.1% |
|
||
| dy_product_ad_daily_metrics | 18,202 | 10,875 | 59.7% |
|
||
| product_daily_metrics | 75,568 | 37,098 | 49.1%(未建档商品 ID 所致) |
|
||
|
||
### 与方案不同之处
|
||
|
||
1. **天猫广告表已先行迁到独立表**:方案编写时天猫指标还写在
|
||
`product_daily_metrics.raw_data.tm_ad_metrics`,实施期间该迁移已完成,因此改为给
|
||
`tm_product_ad_daily_metrics` 加 `style_id`(并纳入 `v_style_id_coverage`),
|
||
并未改动 `raw_data` 节点。
|
||
2. **写入端没有逐个脚本改**:方案原计划在 13 个写入脚本里各自填充,实际收敛到 db 层
|
||
的 `attach_style_ids` / `resolve_style_ids_by_product`(一次批量查询),再加 loader
|
||
下发。少改 13 处、不会漏,代价是 upsert 多一次按款名/商品 ID 的查询。
|
||
3. **`dy_product_scraping.py` 默认流程的评论采集直接摘除**(仍保留 `--reviews-only`
|
||
显式入口),比方案建议的「保留但不再进 product.daily」更彻底。
|
||
4. **物理外键最终没有保留**:P4 建过的 10 个 `<表名>_style_id_fk` 已全部 DROP,
|
||
`schema.sql` 改为「幂等清理 + 只建索引」。`style_id` 与 `dim_style.id` 是纯逻辑
|
||
外键关系,理由见 P4 段(fail-soft 采集语义、款式可重建、与 data-hub 对齐)。
|
||
|
||
### 仍待处理
|
||
|
||
- **data-hub 侧 join 未切**(`ProductLifecycleMapper.xml` 6 处仍是 `metric.style_name = style.name`)。
|
||
现在两边都有 id,可以随时切;切换后是 bigint join 替代 text join。
|
||
- P5 补建档:458 个商品 ID(dy 170 / jd 99 / tm 189)写进 `dim_style.spus`。
|
||
- 既有红灯(与本次无关):`test_tm_brand_profiles` 的 backfill 用例、`shop_intelligence`
|
||
的 12 个 jd 竞店用例,改动前即失败。
|
||
|
||
## 九、P5 补 spus 实施记录(2026-11-01)
|
||
|
||
### 依据的权威字段
|
||
|
||
补 spus 的匹配依据不是商品标题,而是 **`product_daily_metrics.product_no` —— 它存的就是款名**:
|
||
|
||
| 平台 | `product_no` 填充率 | 可用性 |
|
||
|---|---:|---|
|
||
| 天猫 | 100%(29552/29552) | 权威 |
|
||
| 京东 | 97.8%(16664/17044) | 权威 |
|
||
| 抖音 | 0%(0/28972) | 只能靠标题 |
|
||
|
||
这一条此前被忽略,导致「458 个未建档」被误判为只能猜标题。实际天猫/京东 136 个商品
|
||
靠 `product_no` 就精确命中,另有 31 个靠前后缀近似命中(「蓝鹊pro」→「蓝鹊」、
|
||
「知更大号」→「知更」、「星迹」→「星迹2」)。
|
||
|
||
注意 `product_no` **不是** ERP 编码:`dim_style.erp_style_codes` 存的是 `["10510"]`
|
||
这种数字码,两者用 `product_no = erp_style_codes` 只能对上 2 个,别混淆。
|
||
|
||
### 匹配算法(四路,按优先级)
|
||
|
||
1. `product_no` 精确 = `style_name` → 直接写;
|
||
2. `product_no` 与款名互为前后缀且长度差 ≤ 3,且候选唯一 → 直接写;
|
||
3. 标题包含完整款名(抖音只能走这条)→ 用「命中位置后的版本后缀」防串款:
|
||
标题「盖亚斜挎包**3**」、`dim_style` 有「盖亚斜挎3」时必须指后者而非「盖亚斜挎」;
|
||
若该版本款没建档,判「版本款未建档」而不是硬归到基础款;
|
||
4. 编码与标题互相矛盾 → 进人工清单,**不自动写**。
|
||
|
||
实测第 4 类里最贵的一条:天猫 `649545919334` 商品编码写「理岩」、标题写「阿波罗air」,
|
||
支付额 **50.2 万**。猜错就是 50 万挂错款,只能人工定。
|
||
|
||
### 写入结果
|
||
|
||
| 项 | 数值 |
|
||
|---|---:|
|
||
| 写入商品 ID | 199 个 |
|
||
| 涉及款式 | 67 个 |
|
||
| 覆盖 `product_daily_metrics` 行 | 22,636 |
|
||
| 未写(物料 / 冲突待定) | 87 个 |
|
||
|
||
合并策略:`tm_spus / jd_spus / dy_spus` 取**并集追加**,不覆盖已有值;写前备份到
|
||
`var/tmp/dim_style_spus_backup_20261101.json`(204 行)。
|
||
|
||
### 覆盖率变化
|
||
|
||
| 表 | 补前 | 补后 |
|
||
|---|---:|---:|
|
||
| product_daily_metrics | 49.1% | **79.0%** |
|
||
| tm_product_ad_daily_metrics | 94.7% | **99.4%** |
|
||
| jd_product_ad_daily_metrics | 73.1% | **92.4%** |
|
||
| dy_product_ad_daily_metrics | 59.7% | **83.2%** |
|
||
|
||
广告表的提升来自新增的第三段回填(按商品 ID + 补空 `style_name`):京东广告里
|
||
32 个商品 ID 就占了 9,101 行空值,补完 `style_name` 空白从 9,095 降到 2,554,
|
||
抖音从 7,327 降到 3,062。这意味着 **data-hub 侧不改任何 SQL,按款名的 join 已经多认回
|
||
11,359 行**。
|
||
|
||
### 剩余 259 个未建档 ID 的构成
|
||
|
||
| 分类 | 商品数 | 支付额 | 处置 |
|
||
|---|---:|---:|---|
|
||
| 待建档-真款 | 128 | 6.9 万 | 需在 dim_style 新建款(星云系列链接、拾影单肩包、幻月、纪元、极地冰冠、阿瑞斯、盖亚S1、漫游者、光/暗系列等) |
|
||
| 待建档-ozko 线 | 20 | 0.2 万 | ozko 子品牌款(逐云、微笑系列、甜酷系列、银鸥、独角兽、HB、cdbb、761X…) |
|
||
| 白名单-物料配件 | 30 | 5.6 万 | 不该进 spus(内胆包、数据线、风扇、防丢器…) |
|
||
| 白名单-服务/券 | 4 | 0 | 补邮费、兑换券、补贴 |
|
||
| 冲突待定 | 10 | 62.9 万 | 编码与标题矛盾,需人工定 |
|
||
|
||
清单:`var/tmp/spus_todo.csv`(按分档 + 金额排序)。
|
||
|
||
### 新增的长期机制
|
||
|
||
- `backfill_style_id.py` 增加 `AD_PRODUCT_TABLES` 与 `_backfill_ad_tables_by_product`,
|
||
广告表按 `spu_id / product_id` 二次回填,并只在 `style_name` 为空时补权威款名
|
||
(`COALESCE(NULLIF(BTRIM(style_name), ''), %s)`,不覆盖平台给出的值)。
|
||
- 新增视图 `v_style_unmatched_source`:`product_daily_metrics` + 三张广告表统一口径,
|
||
补建档只看这一个视图就够(实测与原 `v_style_unmatched_product` 数值一致,
|
||
说明没有纯投流商品只出现在广告表)。
|