From bb58fb2a299fd0fb99a4c7901f78fc674a9e4ed9 Mon Sep 17 00:00:00 2001 From: OuYingJun1024 <1034284404@qq.com> Date: Sun, 31 May 2026 20:44:54 +0800 Subject: [PATCH] =?UTF-8?q?docs(database):=20=E6=96=B0=E5=BB=BA=20docs/dat?= =?UTF-8?q?abase=20=E8=A1=A8=E6=96=87=E6=A1=A3=20=E2=80=94=2013=20?= =?UTF-8?q?=E5=BC=A0=E8=A1=A8=E5=85=A8=E9=87=8F=20+=20=E7=B4=A2=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按"一表一文件"建库表文档(协作规范第 11 条):字段(类型/约束/默认)+ 索引与约束 + 关系 + 说明,从 model 的 Mapped 定义扒准。 - README 索引:13 张表总览 + 通用约定(主键/外键/金额分/北京时间日期串/JSONB variant) - 覆盖 user / coin_account / coin_transaction / cash_transaction / withdraw_order / signin_record / user_task / savings_record / ad_reward_record / ad_ecpm_record / feedback / comparison_record / comparison_milestone_claim - savings_record 文档强调"成交省钱"≠"比价行为",与 comparison_record 互不喂数据 Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/database/README.md | 37 ++++++++++++++++ docs/database/ad_ecpm_record.md | 26 ++++++++++++ docs/database/ad_reward_record.md | 28 ++++++++++++ docs/database/cash_transaction.md | 26 ++++++++++++ docs/database/coin_account.md | 24 +++++++++++ docs/database/coin_transaction.md | 27 ++++++++++++ docs/database/comparison_milestone_claim.md | 26 ++++++++++++ docs/database/comparison_record.md | 47 +++++++++++++++++++++ docs/database/feedback.md | 25 +++++++++++ docs/database/savings_record.md | 31 ++++++++++++++ docs/database/signin_record.md | 26 ++++++++++++ docs/database/user.md | 32 ++++++++++++++ docs/database/user_task.md | 26 ++++++++++++ docs/database/withdraw_order.md | 30 +++++++++++++ 14 files changed, 411 insertions(+) create mode 100644 docs/database/README.md create mode 100644 docs/database/ad_ecpm_record.md create mode 100644 docs/database/ad_reward_record.md create mode 100644 docs/database/cash_transaction.md create mode 100644 docs/database/coin_account.md create mode 100644 docs/database/coin_transaction.md create mode 100644 docs/database/comparison_milestone_claim.md create mode 100644 docs/database/comparison_record.md create mode 100644 docs/database/feedback.md create mode 100644 docs/database/savings_record.md create mode 100644 docs/database/signin_record.md create mode 100644 docs/database/user.md create mode 100644 docs/database/user_task.md create mode 100644 docs/database/withdraw_order.md diff --git a/docs/database/README.md b/docs/database/README.md new file mode 100644 index 0000000..818a70b --- /dev/null +++ b/docs/database/README.md @@ -0,0 +1,37 @@ +# 傻瓜比价 App 后端 — 数据库表文档(索引) + +> 数据库:SQLite 起步(`data/app.db`),生产可切 PostgreSQL(改 `DATABASE_URL`)。 +> ORM:SQLAlchemy 2.0(`app/models/`),迁移:Alembic(`alembic/versions/`,`render_as_batch` 兼容 SQLite)。 +> 金额字段一律存**整数**:金币=个数,现金=**分**(`*_cents`)。时间列 `DateTime(timezone=True)`。 +> 最后更新:2026-05-31 + +--- + +## 表总览 + +| 表 | 用途 | 模型 | 关联模块 | 文档 | +|---|---|---|---|---| +| `user` | 用户(登录主体) | `models/user.py` | 登录/鉴权 | [详情](./user.md) | +| `coin_account` | 金币+现金余额快照(一用户一行) | `models/wallet.py` | 钱包 | [详情](./coin_account.md) | +| `coin_transaction` | 金币流水账本 | `models/wallet.py` | 钱包 | [详情](./coin_transaction.md) | +| `cash_transaction` | 现金流水账本(分) | `models/wallet.py` | 钱包/提现 | [详情](./cash_transaction.md) | +| `withdraw_order` | 提现单(现金→微信零钱) | `models/wallet.py` | 提现 | [详情](./withdraw_order.md) | +| `signin_record` | 签到记录(7 天循环) | `models/signin.py` | 签到 | [详情](./signin_record.md) | +| `user_task` | 一次性任务领取去重 | `models/task.py` | 任务 | [详情](./user_task.md) | +| `savings_record` | 省钱记录(profile 省钱战绩源) | `models/savings.py` | 省钱 | [详情](./savings_record.md) | +| `ad_reward_record` | 看激励视频发奖记录(S2S 回调) | `models/ad_reward.py` | 看广告发奖 | [详情](./ad_reward_record.md) | +| `ad_ecpm_record` | 广告展示 eCPM 上报(收益对账) | `models/ad_ecpm.py` | 看广告 | [详情](./ad_ecpm_record.md) | +| `feedback` | 用户帮助与反馈 | `models/feedback.py` | 反馈 | [详情](./feedback.md) | +| `comparison_record` | 比价记录(每次比价完整明细) | `models/comparison.py` | 比价记录 | [详情](./comparison_record.md) | +| `comparison_milestone_claim` | 比价战绩里程碑领取记录 | `models/comparison_milestone.py` | 比价记录/福利 | [详情](./comparison_milestone_claim.md) | + +--- + +## 通用约定 + +- **主键**:`id` Integer autoincrement(`coin_account` 例外:`user_id` 既是主键也是外键,一用户一行)。 +- **外键**:所有用户维度表 `user_id` → `user.id`,且建 index。 +- **金额**:整数;金币计数,现金/价格存「分」(`*_cents`)。 +- **时间**:`created_at` 等用 `DateTime(timezone=True)` + `server_default=func.now()`;业务"今天"按**北京时间**(见 `core/rewards.cn_today`),跨天计数用「日期串」列(如 `reward_date`/`report_date`)等值查,不在 SQL 里做跨时区 date 比较(SQLite 不可靠)。 +- **JSON 列**:用 `JSON().with_variant(JSONB(), "postgresql")` —— PG 上 JSONB(可建 GIN 索引),SQLite 退化为通用 JSON(否则 `create_all` 编译报错)。 +- **迁移**:改表必写 alembic 迁移并保持单 head;改表/建表同时更新本目录对应文档(一表一文件)。 diff --git a/docs/database/ad_ecpm_record.md b/docs/database/ad_ecpm_record.md new file mode 100644 index 0000000..4464e36 --- /dev/null +++ b/docs/database/ad_ecpm_record.md @@ -0,0 +1,26 @@ +# ad_ecpm_record — 广告展示 eCPM 上报(收益对账) + +> 模型 `app/models/ad_ecpm.py` | 关联接口 [ad-ecpm-report](../api/ad-ecpm-report.md) | [← 表索引](./README.md) + +每条 = 客户端一次广告展示(`onAdShow`)后读到的 eCPM。与发奖记录 `ad_reward_record` 是**两条独立数据流**(发奖走 S2S 有 trans_id 无 ecpm;eCPM 走客户端有 ecpm 无 trans_id),无公共键,只用于**按用户/按天聚合**收益对账,不做逐条精确关联。穿山甲后台报表才是结算权威。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | +| `ad_type` | String(32) | NOT NULL | 广告类型:`reward_video`(激励视频)/ `draw`(Draw 信息流)等 | +| `adn` | String(32) | nullable | 实际投放 ADN(`getSdkName`,如 pangle/gdt) | +| `slot_id` | String(64) | nullable | 实际展示代码位(底层 mediation rit) | +| `ecpm_raw` | String(32) | NOT NULL | 客户端上报的 eCPM 原始串(单位待确认,原样存) | +| `report_date` | String(10) | index, NOT NULL | 北京时间日期串 `YYYY-MM-DD`,按它做按天聚合 | +| `created_at` | DateTime(tz) | server_default now(), index | 时间 | + +## 索引与约束 +- PK: `id`;index: `user_id`、`report_date`、`created_at` + +## 关系 +- `user_id` → `user.id`(多对一) + +## 说明 +- ⚠️ `ecpm_raw` 单位(分/元)截至 2026-05-31 未最终确认;确认后再加一列解析好的数值,在此之前对账按"待定单位"处理。 diff --git a/docs/database/ad_reward_record.md b/docs/database/ad_reward_record.md new file mode 100644 index 0000000..14e0fff --- /dev/null +++ b/docs/database/ad_reward_record.md @@ -0,0 +1,28 @@ +# ad_reward_record — 看激励视频发奖记录(S2S 回调) + +> 模型 `app/models/ad_reward.py` | 关联接口 [ad-pangle-callback](../api/ad-pangle-callback.md) / [ad-reward-status](../api/ad-reward-status.md) | [← 表索引](./README.md) + +每条 = 穿山甲一次发奖回调。`trans_id` 唯一做幂等键(穿山甲会重试,同号只发一次)。`reward_date`(北京时间日期串)给"每日上限"计数用,按日期串等值查。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `trans_id` | String(64) | UNIQUE, index, NOT NULL | 穿山甲交易号(幂等键) | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户(回调 media_extra 带回) | +| `coin` | Integer | NOT NULL, default 0 | 实发金币(capped 时为 0) | +| `status` | String(16) | NOT NULL, default `granted` | `granted`(已发)/ `capped`(当日超限未发) | +| `reward_date` | String(10) | index, NOT NULL | 北京时间日期串 `YYYY-MM-DD`,按它统计当日次数 | +| `reward_name` | String(64) | nullable | 穿山甲上报奖励名(参考,不作发奖依据) | +| `raw` | String(1024) | nullable | 回调原始参数(审计排查) | +| `created_at` | DateTime(tz) | server_default now(), index | 时间 | + +## 索引与约束 +- PK: `id`;UNIQUE + index: `trans_id`;index: `user_id`、`reward_date`、`created_at` + +## 关系 +- `user_id` → `user.id`(多对一) + +## 说明 +- 三道闸:验签不过 403 → `trans_id` 唯一幂等(并发 catch IntegrityError)→ 当日次数 ≥ `DAILY_AD_REWARD_LIMIT` 记 `capped` 不发币。 +- 发币复用 `grant_coins(biz_type='ad_reward', ref_id=trans_id)`。 diff --git a/docs/database/cash_transaction.md b/docs/database/cash_transaction.md new file mode 100644 index 0000000..87e096d --- /dev/null +++ b/docs/database/cash_transaction.md @@ -0,0 +1,26 @@ +# cash_transaction — 现金流水账本(分) + +> 模型 `app/models/wallet.py` | 关联接口 [wallet-cash-transactions](../api/wallet-cash-transactions.md) | [← 表索引](./README.md) + +现金每次变动一笔流水(单位:分)。金币兑现金、提现、提现退款都记这里。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | +| `amount_cents` | Integer | NOT NULL | 正=入账(兑入),负=出账(提现) | +| `balance_after_cents` | Integer | NOT NULL | 本笔变动后现金余额(分) | +| `biz_type` | String(32) | NOT NULL | `exchange_in`(兑入)/ `withdraw`(提现出账)/ `withdraw_refund`(提现退回) | +| `ref_id` | String(64) | nullable | 关联业务 id(如提现 `out_bill_no`) | +| `remark` | String(128) | nullable | 用户可见备注(如"提现未成功,金额已退回") | +| `created_at` | DateTime(tz) | server_default now(), index | 时间 | + +## 索引与约束 +- PK: `id`;index: `user_id`、`created_at` + +## 关系 +- `user_id` → `user.id`(多对一) + +## 说明 +- 提现失败/取消退款写 `withdraw_refund`(+X);退款流水 `remark` 是用户可见文案,技术原因记在 `withdraw_order.fail_reason`。 diff --git a/docs/database/coin_account.md b/docs/database/coin_account.md new file mode 100644 index 0000000..c039328 --- /dev/null +++ b/docs/database/coin_account.md @@ -0,0 +1,24 @@ +# coin_account — 金币 + 现金余额快照 + +> 模型 `app/models/wallet.py` | 关联接口 [wallet-account](../api/wallet-account.md) | [← 表索引](./README.md) + +一个用户一行的余额快照,供读取展示用;每次余额变动都另写流水(`coin_transaction` / `cash_transaction`)并记 `balance_after`,出问题逐笔回溯。`user_id` 既是主键也是外键。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `user_id` | Integer | PK, FK→user.id | 用户(一用户一行) | +| `coin_balance` | Integer | NOT NULL, default 0 | 当前金币余额 | +| `cash_balance_cents` | Integer | NOT NULL, default 0 | 当前现金余额(分) | +| `total_coin_earned` | Integer | NOT NULL, default 0 | 累计赚取金币(只增不减) | +| `updated_at` | DateTime(tz) | server_default now(), onupdate now() | 最后更新时间 | + +## 索引与约束 +- PK: `user_id`(同时是 FK→user.id) + +## 关系 +- `user_id` → `user.id`(一对一) + +## 说明 +- **唯一发金币入口** `wallet.grant_coins` 更新本表余额快照 + 写 `coin_transaction`,不 commit,由调用方同事务提交。 +- 提现扣现金用带条件 `UPDATE ... WHERE cash_balance_cents >= amount` 原子扣减,防并发超额。 diff --git a/docs/database/coin_transaction.md b/docs/database/coin_transaction.md new file mode 100644 index 0000000..3ffb76a --- /dev/null +++ b/docs/database/coin_transaction.md @@ -0,0 +1,27 @@ +# coin_transaction — 金币流水账本 + +> 模型 `app/models/wallet.py` | 关联接口 [wallet-coin-transactions](../api/wallet-coin-transactions.md) | [← 表索引](./README.md) + +金币每次变动一笔流水,记变动后余额,用于对账与「金币明细」展示。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | +| `amount` | Integer | NOT NULL | 正=入账(赚),负=出账(花/兑换) | +| `balance_after` | Integer | NOT NULL | 本笔变动后金币余额(对账用) | +| `biz_type` | String(32) | NOT NULL | 业务类型:`signin` / `task_` / `exchange_out` / `ad_reward` / `compare_milestone` … | +| `ref_id` | String(64) | nullable | 关联业务 id(签到日期 / 任务 key / trans_id / 里程碑序号等) | +| `remark` | String(128) | nullable | 备注 | +| `created_at` | DateTime(tz) | server_default now(), index | 时间 | + +## 索引与约束 +- PK: `id`;index: `user_id`、`created_at` + +## 关系 +- `user_id` → `user.id`(多对一) + +## 说明 +- 明细接口按 `id` 倒序游标分页。 +- 各 `biz_type` 取值由各业务写入(签到/任务/兑换/看广告/比价里程碑),无独立枚举约束,靠写入方约定。 diff --git a/docs/database/comparison_milestone_claim.md b/docs/database/comparison_milestone_claim.md new file mode 100644 index 0000000..7d626c3 --- /dev/null +++ b/docs/database/comparison_milestone_claim.md @@ -0,0 +1,26 @@ +# comparison_milestone_claim — 比价战绩里程碑领取记录 + +> 模型 `app/models/comparison_milestone.py` | 关联接口 [compare-milestones](../api/compare-milestones.md) / [compare-milestone-claim](../api/compare-milestone-claim.md) | [← 表索引](./README.md) + +「记录比价战绩」每档(第 1~6 次)只能领一次,领取后写一行;解锁进度由 `comparison_record` 里 `status='success'` 的条数决定,本表只记"哪几档已领"。仿 `user_task` 的一次性领取模型。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | +| `milestone` | Integer | NOT NULL | 档位序号(1-based),见 `rewards.RECORD_MILESTONES` | +| `coin_awarded` | Integer | NOT NULL, default 0 | 该档发放金币 | +| `claimed_at` | DateTime(tz) | server_default now() | 领取时间 | + +## 索引与约束 +- PK: `id`;index: `user_id` +- UNIQUE(`user_id`, `milestone`) = `uq_compare_milestone_user`(防同档重复领) + +## 关系 +- `user_id` → `user.id`(多对一) +- 解锁进度依赖 `comparison_record`(`status='success'` 计数),本表不存进度本身。 + +## 说明 +- 发金币走 `wallet.grant_coins(biz_type='compare_milestone', ref_id=str(milestone))`,与本表写入同事务。 +- 领取校验:档位越界 404、未解锁(成功数 < milestone)409、已领 409。 diff --git a/docs/database/comparison_record.md b/docs/database/comparison_record.md new file mode 100644 index 0000000..c9f3c4c --- /dev/null +++ b/docs/database/comparison_record.md @@ -0,0 +1,47 @@ +# comparison_record — 比价记录(每次比价完整明细) + +> 模型 `app/models/comparison.py` | 关联接口 [compare-record-report](../api/compare-record-report.md) / [compare-records](../api/compare-records.md) / [compare-record-detail](../api/compare-record-detail.md) | [← 表索引](./README.md) + +每完成一次比价(外卖/电商/领券),客户端 done 帧后用带 JWT 通道上报一条。「我的比价记录」页数据源,也是比价战绩里程碑解锁进度的计数源(`status='success'` 条数)。 + +> 与 `savings_record` 的区别:本表是「每一次**比价行为**的完整明细」(不省钱、甚至失败也记);`savings_record` 是「真正**下单成交**省了多少」。两表独立、互不喂数据。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | +| `device_id` | String(64) | nullable | 设备号 | +| `business_type` | String(16) | NOT NULL, default `food`, index | `food`(当前唯一接通)/ `ecom` / `coupon` | +| `trace_id` | String(64) | NOT NULL | pricebot 侧 trace_id(关联调试落盘 + 幂等键) | +| `source_platform_id` | String(32) | nullable | 源平台代号 | +| `source_platform_name` | String(32) | nullable | 源平台中文名 | +| `source_package` | String(128) | nullable | 源平台 Android 包名 | +| `source_price_cents` | Integer | nullable | 源平台到手价(分) | +| `best_platform_id` | String(32) | nullable | 最优平台代号(= rank=1) | +| `best_platform_name` | String(32) | nullable | 最优平台中文名 | +| `best_price_cents` | Integer | nullable | 最优价(分) | +| `saved_amount_cents` | Integer | nullable | 源价 − 最优价(可 0/负) | +| `is_source_best` | Boolean | nullable | 源平台就是最便宜(= 没省到) | +| `store_name` | String(128) | nullable | 店铺名 | +| `total_dish_count` | Integer | nullable | 菜品总数 | +| `skipped_dish_count` | Integer | nullable | 跳过(没找到)菜品数 | +| `status` | String(16) | NOT NULL, default `success` | `success`(拿到有效对比)/ `failed`(出错/没采到目标价) | +| `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}]` | +| `skipped_dish_names` | JSON(PG: JSONB) | NOT NULL, default [] | 被跳过的菜名 | +| `raw_payload` | JSON(PG: JSONB) | nullable | 客户端原始上报(calibration + done.params 全量) | +| `created_at` | DateTime(tz) | server_default now(), index | 时间 | + +## 索引与约束 +- PK: `id`;index: `user_id`、`business_type`、`created_at` +- UNIQUE(`user_id`, `trace_id`) = `uq_comparison_user_trace`(同次比价重试/重复上报幂等覆盖) + +## 关系 +- `user_id` → `user.id`(多对一) +- 被 `comparison_milestone_claim` 间接依赖:里程碑解锁进度 = 本表 `status='success'` 条数。 + +## 说明 +- `best_*` / `saved_amount_cents` / `is_source_best` / `status` 由 `repositories/comparison.py:_derive` 从 `comparison_results` 派生,客户端不用自己算。 +- 4 个 JSON 列用 `JSON().with_variant(JSONB(),"postgresql")`(SQLite 退化 JSON)。金额结构化列存「分」,`comparison_results.price` 原样存「元」。 diff --git a/docs/database/feedback.md b/docs/database/feedback.md new file mode 100644 index 0000000..6df7219 --- /dev/null +++ b/docs/database/feedback.md @@ -0,0 +1,25 @@ +# feedback — 用户帮助与反馈 + +> 模型 `app/models/feedback.py` | 关联接口 [feedback](../api/feedback.md) | [← 表索引](./README.md) + +每条 = 用户一次反馈提交。`content` 与 `contact` 必填,`images` 为可选截图 URL 列表。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 提交用户 | +| `content` | Text | NOT NULL | 反馈正文 | +| `contact` | String(128) | NOT NULL | 联系方式(微信/QQ/手机,便于回访) | +| `images` | JSON | nullable | 截图 URL 列表(相对路径 `/media/feedback/...`);无图为 NULL | +| `status` | String(16) | NOT NULL, default `new` | `new`(待处理)/ `handled`(已处理) | +| `created_at` | DateTime(tz) | server_default now(), index | 提交时间 | + +## 索引与约束 +- PK: `id`;index: `user_id`、`created_at` + +## 关系 +- `user_id` → `user.id`(多对一) + +## 说明 +- `images` 用通用 `JSON`(本表未用 JSONB variant);截图先经 `/media/feedback/` 上传拿到相对路径再随反馈提交。 diff --git a/docs/database/savings_record.md b/docs/database/savings_record.md new file mode 100644 index 0000000..a30bbfd --- /dev/null +++ b/docs/database/savings_record.md @@ -0,0 +1,31 @@ +# savings_record — 省钱记录(profile 省钱战绩源) + +> 模型 `app/models/savings.py` | 关联接口 [savings-summary](../api/savings-summary.md) / [savings-battle](../api/savings-battle.md) / [savings-records](../api/savings-records.md) | [← 表索引](./README.md) + +profile 页「累计帮你省了 / 省钱战绩 / 省钱明细」的唯一数据源:**真正下单成交后**省了多少记一行。 + +> ⚠️ **当前为 demo 假数据**:`crud/savings.py:ensure_seeded` 按 user_id 幂等灌 ~23 条(`source='demo'`),聚合(SUM/分组/连续天数)是生产级真实计算。真数据要靠**"用户真下单"信号**(app 目前无:AI 比价止于结算页/结果展示,付款用户手动)——**不是**把比价记录 `comparison_record` 喂过来(那是"比价行为",这是"成交省钱",两个维度)。详见 [[project_shaguabijia_app_server]]。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | +| `order_amount_cents` | Integer | NOT NULL | 订单到手价(分) | +| `saved_amount_cents` | Integer | NOT NULL | 本单省下(分,可为 0) | +| `platform` | String(32) | nullable | 下单平台(美团外卖/淘宝闪购/京东外卖) | +| `title` | String(128) | nullable | 标题 | +| `shop_name` | String(128) | nullable | 店铺名 | +| `dishes` | JSON(PG: JSONB) | NOT NULL, default [] | 菜名列表(前 2 道展示,其余"还有 N 道") | +| `source` | String(16) | NOT NULL, default `compare` | 来源:`demo`(演示)/ `compare`(真实下单上报,待启用) | +| `created_at` | DateTime(tz) | server_default now(), index | 时间 | + +## 索引与约束 +- PK: `id`;index: `user_id`、`created_at` + +## 关系 +- `user_id` → `user.id`(多对一) + +## 说明 +- `dishes` 用 `JSON().with_variant(JSONB(),"postgresql")`(SQLite 退化 JSON)。 +- `beat_percent`(超过百分之多少用户)按各用户累计省下金额做真实分位;为有可比人群造了 5 个种子用户(`register_channel='seed'`)。 diff --git a/docs/database/signin_record.md b/docs/database/signin_record.md new file mode 100644 index 0000000..abe7a96 --- /dev/null +++ b/docs/database/signin_record.md @@ -0,0 +1,26 @@ +# signin_record — 签到记录(7 天循环) + +> 模型 `app/models/signin.py` | 关联接口 [signin-status](../api/signin-status.md) / [signin-do](../api/signin-do.md) | [← 表索引](./README.md) + +每次签到一行,`(user_id, signin_date)` 唯一,天然防一天签两次。`cycle_day`(1..7)决定发多少金币(`rewards.SIGNIN_REWARDS`),断签重置回 1;`streak` 是连续签到天数(不封顶)用于展示。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | +| `signin_date` | Date | NOT NULL | 签到日期(北京时间 date) | +| `cycle_day` | Integer | NOT NULL | 7 天循环里今天第几档(1..7),决定发币 | +| `streak` | Integer | NOT NULL | 连续签到天数(不封顶) | +| `coin_awarded` | Integer | NOT NULL | 本次发放金币 | +| `created_at` | DateTime(tz) | server_default now() | 时间 | + +## 索引与约束 +- PK: `id`;index: `user_id` +- UNIQUE(`user_id`, `signin_date`) = `uq_signin_user_date`(防一天签两次) + +## 关系 +- `user_id` → `user.id`(多对一) + +## 说明 +- 签到"今天"按北京时间 `cn_today()`(`CN_TZ=UTC+8`);发币与写本表记录同事务(`grant_coins(biz_type='signin', ref_id=日期)`)。 diff --git a/docs/database/user.md b/docs/database/user.md new file mode 100644 index 0000000..5c8595e --- /dev/null +++ b/docs/database/user.md @@ -0,0 +1,32 @@ +# user — 用户(登录主体) + +> 模型 `app/models/user.py` | 关联接口 [auth-me](../api/auth-me.md) 等 auth 组 | [← 表索引](./README.md) + +登录主体。极光一键登录与短信登录都映射到同一行,以 `phone` 唯一索引;注册即登录(phone 不存在则 insert,存在则更新 `last_login_at`)。后续加微信/Apple 登录新增 oauth_account 表,本表不动。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | 用户主键 | +| `phone` | String(20) | UNIQUE, index, NOT NULL | 手机号(登录主键;注销后置 `deleted_` 释放唯一约束) | +| `register_channel` | String(20) | NOT NULL, default `jverify` | 注册渠道:`jverify` / `sms` | +| `nickname` | String(64) | nullable | 通用昵称(用户改资料设) | +| `avatar_url` | String(512) | nullable | 通用头像相对 URL(`/media/avatars/...`) | +| `wechat_openid` | String(64) | UNIQUE, index, nullable | 微信 openid(绑定后存,提现转账用);一微信一账号 | +| `wechat_nickname` | String(64) | nullable | 微信昵称(绑定时拉,展示在提现绑定卡) | +| `wechat_avatar_url` | String(512) | nullable | 微信头像 URL | +| `status` | String(20) | NOT NULL, default `active` | `active` / `disabled` / `deleted` | +| `created_at` | DateTime(tz) | server_default now() | 注册时间 | +| `last_login_at` | DateTime(tz) | default utcnow(应用层) | 最近登录时间 | + +## 索引与约束 +- PK: `id` +- UNIQUE + index: `phone` +- UNIQUE + index: `wechat_openid`(允许多个 NULL = 多个未绑定用户) + +## 关系 +- 被引用方:`coin_account` / `coin_transaction` / `cash_transaction` / `withdraw_order` / `signin_record` / `user_task` / `savings_record` / `ad_reward_record` / `ad_ecpm_record` / `feedback` / `comparison_record` / `comparison_milestone_claim` 的 `user_id` 均 → `user.id`。 + +## 说明 +- `nickname/avatar_url`(通用)与 `wechat_nickname/wechat_avatar_url`(微信)**分开存,不互相覆盖**。 +- 注销账号:phone 改占位串、status=deleted,不物理删行(保留外键完整性)。 diff --git a/docs/database/user_task.md b/docs/database/user_task.md new file mode 100644 index 0000000..fe4aeb6 --- /dev/null +++ b/docs/database/user_task.md @@ -0,0 +1,26 @@ +# user_task — 一次性任务领取去重 + +> 模型 `app/models/task.py` | 关联接口 [tasks-list](../api/tasks-list.md) / [tasks-claim](../api/tasks-claim.md) | [← 表索引](./README.md) + +像"打开消息提醒"这类只能领一次的任务,完成后写一行,`(user_id, task_key)` 唯一防重复领奖。可循环领取的任务(签到)不走这张表,有专表 `signin_record`。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | +| `task_key` | String(48) | NOT NULL | 任务标识,见 `rewards.TASK_REWARDS`(如 `enable_notification`) | +| `status` | String(16) | NOT NULL, default `completed` | 任务状态 | +| `coin_awarded` | Integer | NOT NULL, default 0 | 该任务发放金币 | +| `completed_at` | DateTime(tz) | server_default now() | 完成/领取时间 | + +## 索引与约束 +- PK: `id`;index: `user_id` +- UNIQUE(`user_id`, `task_key`) = `uq_task_user_key`(防重复领) + +## 关系 +- `user_id` → `user.id`(多对一) + +## 说明 +- 领取:写本表 + `grant_coins(biz_type='task_', ref_id=task_key)` 同事务;重复领抛 409,未知 key 抛 404。 +- 比价战绩里程碑虽是"领一次"模型但**不复用本表**,另有 `comparison_milestone_claim`(因 task_key 是固定字典,里程碑是按次数解锁的序号)。 diff --git a/docs/database/withdraw_order.md b/docs/database/withdraw_order.md new file mode 100644 index 0000000..7ab6eba --- /dev/null +++ b/docs/database/withdraw_order.md @@ -0,0 +1,30 @@ +# withdraw_order — 提现单(现金 → 微信零钱) + +> 模型 `app/models/wallet.py` | 关联接口 [wallet-withdraw](../api/wallet-withdraw.md) / [wallet-withdraw-status](../api/wallet-withdraw-status.md) | [← 表索引](./README.md) + +提现状态机:`pending → success / failed`。扣现金 + 写 `cash_transaction(withdraw)` + 建本单在同一事务;失败/取消时退回现金并写 `cash_transaction(withdraw_refund)`。`wechat_state` 存微信侧原始状态,`status` 是归一化后的三态。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | +| `out_bill_no` | String(64) | UNIQUE, index, NOT NULL | 商户单号(幂等键 + 微信查单);客户端可传,不传则服务端生成 | +| `amount_cents` | Integer | NOT NULL | 提现金额(分) | +| `status` | String(16) | NOT NULL, default `pending` | 归一化状态:`pending` / `success` / `failed` | +| `wechat_state` | String(32) | nullable | 微信原始状态:`WAIT_USER_CONFIRM` / `SUCCESS` / `FAIL` / `CANCELLED` … | +| `transfer_bill_no` | String(64) | nullable | 微信转账单号 | +| `package_info` | String(512) | nullable | 待用户确认时返回 App 拉起确认页用 | +| `fail_reason` | String(256) | nullable | 失败技术原因(不外露,用户只看流水 remark) | +| `created_at` | DateTime(tz) | server_default now(), index | 发起时间 | +| `updated_at` | DateTime(tz) | server_default now(), onupdate now() | 更新时间 | + +## 索引与约束 +- PK: `id`;UNIQUE + index: `out_bill_no`;index: `user_id`、`created_at` + +## 关系 +- `user_id` → `user.id`(多对一) + +## 说明 +- 资金安全:原子扣款 + `out_bill_no` 幂等 + 模糊失败先查单再决定(绝不盲目退款)+ 孤儿单对账 `reconcile_pending_withdraws`。详见 [[project_shaguabijia_app_server]] 提现段。 +- `WITHDRAW_MIN_CENTS=10`(0.1 元,微信地板)。