47812f7fcc
## 改动 - 支撑客户端比价结算页:补 `compare_record` schema 字段 + `comparison` 模型调整 - docs/database 索引合并(领券三表 + onboarding_completion,表数对齐 28) ## 验证 - 模型/schema parse OK;已 merge origin/main 解 OVERVIEW/README 文档冲突(取并集) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: no_gen_mu <liujianhishen@gmail.com> Reviewed-on: #44 Co-authored-by: liujiahui <liujiahui@wonderable.ai> Co-committed-by: liujiahui <liujiahui@wonderable.ai>
10 KiB
10 KiB
coupon_state — 领券今日状态三张表(弹窗频控 / 首页置灰 / 领券记录)
模型
app/models/coupon_state.py· 仓库app/repositories/coupon_state.py· 接口app/api/v1/coupon.py(prefix/api/v1/coupon) · ← 索引 · 总览
领券(优惠券自动化)联动产生的三张「今日状态」表,都挂在领券透传端点 POST /api/v1/coupon/step 这条链路上(pricebot 跑领券,结果回 app-server 落库;领券过程本身在 pricebot 内存态跑、不落库)。三表各管一件事:
coupon_prompt_engagement— 弹窗频控源。按(device, 自然日)记「今天是否对领券引导窗表达过意向」(点「一键领取」=claim_started/ 点拒绝关闭=dismissed都算)。切到外卖 App 时据此决定弹不弹:今天 engage 过就不再弹。coupon_daily_completion— 首页置灰源。按(device, 自然日)记「今天是否已跑完整轮领券(到 done 帧)」。首页「去领取」卡据此置灰:今天跑完了就不能再领。coupon_claim_record— 资产沉淀层。按(device, 券, 自然日)记每张券的领取结果(success/already_claimed/failed/skipped),纯沉淀(资产/画像/排查/CPS 归因),当前不参与「要不要领 / 弹不弹」的判断。
三表共同口径:
- 判断维度是
device_id,不是user_id:券发到的是设备上登录的那个外卖账号,device 比 user 更贴近「哪个登录环境」,且device_id全链路现成、不依赖领券鉴权(领券 MVP 阶段/coupon/step不鉴权)。客户端getOrCreateDeviceId生成存 SP,卸载重装会变 → 当新设备重新弹一次(产品预期)。 - 日期 =
Asia/Shanghai自然日(claim_date/engage_date/complete_date,repositories/coupon_state.today_cn())。每日可领的券(签到/天天红包)靠这天然每天一条。 user_id可空:领券登录态有就记(资产/画像),可空、不进唯一键、不阻塞判断。trace_id可空:回指 pricebot work_logs,供排查(哪次任务领的)。- engagement vs completion vs claim 的区别:engagement = 用户表达过意向(点了领或拒,不管跑没跑完);completion = 这一轮真跑到了 done(整套流程走完);claim = 每张券一条的结果留痕。
写库全部 best-effort:在
/coupon/step里写库失败只logger.warning、绝不连累领券主流程/返回(见app/api/v1/coupon.py)。
coupon_prompt_engagement — 弹窗频控(今日是否已对引导窗表达意向)
(device_id, engage_date) 唯一,一台设备一天一条;今天 engage 过(领或拒)就不再弹。
用在哪 / 增删改查
- C / U(幂等 upsert):
mark_engagement。两条触发:POST /api/v1/coupon/step且step==0(领券首帧=用户已发起领券)→ 记claim_started;POST /api/v1/coupon/prompt/dismiss(用户点关闭引导窗;server 在透传链路看不到「拒绝」,必须客户端通知)→ 记dismissed。- 已有今天那条则覆盖
engage_type(并补user_id),否则插入。
- D:
POST /api/v1/coupon/prompt/reset(reset_today_engagement)—— 删这台设备今天那条,开发设置「重置今日领券弹窗状态」按钮调,测频控用;删后今天又能弹。 - R:
GET /api/v1/coupon/prompt/should-show?device_id=…(has_engaged_today)→should_show = not 今天已 engage。客户端切外卖 App 前查,纯后台判据。
字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
id |
Integer | PK, autoincrement | |
device_id |
String(64) | NOT NULL | 判断/聚合维度;客户端 getOrCreateDeviceId,重装会变 |
user_id |
Integer | index, 可空 | 登录态有就记(资产);不进唯一键、不阻塞判断 |
engage_date |
Date | NOT NULL | 北京时间自然日(today_cn()) |
engage_type |
String(16) | NOT NULL | claim_started(点一键领取)/ dismissed(点拒绝关闭);仅记录区分,判断只看「今天有没有这条」,type 不影响弹不弹 |
created_at |
DateTime(tz) | server_default now() | |
updated_at |
DateTime(tz) | server_default now(), onupdate now() |
索引与约束
- PK
id;indexuser_id;UNIQUE(device_id,engage_date) =uq_coupon_engage_device_date(一台设备一天一条)。
注意
device_id重装会变 → 重装当新设备,今天重新弹一次(产品预期)。- 判断只看「今天这台设备有没有这条」,不看
engage_type(领或拒都算 engage 过、都不再弹)。
coupon_daily_completion — 首页置灰(今日是否已跑完整轮领券)
(device_id, complete_date) 唯一,一台设备一天一条;今天跑完整轮(到 done 帧)就把首页「去领取」卡置灰。
用在哪 / 增删改查
- C / U(幂等 upsert):
mark_completed_today,由POST /api/v1/coupon/step在 pricebot 返回action.command == "done"那帧调。pricebot 把中途单券 done 改写成wait+continue=true,只有整套全跑完那帧才保留command=="done",故 done 已等价「整轮完成」。已有今天那条则补user_id/trace_id,否则插入。 - U / D:无业务删除。
- R:
GET /api/v1/coupon/completed-today?device_id=…(has_completed_today)→completed。客户端据此把首页「去领取」卡置灰、不可点。
字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
id |
Integer | PK, autoincrement | |
device_id |
String(64) | NOT NULL | 判断维度,与 engagement/claim 一致;客户端两端都用 ANDROID_ID |
user_id |
Integer | index, 可空 | 登录态有就记(资产) |
complete_date |
Date | NOT NULL | 北京时间自然日(today_cn()) |
trace_id |
String(64) | index, 可空 | 哪次任务跑到 done,回指 pricebot work_logs / 排查 |
created_at |
DateTime(tz) | server_default now() | |
updated_at |
DateTime(tz) | server_default now(), onupdate now() |
索引与约束
- PK
id;indexuser_id、trace_id;UNIQUE(device_id,complete_date) =uq_coupon_completion_device_date(一台设备一天一条)。
注意
- 口径(用户决策 2026-06-10 A 方案):到 done 即算完成,不管单券成败——失败/跳过常是无障碍/环境问题,重复点也补不回来。
- 与 engagement 区别:engagement 是「表达过意向」(点了就记,不管跑没跑完);completion 是「真跑到了 done」。
coupon_claim_record — 领券记录(每张券一天一条,资产沉淀层)
(device_id, coupon_id, claim_date) 唯一,同设备同券同一天只一条;纯沉淀,当前不参与判断,留作以后按券去重 / CPS 归因 / 用户画像的数据源。
用在哪 / 增删改查
- C / U(幂等 upsert):
record_claims,由POST /api/v1/coupon/step写入。一帧的券结果来自 pricebot 的last_coupon_result(最后一张)+action.params.coupon_results(全量)——会重复带同一张券,端点_extract_coupon_results先按coupon_id去重(全量覆盖单张),仓库再靠唯一键幂等:已有则更新status/reason/claimed_count/extra(以最后一次为准),否则插入。 - U / D:无业务删除。
- R:当前无读取端点(纯写入沉淀,未来做去重/归因/画像时再用)。
字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
id |
Integer | PK, autoincrement | |
device_id |
String(64) | NOT NULL | 聚合维度;客户端 getOrCreateDeviceId,重装会变 |
user_id |
Integer | index, 可空 | 登录态有就记(资产/画像);不进唯一键 |
coupon_id |
String(64) | NOT NULL | 券标识(取自 pricebot 结果) |
claim_date |
Date | NOT NULL | 北京时间自然日(today_cn());每日可领的券靠它天然每天一条 |
status |
String(24) | NOT NULL | success / already_claimed / failed / skipped(原样取 pricebot coupon 结果) |
vendor |
String(48) | 可空 | 券提供方 |
coupon_name |
String(128) | 可空 | 取 pricebot name |
claimed_count |
Integer | 可空 | 这张领到几张(pricebot display_count,给不出时 None;兼容 claimed_count) |
trace_id |
String(64) | index, 可空 | 哪次任务领的,回指 pricebot work_logs / 排查 |
reason |
String(255) | 可空 | failed / skipped 原因 |
extra |
JSON(PG JSONB) | 可空 | 杂项兜底:券的结构化信息(面额/入口/关键节点摘要等),免得加字段就迁移;当前直接存这帧 pricebot 单券结果 dict |
created_at |
DateTime(tz) | server_default now() | |
updated_at |
DateTime(tz) | server_default now(), onupdate now() |
索引与约束
- PK
id;indexuser_id、trace_id;UNIQUE(device_id,coupon_id,claim_date) =uq_coupon_claim_device_coupon_date;Index(device_id,claim_date) =ix_coupon_claim_device_date(按 device+日 取一天所有券)。
注意
extra别塞原始无障碍树(几十 KB → 行膨胀);原始大树看trace_id指过去的 work_logs。- 同批去重很关键:
autoflush=False下同coupon_id两次add会撞唯一约束、IntegrityError回滚整批(done/单券记录全丢),故端点_extract_coupon_results+ 仓库seen集合双重防御。 extra是JSON().with_variant(JSONB(), "postgresql"):PG 用 JSONB(可建 GIN 索引),SQLite 退化通用 JSON(同price_observation/comparison_record)。
三表共性小结
- 数据流向:客户端 →
POST /api/v1/coupon/step(透传给 pricebot)→ 结果回写这三张表(best-effort,写库失败不影响领券)。 - 唯一键都含
device_id+ 某个北京自然日列;user_id永远是可空旁路(资产留痕,不进唯一键、不阻塞判断)。 - 无硬外键:
user_id软指user.id、trace_id软指 pricebot work_logs(详见 OVERVIEW → 表间关系 & Join Key)。