fix(compare): 「已下单」从店级改按 trace_id 精确对齐 #224

Merged
guke merged 2 commits from fix/compare-ordered-attribution-by-trace-id into main 2026-08-07 18:16:57 +08:00
Member

背景 / 问题
比价记录页「已下单」tab 混进大量本不该出现的记录:同一家店比价多次、只下过一单,却把这家店的每一条比价(含失败灰卡、没真正下单的成功比价)都塞进「已下单」。

根因
「已下单」原先是店级判定——下单上报(POST /order/report)不带 trace_id、savings_record 也没有指向具体某次比价的键,服务端只能用 savings_record.shop_name == comparison_record.store_name 做店名匹配 → 同店所有比价一并命中。属"先有省钱记账、后蹭已下单"的历史债(见 docs/database/savings_record.md 的 Join Key 注释)。

方案
把 trace_id 从比价会话打通到下单上报,「已下单」改为按 trace_id 精确对齐到那一条比价:

下单上报带上本次比价 trace_id → 落 savings_record.trace_id;
「已下单」过滤/打标(C 端 + admin)统一改成 savings_record.trace_id == comparison_record.trace_id(comparison 侧 trace_id 本就是 NOT NULL UNIQUE);
不做店名回退、不回填历史:没有 trace_id 的订单(历史/老客户端)对齐不上任何记录 → 不进「已下单」。

行为变化(评审必看)
上线后「已下单」只反映"更新版客户端下的新单":历史订单 + 老客户端过渡期订单没 trace_id,不再进「已下单」,随客户端铺量回血。
失败/终止比价不再进「已下单」(本就没真正下单;trace_id 只会指向有价可点的成功比价)。
后台看板「下单数」会掉:历史 savings_record 全是 trace_id=NULL,period_ordered_count 上线当天明显下跌——是口径变化、非回归,请知会看数据的同学。
必须与客户端一起上:只上服务端 → 订单永远不带 trace_id → 任何订单都对不上 → 「已下单」永远空。见关联 android PR。

背景 / 问题 比价记录页「已下单」tab 混进大量本不该出现的记录:同一家店比价多次、只下过一单,却把这家店的每一条比价(含失败灰卡、没真正下单的成功比价)都塞进「已下单」。 根因 「已下单」原先是店级判定——下单上报(POST /order/report)不带 trace_id、savings_record 也没有指向具体某次比价的键,服务端只能用 savings_record.shop_name == comparison_record.store_name 做店名匹配 → 同店所有比价一并命中。属"先有省钱记账、后蹭已下单"的历史债(见 docs/database/savings_record.md 的 Join Key 注释)。 方案 把 trace_id 从比价会话打通到下单上报,「已下单」改为按 trace_id 精确对齐到那一条比价: 下单上报带上本次比价 trace_id → 落 savings_record.trace_id; 「已下单」过滤/打标(C 端 + admin)统一改成 savings_record.trace_id == comparison_record.trace_id(comparison 侧 trace_id 本就是 NOT NULL UNIQUE); 不做店名回退、不回填历史:没有 trace_id 的订单(历史/老客户端)对齐不上任何记录 → 不进「已下单」。 行为变化(评审必看) 上线后「已下单」只反映"更新版客户端下的新单":历史订单 + 老客户端过渡期订单没 trace_id,不再进「已下单」,随客户端铺量回血。 失败/终止比价不再进「已下单」(本就没真正下单;trace_id 只会指向有价可点的成功比价)。 后台看板「下单数」会掉:历史 savings_record 全是 trace_id=NULL,period_ordered_count 上线当天明显下跌——是口径变化、非回归,请知会看数据的同学。 必须与客户端一起上:只上服务端 → 订单永远不带 trace_id → 任何订单都对不上 → 「已下单」永远空。见关联 android PR。
guke added 2 commits 2026-08-07 13:56:54 +08:00
下单上报带上本次比价的 trace_id 落 savings_record.trace_id,「已下单」筛选/打标改为
按 trace_id 精确对齐 comparison_record.trace_id —— 同店多次比价只标真正下单那条,失败/
未下单记录不再混入。没有 trace_id 的订单(历史/老客户端)不进「已下单」,不做回退/回填。

- savings_record 加 trace_id 列 + 索引 + Alembic 迁移
- OrderReportRequest 加 trace_id;create_from_report 落库
- comparison.list_records 过滤/打标 + helpers 改按 trace_id(删店名匹配)
- admin 列表/详情/看板下单数同口径改 trace_id
- 迁移既有测试到新语义 + 新增 test_records_ordered_by_trace_id

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
老客户端/历史订单不带 trace_id,即使店名相同也不该标已下单。补这条回归测试,
防止将来有人把店名回退悄悄加回来。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Member

