换装跳舞直播间 — 技术设计文档(Model / View / Control / Process)
依据:产品需求 docs/todo/dressup-dance-room.md(下称"需求文档",本文各处
引用其章节号);系统架构 docs/structure/architecture.md(下称"架构文档")。
本文档把已确认的产品决策落到:数据库表结构(M)、前后端界面清单(V)、
按钮/交互动作(C)、各类用户的操作流程(P),供开发排期和实现参考。
约定:字段名延续现有 library.json/live-state.json 的英文小驼峰风格;
表结构在第一期仍以 JSON 文件承载,第三期(用户账号)后迁移 SQLite(见
架构文档 1.4 节)——本文按"关系表"描述,两种落地方式都能直接映射。
M — Model:数据结构设计
M.1 users
| 字段 |
类型 |
说明 |
id |
string (uuid) |
主键 |
email |
string |
邮箱,唯一(7.1 节,第一期登录方式) |
passwordHash |
string |
bcrypt/argon2,邮箱登录用 |
oauthProviders |
array |
预留 ["google","apple","github"],第一期为空数组 |
nickname |
string |
展示昵称,迁移自现有 cookie viewer_nickname |
legacyUid |
string \ |
null |
ageConfirmed |
boolean |
"我已满18岁" 勾选记录(5.1 节) |
ageConfirmedAt |
number (ts) |
|
creditBalance |
number |
当前积分余额(冗余字段,权威值仍是 credit_ledger 流水汇总,见 M.2) |
createdAt |
number (ts) |
|
M.2 credit_ledger(积分流水,7.2 节)
| 字段 |
类型 |
说明 |
id |
string |
主键 |
userId |
string |
外键 → users.id |
delta |
number |
正数=充值/退款,负数=消费 |
reason |
enum |
topup \ |
relatedAssetId |
string \ |
null |
balanceAfter |
number |
变动后余额,写入时计算好,避免每次汇总流水 |
paymentRef |
string \ |
null |
createdAt |
number (ts) |
|
M.3 characters(人物库)
| 字段 |
类型 |
说明 |
id |
string |
主键 |
name |
string |
|
imageUrl |
string |
人物参考图 |
tags |
array |
|
source |
enum |
admin_seed \ |
ownerUserId |
string \ |
null |
isDefault |
boolean |
是否为直播默认人物;第一期全库最多一条为 true(6.1 节已确认,不做多人物轮换) |
moderationStatus |
enum |
auto_passed \ |
totalSegments |
number \ |
null |
remainingSegments |
number \ |
null |
status |
enum |
active \ |
createdAt |
number (ts) |
|
M.4 garments(衣服库)
| 字段 |
类型 |
说明 |
id |
string |
主键 |
name |
string |
|
category |
string |
分类标签(古风/泳装/校服…) |
thumbnailUrl |
string |
|
imageUrl |
string |
原图 |
source |
enum |
admin_seed \ |
ownerUserId |
string \ |
null |
moderationStatus |
enum |
pending \ |
isActive |
boolean |
上下架开关 |
createdAt |
number (ts) |
|
M.5 dance_refs(参考跳舞视频/音乐库)
| 字段 |
类型 |
说明 |
id |
string |
主键 |
name |
string |
|
videoUrl |
string |
|
styleTags |
array |
|
durationSec |
number |
|
copyrightSource |
enum |
test_placeholder \ |
isActive |
boolean |
|
createdAt |
number (ts) |
|
M.6 composed_images(Stage1 换装图产物)
| 字段 |
类型 |
说明 |
id |
string |
主键 |
characterId |
string |
外键 |
garmentId |
string |
外键 |
imageUrl |
string |
合成结果 |
model |
string |
实际调用的 fal.ai 模型标识(4.1/8 节,待选型) |
status |
enum |
generating \ |
createdAt |
number (ts) |
|
唯一索引 (characterId, garmentId)——这是 4.6 节去重缓存的落地位置:
提交前先按这个组合查是否已有 status: done 的记录,命中直接复用,不
新建。
M.7 generated_videos(Stage2 产物,library.json 风格 clip 的超集)
在现有 library.json clip 字段基础上(见 docs/done 第 3 节)新增:
| 新增字段 |
类型 |
说明 |
characterId |
string |
|
garmentId |
string |
|
composedImageId |
string |
外键 → M.6,点开能看 Stage1 合成图(6.4 节) |
refDanceVideoId |
string |
外键 → M.5 |
channel |
enum |
live \ |
backend |
enum |
h3_console \ |
priority |
enum |
normal \ |
requestedByUserId |
string \ |
null |
width/height |
number |
dance 频道默认 480×832,live 频道现状默认已同步改为 480×832(见文末"勘误"一节) |
M.8 直播时刻表状态(非持久化数据库表,live-state.json 风格的内存+落盘结构,dance.html 独立一份)
{
"channel": "dance",
"schedule": [{ "videoId": "...", "startAt": 0, "endAt": 0, "requestedBy": null, "priority": "normal" }],
"priorityInbox": [ /* paid_boost 产物就绪后先进这里,ensureSchedule 优先消费,见架构文档 2.6 节 */ ],
"freeQueue": [ /* 免费模式一提交,走投票排序,结构同 v1 promptQueue */ ]
}
M.9 moderation_log
| 字段 |
类型 |
说明 |
id |
string |
主键 |
assetType |
enum |
garment_upload \ |
assetId |
string |
|
verdict |
enum |
auto_pass \ |
reason |
string \ |
null |
reviewedBy |
string |
system 或管理员 userId |
createdAt |
number (ts) |
|
M.10 ER 关系图
erDiagram
users ||--o{ credit_ledger : "流水"
users ||--o{ characters : "user_paid 拥有者"
users ||--o{ garments : "user_paid 拥有者"
users ||--o{ generated_videos : "requestedBy"
characters ||--o{ composed_images : "Stage1 输入"
garments ||--o{ composed_images : "Stage1 输入"
composed_images ||--o{ generated_videos : "Stage2 输入"
dance_refs ||--o{ generated_videos : "Stage2 输入"
characters ||--o{ moderation_log : "审核记录"
garments ||--o{ moderation_log : "审核记录"
V — View:前后端界面设计
V.1 观众端页面
| 页面 |
路径(建议) |
说明 |
| 换装跳舞直播间 |
dance.html |
主页面,见 V.1.1 |
| 登录/注册 |
弹窗(不单独开页,跟 v1 昵称弹窗交互一致,见架构文档/docs/done 2.5 节) |
邮箱输入 + 验证码/密码,首次触发付费操作时弹出 |
| 我的作品 |
my-works.html 或 dance.html?tab=mine(建议做成 dance.html 内的一个面板而不是独立页面,减少一个页面壳) |
7.4 节 |
| 充值 |
弹窗,from 积分不足拦截 或 顶部"我的积分"入口 |
7.3 节,具体支付渠道待定 |
V.1.1 dance.html 区域划分(复用 live.html 视觉语言,见需求文档 2 节)
┌─────────────────────────────────────────────┐
│ 顶部条:Logo | 在线人数 | 我的积分 | 头像/登录 │
├───────────┬───────────────────┬─────────────┤
│ │ │ │
│ 左侧栏 │ 播放器(9:16) │ 右侧栏 │
│ 衣柜/传衣 │ + 当前播放信息 │ 在线互动展示 │
│ /传人 tab │ (复用 live.html│ (点赞/排队 │
│ │ 的 now-playing)│ 列表,无输 │
│ │ │ 入框) │
│ │ │ │
├───────────┴───────────────────┴─────────────┤
│ 移动端:底部 tab 切换(衣柜/传衣/传人/我的) │
└─────────────────────────────────────────────┘
- 播放器容器按 9:16 设计(4.4 节),不是
live.html 现有横向容器的简单
拉伸。
- 左侧栏三个 tab:衣柜(默认)/ 传衣服 / 传人,对应需求文档
3 节三种模式。
- 首次进入弹年龄确认(5.1 节),非模态强制,一次性记忆(cookie/账号)。
V.2 后台管理页面
在现有 admin.html(docs/done 2.8 节)基础上新增,交互风格保持一致
(表格 + 行内编辑 + 新增表单,见需求文档 6 节开篇):
| 页面 |
对应资源 |
需求文档章节 |
admin-characters.html |
M.3 |
6.1 |
admin-garments.html |
M.4 |
6.2 |
admin-dance-refs.html |
M.5 |
6.3 |
admin.html(扩展现有表格新增列) |
M.7 |
6.4 |
admin-playback.html |
M.8(dance 频道排播干预) |
6.5 |
admin-users.html |
M.1 + M.2 |
用户/积分管理,7 节 |
后台导航建议在现有 admin.html 顶部加一排二级导航链到这些新页面,
不新起一套独立的后台框架。
V.3 后端 API 清单
延续 server.js 手写路由风格(无框架),按资源分组:
观众端:
GET /api/dance/library?sort= # dance 频道浏览三 tab,等价 v1 /api/library
GET /api/dance/live/state?cid=&uid= # dance 频道播出状态,等价 v1 /api/live/state
GET /api/dance/wardrobe # 衣柜列表(garments,isActive=true)
POST /api/dance/pick # 模式一:{characterId?, garmentId} 免费提交
POST /api/dance/upload-garment # 模式二:multipart 图片 + 走审核+扣积分
POST /api/dance/upload-character # 模式三:multipart 图片 + 年龄确认+黑名单审核+积分档位(决定 remainingSegments)
POST /api/dance/queue/vote/:id # 免费队列投票,等价 v1 /api/queue/vote
GET /api/dance/my-works # 我的作品(需登录)
POST /api/auth/register # 邮箱注册
POST /api/auth/login
POST /api/credit/topup # 发起充值(对接支付网关,7.3 节)
POST /api/credit/topup/webhook # 支付回调
后台:
CRUD /api/admin/characters[/:id]
CRUD /api/admin/garments[/:id]
CRUD /api/admin/dance-refs[/:id]
CRUD /api/admin/generated-videos[/:id] # 扩展现有 /api/admin/library
POST /api/admin/playback/insert # 插播/置顶
POST /api/admin/playback/skip # 跳过当前
POST /api/admin/playback/pause-fill # 暂停自动填充
POST /api/admin/blacklist # 拉黑用户/资产
GET /api/admin/moderation-log
GET /api/admin/users
POST /api/admin/credit/adjust # 人工调整积分(客服场景)
内部(生成流水线编排,不对前端暴露):
generateComposedImage(characterId, garmentId) # Stage1,查缓存(M.6唯一索引)→未命中调fal.ai
generateDanceVideo(composedImageId, refDanceVideoId, backend) # Stage2,backend按4.3节路由
C — Control:界面按钮/交互动作设计
C.1 dance.html
| 按钮/交互 |
触发条件 |
动作 |
反馈 |
| 衣柜卡片点击 |
始终可点(免费,无需登录) |
POST /api/dance/pick {garmentId} |
立即在"生成中"区出现一条;命中缓存(4.6节)秒级进"接下来播放" |
| "传衣服"上传按钮 |
需登录 |
若未登录先弹登录;选图 → 客户端基础校验(大小/格式)→ 上传 → 展示预计消耗积分 → 二次确认 |
提交后进图片审核队列;通过后自动排入生成,展示状态 |
| "传人"上传按钮 |
需登录 + 已完成年龄确认(5.1节) |
选图 → 选择/展示积分档位(决定 remainingSegments,7.2节)→ 可选搭配衣服(选衣柜或再传一张,不选则随机)→ 二次确认扣积分 |
提交后走黑名单自动审核(5节),通过直接生成,不阻塞用户等待 |
| 年龄确认弹窗"确认"按钮 |
首次进入 dance.html |
写 ageConfirmed=true(cookie 或账号,取决于是否已登录) |
弹窗关闭,露出主界面 |
| 点赞(❤️) |
浏览三 tab 内 |
POST /api/like/:id(复用 v1) |
数字 +1 |
| 排队投票(▲) |
免费队列条目 |
POST /api/dance/queue/vote/:id(复用 v1 投票逻辑,仅对 priority: normal 条目生效——paid_boost 条目不展示投票按钮,见需求文档 3 节"共同后置流程") |
高亮 + 票数更新 |
| "我的作品"入口 |
需登录 |
打开 V.1 的我的作品面板 |
列出该账号名下 generated_videos(含 status: expired 的人物产出) |
| "我的积分"/充值入口 |
始终可见(未登录显示"登录后查看") |
打开充值弹窗 |
对接支付网关(7.3节,渠道待定) |
| 静音/全屏 |
复用 live.html 现有 muteBtn/播放器交互 |
同 v1 |
同 v1 |
C.2 后台管理页面
| 按钮/交互 |
所在页面 |
动作 |
| 新增/编辑/删除 人物、衣服、参考视频 |
各自 admin 页 |
对应 CRUD API(V.3),风格照抄现有 admin.html 行内编辑 |
| "设为直播默认人物" 开关 |
admin-characters.html |
置该条 isDefault=true,同时把其他所有条目置 false(第一期全局唯一,6.1节) |
审核通过/驳回(pending→approved/rejected) |
admin-garments.html(user_paid 来源条目) |
更新 moderationStatus,写 moderation_log |
| 人工下架人物/内容 |
admin-characters.html/admin-generated-videos |
5节兜底手段,写 moderation_log(verdict: admin_takedown) |
| 插播/置顶下一条 |
admin-playback.html |
POST /api/admin/playback/insert,写入 M.8 priorityInbox |
| 跳过当前 |
admin-playback.html |
POST /api/admin/playback/skip |
| 暂停/恢复自动填充 |
admin-playback.html |
POST /api/admin/playback/pause-fill,暂停期间 ensureSchedule 只消费 priorityInbox/freeQueue,不再随机填充素材库 |
| 拉黑用户/资产 |
admin-playback.html 或 admin-users.html |
写黑名单表,后续该用户/资产提交直接拒绝,不进流水线 |
| 人工调整积分 |
admin-users.html |
POST /api/admin/credit/adjust,写 credit_ledger(reason: admin_adjust) |
P — Process:用户流程设计
P.1 匿名免费观众:选衣柜(对应需求文档 3 节模式一)
sequenceDiagram
participant V as 观众(未登录)
participant S as server.js
participant Cache as composed_images 缓存(M.6)
participant Fal as fal.ai
participant H3 as H3 Console
V->>S: 首次进入 dance.html
S-->>V: 弹年龄确认(5.1节)
V->>S: 确认已满18岁
V->>S: POST /api/dance/pick {garmentId}
S->>Cache: 查 (defaultCharacterId, garmentId)
alt 命中缓存
S-->>V: 直接排入队列(秒级)
else 未命中
S->>Fal: Stage1 换装合成
Fal-->>S: composedImageUrl
S->>H3: Stage2(冷启动算力,4.3节)
H3-->>S: 视频完成
S->>S: 写入 generated_videos + composed_images 缓存
S-->>V: 排入队列
end
Note over S: ensureSchedule 按 v1 投票逻辑排序(priority: normal)
P.2 注册观众:付费传衣服(模式二)
- 点"传衣服" → 未登录则先走 P.4 登录/注册。
- 选图 → 前端展示"这将消耗 N 积分"(数值待定,8节)。
- 确认 → 后端校验余额 → 不足则拦截,引导充值(P.4 充值流程)。
- 余额充足 → 预扣积分(
credit_ledger 写一条负数流水)→ 图片走轻量
内容审核。
- 审核不过 → 全额退还积分(写正数流水,
reason: refund_moderation_reject)
→ 前端提示。
- 审核通过 →
garments 新建一条 moderationStatus: approved,默认
isActive: true(沉淀进公共库)→ 走 Stage1/Stage2(可能命中已有
"默认人物+这件新衣服"缓存吗?不会命中,因为这件衣服刚创建,Stage1
必然是新生成)→ 完成后排入队列,priority: normal(模式二不享受
插队特权,只有模式三新人物才有,见需求文档 3 节)。
- 生成失败(Stage1/Stage2)→ 退还积分,
garments 该条标记不可用/删除
(避免衣柜库里出现一件"存在但没有可用换装产物"的死资产)。
P.3 注册观众:付费传人物(模式三,风险最高的流程)
sequenceDiagram
participant V as 观众(已登录,已年龄确认)
participant S as server.js
participant Blk as 人脸黑名单模型
participant Fal as fal.ai
participant Credit as credit_ledger
V->>S: 选积分档位(决定 remainingSegments)+ 上传人物照 + (可选)选/传衣服
S->>Credit: 预扣积分
S->>Blk: 自动人脸识别过滤(5节,非强制人工预审)
alt 命中黑名单
S->>Credit: 全额退还
S-->>V: 拒绝,提示原因
else 未命中,自动通过
S->>S: characters 新建一条 source:user_paid, remainingSegments=购买档位对应值
S->>Fal: Stage1 换装合成(若未指定衣服则随机挑一件衣柜库)
S->>Fal: Stage2(付费实时算力,4.3节,约4s级)
Fal-->>S: 视频完成
S->>S: generated_videos 新建,priority: paid_boost
S-->>V: 排入 priorityInbox,插队播放(不参与投票)
end
Note over S: 每播完一段,remainingSegments -= 1
Note over S: 配额耗尽 → characters.status = expired,从排播/复用池移除<br/>但记录保留,用户可在"我的作品"回看,可再充值续播
P.4 登录/注册 + 身份迁移
- 触发点:模式二/三的任一付费操作、或主动点"登录"。
- 邮箱注册/登录(7.1节)成功后,检查是否存在
viewer_uid/
viewer_nickname cookie(v1 遗留身份)——若存在,把该 uid 写入
新账号的 legacyUid 字段,历史投票/点播记录(挂在旧 uid 上的)
通过 legacyUid 关联查询,不做数据搬迁,只做关联(架构文档 2.7
节过渡方案)。
- 后续
uid 仍然写在 cookie 里(免费投票等匿名场景继续用它),但
付费操作一律要求 userId(登录态)。
P.5 充值
- 触发点:付费操作余额不足自动弹出,或主动点"我的积分"。
- 选充值档位 → 跳转/弹出支付网关(7.3节,渠道待定,标注 Stripe 对
国内支付方式支持有限的风险,见架构文档待验证表)。
- 支付成功回调
POST /api/credit/topup/webhook → 写 credit_ledger
(reason: topup, delta: +N, paymentRef) → 更新 users.creditBalance。
- 支付失败/取消 → 不产生流水,前端提示重试。
P.6 运营/管理员:素材维护 + 事后审核 + 播放干预
- 日常巡查(对接 5 节"事后人工兜底"):定期查看
moderation_log
里 verdict: auto_pass 但被举报/人工发现有问题的人物内容 → 在
admin-characters.html 手工下架 → 写 admin_takedown 记录。
- 播放干预:直播过程中运营通过
admin-playback.html 插播/跳过/
暂停自动填充(6.5节),不需要走观众端的正常提交流程。
- 素材库维护:定期给
garments/dance_refs 上下架,给
admin_seed 人物/衣服打标签分类。
P.7 系统自动化流程
- 冷启动批量生成(第一期,9节):运营在后台批量选定
人物×衣服组合,脚本/后台按钮触发批量
generateComposedImage +
generateDanceVideo(走内部 H3 Console,4.3节),铺满初始衣柜库/
composed_images 缓存,不涉及观众端。
- 排播 tick:
dance.html 独立的 ensureSchedule 定时任务(同 v1
每 2s,架构文档 1.3/2.6节),优先消费 priorityInbox(付费插队),
其次按投票消费 freeQueue,都为空时随机填充素材库(同 v1 逻辑,
避开冷却期)。
- 人物到期检测:每次消费完
priorityInbox 里某人物的一段后,
remainingSegments -= 1,归零则置 characters.status = expired
并从后续排播候选中过滤掉(架构文档 2.7节)。
勘误:live.html 分辨率现状更新
写这份需求文档时(docs/todo/dressup-dance-room.md 4.4节)记录的是
"live.html 现状横屏 832×480 不变,只有 dance.html 默认竖屏"。但
server.js 的 H3_GEN_PARAMS 在另一次改动中已经被改成
{ width: 480, height: 832 }(commit 1dc7084,live.html 侧的
说明是"未来生成的新片段走竖屏,存量片段保留原比例,播放器
object-fit: contain 兼容混合比例")——也就是说 live.html 现在
新生成的内容其实也已经是竖屏了,只是存量片段还是横屏混着播。
dance.html 侧的设计不受影响(本来就是独立的 H3_GEN_PARAMS 配置,
见架构文档 2.4节),但需求文档 2/4.4节"live.html 现状横屏不变"这句
描述已经不准确,建议后续同步一次勘误(不在本次技术设计文档范围内,
先在此记录,避免依赖这份文档的人被误导)。