Files
shaguabijia-app-server/docs/database/OVERVIEW.md
T
liujiahui 47812f7fcc 比价结算记录 schema + comparison 模型 (#44)
## 改动
- 支撑客户端比价结算页:补 `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>
2026-06-14 04:16:52 +08:00

15 KiB
Raw Blame History

数据库总览 — 表 × 功能 × 关系

跨表视角。单表字段级细节看同目录 <表名>.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_completiononboarding_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_iduser.id:coin_account(同时是 PK)、coin_transactioncash_transactionwithdraw_orderwechat_transfer_authorization(同时是 PK)、signin_recordsignin_boost_recorduser_taskcomparison_recordcomparison_milestone_claimsavings_recordad_reward_recordad_watch_logad_ecpm_recordad_feed_reward_recordprice_reportfeedback
  • admin_audit_log.admin_idadmin_user.id
  • price_report.comparison_record_idcomparison_record.id(可空:关联记录被删后仍留上报历史)。

语义 join key(无 FK 约束,靠业务字段对齐 —— 排障/对账必看)

  • coin_transaction.ref_id 指向随 biz_type:

    biz_type ref_id 指向 amount 符号
    signin 当天日期串(= signin_record.signin_date 的 ISO YYYY-MM-DD) +
    signin_boost 当天日期串(= signin_boost_record.signin_date 的 ISO YYYY-MM-DD) +
    task_<key> user_task.task_key +
    reward_video / ad_reward(历史) ad_reward_record.trans_id +
    feed_ad_reward ad_feed_reward_record.client_event_id +
    exchange_out null(兑现金,无单据)
    admin_grant / admin_deduct null(原因记在 remark=admin:<reason>) + /
  • cash_transaction.ref_id:

    biz_type ref_id 指向 amount 符号
    withdraw / withdraw_refund withdraw_order.out_bill_no / +
    exchange_in null +
  • comparison_record.store_namesavings_record.shop_name:无 id 关联,按店名字符串相等给比价记录打「已下单」标记(瞬态,不写库)。两边店名同源 = 比价意图识别阶段的门店 query,语义=店级(同店比价多次会一并标已下单)。

  • 广告流会话关联:ad_reward_record.ad_session_id 可与 ad_ecpm_record.ad_session_id 对齐;ad_watch_log 仍是旧版兼容统计,不逐条参与发奖。

  • 里程碑解锁进度不存库:comparison_milestone_claim 只记「哪几档已领」;进度 = comparison_recordstatus='success'count

  • 领券三表无硬 FK,全靠软关联:coupon_prompt_engagement / coupon_daily_completion / coupon_claim_recorduser_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-install device_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 仅软关联)

四、资金模型(金币 / 现金 / 提现,三层)

  1. 余额快照 coin_account:coin_balance(金币个数)+ cash_balance_cents(现金分),一用户一行,读取展示用。
  2. 流水账本 coin_transaction / cash_transaction:每次变动写一笔,balance_after* 记变动后余额,可逐笔回溯对账。
  3. 唯一发金币入口 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 + 同步更新本目录文档。