# 款式主键统一 + 评论采集下线 方案 > 状态:**已实施(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:"` 字符串里,没有作为独立字段下发给写入脚本。把它提成正式字段,全链路就都有了。 ## 二、现状盘点 ### 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` 数值一致, 说明没有纯投流商品只出现在广告表)。