From 6c7eaa9734db1e982cfb84e20f24dcf62022e00a Mon Sep 17 00:00:00 2001 From: marco Date: Sun, 31 May 2026 10:53:18 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=B7=9F=E9=BD=90=205=20=E6=9C=88=20PR?= =?UTF-8?q?=20(#4=20user/feedback=20/=20#5=20ad-cooldown=20/=20#6=20PG=20/?= =?UTF-8?q?=20abd8eab=20meituan=20=E9=99=8D=E7=BA=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 整轮 doc 跟齐 2026-05 落地但 doc 没补全/没更新的内容. 按 P0~P3 优先级处理. P0 - 新 API doc + index 更新: * docs/api/user-profile.md (新) PATCH /api/v1/user/profile 改昵称 * docs/api/user-avatar.md (新) POST /api/v1/user/avatar 上传头像(魔数嗅探/旧文件清理) * docs/api/user-delete.md (新) DELETE /api/v1/user 注销账号(软删+匿名化) * docs/api/feedback.md (新) POST /api/v1/feedback 提交反馈(含截图) * docs/api/README.md 加 User/Feedback 组 + /media 静态服务段 + 修 UserOut "无接口可改恒为 null" 过时描述 P0 - 美团降级 / 现有 doc 校准: * docs/api/meituan-coupons.md 加 "未配 MT_CPS 凭证降级返空" 说明 * docs/api/meituan-referral-link.md 同上 + 客户端兜底建议 * docs/api/meituan-feed.md 区分两种空结果路径(未配 vs 调用失败) + /health.mt_cps_configured 排障入口 * docs/integrations/meituan.md "未配凭证降级" + "feed 静默吞错" 写进集成层 * ad-reward-status.md PR #5 commit 已自带 doc 更新, 复核无需再改 P1 - 主文档大改: * docs/后端技术实现.md - §1 能力表加 用户资料/外卖比价/钱包/福利/广告/反馈 (从 3 条扩到 9 条) - §1 数据模型纠正 "仅 1 张 user 表" -> 现有 10 张 + alembic_version - §2 DB 行 PG 主 / SQLite 兜底 + 加 psycopg3 行 - §3 目录树补 user.py / feedback.py / media.py / repositories/feedback.py / scripts 子项 / docs/postgres-migration.md - §7 数据模型重写: 10 张业务表速查表 + alembic 合并迁移 f01db5d77dac 始末 - §8 部署: 加 init_postgres.py 一句话流程 + checklist 加 "DATABASE_URL 切 PG" + "psycopg3 已装" + "MEDIA_ROOT 可写" - §10 已知问题: 加 美团降级 / agent 不鉴权 / MEDIA_ROOT serve / init_postgres bug / alembic 多 head 风险; 删过时 SQLite 行 - 接口计数 11 -> 38 * docs/待办与技术债.md - 已解决追加: PG 切换 / 外卖比价透传 / 用户资料反馈接口 / 美团降级 / 看广告 4 态冷却 - 新增 "P2 DB 与 Alembic 治理" 段: alembic 多 head 团队规约 (含本次撞坑实录) + init_postgres.py 已知 5 处小 bug + MEDIA_ROOT serve P2 - PG 迁移文档互指 + 迁移链补完: * docs/数据库迁移.md - 顶部加 postgres-migration.md / 后端技术实现.md 互指 - §6 迁移链 9 条 -> 12 条 (含 feedback_table / convert_dishes_jsonb / merge_pg_jsonb_and_feedback_heads) - §7 踩坑里 "多 head" 一行扩成完整故事 + 修复命令 + 团队预防规约 * docs/postgres-migration.md - 顶部加 数据库迁移.md / 后端技术实现.md §10 互指 P3 - 上线清单: * docs/看广告赚金币上线清单.md - 设计要点加 PR #5 4 态 CTA + 后端权威派生说明 + VIDEO_ROUND_REQUIRED_COUNT/VIDEO_ROUND_COOLDOWN_SECONDS 常量 - §E 端到端验收加 本轮冷却 / 弹窗 limit note / 跨设备一致 三项 所有最后更新日期同步到 2026-05-31. --- docs/api/README.md | 32 +++++++-- docs/api/feedback.md | 40 +++++++++++ docs/api/meituan-coupons.md | 5 +- docs/api/meituan-feed.md | 10 ++- docs/api/meituan-referral-link.md | 8 +-- docs/api/user-avatar.md | 26 +++++++ docs/api/user-delete.md | 18 +++++ docs/api/user-profile.md | 20 ++++++ docs/integrations/meituan.md | 15 +++- docs/postgres-migration.md | 7 +- docs/后端技术实现.md | 109 ++++++++++++++++++++++-------- docs/待办与技术债.md | 40 ++++++++++- docs/数据库迁移.md | 63 +++++++++++++---- docs/看广告赚金币上线清单.md | 8 ++- 14 files changed, 344 insertions(+), 57 deletions(-) create mode 100644 docs/api/feedback.md create mode 100644 docs/api/user-avatar.md create mode 100644 docs/api/user-delete.md create mode 100644 docs/api/user-profile.md diff --git a/docs/api/README.md b/docs/api/README.md index 311e27f..d1f4d08 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -3,7 +3,7 @@ > Base URL:生产 `https://app-api.shaguabijia.com`;本地联调 `http://<开发机>:8770` > 协议:HTTP / JSON,请求与响应体均 `application/json`,字段统一 **snake_case** > 鉴权:需鉴权的接口在请求头带 `Authorization: Bearer ` -> 最后更新:2026-05-27 +> 最后更新:2026-05-31 > 架构:`app/api/v1/` 只放很轻的接口层;穿山甲/微信支付/极光/短信/美团等 SDK 集成的重逻辑在 `app/integrations/`,实现细节见 [docs/integrations/](../integrations/README.md)。 --- @@ -52,6 +52,15 @@ | 32 | `GET /api/v1/ad/pangle-callback` | 验签 | [详情](./ad-pangle-callback.md) | | 33 | `GET /api/v1/ad/reward-status` | Bearer | [详情](./ad-reward-status.md) | | 34 | `POST /api/v1/ad/test-grant` | Bearer | [详情](./ad-test-grant.md) | +| **用户资料**(前缀 `/api/v1/user`) ||| +| 35 | `PATCH /api/v1/user/profile` | Bearer | [详情](./user-profile.md) | +| 36 | `POST /api/v1/user/avatar` | Bearer | [详情](./user-avatar.md) | +| 37 | `DELETE /api/v1/user` | Bearer | [详情](./user-delete.md) | +| **帮助与反馈**(前缀 `/api/v1/feedback`) ||| +| 38 | `POST /api/v1/feedback` | Bearer | [详情](./feedback.md) | +| **静态资源**(StaticFiles 挂载,见下方 `/media` 静态服务) ||| +| - | `GET /media/avatars/` | 无 | 用户头像;返回二进制图片 | +| - | `GET /media/feedback/` | 无 | 反馈截图;返回二进制图片 | > ⚠️ 美团三个接口当前**无鉴权**,且 `referral-link` 的 `sid` 允许客户端传值覆盖默认渠道——见各接口"备注"。 > `coupon/step` 及外卖比价的 `intent/recognize` / `price/step` 都透传到 pricebot-backend,**MVP 阶段均不鉴权**(device_id 透传,待补 JWT——见各接口详情)。 @@ -99,9 +108,9 @@ | 字段 | 类型 | 说明 | |---|---|---| | `id` | int | 用户主键 | -| `phone` | string | 手机号 | -| `nickname` | string \| null | 昵称(当前无接口可改,恒为 null) | -| `avatar_url` | string \| null | 头像(同上) | +| `phone` | string | 手机号(注销账号后变 `deleted_` 占位释放唯一约束) | +| `nickname` | string \| null | 昵称,经 [`PATCH /api/v1/user/profile`](./user-profile.md) 修改 | +| `avatar_url` | string \| null | 头像相对 URL(`/media/avatars/...`),经 [`POST /api/v1/user/avatar`](./user-avatar.md) 上传 | | `register_channel` | string | 注册渠道:`jverify` / `sms` | | `status` | string | `active` / `disabled` / `deleted` | | `created_at` | datetime | 注册时间 | @@ -135,6 +144,21 @@ --- +## /media 静态服务 + +用户上传文件(头像/反馈截图)落盘到 `settings.MEDIA_ROOT`(默认 `./data/media/`),由 FastAPI 的 `StaticFiles` 挂在 `settings.MEDIA_URL_PREFIX`(默认 `/media`)对外暴露: + +- `GET /media/avatars/` — 用户头像(JPEG/PNG/WebP) +- `GET /media/feedback/` — 反馈截图(同上) + +**生产建议**:由 nginx 直接 serve `MEDIA_ROOT` 目录,绕过应用进程减少压力。 + +**URL 格式**:服务端返回**相对路径**(如 `/media/avatars/u1_a4f2b3c8e9d2e0a1.jpg`),客户端按自己的 `BASE_URL` 拼绝对地址——dev 下可能是 `10.0.2.2`/LAN IP/`127.0.0.1`,服务端不知道客户端怎么访问到自己。 + +**文件名服务端随机生成** `u_<16 位 hex>.`,杜绝路径穿越与覆盖。 + +--- + ## 附:鉴权与刷新机制 - **签发**:登录成功后签发 access(HS256,2h) + refresh(30d),payload 含 `sub`(user_id)、`typ`(access/refresh)、`iat`、`exp`。 diff --git a/docs/api/feedback.md b/docs/api/feedback.md new file mode 100644 index 0000000..8b65588 --- /dev/null +++ b/docs/api/feedback.md @@ -0,0 +1,40 @@ +# POST /api/v1/feedback — 提交反馈 + +> 所属:Feedback 组(前缀 `/api/v1/feedback`) | 鉴权:Bearer access_token | [← 返回 API 索引](./README.md) + +## 入参 +**multipart/form-data**: +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `content` | string | ✓ | 反馈正文,**1-2000 字**(strip 后) | +| `contact` | string | ✓ | 联系方式(微信/QQ/手机号),**1-128 字**,便于回访 | +| `images` | file[] | ✗ | 截图,**最多 4 张**,每张走头像同款校验(JPEG/PNG/WebP,≤ 5 MB,魔数嗅探) | + +## 出参 +响应 `200`: +```json +{ + "id": 123, + "status": "new", + "created_at": "2026-05-29T12:34:56Z" +} +``` + +| 字段 | 类型 | 说明 | +|---|---|---| +| `id` | int | 反馈单 ID | +| `status` | string | `new`(待处理)/ `handled`(已处理) | +| `created_at` | datetime | 提交时间(UTC) | + +> 不返回上传的 image URL——这是给运营后台看的,客户端通常不需要。 + +## 错误码 +- `400` 内容为空 / 内容超 2000 字 / 联系方式为空 / 联系方式超 128 字 / 图片超 4 张 / 单图非法(空/过大/格式不对) +- `401` 未带 token / token 无效或过期 / 用户被禁用 +- `422` 缺 `content` 或 `contact` 字段 + +## 说明 +- **反馈绑用户**:`feedback.user_id = current_user.id`,便于回访 +- **截图落盘**:存到 `settings.MEDIA_ROOT/feedback/`,URL 写进 `feedback.images`(JSON 列,SQLite 是 TEXT、PG 是 JSON/JSONB——为不影响现有迁移用 JSON 而非 JSONB) +- **截图可读**:`GET /media/feedback/` 静态服务暴露,详见 [API 索引 — /media 静态服务](./README.md#media-静态服务) +- 文件名随机 `u_.`,杜绝路径穿越 diff --git a/docs/api/meituan-coupons.md b/docs/api/meituan-coupons.md index f33cf2f..582708e 100644 --- a/docs/api/meituan-coupons.md +++ b/docs/api/meituan-coupons.md @@ -23,7 +23,8 @@ 响应 `200`:`{ items: CouponCard[], has_next: bool, search_id: string|null }`。`CouponCard` 结构见 [API 索引](./README.md#复用数据结构)。 ## 错误码 -- `502` 美团接口失败 +- `502` 美团接口失败(仅在**已配置 `MT_CPS_APP_KEY/APP_SECRET` 但调用失败**时;未配凭证场景见下) ## 说明 -当前 Android 客户端**未调用**此接口(搜索功能尚未实现),仅后端实现完整。 +- 当前 Android 客户端**未调用**此接口(搜索功能尚未实现),仅后端实现完整 +- **未配置 MT_CPS 凭证时降级**:`settings.mt_cps_configured == false` 时直接返 `{ items: [], has_next: false, search_id: null }`,**不报 502**。设计目的是让新开发机首屏不炸——后端业务层先不依赖 CPS。已配凭证但调美团失败时才走 502 diff --git a/docs/api/meituan-feed.md b/docs/api/meituan-feed.md index 14491f0..5c44bd5 100644 --- a/docs/api/meituan-feed.md +++ b/docs/api/meituan-feed.md @@ -17,9 +17,15 @@ 响应 `200`:`{ items: CouponCard[], has_next: bool, page: int }`。`CouponCard` 结构见 [API 索引](./README.md#复用数据结构)。 ## 错误码 -无(见"说明"中的可观测性盲区) +无(任何失败场景都返 200 + 空 items,见"说明"中的可观测性盲区) ## 说明 后端在 `coupons` 之上封装的"伪推荐"——每页并发拉一次外卖、一次到店(到餐)榜单,按 **2 外卖 + 1 到店** 交叉去重。榜单组合写死 3 页(第1页爆款、第2页今日必推、第3页外卖精选+到店限时),**第 3 页起 `has_next=false`、第 4 页返空**。 -⚠️ **可观测性盲区**:`feed` 内部对美团调用异常是静默吞掉返回空列表(不报 502、不打日志)。若 `items` 为空但无错误,优先用 `coupons` 接口逼出真实错误(它会以 502 暴露,如"MT_CPS_APP_KEY not configured")。 +**两种空结果路径**: +- **未配置 `MT_CPS_APP_KEY/APP_SECRET`**:开头 `settings.mt_cps_configured` 判定后**直接返空**,完全不调美团 +- **已配置但美团调用失败**:`_fetch_topic` 的 `except MeituanCpsError` **静默返空**(不报 502、不打日志) + +⚠️ **可观测性盲区**:两种路径在响应上**无法区分**。若 `items` 为空想区分: +- 看 `settings.mt_cps_configured`(`/health` 接口有暴露此字段)→ 区分"配置缺失" vs "调用失败" +- 改调 `coupons` 接口——它在"已配凭证但调用失败"时会 502 暴露错误细节(未配凭证它也降级返空,跟 feed 一致) diff --git a/docs/api/meituan-referral-link.md b/docs/api/meituan-referral-link.md index 20f9ca0..289b73a 100644 --- a/docs/api/meituan-referral-link.md +++ b/docs/api/meituan-referral-link.md @@ -23,9 +23,9 @@ | `link_map` | object | 各类型链接 `{ "linkType": "url" }`,如 `{"1": "
", "3": ""}` | ## 错误码 -- `502` 美团接口失败 +- `502` 美团接口失败(仅在**已配置 `MT_CPS_APP_KEY/APP_SECRET` 但调用失败**时;未配凭证场景见下) ## 说明 -客户端实际优先用 `link_map["3"]`(deeplink)拉起美团 App,失败降级 `link_map["1"]`(H5)。 - -⚠️ **安全**:`sid` 允许客户端传值覆盖服务端默认渠道,理论上他人可借本接口刷自己渠道的分佣;建议服务端锁定 `sid`、忽略客户端传值。 +- 客户端实际优先用 `link_map["3"]`(deeplink)拉起美团 App,失败降级 `link_map["1"]`(H5) +- **未配置 MT_CPS 凭证时降级**:`settings.mt_cps_configured == false` 时直接返 `{ link: "", link_map: {} }`,**不报 502**。客户端拿到空 link 时跳转会失败——客户端应做兜底(toast "暂无可用链接"或不显示「抢」按钮) +- ⚠️ **安全**:`sid` 允许客户端传值覆盖服务端默认渠道,理论上他人可借本接口刷自己渠道的分佣;建议服务端锁定 `sid`、忽略客户端传值 diff --git a/docs/api/user-avatar.md b/docs/api/user-avatar.md new file mode 100644 index 0000000..21d0cc9 --- /dev/null +++ b/docs/api/user-avatar.md @@ -0,0 +1,26 @@ +# POST /api/v1/user/avatar — 上传头像 + +> 所属:User 组(前缀 `/api/v1/user`) | 鉴权:Bearer access_token | [← 返回 API 索引](./README.md) + +## 入参 +**multipart/form-data**: +| 字段 | 类型 | 说明 | +|---|---|---| +| `file` | file | 头像图片,**仅 JPEG / PNG / WebP**,**≤ 5 MB** | + +> 服务端**按文件头魔数嗅探真实类型**(`FF D8 FF` / `89 50 4E 47` / `RIFF…WEBP`),不信任 Content-Type 与文件名;不在三种之内的一律拒。 + +## 出参 +响应 `200`:`UserOut`(`avatar_url` 字段已更新为新地址,形如 `/media/avatars/u_.jpg`)。结构见 [API 索引](./README.md#复用数据结构)。 + +> 返回**相对路径**——客户端按自己的 `BASE_URL` 拼绝对地址(dev 下 `10.0.2.2`/LAN IP 都可能,服务端不知道客户端怎么访问到自己)。读图走 [/media 静态服务](./README.md#media-静态服务) 的 `GET /media/...`。 + +## 错误码 +- `400` 空文件 / 超过 5 MB / 非 JPEG/PNG/WebP +- `401` 未带 token / token 无效或过期 / 用户被禁用 +- `422` 缺 `file` 字段 + +## 说明 +- **旧头像清理**:落库成功后才删旧文件,避免"删了新的没存上"的丢图风险;非本服务托管的 URL(如微信头像)不动 +- **文件名服务端随机生成** `u_<16 位 hex>.`,杜绝路径穿越与覆盖 +- 存储目录:`settings.MEDIA_ROOT/avatars/`,默认 `data/media/avatars/`(生产 nginx 注意把它放共享卷或对象存储,见 [后端技术实现.md](../后端技术实现.md)) diff --git a/docs/api/user-delete.md b/docs/api/user-delete.md new file mode 100644 index 0000000..611918b --- /dev/null +++ b/docs/api/user-delete.md @@ -0,0 +1,18 @@ +# DELETE /api/v1/user — 注销账号 + +> 所属:User 组(前缀 `/api/v1/user`) | 鉴权:Bearer access_token | [← 返回 API 索引](./README.md) + +## 入参 +无(身份取自 Header token) + +## 出参 +响应 `200`:`{ "ok": true }` + +## 错误码 +- `401` 未带 token / token 无效或过期 / 用户被禁用(已注销账号再次调返 401,因 `status=deleted` 不再 active) + +## 说明 +- **软删除 + 匿名化**:`status="deleted"` + `phone="deleted_"`(释放 phone 唯一约束,允许同号码重新注册成全新账号)+ `nickname`/`avatar_url` 清空。行保留用于审计 / 关联表外键完整性 +- **头像文件物理删除**:本服务托管的头像文件(`/media/avatars/...`)同时删盘;微信头像等外部 URL 不动 +- **后续登录**:`status=deleted` 的行登录接口校验不过,所以"注销账号"后旧 token 仍能用直到自然过期(2h),但一过期就再也换不出新 token。客户端应在调本接口后立刻清本地 token +- **未来若加 jti 黑名单**:本接口应同时把当前 access/refresh 加入黑名单实现"立即吊销" diff --git a/docs/api/user-profile.md b/docs/api/user-profile.md new file mode 100644 index 0000000..b170db8 --- /dev/null +++ b/docs/api/user-profile.md @@ -0,0 +1,20 @@ +# PATCH /api/v1/user/profile — 修改昵称 + +> 所属:User 组(前缀 `/api/v1/user`) | 鉴权:Bearer access_token | [← 返回 API 索引](./README.md) + +## 入参 +请求体 JSON: +| 字段 | 类型 | 说明 | +|---|---|---| +| `nickname` | string | 昵称,**1-16 字**(前后空格自动 strip,strip 后为空判 400) | + +## 出参 +响应 `200`:`UserOut`(返回更新后的完整用户对象,`nickname` 字段已更新)。结构见 [API 索引](./README.md#复用数据结构)。 + +## 错误码 +- `401` 未带 token / token 无效或过期 / 用户被禁用 +- `422` 字段校验失败(`nickname` 缺失 / 长度超 16 字 / 全空白) + +## 说明 +- **服务端持久化**:写入 `user.nickname` 列。这是昵称的唯一数据源——客户端之前的本地缓存退登即丢,改完后下次登录走 [`/auth/me`](./auth-me.md) 与登录响应回传仍生效 +- **strip 行为**:服务端会先去除前后空白再校验长度,所以 `" bob "` 等价于 `"bob"` diff --git a/docs/integrations/meituan.md b/docs/integrations/meituan.md index e8e3e95..e3259bf 100644 --- a/docs/integrations/meituan.md +++ b/docs/integrations/meituan.md @@ -33,5 +33,18 @@ METHOD\n + Content-MD5\n + Headers(按 key 升序 "k:v\n") + Url | `MT_CPS_DEFAULT_SID` | 默认推广位 sid(渠道) | | `MT_CPS_TIMEOUT_SEC` | 请求超时 | +## 未配凭证时降级(2026-05-28 引入) +api 层 3 个端点都在入口处 `if not settings.mt_cps_configured:` 早返空(`coupons` 返空数组、`feed` 返空 items、`referral-link` 返 `link=""` + `link_map={}`),**不报 502**。 + +**为什么这么设计**:新开发机没填 `MT_CPS_APP_KEY` 时,首页一进就拉 `/feed` → 502 → 整屏错误,首屏体验差。降级后端业务可以先跑起来,等真要测美团 CPS 再补 key。 + +**坏副作用**:`feed` 本来就有"已配凭证但美团调用失败时 `_fetch_topic` 静默吞错"的可观测性盲区,加上"未配凭证时也返空",**两种空结果路径在响应上无法区分**。排障路径: +- `/health` 接口暴露 `mt_cps_configured: bool`——先看配置 +- 改调 `/coupons` 让"已配凭证但调用失败"以 502 + 错误文案暴露细节 + +## feed 调用静默吞错 +`api/v1/meituan.py` 的 `_fetch_topic` 对 `MeituanCpsError` `except 返回 []` 且**不打日志**。导致美团接口失败时 `feed` 返 200 + 空 items 无声失败。建议补一条 `logger.error` 至少留痕,真实排障还得改调 `coupons`(它在配置就绪时以 502 暴露)。 + ## 备注 -美团接口当前**无鉴权**,且换链的 `sid` 允许客户端传值覆盖默认渠道——见对应 api 文档「备注」。 +- 美团接口当前**无鉴权**,且换链的 `sid` 允许客户端传值覆盖默认渠道——见对应 api 文档「备注」 +- 未配凭证降级、feed 静默吞错都在 [待办与技术债.md](../待办与技术债.md) 已记账 diff --git a/docs/postgres-migration.md b/docs/postgres-migration.md index 0e6ae5b..8d1ee58 100644 --- a/docs/postgres-migration.md +++ b/docs/postgres-migration.md @@ -2,9 +2,14 @@ > 上线前把数据库从 SQLite 切到 PostgreSQL。本服务自始就为这一步预留了路径(`db/session.py` 的 dialect 特判 / `alembic/env.py` 的 batch mode 开关 / SQLAlchemy 2.0 + Alembic),代码改动很小。 > +> **相关文档**: +> - 这份文档讲 **切引擎的完整步骤**(本机装 PG / 建库 / `scripts/init_postgres.py` 用法 / 生产切换) +> - [数据库迁移.md](./数据库迁移.md) 讲 **alembic 操作**(如何建表/升级/新增迁移/多 head 排查/迁移链 12 条) +> - [后端技术实现.md §10](./后端技术实现.md) 列待办与已知问题(含 `init_postgres.py` 已知小 bug) +> > **背景假设**:当前生产**无真实用户数据**(MVP 阶段),所以整个迁移本质是"切引擎",不涉及数据搬迁。如果将来已有真实数据,本文档不适用,需补充 `pgloader` 演练 + 钱表金额逐行核对 + 停服窗口 + 回滚预案。 > -> 最后更新:2026-05-29 +> 最后更新:2026-05-31 --- diff --git a/docs/后端技术实现.md b/docs/后端技术实现.md index 5a657fe..66ba37a 100644 --- a/docs/后端技术实现.md +++ b/docs/后端技术实现.md @@ -3,21 +3,27 @@ > 域名:`app-api.shaguabijia.com`(HTTPS,nginx 反代) > 仓库:`shaguabijia-app-server` > 接口协议详见 [`docs/api/`](./api/)(索引 + 一接口一文件) -> 最后更新:2026-05-27 +> 最后更新:2026-05-31 --- ## 1. 这个后端是干什么的 -为正式版 App(`shaguabijia-app-android`,包名 `com.jishisongfu.shaguabijia`)提供三块能力: +为正式版 App(`shaguabijia-app-android`,包名 `com.jishisongfu.shaguabijia`)提供以下能力: | 能力 | 说明 | |---|---| | **账号与登录** | 极光一键登录 + 短信验证码登录(mock)→ 签发 JWT | -| **美团 CPS 选品** | 透传美团联盟优惠券(外卖/到店)、点击换取推广链接(分佣) | +| **用户资料** | 昵称 / 头像(上传图片含魔数嗅探) / 注销账号(软删除+匿名化) | +| **美团 CPS 选品** | 透传美团联盟优惠券(外卖/到店)、点击换取推广链接(分佣)、未配凭证时降级返空 | | **领券透传** | `/coupon/step` 透传到 pricebot-backend(一键领券核心,MVP 不鉴权,前端已接通) | +| **外卖比价透传** | `/intent/recognize` + `/price/step` 透传到 pricebot-backend(food MVP,MVP 不鉴权) | +| **金币 / 现金钱包** | 金币账户/流水/兑换/微信绑定/提现单 11 端点 | +| **签到 + 任务 + 省钱战绩** | 福利模块 | +| **看广告发奖** | 穿山甲 GroMore 激励视频 S2S 回调(SHA256 验签)+ 4 态 CTA 冷却 | +| **帮助与反馈** | 用户提交反馈(含截图)+ 静态 `/media` 服务 | -**本服务自身没有"比价"实现**——名字叫比价,实质是:登录态 + 美团券聚合分佣 + 给真正的领券/比价核心(pricebot-backend,另一个 repo)做"透传壳"。无爬虫、无 LLM、无积分/提现/钱包,数据模型仅一张 `user` 表(美团数据与领券请求均实时透传不落库)。 +**本服务自身没有"比价"实现**——比价/领券业务的核心都在 pricebot-backend(另一个 repo),本服务对 step 类接口做"透传壳"。无爬虫、无 LLM,但**有钱包/福利等业务模型**(数据模型从早期 1 张 `user` 表已扩展到 10 张,见 §7)。 --- @@ -30,7 +36,8 @@ | ASGI | uvicorn[standard] | 生产 `--host 127.0.0.1 --port 8770` | | ORM | SQLAlchemy 2.0(Mapped 风格) | | | 迁移 | Alembic | | -| DB | SQLite 起步 | 改 `DATABASE_URL` 可切 PostgreSQL,无 Redis | +| DB | **PostgreSQL 16(生产)/ SQLite(开发兜底)** | 一键脚本 `scripts/init_postgres.py` 建库 + 写 `.env` + 跑迁移;开发环境改 `DATABASE_URL=sqlite:///./data/app.db` 即回退。无 Redis。详见 [docs/postgres-migration.md](./postgres-migration.md) | +| DB 驱动 | psycopg3(`psycopg[binary]>=3.1`) | `DATABASE_URL=postgresql+psycopg://...`,**不要装 psycopg2**——SQLAlchemy 2.0 时代默认 psycopg3 | | Auth | PyJWT(HS256) | access + refresh | | 极光 | httpx + cryptography | REST 验 token + RSA 解密手机号 | | 美团 / pricebot | httpx(含 async) | 美团 S-Ca 网关签名;领券 async 透传 | @@ -50,16 +57,20 @@ app/ │ ├── deps.py # 共享依赖:get_current_user(鉴权)、get_db(注入 session) │ └── v1/ # 接口层(薄):解析请求 → 调 repositories/integration → 组装响应 + HTTP 错误码 │ ├── auth.py # 登录 6 端点(极光一键登录 / 短信 send+login / refresh / me / logout) +│ ├── user.py # 用户资料 3 端点(改昵称 / 上传头像 / 注销账号) +│ ├── feedback.py # 帮助与反馈 1 端点(提交反馈含截图) │ ├── coupon.py # 领券透传 /coupon/step(转发 pricebot,MVP 不鉴权) │ ├── compare.py # 外卖比价透传 /intent/recognize + /price/step(转发 pricebot,MVP 不鉴权) -│ ├── meituan.py # 美团 3 端点 + feed 拼接(_interleave / _TOPIC_ROUNDS) +│ ├── meituan.py # 美团 3 端点 + feed 拼接(_interleave / _TOPIC_ROUNDS),未配 MT_CPS 凭证降级返空 │ ├── wallet.py # 钱包/提现 11 端点(余额/流水/兑换/绑微信/提现/查单) │ ├── signin.py # 签到 2 端点(状态 / 执行签到) │ ├── tasks.py # 一次性任务 2 端点(列表 / 领取) │ ├── savings.py # 省钱 3 端点(汇总 / 战绩 / 明细) -│ └── ad.py # 看广告发奖 3 端点(穿山甲 S2S 回调 / 进度 / 联调发奖) +│ └── ad.py # 看广告发奖 3 端点(穿山甲 S2S 回调 / 进度+本轮冷却 / 联调发奖) ├── schemas/ # Pydantic:API 收发的数据契约(与客户端对齐字段看这里) │ ├── auth.py +│ ├── user.py # 改昵称请求 + OkResponse +│ ├── feedback.py # 反馈出参(请求是 multipart,在 router 直接校验) │ ├── meituan.py │ ├── welfare.py # 钱包/签到/任务/省钱 收发模型 │ └── ad.py # 看广告发奖收发模型 @@ -73,35 +84,45 @@ app/ │ ├── config.py # pydantic-settings │ ├── security.py # JWT 签发/校验 │ ├── ratelimit.py # 同 IP 滑动窗口限流依赖 -│ ├── rewards.py # 发奖/兑换/提现额度等业务常量与换算 +│ ├── rewards.py # 发奖/兑换/提现额度等业务常量与换算(2026-05 加 VIDEO_ROUND_REQUIRED_COUNT / VIDEO_ROUND_COOLDOWN_SECONDS) +│ ├── media.py # 用户上传文件(头像/反馈截图)落盘 + 魔数嗅探 + 随机文件名 │ └── logging.py ├── repositories/ # 数据访问 + 事务(早期叫 crud,2026-05 统一并入此目录) -│ ├── user.py # get_user_by_id / get_user_by_phone / upsert_user_for_login +│ ├── user.py # get_user_by_id / by_phone / upsert_for_login / update_nickname / set_avatar_url / soft_delete_account +│ ├── feedback.py # 提交反馈写库 │ ├── wallet.py # 账户/流水/兑换/提现单(调 integrations/wxpay) │ ├── signin.py # 签到记录 / 连续天数 / 档位 │ ├── task.py # 一次性任务领取 │ ├── savings.py # 省钱汇总 / 战绩 / 明细 -│ └── ad_reward.py # 看广告发奖(按 trans_id 幂等 + 每日上限) +│ └── ad_reward.py # 看广告发奖(按 trans_id 幂等 + 每日上限 + 本轮冷却派生) ├── models/ # ORM 表结构 │ ├── user.py # user(含微信 openid/nickname/avatar) +│ ├── feedback.py # 用户反馈(content/contact/images JSON 列/status) │ ├── wallet.py # 金币账户 / 金币流水 / 现金流水 / 提现单 │ ├── signin.py # 签到记录 │ ├── task.py # 任务领取记录 -│ ├── savings.py # 省钱明细 / 店铺菜品 +│ ├── savings.py # 省钱明细 / 店铺菜品 / dishes(PG 上 JSONB) │ └── ad_reward.py # 看广告发奖记录 └── db/ ├── base.py # DeclarativeBase - └── session.py # engine + get_db + └── session.py # engine + get_db(非 SQLite 时启 pool: size=10/overflow=20/recycle=3600) -alembic/ # 数据库迁移(versions/ 9 个迁移;文件名已去 hex 前缀,链靠文件内 down_revision) +alembic/ # 数据库迁移(versions/ 11+ 个迁移含 feedback_table / convert_dishes_jsonb / merge 等) deploy/ # systemd(.service) + nginx(.conf) secrets/ # 极光 RSA 私钥 / 微信支付证书(不入 git,仅 .gitkeep 占位) -scripts/ # 运维脚本(migrate 迁移 / 对账 / 重置签到 / 重置福利 / 模拟穿山甲回调) -tests/ # pytest(auth / health / welfare / withdraw / ad_reward / coupon_proxy) +scripts/ + ├── init_postgres.py # 一键 PG 初始化:建用户 + 建库 + 写 .env + 跑迁移(2026-05 新增,见已知 bug §10) + ├── migrate.sh # 单独跑 alembic upgrade head(部署/CI 用) + ├── reset_signin.py # 重置今日签到 + ├── reset_welfare.py # 重置福利数据 + ├── reconcile_withdraws.py # 提现对账 + └── sim_pangle_callback.py # 模拟穿山甲回调 +tests/ # pytest(auth / health / welfare / withdraw / ad_reward / coupon_proxy / compare_proxy) run.sh # 本地启动脚本(自动先跑迁移再起服务) docs/api/ # API 接口文档(索引 README + 一接口一文件) docs/integrations/ # 集成层实现文档(SDK 签名/加解密/协议细节) docs/数据库迁移.md # Alembic 迁移指南(如何建表/升级/新增迁移) +docs/postgres-migration.md # SQLite → PostgreSQL 切换指南(配套 scripts/init_postgres.py) ``` > **命名说明**:`api/v1/` 的 `v1` 用于 URL 版本化(移动端无法强制即时升级,需新旧版本并存能力);`integrations` 装外部 SDK 集成、`repositories` 装数据访问、`core` 装基础设施,三者分离。**数据访问层统一在 `repositories/`**(早期叫 `crud/`,2026-05 已整体并入,`crud/` 不再存在)。`coupon.py` 是领券透传,勿与 `meituan.py` 里的 `coupons`(券列表)混淆。 @@ -197,41 +218,70 @@ POST /api/v1/auth/sms/login { phone, code } → 任意 6 位通过 → upsert ## 7. 数据模型 -仅一张 `user` 表(`models/user.py`): +10 张业务表 + `alembic_version` 框架表。生产 PG / 开发可回退 SQLite。 +| 表 | models 文件 | 说明 | +|---|---|---| +| `user` | `models/user.py` | 用户(phone 唯一索引;注销时改成 `deleted_` 释放约束) | +| `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) | +| `ad_reward_record` | `models/ad_reward.py` | 看广告发奖(`trans_id` 唯一,幂等) | + +**user 表字段速查**: | 字段 | 类型 | 说明 | |---|---|---| | id | INTEGER PK | 自增 | -| phone | VARCHAR(20) UNIQUE | 登录主键,唯一索引 `ix_user_phone` | +| phone | VARCHAR(20) UNIQUE | 登录主键(注销后变 `deleted_` 占位) | | register_channel | VARCHAR(20) | `jverify` / `sms` | -| nickname | VARCHAR(64) NULL | 预留,**无接口可写,恒为 null** | -| avatar_url | VARCHAR(512) NULL | 同上 | +| nickname | VARCHAR(64) NULL | 由 `PATCH /api/v1/user/profile` 写入 | +| avatar_url | VARCHAR(512) NULL | 相对路径(`/media/avatars/...`),由 `POST /api/v1/user/avatar` 写入 | +| wechat_openid | VARCHAR(64) NULL UNIQUE | 微信绑定(提现用),一个微信只绑一个账号 | +| wechat_nickname / wechat_avatar_url | VARCHAR NULL | 微信侧资料(绑定时拉取) | | status | VARCHAR(20) | `active` / `disabled` / `deleted`,仅 active 可登录/鉴权 | | created_at / last_login_at | DATETIME(tz) | | -`upsert_user_for_login`:phone 存在则更新 `last_login_at`,不存在则注册(注册即登录)。Alembic 实际库里另有框架表 `alembic_version`。 +`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](./数据库迁移.md)。 --- ## 8. 配置与部署 -配置见 `core/config.py`(pydantic-settings 读 `.env`)。分组:环境、`DATABASE_URL`、JWT、极光(`JG_*`)、短信(`SMS_MOCK` 默认 true)、美团(`MT_CPS_*`)、**pricebot 上游(`PRICEBOT_BASE_URL` / `PRICEBOT_REQUEST_TIMEOUT_SEC`)**、CORS。 +配置见 `core/config.py`(pydantic-settings 读 `.env`)。分组:环境、`DATABASE_URL`(默认 SQLite,生产应切 PG)、JWT、极光(`JG_*`)、短信(`SMS_MOCK` 默认 true)、美团(`MT_CPS_*`)、**pricebot 上游(`PRICEBOT_BASE_URL` / `PRICEBOT_REQUEST_TIMEOUT_SEC=30` / `PRICEBOT_COMPARE_TIMEOUT_SEC=60`)**、**媒体存储(`MEDIA_ROOT=./data/media` / `MEDIA_URL_PREFIX=/media`)**、CORS。 -**生产部署**:systemd `shaguabijia-app-server.service`(WorkingDirectory `/opt/shaguabijia-app-server`,`EnvironmentFile=.env`,uvicorn 监听 `127.0.0.1:8770`,`--workers 1`)+ nginx 443 反代 → 8770。**无 Docker**。 +**生产部署**:systemd `shaguabijia-app-server.service`(WorkingDirectory `/opt/shaguabijia-app-server`,`EnvironmentFile=.env`,uvicorn 监听 `127.0.0.1:8770`,`--workers 1`)+ nginx 443 反代 → 8770。**无 Docker**。**PostgreSQL 16** 同机部署。 ```bash +# 首次 PG 初始化(新机器或新环境): +ssh server "sudo apt install -y postgresql-16 && sudo systemctl enable --now postgresql" +ssh server "cd /opt/shaguabijia-app-server && .venv/bin/python scripts/init_postgres.py" +# 该脚本会建业务用户 + 建库 + 写 .env 的 DATABASE_URL + 跑 alembic upgrade head + +# 后续日常部署: rsync -avz --exclude='.venv' --exclude='__pycache__' --exclude='data' --exclude='secrets/*.pem' ./ server:/opt/shaguabijia-app-server/ scp secrets/jverify_rsa_private.pem server:/opt/shaguabijia-app-server/secrets/ # 私钥单独传,不入 git ssh server "cd /opt/shaguabijia-app-server && .venv/bin/alembic upgrade head && systemctl restart shaguabijia-app-server" ``` +完整 PG 切换流程见 [docs/postgres-migration.md](./postgres-migration.md)。 + **生产 checklist(均为上线必查)**: - [ ] `JWT_SECRET_KEY` 改为高熵随机串——默认值 `change-me` 可被伪造 token - [ ] `SMS_MOCK=false` 并接真实短信供应商——mock 下任意 6 位码可登录任意手机号 -- [ ] `MT_CPS_APP_KEY` / `MT_CPS_APP_SECRET` 已填(否则美团接口 502) +- [ ] `MT_CPS_APP_KEY` / `MT_CPS_APP_SECRET` 已填(未配凭证时美团 3 端点降级返空,前端首页空白) - [ ] `PRICEBOT_BASE_URL` 指向真实 pricebot-backend(否则领券 502) - [ ] RSA 私钥就位且与极光控制台公钥**配对**,权限 600 +- [ ] **DATABASE_URL 已切 PG**(默认 SQLite 单文件并发写锁库,扩 worker 立即撞墙) +- [ ] **psycopg3 已装**(`pip install "psycopg[binary]>=3.1"`) +- [ ] `MEDIA_ROOT` 目录可写(头像/反馈截图落盘);建议 nginx 直接 serve 该目录绕过应用进程 - [ ] `APP_ENV=prod`、`APP_DEBUG=false`、nginx SSL 有效、`alembic upgrade head` 已执行 --- @@ -255,12 +305,15 @@ conda activate price # 首次:pip install -e . | 项 | 说明 | |---|---| -| logout 无服务端失效 | 靠客户端清 token;后续加 jti 黑名单表 | +| logout 无服务端失效 | 靠客户端清 token;后续加 jti 黑名单表(注销账号也是同问题——软删后旧 token 仍能用到自然过期) | | 美团接口无鉴权 + sid 可覆盖 | 评估加鉴权/锁定 sid(注意首页要求未登录可见) | -| feed 静默吞异常 | 建议给 `_fetch_topic` 加日志,避免无声失败 | -| 领券依赖 pricebot | `coupon/step` 仅透传,真正逻辑在 pricebot-backend;前端已接通领券链路 | +| 美团接口未配凭证降级 | 未配 `MT_CPS_APP_KEY` 时 3 端点返空(不报 502),`/feed` 跟"已配但调用失败"路径无法区分——见 [integrations/meituan](./integrations/meituan.md) | +| 领券/比价依赖 pricebot | `coupon/step` / `intent/recognize` / `price/step` 仅透传,真正逻辑在 pricebot-backend;前端已接通领券链路,比价 food MVP 也已接通 | +| agent 系列接口 MVP 不鉴权 | 拿不到 user_id → 无法采集"哪个用户领了/买了什么"用户级画像(商业模式核心资产)。见 [待办与技术债.md](./待办与技术债.md) P1 | | SMS 为 mock | 上线接真实供应商 + `SMS_MOCK=false` | | 短信冷却存内存 | 扩 worker 前需迁移到 Redis | -| SQLite | 流量上来切 PostgreSQL,只改 `DATABASE_URL` | +| `MEDIA_ROOT` 进程内 serve | 头像/反馈截图当前用 FastAPI StaticFiles,生产建议 nginx 直 serve 该目录 | +| `init_postgres.py` 已知小 bug | 5 条小坑,见 [待办与技术债.md](./待办与技术债.md) | +| Alembic 多 head 风险 | 跨分支并行开发要在 PR 合并前 `alembic heads` 检查只有一个;漏检会出现 "Multiple head revisions",需要 `alembic merge` 合并 | -> 完整接口协议(11 个)见 [`docs/api/`](./api/),以代码为准。 +> 完整接口协议见 [`docs/api/`](./api/)(38 个端点 + `/health` + `/media` 静态服务),以代码为准。 diff --git a/docs/待办与技术债.md b/docs/待办与技术债.md index ae3b543..0ee7dd9 100644 --- a/docs/待办与技术债.md +++ b/docs/待办与技术债.md @@ -3,7 +3,7 @@ > 用途:记"现在先简化、以后要补"的事,和暂不处理的技术债,免得忘。每条标优先级 + 触发背景。做掉的移到末尾「已解决」。 > 范围:跨栈账本——本仓 `shaguabijia-app-server`(转发层) + `shaguabijia-app-android`(无障碍引擎移植) + `pricebot-backend`(agent 上游,不改)。 > 大背景:把"后端当大脑、无障碍服务遥控前端操作其它 App"的比价/领券引擎,从 demo app `pricebot` 移植进正式版傻瓜比价。引擎移植采用「代码全搬、独立子包 `…shaguabijia.agent.*`、验证分步(先领券后比价)」。 -> 最后更新:2026-05-27 +> 最后更新:2026-05-31 --- @@ -44,6 +44,39 @@ --- +## P2 · DB 与 Alembic 治理(2026-05-29 新增) + +### Alembic 多 head 风险 — 团队流程问题 + +**触发背景**:2026-05-29 用 PG 跑 `alembic upgrade head` 报 "Multiple head revisions are present"。 +- 实习生的 `feat/postgres-migration-and-bootstrap-script` 分支基于 `4d444b7` (已合 PR #4 feedback) 创建,但新迁移 `ef96beb47b1e` 的 `down_revision` 挂在 `c8d9e0f1a2b3` (ad_reward),跟 PR #4 引入的 `d1e2f3a4b5c6` (feedback) 并行成两个 head +- **根因**:跨分支并行开发,PR 合并时没人检查 alembic 迁移链是否线性 + +**临时修复**:`alembic merge -m "merge pg_jsonb and feedback heads" ef96beb47b1e d1e2f3a4b5c6` 生成合并迁移 `f01db5d77dac_merge_pg_jsonb_and_feedback_heads.py`(已合进 main)。 + +**团队规约(待写进 CONTRIBUTING)**: +- 新建包含 alembic 迁移的 PR 前,在分支上跑 `alembic heads`,确认**只有一个 head**;有多个就先 rebase 自己的迁移文件的 `down_revision` +- review 别人含 alembic 迁移的 PR 时,同样检查 heads 数 +- 合并 main 之后再跑一次 `alembic heads` 兜底——本次撞到就是 review 没拦住 + +### `init_postgres.py` 已知小 bug(2026-05-29 review) + +`scripts/init_postgres.py` 是实习生为 PG 切换写的 bootstrap 脚本(建用户/库/写 .env/跑迁移),有 5 处可改进项: + +1. **`if val:` 把空字符串当未设** → `PG_SUPER_PASS=""` 不能传(本机 trust 认证想跳过密码就只能给占位字符串) +2. **用户自定义 `APP_DB_PASS` 含 `@:/` 会破坏 URL** → 应 `urllib.parse.quote(db_pass, safe='')`(自动生成的 `secrets.token_urlsafe` 是 URL 安全字符,但用户传值不一定) +3. **subprocess 跑 alembic 没显式注 `DATABASE_URL`** → 父 shell 残留旧值会覆盖刚写入 .env 的新值,可能把表建到错的库 +4. **强制 TCP localhost** → macOS Homebrew(默认 trust)和 Linux apt(默认 peer)的默认认证模式都不直接走 TCP,新机首次跑大概率失败 +5. **承诺的 JSONB GIN 索引没真加** → commit message 和文档都说"PG 上可建 GIN 索引",但 `savings_record.dishes` 实际没加;要么补 `CREATE INDEX ... USING GIN (dishes)`,要么把"GIN"措辞从理由里删掉 + +短期不阻塞使用(熟手按预期参数能跑通),但累积起来会让新人踩坑。低优先级清理。 + +### `MEDIA_ROOT` 进程内 serve + +`/media` 路径当前由 FastAPI StaticFiles 挂载 serve(头像/反馈截图)。生产建议改 nginx 直 serve `MEDIA_ROOT` 目录,绕过应用进程减少压力。一行 `location /media/ { alias /opt/shaguabijia-app-server/data/media/; }` 即可。 + +--- + ## 阶段 1 领券落地后的遗留(待清理,不阻塞跑通) - **FloatingButton 死代码**:Running 面板已换成 ComposeView 承载 `CouponProgressPanel`(=复用 AgentFloat)。旧原生面板字段(`line1~4View`/`elapsedView`/`progressBarView`/`historyContainerView` 等)+ `updateStatus(StepStatus)` 里保留的那段更新逻辑 + `appendActionLine`/`markCurrentActionDone`/`markPlatformDone`/`addCancelButton` 全部 no-op,待删。 @@ -61,3 +94,8 @@ - ✅ **无障碍身份统一傻瓜比价**:服务名/图标继承 app;`accessibility_service_description`/前台通知/`GuideCopy`/引导 Activity 里的 "PriceBot" 全改「傻瓜比价」;app 图标换成完整 `logo1`(adaptive 居中+黄边,不裁切)。 - ✅ **GuideOverlayService 组件名 bug**:批量 sed 改包名误把无障碍服务 ComponentName 的 packageName 改成 `…shaguabijia.agent`(应是 applicationId `…shaguabijia`)→ 无障碍开启检测永远 false;改用 `PermissionHelper.isAccessibilityEnabled` 动态构造。 - ✅ **BuildConfig 包名**:`com.pricebot.app.BuildConfig` import 改本 app 包名。 +- ✅ **SQLite → PostgreSQL 切换**(2026-05-29,PR #6 + alembic 合并迁移):新增 `scripts/init_postgres.py` 一键脚本、`psycopg[binary]>=3.1` 依赖、`savings_record.dishes` 列改 JSONB、连接池参数(`pool_size=10/overflow=20/recycle=3600`)。SQLite 仍可作开发兜底(只改 `DATABASE_URL`)。详见 [postgres-migration.md](./postgres-migration.md)。 +- ✅ **外卖比价透传 2 端点**(2026-05-27,`feat(compare)`):`/api/v1/intent/recognize` + `/api/v1/price/step` 在 `compare.py`,纯 body 透传到 pricebot-backend,`PRICEBOT_COMPARE_TIMEOUT_SEC=60` 独立超时。电商 2 个待接(`compare.py` 加两行即可)。 +- ✅ **用户资料 + 帮助反馈接口**(2026-05-28,PR #4):`PATCH /api/v1/user/profile` 改昵称、`POST /api/v1/user/avatar` 上传头像(魔数嗅探)、`DELETE /api/v1/user` 注销账号(软删+匿名化)、`POST /api/v1/feedback` 提交反馈(含截图);新建 `feedback` 表 + `core/media.py` + `/media` 静态服务。 +- ✅ **美团 3 端点未配凭证降级**(2026-05-28,`feat(meituan)`):未配 `MT_CPS_APP_KEY` 时 `coupons`/`feed`/`referral-link` 直接返空(不再 502),解决新开发机首屏炸的问题。**坏副作用**:`/feed` 跟"已配但调用失败"路径在响应上无法区分(都是 200 + 空 items),靠 `/health` 的 `mt_cps_configured` 字段区分。 +- ✅ **看广告 4 态 CTA + 本轮冷却**(2026-05-29,PR #5):`reward-status` 加 `round_count` + `cooldown_until` 字段,客户端 CTA 4 态(Normal/Loading/Capped/CoolingDown)+ 弹窗 limit note("本轮看完"/"今日看完")全由后端权威派生,跨设备/重装/杀进程都一致。**冷却仅 UX**,发奖不受影响。 diff --git a/docs/数据库迁移.md b/docs/数据库迁移.md index 3661a0b..599f313 100644 --- a/docs/数据库迁移.md +++ b/docs/数据库迁移.md @@ -2,6 +2,11 @@ > 本项目用 **Alembic** 管理数据库表结构。所有"建表/改表"都写成 `alembic/versions/` 下的迁移脚本, > 数据库的真实结构 = 把这些迁移按顺序跑一遍的结果。**不要手动改库结构**,一律走迁移。 +> +> **相关文档**: +> - 这份文档讲 **alembic 操作**(如何建表/升级/新增迁移/多 head 排查) +> - [postgres-migration.md](./postgres-migration.md) 讲 **从 SQLite 切到 PostgreSQL** 的完整步骤(本机安装/建库/`scripts/init_postgres.py` 用法) +> - 配合 [后端技术实现.md §10](./后端技术实现.md) 看待办与已知问题 --- @@ -12,6 +17,8 @@ alembic upgrade head ``` 它会把数据库结构升到最新(幂等,已是最新则什么都不做)。 +**新 PG 环境从零开始**:跑 `python scripts/init_postgres.py`,它会建用户 + 建库 + 写 .env + 跑迁移。详见 [postgres-migration.md](./postgres-migration.md)。 + --- ## 1. 首次初始化(clone 后) @@ -85,21 +92,53 @@ alembic upgrade head - 生成后**手动把文件名前缀去掉**(内部 id 别动);或 - 在 `alembic.ini` 配 `file_template = %%(slug)s`(注意 ini 里 `%` 要写成 `%%`)让以后直接生成无前缀文件名(缺点:同名 slug 会冲突,描述写具体点)。 -## 6. 当前迁移链(9 条) +## 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,最新) +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` 链,不是文件名字母序)。 ## 7. 踩坑 -- **SQLite 找不到目录**:报 `unable to open database file` → 先 `mkdir -p data`。 -- **多个 head**:`alembic heads` 出现 2 个 → 有人并行各加了一条迁移分叉,需 `alembic merge -m "merge" ` 合并。 + +### SQLite 找不到目录 +报 `unable to open database file` → 先 `mkdir -p data`。 + +### 多 head 分叉(2026-05-29 撞过) + +`alembic upgrade head` 报: +``` +Multiple head revisions are present for given argument 'head'; +please specify a specific target revision, '@head'... +``` + +**原因**:跨分支并行开发,两条分支都加了新迁移且 `down_revision` 都指向同一个 commit,合并到 main 后形成两条平行链。本次实例: +- PR #4 (feedback) 加了 `d1e2f3a4b5c6`,`down_revision='c8d9e0f1a2b3'` (ad_reward) +- PR #6 (PG/JSONB) 加了 `ef96beb47b1e`,`down_revision='c8d9e0f1a2b3'`(也是 ad_reward) ← 跟 #4 同源,撞车 +- 两个 PR 都合进 main 后,`alembic heads` 出现两个 + +**修复**: +```bash +alembic heads # 列出冲突的两个 head id +alembic merge -m "merge " +# 生成一个空迁移文件,把两个 head 合成新的单一 head +git add alembic/versions/.py +# commit + push, PR review 后合并到 main +``` + +**预防(团队规约)**: +- 写迁移的 PR 在提交前跑 `alembic heads`,确认本分支只有一个 head +- review 别人含迁移的 PR 时,看 `down_revision` 是否还是当前 main 的最新——如果上游已经被别人合了新迁移,需要 rebase 修改 `down_revision` 指过去 +- main 拉新代码后再跑一次 `alembic heads` 兜底 - **生产库**:不要随意 `downgrade`;新迁移先在本地/测试库验证过再上。生产 `DATABASE_URL` 由部署侧配置,不在本仓库 `.env`。 diff --git a/docs/看广告赚金币上线清单.md b/docs/看广告赚金币上线清单.md index 12cb3bf..c410817 100644 --- a/docs/看广告赚金币上线清单.md +++ b/docs/看广告赚金币上线清单.md @@ -8,7 +8,8 @@ - **发奖只走服务端**:激励视频播完,穿山甲服务器 S2S 回调后端 `/api/v1/ad/pangle-callback` 发金币。 客户端**不发奖**,`onRewardArrived` 只触发"去后端刷余额"。客户端被破解也刷不到钱。 - 后端发奖**幂等**(按 `trans_id` 去重)+ **每日上限**(`DAILY_AD_REWARD_LIMIT`,按北京时间)。 -- 关键常量:`app/core/rewards.py` → `AD_REWARD_COIN=100`、`DAILY_AD_REWARD_LIMIT=10`(**占位值,上线前按 eCPM 实测收益重定**)。 +- 关键常量:`app/core/rewards.py` → `AD_REWARD_COIN=100`、`DAILY_AD_REWARD_LIMIT=10`(**占位值,上线前按 eCPM 实测收益重定**)、`VIDEO_ROUND_REQUIRED_COUNT=3`、`VIDEO_ROUND_COOLDOWN_SECONDS=600`(本轮 3 次后冷却 10 分钟)。 +- **4 态 CTA**(2026-05-29 PR #5):客户端任务行按钮在 Normal / Loading / Capped / CoolingDown 之间切,**全由后端 `reward-status` 的 `round_count` + `cooldown_until` 派生**(权威源),跨设备/重装/杀进程一致。详见 [api/ad-reward-status](./api/ad-reward-status.md)。 --- @@ -72,7 +73,10 @@ - [ ] 真机看完一条**真实**激励视频 → 穿山甲 S2S 回调 → 后端发奖 → 客户端余额真到账 - [ ] **幂等**:同一 `trans_id` 不重复发 -- [ ] **每日上限**:超过 `DAILY_AD_REWARD_LIMIT` 返回 capped、不发币、按钮显示「已达上限」 +- [ ] **每日上限**:超过 `DAILY_AD_REWARD_LIMIT` 返回 capped、不发币、按钮显示「已达上限」(Capped 态) +- [ ] **本轮冷却**(PR #5):连看 3 张 → 按钮变 CoolingDown 显 MM:SS 倒计时不可点 → 10 分钟后自动恢复 Normal +- [ ] **弹窗 limit note**:`round_count==0 && cooldown_until!=null` → "本轮视频已看完,10分钟后再来";`used_today>=daily_limit` → "今日视频已到限额,明天再来" +- [ ] **跨设备/杀进程一致**:在手机 A 看完 3 张进入冷却 → 手机 B(同账号)登录后 reward-status 也返回相同的 `cooldown_until`(权威源在后端,客户端不存本地) - [ ] 中途退出广告 → 客户端提示「未看完视频,本次没有奖励哦」,且不发奖 - [ ] 收益明细显示「看广告奖励」(biz_type=`ad_reward`)