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:
2026-06-24 04:07:54 +08:00
parent 128db7fb86
commit 43f28d358b
12 changed files with 339 additions and 57 deletions
+27 -27
View File
@@ -3,7 +3,7 @@
> 域名:`app-api.shaguabijia.com`(HTTPS,nginx 反代)
> 仓库:`shaguabijia-app-server`
> 接口协议详见 [`docs/api/`](./api/)(索引 + 一接口一文件)
> 最后更新:2026-05-31
> 最后更新:2026-06-23(§7 数据模型改为指向 OVERVIEW、§5 美团 CPS 对齐多 tab feed/4 接口、§6 厘清 coupon/step 有写库副作用 ≠ 纯透传壳、§6.5 补 `/internal/app-version`、Alembic 迁移数更新)
---
@@ -23,7 +23,7 @@
| **看广告发奖** | 穿山甲 GroMore 激励视频 S2S 回调(SHA256 验签)+ 4 态 CTA 冷却 |
| **帮助与反馈** | 用户提交反馈(含截图)+ 静态 `/media` 服务 |
**本服务自身没有"比价"实现**——比价/领券业务的核心都在 pricebot-backend(另一个 repo),本服务对 step 类接口做"透传壳"。无爬虫、无 LLM,但**有钱包/福利等业务模型**(数据模型从早期 1 张 `user` 表已扩展到 10 张,见 §7)。
**本服务自身没有"比价"实现**——比价/领券业务的核心都在 pricebot-backend(另一个 repo),本服务对 step 类接口做"透传壳"。无爬虫、无 LLM,但**有钱包/福利等业务模型**(数据模型从早期 1 张 `user` 表已扩展到 **40 张业务表**,见 §7 / [database/OVERVIEW.md](./database/OVERVIEW.md))。
---
@@ -188,32 +188,44 @@ POST /api/v1/auth/sms/login { phone, code } → 任意 6 位通过 → upsert
> 美团请求字段 `listTopiId`(少个 c)是官方拼写,非笔误。经纬度 `*1_000_000` 取整。
### 5.2 个接口
### 5.2 对外四个接口
- `query_coupon`:底层取数,被 `coupons` 路由和 `feed` 路由共用
- `coupons` 路由:对外的搜索/榜单接口,**客户端暂未接入**。
- `feed` 路由:首页推荐流,每页并发拉外卖(platform=1)+ 到店(platform=2,到餐)各一次,按 **2 外卖 + 1 到店** 交叉去重(`_interleave`);榜单组合写死 3 页(`_TOPIC_ROUNDS`:爆款 / 今日必推 / 外卖精选+到店限时),第 3 页 `has_next=false`、第 4 页返空
> 接口级入参/出参/各 tab 行为详见 [api/meituan-feed.md](./api/meituan-feed.md) 等,本节只讲后端形态
- `coupons`:对外的搜索/榜单接口(底层 `query_coupon`),**客户端暂未接入**
- `feed`:首页推荐流,**已是多 tab**(入参 `tab`):
- `rec` 智能推荐:走**离线库 `meituan_coupon`**(筛佣金率≥3%、`DISTINCT ON` 去重、按销量降序分页),**纯库查询、不打美团、不依赖 MT 凭证**(实测同城热销中位佣金 ~0.8%,实时筛≥3% 每页剩 0–1 条又撞 402,故从库出)。不显示距离。
- `distance` 距离最近:实时拉外卖+到店两路,按用户坐标由近及远。
- 默认(空 tab):旧的逐轮分页混合 feed(2 外卖 + 1 到店交叉,写死 3 页爆款/今日必推/精选+限时,第 4 页返空),**仅老客户端兼容**。
- 任何失败场景返 `200` + 空 `items` + `status=degraded`(不抛 5xx)。
- `top-sales`:独立的销量榜接口,离线库 `meituan_coupon` 按销量降序 + 跨源去重,**不实时打美团**。
- `referral-link`:换推广链接,客户端取 `link_map["3"]`(deeplink)优先跳美团 App。
- `query_coupon`:底层取数(被 `coupons`/`distance` feed 共用),不直接对外。
### 5.3 风险点
- **接口无鉴权**:任何人可调,消耗你的美团 appkey 能力/配额。
- **对外接口无鉴权**:任何人可调,消耗你的美团 appkey 能力/配额。
- **`sid` 可被客户端覆盖**:他人可借接口刷自己渠道分佣,建议服务端锁定。
- **`feed` 静默吞异常**:`_fetch_topic` 对美团错误 `except 返回 []` 且不打日志,导致美团失败时 `feed` 返回"200 + 空 items"无声失败。排查时改用 `coupons`(它以 502 暴露错误)
- **`feed` 失败返 `status=degraded`**:任何失败场景返 `200` + 空 `items` + `status=degraded`(不抛 5xx,避免前端弹报错 toast)。排查时看 `status` 字段区分 `empty`(真没数据)与 `degraded`(上游/库查询失败);或改用 `coupons`(它以 502 暴露错误)。`rec`/`top-sales` 走离线库,prod 刚部署 ETL 未灌则返 `empty`——开内测前先手动跑一次 ETL
---
## 6. 领券透传(coupon/step)
产品"一键领券"的核心接入点,**真正的领券逻辑不在本服务**——在另一个 repo `pricebot-backend`(GoalEngine + 事件驱动)。本服务只做"透传壳"
产品"一键领券"的核心接入点,**真正的领券逻辑不在本服务**——在另一个 repo `pricebot-backend`(GoalEngine + 事件驱动)。`coupon/step` **不是纯透传壳**:它在转发 pricebot 之余,还**best-effort 写库**(纯透传壳是 `compare.py``intent/recognize``price/step`,它们只转发不写库)
```
客户端 → POST /api/v1/coupon/step (任意 JSON body,含 device_id/trace_id/step)
→ coupon.py:不做 schema 校验,仅读 device_id/trace_id/step 打日志
async httpx 把 body 原样 POST 到 PRICEBOT_BASE_URL/api/coupon/step
→ coupon.py:不做 schema 校验,仅读 device_id/trace_id/step
[副作用1] step==0 → 写 coupon_prompt_engagement(claim_started)
→ async httpx 把 body 原样 POST 到 PRICEBOT_BASE_URL/api/coupon/step(按 trace_id 一致性 hash 选实例)
→ [副作用2] 从响应抽券结果(去重)→ 写 coupon_claim_record
→ [副作用3] action.command==done → 写 coupon_daily_completion(首页「去领取」卡置灰源)
→ 把 pricebot 的响应原样返回
```
> 三处写库(`_mark_engagement_blocking` / `_record_claims_blocking` / `_mark_completed_blocking`)全 **best-effort**:各自独立 session、经 `run_in_threadpool` 不阻塞事件循环,写失败只 `logger.warning`、绝不连累领券返回。判断维度是 `device_id`(`user_id` 可空旁路,仅资产留痕)。落库细节见 [database/coupon_state.md](./database/coupon_state.md)。
- **MVP 不鉴权**:已去掉 `CurrentUser` 依赖。`device_id` 透传给 pricebot 区分设备,但后端拿不到 `user_id` → 领券行为暂时绑不到登录用户(采集不到用户级画像)。待补 JWT,见 `待办与技术债.md` P1。
- **不做 schema 校验**:body 原样透传,pricebot 自己校验。
- **错误**:body 非合法 JSON → 400;pricebot 不可达或返回 5xx → 502。
@@ -228,27 +240,15 @@ POST /api/v1/auth/sms/login { phone, code } → 任意 6 位通过 → upsert
**启动确认窗兜底样本**(2026-06):国产 ROM 打开别的 App 时弹"想要打开 XX"确认窗,pricebot 对没见过文案(繁体/英文/ROM 改版)的窗用 LLM 兜底放行后,把样本 POST 到 [`/internal/launch-confirm-sample`](../app/api/internal/launch_confirm.py) 落 `launch_confirm_sample` 表(host 包 + 弹窗树 + LLM plan + 设备 locale/机型),供研发定期人工沉淀回 pricebot 的规则 yaml(回到快路径)。**需 pricebot 与 app-server 两边 `.env` 配同一 `INTERNAL_API_SECRET` 才生效**(未配则 pricebot 侧跳过上报、不影响比价/领券)。
> 同类内部回写端点还有 `/internal/price-observation`(价格观测)、`/internal/store-mapping`(跨平台店铺映射)等 pricebot 比价资产沉淀
> 同类内部回写端点(同走 `X-Internal-Secret`)还有:`/internal/price-observation`(价格观测)、`/internal/store-mapping` + `/store-mapping/lookup` + `/store-mapping/invalidate`(跨平台店铺映射沉淀/反查/失效)等 pricebot 比价资产沉淀;以及 **`/internal/app-version`(OTA)**——**发布流程**(非 pricebot)出 APK 后写最新版本号/下载链接/sha256 落 `app_config`,客户端再 `GET /api/v1/platform/app-version` 读做检查更新。完整 6 个内部端点见 [api/internal.md](./api/internal.md)
---
## 7. 数据模型
业务表(下表)+ `alembic_version` 框架表。生产 PG / 开发可回退 SQLite。
**完整表清单不在本文维护**(避免双份漂移)——共 **40 张业务表** + `alembic_version` 框架表,逐表字段级说明 + 跨表关系/写入路径见 **[database/OVERVIEW.md](./database/OVERVIEW.md)**(总览)与 [database/README.md](./database/README.md)(一表一文件索引)。生产 PG / 开发可回退 SQLite。
| 表 | models 文件 | 说明 |
|---|---|---|
| `user` | `models/user.py` | 用户(phone 唯一索引;注销时改成 `deleted_<id>` 释放约束) |
| `feedback` | `models/feedback.py` | 用户反馈(content/contact/images JSON 列/status) |
| `coin_account` | `models/wallet.py` | 用户的金币余额(`balance_cents` 单位:分) |
| `coin_transaction` | `models/wallet.py` | 金币流水(签到/任务/兑换/广告;`type` + `amount_cents`) |
| `cash_transaction` | `models/wallet.py` | 现金流水(兑换/提现) |
| `withdraw_order` | `models/wallet.py` | 提现单(`status` + `wxpay_batch_id`,微信支付商家转账) |
| `signin_record` | `models/signin.py` | 签到记录(每日一行,索引 `(user_id, signin_date)`) |
| `user_task` | `models/task.py` | 一次性任务领取记录 |
| `savings_record` | `models/savings.py` | 省钱明细(`dishes` 在 PG 上为 JSONB,SQLite 上是 TEXT) |
| `comparison_record` | `models/comparison.py` | 比价记录(每次比价完整明细;`(user_id, trace_id)` 唯一幂等;4 个 JSON(B) 列含 `raw_payload`;独立于 savings) |
| `ad_reward_record` | `models/ad_reward.py` | 看广告发奖(`trans_id` 唯一,幂等) |
本文只保留分层与高频维度的速查:数据访问统一在 `repositories/`,ORM 表结构在 `models/`(钱包/福利在 `wallet.py`/`signin.py`/`task.py`、比价在 `comparison.py`、领券今日状态在 `coupon_state.py`、CPS 群发在 `cps_*.py`、pricebot 内部沉淀在 `price_observation.py`/`store_mapping.py`/`launch_confirm_sample.py`、无障碍存活在 `device.py`)。下面单列最常用的 `user` 表字段。
**user 表字段速查**:
| 字段 | 类型 | 说明 |
@@ -265,7 +265,7 @@ POST /api/v1/auth/sms/login { phone, code } → 任意 6 位通过 → upsert
`upsert_user_for_login`:phone 存在则更新 `last_login_at`,不存在则注册(注册即登录)。
**Alembic 迁移**:`alembic/versions/` 当前 11 个迁移 + 1 个合并迁移(`f01db5d77dac_merge_pg_jsonb_and_feedback_heads.py`,2026-05-29 创建——实习生 PG 分支和 user-profile-feedback 分支并行开发时 down_revision 都挂在 `c8d9e0f1a2b3` 上,导致多 head,合并迁移把两条链合并)。详见 [数据库迁移.md](./database/数据库迁移.md)。
**Alembic 迁移**:`alembic/versions/` 当前**78 个迁移文件**(含 14 个合并迁移),当前单一 head `4dc2af7ebe74`(2026-06-23 合并 #65 device 链 + comparison token 列两条并行链)。多人/多分支并行改表频繁产生多 head,靠 `alembic merge` 收敛回单 head。规模随改表增长,**别背具体链**,以 `alembic history`/`alembic heads` 实时输出为准。详见 [数据库迁移.md](./database/数据库迁移.md)。
---