需求 1:管理端端口与软件 API 端口分离 - 软件 API 端口 PORT(默认 3000)只提供 /api、/health、/static、/contracts, 不提供管理页面(未命中路径 404);管理端端口 ADMIN_PORT(默认 3001) 提供管理页面与全部接口;ADMIN_HOST 可单独限制管理端来源 - main.ts 同进程监听两个端口,用 req.socket.localPort 区分端口行为; ADMIN_PORT=PORT 为单端口兼容模式(只创建一个 server,无 EADDRINUSE) - 端口非法在生产环境中止启动;ADMIN_PORT=PORT 只 warn - 启动日志分别打印两个端口的访问地址 需求 2:动作数据支持 VMD(MikuMikuDance) - vmd-parser.ts:二进制解析(30 字节头 + Shift-JIS 模型名 + 111 字节/条骨骼 关键帧 + morph 段)、MMD 贝塞尔插值、GBK 模型名回退、固定 30fps - mmd-skeleton.ts:标准 MMD 骨架;静止比例从参考 PMX 骨骼段实测得到 (足首 1.43 / 膝 6.57 / 髋 11.50 / 首 16.21 / 頭 17.16,腿长 10.09) - mmd-pose.ts:FK + 两骨骼解析腿部 IK(膝盖朝正面,不可达收敛为伸直) + 左手系→右手系 Z 镜像(由真实数据对照实验确定:94.1% vs 5.9%) - retarget.ts:抽出 worldFramesToKeypointFrames(),BVH 与 VMD 共用重定向 - MotionFormat 增 vmd、扩展名映射增 .vmd;管理端上传支持 .vmd - scripts/inspect-vmd.mjs:上传前自查工具 契约:rest-api / data-models / model-formats → 0.2.0(另两库已同步逐字节一致) 验证:85 项单测通过(VMD 31 项);契约与 29 条路由一致;typecheck/build 通过; 真实样本经 HTTP 端到端:霜雪千年 2901 帧(IK 腿)、恋爱循环 4604 帧(FK 腿) 均 preprocessState=done、0 NaN、0 越界(测试数据已从数据库清理)。
151 lines
5.1 KiB
Markdown
151 lines
5.1 KiB
Markdown
# contracts/data-models.md — 数据模型契约 v0.2.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` |
|
||
| `vrmVersion` | string\|null | 读 | `0.x` / `1.0`,非 VRM 为 null |
|
||
| `isPrimary` | boolean | | 全局默认模型;**同一时刻全库至多一个 `true`** |
|
||
| `fileUrl` | string | 读 | |
|
||
| `fileSize` | number | 读 | |
|
||
| `thumbnailUrl` | string\|null | 读 | |
|
||
| `source*` | | ✅ | 来源五元组 |
|
||
| `status` | `PublishStatus` | | |
|
||
| `createdAt` / `updatedAt` | string | 读 | |
|
||
|
||
### 4.1 VRM 优先规则(冻结)
|
||
|
||
客户端选择默认模型的顺序:
|
||
|
||
```
|
||
1. 显式指定 modelId(曲目动作绑定)
|
||
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. 无可用模型 → 客户端使用内置占位模型
|
||
```
|
||
|
||
`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 的规则解析得出,客户端无需自行实现优先级。
|