Files
cmhub/docs/phase-4-review.md
T

72 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 4 用户端审核报告(T-501 ~ T-504)
> 审核人:Claude Code(全栈视角)|日期:2026-07-03|结论:**功能验收全部达标,测试覆盖极全,零 P1(无安全/资金/越权缺陷)**。
> Phase 4 是自助用户端(Django 模板 SSR + allauth)。逐条比对当时验收:注册不送点数、session+CSRF、越权(IDOR)防护、API Key 明文只显一次、吊销后调用 403、记录仅见本人、充值到账以回调为权威——**全部正确落地且有专测**。2026-07-08 新需求已由 T-608 改为「新用户注册送 100 点」,本报告保留历史审核口径。剩余为前端资源与 UX 优化项(P2/P3)。
> 本文面向 codex 执行:每条给出「症状 / 位置 / 怎么改 / 怎么验证」。修补任务见 [`06-tasks.md`](06-tasks.md) 的 **T-505**;按任务看板领取规则,当前应先完成 T-505,再进入 T-401。
## 一、验收核对(功能全部达标)
| 任务 | 验收要点 | 结果 |
| --- | --- | --- |
| T-501 | 自助注册(邮箱验证)、登录、登出;注册后钱包 0 点(不送点数);session+CSRF | ✅ 注册建 0 点钱包且**不写 `PointsLedger`**(专测);未验证邮箱不能登录;登录 POST CSRF 保护 |
| T-502 | 生成/删除 Key;明文只显一次、库内只存哈希;列表只显 prefix;删除即吊销、吊销后调用 403 | ✅ 全部专测覆盖,含「删除→外部 API 返回 403」与「不能删他人 Key」(IDOR) |
| T-503 | 剩余点数、充值总额、充值/消费记录;数据与流水一致;仅见本人 | ✅ 余额/汇总同源于流水;三处记录页均按 `user=request.user` 过滤(专测越权) |
| T-504 | 发起充值→二维码→轮询→到账刷新;到账以回调为权威 | ✅ 下单**不加点不写流水**(专测);轮询 T-305 status 端点;paid 后 reload;入账仍走幂等回调/查单 |
## 二、做对的(勿在修补中回退)
1. **注册不送点数、且不留误导流水**:`CmhubAccountAdapter.save_user` 在事务内 `get_or_create` 建 `points_balance=0` 钱包,**刻意不写任何 `PointsLedger`**(避免被误解为赠点);专测断言注册后 0 点、无流水。顺带解决了 Phase 2 P3「钱包应在注册时创建」——precharge 不再依赖热路径建钱包。
2. **越权(IDOR)防护到位**:删除 Key 用 `get_object_or_404(ApiKey, pk=pk, user=request.user)`(他人 Key 返回 404 不泄露存在性);所有记录/订单查询一律 `filter(user=request.user)`;充值页 `current_order` 也按 user 过滤。每条都有「只见本人」专测。
3. **API Key 明文只显一次**:生成后 `raw_key` 存入 session,下次渲染 `session.pop` 取出展示后即清除(PRG 模式);刷新不再出现;库内只存 `key_hash`+`key_prefix`。专测断言明文只显一次、库内只存哈希。
4. **删除=吊销(软删)**:`status→revoked` 而非物理删除,保留审计与 `CallRecord` 关联;吊销后外部 API 返回 403(与 T-301 口径一致:revoked→403、缺失/无效→401)。codex 主动修正了任务原文的「401」为正确的「403」。
5. **session+CSRF 全覆盖**:所有页面 `LoginRequiredMixin`;所有 POST 表单带 `{% csrf_token %}`;登录、建/删 Key、充值下单的 CSRF 都有专测。
6. **充值到账以回调为权威**:下单只建 `pending` 订单、**不碰余额**;前端轮询 `api-recharge-status`(`same-origin` 会话),status 端点内部对 pending 单走 `query_and_apply_recharge_payment`(复用幂等、金额校验的 `apply_recharge_payment`);paid 后 reload 刷新余额。资金入账始终经计费层,页面只读。
7. **记录与流水同源**:余额用 `get_balance_snapshot()`;充值总额按 paid `RechargeOrder.amount_money`;入账/消费按 `PointsLedger` 汇总;`net_used_points = 消费 − 退款`(退款 delta 为正、消费取绝对值,口径正确)。
8. **用当前 allauth API**:`ACCOUNT_LOGIN_METHODS` / `ACCOUNT_SIGNUP_FIELDS`(65.x 新式设置)、`ACCOUNT_EMAIL_VERIFICATION="mandatory"`、`ACCOUNT_UNIQUE_EMAIL`,与 T-201 的 `User.email` 唯一约束对齐。未引入 crispy-forms(MVP 保持轻)。
## 三、优化建议 / 修补清单
### P1 · 现在改
**无。** 用户端零 P1——越权、CSRF、注册不送点、明文只显一次、吊销 403、到账以回调为权威全部正确且有测试。以下为前端资源与 UX 优化。
### P2 · 建议处理
#### P2-1 外部 CDN 无 SRI + 国内可用性(含支付页)
- **症状**:`base.html` 从 jsdelivr 加载 Bootstrap CSS、`recharge.html` 从 jsdelivr 加载 `qrcode.min.js`,**均无 `integrity`(SRI)/`crossorigin`**。两个问题:
1. **供应链/XSS**:无 SRI 的 `<script src=cdn>` 一旦 CDN 被投毒或遭 MITM,任意 JS 会在**已登录、带 session 的用户端(尤其充值页)**执行。
2. **国内可用性**:本项目面向微信/支付宝用户,而 jsdelivr 在中国大陆**间歇性被墙/不稳**。CDN 失败时 Bootstrap 丢失→页面错版;`qrcode.js` 丢失→二维码画不出(有 code_url 文本兜底但体验退化)。
- **位置**:`apps/portal/templates/portal/base.html`、`apps/portal/templates/portal/recharge.html`。
- **怎么改(推荐自托管)**:把 Bootstrap、qrcode.js 纳入 `static/`,用 Django staticfiles(+ `collectstatic` + WhiteNoise/nginx)本地提供;若坚持用 CDN,则**必须**加 `integrity="sha384-..."` + `crossorigin="anonymous"`。自托管同时解决国内可用性,且与 T-403 部署(静态资源收集)天然衔接。
- **验证**:断网/屏蔽 jsdelivr 后充值页仍能出二维码、样式正常;页面源码无无 SRI 的外链脚本。
#### P2-2 记录页无真正分页(静默截断历史)
- **症状**:`RECORDS_PAGE_SIZE=50` 只是 `queryset[:50]` 切片——充值记录 / 消费记录**超过 50 条后,更早的记录用户永远看不到**,且无「仅显示最近 50 条」提示、无总数、无翻页。与 T-503「查看自己的充值记录/消费记录」的完整性预期不符。
- **位置**:`apps/portal/views.py`(`RechargeRecordListView` / `UsageRecordListView`)。
- **怎么改**:用 Django `Paginator`(或 `ListView` + `paginate_by`)做真正翻页;至少先显示总条数并标注「仅显示最近 N 条」。
- **验证**:造 >50 条记录,能翻到第 2 页 / 或页面明确提示被截断。
### P3 · 登记 / 后续
- **API Key 数量无上限**:用户可无限建 Key(每个都是一行 + 列表项)。建议加每用户活跃 Key 上限(如 10)。→ Backlog / T-401 后台侧。
- **充值下单无频率限制**:用户可反复点「创建订单」刷出大量 pending 订单(每次调支付网关)。建议限制每用户未支付订单数或加节流。→ Backlog。
- **生产邮件后端**:`ACCOUNT_EMAIL_VERIFICATION="mandatory"` + 默认 `console` 邮件后端——**生产必须配真实 SMTP**,否则验证邮件发不出、用户无法验证→无法登录。→ 明确写入 T-403 部署文档。
- **`ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION=True`**:点验证邮件链接即自动登录,便利但邮件链接=登录入口(转发风险)。MVP 可接受,登记备忘。
- **新 Key 明文经服务端 session 存储**:`raw_key` 在下次渲染前短暂存于 session(DB 后端)表中,`pop` 后清除。窗口极短可接受,登记。
- **allauth `account.EmailAddress` 的 `models.W036`**(MySQL 条件唯一约束警告):邮箱唯一性已由 `User.email` 唯一约束承担,属 cosmetic 警告,登记。
## 四、说明:未本地复跑
审核机(WSL)无 `python3.12`,**未本地复跑**。本报告基于静态审查(`portal/views`、`adapters`、`forms`、`urls`、全部模板、`tests`;`config/settings` allauth 段)+ codex 执行记录(T-501~504 分 app 通过;T-501 有一次完整 **95 tests 单次全绿**;T-504 完整套件 60 tests 通过,失败点均为远程 MySQL 连接超时非断言失败)。
T-505 处理后,请重跑 `check`/`test`/`init` 并把证据记入 `progress.md`(`06-tasks.md` 使用规则第 5 条)。
## 五、T-505 完成定义
- **P2-1** 已处理:Bootstrap/qrcode 自托管(或加 SRI+crossorigin);断网 jsdelivr 后充值页仍可用。
- **P2-2** 已处理:记录页真正分页或明确截断提示;含 >50 条测试。
- **P3** 各项已在 `06-tasks.md` 对应任务(T-401 / T-403)或 Backlog 登记,不遗失。
- `check` 0 issues、`test` 全绿、`init` 通过,证据入 `progress.md`。