docs: add phase 2 billing review

This commit is contained in:
QiuSW
2026-07-02 16:33:31 +08:00
parent 826f5f685b
commit 3afa5284c7
11 changed files with 151 additions and 24 deletions
+40
View File
@@ -459,3 +459,43 @@
- 定价规则绑定能力别名字符串,不外键到具体 `AiModel`;后台切换 `ModelAlias` 指向不改变价格。
- T-202 不实现扣点、退点、充值订单或 API 编排;这些留给 T-203/T-304/T-305/T-302。
- 下一步:领取 T-203 并发安全扣点 / 退点(billing 层)。
## 2026-07-02 T-203 并发安全扣点 / 退点(billing 层)
- 状态:DONE
- 变更:
- 新增 `apps/billing/services.py`:`precharge_call()` 在事务内 `select_for_update()` 锁 `UserWallet` 行,余额足够才预扣,写 `CallRecord(status=pending)` 与 `PointsLedger(consume, -N)`;余额不足抛 `InsufficientPointsError(code="insufficient_points")`,不写调用/流水。
- 新增 `mark_call_success()`:上游成功后只更新调用记录为 `success` 与结果摘要/耗时,不再改余额。
- 新增 `refund_call_points()`:失败退点锁 `CallRecord`,同一调用只写一条 `refund` 流水并恢复余额;重复调用幂等,不重复加点;成功调用不能走失败退点。
- 扩展 `apps/billing/tests.py`:覆盖预扣写账、余额不足不写账、失败退点幂等、成功后拒绝失败退点、2 线程并发扣同一钱包不超扣不为负。测试数据改成唯一用户名/别名,避免远程 `test_cmhub` 断线残留造成重复键假失败。
- 同步更新 `README.md`、`docs/00-ai-start-here.md`、`docs/04-architecture.md`、`docs/api.md`、`docs/06-tasks.md`、`docs/current-state.md`、`docs/project-brief.md`、`docs/project-onepager.md`:Phase 2 完成,下一步 T-301。
- 验证:
- `py -3.12 -m py_compile apps\billing\services.py apps\billing\tests.py`:通过。
- `py -3.12 manage.py test apps.billing.tests.BillingServiceTests --noinput --keepdb`:通过,4 tests OK。
- `py -3.12 manage.py test apps.billing.tests.ConcurrentDebitTests --noinput --keepdb`:通过,1 test OK。
- `py -3.12 manage.py check`:通过,0 issues。
- `py -3.12 manage.py makemigrations --check`:通过,No changes detected。
- `py -3.12 -m compileall apps`:通过。
- `git diff --check`:通过,仅 Windows CRLF 提示。
- `./init.ps1`:通过,依赖同步与基础检查正常。
- `py -3.12 manage.py test --noinput --keepdb`:未作为绿灯;发现 43 tests,跑到 42 tests 后远程 MySQL 连接超时,导致并发测试线程拿不到连接,测试库 flush/teardown 也断线。
- `py -3.12 manage.py test apps.billing --noinput --keepdb` / 不带 `--keepdb`:未作为绿灯;远程 MySQL 在 `PricingCalculationTests` 或测试库创建/销毁阶段超时/断线。T-203 新增的服务测试与并发测试已单独通过。
- 阻塞:无代码阻塞。远程 MySQL `43.128.3.240:3306` 对完整测试仍间歇超时/重置,并且失败后可能保留脏 `test_cmhub`;后续建议优先用 `--keepdb` + 唯一测试数据分 app 验证,必要时清理测试库后再跑全量。
- 决策:
- T-203 只做 billing 层扣点/退点服务,不做 DRF 认证、生成接口编排、充值订单或支付回调。
- 失败退点以 `CallRecord` 行锁 + 已有 refund 流水检查实现幂等,不给 `CallRecord` 增加 refunded 状态。
- 下一步:领取 T-301 API Key 鉴权(DRF Authentication)。
## 2026-07-02 Phase 2 计费核心审核(Claude Code review,非任务)
- 状态:DONE(审核完成)
- 变更:新增 `docs/phase-2-review.md`;`06-tasks.md` 新增 T-204 加固任务;`docs/README.md` 导航登记。
- 审核结论:**T-201~203 验收全部达标,质量为三阶段最高,零 P1**。§8 资金安全硬约束逐条核对通过——先扣后调/失败必退(precharge/mark_success/refund 三原语对齐 4.1 时序)、并发安全(事务 + `select_for_update` 锁 wallet + DB 级 `points_balance>=0`)、退点幂等(锁 CallRecord 行 + 查 REFUND 流水去重)、全程留痕(ledger/call_record admin 只读=只追加)、wallet 余额 admin 只读(后台无法绕过账本)、ApiKey sha256+hmac.compare_digest+prefix+hash 脱敏、计费按别名解耦模型 + 分辨率 wildcard、Decimal floor 计费、CallRecord 无 raw 字段、错误码 api.md 对齐(insufficient_points→402/no_pricing_rule→400)。
- 关键证据:`ConcurrentDebitTests` 用真线程 + Barrier 强制并发扣同一钱包,**在 MySQL `test_cmhub` 上单独跑过(1 test OK)**——`select_for_update` 真生效,不是 SQLite 假绿。DB 级 CHECK/Unique 约束全部落到迁移文件。
- 发现(详见 `phase-2-review.md`):
- P1:无。
- P2-1:退点幂等缺 DB 级兜底(无「每 ref_call 最多一条 REFUND」约束,锁内安全但无 backstop);T-204 应加 MySQL 可落地的 `ref_call + change_type` 复合唯一约束 + 直写 IntegrityError 测试,不使用 MySQL 不支持的 partial unique / 条件唯一约束。
- P2-2:完整测试套件从未单次全绿(远程 MySQL 间歇超时/重置),靠分 app 子集通过佐证;建议在稳定 MySQL 上跑一次完整全绿留证,并标注「并发测试须在 MySQL 上跑,SQLite 会假绿」。
- P3(登记):手工调点服务未实现(模型层 reason 守卫已备好、wallet admin 已锁死=安全中间态)→ T-401;扣点热路径 wallet get_or_create 掩盖钱包缺失 → T-501;锁顺序约定;只追加靠 admin+无 update 保证(DB 无触发器);OperationType 双份枚举 DRY。
- 未本地复跑:审核机无 python3.12,结论基于静态审查(users/billing models、services、pricing、两 app admin、tests、全部迁移、settings)+ codex 执行记录(T-201 34 tests / T-202 billing 10 tests / T-203 BillingServiceTests 4 + ConcurrentDebitTests 1 均单独在 MySQL 通过;完整套件因连接问题未单次全绿,失败点均为连接/建库超时非断言失败)。
- 下一步:codex 可领 T-204 做 P2 加固(无 P1,不阻塞 T-301)。