dressup-dance-room-mvcp.md 22.2 KB

换装跳舞直播间 — 技术设计文档(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 注册观众:付费传衣服(模式二)

  1. 点"传衣服" → 未登录则先走 P.4 登录/注册。
  2. 选图 → 前端展示"这将消耗 N 积分"(数值待定,8节)。
  3. 确认 → 后端校验余额 → 不足则拦截,引导充值(P.4 充值流程)。
  4. 余额充足 → 预扣积分(credit_ledger 写一条负数流水)→ 图片走轻量 内容审核。
  5. 审核不过 → 全额退还积分(写正数流水,reason: refund_moderation_reject) → 前端提示。
  6. 审核通过 → garments 新建一条 moderationStatus: approved,默认 isActive: true(沉淀进公共库)→ 走 Stage1/Stage2(可能命中已有 "默认人物+这件新衣服"缓存吗?不会命中,因为这件衣服刚创建,Stage1 必然是新生成)→ 完成后排入队列,priority: normal(模式二不享受 插队特权,只有模式三新人物才有,见需求文档 3 节)。
  7. 生成失败(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 登录/注册 + 身份迁移

  1. 触发点:模式二/三的任一付费操作、或主动点"登录"。
  2. 邮箱注册/登录(7.1节)成功后,检查是否存在 viewer_uid/ viewer_nickname cookie(v1 遗留身份)——若存在,把该 uid 写入 新账号的 legacyUid 字段,历史投票/点播记录(挂在旧 uid 上的) 通过 legacyUid 关联查询,不做数据搬迁,只做关联(架构文档 2.7 节过渡方案)。
  3. 后续 uid 仍然写在 cookie 里(免费投票等匿名场景继续用它),但 付费操作一律要求 userId(登录态)。

P.5 充值

  1. 触发点:付费操作余额不足自动弹出,或主动点"我的积分"。
  2. 选充值档位 → 跳转/弹出支付网关(7.3节,渠道待定,标注 Stripe 对 国内支付方式支持有限的风险,见架构文档待验证表)。
  3. 支付成功回调 POST /api/credit/topup/webhook → 写 credit_ledger (reason: topup, delta: +N, paymentRef) → 更新 users.creditBalance。
  4. 支付失败/取消 → 不产生流水,前端提示重试。

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 系统自动化流程

  1. 冷启动批量生成(第一期,9节):运营在后台批量选定 人物×衣服组合,脚本/后台按钮触发批量 generateComposedImage + generateDanceVideo(走内部 H3 Console,4.3节),铺满初始衣柜库/ composed_images 缓存,不涉及观众端。
  2. 排播 tick:dance.html 独立的 ensureSchedule 定时任务(同 v1 每 2s,架构文档 1.3/2.6节),优先消费 priorityInbox(付费插队), 其次按投票消费 freeQueue,都为空时随机填充素材库(同 v1 逻辑, 避开冷却期)。
  3. 人物到期检测:每次消费完 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 现状横屏不变"这句 描述已经不准确,建议后续同步一次勘误(不在本次技术设计文档范围内, 先在此记录,避免依赖这份文档的人被误导)。