# 待办与技术债账本

> 用途：记"现在先简化、以后要补"的事，和暂不处理的技术债，免得忘。每条标优先级 + 触发背景。做掉的移到末尾「已解决」。
> 范围：跨栈账本——本仓 `shaguabijia-app-server`(转发层) + `shaguabijia-app-android`(无障碍引擎移植) + `pricebot-backend`(agent 上游，不改)。
> 大背景：把"后端当大脑、无障碍服务遥控前端操作其它 App"的比价/领券引擎，从 demo app `pricebot` 移植进正式版傻瓜比价。引擎移植采用「代码全搬、独立子包 `…shaguabijia.agent.*`、验证分步(先领券后比价)」。
> 最后更新：2026-05-31

---

## P1 · 鉴权与用户绑定(最该补)

- **现状**：agent 系列接口 MVP 阶段**全部不鉴权**。领券 `coupon/step` **已落地不鉴权**(✅ 已去掉 `CurrentUser` 依赖);比价 4 个(`intent/recognize`、`price/step`、`ecom/intent/recognize`、`ecom/step`)端点尚未建(见 P2),建时同样先不加鉴权。
- **代价(为什么记这笔账)**：不验 JWT → 转发时 server 拿不到 `user_id` → agent 行为只能绑到 `device_id`(设备级)，**采集不到"哪个用户领了/买了什么"的用户级画像**。而精准人群画像、私域分群运营是商业模式的核心资产，靠的就是这份用户级行为数据。`device_id` 仍照传(后端按设备串领券队列够用)。
- **待补**：① agent 接口加 JWT 鉴权；② 建立 `device_id ↔ user_id` 绑定(登录后上报一次即可)；③ ~~领券/比价记录按 user 维度落库~~ → **比价记录已落地**(见文末「已解决」)：走**独立的鉴权端点** `POST /api/v1/compare/record`,客户端在 done 帧后用带 JWT 的通道上报、不依赖透传链路。**所以透传端点本身仍不鉴权、本条 P1 主诉求(给透传链路补 JWT + device↔user 绑定)未解**;领券记录尚未落库。
- **连带**：补鉴权后，客户端引擎 `ApiClient` 要接回 JWT(复用 app 现有 `AuthInterceptor` 思路：注入带鉴权头的 OkHttpClient)。比价记录上报已**先行复用**这套(`authedClient` 挂 `AuthInterceptor`+`RefreshAuthenticator`),可作透传链路补鉴权的参考样板。

---

## P2 · 引擎移植 · 后端转发层(本仓)

- **比价透传端点**：仿 `coupon_step` **纯 body 透传、不加 Pydantic schema**(协议归 pricebot 校验，否则它一改字段本仓就得跟着改，白白耦合)。client `/api/v1/xxx` → 转发 backend `/api/xxx`(去掉 v1，同 `coupon_step`)。
  - ✅ **外卖 2 个已落地**(food MVP，2026-05-27)：`intent/recognize` + `price/step`，在 `app/api/v1/compare.py`(不鉴权、超时 60s)，测试 `tests/test_compare_proxy.py`，文档 `docs/api/compare-*.md`。
  - 电商 2 个待接：`ecom/intent/recognize` + `ecom/step`——放开电商 scene 时在 `compare.py` 加两行即可。
- ✅ **转发超时已分离**(2026-05-27)：比价用新增的 `PRICEBOT_COMPARE_TIMEOUT_SEC=60`(意图识别大上下文 LLM、price/step 每帧 LLM)，与领券的 `PRICEBOT_REQUEST_TIMEOUT_SEC=30` 区分；真实耗时待真机实测再定终值。
- **生产 server 与 backend 同内网**：`PRICEBOT_BASE_URL` 部署时设内网地址。比价是 30~80 步循环，每步多一跳，走公网延迟会累积。
- **nginx body 上限**：比价 debug 包带 `screenshot`(webp base64，几百 KB)。若 debug 联调走线上域名经 nginx，注意 `client_max_body_size`(release 不带截图、debug 直连本地都无此问题，仅"debug 走域名"才撞)。

---

## P2 · 引擎移植 · 客户端(android 仓)

**领券链路相关已完成**(详见文末「已解决」):无障碍身份改名、BuildConfig 包名、领券入口、SettingsScreen 权限卡、ApiClient 改址。剩余:

- ✅ **比价悬浮球(外卖)已接**(2026-05-27)：`SUPPORTED_BIZ_PACKAGES` 收窄到 3 个外卖源(美团/淘宝闪购/京东外卖)；scene 写死 food(`getSceneMode` 默认改「外卖比价」)。点球 → `startTask` → Phase1/2 → 比价结果悬浮窗,这条链客户端本就齐(随引擎移植搬入),本次只收窄入口 + 写死场景 + 接通 server。电商放开时再加回 PDD/抖音 + scene 选择。
- **仿腾讯权限引导**：搬来的 `AccessibilityGuideActivity`/`PermissionGuide*`/`GuideOverlayService` 尚未接入口;当前是"跳系统设置页"最简版(SettingsScreen 权限卡 + 领券前检查已用)。体验后续可升级成仿腾讯浮层。
- **类名可选改名**：`PriceBotService` 等用户不可见，保留无妨；想整洁可改 `ShaguaAccessibilityService`(牵动引用，不急)。

---

## 技术债(暂不处理，知情即可)

