# 换装跳舞直播间 — 第二期开发任务规划 需求见 `docs/todo/dressup-dance-room.md`(下称"需求文档")第 3 节模式一、 第 5.1 节年龄确认;设计见 `docs/todo/dressup-dance-room-mvcp.md`(下称 "MVCP")V.1.1/M.8/P.1;架构见 `docs/structure/architecture.md`(下称 "架构文档")2.1/2.5/2.6 节。第一期(后台三库 + 冷启动批量生成)任务表 见 `docs/todo/dev-plan.md`,T1-T10 已全部完成并提交 (`59c7398`/`4d437a4`/`7361786` 等)。 > **2026-09-02 更新**:T11-T17(本文档全部任务)已完成并提交 > (`719fdbb`),6/6 测试通过。同一天 T1-T10 也补上了端到端实测(真实 > H3 生成,见 `dev-plan.md` §2.3 的更新说明):1 人物/10 件衣服/3 条 > 参考视频、11 条真实视频落库,衣柜已经有真实内容可选,不再是空的。 > 唯一没达成的是 `dev-plan.md` 原定的"≥30 条"量化标准(M.7 去重缓存 > 按人物×衣服算,10 件衣服上限就是 10 条不重复视频,要 30 条得配 30 > 件衣服)——但这不阻塞 T15 的"选衣服→生成→播放"链路,因为那条链路本 > 来就是按需实时生成(命中缓存的直接播,没缓存的现起 H3 任务),不依赖 > 提前批量铺好 30 条。 工时是**粗估**,单位人天,用来排先后不用来考核。任务编号接着第一期的 T1-T10 往下编,避免和 `dev-plan.md` 混淆。 --- ## 0. 第二期范围(需求文档 9 节第 2 条) **做**:`dance.html` 上线(含首次进入的年龄确认+免责声明,5.1 节), 接入"衣柜选衣服"免费模式(3.1 节)+ 相同人物+衣服组合去重缓存(4.6 节,M.7 唯一索引已在 T8 落地),复用现状排播队列基础设施思路但**要为 `dance` 频道重新实现一份独立的 schedule/promptQueue**(架构文档 2.1/2.5 节:两条频道各自维护自己的时刻表,不是共享一份)。此阶段全部免费内容 仍按 v1 现有投票机制排序,**不涉及"付费插队"这条新规则**(那是 `priority: paid_boost`,架构文档 2.6 节,第五期才有)。 **明确不做**(留到后续分期,见需求文档 9 节 3-5): - 模式二(付费传衣服)、模式三(付费传人物)——两者都要求账号+积分体系 先落地(第三/四期),本期不做上传+审核+扣积分链路。 - 用户注册/登录(`sessions` 完整实现,M.10)——本期沿用现有匿名 `viewer_uid` cookie,跟 `live.html` 一致(架构文档 2.7 节:"免费模式一 不强制登录")。 - 积分、`pricing_config`、充值、退款(第四期)。 - `blacklist`(M.11)人工拉黑表、人脸黑名单审核(第四/五期;模式一本身 不接受用户上传内容,风险面本来就比模式二/三小得多,可以先不接这道 口子)。 - `admin-playback.html`(插播/置顶/暂停填充,6.5 节)——锦上添花,不是 第二期出口条件,见第 4 节"明确不在第二期范围"。 - fal.ai 接入(第五期)。 --- ## 1. 依赖关系 ```mermaid flowchart TD T11[T11 dance 频道独立播出状态机] --> T15[T15 POST /api/dance/pick] T12[T12 GET /api/dance/wardrobe] --> T16[T16 衣柜 tab 前端接线] T15 --> T16 T13[T13 dance.html 页面骨架] --> T16 T13 --> T14[T14 年龄确认弹窗] T11 --> T17[T17 点赞/在线人数] T13 --> T17 classDef ready fill:#d4edda,stroke:#28a745 class T11,T12,T13,T14 ready ``` T11/T12/T13/T14 互不依赖,可以并行开工;T15 需要 T11 的时刻表先能收 `videoId` 入队;T16 是前端接线,需要 T12/T13/T15 都有了才能真正跑通端 到端(但页面骨架本身可以先搭起来,用假数据联调);T17 依赖 T11(复用它 的 tick)和 T13(页面上要有展示位)。 --- ## 2. 任务表 | # | 任务 | 依赖 | 验收标准 | 工时 | |---|---|---|---|---| | T11 | **dance 频道独立播出状态机**:新增 `dance-state.json`(`{ schedule, promptQueue }`,结构参照 `live-state.json` 但按 MVCP M.8 的**要求**——`schedule` 条目存 `videoId` 引用,不内联整份 clip 快照,读取时 join `library.json`);`ensureSchedule`/tick 逻辑参照 v1(`server.js` 现有 `schedule`/`promptQueue`/`ensureSchedule`),改成只从 `library.json` 里 `channel === "dance"` 的 clip 里选片 | T1(已完成,读写模式复用)、T8(已完成,产出的 clip 已带 `channel:"dance"`) | 独立于 `live.html` 的 schedule 运行,互不干扰;进程重启后时刻表从 `dance-state.json` 恢复,不会因为读到旧的内联 clip 快照而跟 `library.json` 当前数据不一致(这是 M.8 强调的坑);新增 `GET /api/dance/live/state`,返回结构镜像 `/api/live/state`(`current`/`upNext`/`queue`) | 1.5 | | T12 | **`GET /api/dance/wardrobe`**:公开只读接口,列出 `garments` 里 `isActive === true` 的条目(复用 `dressup-store.js` 的 `garments` store,不需要 admin 鉴权——这是观众端接口) | T4(已完成) | 下架的衣服不出现在返回列表里;无需登录/cookie 也能拿到 200 | 0.3 | | T13 | **`dance.html` 页面骨架**(MVCP V.1.1):顶部条(Logo/在线人数占位)+ 左侧衣柜 tab(先只做这一个 tab,传衣服/传人 tab 留空占位,第三/四期再接)+ 播放器容器按 **9:16** 设计(不是 `live.html` 现有横向容器拉伸)+ 右侧点赞/排队展示(无文字输入框)+ 移动端底部 tab 切换 | 无 | 页面能独立打开(不依赖登录);播放器容器在桌面和一个典型移动端视口下都不出现大黑边;跟 `live.html` 视觉语言一致(同一套 `style.css`/组件风格) | 1.5 | | T14 | **首次进入年龄确认弹窗**(5.1 节):非模态强制(不能通过点击背景关闭,必须点"已满18岁"才能继续),确认后写 cookie(**不需要**登录态,架构文档 2.7 节:免费模式一不强制登录,这条确认走 v1 昵称弹窗同款的纯前端 cookie 记忆套路),刷新页面不再弹 | T13 | 首次进入必弹;确认一次后关闭浏览器再打开(cookie 还在)不再弹;显式展示一句免责声明文案 | 0.3 | | T15 | **`POST /api/dance/pick` {garmentId}**(P.1 流程):取当前 `characters` 里 `isDefault === true` 的那条(T3 已支持"只有一条默认"),调用 T8 已实现的 `submitGeneration({characterId, garmentId})`(`server.js`,已支持 M.7 去重缓存);命中缓存或生成完成后,把 `videoId` 推进 T11 的 dance `promptQueue`(而不是止步于"落进 library.json"——T8 原本只做到落库,这里要接上"排入队列"这一步,是 T8 的直接延伸) | T11 | 提交一件下架的衣服(`isActive:false`)返回 400;同一衣服两个不同 cookie 各提交一次,第二次是缓存命中、秒级返回并排入队列(不重新起 H3 任务);进程重启后,仍在 `generating` 状态的 pick 请求能续上(复用 T8 `resumeDressupGenerations` 的机制,不用重写) | 1 | | T16 | **衣柜 tab 前端接线**:调 T12 拿列表渲染缩略图网格,点击调 T15 提交,提交后在"生成中"区域展示一条状态(复用 `live.html` 现有"生成中"列表的交互思路——`promptQueue` 里 `status` 驱动 UI,不是重新设计一套状态展示);播放器轮询 T11 的 `/api/dance/live/state` 按 `endAt` 精确切片(同 `live.html` 现有逻辑) | T12, T13, T15 | 选一件衣服 → 出现"生成中"提示 → 完成后自动进入播放队列,不需要手动刷新页面;两个浏览器 tab 看到的"当前播放"一致(服务端权威时钟,架构文档 1.2 节这条铁律在 dance 频道也要成立) | 1 | | T17 | **点赞 + 在线人数**(dance 频道独立计数):复用 v1 `POST /api/queue/vote/:id`/在线人数统计的逻辑,但作用域限定在 dance 频道自己的 `promptQueue`/viewer 心跳(不能跟 `live.html` 共用同一份计数,否则"在线人数"会把两个频道的观众混在一起) | T11, T13 | 点赞数变化影响 dance 免费队列的排序(v1 现有投票机制,本期不做付费插队);`live.html` 和 `dance.html` 分别打开,两边"在线人数"独立变化,互不叠加 | 0.5 | **总工时约 6.1 人天**,T11/T12/T13/T14 四条(约 3.6 人天)可以完全并 行;T15-T17(约 2.5 人天)依赖前面几条但彼此间也有一定并行空间(T15 后端 和 T16 前端骨架可以同时开工,最后再联调)。 --- ## 3. 第二期出口 - `dance.html` 能独立打开,首次进入弹年龄确认,确认后不再弹; - 衣柜 tab 能看到后台已上架的衣服(**依赖第一期出口"批量生成 ≥30 条" 先落地,不然衣柜是空的,见本文档开头的提示**); - 免费选一件衣服 → 提交 → 排入独立于 `live.html` 的播出队列 → 播放; - 同一件衣服被多个观众选中时,第二次起命中缓存,秒级排队,不重新起 GPU 任务; - 点赞/在线人数在 dance 频道内正常工作,且跟 `live.html` 的计数互不 干扰; - 全程不涉及登录、积分、上传、审核。 达成后才进第三期(用户注册体系,见需求文档 9 节第 3 条)。 --- ## 4. 明确不在第二期范围 避免执行时范围蔓延: - 模式二/三(付费传衣服/传人)的任何后端或前端部分(第三/四期,需要先 有账号+积分) - 用户注册/登录 `sessions` 完整实现(第三期) - 积分、`pricing_config`、充值、退款(第四期) - `blacklist`(M.11)CRUD 后台页面、人脸黑名单审核(第四/五期) - `admin-playback.html` 插播/置顶/暂停填充(6.5 节,非本期出口条件, 有余力可顺手做但不阻塞验收) - "我的作品"页面(7.4 节,依赖账号体系,第五期) - fal.ai 接入、付费插队排播优先级(`priority: paid_boost`,第五期)