Files
marco 43f28d358b docs: 补全 API 与数据库文档
- 新增 device-liveness(无障碍存活监控)、internal(内部回写端点)、cps-redirect(CPS 短链落地)三族单文件文档
- API 总览补录此前缺失端点:coupon prompt 频控、report/invite、wxpay 回调、platform 客户端配置、feedback/user/ad 零散读端点
- database 新增 cps_wx_user / device_liveness / launch_confirm_sample 三表文档,更新 OVERVIEW/README/comparison_record/数据库迁移/后端技术实现

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 04:07:54 +08:00

228 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据库总览 — 表 × 功能 × 关系
> 跨表视角。单表字段级细节看同目录 `<表名>.md`(索引见 [README](./README.md))。
> 本文专门回答三件「跨表」的事:**① 每块 App 功能用到哪些表 ② 什么操作往哪张表写 ③ 表和表怎么连(join key,含没有外键约束、靠业务字段对齐的语义关联)**。
> **范围**:业务表全部在 `shaguabijia-app-server`(SQLAlchemy 2.0 + SQLite 开发 / PostgreSQL 生产)。`pricebot-backend`(比价/领券 Agent)是纯内存态、**无任何表**;Android 客户端只有 EncryptedSharedPreferences / SharedPreferences、**无关系库**。共 **40 张业务表** + `alembic_version`(框架的迁移版本指针)。注意「比价/领券**过程**」始终在 pricebot 内存态跑、**不落库**——只有**结果**回 app-server 才落库:领券结果落 `coupon_*` 三张今日状态表;比价结果分两路——客户端带 JWT 上报「我的记录」落 `comparison_record`,pricebot 另经 `app/api/internal/` server→server 把客观价格/门店事实落 `price_observation`/`store_mapping`(不鉴权、匿名也记)+ 启动确认窗兜底样本落 `launch_confirm_sample`。此外好友邀请(`invite_*` 2 张)与美团 CPS 群发联盟(`cps_*` 6 张,含落地页微信身份 `cps_wx_user`)是两个独立子系统。无障碍存活监控 `device_liveness`(#65)按用户设备维度记心跳、检掉线召回。
---
## 一、按功能域看表(每张表用在 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 UI)比价 done 后客观价格沉淀 | [`price_observation`](./price_observation.md) | **pricebot server→server 内部上报**;平台/门店视角的到手价事实,匿名也记,与 `comparison_record`(用户视角)互补 |
| (无 App UI)跨平台门店身份映射 | [`store_mapping`](./store_mapping.md) | **pricebot 内部上报**;同店在淘宝/美团/京东的 id/名/deeplink,供下次比价 `lookup` 反查省掉现场搜店 |
| (无 App UI)启动确认窗兜底样本 | [`launch_confirm_sample`](./launch_confirm_sample.md) | **pricebot 内部上报**;LLM 兜底放行「想要打开 XX」跨 App 启动窗后回写样本(host 包 + 弹窗树 + plan + locale),供研发人工沉淀进 pricebot 规则 |
### 领券(每日领券联动 · 今日状态)
| App 位置 / 动作 | 表 | 说明 |
|---|---|---|
| 领券**过程**(看屏→领券) | (无) | 在 pricebot-backend 内存态跑,**过程不落库**;结果回 app-server 才落下面三张表 |
| 切外卖 App 时是否弹领券引导窗 | [`coupon_prompt_engagement`](./coupon_state.md) | 今天 engage 过(点领/点拒)就不再弹;判断维度 device_id |
| 首页「去领取」卡是否置灰 | [`coupon_daily_completion`](./coupon_state.md) | 今天跑完整轮(到 done)就置灰;判断维度 device_id |
| 每张券领取结果留痕 | [`coupon_claim_record`](./coupon_state.md) | 资产/画像/排查/CPS;当前**不参与**判断 |
### 钱包 / 福利(看广告赚钱闭环)
| 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) | 7 天循环发币;签到后看广告可膨胀一次 |
| 一次性任务(开消息提醒等) | [`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) | 登录主体,注册即登录 |
| 新手引导是否再展示 | [`onboarding_completion`](./onboarding_completion.md) | 按 设备+账号 去重;登录响应回 `onboarding_completed`,走完引导时标记,跨卸载重装 |
| 帮助与反馈 | [`feedback`](./feedback.md) | 含截图,后台人工处理 |
| 无障碍存活监控 / 掉线召回 | [`device_liveness`](./device_liveness.md) | 无障碍服务周期心跳;worker 检出心跳超时 → 推送/客户端进 App 弹「开启自启动」引导 |
### 好友邀请(注册增长)
| App 位置 / 动作 | 表 | 说明 |
|---|---|---|
| 输入/剪贴板邀请码绑定 | [`invite_relation`](./invite_relation.md) | 注册即生效,邀请人+被邀请人各发 1 万金币;`invitee_user_id` 唯一=幂等防重复发奖 |
| 落地页访问指纹(剪贴板归因兜底) | [`invite_fingerprint`](./invite_fingerprint.md) | 剪贴板没拿到码时,用 (ip+机型+屏幕) 7 天内反查邀请人 |
### 美团 CPS 群发联盟(私域社群比价,运营后台驱动 · 群发选品→点击→对账漏斗)
| 后台/用户动作 | 表 | 说明 |
|---|---|---|
| 运营建可推广活动 | [`cps_activity`](./cps_activity.md) | 预存券/物料(美团 actId / 淘宝淘口令 / 京东链接) |
| 运营建微信推广群 | [`cps_group`](./cps_group.md) | 一群一行,`sid`=美团二级渠道追踪位 |
| 后台批量生成群发短链 | [`cps_link`](./cps_link.md) | 群×活动 → `/c/{code}`,运营复制到微信群 |
| 用户点短链(落地/复制口令) | [`cps_click`](./cps_click.md) | 记 visit/copy 事件,统计 PV/UV |
| 微信内打开落地页授权 | [`cps_wx_user`](./cps_wx_user.md) | 服务号网页授权拿 openid(base 静默)/ 昵称头像 unionid(userinfo,点领券触发),记首次来源群 |
| 定时拉美团联盟订单对账 | [`cps_order`](./cps_order.md) | `query_order` 按 sid 归群,串成点击→下单→佣金漏斗 |
### 运营后台 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`);并**读** `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` 幂等 |
| 注册设备 / 更新 push token `POST /device/register` | `device_liveness`(C/U upsert) | `(user_id, device_id)` 幂等;只在传入非空时更新 `registration_id`/版本 |
| 无障碍服务心跳 `POST /device/heartbeat` | `device_liveness`(C/U) | 心跳也能自注册;`accessibility_enabled=true` 时刷 `last_heartbeat_at`、置 `alive`、清 `notified_at` |
| 客户端 ack 掉线提醒 `POST /device/liveness/ack` | `device_liveness`(U) | 清 `kill_alert_pending`(幂等;下次真掉线 worker 再置) |
| 比价 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 /invite/bind` | `invite_relation`(C `effective`) + `coin_account`(U×2) + `coin_transaction`(C `invite_inviter` + `invite_invitee`) | 同事务;`invitee_user_id` 唯一幂等,双方各发 1 万金币 |
| 落地页归因 `POST /invite/landing-track` | `invite_fingerprint`(C) | 剪贴板归因兜底线索,登录后用 (ip+机型+屏幕) 反查 |
| 领券首帧 `POST /api/v1/coupon/step`(step=0) | `coupon_prompt_engagement`(C/U `claim_started`) | `(device_id, package, 北京日)` 幂等;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(领券 `/prompt/reset`、`/completed-today/reset` 是开发用删除,非业务流程)。
### CPS 群发联盟 & pricebot 内部上报(非 C 端用户、非 admin 常规管理触发)
| 触发 | 写入 | 操作 |
|---|---|---|
| 运营后台建活动 / 建群 | `cps_activity`(C/U) / `cps_group`(C/U) | `app/admin/routers/cps.py`;群含美团平台才分配 `sid` |
| 后台批量生成短链 `POST /admin/api/cps/referral-links` | `cps_link`(C) | 每 群×活动 一条;美团经 `sid` 转链拿 `target_url`(同群同活动重复生成产生多条) |
| 用户点群发短链 `GET /c/{code}` / `POST /c/{code}/copy` | `cps_click`(C `visit`/`copy`) | 公开端点不鉴权;`group_id`/`sid` 从 link 冗余进来免 join |
| 微信落地页授权回调 `GET /wx/oauth/cb` | `cps_wx_user`(C/U upsert) | 按 `openid` 幂等;base 拿 openid,userinfo 补昵称/unionid(非 None 才覆盖);任何失败兜底回落地页不阻断领券 |
| 定时拉美团联盟订单对账 | `cps_order`(C/U upsert) | `query_order``sid` 归群;`order_id` 幂等(状态会变,重复拉则更新) |
| pricebot 比价 done 内部上报 `POST /internal/price-observation` | `price_observation`(C 批量) | `(trace_id,platform,scope)` 幂等;**不走 JWT、靠 `X-Internal-Secret`**(未配→503) |
| pricebot 比价 done 内部上报 `POST /internal/store-mapping` | `store_mapping`(C/U 填空合并) | `trace_id` 幂等;另有 `lookup` 反查 + `invalidate` deeplink 失效标记(淘宝/京东) |
| pricebot LLM 兜底放行启动确认窗后上报 `POST /internal/launch-confirm-sample` | `launch_confirm_sample`(C) | **都上报、不去重**;靠 `X-Internal-Secret`(未配→503) |
| 后台 worker 检出心跳超时 `heartbeat_monitor_worker` | `device_liveness`(U `mark_notified`) | 置 `notified` + `notified_at` + `kill_alert_pending=True`;非 C 端、非 admin,定时扫 `list_overdue` |
> CPS 6 表与本 App `user` **无关**(被推广群的用户在美团下单 / 在微信落地页授权,不是本 App 注册用户;`cps_wx_user` 是微信 `openid` 维度的另一套用户身份);`price_observation`/`store_mapping`/`launch_confirm_sample` 的 `source_user_id`/`device_id` 是可空旁路(链路不鉴权,匿名也记)。
---
## 三、表间关系 & Join Key
### 硬外键(数据库 FK 约束)
- **18 张用户维度表 `.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``device_liveness`
- `admin_audit_log.admin_id``admin_user.id`
- `price_report.comparison_record_id``comparison_record.id`(可空:关联记录被删后仍留上报历史)。
- **邀请两表** → `user.id`:`invite_relation.inviter_user_id``invite_relation.invitee_user_id`(唯一)、`invite_fingerprint.inviter_user_id`——注意 FK 列名是 `inviter`/`invitee_user_id`,不是 `user_id`
- **CPS 6 表 + 比价沉淀 2 表 + `launch_confirm_sample` 均无硬 FK**(全靠 `sid`/`trace_id`/`openid` 语义对齐,见下)。
### 语义 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_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`](./coupon_state.md))。
- **`onboarding_completion.(user_id, device_id)`**:`user_id` 语义关联 `user.id`(无硬 FK,同 `coupon_*` 设备表),`device_id` = 客户端硬件级 `ANDROID_ID`(≠ 领券 per-install `device_id`)。登录读、走完引导写,决定是否再展示新手引导。
- **CPS 群发 6 表全靠 `sid` / id / `openid` 语义串联(无硬 FK)**:`cps_link.group_id``cps_group.id``cps_link.activity_id``cps_activity.id``cps_click.link_id``cps_link.id``cps_wx_user.first_group_id``cps_group.id`;**点击与订单无法对到单笔**,只在群维度(`sid`)汇合——`cps_order.sid``cps_group.sid``cps_link.sid``cps_click.sid`(仅美团有 sid,淘宝/京东无)。统计按群聚合,故 `group_id`/`sid` 冗余进 `cps_click` 免 join。`cps_wx_user``openid` 自成用户身份维度,与 `cps_click`/`cps_order` 无 id 级 join。
- **比价沉淀两表(`price_observation` / `store_mapping`)**:`trace_id` 软指 pricebot work_logs(与 `comparison_record.trace_id` 同源但不互 join,各存各视角);`source_user_id` / `source_device_id` 软指用户/设备(可空,匿名也记)。`store_mapping.lookup` 靠**店名字符串精确相等** + geo 取最近,非 id 级 join。
### 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, device_liveness }
(device_liveness 硬 FK; (user_id,device_id) 唯一)
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 仅软关联)
user ─1:N─ invite_relation (inviter_user_id 硬 FK); invitee_user_id ─1:1─ user (唯一硬 FK)
user ─1:N─ invite_fingerprint (inviter_user_id 硬 FK)
cps_activity / cps_group ──语义(无FK)──▶ cps_link ─1:N─ cps_click
(CPS 6 表自成子系统; cps_order 经 sid 归群对账, 与本 App user 无关)
cps_group ◀──语义(无FK)── cps_wx_user (first_group_id; openid 维度的微信落地页用户身份)
price_observation / store_mapping (独立, 无硬 FK; 维度=trace_id, source_user/device 仅软关联; pricebot 内部上报)
launch_confirm_sample (独立, 无硬 FK; 都上报不去重; pricebot 内部上报启动确认窗兜底样本)
```
---
## 四、资金模型(金币 / 现金 / 提现,三层)
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 + 同步更新本目录文档。