Commit a446205432ea567b0b6cc916c6c4799fcea1348f
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
Showing
2 changed files
with
430 additions
and
2 deletions
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,8 +26,10 @@ | ||
| 26 | - 两个页面各自维护独立的播出时刻表(各自的 `schedule`/`promptQueue`), | 26 | - 两个页面各自维护独立的播出时刻表(各自的 `schedule`/`promptQueue`), |
| 27 | 不是同一个直播间的两个 tab——它们是两条并行的产品线,共用后端的 | 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 | ## 3. 观众体验流程(已确认:使用已有资产库免费,新增资产付费) | 34 | ## 3. 观众体验流程(已确认:使用已有资产库免费,新增资产付费) |
| 33 | 35 | ||
| @@ -124,6 +126,13 @@ Stage1(换装图合成)在两条路径下大概率都是 fal.ai(H3 Console | @@ -124,6 +126,13 @@ Stage1(换装图合成)在两条路径下大概率都是 fal.ai(H3 Console | ||
| 124 | `live.html` 现有横屏播放器的宽高比,否则会出现大黑边——这是 | 126 | `live.html` 现有横屏播放器的宽高比,否则会出现大黑边——这是 |
| 125 | `dance.html` 相对 `live.html` 除了"没有文字输入框"之外的第二个视觉 | 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 | ### 4.5 失败/超时处理 | 137 | ### 4.5 失败/超时处理 |
| 129 | 138 |