收尾两批并行开发的文档回写与任务包

- Sprint 执行计划 v1.1:§1.1 补第一批交付实据;T9 SPIKE 结论回写 Sprint 1 表
  (T-P0-16 通过 2.8ms;T-P0-12 有条件;T-P0-10 原型 6/6 + 真实照片重验前置);
  Sprint 2 表标注 T-P2-01~06 交付;新增 §9 两批交付记录
- 开发任务拆解 v1.5:交付回写 + 新风险登记(RTMPose 域差异、BiRefNet 异步化)
- 新增 开发任务包/:三批多 agent 并行的协作规程与 14 份任务契约
- 新增 前置开通清单-云资源与资质.md
- 清理已用毕的评审报告(文档评审 2026-08-13、项目整体评审 2026-08-14),
  README 中相应引用改为留痕表述
This commit is contained in:
xsl
2026-08-14 21:07:20 +08:00
parent 2934954eb2
commit e511b6a66f
22 changed files with 1407 additions and 461 deletions
@@ -0,0 +1,98 @@
# T2 · 任务奖励额度领取(claim)
> 对应任务:`T-P1-26` 收尾件 | 需求:`FR-BILL-03`、架构 §9.3 | 分支:`task/t2-task-bonus` | worktree:`/home/xsl/meida_backend-t2`
> 先读《README.md(协作规程)》,遵守全部纪律。
## 1. 目标
实现产品方案 §3.2 的「做任务换额度」闭环的服务端:`POST /quota/tasks/{task_key}/claim`。
发放即到账、**发放幂等**(同一任务同一周期不重复发放,FR-BILL-03 验收原文)、受 `bl_quota_rule.task_bonus_max` 封顶。
## 2. 已就绪的契约(协调提交已提供)
### 配置(`config.py` 已加字段,`.env.example` 已示例)
```python
task_bonus_rules: dict[str, dict[str, Any]] # env: TASK_BONUS_RULES_JSON
```
默认值(协调提交写死在 config.py,含义:task_key → {quota_key, amount, claim_period}):
```json
{
"sign_in": {"quota_key": "agent_chat", "amount": 1, "claim_period": "day"},
"profile_complete":{"quota_key": "agent_chat", "amount": 1, "claim_period": "lifetime"},
"report_feedback": {"quota_key": "report_regen", "amount": 1, "claim_period": "day"},
"share_report": {"quota_key": "tryon", "amount": 1, "claim_period": "day"},
"invite_friend": {"quota_key": "tryon", "amount": 1, "claim_period": "day"}
}
```
⚠️ `amount` 全部是**占位值 1**——具体数值待产品在产品方案 §3.2 补充后改 env,不改代码(FR-BILL-04 配置化)。代码注释里必须写明这一点。
### 错误码(`errors.py` 已加)
- `TASK_BONUS_UNKNOWN = 40410`:404「任务不存在或暂未开放」;
- `TASK_BONUS_ALREADY_CLAIMED = 40910`:409「这个周期的任务奖励已领取」,hint「下个周期再来」;
- `TASK_BONUS_CAP_REACHED = 40310`:403「任务奖励已达上限」,hint「明天再来,或等额度自然恢复」。
(注意:这些文案已按 FR-BILL-07 写好,**不得出现价格/购买/会员/升级字样**,你不需要也不要改 meta。)
### 现有代码事实(先读再写)
- `packages/domain/billing/quota.py`:`QuotaState(used, base_limit, bonus, hard_cap)`、`period_key(now, period_type, tz_offset_hours)`、`period_reset_at(...)`、`reserve_idempotency_key`/`refund_idempotency_key`;`PeriodType = Literal["day","month","lifetime"]`。
- `packages/billing/service.py`:`BillingService(session, tz_offset_hours)`;`QuotaSnapshot`;`reserve/commit/refund/get_quota/list_ledger`;内部 `_load_rule(quota_key)`、`lock_or_create_quota(...)`(返回已行锁的 `Quota` 行,`limit_value` 是 base_limit 快照,`bonus_granted` 是本周期已获奖励)。
- `packages/billing/repo.py`:`add_ledger`/`find_ledger_by_idempotency_key` 等(照现有模式补你需要的)。
- `packages/shared/models.py`:`Ledger`(`bl_ledger`:`tx_id` PK、`delta`、`tx_type`、`status`、`idempotency_key` UNIQUE、`balance_after`、`quota_key`、`period_key`、`reason`、`ref_type`、`ref_id`)。
- `apps/api/routers/quota.py`:现有 `GET /quota`、`GET /quota/ledger`,响应模式照抄。
## 3. 实现要点
允许改动:`packages/billing/service.py`、`packages/billing/repo.py`、`packages/billing/schemas.py`(新建)、`packages/domain/billing/quota.py`、`apps/api/routers/quota.py`、测试文件。
1. **纯域函数**(`packages/domain/billing/quota.py`,mypy strict):
```python
@dataclass(frozen=True, slots=True)
class BonusDecision: ...
def grant_bonus(state: QuotaState, amount: int, task_bonus_max: int) -> QuotaDecision-like
```
语义:`bonus + amount > task_bonus_max` → 不允许(封顶);否则返回 `replace(state, bonus=state.bonus + amount)`。`task_bonus_max` 来自 `bl_quota_rule`。
2. **服务方法** `BillingService.claim_task_bonus(user_id, task_key, rules)`:
- task_key 不在 rules → `AppError(TASK_BONUS_UNKNOWN)`;
- 解析 `{quota_key, amount, claim_period}`;`rule = await self._load_rule(quota_key)`;
- `pk = period_key(now, claim_period, tz)`;幂等键 `f"grant:{user_id}:{task_key}:{pk}"`;
- 先查 `bl_ledger.idempotency_key` 已存在 → `TASK_BONUS_ALREADY_CLAIMED`(幂等快捷路径);
- `lock_or_create_quota`(同周期)→ `grant_bonus(...)` 封顶判定 → 不允许则 `TASK_BONUS_CAP_REACHED`(**不写任何流水**);
- 同一事务内:更新 `bl_quota.bonus_granted`,插入 `Ledger(tx_type="system_grant", delta=0, status="committed", balance_after=state.remaining, reason=task_key, ref_type="task_bonus", ref_id=task_key)`,提交;
- 并发兜底:`IntegrityError`(唯一键冲突)→ 回滚 → `TASK_BONUS_ALREADY_CLAIMED`。**`delta=0` 的理由写进注释**:system_grant 不改变 `used`,只提高有效额度,`balance_after` 仍按 remaining 语义记录(流水可重算的是 used,不是余额本身——与现有 reserve/commit 的记账口径一致)。
3. **路由**(`apps/api/routers/quota.py` 追加):`POST /tasks/{task_key}/claim`(注意 router prefix 是 `/quota`,最终路径即架构 §9.3 的 `POST /quota/tasks/{task_key}/claim`)。请求无需 body;响应 `ok(TaskBonusClaimResponse(...))`,字段:`task_key/quota_key/amount/bonus_granted/remaining`。Pydantic 模型放 **`packages/billing/schemas.py`**(新建;apps/api/schemas.py 禁改)。
- rules 从 settings 读:路由函数签名加 `settings: SettingsDep`,传 `dict(settings.task_bonus_rules)`。
## 4. 测试要求
- 单测 `tests/unit/test_bonus_domain.py`:`grant_bonus` 的封顶边界(bonus+amount == max 允许、== max+1 拒绝)、amount≤0 抛 ValueError、不改变 used。
- 单测 `tests/unit/test_task_bonus_rules.py`:settings 解析 `TASK_BONUS_RULES_JSON`(字符串 JSON / dict / 空值容错)——参考 config.py 的 `_parse_json_object` 模式,如果协调提交已把 `task_bonus_rules` 加进该 validator 的字段列表就不用测解析,直接测默认值形状(每个 entry 必有 quota_key/amount/claim_period,claim_period ∈ PeriodType)。
- 集成测试 `tests/integration/test_task_bonus.py`(新文件,允许跑):
1. dev-token 登录 → claim `sign_in` → 200,`GET /quota?quota_key=agent_chat` 的 `bonus_granted` = 1;
2. 再 claim 同一 task → 40910;
3. 把 Redis 无关、纯 DB 断言:`bl_ledger` 出现 `system_grant` 行且 `balance_after` = base+bonus;
4. 封顶路径可用 `TASK_BONUS_RULES_JSON` 覆盖 amount 为 11(> task_bonus_max=10)→ claim → 40310,且无新流水。
(settings 覆盖用 `monkeypatch.setenv + get_settings.cache_clear()`,结束恢复,参考 T1 文档的写法。)
## 5. 禁改清单(通用禁改之外)
`packages/llm/**`、`packages/agent/**`、`packages/identity/**`、`packages/analytics/**`、`packages/storage/**`、`apps/api/routers/{agent,auth,tasks,events,uploads}.py`、`apps/api/deps.py`、`tests/integration/test_vertical_slice.py`。
## 6. 验收标准(FR-BILL-03 原文)
1. 「签到、完善资料、反馈、邀请、分享完成即到账」——五类 task_key 全部走通 claim;
2. **发放幂等**:同一任务同一周期重复 claim 返回 40910,账本无重复行;
3. 封顶:`bonus_granted` 永不超过 `bl_quota_rule.task_bonus_max`;
4. 全部检查绿(mypy 范围:`apps packages/shared packages/identity packages/billing packages/task packages/agent packages/compliance packages/analytics packages/storage` + `packages/domain` strict)。
## 7. 提交信息
```
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)。
偏差说明:<如有写在这里,无则写"无">
```