Commit f19672ffd021fdb60a285d59e72221379d1bd6c1

Authored by yibo
1 parent 38245bd4

Plan fourth-phase tasks: credits, Stripe topup, paid garment upload (T23-T30)

Breaks down dressup-dance-room.md 9节第4条 (credit ledger, Stripe
topup, mode 2 paid garment upload — explicitly not mode 3, which is
period 5) into T23-T30 with acceptance criteria, estimates, and a
dependency graph, matching dev-plan.md/dev-plan-2.md/dev-plan-3.md's
format.

Key decisions:
- credit_ledger joins users/sessions in the same SQLite db.js (T18) —
  architecture.md 1.4 flagged this atomicity requirement explicitly,
  this is the other half of why SQLite got picked in period 3.
  pricing_config stays a JSON file (store.js) since it's not on that
  hot atomic path.
- Flags a real doc inconsistency: architecture.md's diagram labels
  fal.ai integration "period 5", but dressup-dance-room.md §8 item 10
  and §9 item 4 both say mode 2's fal.ai path belongs in period 4.
  Resolved in favor of the more specific/recent requirements doc.
- Flags an uncovered gap: §5 references an image-moderation vendor
  "TBD, see §8" but §8 has no matching item for it — added as an open
  question here (§1.2).
- Three genuinely external blockers (image moderation vendor, vision
  model vendor for auto-tagging, real Stripe API keys) are each
  designed around with a pluggable interface (same pattern as T20's
  sendVerificationEmail) so building isn't blocked on vendor picks —
  except the Stripe key itself, which only the user can supply.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Deddn3sM3oQnmvqWSMb4y
