POST /quota/tasks/{task_key}/claim:配置驱动任务注册表、
grant_bonus 纯域函数、ledger 幂等键防重、task_bonus_max 封顶(FR-BILL-03/04)。
实现:packages/domain/billing/quota.py 新增 BonusDecision/grant_bonus(mypy strict,
恰好到顶允许、超一格整体拒绝)、grant_idempotency_key、PERIOD_TYPES;
packages/billing/service.py 新增 claim_task_bonus(幂等快捷路径 → 行锁 → 封顶判定
→ 同事务更新 bonus_granted 并写 system_grant 流水,delta=0 且 balance_after 按
remaining 口径;IntegrityError 并发兜底回滚为 40910);响应模型 TaskBonusClaimResponse
放新建的 packages/billing/schemas.py;路由挂在 apps/api/routers/quota.py,
rules 由 settings.task_bonus_rules 注入(amount 为占位值,产品定稿后改 env)。
测试:新增单测 11 个(封顶边界、amount<=0、不动 used、hard_cap 独立生效、
默认规则形状与 env 覆盖通路);新增集成 4 个(领取+重复 40910+纯 DB 流水断言、
五类任务全部到账、未知任务 40410、amount=11>max=10 → 40310 且无新流水)。
门禁全绿:ruff check / ruff format --check / mypy 两段 / lint-imports;
单测 180 passed,集成 7 passed。
偏差说明:1) 任务文档提到的 BillingService._load_rule 实际名为 _require_rule,
直接复用现有助手;2) packages/billing/repo.py 无需新增函数(现有
get_ledger_by_idempotency_key/lock_or_create_quota/add_ledger 已覆盖本任务);
3) 文档设想的「TASK_BONUS_RULES_JSON 空值容错」经 env 源实测不成立——
pydantic-settings 对 dict 字段先做 JSON 解码,空白字符串在 Settings 构造时即
报错(共享校验器/config.py 属禁改,未动);单测改为覆盖受支持的空配置形态 "{}"。
packages/billing/ — 配额与计量(V1)
⚠️ V1 不收费(TD-28)。本域在 V1 的职责是配额限制 + 计量, 不含任何支付、订单、购买、会员能力。
已实现
bl_quota_rule(规则配置化,改配置不发版,FR-BILL-04)bl_quota(按period_key自然重置,无需定时任务清零)bl_ledger(流水是真相,余额是缓存)- 预扣三段式
reserve → commit → refund(架构 §7.4,FR-BILL-05) - 服务端硬校验(FR-BILL-06);退还幂等(refund 键由 reserve 键派生)
架构约束(不可妥协)
V1 的配额账本与 V2 的点数账本必须是同一张表。 V1 只用 system_grant /
consume / refund 三种 tx_type,V2 增加 purchase / gift。这样 V2 上
支付时不需要数据迁移,也不需要重写扣减逻辑。不要为了"V1 更简单"把表拆开。
为什么是预扣不是直接扣
长耗时任务可能失败,直接扣会产生「扣了额度没拿到结果」的客诉。V1 虽不涉及 金钱,用户对"次数被白扣"的敏感度不比扣钱低,而且 V1 没有客服退款兜底手段, 只能靠机制保证。
待实现
- FR-BILL-03 任务奖励发放(签到/完善资料/反馈/邀请/分享,同任务同周期幂等)
- 每日对账:余额由流水重算校验,不一致则告警。V1 就要做,否则 V2 上支付时 对账逻辑得从零开发且无历史数据验证。
- 设备/账号/IP 三重限流(FR-BILL-06)
- Redis 配额计数器 + PG 落账的两级结构(架构 §10)
文案约束(FR-BILL-07)
额度相关提示里不得出现任何价格、购买、会员、升级字样,只提供「等待恢复」
与「做任务换额度」两条路径。tests/unit/test_response_envelope.py 会扫描全部
错误码文案。