Commit a446205432ea567b0b6cc916c6c4799fcea1348f

Authored by yibo
1 parent c638c96c

Add MVCP technical design doc for the dressup dance room

Data model, front/back-end view inventory, button-level control design, and
per-user-type process flows for the not-yet-built feature. Also corrects the
requirements doc's stale "live.html stays landscape" claim now that
H3_GEN_PARAMS was changed to portrait there too (commit 1dc7084).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdtXTsDRz5QWMjvt1HBXLz
docs/todo/dressup-dance-room-mvcp.md 0 → 100644
  1 +# 换装跳舞直播间 — 技术设计文档(Model / View / Control / Process)
  2 +
  3 +依据:产品需求 `docs/todo/dressup-dance-room.md`(下称"需求文档",本文各处
  4 +引用其章节号);系统架构 `docs/structure/architecture.md`(下称"架构文档")。
  5 +本文档把已确认的产品决策落到:数据库表结构(M)、前后端界面清单(V)、
  6 +按钮/交互动作(C)、各类用户的操作流程(P),供开发排期和实现参考。
  7 +
  8 +约定:字段名延续现有 `library.json`/`live-state.json` 的英文小驼峰风格;
  9 +表结构在第一期仍以 JSON 文件承载,第三期(用户账号)后迁移 SQLite(见
  10 +架构文档 1.4 节)——本文按"关系表"描述,两种落地方式都能直接映射。
  11 +
  12 +---
  13 +
  14 +## M — Model:数据结构设计
  15 +
  16 +### M.1 `users`
  17 +
  18 +| 字段 | 类型 | 说明 |
  19 +|---|---|---|
  20 +| `id` | string (uuid) | 主键 |
  21 +| `email` | string | 邮箱,唯一(7.1 节,第一期登录方式) |
  22 +| `passwordHash` | string | bcrypt/argon2,邮箱登录用 |
  23 +| `oauthProviders` | array | 预留 `["google","apple","github"]`,第一期为空数组 |
  24 +| `nickname` | string | 展示昵称,迁移自现有 cookie `viewer_nickname` |
  25 +| `legacyUid` | string \| null | 绑定的旧 `viewer_uid`(cookie),用于历史投票/点播记录迁移,见 P.4 |
  26 +| `ageConfirmed` | boolean | "我已满18岁" 勾选记录(5.1 节) |
  27 +| `ageConfirmedAt` | number (ts) | |
  28 +| `creditBalance` | number | 当前积分余额(冗余字段,权威值仍是 `credit_ledger` 流水汇总,见 M.2) |
  29 +| `createdAt` | number (ts) | |
  30 +
  31 +### M.2 `credit_ledger`(积分流水,7.2 节)
  32 +
  33 +| 字段 | 类型 | 说明 |
  34 +|---|---|---|
  35 +| `id` | string | 主键 |
  36 +| `userId` | string | 外键 → `users.id` |
  37 +| `delta` | number | 正数=充值/退款,负数=消费 |
  38 +| `reason` | enum | `topup` \| `spend_garment` \| `spend_character` \| `refund_stage1_fail` \| `refund_stage2_fail` \| `refund_moderation_reject` \| `admin_adjust` |
  39 +| `relatedAssetId` | string \| null | 关联的 `garments.id` 或 `characters.id`(消费/退款时) |
  40 +| `balanceAfter` | number | 变动后余额,写入时计算好,避免每次汇总流水 |
  41 +| `paymentRef` | string \| null | 充值时的外部支付流水号(Stripe/支付宝等,7.3 节) |
  42 +| `createdAt` | number (ts) | |
  43 +
  44 +### M.3 `characters`(人物库)
  45 +
  46 +| 字段 | 类型 | 说明 |
  47 +|---|---|---|
  48 +| `id` | string | 主键 |
  49 +| `name` | string | |
  50 +| `imageUrl` | string | 人物参考图 |
  51 +| `tags` | array | |
  52 +| `source` | enum | `admin_seed` \| `user_paid`(6.1 节) |
  53 +| `ownerUserId` | string \| null | `user_paid` 来源必填 |
  54 +| `isDefault` | boolean | 是否为直播默认人物;第一期**全库最多一条为 true**(6.1 节已确认,不做多人物轮换) |
  55 +| `moderationStatus` | enum | `auto_passed` \| `blacklisted` \| `manually_removed`(5 节:自动黑名单过滤,无强制人工预审) |
  56 +| `totalSegments` | number \| null | 付费购买的 10 秒片段总配额,`admin_seed`/默认人物为 null(不限量,只受 4.6 节去重缓存影响的是复用次数,不是配额) |
  57 +| `remainingSegments` | number \| null | 剩余可播片段数,每播放一段 -1(7.2 节;架构文档 2.7 节) |
  58 +| `status` | enum | `active` \| `expired`(配额耗尽后置 `expired`,记录不删除,7.4 节/架构文档 2.7 节) |
  59 +| `createdAt` | number (ts) | |
  60 +
  61 +### M.4 `garments`(衣服库)
  62 +
  63 +| 字段 | 类型 | 说明 |
  64 +|---|---|---|
  65 +| `id` | string | 主键 |
  66 +| `name` | string | |
  67 +| `category` | string | 分类标签(古风/泳装/校服…) |
  68 +| `thumbnailUrl` | string | |
  69 +| `imageUrl` | string | 原图 |
  70 +| `source` | enum | `admin_seed` \| `user_paid`(6.2 节) |
  71 +| `ownerUserId` | string \| null | |
  72 +| `moderationStatus` | enum | `pending` \| `approved` \| `rejected`(模式二走轻量图片审核,审核通过默认沉淀进公共库,3 节模式二第 3 点) |
  73 +| `isActive` | boolean | 上下架开关 |
  74 +| `createdAt` | number (ts) | |
  75 +
  76 +### M.5 `dance_refs`(参考跳舞视频/音乐库)
  77 +
  78 +| 字段 | 类型 | 说明 |
  79 +|---|---|---|
  80 +| `id` | string | 主键 |
  81 +| `name` | string | |
  82 +| `videoUrl` | string | |
  83 +| `styleTags` | array | |
  84 +| `durationSec` | number | |
  85 +| `copyrightSource` | enum | `test_placeholder` \| `self_made` \| `licensed`(第一期全部 `test_placeholder`,6.3/8 节) |
  86 +| `isActive` | boolean | |
  87 +| `createdAt` | number (ts) | |
  88 +
  89 +### M.6 `composed_images`(Stage1 换装图产物)
  90 +
  91 +| 字段 | 类型 | 说明 |
  92 +|---|---|---|
  93 +| `id` | string | 主键 |
  94 +| `characterId` | string | 外键 |
  95 +| `garmentId` | string | 外键 |
  96 +| `imageUrl` | string | 合成结果 |
  97 +| `model` | string | 实际调用的 fal.ai 模型标识(4.1/8 节,待选型) |
  98 +| `status` | enum | `generating` \| `done` \| `failed` |
  99 +| `createdAt` | number (ts) | |
  100 +
  101 +**唯一索引 `(characterId, garmentId)`**——这是 4.6 节去重缓存的落地位置:
  102 +提交前先按这个组合查是否已有 `status: done` 的记录,命中直接复用,不
  103 +新建。
  104 +
  105 +### M.7 `generated_videos`(Stage2 产物,`library.json` 风格 clip 的超集)
  106 +
  107 +在现有 `library.json` clip 字段基础上(见 `docs/done` 第 3 节)新增:
  108 +
  109 +| 新增字段 | 类型 | 说明 |
  110 +|---|---|---|
  111 +| `characterId` | string | |
  112 +| `garmentId` | string | |
  113 +| `composedImageId` | string | 外键 → M.6,点开能看 Stage1 合成图(6.4 节) |
  114 +| `refDanceVideoId` | string | 外键 → M.5 |
  115 +| `channel` | enum | `live` \| `dance`(区分是 `live.html` 还是 `dance.html` 的产物,两条独立时刻表共用同一张表时靠这个字段区分,见架构文档 2.1 节) |
  116 +| `backend` | enum | `h3_console` \| `fal`(4.3 节双算力策略,记录这条具体是哪条算力路径生成的,便于追溯成本) |
  117 +| `priority` | enum | `normal` \| `paid_boost`(`paid_boost` = 付费新人物内容,排播时走 2.6 节"插队"通道,不参与投票) |
  118 +| `requestedByUserId` | string \| null | 付费请求必填,免费匿名请求为 null(用 `legacyUid` 兜底) |
  119 +| `width`/`height` | number | `dance` 频道默认 480×832,`live` 频道现状默认已同步改为 480×832(见文末"勘误"一节) |
  120 +
  121 +### M.8 直播时刻表状态(非持久化数据库表,`live-state.json` 风格的内存+落盘结构,`dance.html` 独立一份)
  122 +
  123 +```jsonc
  124 +{
  125 + "channel": "dance",
  126 + "schedule": [{ "videoId": "...", "startAt": 0, "endAt": 0, "requestedBy": null, "priority": "normal" }],
  127 + "priorityInbox": [ /* paid_boost 产物就绪后先进这里,ensureSchedule 优先消费,见架构文档 2.6 节 */ ],
  128 + "freeQueue": [ /* 免费模式一提交,走投票排序,结构同 v1 promptQueue */ ]
  129 +}
  130 +```
  131 +
  132 +### M.9 `moderation_log`
  133 +
  134 +| 字段 | 类型 | 说明 |
  135 +|---|---|---|
  136 +| `id` | string | 主键 |
  137 +| `assetType` | enum | `garment_upload` \| `character_upload` |
  138 +| `assetId` | string | |
  139 +| `verdict` | enum | `auto_pass` \| `auto_reject_blacklist` \| `admin_takedown`(5 节:自动过 + 事后人工兜底) |
  140 +| `reason` | string \| null | |
  141 +| `reviewedBy` | string | `system` 或管理员 `userId` |
  142 +| `createdAt` | number (ts) | |
  143 +
  144 +### M.10 ER 关系图
  145 +
  146 +```mermaid
  147 +erDiagram
  148 + users ||--o{ credit_ledger : "流水"
  149 + users ||--o{ characters : "user_paid 拥有者"
  150 + users ||--o{ garments : "user_paid 拥有者"
  151 + users ||--o{ generated_videos : "requestedBy"
  152 + characters ||--o{ composed_images : "Stage1 输入"
  153 + garments ||--o{ composed_images : "Stage1 输入"
  154 + composed_images ||--o{ generated_videos : "Stage2 输入"
  155 + dance_refs ||--o{ generated_videos : "Stage2 输入"
  156 + characters ||--o{ moderation_log : "审核记录"
  157 + garments ||--o{ moderation_log : "审核记录"
  158 +```
  159 +
  160 +---
  161 +
  162 +## V — View:前后端界面设计
  163 +
  164 +### V.1 观众端页面
  165 +
  166 +| 页面 | 路径(建议) | 说明 |
  167 +|---|---|---|
  168 +| 换装跳舞直播间 | `dance.html` | 主页面,见 V.1.1 |
  169 +| 登录/注册 | 弹窗(不单独开页,跟 v1 昵称弹窗交互一致,见架构文档/`docs/done` 2.5 节) | 邮箱输入 + 验证码/密码,首次触发付费操作时弹出 |
  170 +| 我的作品 | `my-works.html` 或 `dance.html?tab=mine`(建议做成 `dance.html` 内的一个面板而不是独立页面,减少一个页面壳) | 7.4 节 |
  171 +| 充值 | 弹窗,from 积分不足拦截 或 顶部"我的积分"入口 | 7.3 节,具体支付渠道待定 |
  172 +
  173 +#### V.1.1 `dance.html` 区域划分(复用 `live.html` 视觉语言,见需求文档 2 节)
  174 +
  175 +```
  176 +┌─────────────────────────────────────────────┐
  177 +│ 顶部条:Logo | 在线人数 | 我的积分 | 头像/登录 │
  178 +├───────────┬───────────────────┬─────────────┤
  179 +│ │ │ │
  180 +│ 左侧栏 │ 播放器(9:16) │ 右侧栏 │
  181 +│ 衣柜/传衣 │ + 当前播放信息 │ 在线互动展示 │
  182 +│ /传人 tab │ (复用 live.html│ (点赞/排队 │
  183 +│ │ 的 now-playing)│ 列表,无输 │
  184 +│ │ │ 入框) │
  185 +│ │ │ │
  186 +├───────────┴───────────────────┴─────────────┤
  187 +│ 移动端:底部 tab 切换(衣柜/传衣/传人/我的) │
  188 +└─────────────────────────────────────────────┘
  189 +```
  190 +
  191 +- 播放器容器按 9:16 设计(4.4 节),不是 `live.html` 现有横向容器的简单
  192 + 拉伸。
  193 +- 左侧栏三个 tab:**衣柜**(默认)/ **传衣服** / **传人**,对应需求文档
  194 + 3 节三种模式。
  195 +- 首次进入弹年龄确认(5.1 节),非模态强制,一次性记忆(cookie/账号)。
  196 +
  197 +### V.2 后台管理页面
  198 +
  199 +在现有 `admin.html`(`docs/done` 2.8 节)基础上新增,交互风格保持一致
  200 +(表格 + 行内编辑 + 新增表单,见需求文档 6 节开篇):
  201 +
  202 +| 页面 | 对应资源 | 需求文档章节 |
  203 +|---|---|---|
  204 +| `admin-characters.html` | M.3 | 6.1 |
  205 +| `admin-garments.html` | M.4 | 6.2 |
  206 +| `admin-dance-refs.html` | M.5 | 6.3 |
  207 +| `admin.html`(扩展现有表格新增列) | M.7 | 6.4 |
  208 +| `admin-playback.html` | M.8(dance 频道排播干预) | 6.5 |
  209 +| `admin-users.html` | M.1 + M.2 | 用户/积分管理,7 节 |
  210 +
  211 +后台导航建议在现有 `admin.html` 顶部加一排二级导航链到这些新页面,
  212 +不新起一套独立的后台框架。
  213 +
  214 +### V.3 后端 API 清单
  215 +
  216 +延续 `server.js` 手写路由风格(无框架),按资源分组:
  217 +
  218 +```
  219 +观众端:
  220 +GET /api/dance/library?sort= # dance 频道浏览三 tab,等价 v1 /api/library
  221 +GET /api/dance/live/state?cid=&uid= # dance 频道播出状态,等价 v1 /api/live/state
  222 +GET /api/dance/wardrobe # 衣柜列表(garments,isActive=true)
  223 +POST /api/dance/pick # 模式一:{characterId?, garmentId} 免费提交
  224 +POST /api/dance/upload-garment # 模式二:multipart 图片 + 走审核+扣积分
  225 +POST /api/dance/upload-character # 模式三:multipart 图片 + 年龄确认+黑名单审核+积分档位(决定 remainingSegments)
  226 +POST /api/dance/queue/vote/:id # 免费队列投票,等价 v1 /api/queue/vote
  227 +GET /api/dance/my-works # 我的作品(需登录)
  228 +POST /api/auth/register # 邮箱注册
  229 +POST /api/auth/login
  230 +POST /api/credit/topup # 发起充值(对接支付网关,7.3 节)
  231 +POST /api/credit/topup/webhook # 支付回调
  232 +
  233 +后台:
  234 +CRUD /api/admin/characters[/:id]
  235 +CRUD /api/admin/garments[/:id]
  236 +CRUD /api/admin/dance-refs[/:id]
  237 +CRUD /api/admin/generated-videos[/:id] # 扩展现有 /api/admin/library
  238 +POST /api/admin/playback/insert # 插播/置顶
  239 +POST /api/admin/playback/skip # 跳过当前
  240 +POST /api/admin/playback/pause-fill # 暂停自动填充
  241 +POST /api/admin/blacklist # 拉黑用户/资产
  242 +GET /api/admin/moderation-log
  243 +GET /api/admin/users
  244 +POST /api/admin/credit/adjust # 人工调整积分(客服场景)
  245 +
  246 +内部(生成流水线编排,不对前端暴露):
  247 +generateComposedImage(characterId, garmentId) # Stage1,查缓存(M.6唯一索引)→未命中调fal.ai
  248 +generateDanceVideo(composedImageId, refDanceVideoId, backend) # Stage2,backend按4.3节路由
  249 +```
  250 +
  251 +---
  252 +
  253 +## C — Control:界面按钮/交互动作设计
  254 +
  255 +### C.1 `dance.html`
  256 +
  257 +| 按钮/交互 | 触发条件 | 动作 | 反馈 |
  258 +|---|---|---|---|
  259 +| 衣柜卡片点击 | 始终可点(免费,无需登录) | `POST /api/dance/pick {garmentId}` | 立即在"生成中"区出现一条;命中缓存(4.6节)秒级进"接下来播放" |
  260 +| "传衣服"上传按钮 | 需登录 | 若未登录先弹登录;选图 → 客户端基础校验(大小/格式)→ 上传 → 展示预计消耗积分 → 二次确认 | 提交后进图片审核队列;通过后自动排入生成,展示状态 |
  261 +| "传人"上传按钮 | 需登录 + 已完成年龄确认(5.1节) | 选图 → 选择/展示积分档位(决定 `remainingSegments`,7.2节)→ 可选搭配衣服(选衣柜或再传一张,不选则随机)→ 二次确认扣积分 | 提交后走黑名单自动审核(5节),通过直接生成,不阻塞用户等待 |
  262 +| 年龄确认弹窗"确认"按钮 | 首次进入 `dance.html` | 写 `ageConfirmed=true`(cookie 或账号,取决于是否已登录) | 弹窗关闭,露出主界面 |
  263 +| 点赞(❤️) | 浏览三 tab 内 | `POST /api/like/:id`(复用 v1) | 数字 +1 |
  264 +| 排队投票(▲) | 免费队列条目 | `POST /api/dance/queue/vote/:id`(复用 v1 投票逻辑,仅对 `priority: normal` 条目生效——`paid_boost` 条目不展示投票按钮,见需求文档 3 节"共同后置流程") | 高亮 + 票数更新 |
  265 +| "我的作品"入口 | 需登录 | 打开 V.1 的我的作品面板 | 列出该账号名下 `generated_videos`(含 `status: expired` 的人物产出) |
  266 +| "我的积分"/充值入口 | 始终可见(未登录显示"登录后查看") | 打开充值弹窗 | 对接支付网关(7.3节,渠道待定) |
  267 +| 静音/全屏 | 复用 `live.html` 现有 `muteBtn`/播放器交互 | 同 v1 | 同 v1 |
  268 +
  269 +### C.2 后台管理页面
  270 +
  271 +| 按钮/交互 | 所在页面 | 动作 |
  272 +|---|---|---|
  273 +| 新增/编辑/删除 人物、衣服、参考视频 | 各自 admin 页 | 对应 CRUD API(V.3),风格照抄现有 `admin.html` 行内编辑 |
  274 +| "设为直播默认人物" 开关 | `admin-characters.html` | 置该条 `isDefault=true`,**同时把其他所有条目置 false**(第一期全局唯一,6.1节) |
  275 +| 审核通过/驳回(`pending`→`approved`/`rejected`) | `admin-garments.html`(`user_paid` 来源条目) | 更新 `moderationStatus`,写 `moderation_log` |
  276 +| 人工下架人物/内容 | `admin-characters.html`/`admin-generated-videos` | 5节兜底手段,写 `moderation_log(verdict: admin_takedown)` |
  277 +| 插播/置顶下一条 | `admin-playback.html` | `POST /api/admin/playback/insert`,写入 M.8 `priorityInbox` |
  278 +| 跳过当前 | `admin-playback.html` | `POST /api/admin/playback/skip` |
  279 +| 暂停/恢复自动填充 | `admin-playback.html` | `POST /api/admin/playback/pause-fill`,暂停期间 `ensureSchedule` 只消费 `priorityInbox`/`freeQueue`,不再随机填充素材库 |
  280 +| 拉黑用户/资产 | `admin-playback.html` 或 `admin-users.html` | 写黑名单表,后续该用户/资产提交直接拒绝,不进流水线 |
  281 +| 人工调整积分 | `admin-users.html` | `POST /api/admin/credit/adjust`,写 `credit_ledger(reason: admin_adjust)` |
  282 +
  283 +---
  284 +
  285 +## P — Process:用户流程设计
  286 +
  287 +### P.1 匿名免费观众:选衣柜(对应需求文档 3 节模式一)
  288 +
  289 +```mermaid
  290 +sequenceDiagram
  291 + participant V as 观众(未登录)
  292 + participant S as server.js
  293 + participant Cache as composed_images 缓存(M.6)
  294 + participant Fal as fal.ai
  295 + participant H3 as H3 Console
  296 +
  297 + V->>S: 首次进入 dance.html
  298 + S-->>V: 弹年龄确认(5.1节)
  299 + V->>S: 确认已满18岁
  300 + V->>S: POST /api/dance/pick {garmentId}
  301 + S->>Cache: 查 (defaultCharacterId, garmentId)
  302 + alt 命中缓存
  303 + S-->>V: 直接排入队列(秒级)
  304 + else 未命中
  305 + S->>Fal: Stage1 换装合成
  306 + Fal-->>S: composedImageUrl
  307 + S->>H3: Stage2(冷启动算力,4.3节)
  308 + H3-->>S: 视频完成
  309 + S->>S: 写入 generated_videos + composed_images 缓存
  310 + S-->>V: 排入队列
  311 + end
  312 + Note over S: ensureSchedule 按 v1 投票逻辑排序(priority: normal)
  313 +```
  314 +
  315 +### P.2 注册观众:付费传衣服(模式二)
  316 +
  317 +1. 点"传衣服" → 未登录则先走 P.4 登录/注册。
  318 +2. 选图 → 前端展示"这将消耗 N 积分"(数值待定,8节)。
  319 +3. 确认 → 后端校验余额 → 不足则拦截,引导充值(P.4 充值流程)。
  320 +4. 余额充足 → 预扣积分(`credit_ledger` 写一条负数流水)→ 图片走轻量
  321 + 内容审核。
  322 +5. 审核不过 → 全额退还积分(写正数流水,`reason: refund_moderation_reject`)
  323 + → 前端提示。
  324 +6. 审核通过 → `garments` 新建一条 `moderationStatus: approved`,默认
  325 + `isActive: true`(沉淀进公共库)→ 走 Stage1/Stage2(可能命中已有
  326 + "默认人物+这件新衣服"缓存吗?不会命中,因为这件衣服刚创建,Stage1
  327 + 必然是新生成)→ 完成后排入队列,`priority: normal`(模式二不享受
  328 + 插队特权,只有模式三新人物才有,见需求文档 3 节)。
  329 +7. 生成失败(Stage1/Stage2)→ 退还积分,`garments` 该条标记不可用/删除
  330 + (避免衣柜库里出现一件"存在但没有可用换装产物"的死资产)。
  331 +
  332 +### P.3 注册观众:付费传人物(模式三,风险最高的流程)
  333 +
  334 +```mermaid
  335 +sequenceDiagram
  336 + participant V as 观众(已登录,已年龄确认)
  337 + participant S as server.js
  338 + participant Blk as 人脸黑名单模型
  339 + participant Fal as fal.ai
  340 + participant Credit as credit_ledger
  341 +
  342 + V->>S: 选积分档位(决定 remainingSegments)+ 上传人物照 + (可选)选/传衣服
  343 + S->>Credit: 预扣积分
  344 + S->>Blk: 自动人脸识别过滤(5节,非强制人工预审)
  345 + alt 命中黑名单
  346 + S->>Credit: 全额退还
  347 + S-->>V: 拒绝,提示原因
  348 + else 未命中,自动通过
  349 + S->>S: characters 新建一条 source:user_paid, remainingSegments=购买档位对应值
  350 + S->>Fal: Stage1 换装合成(若未指定衣服则随机挑一件衣柜库)
  351 + S->>Fal: Stage2(付费实时算力,4.3节,约4s级)
  352 + Fal-->>S: 视频完成
  353 + S->>S: generated_videos 新建,priority: paid_boost
  354 + S-->>V: 排入 priorityInbox,插队播放(不参与投票)
  355 + end
  356 + Note over S: 每播完一段,remainingSegments -= 1
  357 + Note over S: 配额耗尽 → characters.status = expired,从排播/复用池移除<br/>但记录保留,用户可在"我的作品"回看,可再充值续播
  358 +```
  359 +
  360 +### P.4 登录/注册 + 身份迁移
  361 +
  362 +1. 触发点:模式二/三的任一付费操作、或主动点"登录"。
  363 +2. 邮箱注册/登录(7.1节)成功后,检查是否存在 `viewer_uid`/
  364 + `viewer_nickname` cookie(v1 遗留身份)——若存在,把该 `uid` 写入
  365 + 新账号的 `legacyUid` 字段,历史投票/点播记录(挂在旧 `uid` 上的)
  366 + 通过 `legacyUid` 关联查询,不做数据搬迁,只做关联(架构文档 2.7
  367 + 节过渡方案)。
  368 +3. 后续 `uid` 仍然写在 cookie 里(免费投票等匿名场景继续用它),但
  369 + 付费操作一律要求 `userId`(登录态)。
  370 +
  371 +### P.5 充值
  372 +
  373 +1. 触发点:付费操作余额不足自动弹出,或主动点"我的积分"。
  374 +2. 选充值档位 → 跳转/弹出支付网关(7.3节,渠道待定,标注 Stripe 对
  375 + 国内支付方式支持有限的风险,见架构文档待验证表)。
  376 +3. 支付成功回调 `POST /api/credit/topup/webhook` → 写 `credit_ledger
  377 + (reason: topup, delta: +N, paymentRef)` → 更新 `users.creditBalance`。
  378 +4. 支付失败/取消 → 不产生流水,前端提示重试。
  379 +
  380 +### P.6 运营/管理员:素材维护 + 事后审核 + 播放干预
  381 +
  382 +- **日常巡查**(对接 5 节"事后人工兜底"):定期查看 `moderation_log`
  383 + 里 `verdict: auto_pass` 但被举报/人工发现有问题的人物内容 → 在
  384 + `admin-characters.html` 手工下架 → 写 `admin_takedown` 记录。
  385 +- **播放干预**:直播过程中运营通过 `admin-playback.html` 插播/跳过/
  386 + 暂停自动填充(6.5节),不需要走观众端的正常提交流程。
  387 +- **素材库维护**:定期给 `garments`/`dance_refs` 上下架,给
  388 + `admin_seed` 人物/衣服打标签分类。
  389 +
  390 +### P.7 系统自动化流程
  391 +
  392 +1. **冷启动批量生成**(第一期,9节):运营在后台批量选定
  393 + 人物×衣服组合,脚本/后台按钮触发批量 `generateComposedImage` +
  394 + `generateDanceVideo`(走内部 H3 Console,4.3节),铺满初始衣柜库/
  395 + `composed_images` 缓存,不涉及观众端。
  396 +2. **排播 tick**:`dance.html` 独立的 `ensureSchedule` 定时任务(同 v1
  397 + 每 2s,架构文档 1.3/2.6节),优先消费 `priorityInbox`(付费插队),
  398 + 其次按投票消费 `freeQueue`,都为空时随机填充素材库(同 v1 逻辑,
  399 + 避开冷却期)。
  400 +3. **人物到期检测**:每次消费完 `priorityInbox` 里某人物的一段后,
  401 + `remainingSegments -= 1`,归零则置 `characters.status = expired`
  402 + 并从后续排播候选中过滤掉(架构文档 2.7节)。
  403 +
  404 +---
  405 +
  406 +## 勘误:`live.html` 分辨率现状更新
  407 +
  408 +写这份需求文档时(`docs/todo/dressup-dance-room.md` 4.4节)记录的是
  409 +"`live.html` 现状横屏 832×480 不变,只有 `dance.html` 默认竖屏"。但
  410 +`server.js` 的 `H3_GEN_PARAMS` 在另一次改动中已经被改成
  411 +`{ width: 480, height: 832 }`(commit `1dc7084`,`live.html` 侧的
  412 +说明是"未来生成的新片段走竖屏,存量片段保留原比例,播放器
  413 +`object-fit: contain` 兼容混合比例")——也就是说 **`live.html` 现在
  414 +新生成的内容其实也已经是竖屏了**,只是存量片段还是横屏混着播。
  415 +
  416 +`dance.html` 侧的设计不受影响(本来就是独立的 `H3_GEN_PARAMS` 配置,
  417 +见架构文档 2.4节),但需求文档 2/4.4节"live.html 现状横屏不变"这句
  418 +描述已经不准确,建议后续同步一次勘误(不在本次技术设计文档范围内,
  419 +先在此记录,避免依赖这份文档的人被误导)。
... ...
docs/todo/dressup-dance-room.md
... ... @@ -26,8 +26,10 @@
26 26 - 两个页面各自维护独立的播出时刻表(各自的 `schedule`/`promptQueue`),
27 27 不是同一个直播间的两个 tab——它们是两条并行的产品线,共用后端的
28 28 素材库/生成流水线/账号体系等基础设施。
29   -- **`dance.html` 默认竖屏、移动端优先**(`live.html` 现状横屏不变,
30   - 已上线跑着,不在本次改造范围内)——具体规格见第 4.4 节。
  29 +- **`dance.html` 默认竖屏、移动端优先**——具体规格见第 4.4 节。
  30 + (勘误:`live.html` 一侧此后也已独立改成竖屏默认,见 4.4 节末尾说明,
  31 + 两个频道各自的 `H3_GEN_PARAMS` 配置互不影响,只是当时判断的"横屏不变"
  32 + 前提已经过时。)
31 33  
32 34 ## 3. 观众体验流程(已确认:使用已有资产库免费,新增资产付费)
33 35  
... ... @@ -124,6 +126,13 @@ Stage1(换装图合成)在两条路径下大概率都是 fal.ai(H3 Console
124 126 `live.html` 现有横屏播放器的宽高比,否则会出现大黑边——这是
125 127 `dance.html` 相对 `live.html` 除了"没有文字输入框"之外的第二个视觉
126 128 差异点。
  129 +- **勘误**:`server.js` 的 `H3_GEN_PARAMS`(`live.html` 使用的那份)
  130 + 之后被单独改成了 `480×832`(commit `1dc7084`),也就是说 `live.html`
  131 + 新生成的内容现在其实也是竖屏了,存量片段仍是原比例混播(播放器靠
  132 + `object-fit: contain` 兼容)。这是 `live.html` 那条独立产品线自己的
  133 + 改动,跟 `dance.html` 的配置互不影响,但上面"仅 `dance.html`"的表述
  134 + 已不完全准确,记录于此,详见 `docs/todo/dressup-dance-room-mvcp.md`
  135 + 文末"勘误"一节。
127 136  
128 137 ### 4.5 失败/超时处理
129 138  
... ...