Showing 1 changed file with 146 additions and 0 deletions
docs/todo/dev-plan-4.md 0 → 100644
  1 +# 换装跳舞直播间 — 第四期开发任务规划
  2 +
  3 +需求见 `docs/todo/dressup-dance-room.md`(下称"需求文档")第 3.2 节模式二、
  4 +第 5 节内容审核、第 7 节积分与充值、第 9 节第 4 条;设计见
  5 +`docs/todo/dressup-dance-room-mvcp.md`(下称"MVCP")M.2/M.4/M.9/M.12;
  6 +架构见 `docs/structure/architecture.md`(下称"架构文档")1.4/2.3 节。第一
  7 +(T1-T10)、第二(T11-T17)、第三期(T18-T22)均已完成并提交。任务编号
  8 +接着往下编。
  9 +
  10 +工时是**粗估**,单位人天,用来排先后不用来考核。
  11 +
  12 +---
  13 +
  14 +## 0. 第四期范围(需求文档 9 节第 4 条)
  15 +
  16 +**做**:积分体系(余额/流水/配置化扣费)+ Stripe 充值接入,上线**免费
  17 +换衣服(已有)+ 付费传衣服(模式二)**,验证"扣费→生成→失败退还→
  18 +审核通过后沉淀进公共库"这条完整链路。
  19 +
  20 +**明确不做**(留到第五期,需求文档 9 节第 5 条):
  21 +
  22 +- 模式三(付费传人物)——包括它的核心业务规则"付费新人物插队播放,
  23 + 不参与投票"(架构文档 2.6 节 `priority: paid_boost`),跟模式二完全
  24 + 不同,本期不碰排播优先级逻辑。**模式二内容仍然走 v1 现有投票机制
  25 + 排序**,需求文档 3 节"共同后置流程"里写得很明确:只有模式三跳过
  26 + 投票,模式二不跳。
  27 +- 人脸识别黑名单审核(模式三专用,需求文档 5 节)。
  28 +- `blacklist`(MVCP M.11)完整的运营手工拉黑管理页面——本期只做
  29 + `moderation_log`(M.9)记录自动审核结果,不做"管理员随手拉黑一个
  30 + 用户/资产"这套后台 CRUD。
  31 +- "我的作品"个人中心页面(需求文档 7.4 节)。
  32 +- 支付宝/微信支付等 Stripe 之外的渠道(7.3 节已说"需要时再加一个入口
  33 + 即可",本期不预先做)。
  34 +
  35 +---
  36 +
  37 +## 1. 已确认的技术决策 + 需要你决定的开放问题
  38 +
  39 +### 1.1 已确认
  40 +
  41 +- **`credit_ledger` 落 SQLite,跟 `users`/`sessions` 同一个 `data.sqlite`**
  42 + (复用 T18 的 `db.js`)——架构文档 1.4 节点名了"扣积分 + 创建生成
  43 + 任务 + 扣减片段配额"这几步操作需要原子性,JSON 全量重写文件在并发下
  44 + 会丢更新,这正是当初选 SQLite 的另一半理由(T18 只先用上了"密码存储"
  45 + 那一半)。`node:sqlite` 的 `DatabaseSync` 支持事务
  46 + (`db.exec("BEGIN")...COMMIT/ROLLBACK`),扣积分和退还都要包在事务里。
  47 +- **`pricing_config`(M.12)走 JSON 文件**,不进 SQLite——它是运营偶尔
  48 + 手改的配置,不在"扣积分"这条热路径的原子性要求里,复用 `store.js`
  49 + 的模式(类似 `characters.json`)比再建一张表更省事,跟"能用现成的
  50 + 就别加新东西"的项目风格一致。
  51 +- **`garments`/`characters` 的 `source`/`ownerUserId`/`moderationStatus`
  52 + 字段第一期就已经建好**(T1,见 MVCP M.3/M.4),本期不需要动
  53 + schema,只是第一次真正用上 `user_paid`/`pending` 这些取值。
  54 +- **文档发现一处不一致,按需求文档为准**:架构文档 2.2 节的流程图把
  55 + fal.ai 接入标成"第五期",但需求文档 §8 第 10 条明确写"到**第四期**
  56 + 模式二付费实时路径上线时再解决",且本文档 §9 第 4 条本来就是"上线
  57 + …付费传衣服(模式二)"。按需求文档更新、更具体的说法为准:**fal.ai
  58 + 接入是本期(第四期)的任务**,架构文档那张图的标注是旧的,没跟上
  59 + 后来的分期方案,此处不改架构文档原文,只在这里记一笔。
  60 +
  61 +### 1.2 需要你决定,但不阻塞开工(每条都设计成可插拔接口,供应商定了随时接进来)
  62 +
  63 +| # | 开放问题 | 现在怎么办 |
  64 +|---|---|---|
  65 +| 1 | **图片内容审核供应商**(需求文档 5 节提到"需要图片内容审核能力(供应商待定)",但第 8 节居然没有专门列这一条——本文档补上这个缺口) | T26 先接一个 `moderateGarmentImage()` 可插拔函数,未配置真实供应商时走最基础的规则版兜底(文件类型/尺寸已在 T2 做过,这里只做"占位通过"),同 T20 `sendVerificationEmail()` 的套路 |
  66 +| 2 | **视觉模型供应商**(需求文档 §8 第 8 条,给用户上传的衣服图自动生成 `garmentDescriptor`,因为模式二没有管理员手写这一步) | T25 同样做成可插拔的 `describeGarmentImage()`,未配置真实供应商时退化成一句机械占位描述(如"用户上传的服装"),生成流程照常走,只是效果不如手写描述精细——记在验收标准里,不当阻塞项 |
  67 +| 3 | **积分具体数值**(需求文档 §8 第 5 条:充值档位、每次传衣服扣多少积分) | `pricing_config` 里先填占位数值(如 `spend_garment: 10` 积分),字段/机制先打好,数值上线前再改,MVCP M.12 原话就是这个意思 |
  68 +| 4 | **Stripe 真实账号的 API key**(`STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET`) | 我这边没有你的 Stripe 账号,没法自己申请测试模式 key。T24 的代码可以先写完,但**真正跑通一次真实 Checkout 需要你提供测试模式的 key** 放进 `.env`,这条我做不到自己解决,标在这里等你 |
  69 +
  70 +---
  71 +
  72 +## 2. 依赖关系
  73 +
  74 +```mermaid
  75 +flowchart TD
  76 + T23[T23 credit_ledger + pricing_config] --> T24[T24 Stripe 充值]
  77 + T23 --> T27[T27 模式二后端]
  78 + T25[T25 衣服图片自动打标] --> T27
  79 + T26[T26 衣服图片内容审核] --> T27
  80 + T27 --> T28[T28 传衣服前端接线]
  81 + T24 --> T29[T29 积分余额/充值 UI]
  82 + T23 --> T30[T30 admin 积分调整 + admin 鉴权迁移]
  83 +
  84 + classDef ready fill:#d4edda,stroke:#28a745
  85 + class T23,T25,T26,T30 ready
  86 +```
  87 +
  88 +T23/T25/T26/T30 互不依赖,可以并行开工;T27 是模式二的核心,要等
  89 +T23(能扣积分)、T25(能打标)、T26(能审核)都好了才能真正端到端跑通
  90 +(三者的桩/占位实现都能先联调,不必等真实供应商);T28 是前端接线,
  91 +需要 T27 的接口稳定;T29 只依赖 T24。
  92 +
  93 +---
  94 +
  95 +## 3. 任务表
  96 +
  97 +| # | 任务 | 依赖 | 验收标准 | 工时 |
  98 +|---|---|---|---|---|
  99 +| T23 | **`credit_ledger` + `pricing_config` 落地**:`db.js` 加 `credit_ledger` 表(M.2)和一对事务性的 `spend`/`refund`/`topup` 函数(扣减/退还/充值时同步更新 `users.creditBalance`,`balanceAfter` 写入时算好,整个操作包一个 SQLite 事务);新建 `pricing.json`(M.12,走 `store.js`),填 `spend_garment` 等占位扣费额 | T18(已完成,`users`/`sessions` 表已建) | 并发发起两次扣积分请求,`creditBalance` 不会因为读-改-写竞态而算错(写个并发测试);余额不够时 `spend` 拒绝而不是扣成负数;`refund` 能把一笔 `spend_garment` 的流水完整冲正,`balanceAfter` 连续可算 | 1.5 |
  100 +| T24 | **Stripe 充值**:`POST /api/credits/checkout` 创建 Stripe Checkout Session(金额/档位读 `pricing_config` 的 `topup_tier` 系列),`POST /api/credits/webhook` 接 Stripe 的支付成功回调,验证签名后写一笔 `credit_ledger(reason=topup, paymentRef=<Stripe 流水号>)` 并加余额(复用 T23 的 `topup` 事务函数) | T23 | webhook 签名校验失败的请求直接拒绝(不能任何人 POST 一个假的"支付成功"就加积分);同一个 Stripe 事件 ID 重复投递不会重复加两次积分(Stripe 官方保证 at-least-once,端点必须自己去重);**真实验证需要你提供 Stripe 测试模式 API key**,见 §1.2 第 4 条 | 1.5 |
  101 +| T25 | **衣服图片自动打标**:`describeGarmentImage(imagePath)` 可插拔函数(同 T20 `sendVerificationEmail` 的接口设计思路),未配置视觉模型供应商时返回一句机械占位描述,供应商定了直接换实现,调用方(T27)不用改 | 无 | 传一张图能拿到非空的 `garmentDescriptor` 字符串(哪怕是占位文案),拼进 T7 的 Stage2 prompt 模板不会报错/漏字段 | 1 |
  102 +| T26 | **衣服图片内容审核**:`moderateGarmentImage(imagePath)` 可插拔函数,未配置真实审核供应商时走最基础的规则版(复用 T2 已有的文件类型/大小校验,额外加一步占位"通过"),返回 `approved`/`rejected` 落一条 `moderation_log`(M.9) | 无 | 每次调用都在 `moderation_log` 落一条记录(`assetType: garment_upload`, `verdict`, `reviewedBy: "system"`);未配置供应商时默认放行,不阻塞模式二的其余部分先跑通 | 1 |
  103 +| T27 | **模式二后端**:`POST /api/dance/upload-garment`(需登录 + `emailVerified`,M.10 鉴权规则)——图片走 T2 的上传+EXIF清理 → T26 审核 → 不通过直接 400 且不扣积分 → 通过则 T23 扣积分(失败则整单回滚,不留半扣的积分)→ T25 自动打标 → `garments.create({source:"user_paid", ownerUserId, moderationStatus, appearanceDescriptor})` → 复用 T8/T9 现成的 `submitGeneration({characterId: 默认人物, garmentId})` 排入生成;生成失败要退还已扣积分(`refund_stage2_fail`,需求文档 3 节"共同后置流程"的底线) | T23, T25, T26 | 提交一张图能走完整条链路,产出一条视频且带 `characterId`/`garmentId` 溯源;故意让生成失败(mock H3 报错)能验证到积分被退回、流水里有 `refund_stage2_fail` 记录;审核不通过的图片不创建 `garments` 记录、不扣分;`fal.ai` 路径:图片先转发到 fal 可达的存储(同 T2 `forwardToH3Assets` 的思路,写一个 `forwardToFalAssets`,解决需求文档 §8 第 10 条"fal 够不着我们内网 uploads/"的问题)再提交生成 | 1.5 |
  104 +| T28 | **`dance.html`"传衣服" tab 接线**(T13 当前占位"开发中敬请期待"):未登录/邮箱未验证先引导去登录(复用 T22 的登录模态框);选图 → 展示 `pricing_config` 里当前扣费额 → 二次确认 → 调 T27 → "生成中"状态展示(复用衣柜 tab 现有的排队 UI 思路,不重新设计) | T27 | 未登录点"传衣服"会先弹登录,不会静默失败;确认框里显示的扣费额跟 `pricing_config` 一致,不是前端写死的数字;提交后能看到生成中状态,完成后自动进播放队列 | 1.5 |
  105 +| T29 | **积分余额 + 充值入口 UI**:登录后 T22 的账号按钮旁/下拉里展示 `creditBalance`;点"充值"跳转 T24 的 Checkout;充值成功跳回后余额刷新(轮询 `/api/auth/me` 或专门加一个 `/api/credits/balance`) | T24 | 充值成功后不用手动刷新页面就能看到余额变化;余额显示跟 `credit_ledger` 汇总值一致 | 0.5 |
  106 +| T30 | **管理员积分调整 + admin 鉴权迁移**:`POST /api/admin/credit/adjust {userId, delta, reason}`(M.2 `admin_adjust`,写流水+改余额,一样走 T23 的事务函数);顺带把 `/api/admin/*` 的鉴权从 T6 的单管理员口令切到 `users.role==='admin'` 的 `user_session`——**T6 的 `ADMIN_PASSWORD` 环境变量登录保留作为后备通道**(不删,避免 `role='admin'` 账号还没手动建出来之前把自己锁在后台外面),两条鉴权路径任一通过即可 | T23 | 现有管理员账号能通过 `ADMIN_PASSWORD` 照常登录(后备通道没坏);给某个 `users` 行手动置 `role='admin'` 后,用那个账号的 `user_session` 也能访问 `/api/admin/*`;`credit/adjust` 调整后 `moderation_log`/`credit_ledger` 都能查到是哪个管理员做的 | 1 |
  107 +
  108 +**总工时约 8 人天**。T23/T25/T26/T30 可以完全并行(约 4.5 人天),关键
  109 +路径是 T23→T27→T28,约 4 人天。
  110 +
  111 +---
  112 +
  113 +## 4. 第四期出口
  114 +
  115 +- 免费模式一(衣柜选衣服)行为不受任何影响;
  116 +- 已登录 + 邮箱已验证的用户能走通一次真实 Stripe 充值(测试模式),
  117 + 余额正确入账、页面上能看到;
  118 +- 上传一张衣服照片能走完整条链路:内容审核 → 扣积分 → 自动打标 →
  119 + 生成 → 排入播放队列 → 播放,且落库时带 `source: user_paid`/
  120 + `ownerUserId` 溯源;
  121 +- 审核通过的衣服默认沉淀进公共衣柜库,免费用户在模式一也能选到它;
  122 +- 生成失败或审核不通过,已扣积分(如有)必须退还,流水可查;
  123 +- 管理员能通过 `POST /api/admin/credit/adjust` 手工调整用户积分,且
  124 + 这类操作走 `users.role='admin'` 鉴权,不再是"谁知道那个共享口令谁
  125 + 就能做任何事";
  126 +- 全程只接 Stripe 一个支付渠道,不做多渠道。
  127 +
  128 +达成后才进第五期(模式三付费传人物 + 人脸黑名单 + 排播优先级改造 +
  129 +fal.ai 剩余用途 + "我的作品"页面)。
  130 +
  131 +---
  132 +
  133 +## 5. 明确不在第四期范围
  134 +
  135 +避免执行时范围蔓延:
  136 +
  137 +- 模式三(付费传人物)的任何后端/前端部分,及其"插队播放不参投票"的
  138 + 排播优先级改造(第五期,需要动 `ensureSchedule` 选片算法)
  139 +- 人脸识别黑名单审核(第五期)
  140 +- `blacklist`(M.11)运营手工拉黑管理页面(第五期;本期的图片审核走
  141 + `moderation_log` 记录结果,不做"管理员随手拉黑"这套 CRUD)
  142 +- "我的作品"个人中心页面(第五期,需求文档 7.4 节)
  143 +- Stripe 之外的支付渠道(支付宝/微信支付等,需要时再加)
  144 +- 人物的"剩余可播 10 秒片段数"配额机制(`characters.totalSegments`/
  145 + `remainingSegments`,MVCP M.3)——这是模式三专用的字段,第五期跟
  146 + 模式三一起做
... ...