## 改动 - 支撑客户端比价结算页:补 `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>
15 KiB
数据库总览 — 表 × 功能 × 关系
跨表视角。单表字段级细节看同目录
<表名>.md(索引见 README)。 本文专门回答三件「跨表」的事:① 每块 App 功能用到哪些表 ② 什么操作往哪张表写 ③ 表和表怎么连(join key,含没有外键约束、靠业务字段对齐的语义关联)。 范围:业务表全部在shaguabijia-app-server(SQLAlchemy 2.0 + SQLite 开发 / PostgreSQL 生产)。pricebot-backend(比价/领券 Agent)是纯内存态、无任何表;Android 客户端只有 EncryptedSharedPreferences / SharedPreferences、无关系库。共 28 张业务表 +alembic_version(框架的迁移版本指针)。领券联动的「今日状态」三张表(coupon_*)同理:领券过程在 pricebot 内存态跑、不落库,只有结果回到 app-server 才落这三张表。
一、按功能域看表(每张表用在 App 哪里)
比价(核心业务)
| App 位置 / 动作 | 表 | 说明 |
|---|---|---|
| 比价/领券过程(看屏→决策→操作) | (无) | 在 pricebot-backend 内存态跑,过程不落库;只有结果回到 app-server 才落库 |
| 「我的比价记录」列表 / 详情 | comparison_record |
每次比价 done 后客户端带 JWT 上报一条完整明细 |
| 比价战绩里程碑(逐档领金币) | comparison_milestone_claim |
累计成功比价 N 次解锁;进度读 comparison_record 计数 |
| profile「累计省了 / 省钱战绩 / 省钱明细」 | savings_record |
真实下单归因(source=compare)+ 无真实数据时 demo 兜底 |
| 「上报更低价」提交 / 列表 | price_report |
众包纠偏:用户举证某平台更便宜,人工审核发奖 |
领券(每日领券联动 · 今日状态)
| App 位置 / 动作 | 表 | 说明 |
|---|---|---|
| 领券过程(看屏→领券) | (无) | 在 pricebot-backend 内存态跑,过程不落库;结果回 app-server 才落下面三张表 |
| 切外卖 App 时是否弹领券引导窗 | coupon_prompt_engagement |
今天 engage 过(点领/点拒)就不再弹;判断维度 device_id |
| 首页「去领取」卡是否置灰 | coupon_daily_completion |
今天跑完整轮(到 done)就置灰;判断维度 device_id |
| 每张券领取结果留痕 | coupon_claim_record |
资产/画像/排查/CPS;当前不参与判断 |
钱包 / 福利(看广告赚钱闭环)
| App 位置 / 动作 | 表 | 说明 |
|---|---|---|
| 资产卡 / 钱包余额 | coin_account |
一用户一行的金币+现金余额快照 |
| 金币明细 | coin_transaction |
每次金币变动一笔流水 |
| 现金明细 | cash_transaction |
每次现金变动一笔流水(分) |
| 每日签到 | signin_record + signin_boost_record |
14 天循环发币;签到后看广告可膨胀一次 |
| 一次性任务(开消息提醒等) | user_task |
领一次发币 |
| 看激励视频赚金币 | ad_reward_record + ad_watch_log + ad_ecpm_record |
独立数据流:发奖 / 旧版观看时长 / 收益对账 |
| 信息流广告结算 | ad_feed_reward_record |
每展示满 10 秒累计一份奖励,完成后一次性入账 |
| 金币兑现金 | coin_account + coin_transaction + cash_transaction |
exchange_out + exchange_in 两笔流水 |
| 提现到微信零钱 | withdraw_order + wechat_transfer_authorization + cash_transaction |
人工审核 + 微信商家转账 |
| 绑定微信(提现前置) | user.wechat_* |
openid 唯一,一微信一账号 |
账号 / 反馈
| App 位置 / 动作 | 表 | 说明 |
|---|---|---|
| 登录(极光/短信)/ 改资料 / 注销 | user |
登录主体,注册即登录 |
| 新手引导是否再展示 | onboarding_completion |
按 设备+账号 去重;登录响应回 onboarding_completed,走完引导时标记,跨卸载重装 |
| 帮助与反馈 | feedback |
含截图,后台人工处理 |
运营后台 admin(独立子应用 app/admin/,端口 8771,独立鉴权)
| 后台模块 | 表 | 说明 |
|---|---|---|
| 管理员账号 / 登录 | admin_user |
与 C 端 user 完全隔离,独立 JWT + RBAC |
| 操作审计 | admin_audit_log |
每个写操作落一条,只增不改不删 |
| 运营可配置项(改奖励常量) | app_config |
空表 = 用代码默认;后台改了即覆盖 |
| 用户/钱包/提现/反馈管理 | 跨读写上面的 C 端表 | 见下「写入路径」admin 段 |
二、写入路径(什么操作 → 写哪张表)
C=插入新行 · U=更新已有行 · 同一行内多张表表示同一事务原子写。
C 端(App 用户触发)
| 触发(用户动作 / endpoint / 回调) | 写入 | 操作 |
|---|---|---|
登录 POST /auth/jverify-login、/auth/sms/login |
user |
C(首次=注册)/ U(last_login_at);并读 onboarding_completion 回 onboarding_completed |
走完新手引导 POST /user/onboarding/complete |
onboarding_completion(C) |
(user_id, device_id) 幂等,撞唯一约束即忽略 |
改昵称 PATCH /user/profile、传头像 POST /user/avatar |
user |
U |
注销 DELETE /user |
user |
U(软删:phone→deleted_<id>、status=deleted) |
绑/解绑微信 POST /wallet/bind-wechat、/unbind-wechat |
user.wechat_* |
U |
签到 POST /signin/do |
signin_record(C) + coin_account(U) + coin_transaction(C signin) |
同事务 |
签到膨胀 POST /signin/boost |
signin_boost_record(C) + coin_account(U) + coin_transaction(C signin_boost) |
同事务;同日一次 |
领任务 POST /tasks/claim |
user_task(C) + coin_account(U) + coin_transaction(C task_<key>) |
同事务 |
金币兑现金 POST /wallet/exchange |
coin_account(U) + coin_transaction(C exchange_out −) + cash_transaction(C exchange_in +) |
同事务 |
发起提现 POST /wallet/withdraw |
withdraw_order(C reviewing) + coin_account(U 扣现金) + cash_transaction(C withdraw −) |
同事务,不打款 |
查提现状态 / 用户取消 GET /wallet/withdraw/status |
withdraw_order(U) + 失败→cash_transaction(C withdraw_refund +) |
|
穿山甲发奖 S2S 回调 POST /ad/pangle-callback |
ad_reward_record(C)+ granted→coin_account(U)+coin_transaction(C reward_video/signin_boost) |
trans_id 幂等 |
看广告时长上报 POST /ad/watch-report |
ad_watch_log(C) |
|
广告 eCPM 上报 POST /ad/ecpm-report |
ad_ecpm_record(C) |
|
信息流广告结算 POST /ad/feed-reward |
ad_feed_reward_record(C)+ granted→coin_account(U)+coin_transaction(C feed_ad_reward) |
client_event_id 幂等 |
比价 done 上报 POST /compare/record |
comparison_record(C 或 U) |
(user_id, trace_id) 幂等覆盖 |
领里程碑 POST /compare/milestone/claim |
comparison_milestone_claim(C) |
当前不发币(coin_awarded=0) |
支付归因上报 POST /order/report |
savings_record(C source=compare) |
(user_id, client_event_id) 幂等 |
| 首次进 profile 省钱页且无真实记录 | savings_record(C source=demo) |
懒种子,ensure_seeded 按 user 幂等 |
上报更低价 POST /report |
price_report(C) |
读 comparison_record.best_price_cents 校验 |
提交反馈 POST /feedback |
feedback(C) |
|
领券首帧 POST /api/v1/coupon/step(step=0) |
coupon_prompt_engagement(C/U claim_started) |
(device_id, 北京日) 幂等;best-effort |
领券每帧结果 POST /api/v1/coupon/step |
coupon_claim_record(C/U) |
(device_id, coupon_id, 北京日) 幂等;best-effort |
领券跑完 POST /api/v1/coupon/step(action.command=done) |
coupon_daily_completion(C/U) |
(device_id, 北京日) 幂等;best-effort |
拒绝领券引导窗 POST /api/v1/coupon/prompt/dismiss |
coupon_prompt_engagement(C/U dismissed) |
同上;客户端通知(透传链路看不到拒绝) |
重置今日弹窗 POST /api/v1/coupon/prompt/reset(开发) |
coupon_prompt_engagement(D 今日条) |
删后今天又能弹 |
领券三表写库全 best-effort:
/coupon/step里写失败只logger.warning、不连累领券返回;判断只看device_id,user_id可空旁路(资产留痕)。
admin 端(管理员触发,均额外写一条 admin_audit_log)
| 后台操作 | 写入 | 操作 |
|---|---|---|
| 手动增减金币 | coin_account(U)+coin_transaction(C admin_grant/admin_deduct)+admin_audit_log(C) |
同事务 |
| 改用户状态(禁用/启用) | user(U)+admin_audit_log(C) |
同事务 |
| 审核通过提现 | withdraw_order(U→pending/success/failed)+wechat_transfer_authorization(C/U)+失败时cash_transaction(refund)+admin_audit_log(C) |
|
| 审核拒绝提现 | withdraw_order(U→rejected)+cash_transaction(C withdraw_refund)+admin_audit_log(C) |
|
| 处理反馈 | feedback(U)+admin_audit_log(C) |
同事务 |
| 改运营配置 | app_config(C/U)+admin_audit_log(C) |
同事务 |
| 任意写操作 | admin_audit_log(C,永不 U/D) |
没有任何表会被业务流程物理 DELETE。注销是软删(改 user 行),其余只 C/U。
三、表间关系 & Join Key
硬外键(数据库 FK 约束)
- 17 张用户维度表
.user_id→user.id:coin_account(同时是 PK)、coin_transaction、cash_transaction、withdraw_order、wechat_transfer_authorization(同时是 PK)、signin_record、signin_boost_record、user_task、comparison_record、comparison_milestone_claim、savings_record、ad_reward_record、ad_watch_log、ad_ecpm_record、ad_feed_reward_record、price_report、feedback。 admin_audit_log.admin_id→admin_user.id。price_report.comparison_record_id→comparison_record.id(可空:关联记录被删后仍留上报历史)。
语义 join key(无 FK 约束,靠业务字段对齐 —— 排障/对账必看)
-
coin_transaction.ref_id指向随biz_type变:biz_type ref_id 指向 amount 符号 signin当天日期串(= signin_record.signin_date的 ISOYYYY-MM-DD)+ signin_boost当天日期串(= signin_boost_record.signin_date的 ISOYYYY-MM-DD)+ task_<key>user_task.task_key+ reward_video/ad_reward(历史)ad_reward_record.trans_id+ feed_ad_rewardad_feed_reward_record.client_event_id+ exchange_outnull(兑现金,无单据) − admin_grant/admin_deductnull(原因记在 remark=admin:<reason>)+ / − -
cash_transaction.ref_id:biz_type ref_id 指向 amount 符号 withdraw/withdraw_refundwithdraw_order.out_bill_no− / + exchange_innull + -
comparison_record.store_name≈savings_record.shop_name:无 id 关联,按店名字符串相等给比价记录打「已下单」标记(瞬态,不写库)。两边店名同源 = 比价意图识别阶段的门店 query,语义=店级(同店比价多次会一并标已下单)。 -
广告流会话关联:
ad_reward_record.ad_session_id可与ad_ecpm_record.ad_session_id对齐;ad_watch_log仍是旧版兼容统计,不逐条参与发奖。 -
里程碑解锁进度不存库:
comparison_milestone_claim只记「哪几档已领」;进度 =comparison_record里status='success'的count。 -
领券三表无硬 FK,全靠软关联:
coupon_prompt_engagement/coupon_daily_completion/coupon_claim_record的user_id软指user.id(可空、有登录态才记、不进唯一键、不阻塞判断);trace_id软指 pricebot work_logs(排查回指);唯一键都以device_id+ 北京自然日为主(详见coupon_state.md)。 -
onboarding_completion.(user_id, device_id):user_id语义关联user.id(无硬 FK,同coupon_*设备表),device_id= 客户端硬件级ANDROID_ID(≠ 领券 per-installdevice_id)。登录读、走完引导写,决定是否再展示新手引导。
ER 关系(文字版)
user ─1:1─ coin_account
user ─1:1─ wechat_transfer_authorization
user ─1:N─ { coin_transaction, cash_transaction, withdraw_order, signin_record,
signin_boost_record, user_task, comparison_record, comparison_milestone_claim,
savings_record, ad_reward_record, ad_watch_log, ad_ecpm_record, ad_feed_reward_record,
price_report, feedback }
user ─1:N─ onboarding_completion (user_id, 无硬 FK; (user_id,device_id) 去重)
comparison_record ─1:N─ price_report (comparison_record_id, 可空)
admin_user ─1:N─ admin_audit_log
app_config (独立, 无外键, key 为主键)
coupon_prompt_engagement / coupon_daily_completion / coupon_claim_record
(独立, 无硬 FK; 维度=device_id+北京日, user_id/trace_id 仅软关联)
四、资金模型(金币 / 现金 / 提现,三层)
- 余额快照
coin_account:coin_balance(金币个数)+cash_balance_cents(现金分),一用户一行,读取展示用。 - 流水账本
coin_transaction/cash_transaction:每次变动写一笔,balance_after*记变动后余额,可逐笔回溯对账。 - 唯一发金币入口
repositories/wallet.grant_coins:更新快照 + 写流水,不 commit,由调用方在同一事务里 commit(保证"记录"和"加币"原子化)。signin / signin_boost / task / ad_reward / feed_ad_reward / exchange / admin 都走它,靠biz_type区分来源。
- 汇率:
10000 金币 = 1 元 = 100 分(rewards.COIN_PER_YUAN);兑换额必须是整分倍数。 - 提现状态机:
reviewing(发起即原子扣现金、待人工审核、不打款)→ 审核通过pending(微信转账在途)→success/failed(失败自动退款);审核拒绝rejected(退款)。扣款/退款都写cash_transaction,out_bill_no幂等,孤儿 pending 单由reconcile_pending_withdraws对账兜底。 - 防超额:扣现金用带条件
UPDATE ... WHERE cash_balance_cents >= amount,并发/重试不会双扣。
五、通用约定
见 README → 通用约定:金额存整数(金币计数 / 现金存分)、业务「今天」按北京时间日期串等值聚合(不在 SQL 跨时区比 date)、JSON 列 PG 用 JSONB·SQLite 退化 JSON、改表必写 alembic 迁移并保持单 head + 同步更新本目录文档。