dev-plan-4.md 13.1 KB

换装跳舞直播间 — 第四期开发任务规划

需求见 docs/todo/dressup-dance-room.md(下称"需求文档")第 3.2 节模式二、 第 5 节内容审核、第 7 节积分与充值、第 9 节第 4 条;设计见 docs/todo/dressup-dance-room-mvcp.md(下称"MVCP")M.2/M.4/M.9/M.12; 架构见 docs/structure/architecture.md(下称"架构文档")1.4/2.3 节。第一 (T1-T10)、第二(T11-T17)、第三期(T18-T22)均已完成并提交。任务编号 接着往下编。

工时是粗估,单位人天,用来排先后不用来考核。


0. 第四期范围(需求文档 9 节第 4 条)

做:积分体系(余额/流水/配置化扣费)+ Stripe 充值接入,上线免费 换衣服(已有)+ 付费传衣服(模式二),验证"扣费→生成→失败退还→ 审核通过后沉淀进公共库"这条完整链路。

明确不做(留到第五期,需求文档 9 节第 5 条):

  • 模式三(付费传人物)——包括它的核心业务规则"付费新人物插队播放, 不参与投票"(架构文档 2.6 节 priority: paid_boost),跟模式二完全 不同,本期不碰排播优先级逻辑。模式二内容仍然走 v1 现有投票机制 排序,需求文档 3 节"共同后置流程"里写得很明确:只有模式三跳过 投票,模式二不跳。
  • 人脸识别黑名单审核(模式三专用,需求文档 5 节)。
  • blacklist(MVCP M.11)完整的运营手工拉黑管理页面——本期只做 moderation_log(M.9)记录自动审核结果,不做"管理员随手拉黑一个 用户/资产"这套后台 CRUD。
  • "我的作品"个人中心页面(需求文档 7.4 节)。
  • 支付宝/微信支付等 Stripe 之外的渠道(7.3 节已说"需要时再加一个入口 即可",本期不预先做)。

1. 已确认的技术决策 + 需要你决定的开放问题

1.1 已确认

  • credit_ledger 落 SQLite,跟 users/sessions 同一个 data.sqlite (复用 T18 的 db.js)——架构文档 1.4 节点名了"扣积分 + 创建生成 任务 + 扣减片段配额"这几步操作需要原子性,JSON 全量重写文件在并发下 会丢更新,这正是当初选 SQLite 的另一半理由(T18 只先用上了"密码存储" 那一半)。node:sqlite 的 DatabaseSync 支持事务 (db.exec("BEGIN")...COMMIT/ROLLBACK),扣积分和退还都要包在事务里。
  • pricing_config(M.12)走 JSON 文件,不进 SQLite——它是运营偶尔 手改的配置,不在"扣积分"这条热路径的原子性要求里,复用 store.js 的模式(类似 characters.json)比再建一张表更省事,跟"能用现成的 就别加新东西"的项目风格一致。
  • garments/characters 的 source/ownerUserId/moderationStatus 字段第一期就已经建好(T1,见 MVCP M.3/M.4),本期不需要动 schema,只是第一次真正用上 user_paid/pending 这些取值。
  • 文档发现一处不一致,按需求文档为准:架构文档 2.2 节的流程图把 fal.ai 接入标成"第五期",但需求文档 §8 第 10 条明确写"到第四期 模式二付费实时路径上线时再解决",且本文档 §9 第 4 条本来就是"上线 …付费传衣服(模式二)"。按需求文档更新、更具体的说法为准:fal.ai 接入是本期(第四期)的任务,架构文档那张图的标注是旧的,没跟上 后来的分期方案,此处不改架构文档原文,只在这里记一笔。

1.2 需要你决定,但不阻塞开工(每条都设计成可插拔接口,供应商定了随时接进来)

# 开放问题 现在怎么办
1 图片内容审核供应商(需求文档 5 节提到"需要图片内容审核能力(供应商待定)",但第 8 节居然没有专门列这一条——本文档补上这个缺口) T26 先接一个 moderateGarmentImage() 可插拔函数,未配置真实供应商时走最基础的规则版兜底(文件类型/尺寸已在 T2 做过,这里只做"占位通过"),同 T20 sendVerificationEmail() 的套路
2 视觉模型供应商(需求文档 §8 第 8 条,给用户上传的衣服图自动生成 garmentDescriptor,因为模式二没有管理员手写这一步) T25 同样做成可插拔的 describeGarmentImage(),未配置真实供应商时退化成一句机械占位描述(如"用户上传的服装"),生成流程照常走,只是效果不如手写描述精细——记在验收标准里,不当阻塞项
3 积分具体数值(需求文档 §8 第 5 条:充值档位、每次传衣服扣多少积分) pricing_config 里先填占位数值(如 spend_garment: 10 积分),字段/机制先打好,数值上线前再改,MVCP M.12 原话就是这个意思
4 Stripe 真实账号的 API key(STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET) 我这边没有你的 Stripe 账号,没法自己申请测试模式 key。T24 的代码可以先写完,但真正跑通一次真实 Checkout 需要你提供测试模式的 key 放进 .env,这条我做不到自己解决,标在这里等你

