Files
meida/docs/开发任务包/T2b-任务换额度-端侧闭环.md
T
xsl f244f14ab8 feat(billing): 任务换额度端侧闭环(T2b 定稿 4 任务)
- TaskBonusPanel 重写:每日任务/新手任务分组、五态按钮、真实「去完成」跳转(first_report→诊断落地页;report_feedback/share_report→报告结果页)、40311/40310 文案、panel_view/task_tap 埋点
- ReportPdfPage 分享成功→静默 claim share_report;ReportResultPage SUCCEEDED 展示→静默 claim first_report(后端 40910 幂等兜底,老用户首进补发一次属预期回填)
- ChatPage/ReportResultPage 修 bonus_granted 类型误写(boolean→number)
- docs:T2b 任务书(验收 11 条)+ 产品方案 §3.2 任务目录定稿(签到对话+10/日、报告反馈发型+2/日、分享换装+3/日、首测重生成+1/终身;邀请好友砍)

偏差说明:share_report 落点为报告结果页而非 PDF 预览页(该页强依赖本地 pdfPath,直达需下迭代加持久化)。
2026-09-12 00:25:08 +08:00

194 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T2b · 任务换额度:端侧闭环与完成判定
> 对应任务:`T-P1-26` 收尾件之二(T2 的后续迭代)| 需求:`FR-BILL-03`、`FR-BILL-04`、`FR-BILL-07` | 跨端任务:后端(meida_backend)+ 端侧(app/)
> 先读《README.md(协作规程)》与《README-FE批次.md》,遵守全部纪律。
> 产品定稿依据:产品方案 §3.2「任务奖励额度的获取方式」(2026-09-11 定稿表与决策理由)。
## 1. 目标
把「做任务换额度」从「只有签到能真领」补齐为完整闭环:
1. 任务目录按产品定稿落地(4 项上线 + `invite_friend` 下架),奖励数值由占位值 1 改为定稿值;
2. 有服务端可信信号的任务(`report_feedback` / `first_report`)在 claim 时做完成校验,无凭据拒绝;
3. 端侧面板从「toast 占位」升级为真实「去完成」跳转 + 完成点自动 claim;
4. 全程遵守 `FR-BILL-07`:文案不出现价格/购买/会员/升级字样。
## 2. 现状事实(2026-09-11 读码核实)
### 后端(meida_backend)
- ✅ `POST /quota/tasks/{task_key}/claim` 已实现(T2):幂等(`bl_ledger.idempotency_key` 快捷路径 + 唯一约束并发兜底)、封顶(`grant_bonus` 对 `task_bonus_max`)、配置化(`TASK_BONUS_RULES_JSON`)、错误码 40410/40910/40310 文案合规。见 `apps/api/routers/quota.py:65`、`packages/billing/service.py:306`、`packages/domain/billing/quota.py:146`。
- ✅ 配额种子与产品方案 §3.2 对齐(`migrations/versions/0003_align_quota_with_product_ssot.py`):`agent_chat` 10/10/40、`tryon` 3/3/8、`hairstyle` 3/2/6、`hd_export` 1/0/3、`report_basic` 终身 1、`report_regen` 月 1/1/2。
- ❌ **claim 不做任何完成凭据校验**:任何登录用户对任何 task_key 直接 POST 即领。`report_feedback`(`rp_feedback` 表)与 `first_report`(`rp_report` 表)明明有可信信号却没用上。
- ❌ `task_bonus_rules` 默认值(`packages/shared/config.py:136`)amount 全是占位值 1,且映射与 §3.2 总量对不上:换装最多 +2/日(表定 +3)、发型 0(表定 +2)、对话 +2(表定 +10)。
- ❌ 无任务目录下发接口——V1 决定端侧静态表,不做。
- 事实备注:`rp_share` 表已建结构但无路由(models.py 注释「V1 只建结构」);`POST /events/batch` 无事件白名单(事件字典待 TD-23)。
### 端侧(app/entry/src/main/ets/)
- ✅ `components/f1/TaskBonusPanel.ets`:五项静态任务、已领态走 `core/TaskBonusState.ets`(preferences `meida_task_bonus`,跨重启)、40910 同步已领态、文案合规;已挂载 5 处(`ProfileContent` / `ChatPage` / `FittingRoomPage` / `HairDesignPage` / `HairColorPage`)。
- ✅ 真实闭环已有两条:`sign_in` 点击即 claim(面板内);`report_feedback` 在报告页反馈提交成功后静默自动 claim(`ReportResultPage.ets:435`)。
- ✅ `DailyLimiter.setBonus` 已把 `bonus_granted` 计入试穿/试戴当日上限;`ChatPage` 对话额度服务端口径(remaining 含 bonus)。
- ❌ 其余 3 项任务是 toast 占位(「即将上线」),无「去完成」跳转。
- ❌ `share_report` 无联动:`ReportPdfPage.ets:90` 已有 Share Kit `systemShare` 分享 PDF,但分享动作与 claim 之间没有凭据、没有联动。
- ❌ 报告生成成功处无 `first_report` claim 联动。
- ❌ 面板不展示每项的奖励数值与周期(任务表纪律是"数值不硬编码",定稿后可写入展示文案,到账仍以 claim 响应为准)。
- 🐞 `ChatPage.ets:36` `QuotaWireResp.bonus_granted` 类型误写为 `boolean`(后端返回 number;该字段暂未消费,本迭代顺手修正)。
### 判定与埋点
- ✅ 埋点链路通:`Tracker` → `POST /events/batch` → `ob_event`(整批 fail-close 校验)。
- ❌ 任务完成事件目前只有 `task_bonus.claim_result`;无面板曝光、任务点击、分享调起事件。
- 产品决策(已写进 §3.2):**埋点只作观测,不作发放凭据**——埋点本身可伪造且有 20 条/30s 节流延迟,拿它做校验是自欺。
## 3. V1 任务目录(产品定稿,2026-09-11)
| task_key | 名称 | quota_key | amount | claim_period | 完成判定 | 状态 |
|----------|------|-----------|--------|--------------|---------|------|
| `sign_in` | 每日签到 | `agent_chat` | **10** | `day` | 点击签到即完成,无额外校验 | 上 |
| `report_feedback` | 给报告结论打反馈 | `hairstyle` | **2** | `day` | 服务端校验当日 `rp_feedback` 存在 | 上 |
| `share_report` | 分享我的报告 | `tryon` | **3** | `day` | 端侧自报,无服务端校验(内测水位) | 上 |
| `first_report` | 完成首次形象速测 | `report_regen` | **1** | `lifetime` | 服务端校验 `rp_report` 存在已完成报告 | 上 |
| `invite_friend` | 邀请好友 | — | — | — | 无可信信号 | **下架**(env 不下发该键) |
变更点:删除 `profile_complete`(资料维护 `FR-ACC-06` V1 未实现,无可信信号),新增 `first_report`;`report_feedback` 由 `report_regen` 改挂 `hairstyle`(理由见产品方案 §3.2 决策理由第 3 条)。
## 4. 实现要点
### 后端(worktree 纪律见《README.md》)
允许改动:`packages/billing/service.py`、`packages/billing/repo.py`(如需)、`packages/billing/schemas.py`(如需)、测试新文件。
1. **B1 · 定稿数值走 env**:部署/本地 `.env` 的 `TASK_BONUS_RULES_JSON` 改为:
```json
{
"sign_in": {"quota_key": "agent_chat", "amount": 10, "claim_period": "day"},
"report_feedback": {"quota_key": "hairstyle", "amount": 2, "claim_period": "day"},
"share_report": {"quota_key": "tryon", "amount": 3, "claim_period": "day"},
"first_report": {"quota_key": "report_regen", "amount": 1, "claim_period": "lifetime"}
}
```
不含 `invite_friend`——不下发即下架(claim 返回 40410「任务不存在或暂未开放」),这是配置化设计的红利,不要为它写分支代码。
⚠️ `config.py` 的默认值是占位且**禁改**;在提交信息里写明「默认值与定稿不一致,建议协调提交同步默认值与 `.env.example`」,由协调者统一处理。
2. **B2 · claim 完成校验钩子**:`BillingService.claim_task_bonus` 在解析规则后、幂等快捷路径**之前**,按 task_key 执行完成校验(映射写死在 service 内即可,不做配置化):
- `report_feedback`:`rp_feedback` 存在 `user_id` 匹配且 `created_at` 落在当日(UTC+8,与 `period_key` 同时区口径)的行;
- `first_report`:`rp_report` 存在 `user_id` 匹配且状态为已完成的行;
- `sign_in` / `share_report`:**不校验**,写注释说明产品决策(签到行为本身即任务;分享内测期自报,`hard_cap` 兜底,见 §3.2 决策理由 4)。
- 校验不过 → `AppError(ErrorCode.TASK_BONUS_NOT_ELIGIBLE)`。**该错误码尚不存在**(建议 40311,meta「先完成任务,再来领奖励」)——`errors.py` 禁改,在提交信息中列出需求,由协调提交统一加;本地开发可先在 service 里 `raise AppError(...)` 引用占位并在提交信息标注。
3. **B3 · 幂等与封顶语义不变**:40910/40310 路径、`delta=0` 流水口径不动;校验逻辑不得影响既有测试。
### 端侧(纯 Windows,构建门禁见《README-FE批次.md》§1)
允许改动:`components/f1/TaskBonusPanel.ets`、`pages/ReportResultPage.ets`、`pages/ReportPdfPage.ets`、`pages/ChatPage.ets`(仅 B 端 wire 类型修正)。其余页面如需挂回调点,在提交信息里说明。
1. **E1 · 面板任务表更新为定稿 4 项**:删 `invite_friend`;`profile_complete` 替换为 `first_report`(名称「完成首次形象速测」);每项 desc 写清奖励内容(如「领 10 轮 AI 对话额度」「领 2 次发型试戴额度」)并标注周期(「每日」/「一次性」)。到账数值仍以 claim 响应 `amount` 为准,desc 只是展示。
2. **E2 · 「去完成」真实跳转**(`router.pushUrl`,禁止 import 页面):
- `first_report` → `pages/DiagnosisLandingPage`;进入面板时若已可领(服务端已有报告且未领过)则按钮直接为「领额度」走 claim;
- `report_feedback` → 报告结果页(带 `reportId` 参数);无报告时 toast「先生成你的形象速测,再来反馈」;
- `share_report` → `pages/ReportPdfPage`(分享入口所在页);无报告时同上引导;
- `sign_in` 保持点击即领。
3. **E3 · 完成点自动 claim**(与既有 `claimReportFeedback` 同款静默闭环:成功 toast 到账、40910 静默同步已领态、其他失败静默):
- `ReportPdfPage.share()` 调起系统分享面板成功后:静默 claim `share_report` + `TaskBonusState.markClaimed('share_report', PERIOD_DAY)` + `Tracker.track('report.share_invoke', ...)`;
- 报告生成任务成功(`ReportResultPage` 展示 SUCCEEDED 结果的入口路径):静默 claim `first_report` + `markClaimed`。
4. **E4 · 状态与文案**:见 §5,逐字遵守。
5. **E5 · 埋点补齐**(事件名字面量、props 只放低维枚举/数值):`task_bonus.panel_view`、`task_bonus.task_tap`(props: `task_key`)、`report.share_invoke`、`task_bonus.claim_result`(已有,保留)。
6. **E6 · 修正** `ChatPage.ets:36` `bonus_granted: boolean` → `number`。
## 5. UX 与文案规格(FR-BILL-07,逐字遵守)
**面板信息架构**:标题「做任务换额度」;分组一「每日任务」(签到 / 打反馈 / 分享报告);分组二「新手任务」(完成首次形象速测);底部说明保留「奖励以实际到账为准,额度每日重置」。
**按钮五态**:
| 状态 | 文案 |
|------|------|
| 可领取(签到 / 已满足条件) | 「领额度」 |
| 需跳转完成 | 「去完成」 |
| 领取中 | 「领取中…」 |
| 已领(日周期) | 「今日已领」 |
| 已领(终身) | 「已领取」 |
**toast**:
| 场景 | 文案 |
|------|------|
| 领取成功 | 「签到成功,额度 +{amount}」/「感谢反馈,额度 +{amount}」(amount 取响应值,不硬编码) |
| 40910 本周期已领 | 「今天已经领过啦」 |
| 40310 奖励封顶 | 「今日任务奖励已达上限,明天再来」 |
| 40311 未完成 | 「先完成任务,再来领奖励」 |
| 无报告时去做反馈/分享 | 「先生成你的形象速测,再来反馈」 |
**红线**:全部新增文案不得出现价格、购买、会员、升级、充值等字样(`FR-BILL-07`);额度耗尽入口文案维持「今日额度已用完,明天再来,或做个任务多换几次」口径。
## 6. 测试要求
### 后端
- 单测:校验钩子纯逻辑(若抽纯函数)覆盖有/无凭据两分支。
- 集成测试新文件 `tests/integration/test_task_bonus_eligibility.py`(允许跑,`MEIDA_RUN_INTEGRATION=1`):
1. 无报告用户 claim `first_report` → 40311;创建报告完成后 → 200 且 `bonus_granted`=1;
2. 无反馈 claim `report_feedback` → 40311;提交一条反馈后 → 200 且发型 `bonus_granted`=2;
3. `sign_in` claim → 200,`agent_chat` `bonus_granted`=10;当日再 claim → 40910;
4. `invite_friend`(不在 env)→ 40410;
5. 40311 与 40310 路径均**不写流水**(查 `bl_ledger` 无新行)。
- 提交前全绿:`ruff check` / `ruff format --check` / `mypy`(范围同 T2)/ `lint-imports` / `pytest -q`。
### 端侧
- 构建门禁(唯一验收命令,见《README-FE批次.md》§1)`BUILD SUCCESSFUL`;
- ArkTS 严格模式铁律(README-FE批次 §3)逐条遵守:显式泛型、具名 wire interface、throw 只抛 ApiError。
## 7. 禁改清单(通用禁改之外)
- 后端:`packages/shared/config.py`、`packages/shared/errors.py`、`packages/shared/models.py`、`migrations/**`、`apps/api/routers/events.py`、`packages/analytics/**`。新错误码与默认值同步**只写进提交信息**,由协调者处理。
- 端侧:`core/**`(含 `TaskBonusState.ets`、`Tracker.ets`、`DailyLimiter.ets`——本迭代需求已够,不要顺手扩展)、`module.json5`、`main_pages.json`、resources、`oh-package*.json5`。
- 不改配额种子数值、不动 `grant_bonus` 语义、不改既有测试断言语义。
## 8. 验收标准(按组,每条可验收)
### 后端
1. 四个 task_key 按 §3 定稿数值 claim 成功,响应 `amount` 与定稿一致;`invite_friend` 返回 40410。
2. 无凭据 claim `report_feedback` / `first_report` 返回 40311 且无流水;有凭据后 claim 成功。
3. 重复 claim 40910、封顶 40310 行为与 T2 验收一致,无回归(既有 `test_task_bonus.py` 全绿)。
4. 后端检查全绿(§6)。
### 端侧
5. 面板只展示 4 项定稿任务,奖励文案、周期标注、五态按钮与 §5 一致。
6. 三项「去完成」跳转可达目标页;无报告场景给引导 toast 而非报错。
7. 分享调起成功、报告生成成功两处自动 claim 生效,面板已领态同步(含杀进程重启后)。
8. `ChatPage` wire 类型修正后构建绿;埋点新增事件在 `ob_event` 可见。
### 联调
9. 全链路走查(真机或模拟器连本地后端):签到 → 对话额度卡 +10;报告页打反馈 → 发型当日上限 +2(`DailyLimiter` 生效);PDF 页分享 → 换装当日上限 +3;完成首次速测 → 重生成额度 +1(个人中心额度卡可见)。
10. 异常路径走查:同日重复领取 →「今天已经领过啦」;未打反馈直接在面板点领取(若 UI 可达)→「先完成任务,再来领奖励」。
11. 文案红线检查:任务面板、toast、额度耗尽提示全文检索无「价格/购买/会员/升级/充值/¥」字样。
## 9. 提交信息
后端:
```
feat(billing): task bonus final rules + eligibility check (T2b)
TASK_BONUS_RULES_JSON 定稿(签到+10对话/反馈+2发型/分享+3换装/首测+1重生成,
invite_friend 下架);report_feedback/first_report claim 前完成校验(rp_feedback/
rp_report 可信信号),sign_in/share_report 不校验(产品决策见产品方案 §3.2)。
偏差说明:TASK_BONUS_NOT_ELIGIBLE(40311) 错误码与 config.py 默认值同步需协调提交;
<其余偏差写这里,无则写"无">
```
端侧:
```
feat(f1): task bonus panel final catalog + auto-claim hooks (T2b)
面板 4 项定稿任务与「去完成」真实跳转;分享调起/报告生成成功自动 claim;
新增 panel_view/task_tap/share_invoke 埋点;修正 ChatPage bonus_granted 类型。
偏差说明:<如有写在这里,无则写"无">
```