🤖 review-pr 深审结论

🟢 看着没问题(置信度 0.88)。服务端把「已下单」从店级(店名匹配)改为按 trace_id 精确对齐单次比价:新增 savings_record.trace_id 列 + 迁移,上报落库,读取端(C 端分页 / admin queries / dashboard 统计)全部切到 trace_id。与 Android #396 契约字段名一致,闭环。

契约核对(合并 main 后)

  • schemas/order.py OrderReportRequest.trace_id(≤64)↔ Android #396 上报体 put("trace_id", ...) 字段名一致 ✓
  • ComparisonRecord.trace_id非空必填(compare/record 无 trace_id 会 422),故 dashboard_overview 去掉 store_name.is_not(None) 守卫、改用 SavingsRecord.trace_id == ComparisonRecord.trace_id 的 EXISTS 是安全的 ✓
  • alembic heads → 单 head savings_record_trace_id(down_revision=comparison_updated_at,无兄弟迁移撞车、无多头)✓

测试/构建 已实跑(合并 main 后 worktree,venv Py3.12)

  • pytest tests/test_compare_record.py tests/test_admin_read.py43 passed
  • pytest tests/test_admin_roles.py tests/test_cps_admin.py21 passed
  • 新增 2 测精确锁定契约:同店多次比价只标真正下单那条;无 trace_id 订单不回退店名匹配

issues:无 high/med。

  • note(需产品知悉) 迁移 nullable 无回填:历史订单 + 老客户端订单(trace_id=NULL)不再标「已下单」。作者已在迁移 docstring 明确「不做回填」,是本次「店级→单次级」的既定取舍。上线需与 App #396 协同发布——老 App 未更新期间新下单不带 trace_id,「已下单」率会临时下降。

正面:契约闭环干净;保留了原「只查本页 trace_id、不全量捞回内存」的性能写法(避免随下单量线性变慢 + SQLite 绑定变量上限);注释把 demo/老客户端/历史三种 NULL 情形都讲清了。

建议:与 App #396 协同上线;product 知悉历史「已下单」会因无回填而消失(如需保留可另做一次性回填脚本,但会牺牲精确性,通常不必)。

## 🤖 review-pr 深审结论 🟢 **看着没问题**(置信度 0.88)。服务端把「已下单」从店级(店名匹配)改为按 `trace_id` 精确对齐单次比价:新增 `savings_record.trace_id` 列 + 迁移,上报落库,读取端(C 端分页 / admin queries / dashboard 统计)全部切到 trace_id。与 Android #396 契约字段名一致,闭环。 **契约核对(合并 main 后)** - `schemas/order.py` `OrderReportRequest.trace_id`(≤64)↔ Android #396 上报体 `put("trace_id", ...)` 字段名一致 ✓ - `ComparisonRecord.trace_id` 为 **非空**必填(`compare/record` 无 trace_id 会 422),故 `dashboard_overview` 去掉 `store_name.is_not(None)` 守卫、改用 `SavingsRecord.trace_id == ComparisonRecord.trace_id` 的 EXISTS 是安全的 ✓ - `alembic heads` → 单 head `savings_record_trace_id`(down_revision=`comparison_updated_at`,无兄弟迁移撞车、无多头)✓ **测试/构建**:✅ 已实跑(合并 main 后 worktree,venv Py3.12) - `pytest tests/test_compare_record.py tests/test_admin_read.py` → **43 passed** - `pytest tests/test_admin_roles.py tests/test_cps_admin.py` → **21 passed** - 新增 2 测精确锁定契约:同店多次比价只标真正下单那条;**无 trace_id 订单不回退店名匹配**。 **issues**:无 high/med。 - **note(需产品知悉)** 迁移 nullable **无回填**:历史订单 + 老客户端订单(`trace_id=NULL`)不再标「已下单」。作者已在迁移 docstring 明确「不做回填」,是本次「店级→单次级」的既定取舍。上线需与 App #396 **协同发布**——老 App 未更新期间新下单不带 trace_id,「已下单」率会临时下降。 **正面**:契约闭环干净;保留了原「只查本页 trace_id、不全量捞回内存」的性能写法(避免随下单量线性变慢 + SQLite 绑定变量上限);注释把 demo/老客户端/历史三种 NULL 情形都讲清了。 建议:与 App #396 协同上线;product 知悉历史「已下单」会因无回填而消失(如需保留可另做一次性回填脚本,但会牺牲精确性,通常不必)。
guke merged commit 3663b5b11c into main 2026-08-07 18:16:57 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: WonderableAI/shaguabijia-app-server#224