2. 依赖关系

flowchart TD
  T23[T23 credit_ledger + pricing_config] --> T24[T24 Stripe 充值]
  T23 --> T27[T27 模式二后端]
  T25[T25 衣服图片自动打标] --> T27
  T26[T26 衣服图片内容审核] --> T27
  T27 --> T28[T28 传衣服前端接线]
  T24 --> T29[T29 积分余额/充值 UI]
  T23 --> T30[T30 admin 积分调整 + admin 鉴权迁移]

  classDef ready fill:#d4edda,stroke:#28a745
  class T23,T25,T26,T30 ready

T23/T25/T26/T30 互不依赖,可以并行开工;T27 是模式二的核心,要等 T23(能扣积分)、T25(能打标)、T26(能审核)都好了才能真正端到端跑通 (三者的桩/占位实现都能先联调,不必等真实供应商);T28 是前端接线, 需要 T27 的接口稳定;T29 只依赖 T24。


3. 任务表

# 任务 依赖 验收标准 工时
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
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
T25 衣服图片自动打标:describeGarmentImage(imagePath) 可插拔函数(同 T20 sendVerificationEmail 的接口设计思路),未配置视觉模型供应商时返回一句机械占位描述,供应商定了直接换实现,调用方(T27)不用改 无 传一张图能拿到非空的 garmentDescriptor 字符串(哪怕是占位文案),拼进 T7 的 Stage2 prompt 模板不会报错/漏字段 1
T26 衣服图片内容审核:moderateGarmentImage(imagePath) 可插拔函数,未配置真实审核供应商时走最基础的规则版(复用 T2 已有的文件类型/大小校验,额外加一步占位"通过"),返回 approved/rejected 落一条 moderation_log(M.9) 无 每次调用都在 moderation_log 落一条记录(assetType: garment_upload, verdict, reviewedBy: "system");未配置供应商时默认放行,不阻塞模式二的其余部分先跑通 1
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
T28 dance.html"传衣服" tab 接线(T13 当前占位"开发中敬请期待"):未登录/邮箱未验证先引导去登录(复用 T22 的登录模态框);选图 → 展示 pricing_config 里当前扣费额 → 二次确认 → 调 T27 → "生成中"状态展示(复用衣柜 tab 现有的排队 UI 思路,不重新设计) T27 未登录点"传衣服"会先弹登录,不会静默失败;确认框里显示的扣费额跟 pricing_config 一致,不是前端写死的数字;提交后能看到生成中状态,完成后自动进播放队列 1.5
T29 积分余额 + 充值入口 UI:登录后 T22 的账号按钮旁/下拉里展示 creditBalance;点"充值"跳转 T24 的 Checkout;充值成功跳回后余额刷新(轮询 /api/auth/me 或专门加一个 /api/credits/balance) T24 充值成功后不用手动刷新页面就能看到余额变化;余额显示跟 credit_ledger 汇总值一致 0.5
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

总工时约 8 人天。T23/T25/T26/T30 可以完全并行(约 4.5 人天),关键 路径是 T23→T27→T28,约 4 人天。


4. 第四期出口

  • 免费模式一(衣柜选衣服)行为不受任何影响;
  • 已登录 + 邮箱已验证的用户能走通一次真实 Stripe 充值(测试模式), 余额正确入账、页面上能看到;
  • 上传一张衣服照片能走完整条链路:内容审核 → 扣积分 → 自动打标 → 生成 → 排入播放队列 → 播放,且落库时带 source: user_paid/ ownerUserId 溯源;
  • 审核通过的衣服默认沉淀进公共衣柜库,免费用户在模式一也能选到它;
  • 生成失败或审核不通过,已扣积分(如有)必须退还,流水可查;
  • 管理员能通过 POST /api/admin/credit/adjust 手工调整用户积分,且 这类操作走 users.role='admin' 鉴权,不再是"谁知道那个共享口令谁 就能做任何事";
  • 全程只接 Stripe 一个支付渠道,不做多渠道。

达成后才进第五期(模式三付费传人物 + 人脸黑名单 + 排播优先级改造 + fal.ai 剩余用途 + "我的作品"页面)。


5. 明确不在第四期范围

避免执行时范围蔓延:

  • 模式三(付费传人物)的任何后端/前端部分,及其"插队播放不参投票"的 排播优先级改造(第五期,需要动 ensureSchedule 选片算法)
  • 人脸识别黑名单审核(第五期)
  • blacklist(M.11)运营手工拉黑管理页面(第五期;本期的图片审核走 moderation_log 记录结果,不做"管理员随手拉黑"这套 CRUD)
  • "我的作品"个人中心页面(第五期,需求文档 7.4 节)
  • Stripe 之外的支付渠道(支付宝/微信支付等,需要时再加)
  • 人物的"剩余可播 10 秒片段数"配额机制(characters.totalSegments/ remainingSegments,MVCP M.3)——这是模式三专用的字段,第五期跟 模式三一起做