Reviewed-on: #114
8.0 KiB
comparison_record — 比价记录(每次比价完整明细)
模型
app/models/comparison.py· 仓库app/repositories/comparison.py· 接口 compare-record-report / compare-records / compare-record-detail · ← 索引 · 总览
每完成一次比价(外卖/电商/领券)记一行。写入以 app-server 后端 harvest 为主(2026-07 起):比价透传壳 compare.py 在帧0(pricebot 出 trace_id)即建 running 行,随 done / trace/finalize 逐步补全成终态,客户端不再主动 POST 记录;老客户端仍可走带 JWT 的 POST /compare/record 兜底(灰度期两条写路径按 trace_id reconcile)。App「我的比价记录」列表/详情的数据源,也是比价战绩里程碑解锁进度的计数源(status='success' 条数),还被「上报更低价」反查原最低价。
与
savings_record的区别:本表是「每一次比价行为的完整明细」(不省钱、甚至失败也记);savings_record是「真正下单成交省了多少」。两表独立、互不喂数据。 与price_observation/store_mapping的区别:本表是用户视角(登录后按user_id存「我的比价记录」);后两张是 server 侧无条件沉淀的平台/门店视角客观事实(价格事实 / 跨平台店铺身份映射),与本表trace_id同源但不互相 join,各存各的视角。
用在哪 / 增删改查
- C / U(harvest 为主,按
trace_id幂等):透传壳compare.py三段式落库(app/repositories/comparison.py)——harvest_running(帧0 建running行)→harvest_done(done 帧转success/failed+ 派生best_*/saved_amount_cents+ 返newly_success供幂等发邀请奖)→harvest_abort(trace/finalize转cancelled/failed,不降级已 success)。老客户端仍可POST /compare/record(upsert_record,按trace_id查、整行覆盖)兜底。best_*/saved_amount_cents/is_source_best/status一律由_derive从comparison_results算出(协议已按 price 升序、rank=1 最便宜),不信客户端自算。 - D:无(关联的
price_report也只把comparison_record_id置空,不删本表)。 - R:
GET /compare/records(列表,created_at倒序游标 + 「已下单」标记)、GET /compare/records/{id}(详情,限本人);count_success给里程碑;get_stats(status='success'计数 +saved_amount_cents求和)给GET /compare/stats喂「我的」页省钱战绩卡(完成比价 + 累计发现可省,比价口径);report.py反查best_*;admin 大盘/明细。
字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
id |
Integer | PK, autoincrement | 被 price_report.comparison_record_id 引用 |
user_id |
Integer | FK→user.id, index, nullable(2026-07 从 NOT NULL 放开) | 归属用户。后端 harvest 帧0 建行时(软鉴权 / 老客户端匿名)可能暂缺 → 可空;C 端「我的比价记录」按 user_id 过滤天然排除 null 行,admin 全看(含孤儿行) |
device_id |
String(64) | nullable | 设备号(多设备区分 / 与不鉴权期对账) |
business_type |
String(16) | NOT NULL, default food, index |
取值:food(当前唯一接通)/ ecom / coupon |
trace_id |
String(64) | NOT NULL, UNIQUE | 一次比价的唯一标识。由 app-server 帧0 用 uuid 签发(注入转发 body + 回填响应顶层给客户端;老客户端自带),全局唯一 = harvest upsert 键 + 关联 pricebot 调试落盘 |
trace_url |
String(512) | nullable | 本次比价公网调试链接(price.shaguabijia.com/traces/{dir}/);dir 名含 pricebot 落盘时分秒,前端/server 拼不出必须存。查看接口按 user.debug_trace_enabled(或本机 agent 调试 include_trace)决定返不返回。旧记录 / 未开上云为 null |
source_platform_id / _name |
String(32) | nullable | 源平台代号 / 中文名 |
source_package |
String(128) | nullable | 源平台 Android 包名 |
source_price_cents |
Integer | nullable | 源平台到手价(分) |
best_platform_id / _name |
String(32) | nullable | 最优平台代号 / 中文名(= rank=1) |
best_price_cents |
Integer | nullable | 最优价(分)。被 price_report 反查作"原最低价",校验上报价必须更低 |
best_deeplink |
String(1024) | nullable | 最优平台商家深链(再次比价/直达用);2026-06 起新比价才有,旧记录 null |
saved_amount_cents |
Integer | nullable | 源价 − 最优价(可 0/负:源平台本就最便宜) |
is_source_best |
Boolean | nullable | 源平台就是最便宜(= 这次没省到) |
store_name |
String(128) | nullable | 店铺名。与 savings_record.shop_name 按字符串相等关联,给本记录打「已下单」 |
total_dish_count / skipped_dish_count |
Integer | nullable | 菜品总数 / 目标平台没找到被跳过数 |
status |
String(16) | NOT NULL, default success |
取值:running(harvest 帧0 建行、比价进行中)/ success(有非源且有价的目标结果)/ failed(出错/没采到目标价)/ cancelled(用户终止 / Phase1 未识别,harvest_abort 写,不降级已 success)。里程碑只数 success |
information |
String(256) | nullable | done 帧文案;成功=摘要,失败=具体原因(前端失败时当原因展示) |
items |
JSON(PG: JSONB) | NOT NULL, default [] | 下单菜品 [{name, qty, specs?}] |
comparison_results |
JSON(PG: JSONB) | NOT NULL, default [] | 逐平台对比 [{platform_id,platform_name,package,price(元),is_source,rank,coupon_saved(元),coupon_name,applied_coupons}];coupon_saved=该平台主优惠额(美团红包/淘宝平台红包/京东百亿补贴,只取一笔),coupon_name=优惠来源名(展示用),applied_coupons=[{name,amount}] 多券明细 |
skipped_dish_names |
JSON(PG: JSONB) | NOT NULL, default [] | 被跳过的菜名 |
raw_payload |
JSON(PG: JSONB) | nullable | 客户端原始上报全量(calibration + done.params),取数兜底 |
input_tokens |
Integer | nullable | 本次 LLM 累计输入 token = Σ llm_calls[].usage.prompt_tokens(server 收上报后从 llm_calls 累加;旧记录/未采集为 null) |
output_tokens |
Integer | nullable | 本次 LLM 累计输出 token = Σ llm_calls[].usage.completion_tokens(同上) |
created_at |
DateTime(tz) | server_default now(), index | 时间 |
ordered(已下单)是瞬态字段,不在表里:list_records读取时按store_name ∈ 该用户 source='compare' 的 savings_record.shop_name 集合现挂到实例上供出参用。
关系 / Join Key
user_id→user.id(多对一)。- ← 被
price_report.comparison_record_id引用(选中某条比价记录上报更低价);price_report同时读本表best_price_cents/best_platform_*/store_name/items做快照与校验。 - ≈
savings_record(语义 join,无 FK):store_name==savings_record.shop_name(source='compare')→ 标「已下单」,店级。 - 被
comparison_milestone_claim间接依赖:解锁进度 = 本表status='success'计数。
索引与约束
- PK
id;indexuser_id、business_type、created_at;复合 indexix_comparison_status_created(status,created_at)(按status='success'过滤 + 近期排序的聚合/轮播,避免随数据量退化为全表扫);UNIQUE(trace_id) =uq_comparison_trace(harvest 按它 upsert,一次比价一行)。2026-07 迁移
comparison_record_trace_unique:唯一键从复合uq_comparison_user_trace(user_id,trace_id)改为trace_id单列——harvest 帧0 建行时 user_id 可能暂缺,不能再用复合键去重。上线前须确认历史无重复trace_id(SELECT trace_id,COUNT(*) c FROM comparison_record GROUP BY trace_id HAVING c>1),否则建单列唯一会失败。
注意
- 4 个 JSON 列用
JSON().with_variant(JSONB(),"postgresql")(SQLite 退化 JSON)。结构化金额列存「分」,comparison_results.price/coupon_saved原样存「元」。