按《DSH 开发文档 v1.6》4.9 与第十一章实现,Web 端第一步:
1. 契约先行(先改 contracts/ 再同步三端):
* model-formats.md -> 0.5.0:模型主力格式改为 VRM,所有上传模型一律转 VRM
(.pmx/.fbx: assimp -> GLB -> VRM;glb/gltf: 包装为 VRM);
新增 humanoid 硬门槛与"不达标回退 GLB + usable=false"的交付表;
* data-models.md -> 0.5.0:Model 新增 usable / unusableReason,明确
usable=false 不进入 catalog/export;format 说明改为主力 vrm;
* VERSION 与 contract-versions.ts 同步(三端契约文件逐字节一致)。
2. 实现:
* MODEL_CONVERT_FORMAT 默认改为 vrm(deploy/.env.example 同步);
* 新增 isUsableModel()/unusableReason():usable 等价于"已产出 VRM",
原因从 stats 里带出 humanoid 覆盖度;
* catalog/export 与 catalog/version 指纹都只统计 format=vrm 的已发布模型,
resolveDefaultModelId 同样只在可用模型中选;
* 管理端:模型列表与审核面板显示「不可用」及原因;
* 管理端左下角展示当前版本号(构建期从 web-server/package.json 注入
__APP_VERSION__,与 GET /health 同源),并去掉写死的"契约 rest-api 0.1.0"。
3. 测试:新增 test/model-usability.spec.ts 5 项;规模 16 文件 / 225 项。
4. 版本 0.3.0。
165 lines
6.9 KiB
Markdown
165 lines
6.9 KiB
Markdown
# 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`。
|
|
- 列表接口统一分页响应:
|
|
|
|
```json
|
|
{ "items": [], "total": 0, "page": 1, "pageSize": 20 }
|
|
```
|
|
|
|
- 错误响应统一:
|
|
|
|
```json
|
|
{ "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 封装) |
|
|
| `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`)
|
|
|
|
```json
|
|
{
|
|
"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 的规则解析得出,客户端无需自行实现优先级。
|