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>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
> 跨表视角。单表字段级细节看同目录 `<表名>.md`(索引见 [README](./README.md))。
|
||||
> 本文专门回答三件「跨表」的事:**① 每块 App 功能用到哪些表 ② 什么操作往哪张表写 ③ 表和表怎么连(join key,含没有外键约束、靠业务字段对齐的语义关联)**。
|
||||
> **范围**:业务表全部在 `shaguabijia-app-server`(SQLAlchemy 2.0 + SQLite 开发 / PostgreSQL 生产)。`pricebot-backend`(比价/领券 Agent)是纯内存态、**无任何表**;Android 客户端只有 EncryptedSharedPreferences / SharedPreferences、**无关系库**。共 **37 张业务表** + `alembic_version`(框架的迁移版本指针)。注意「比价/领券**过程**」始终在 pricebot 内存态跑、**不落库**——只有**结果**回 app-server 才落库:领券结果落 `coupon_*` 三张今日状态表;比价结果分两路——客户端带 JWT 上报「我的记录」落 `comparison_record`,pricebot 另经 `app/api/internal/` server→server 把客观价格/门店事实落 `price_observation`/`store_mapping`(不鉴权、匿名也记)。此外好友邀请(`invite_*` 2 张)与美团 CPS 群发联盟(`cps_*` 5 张)是两个独立子系统。
|
||||
> **范围**:业务表全部在 `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)按用户设备维度记心跳、检掉线召回。
|
||||
|
||||
---
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
| 「上报更低价」提交 / 列表 | [`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 位置 / 动作 | 表 | 说明 |
|
||||
@@ -47,6 +48,7 @@
|
||||
| 登录(极光/短信)/ 改资料 / 注销 | [`user`](./user.md) | 登录主体,注册即登录 |
|
||||
| 新手引导是否再展示 | [`onboarding_completion`](./onboarding_completion.md) | 按 设备+账号 去重;登录响应回 `onboarding_completed`,走完引导时标记,跨卸载重装 |
|
||||
| 帮助与反馈 | [`feedback`](./feedback.md) | 含截图,后台人工处理 |
|
||||
| 无障碍存活监控 / 掉线召回 | [`device_liveness`](./device_liveness.md) | 无障碍服务周期心跳;worker 检出心跳超时 → 推送/客户端进 App 弹「开启自启动」引导 |
|
||||
|
||||
### 好友邀请(注册增长)
|
||||
| App 位置 / 动作 | 表 | 说明 |
|
||||
@@ -61,6 +63,7 @@
|
||||
| 运营建微信推广群 | [`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,独立鉴权)
|
||||
@@ -95,6 +98,9 @@
|
||||
| 看广告时长上报 `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)` 幂等 |
|
||||
@@ -130,22 +136,25 @@
|
||||
| 运营后台建活动 / 建群 | `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 5 表与本 App `user` **无关**(被推广群的用户在美团下单,不是本 App 注册用户);`price_observation`/`store_mapping` 的 `source_user_id` 是可空旁路(链路不鉴权,匿名也记)。
|
||||
> CPS 6 表与本 App `user` **无关**(被推广群的用户在美团下单 / 在微信落地页授权,不是本 App 注册用户;`cps_wx_user` 是微信 `openid` 维度的另一套用户身份);`price_observation`/`store_mapping`/`launch_confirm_sample` 的 `source_user_id`/`device_id` 是可空旁路(链路不鉴权,匿名也记)。
|
||||
|
||||
---
|
||||
|
||||
## 三、表间关系 & 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`。
|
||||
- **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 5 表 + 比价沉淀 2 表均无硬 FK**(全靠 `sid`/`trace_id` 语义对齐,见下)。
|
||||
- **CPS 6 表 + 比价沉淀 2 表 + `launch_confirm_sample` 均无硬 FK**(全靠 `sid`/`trace_id`/`openid` 语义对齐,见下)。
|
||||
|
||||
### 语义 join key(无 FK 约束,靠业务字段对齐 —— 排障/对账必看)
|
||||
- **`coin_transaction.ref_id` 指向随 `biz_type` 变**:
|
||||
@@ -172,7 +181,7 @@
|
||||
- **里程碑解锁进度不存库**:`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 群发 5 表全靠 `sid` / id 语义串联(无硬 FK)**:`cps_link.group_id`→`cps_group.id`、`cps_link.activity_id`→`cps_activity.id`、`cps_click.link_id`→`cps_link.id`;**点击与订单无法对到单笔**,只在群维度(`sid`)汇合——`cps_order.sid` ≈ `cps_group.sid` ≈ `cps_link.sid` ≈ `cps_click.sid`(仅美团有 sid,淘宝/京东无)。统计按群聚合,故 `group_id`/`sid` 冗余进 `cps_click` 免 join。
|
||||
- **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 关系(文字版)
|
||||
@@ -182,7 +191,8 @@ 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 }
|
||||
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
|
||||
@@ -192,8 +202,10 @@ coupon_prompt_engagement / coupon_daily_completion / coupon_claim_record
|
||||
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 5 表自成子系统; cps_order 经 sid 归群对账, 与本 App user 无关)
|
||||
(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 内部上报启动确认窗兜底样本)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
> 数据库: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-06-17(补全 9 张此前缺文档的表:CPS 群发 5 张 `cps_*` + 好友邀请 2 张 `invite_*` + 比价沉淀 `price_observation`/`store_mapping`;并全表 review 对齐 model——`signin_record` 改 7 天循环、`coupon_prompt_engagement` 加 `package` 列按 App 频控、`user` 补 `username`/`invite_code`/`debug_trace_enabled` 等;含 [OVERVIEW 总览](./OVERVIEW.md))
|
||||
> 最后更新:2026-06-23(补 3 张表文档:`device_liveness`(#65 无障碍存活监控)、`cps_wx_user`(CPS 落地页微信身份)、`launch_confirm_sample`(启动确认窗兜底样本);`comparison_record` 补 `input_tokens`/`output_tokens`。上一次 2026-06-17 补全 CPS 群发 5 张 `cps_*` + 好友邀请 2 张 `invite_*` + 比价沉淀 `price_observation`/`store_mapping` 并全表 review 对齐 model;含 [OVERVIEW 总览](./OVERVIEW.md))
|
||||
|
||||
> 🧭 **先看 [OVERVIEW.md — 表 × 功能 × 关系](./OVERVIEW.md)**:跨表的「每块功能用哪些表 / 什么操作写哪张表 / 表间 join key」都在那;本页只做**单表索引**,点进每张表的详情看字段级说明。
|
||||
|
||||
---
|
||||
|
||||
## 表总览(37 张业务表 + `alembic_version` 框架表)
|
||||
## 表总览(40 张业务表 + `alembic_version` 框架表)
|
||||
|
||||
### 账号 / 反馈
|
||||
| 表 | 用途 | 模型 | 文档 |
|
||||
@@ -17,6 +17,7 @@
|
||||
| `user` | 用户(登录主体);几乎所有表的外键宿主 | `models/user.py` | [详情](./user.md) |
|
||||
| `onboarding_completion` | 新手引导完成标记(按 设备+账号 去重,登录时据此跳过引导) | `models/onboarding.py` | [详情](./onboarding_completion.md) |
|
||||
| `feedback` | 用户帮助与反馈(含截图) | `models/feedback.py` | [详情](./feedback.md) |
|
||||
| `device_liveness` | 无障碍存活监控(心跳超时检出 + 掉线召回;`(user_id, device_id)` 唯一) | `models/device.py` | [详情](./device_liveness.md) |
|
||||
|
||||
### 好友邀请(注册增长)
|
||||
| 表 | 用途 | 模型 | 文档 |
|
||||
@@ -53,6 +54,7 @@
|
||||
|---|---|---|---|
|
||||
| `price_observation` | 价格观测(平台/门店视角的到手价事实,匿名也记;`comparison_record` 的客观对照) | `models/price_observation.py` | [详情](./price_observation.md) |
|
||||
| `store_mapping` | 跨平台「同一家店」id/名/deeplink 身份映射(淘宝/美团/京东,供 lookup 反查省现场搜店) | `models/store_mapping.py` | [详情](./store_mapping.md) |
|
||||
| `launch_confirm_sample` | 启动确认窗 agent 兜底样本(pricebot LLM 放行跨 App 启动窗后回写,供研发人工沉淀规则) | `models/launch_confirm_sample.py` | [详情](./launch_confirm_sample.md) |
|
||||
|
||||
### 领券(每日领券联动 · 今日状态)
|
||||
| 表 | 用途 | 模型 | 文档 |
|
||||
@@ -73,6 +75,7 @@
|
||||
| `cps_group` | 推广微信群(`sid`=美团二级渠道追踪位,订单按 sid 归群对账) | `models/cps_group.py` | [详情](./cps_group.md) |
|
||||
| `cps_link` | 群发短链 `/c/{code}`(群×活动生成,302 跳/淘宝落地页) | `models/cps_link.py` | [详情](./cps_link.md) |
|
||||
| `cps_click` | 短链点击事件流(visit/copy,PV/UV 漏斗源) | `models/cps_link.py` | [详情](./cps_click.md) |
|
||||
| `cps_wx_user` | CPS 落地页微信用户身份(openid 维度;网页授权拿 openid/昵称/unionid,记首次来源群) | `models/cps_wx_user.py` | [详情](./cps_wx_user.md) |
|
||||
| `cps_order` | 美团联盟 query_order 拉回的对账订单(按 sid 归群算佣金) | `models/cps_order.py` | [详情](./cps_order.md) |
|
||||
|
||||
### 首页门面数据
|
||||
@@ -93,7 +96,7 @@
|
||||
## 通用约定
|
||||
|
||||
- **主键**:`id` Integer autoincrement;例外:`coin_account` / `wechat_transfer_authorization` 的 `user_id` 既是主键也是外键(一用户一行),`app_config` 用 `key` 作主键。
|
||||
- **外键**:所有用户维度表 `user_id` → `user.id` 且建 index;`admin_audit_log.admin_id` → `admin_user.id`;`price_report.comparison_record_id` → `comparison_record.id`。
|
||||
- **外键**:所有用户维度表 `user_id` → `user.id` 且建 index(含 `device_liveness`);`admin_audit_log.admin_id` → `admin_user.id`;`price_report.comparison_record_id` → `comparison_record.id`。
|
||||
- **金额**:整数;金币计数,现金/价格存「分」(`*_cents`)。汇率 `10000 金币 = 1 元 = 100 分`。
|
||||
- **时间**:`created_at` 等用 `DateTime(timezone=True)` + `server_default=func.now()`;业务"今天"按**北京时间**(`core/rewards.cn_today`),跨天计数用「日期串」列(`reward_date` / `watch_date` / `report_date`,`String(10)` 存 `YYYY-MM-DD`)等值查,**不在 SQL 里做跨时区 date 比较**(SQLite 不可靠)。
|
||||
- **JSON 列**:`JSON().with_variant(JSONB(), "postgresql")` —— PG 上 JSONB(可建 GIN 索引),SQLite 退化为通用 JSON(否则 `create_all` 编译报错);`feedback.images` 例外,用通用 `JSON`。
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
| `comparison_results` | JSON(PG: JSONB) | NOT NULL, default [] | 逐平台对比 `[{platform_id,platform_name,package,price(元),is_source,rank,coupon_saved(元),coupon_name,applied_coupons}]`;`coupon_saved`=该平台主优惠额(美团红包/淘宝平台红包/京东百亿补贴,只取一笔),`coupon_name`=优惠来源名(展示用),`applied_coupons`=`[{name,amount}]` 多券明细 |
|
||||
| `skipped_dish_names` | JSON(PG: JSONB) | NOT NULL, default [] | 被跳过的菜名 |
|
||||
| `raw_payload` | JSON(PG: JSONB) | nullable | 客户端原始上报全量(calibration + done.params),取数兜底 |
|
||||
| `input_tokens` | Integer | nullable | 本次 LLM 累计输入 token = Σ `llm_calls[].usage.prompt_tokens`(server 收上报后从 `llm_calls` 累加;旧记录/未采集为 null) |
|
||||
| `output_tokens` | Integer | nullable | 本次 LLM 累计输出 token = Σ `llm_calls[].usage.completion_tokens`(同上) |
|
||||
| `created_at` | DateTime(tz) | server_default now(), index | 时间 |
|
||||
|
||||
> `ordered`(已下单)是**瞬态字段**,不在表里:`list_records` 读取时按 `store_name ∈ 该用户 source='compare' 的 savings_record.shop_name 集合` 现挂到实例上供出参用。
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# cps_wx_user — CPS 落地页微信用户身份(openid 维度)
|
||||
|
||||
> 模型 `app/models/cps_wx_user.py`(`CpsWxUser`) · 仓库 `app/repositories/cps_wx_user.py`(`get_by_openid` / `upsert`) · 接口 `GET /wx/oauth/cb`(微信网页授权回调,`app/api/v1/cps_redirect.py`) · [← 索引](./README.md) · [总览](./OVERVIEW.md)
|
||||
|
||||
用户在微信内打开群发短链落地页 [`/c/{code}`](./cps_link.md),经服务号网页授权拿到的微信身份,按 `openid` 唯一存一行。在 CPS 群发漏斗里,它给点击/领券补上**「是谁」**这一维度(此前 [`cps_click`](./cps_click.md) 只有 PV/UV 近似)。授权分两阶:`snsapi_base` 静默拿 `openid`(统计主力);用户点「领券」触发 `snsapi_userinfo` 才补 `nickname`/`headimgurl`/`unionid`。
|
||||
|
||||
> 注意:**下单归因到人**需 user-level sid(另一期),本表只承载身份 + 领券/点击侧,**不与 `cps_order` 关联**。
|
||||
|
||||
## 用在哪 / 增删改查
|
||||
- **C / U(upsert)**:`upsert`(`get_by_openid` 查 → 无则建、有则补),由 `wx_oauth_cb`(`GET /wx/oauth/cb`)在微信授权回调里调。首次建行记来源 `first_code`/`first_group_id`;已存在则**仅在传入非 None 时**更新 `nickname`/`headimgurl`/`unionid`(`base` 阶段为 None,不覆盖已有画像),并显式刷新 `last_seen`(`onupdate` 仅字段有变更才触发,故 base-only 回访也要手动 set)。任何授权失败兜底回落地页,**绝不阻断领券**。
|
||||
- **R**:`get_by_openid`(upsert 内部按 openid 查)。
|
||||
- **D**:无。
|
||||
|
||||
## 字段
|
||||
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|
||||
|---|---|---|---|
|
||||
| `id` | Integer | PK, autoincrement | |
|
||||
| `openid` | String(64) | **UNIQUE**, index, NOT NULL | 服务号下用户唯一标识(网页授权拿到);upsert 主键 |
|
||||
| `unionid` | String(64) | index, nullable | 开放平台 unionid(服务号绑开放平台 + `scope=userinfo` 才有);跨 App/服务号统一用户 |
|
||||
| `nickname` | String(128) | nullable | 昵称;`userinfo` 授权后才有(base 阶段为空) |
|
||||
| `headimgurl` | String(512) | nullable | 头像 URL;同 `nickname`,userinfo 阶段才有 |
|
||||
| `first_code` | String(16) | nullable | 首次进入来源短链 code(从哪个群的链接授权进来) |
|
||||
| `first_group_id` | Integer | index, nullable | 首次来源群 = `cps_group.id`(语义关联,非 FK);群内用户级统计用 |
|
||||
| `first_seen` | DateTime(tz) | server_default now() | 首次授权时间 |
|
||||
| `last_seen` | DateTime(tz) | server_default now(), onupdate now() | 最近一次落地(upsert 时显式刷新) |
|
||||
|
||||
## 关系 / Join Key
|
||||
- `first_group_id` → `cps_group.id`(**语义关联,无 ForeignKey**;记首次来源群)。
|
||||
- 与 [`cps_click`](./cps_click.md) **无 id 级 join**:点击事件流目前不回写 openid 维度的用户统计,两者各按各的维度看(cps_click 看 PV/UV,本表看「谁」)。
|
||||
- 与本 App `user` 表**无关**:微信落地页用户不是本 App 注册用户(同 CPS 其它表)。
|
||||
|
||||
## 索引与约束
|
||||
- PK `id`;UNIQUE index `openid`;index `unionid`、`first_group_id`。
|
||||
|
||||
## 注意
|
||||
- **两阶授权**:`base`(静默)只能拿 `openid`,不弹窗、不打扰,用作统计主力;`userinfo`(弹授权页)拿昵称头像 + unionid,只在用户点「领券」这种有交互的时刻触发(避免微信快照页),授权回流带 `?authed=1` 自动衔接复制口令。详见 `cps_redirect.py` 落地页逻辑。
|
||||
- **`unionid` 不一定有**:需服务号已绑微信开放平台 + 走到 `userinfo` 授权;base 阶段、未绑开放平台时为 None。
|
||||
@@ -0,0 +1,43 @@
|
||||
# device_liveness — 无障碍存活监控(心跳 + 掉线召回)
|
||||
|
||||
> 模型 `app/models/device.py`(`DeviceLiveness`) · 仓库 `app/repositories/device.py`(`register_or_update` / `touch_heartbeat` / `list_overdue` / `mark_notified` / `get_device` / `ack_kill_alert`) · 接口 用户 `POST /api/v1/device/register`、`POST /api/v1/device/heartbeat`、`GET /api/v1/device/liveness`、`POST /api/v1/device/liveness/ack`(`app/api/v1/device.py`);后台 worker `heartbeat_monitor_worker` · [← 索引](./README.md) · [总览](./OVERVIEW.md)
|
||||
|
||||
每行 = 一个用户的一台设备(per-install,`(user_id, device_id)` 唯一)。客户端无障碍服务存活时周期上报心跳刷新 `last_heartbeat_at`;App 前台/登录拿到极光 push token 时上报 `registration_id`。后端 `heartbeat_monitor_worker` 扫「曾保护过、现已心跳超时」的设备,推送(或本期仅终端打印)提醒用户重开无障碍。**表名不叫 `device`**:它存的不是设备信息(品牌/型号),而是**无障碍存活状态**。#65 新增。
|
||||
|
||||
> 设计见 spec `spec/accessibility-liveness-push.md`(推送版)+ `spec/accessibility-liveness-pull-prompt.md`(后置 pull 提醒版)。
|
||||
|
||||
## 用在哪 / 增删改查
|
||||
- **C / U(upsert)**:`register_or_update`(`POST /device/register`,App 拿到 push token 时调)按 `(user_id, device_id)` upsert,只在传入非空时更新 `registration_id`/`platform`/`app_version`;`touch_heartbeat`(`POST /device/heartbeat`,无障碍服务存活时周期调,**心跳也能自注册**)在 `accessibility_enabled=true` 时刷 `last_heartbeat_at`、置 `ever_protected=true`、状态机重置回 `alive`、清 `notified_at`(掉线恢复→下次再断才再推一条)。
|
||||
- **U(worker)**:`mark_notified`(`heartbeat_monitor_worker` 检出掉线后)置 `liveness_state='notified'` + `notified_at` + **`kill_alert_pending=True`**;`ack_kill_alert`(`POST /device/liveness/ack`,客户端弹过引导后)清 `kill_alert_pending`(幂等)。
|
||||
- **R**:`list_overdue`(worker 扫描:`ever_protected=true` + `liveness_state='alive'` + `last_heartbeat_at` 早于 `now - timeout`)→ 掉线设备列表;`get_device`(`GET /device/liveness`,客户端进 App 拉本机是否被判掉线过)。
|
||||
- **D**:无。
|
||||
|
||||
## 字段
|
||||
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|
||||
|---|---|---|---|
|
||||
| `id` | Integer | PK, autoincrement | |
|
||||
| `user_id` | Integer | **FK→user.id**, index, NOT NULL | 归属用户 |
|
||||
| `device_id` | String(128) | index, NOT NULL | 客户端 `DeviceId.get()` 生成的 per-install id(如 `device_Pixel_ab12cd34`) |
|
||||
| `registration_id` | String(64) | nullable | 极光推送 registration id;`JCollectionAuth` 同意后才下发,拿到才填 |
|
||||
| `platform` | String(16) | NOT NULL, default `android` | |
|
||||
| `app_version` | String(32) | nullable | 上报时 App 版本 |
|
||||
| `ever_protected` | Boolean | NOT NULL, default false | 收到过 service 心跳即 true(=该设备开过无障碍,功能对它有意义)。`list_overdue` 的过滤前提 |
|
||||
| `last_heartbeat_at` | DateTime(tz) | index, nullable | 最近一次 service 心跳时间(存活证明);超时即视为保护掉线 |
|
||||
| `last_report_protection_on` | Boolean | NOT NULL, default false | 最近一次上报的无障碍开关状态(观测用) |
|
||||
| `liveness_state` | String(16) | NOT NULL, default `unknown` | 状态机:`unknown` → `alive`(收到 service 心跳)→ `silent`/`notified`(扫描发现超时并已推送);心跳恢复 handler 重置回 `alive` |
|
||||
| `notified_at` | DateTime(tz) | nullable | 最近一次推送告警时间 |
|
||||
| `kill_alert_pending` | Boolean | NOT NULL, default false | 掉线告警「待客户端提醒」标记(后置 pull 版)。**与 `liveness_state` 解耦**:worker 检出掉线即置 True;`touch_heartbeat`(心跳恢复)不动它;只由客户端 ack 清 |
|
||||
| `created_at` | DateTime(tz) | server_default now() | |
|
||||
| `updated_at` | DateTime(tz) | server_default now(), onupdate now() | |
|
||||
|
||||
## 关系 / Join Key
|
||||
- `user_id` → `user.id`(多对一,**硬外键**)。
|
||||
- 与 `comparison_record.device_id` 等表的 `device_id` **同源**(都来自客户端 per-install id),但本表无 id 级 join。
|
||||
|
||||
## 索引与约束
|
||||
- PK `id`;index `user_id`、`device_id`、`last_heartbeat_at`;UNIQUE(`user_id`, `device_id`) = `uq_device_liveness_user_device`(一用户一设备一行,upsert 幂等键)。
|
||||
|
||||
## 注意
|
||||
- **`kill_alert_pending` 为什么和 `liveness_state` 解耦**:服务随 App 重启会先发心跳把 `state` 重置回 `alive`,若复用 `state` 判「待提醒」,客户端进 App 这一刻可能恰好已被重置 → 漏看这次掉线。故另设一个只由 worker 置、只由客户端 ack 清的标记,规避竞态。
|
||||
- **`list_overdue` 本期不要求有 `registration_id`**:本期只做终端打印检测、未真推送,没接极光 token 的设备也要检出。
|
||||
- 心跳超时阈值由 worker 的 `timeout_minutes` 决定(不在表里)。
|
||||
@@ -0,0 +1,36 @@
|
||||
# launch_confirm_sample — 启动确认窗 agent 兜底样本
|
||||
|
||||
> 模型 `app/models/launch_confirm_sample.py`(`LaunchConfirmSample`) · 仓库 `app/repositories/launch_confirm_sample.py`(`insert_sample`) · 接口 `POST /internal/launch-confirm-sample`(pricebot→app-server,`app/api/internal/launch_confirm.py`,`X-Internal-Secret` 校验) · [← 索引](./README.md) · [总览](./OVERVIEW.md)
|
||||
|
||||
pricebot 的 `launch_confirm_agent` 每次「静态 PROFILES 没认出、靠 LLM 兜底放行」一个跨 App 启动确认窗(繁体 / 英文 / 小语种 / ROM 改版的「想要打开 XX」弹窗)时,把完整样本 server→server 落这里。研发定期**人工**把 `exec_success=true` 的样本沉淀进 pricebot 的 `launch_confirm.PROFILES`(静态快路径),让以后同款窗不必再调 LLM(回到快路径)。
|
||||
|
||||
> **不是给客户端的接口**:与登录无关、不鉴权,靠共享密钥头 `X-Internal-Secret`(== `settings.INTERNAL_API_SECRET`)校验,未配置则 503。
|
||||
|
||||
## 用在哪 / 增删改查
|
||||
- **C(插入)**:`insert_sample`(`POST /internal/launch-confirm-sample`)。**都上报、不去重**(by design):同一种窗多台机器上报多条,研发按 `host_package`/`system_locale` 聚合后人工筛。大字段截断到列长上限(繁体长标题等),空串入库为 None。
|
||||
- **R / U / D**:**无**(研发查库靠 SQL 直接看 + 人工沉淀,不经本服务接口)。
|
||||
|
||||
## 字段
|
||||
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|
||||
|---|---|---|---|
|
||||
| `id` | Integer | PK, autoincrement | |
|
||||
| `created_at` | DateTime(tz) | server_default now(), index | 上报时间 |
|
||||
| `trace_id` | String(64) | index, nullable | pricebot 侧 trace_id:回指原始 trace,可去 `price.shaguabijia.com` 查完整上下文 |
|
||||
| `device_id` | String(64) | index, nullable | 上报设备 |
|
||||
| `host_package` | String(128) | index, nullable | 弹窗宿主包(= 触发判据,也是研发沉淀进 `PROFILES.host_packages` 的键)。**按它聚合人工筛** |
|
||||
| `target_app` | String(128) | nullable | 被打开的目标 App 包名(标题里「想要打开 X」的 X 是变量,据此把它从 sign 里挖空) |
|
||||
| `system_locale` | String(32) | index, nullable | 系统语言地区(`zh-TW`/`en-US`…)—— 多语言问题的核心维度。⚠️ 前端 `AgentStepRequest` 暂未上报设备信息 → **现恒 None**,待前端加 `device_info` 字段后回填 |
|
||||
| `exec_success` | Boolean | NOT NULL, default false | 这次兜底是否成功放行(弹窗消失=true;放弃 / LLM 没认出=false)。**研发只沉淀 true 的** |
|
||||
| `dialog_title` | String(256) | nullable | 弹窗标题全文(研发据此提 `launch_sign`),如「『傻瓜比价』想要開啟『京東』」 |
|
||||
| `payload` | JSON(PG: JSONB) | nullable | 完整样本大 JSON:`{is_launch_confirm, llm_model, llm_reason, plan, dialog_tree, device_info:{screen,locale,brand,model,android_version,rom_version}}`。加字段不必迁移 schema |
|
||||
|
||||
## 关系 / Join Key
|
||||
- 无外键。`trace_id` 软指 pricebot trace(回指排查),`device_id` 软指设备;均不与本服务其它表 join。
|
||||
|
||||
## 索引与约束
|
||||
- PK `id`;index `created_at`、`trace_id`、`device_id`、`host_package`、`system_locale`。
|
||||
- **无唯一约束**(by design 不去重,见上)。
|
||||
|
||||
## 注意
|
||||
- **PG 上 `payload` 用 JSONB**(可建 GIN 索引),SQLite(本地/测试)退化为通用 `JSON`(同 `price_observation`、`comparison_record`)。
|
||||
- **为什么大字段塞 `payload` 而非拆列**:弹窗树快照 + LLM plan + 设备信息都是「研发偶尔人工看」的兜底数据,拆列会让每次扩样本字段都要迁移 schema;只把聚合/筛选要用的维度(`host_package`/`system_locale`/`exec_success`)提成结构化列建索引。
|
||||
+11
-17
@@ -92,23 +92,15 @@ alembic upgrade head
|
||||
- 生成后**手动把文件名前缀去掉**(内部 id 别动);或
|
||||
- 在 `alembic.ini` 配 `file_template = %%(slug)s`(注意 ini 里 `%` 要写成 `%%`)让以后直接生成无前缀文件名(缺点:同名 slug 会冲突,描述写具体点)。
|
||||
|
||||
## 6. 当前迁移链(12 条,含合并迁移)
|
||||
```
|
||||
init_user_table ← 起点(down_revision=None)
|
||||
→ welfare_tables_coin_account_coin_txn 金币账户/金币流水/签到/任务表
|
||||
→ cash_transaction_table 现金流水表
|
||||
→ savings_record_table 省钱明细表
|
||||
→ withdraw_order_and_user_openid 提现单表 + user.wechat_openid
|
||||
→ user_wechat_nickname_avatar user 微信昵称/头像字段
|
||||
→ unique_wechat_openid user.wechat_openid 唯一约束
|
||||
→ savings_shop_dishes 省钱明细 shop_name + dishes
|
||||
→ ad_reward_record 看广告发奖记录表
|
||||
→ (此处分叉,两条并行 head)
|
||||
├→ feedback_table 帮助与反馈表(PR #4)
|
||||
└→ ef96beb47b1e_convert_savings_record_dishes_from_json dishes 列改 JSONB(PR #6)
|
||||
→ f01db5d77dac_merge_pg_jsonb_and_feedback_heads (head,最新) 合并两条分叉
|
||||
```
|
||||
> 顺序以 `alembic history` 输出为准(按 `down_revision` 链,不是文件名字母序)。
|
||||
## 6. 当前迁移链(规模与当前 head)
|
||||
- `alembic/versions/` 当前约 **78 个迁移文件**(含 14 个合并迁移),从 `init_user_table`(`down_revision=None`)起一路接到当前单一 head。**别背具体链**——它随每次改表增长,看实时输出:
|
||||
```bash
|
||||
ls alembic/versions/*.py | wc -l # 迁移文件数
|
||||
alembic history # 完整链(按 down_revision,不是文件名字母序)
|
||||
alembic heads # 当前 head(正常只 1 个)
|
||||
```
|
||||
- **当前 head**(2026-06-23):`4dc2af7ebe74`(文件 `4dc2af7ebe74_merge_device_65_and_comparison_token_.py`)——它合并了今日两条并行链:`comparison_token_cols`(#a584e1f,`comparison_record` 加 `input_tokens`/`output_tokens`)与 #65 device 链末端(`device_liveness` 表 + `ad_feed_reward_trace_id`)。即 `down_revision = ('comparison_token_cols', 'ad_feed_reward_trace_id')`。
|
||||
- 频繁多 head 是常态(多人/多分支并行改表),靠合并迁移收敛回单 head,详见下面「踩坑 → 多 head 分叉」。
|
||||
|
||||
## 7. 踩坑
|
||||
|
||||
@@ -128,6 +120,8 @@ please specify a specific target revision, '<branchname>@head'...
|
||||
- PR #6 (PG/JSONB) 加了 `ef96beb47b1e`,`down_revision='c8d9e0f1a2b3'`(也是 ad_reward) ← 跟 #4 同源,撞车
|
||||
- 两个 PR 都合进 main 后,`alembic heads` 出现两个
|
||||
|
||||
> 此后这种合并是常态,每次都用 `alembic merge` 收敛。最近一次(2026-06-23):#65 device 链(`device_table` → `device_kill_alert_pending`,经 `ad_feed_reward_trace_id`)与 `comparison_token_cols`(`comparison_record` 加 token 列)并行,用 `4dc2af7ebe74_merge_device_65_and_comparison_token_.py` 合成单一 head。
|
||||
|
||||
**修复**:
|
||||
```bash
|
||||
alembic heads # 列出冲突的两个 head id
|
||||
|
||||
Reference in New Issue
Block a user