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
+45 -3
View File
@@ -3,7 +3,7 @@
> Base URL:生产 `https://app-api.shaguabijia.com`;本地联调 `http://<开发机>:8770`
> 协议:HTTP / JSON,请求与响应体均 `application/json`,字段统一 **snake_case**
> 鉴权:需鉴权的接口在请求头带 `Authorization: Bearer <access_token>`
> 最后更新:2026-06-18(补录比价透传端点 `intent/precoupon/step`、`intent/step`、`trace/finalize` 与 `meituan/top-sales` 到总览;上一次功能更新 2026-06-07 金币数值体系一期
> 最后更新:2026-06-23(补全此前缺整族的端点:device 无障碍存活监控、internal 内部回写、CPS 短链落地、report/invite/wxpay,及 platform/coupon/feedback/user 的零散读端点;device/internal/cps-redirect 三族新建单文件文档。上一次 2026-06-18 补录比价透传端点与 `meituan/top-sales`
> 架构:`app/api/v1/` 只放很轻的接口层;穿山甲/微信支付/极光/短信/美团等 SDK 集成的重逻辑在 `app/integrations/`,实现细节见 [docs/integrations/](../integrations/README.md)。
---
@@ -19,7 +19,14 @@
| 5 | `POST /api/v1/auth/refresh` | 无 | [详情](./auth-refresh.md) |
| 6 | `GET /api/v1/auth/me` | Bearer | [详情](./auth-me.md) |
| 7 | `POST /api/v1/auth/logout` | Bearer | [详情](./auth-logout.md) |
| 8 | `POST /api/v1/coupon/step` | 无 | [详情](./coupon-step.md) |
| 8 | `POST /api/v1/coupon/step` | 无 | [详情](./coupon-step.md)(透传 pricebot + best-effort 写 `coupon_*` 三表) |
| 8a | `POST /api/v1/coupon/prompt/shown` | 无 | 引导窗弹出即上报(按 device+package+日记 `shown`,今天这个 App 不再自动弹)(无单独文档) |
| 8b | `POST /api/v1/coupon/prompt/dismiss` | 无 | 用户拒绝/关闭引导窗(透传链路看不到拒绝,客户端通知)(无单独文档) |
| 8c | `GET /api/v1/coupon/prompt/should-show` | 无 | 切到外卖 App 时是否还应弹引导窗(`device_id`+`package`)(无单独文档) |
| 8d | `POST /api/v1/coupon/prompt/reset` | 无 | 重置今日引导窗 engagement(开发测频控用)(无单独文档) |
| 8e | `GET /api/v1/coupon/completed-today` | 无 | 这台设备今天是否已跑完整轮领券(首页「去领取」卡置灰源)(无单独文档) |
| 8f | `POST /api/v1/coupon/completed-today/reset` | 无 | 重置今日已完成(开发用)(无单独文档) |
| 8g | `GET /api/v1/coupon/stats` | Bearer | 累计领券数(「我的」页战绩卡;按 user_id 聚合,**鉴权**)(无单独文档) |
| 9 | `POST /api/v1/meituan/coupons` | 无 | [详情](./meituan-coupons.md) |
| 10 | `POST /api/v1/meituan/feed` | 无 | [详情](./meituan-feed.md) |
| 11 | `POST /api/v1/meituan/referral-link` | 无 | [详情](./meituan-referral-link.md) |
@@ -38,6 +45,19 @@
| **比价战绩里程碑**(前缀 `/api/v1/compare`;福利页「记录比价战绩」,按成功比价数解锁逐档发金币) |||
| 12d | `GET /api/v1/compare/milestones` | Bearer | [详情](./compare-milestones.md) |
| 12e | `POST /api/v1/compare/milestones/{milestone}/claim` | Bearer | [详情](./compare-milestone-claim.md) |
| **设备 / 无障碍存活监控**(前缀 `/api/v1/device`;心跳超时检出 + 掉线召回,#65 |||
| D1 | `POST /api/v1/device/register` | Bearer | [详情](./device-liveness.md)(注册设备/更新极光 push token |
| D2 | `POST /api/v1/device/heartbeat` | Bearer | [详情](./device-liveness.md)(无障碍服务存活心跳,心跳也能自注册) |
| D3 | `GET /api/v1/device/liveness` | Bearer | [详情](./device-liveness.md)(进 App 查本机是否被判掉线过) |
| D4 | `POST /api/v1/device/liveness/ack` | Bearer | [详情](./device-liveness.md)(确认已弹引导,清掉线告警) |
| **上报更低价**(前缀 `/api/v1/report`;众包纠偏,人工审核发奖) |||
| R1 | `POST /api/v1/report` | Bearer | 提交上报(multipart:`comparison_record_id`/`reported_platform_id`/`reported_price`(元) + 1~4 张截图;原最低价反查 `comparison_record.best_*` 校验须更低)(无单独文档) |
| R2 | `GET /api/v1/report/records` | Bearer | 上报记录列表(`?status=` pending/approved/rejected 可选筛选)(无单独文档) |
| **好友邀请**(前缀 `/api/v1/invite`;注册即生效,双方各发 1 万金币) |||
| I1 | `GET /api/v1/invite/me` | Bearer | 我的邀请码 + 分享链接 + 已邀人数/已得金币(无单独文档) |
| I2 | `GET /api/v1/invite/invitees` | Bearer | 我邀请的人列表(`limit`/`offset` 分页)(无单独文档) |
| I3 | `POST /api/v1/invite/landing-track` | 无 | 落地页 `dl.html` 访问上报指纹(剪贴板归因兜底;浏览器无 token)(无单独文档) |
| I4 | `POST /api/v1/invite/bind` | Bearer | 绑定邀请人;支持 clipboard/manual 邀请码 + fingerprint 指纹反查三种归因(无单独文档) |
| **钱包 / 我的资产**(前缀 `/api/v1/wallet` |||
| 14 | `GET /api/v1/wallet/account` | Bearer | [详情](./wallet-account.md) |
| 15 | `GET /api/v1/wallet/coin-transactions` | Bearer | [详情](./wallet-coin-transactions.md) |
@@ -68,15 +88,37 @@
| 35 | `POST /api/v1/ad/ecpm-report` | Bearer | [详情](./ad-ecpm-report.md) |
| 35a | `POST /api/v1/ad/feed-reward` | Bearer | [详情](./ad-feed-reward.md) |
| 35b | `POST /api/v1/ad/reward-noshow` | Bearer | [详情](./ad-reward-noshow.md)(激励视频提前关闭/未发奖留痕,只记原因不发币) |
| 35c | `GET /api/v1/ad/feed-reward/units` | Bearer | 信息流广告今日已发份数/上限(配合 `feed-reward` 看进度)(无单独文档) |
| **用户资料**(前缀 `/api/v1/user` |||
| 35 | `PATCH /api/v1/user/profile` | Bearer | [详情](./user-profile.md) |
| 36 | `POST /api/v1/user/avatar` | Bearer | [详情](./user-avatar.md) |
| 36a | `POST /api/v1/user/onboarding/complete` | Bearer | 标记新手引导完成(按 账号+device_id 幂等,跨卸载重装持久)(无单独文档) |
| 36b | `GET /api/v1/user/onboarding/status` | Bearer | 查该 (账号,设备) 是否走过引导(运营在 admin 删记录即触发重走)(无单独文档) |
| 37 | `DELETE /api/v1/user` | Bearer | [详情](./user-delete.md) |
| **帮助与反馈**(前缀 `/api/v1/feedback` |||
| 38 | `POST /api/v1/feedback` | Bearer | [详情](./feedback.md) |
| **首页门面数据**(前缀 `/api/v1/platform`;全平台展示数字,登录前可读) |||
| 38a | `GET /api/v1/feedback/config` | Bearer | 反馈页「加群二维码」卡配置(开关 + 二维码图 + 三行文案)(无单独文档) |
| 38b | `GET /api/v1/feedback/records` | Bearer | 我的反馈历史(pending/adopted/rejected(无单独文档) |
| **首页门面数据 / 客户端配置**(前缀 `/api/v1/platform`;全平台展示数字 + 运营开关,**全部不鉴权**,登录前可读) |||
| 39 | `GET /api/v1/platform/stats` | 无 | [详情](./platform-stats.md) |
| 40 | `GET /api/v1/platform/savings-feed` | 无 | [详情](./platform-savings-feed.md) |
| 40a | `GET /api/v1/platform/flags` | 无 | 客户端运营 feature flag(比价/领券期广告开关等),拉取后缓存(无单独文档) |
| 40b | `GET /api/v1/platform/ad-config` | 无 | 客户端拉广告配置(穿山甲 app_id + 各位 ID + 各场景开关;不含验签密钥)(无单独文档) |
| 40c | `GET /api/v1/platform/app-version` | 无 | 最新 App 版本(OTA 检查更新;与本机 versionCode 比)(无单独文档) |
| **微信支付回调**(前缀 `/api/v1/wxpay` |||
| W1 | `POST /api/v1/wxpay/transfer-auth-notify` | 无 | 免确认收款授权结果通知(一期 stub:仅应答 200 不验签不改账,授权状态靠主动查询兜底)(无单独文档) |
| **CPS 群发短链落地**(**无前缀**,挂域名根;公网不鉴权) |||
| C1 | `GET /c/{code}` | 无 | [详情](./cps-redirect.md)(短链落地:微信授权拿 openid + 记点击 + 302 跳/淘宝 H5 落地页) |
| C2 | `POST /c/{code}/copy` | 无 | [详情](./cps-redirect.md)(淘宝落地页点「复制口令」记 `copy` |
| C3 | `GET /wx/oauth/cb` | 无 | [详情](./cps-redirect.md)(微信网页授权回调;upsert `cps_wx_user` + 种 cookie,`include_in_schema=False` |
| C4 | `GET /MP_verify_*.txt` | 无 | [详情](./cps-redirect.md)(微信「网页授权域名」归属校验文件,`include_in_schema=False` |
| **内部回写端点**(前缀 `/internal`;pricebot/发布流程→app-server,**`X-Internal-Secret` 头**,非客户端接口) |||
| N1 | `POST /internal/price-observation` | 内部密钥 | [详情](./internal.md)(比价价格事实批量落 `price_observation` |
| N2 | `GET /internal/store-mapping/lookup` | 内部密钥 | [详情](./internal.md)(按源平台店名反查目标平台已沉淀店铺 id/deeplink |
| N3 | `POST /internal/store-mapping` | 内部密钥 | [详情](./internal.md)(跨平台店铺身份映射落 `store_mapping` |
| N4 | `POST /internal/store-mapping/invalidate` | 内部密钥 | [详情](./internal.md)(标记某平台 shopId 缓存 deeplink 失效) |
| N5 | `POST /internal/launch-confirm-sample` | 内部密钥 | [详情](./internal.md)(启动确认窗兜底样本落 `launch_confirm_sample` |
| N6 | `POST /internal/app-version` | 内部密钥 | [详情](./internal.md)(发布流程写最新 App 版本,落 `app_config` |
| **静态资源**StaticFiles 挂载,见下方 `/media` 静态服务) |||
| - | `GET /media/avatars/<file>` | 无 | 用户头像;返回二进制图片 |
| - | `GET /media/feedback/<file>` | 无 | 反馈截图;返回二进制图片 |
+30
View File
@@ -0,0 +1,30 @@
# CPS 群发短链落地(cps-redirect 族)
> 所属:cps-redirect 组(**无前缀**,挂域名根;源 `app/api/v1/cps_redirect.py`) | 鉴权:**公网无鉴权**(群里任何人点都要能跳/能领) | [← 返回 API 索引](./README.md)
>
> 落库:点击落 [`cps_click`](../database/cps_click.md)、微信落地页用户落 [`cps_wx_user`](../database/cps_wx_user.md);短链由 [`cps_link`](../database/cps_link.md) 解析。
运营把 `/c/{code}` 短链发到微信群,用户点击 → 记点击 → 按平台 302 跳转(美团/京东)或返回 H5 淘宝落地页。**记点击/写用户失败绝不阻断用户跳转/领券**(try/except 吞掉)。
## GET /c/{code} — 短链落地
- 微信内 + 还没拿到 openid + 配了服务号 → 先 `snsapi_base` 静默授权拿 openid,再 302 回本页。
- 非微信 / 已有 openid cookie / 未配服务号 → 直接走原逻辑(openid 可能为 None,**绝不阻断**)。
-`visit`(写 `cps_click`)后:**美团/京东** 302 跳 `target_url`;**淘宝** 返回 H5 落地页(主视觉图 + 「复制口令」按钮)。
- `code` 不存在/失效 → 302 跳兜底(`https://www.meituan.com/`)。
**入参**:路径 `code`(短链码)。**出参**:`302 Redirect``200 HTML`(淘宝)。
## POST /c/{code}/copy — 淘宝落地页点了「复制口令」
淘宝落地页是 H5,用户点「复制口令」才算转化 → 记一条 `copy` 事件(带 openid cookie)。
**入参**:路径 `code`;body 空。**出参**:`{ "ok": true }`
## GET /wx/oauth/cb — 微信网页授权回调(`include_in_schema=False`)
`state='base:{code}'`(静默拿 openid)或 `'uinfo:{code}'`(补昵称头像)。换 openid(+userinfo)→ upsert [`cps_wx_user`](../database/cps_wx_user.md) → 种 cookie(`wx_openid` / `wx_uinfo`,30 天)→ 302 回落地页。**任何失败兜底回落地页,绝不阻断领券**。`uinfo` 回流带 `?authed=1`,落地页据此自动触发复制(把「点领券→授权→复制」衔接成一步)。
## GET /MP_verify_*.txt — 微信「网页授权域名」归属校验(`include_in_schema=False`)
微信请求 `coupon.shaguabijia.com/MP_verify_xxx.txt` 验证域名归属;端点放域名根、返回文件名核心串(纯文本)。为微信网页授权(拿 openid)接入做准备。
## 说明
- **两阶授权**:`base` 静默拿 openid(统计主力、不打扰);`userinfo` 在用户点「领券」时触发(交互触发避免微信快照页),拿昵称/头像/unionid。
- **`cps_click` 不阻断**:落地/跳转优先级高于统计——丢几条点击可接受,挡住用户领券不可接受。
+52
View File
@@ -0,0 +1,52 @@
# 设备 / 无障碍存活监控(device 族)
> 所属:device 组(前缀 `/api/v1/device`,源 `app/api/v1/device.py`) | 鉴权:**全部 Bearer**(设备绑登录用户) | [← 返回 API 索引](./README.md)
>
> 落库:[`device_liveness`](../database/device_liveness.md) 表;后台 `heartbeat_monitor_worker` 据此检出心跳超时的设备并召回。设计见 spec `accessibility-liveness-push.md`(推送版)+ `accessibility-liveness-pull-prompt.md`(后置 pull 提醒版)。#65 新增。
无障碍服务存活监控链路:客户端无障碍服务存活时周期上报心跳,后端发现「曾保护过、现已超时」的设备 → 召回(推送 / 客户端进 App 弹「开启自启动」引导)。
## POST /register — 注册设备 / 更新推送 token
App 前台、拿到极光 push token 时调。按 `(user_id, device_id)` upsert。
**入参(JSON)**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `device_id` | string | ✅ | 客户端 `DeviceId.get()` 生成的 per-install id |
| `registration_id` | string \| null | ❌ | 极光推送 registration id(`JCollectionAuth` 同意后才有) |
| `platform` | string | ❌ | 默认 `android` |
| `app_version` | string \| null | ❌ | App 版本 |
**出参**:`DeviceOut`(设备当前状态)。
## POST /heartbeat — 上报心跳
无障碍服务存活时周期调(**心跳也能自注册**)。`accessibility_enabled=true` 时刷新 `last_heartbeat_at`、状态机置 `alive`、清掉线告警。
**入参(JSON)**
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `device_id` | string | ✅ | 同上 |
| `accessibility_enabled` | bool | ✅ | 本机无障碍开关当前是否开 |
| `registration_id` | string \| null | ❌ | 顺带更新 push token |
**出参**:`{ "ok": true }`
## GET /liveness — 查本机掉线告警(后置检测)
客户端进 App 时拉:本机是否被判定掉线过(`kill_alert_pending`)。命中 → 客户端弹「开启自启动」引导,之后调 `/liveness/ack` 清。设备从未注册过 → 返回默认(无告警)。
**入参(query)**:`device_id`(string,必填)。
**出参**:`LivenessOut`(含 `kill_alert_pending` 等)。
## POST /liveness/ack — 确认已提醒(清掉线告警)
客户端已弹过引导 → 清 `kill_alert_pending`(幂等;下次真掉线 worker 再置)。
**入参(JSON)**:`{ device_id }`
**出参**:`{ "ok": true }`
## 错误
- `401` 未鉴权 / token 失效。
- `422``device_id` 或字段类型不符。
## 说明
- **`kill_alert_pending``liveness_state` 解耦**:worker 检出掉线置 True,心跳恢复(`/heartbeat`)只重置 state、不动此标记,只由 `/liveness/ack` 清 → 规避「服务随 App 重启先发心跳把 state 重置回 alive、客户端进 App 漏看这次掉线」竞态。
- 心跳超时阈值由 worker 配置决定,不在接口里。
+31
View File
@@ -0,0 +1,31 @@
# 内部回写端点(internal 族,pricebot / 发布流程 → app-server)
> 所属:internal 组(前缀 `/internal`,源 `app/api/internal/`) | 鉴权:**`X-Internal-Secret` 头**(== `settings.INTERNAL_API_SECRET`) | [← 返回 API 索引](./README.md)
**不是给客户端的接口**:不走用户 JWT。`§6` 是 app-server 透传给 pricebot,这里反过来——pricebot(及发布流程)把少量数据 server→server 回写 app-server 落库。
## 鉴权(所有 internal 端点共用)
请求头带 `X-Internal-Secret: <密钥>`,服务端常量时间比对 `settings.INTERNAL_API_SECRET`:
- 密钥**未配置**(默认空)→ `503`(挡裸奔写端点)。
- 不匹配 / 缺失 → `401`
- 需 pricebot 与 app-server 两边 `.env` 配**同一** `INTERNAL_API_SECRET` 才生效(未配则 pricebot 侧跳过上报、不影响比价/领券)。
## 端点
| 方法 + 路径 | 落库 | 说明 |
|---|---|---|
| `POST /internal/price-observation` | [`price_observation`](../database/price_observation.md) | 比价 done 帧整批价格事实上报;`(trace_id, platform, scope)` 幂等,返回 `{inserted, skipped}` |
| `GET /internal/store-mapping/lookup` | (只读 [`store_mapping`](../database/store_mapping.md)) | 比价前按 `source_platform`+`name`(+`lat`/`lng`)反查各目标平台已沉淀的店铺 id/deeplink;命中→pricebot 直接 deeplink 省现场搜店 |
| `POST /internal/store-mapping` | `store_mapping` | 跨平台「同一家店」身份映射上报;`trace_id` 幂等、填空合并,返回 `{inserted, row_id}` |
| `POST /internal/store-mapping/invalidate` | `store_mapping` | 标记某平台 `shop_id` 的缓存 deeplink 失效(pricebot 撞错误页回退时报);当前支持 `taobao`/`jd`,其它平台 no-op,返回 `{ok, affected}` |
| `POST /internal/launch-confirm-sample` | [`launch_confirm_sample`](../database/launch_confirm_sample.md) | 启动确认窗 LLM 兜底放行后回写样本(host 包 + 弹窗树 + plan + locale);**都上报、不去重**,返回 `{id}` |
| `POST /internal/app-version` | [`app_config`](../database/app_config.md)(key=`latest_app_version`) | **发布流程**(非 pricebot)出 APK 后写最新版本号/下载链接/sha256;客户端再 `GET /api/v1/platform/app-version` 读做 OTA。也是应急改版本信息(紧急下线/改 `apk_url`)入口 |
## 错误
- `401` 密钥不匹配 / 缺失。
- `503` `INTERNAL_API_SECRET` 未配置。
- `422` 请求体字段校验失败。
## 说明
- 与不鉴权的比价透传(`compare.py` / `coupon.py`)无关:那两个是**客户端→server** 的透传壳,这里是 **server→server** 的内部写库。
- `launch-confirm-sample` 的样本供研发**人工**定期沉淀进 pricebot 的 `launch_confirm.PROFILES` 静态规则(让以后同款启动窗回到快路径,不必再调 LLM)。