- **agent 接口公网裸奔**：不鉴权 = 任何人可调，烧 pricebot 的 LLM 算力。与美团 3 接口现状同级，记账后随 P1 一起补。
- **既有债(已记录在别处)**：美团 `referral-link` 的 `sid` 客户端可覆盖刷分佣；`logout` 占位不吊销 token；`SMS_MOCK=true` 全站免密登录；`JWT_SECRET_KEY` 默认值可伪造 token。详见 [后端技术实现.md](../后端技术实现.md) 与 [api/](./api/)。

---

## 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,待删。
- **ComparingScreen / CompResultScreen 弃用**:领券改走悬浮窗,不再走全屏假动画。`Routes.COMPARING` / `COMP_RESULT` 成死路由,连同两个 Screen 文件待删(`CouponPromptDialog` 已抽到 `ui/coupon/`,删 ComparingScreen 不影响它)。
- **ApiClient 常量 unused**:`getBackendUrl()` 改用 `BuildConfig.BASE_URL` 后,`DEFAULT_BACKEND_URL`/`KEY_BACKEND_URL` 常量 unused,待清。
- **领券大数字语义**:悬浮窗左侧"已领券"大数字目前接 `progress.current`(正在领第几张),非"已成功数"。中间帧后端不回传已领明细,要精确需后端在 status.progress 加 `completed[]`(协议文档第四节已留扩展点)。
- **正式版 versionCode 必须 > 占坑版线上最高**:占坑版(`shaguabijia-android`)与正式版同包名 `com.jishisongfu.shaguabijia` 原地覆盖。正式版当前 `versionCode=1`,真机占坑版是 5 → 同签名降级被 Android 拒装,**正式版无法覆盖更新线上占坑版用户**。上线前 versionCode 要提到 > 线上占坑版最高值。

## 已解决

- ✅ **短信验证码接入极光(real 模式,2026-06-03)**:`/sms/send`+`/sms/login` 从 mock 升级为真实可用。`integrations/sms.py` 走**自定义验证码模式**(本服务 `secrets` 生成 6 位码 + 进程内存存储 + 极光 `/v1/messages` 只负责发 + 本地校验一次性/防爆破);鉴权**复用极光一键登录的 `JG_APP_KEY`/`JG_MASTER_SECRET`**(同一极光应用,**上线只需 `SMS_MOCK=false`**,无需额外凭证;`sign_id=31729`/`temp_id=1` 已审核就绪)。防刷四层(单号冷却 + 单号每日上限 + 单 IP `rate_limit` + 单码失败次数上限);`SmsError.status_code` 让发送失败按类型返回 429/503/400。**mock 保留"任意6位通过"**(不动其他 11 个测试文件的 `_login` helper);`tests/test_auth.py` 加 real 模式单测(极光协议 / 一次性 / 防爆破 / 余额回滚)。文档 [integrations/sms.md](../integrations/sms.md)。**遗留**:验证码进程内存,多 worker 需迁 DB/Redis(下方技术债)。
- ✅ **比价记录落库(server + client,2026-05-31)**:每次比价 done 后客户端上报、按 user 落库,作「我的比价记录」数据源 + 用户级画像沉淀。
  - **server**(本仓):新表 `comparison_record`(独立于 `savings_record`;结构化列 + `items`/`comparison_results`/`skipped_dish_names`/`raw_payload` 四个 JSON(B) 列)+ 3 个**鉴权**端点 `POST /api/v1/compare/record`(`(user_id,trace_id)` 幂等 upsert,best/saved/is_source_best/status 服务端从 comparison_results 派生)/ `GET /api/v1/compare/records`(游标分页)/ `GET /records/{id}`(含 raw_payload);`models/comparison.py` + `repositories/comparison.py` + `schemas/compare_record.py` + `api/v1/compare_record.py` + 迁移 `comparison_record_table`(head `b2c3d4e5f6a7`)+ `tests/test_compare_record.py`(8 例全过)+ `docs/api/compare-record-*.md`。
  - **client**(android 仓):`PriceBotService.runTask()` 比价 done 后(`lastDoneParams!=null`,成功/引擎失败都报)用独立 IO 协程尽力上报;`Protocol.CompareRecordRequest.fromComparison()` 从 calibration+done.params 零翻译组装;`ApiClient.reportCompareRecord()` 走新建的 `authedClient`(复用 `AuthInterceptor`+`RefreshAuthenticator`,自动 Bearer+401 刷新)。领券不报(非价格对比,本期范围只外卖)。
  - **未做**:UI(「我的比价记录」页)等原型;真机端到端待验。
- ✅ **领券链路接通**(阶段 1):首页「去领取」→ `CouponPromptDialog` → 权限检查 → `startCouponClaim` → 循环 `/api/v1/coupon/step`;Running 悬浮窗换皮(ComposeView 承载 `CouponProgressPanel`/AgentFloat,`OverlayLifecycleOwner` 撑 Compose)。
- ✅ **coupon/step 去鉴权**:去掉 `CurrentUser`,MVP 不鉴权(device_id 透传)。
- ✅ **ApiClient 改址**:`BuildConfig.BASE_URL` + `/api/v1/` 前缀。
- ✅ **SettingsScreen 权限卡**:5 项(无障碍/悬浮窗/录屏<API30/电池/自启动)真实检测 + 点击跳转 + 自启动无法检测态 + onResume 自动刷新 + 刷新按钮;逻辑抽到 `PermissionHelper`(HomeScreen 领券前检查复用)。
- ✅ **无障碍身份统一傻瓜比价**:服务名/图标继承 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](../database/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**,发奖不受影响。
