需求 1:模型上传支持压缩包(MMD 模型包) - common/zip-reader.ts:纯 TS 极简 ZIP 读取器(EOCD/中央目录、STORE+DEFLATE、 ZIP64 大小字段、Shift-JIS/GBK 文件名回退、拒绝 zip-slip 与加密条目) - models/model-convert.ts:解压 → 选主模型(指名字/最浅/最大)→ Assimp 导出 FBX → **assimp info 回读校验**(Assimp 对带蒙皮大骨架模型会写出损坏文件: 实测 714 骨骼 PMX 导出退出码 11、产物回读报 FBX-DOM no Objects dictionary found) → 失败自动回退 GLB;两种都失败则 422 - models/glb-textures.ts:把 Assimp 写成外部 URI 的贴图内嵌进 GLB 的 BIN chunk, 保证模型库里单文件即可加载(否则是白模) - 溯源:压缩包原件存为 model-archive 资产;models 新增 source_asset_id / conversion_note(迁移 1789450000000-AddModelConversionFields,已在真实库上 验证 up/down 可逆) - 默认模型:转换成功且 autoDefault 不为 false → is_primary=1 + published, 并把身份(名称/格式/产物 sha256)写进源码 src/config/default-model.ts; resolveDefaultModelId 增加 DEFAULT_MODEL_ID 与源码种子两步(按 sha256 匹配, 换库仍有效),实测库中有 2 个 VRM 时默认模型仍解析为该 GLB - scripts/import-default-model.mjs:运维 CLI(上传 zip 并写源码 / --write-source-only) - 修复 multipart 中文文件名乱码(upload.util.decodeUploadName) - DTO/契约新增 conversionNote、fileSha256;管理端 accept 增加 .zip 并展示转换来源 需求 2:首次启动 .env 引导 - deploy/init-env.sh:交互式逐项填写 8 个键(回车保留默认、口令不回显不加引号、 端口校验、mysql2 连通性自检且口令只经环境变量传递、只改这 8 个键并保留模板注释、 chmod 600、结尾打码摘要);--force/--check/-y/-i/--skip-db-check 等开关; 非交互(-y、OPENDANCE_NONINTERACTIVE、stdin 关闭)绝不提问、读取带超时 - install.sh 缺 .env 时改为调用引导,失败才 die 需求 3:默认监听 0.0.0.0 - 代码默认值、两份 .env 模板、install.sh、init-env.sh 五处一致,并新增测试守护 (未设置 HOST 时为 0.0.0.0;两份模板都写着 HOST=0.0.0.0) 契约:rest-api / data-models / model-formats → 0.3.0,database → 0.2.0(另两库已同步) 验证:125 项单测通过(7 文件);契约 6 项与 29 条路由一致;typecheck/build 通过; 真实包端到端 HTTP 201 / 0.89s / format=glb / isPrimary=true / published; GLB 1,152,464 字节、1 mesh / 240 nodes / 1 skin(238 joints) / 2 张内嵌贴图; 迁移 revert→run 实测可逆且数据无损恢复
5.9 KiB
5.9 KiB
contracts/data-models.md — 数据模型契约 v0.3.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 封装) |
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 | 读 | |
conversionNote |
string|null | 读 | 由压缩包转换而来时的说明(Hatsune Miku.pmx → GLB,已内嵌 2 张贴图);直传模型为 null |
fileSha256 |
string|null | 读 | 模型文件(转换产物)的 sha256,用于换库后定位默认模型 |
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):按转换产物 sha256 匹配已发布模型 —— 换库/重建库后仍然有效
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 的规则解析得出,客户端无需自行实现优先级。