356d6e52b9
签到加成(boost): - 新增 signin_boost_record 表(user+date 唯一)+ POST /api/v1/signin/boost - signin 状态/结算逻辑调整 信息流广告结算: - 新增 ad_feed_reward_record 表(client_event_id 幂等)+ POST /api/v1/ad/feed-reward - ad_feed_reward 模型/repo、ad schema、按 ecpm/时长结算金币 金币数值:core/rewards.py 数值调整、config_schema/admin config 适配 迁移 coin_reward_phase1(down_revision pstat3growth);测试 + API/DB 文档同步 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
12 KiB
Markdown
154 lines
12 KiB
Markdown
# 数据库总览 — 表 × 功能 × 关系
|
||
|
||
> 跨表视角。单表字段级细节看同目录 `<表名>.md`(索引见 [README](./README.md))。
|
||
> 本文专门回答三件「跨表」的事:**① 每块 App 功能用到哪些表 ② 什么操作往哪张表写 ③ 表和表怎么连(join key,含没有外键约束、靠业务字段对齐的语义关联)**。
|
||
> **范围**:业务表全部在 `shaguabijia-app-server`(SQLAlchemy 2.0 + SQLite 开发 / PostgreSQL 生产)。`pricebot-backend`(比价/领券 Agent)是纯内存态、**无任何表**;Android 客户端只有 EncryptedSharedPreferences / SharedPreferences、**无关系库**。共 **22 张业务表** + `alembic_version`(框架的迁移版本指针)。
|
||
|
||
---
|
||
|
||
## 一、按功能域看表(每张表用在 App 哪里)
|
||
|
||
### 比价(核心业务)
|
||
| App 位置 / 动作 | 表 | 说明 |
|
||
|---|---|---|
|
||
| 比价/领券**过程**(看屏→决策→操作) | (无) | 在 pricebot-backend 内存态跑,**过程不落库**;只有结果回到 app-server 才落库 |
|
||
| 「我的比价记录」列表 / 详情 | [`comparison_record`](./comparison_record.md) | 每次比价 done 后客户端带 JWT 上报一条完整明细 |
|
||
| 比价战绩里程碑(逐档领金币) | [`comparison_milestone_claim`](./comparison_milestone_claim.md) | 累计成功比价 N 次解锁;进度读 `comparison_record` 计数 |
|
||
| profile「累计省了 / 省钱战绩 / 省钱明细」 | [`savings_record`](./savings_record.md) | 真实下单归因(source=compare)+ 无真实数据时 demo 兜底 |
|
||
| 「上报更低价」提交 / 列表 | [`price_report`](./price_report.md) | 众包纠偏:用户举证某平台更便宜,人工审核发奖 |
|
||
|
||
### 钱包 / 福利(看广告赚钱闭环)
|
||
| App 位置 / 动作 | 表 | 说明 |
|
||
|---|---|---|
|
||
| 资产卡 / 钱包余额 | [`coin_account`](./coin_account.md) | 一用户一行的金币+现金余额快照 |
|
||
| 金币明细 | [`coin_transaction`](./coin_transaction.md) | 每次金币变动一笔流水 |
|
||
| 现金明细 | [`cash_transaction`](./cash_transaction.md) | 每次现金变动一笔流水(分) |
|
||
| 每日签到 | [`signin_record`](./signin_record.md) + [`signin_boost_record`](./signin_boost_record.md) | 14 天循环发币;签到后看广告可膨胀一次 |
|
||
| 一次性任务(开消息提醒等) | [`user_task`](./user_task.md) | 领一次发币 |
|
||
| 看激励视频赚金币 | [`ad_reward_record`](./ad_reward_record.md) + [`ad_watch_log`](./ad_watch_log.md) + [`ad_ecpm_record`](./ad_ecpm_record.md) | 独立数据流:发奖 / 旧版观看时长 / 收益对账 |
|
||
| 信息流广告结算 | [`ad_feed_reward_record`](./ad_feed_reward_record.md) | 每展示满 10 秒累计一份奖励,完成后一次性入账 |
|
||
| 金币兑现金 | `coin_account` + `coin_transaction` + `cash_transaction` | exchange_out + exchange_in 两笔流水 |
|
||
| 提现到微信零钱 | [`withdraw_order`](./withdraw_order.md) + [`wechat_transfer_authorization`](./wechat_transfer_authorization.md) + `cash_transaction` | 人工审核 + 微信商家转账 |
|
||
| 绑定微信(提现前置) | `user`.wechat_* | openid 唯一,一微信一账号 |
|
||
|
||
### 账号 / 反馈
|
||
| App 位置 / 动作 | 表 | 说明 |
|
||
|---|---|---|
|
||
| 登录(极光/短信)/ 改资料 / 注销 | [`user`](./user.md) | 登录主体,注册即登录 |
|
||
| 帮助与反馈 | [`feedback`](./feedback.md) | 含截图,后台人工处理 |
|
||
|
||
### 运营后台 admin(独立子应用 `app/admin/`,端口 8771,独立鉴权)
|
||
| 后台模块 | 表 | 说明 |
|
||
|---|---|---|
|
||
| 管理员账号 / 登录 | [`admin_user`](./admin_user.md) | 与 C 端 `user` 完全隔离,独立 JWT + RBAC |
|
||
| 操作审计 | [`admin_audit_log`](./admin_audit_log.md) | 每个写操作落一条,只增不改不删 |
|
||
| 运营可配置项(改奖励常量) | [`app_config`](./app_config.md) | 空表 = 用代码默认;后台改了即覆盖 |
|
||
| 用户/钱包/提现/反馈管理 | 跨读写上面的 C 端表 | 见下「写入路径」admin 段 |
|
||
|
||
---
|
||
|
||
## 二、写入路径(什么操作 → 写哪张表)
|
||
|
||
> C=插入新行 · U=更新已有行 · 同一行内多张表表示**同一事务原子写**。
|
||
|
||
### C 端(App 用户触发)
|
||
| 触发(用户动作 / endpoint / 回调) | 写入 | 操作 |
|
||
|---|---|---|
|
||
| 登录 `POST /auth/jverify-login`、`/auth/sms/login` | `user` | C(首次=注册)/ U(`last_login_at`) |
|
||
| 改昵称 `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 `ad_reward`) | `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) | |
|
||
|
||
### 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` 的 ISO `YYYY-MM-DD`) | + |
|
||
| `signin_boost` | 当天日期串(= `signin_boost_record.signin_date` 的 ISO `YYYY-MM-DD`) | + |
|
||
| `task_<key>` | `user_task.task_key` | + |
|
||
| `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_name` ≈ `savings_record.shop_name`**:无 id 关联,按**店名字符串相等**给比价记录打「已下单」标记(瞬态,不写库)。两边店名同源 = 比价意图识别阶段的门店 query,语义=**店级**(同店比价多次会一并标已下单)。
|
||
- **广告流互不关联**:`ad_reward_record` / `ad_watch_log` / `ad_ecpm_record` / `ad_feed_reward_record` 之间**无公共键**,各自只按 `(user_id, 日期串)` 聚合。别试图 join 它们逐条对应。
|
||
- **里程碑解锁进度不存库**:`comparison_milestone_claim` 只记「哪几档已领」;进度 = `comparison_record` 里 `status='success'` 的 `count`。
|
||
|
||
### 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 }
|
||
comparison_record ─1:N─ price_report (comparison_record_id, 可空)
|
||
admin_user ─1:N─ admin_audit_log
|
||
app_config (独立, 无外键, key 为主键)
|
||
```
|
||
|
||
---
|
||
|
||
## 四、资金模型(金币 / 现金 / 提现,三层)
|
||
|
||
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 → 通用约定](./README.md#通用约定):金额存整数(金币计数 / 现金存分)、业务「今天」按北京时间日期串等值聚合(不在 SQL 跨时区比 date)、JSON 列 PG 用 JSONB·SQLite 退化 JSON、改表必写 alembic 迁移并保持单 head + 同步更新本目录文档。
|