契约先行(三端逐字节一致,已同步 windows/android):
- rest-api -> 0.5.0:新增 GET/POST /api/scenes、GET/PATCH/DELETE /api/scenes/{id}
- data-models -> 0.7.0:新增 §7 Scene 与 7.1 SceneConfig(物理/环境/出生点/相机)
- database -> 0.4.0:新增 scenes 表(config/stats 用 MEDIUMTEXT,迁移可逆)
后端:
- entities/scene.entity.ts + 迁移 AddScenes1789650000000(scenes 表)
- scenes/scene-config.ts:SceneConfig 定义 + 默认值 + **边界收敛**(越界夹紧、
非法枚举回退),保证客户端拿到的参数一定可解释;服务端只存参数不跑物理
- scenes/scenes.service.ts:上传(.glb/.gltf 直接导入;.zip/.pmx/.fbx 转 GLB,
场景不做 humanoid 重绑定、不转 VRM)、列表/详情/更新/软删除、缩略图复用模型
光栅化器、分析复用模型检查器、config 同时接受 JSON 字符串与平铺字段
- scenes/scenes.controller.ts + scenes.module.ts(app.module 注册)
- 审核复用 /api/reviews/:type/:id:TargetType.Scene 接入待审列表与状态流转
- catalog/export 增 scenes[],目录指纹纳入场景变更
- 枚举:TargetType.Scene、AssetKind.Scene / SceneThumbnail
测试:新增 test/scene-config.spec.ts 6 项(默认值/夹紧/枚举回退/出生点上限/颜色/
序列化);规模 19 文件 / 243 项;check-routes 35 条一致。版本 0.6.0。
注:管理端场景页(GLB 预览 + 物理参数可视化)与 Windows 场景选择留待下一轮。
9.2 KiB
9.2 KiB
contracts/data-models.md - 数据模型契约 v0.5.0
Web 服务端是唯一权威定义,本文件是另两端消费的稳定视图。
字段名在 JSON 中统一 camelCase;数据库列名 snake_case(见 database.md)。
1. 通用约定
- 所有资源返回
id: number(BIGINT,超出 2^53 前不会发生)。 - 所有时间为 ISO 8601 UTC 字符串,例:
2026-09-14T10:00:00.000Z。 - 列表接口统一分页响应:
{ "items": [], "total": 0, "page": 1, "pageSize": 20 }
- 错误响应统一:
{ "statusCode": 400, "message": "...", "error": "Bad Request", "details": {} }
- 来源(出处)五元组是必填项,出现在
songs/motions/models三处:
| 字段 | 类型 | 说明 |
|---|---|---|
sourceType |
enum | 见 1.1 |
sourceName |
string | 来源名称,如「官方曲库」「作者名」 |
sourceUrl |
string | 来源链接,必须为 http(s) |
license |
string | 许可标识,如 CC-BY-4.0、MIT、proprietary、unknown |
author |
string | 作者 / 权利人 |
1.1 SourceType 枚举
original 原创
licensed 已获授权
cc Creative Commons
public_domain 公共领域
user_upload 用户上传(需审核)
other 其他(须在 sourceName 说明)
1.2 PublishStatus 枚举
draft 草稿,仅管理端可见
pending 待审核
published 已发布,客户端可见
rejected 已驳回
2. Song
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
number | 读 | |
title |
string | ✅ | 曲名,<=200 |
artist |
string | ✅ | 演唱者,<=200 |
album |
string | 专辑,<=200 | |
language |
string | zh / en / ja / ko / instrumental |
|
durationMs |
number | 时长毫秒 | |
bpm |
number | 节拍 | |
difficulty |
number | 1..5 | |
tags |
string[] | ||
coverUrl |
string|null | 封面访问地址 | |
audioUrl |
string|null | 音频访问地址 | |
source* |
✅ | 来源五元组 | |
status |
PublishStatus |
默认 draft |
|
createdAt / updatedAt |
string | 读 |
3. Motion(动作数据)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
number | 读 | |
songId |
number|null | 关联曲目 | |
name |
string | ✅ | 动作名称,<=200 |
format |
enum | ✅ | bvh / vmd / fbx / gltf / glb / keypoints-json |
fileUrl |
string | 读 | 原始动作文件 |
fileSize |
number | 读 | 字节 |
fps |
number|null | 原始帧率 | |
frameCount |
number|null | 原始帧数 | |
durationMs |
number|null | ||
keypointFormat |
string|null | 读 | 预处理后固定为 mediapipe33@0.1.0 |
keypointUrl |
string|null | 读 | 预处理产物(NDJSON 或.npz 的 JSON 封装) |
thumbnailUrl |
string|null | 读 | 骨架缩略图(/preprocess 时生成,动作管理列表展示;未生成时为 null)(v0.7.0) |
keypointFrames |
number|null | 读 | 关键点帧数 |
preprocessState |
enum | 读 | none / running / done / failed |
preprocessError |
string|null | 读 | |
source* |
✅ | 来源五元组 | |
status |
PublishStatus |
||
createdAt / updatedAt |
string | 读 |
4. Model(角色模型,VRM 优先)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
number | 读 | |
name |
string | ✅ | 模型名称,<=200 |
format |
enum | ✅ | vrm(主力)/ glb(回退或历史数据)/ gltf / fbx。所有上传模型一律转换为 VRM,只有 humanoid 不达标时回退为 glb 并置 usable=false,见 model-formats.md 1.1 |
vrmVersion |
string|null | 读 | 0.x / 1.0,非 VRM 为 null |
usable |
boolean | 读 | 是否可被客户端使用:等价于"已产出 VRM"(format=vrm)。false 的模型不出现在 catalog/export(v0.5.0) |
unusableReason |
string|null | 读 | usable=false 时的原因,例如 humanoid 不达标(0/22),未产出 VRM |
isPrimary |
boolean | 全局默认模型;同一时刻全库至多一个 true |
|
fileUrl |
string | 读 | |
fileSize |
number | 读 | |
thumbnailUrl |
string|null | 读 | |
conversionNote |
string|null | 读 | 由压缩包转换而来时的说明(Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图);直传模型为 null |
fileSha256 |
string|null | 读 | 模型文件(转换产物)的 sha256 -- 每次转换都会变,仅作记录 |
sourceSha256 |
string|null | 读 | 上传压缩包的 sha256(稳定);默认模型匹配以它为准,非压缩包上传为 null |
stats |
object|null | 读 | 模型分析结果:面数 / 顶点数 / 贴图 / 骨骼树 / humanoid 覆盖 / 可驱动性,见 model-formats.md 1.1 |
source* |
✅ | 来源五元组 | |
status |
PublishStatus |
||
createdAt / updatedAt |
string | 读 |
4.1 默认模型选择规则(冻结)
服务端 GET /api/catalog/export 的 defaultModelId 按以下顺序解析:
0. 环境变量 DEFAULT_MODEL_ID 指定,且该模型存在且已发布
1. 源码内写死的默认模型种子(web-server/src/config/default-model.ts 的
DEFAULT_MODEL_SEED):按 **sourceSha256(上传压缩包的 sha256)** 匹配已发布模型,
换库/重建库后仍然有效;没有压缩包时退回按产物 sha256,最后按名称兜底。
⚠ 不能用产物 sha256 当主键:实测 Assimp 的 GLB 导出不是字节级确定的,
同一份 zip 每次转换得到的文件 sha256 都不同(BIN chunk 有十几字节浮动)
2. models 中 format = 'vrm' 且 status = 'published' 且 is_primary = 1
3. models 中 format = 'vrm' 且 status = 'published',按 id 升序取第一个
4. models 中 status = 'published',按 (format='vrm' DESC, id ASC) 取第一个
5. 无可用模型 -> 客户端使用内置占位模型
第 0/1 步是 v0.3.0 新增:上传 MMD 模型压缩包并转换成功后,服务端会把该模型
is_primary=1 + status=published,并把身份写进源码,使它成为开箱默认模型。
isPrimary 通过 POST /api/models/:id/primary 设置,服务端保证互斥(同一事务内先清零)。
5. ReviewRecord(审核流水)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
number | |
targetType |
enum | song / motion / model |
targetId |
number | |
action |
enum | submit / approve / reject / withdraw |
fromStatus / toStatus |
PublishStatus |
|
reviewer |
string|null | 审核人标识 |
comment |
string|null | 审核意见 |
createdAt |
string |
6. CatalogBundle(客户端一次性拉取,GET /api/catalog/export)
{
"contractVersion": { "rest-api": "0.1.0", "data-models": "0.1.0", "skeleton-topology": "0.1.0", "websocket-keypoints": "0.1.0" },
"generatedAt": "2026-09-14T10:00:00.000Z",
"songs": [],
"motions": [],
"models": [],
"defaultModelId": 3
}
- 仅包含
status = 'published'的记录。 defaultModelId由 4.1 的规则解析得出,客户端无需自行实现优先级。
7. Scene(场景,GLB 为主)(v0.7.0)
场景与模型的区别:以 GLB 为主(不做 humanoid 重绑定、不转 VRM), 并携带物理参数(服务端只存参数、不跑物理;仿真在客户端)。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
number | 读 | |
name |
string | ✅ | 场景名称,<=200 |
glbUrl |
string|null | 读 | 场景 GLB 下载地址 |
thumbnailUrl |
string|null | 读 | 缩略图(服务端离线渲染) |
fileSize |
number|null | 读 | |
fileSha256 |
string|null | 读 | |
conversionNote |
string|null | 读 | 由 .zip/.pmx/.fbx 转换而来时的说明 |
config |
SceneConfig |
✅ | 物理 / 环境 / 出生点 / 相机,见 7.1 |
stats |
object|null | 读 | 场景 GLB 分析(面数 / 网格 / 材质 / warnings) |
sourceType sourceName sourceUrl license author |
string | ✅ | 来源五元组 |
status |
enum | 读 | 上传一律 pending,经审核后 published |
7.1 SceneConfig(冻结)
physics:
gravity: { x: 0, y: -9.8, z: 0 } # 米/秒^2,各分量 ±200
wind:
base: { x: 0, y: 0, z: 0 } # 基础风向量(米/秒,±100)
gustAmplitude: 0 # 阵风幅度 0~100
gustFrequency: 0.2 # 阵风频率 Hz,0.01~5
gustDirection: { x: 1, y: 0, z: 0 } # 阵风方向(±1)
space: world | local # 风的作用坐标系
ground: { friction: 0.6, restitution: 0, slopeLimit: 45 } # 0~1 / 0~1 / 0~89 度
colliders: auto | mesh | none # auto=按 GLB 命名约定,mesh=三角网
environment:
ambientColor: "#8fb4ff" # #RRGGBB
ambientIntensity: 1 # 0~4
fogDensity: 0 # 0~1(0=关闭)
spawnPoints: [{ x: 0, y: 0, z: 0, yaw: 0 }] # 最多 16 个
camera: { mode: follow | fixed | orbit, distance: 4.5, height: 1.6 }
服务端入库时对每项做边界收敛(越界夹紧、非法枚举回退默认值),
保证客户端拿到的参数一定可解释;catalog/export 的 scenes[] 直接下发该结构。