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

8.5 KiB
Raw Permalink Blame History

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 的 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。