Files
xsl ecdfbb64c3 feat(billing): task bonus claim endpoint with idempotency and cap
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 属禁改,未动);单测改为覆盖受支持的空配置形态 "{}"。
2026-08-14 16:23:14 +08:00
..

packages/domain/ — 纯领域逻辑

架构 §3.2 硬性规则 2:领域逻辑放在这里,不依赖 FastAPI、不依赖数据库会话, 可纯单元测试。

能放什么

  • 数据类与值对象(配额状态、任务状态、诊断维度…)
  • 判定与计算(配额是否够、状态能否流转、置信度冲突消解、色差计算…)
  • 纯函数、不可变数据结构

绝对不能放什么

  • import fastapi / sqlalchemy / httpx / redis / celery
  • import packages.shared(连日志和配置都不行)
  • 任何 IO:读文件、发请求、查库、读环境变量
  • async def 里真的 await 了 IO 的东西

违反会被 import-linter 的 packages.domain is pure 契约在 CI 拦下。

为什么这么严

这是 AI 大量写代码时唯一还能快速验证正确性的地方 —— 没有 mock、没有 fixture、没有容器,几毫秒跑完几百个用例。一旦让 IO 渗进来,这个性质就没了, 后面所有业务规则的测试都会退化成集成测试。

mypy strict

本目录与 packages/llm/ 一起,是 mypy strict 模式覆盖的范围 (架构 §16.2)。所有函数必须有完整类型注解。

现有内容

模块 内容
ids.py 雪花 ID 生成(§6.1 原则 2)
errors.py 纯领域异常,适配层负责翻译成 AppError
billing/quota.py 配额三段式的算术与周期键(§7.4)
task/state.py tk_task 状态机(§6.2)