From b9215d4190e12e10b02a743c7eaee213d9fa849b Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Mon, 14 Sep 2026 19:00:55 +0800 Subject: [PATCH] =?UTF-8?q?docs(web):=20=E4=BB=BB=E5=8A=A1=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E3=80=81DSH=20=E7=BA=A6=E6=9D=9F=E4=B8=8E=E4=BA=A4?= =?UTF-8?q?=E4=BB=98=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DSH.md | 154 +++++++++++++++++ README.txt | 149 +++++++++++++++++ THIRD_PARTY.md | 124 ++++++++++++++ project.yaml | 71 ++++++++ scripts/package-release.sh | 84 ++++++++++ tasks/001-scaffold.md | 63 +++++++ tasks/002-db-migration.md | 57 +++++++ tasks/003-song-crud.md | 52 ++++++ tasks/004-motion-upload.md | 56 +++++++ tasks/005-model-upload.md | 61 +++++++ tasks/006-keypoint-preprocess.md | 67 ++++++++ tasks/007-upload-review.md | 55 ++++++ tasks/008-admin-ui-songs.md | 53 ++++++ tasks/009-admin-ui-models.md | 49 ++++++ tasks/010-deploy.md | 87 ++++++++++ web-server/test/smoke.mjs | 279 +++++++++++++++++++++++++++++++ 16 files changed, 1461 insertions(+) create mode 100644 DSH.md create mode 100644 README.txt create mode 100644 THIRD_PARTY.md create mode 100644 project.yaml create mode 100755 scripts/package-release.sh create mode 100644 tasks/001-scaffold.md create mode 100644 tasks/002-db-migration.md create mode 100644 tasks/003-song-crud.md create mode 100644 tasks/004-motion-upload.md create mode 100644 tasks/005-model-upload.md create mode 100644 tasks/006-keypoint-preprocess.md create mode 100644 tasks/007-upload-review.md create mode 100644 tasks/008-admin-ui-songs.md create mode 100644 tasks/009-admin-ui-models.md create mode 100644 tasks/010-deploy.md create mode 100644 web-server/test/smoke.mjs diff --git a/DSH.md b/DSH.md new file mode 100644 index 0000000..73a7304 --- /dev/null +++ b/DSH.md @@ -0,0 +1,154 @@ +# DSH.md —— opendance-web(OpenDance Studio 项目一:Web 管理服务端) + +## 0. 本文件的作用 + +本文件是 **opendance-web 仓库内**的 DSH 执行约束。顶层说明书见用户提供的 +《DSH 开发文档:OpenDance Studio》,二者冲突时以顶层文档为准。 + +## 1. 项目定位 + +Web 管理服务端是另两端(Windows 客户端、Android 辅助 App)的**唯一数据源**与 +**契约权威**。它管理歌曲、VRM 模型与动作来源,并输出冻结的 REST / WebSocket / +数据模型契约。 + +``` +现有 MySQL 实例(外部,本项目不部署) + │ + ▼ +opendance-web ←── 本仓库 + │ 冻结 REST API + 数据模型 + WebSocket 关键点协议 + ├──▶ opendance-windows + └──▶ opendance-android +``` + +## 2. 技术栈 + +| 组件 | 选型 | +|---|---| +| 后端 | NestJS 11 | +| ORM | TypeORM 0.3 | +| 数据库 | **现有 MySQL 兼容实例**(当前部署为 MariaDB 10.11.19),mysql2 驱动 | +| 管理前端 | React 18 + Vite + TypeScript | +| 3D 预览 | three.js + @pixiv/three-vrm | +| 多格式解析 | BVH 内置纯 TS 解析器;FBX/glTF 走可选外部 Assimp | +| 认证 | **本版本不含认证**(内网部署);上线前须补 ≥ T-WEB-011 | + +## 3. 单次会话加载范围 + +允许加载: + +1. 本文件 `DSH.md` +2. `project.yaml` +3. 当前 `tasks/xxx.md` +4. 该任务文件中 `load` 列出的文件 +5. `contracts/` 中被明确引用的**锚点片段** + +禁止加载: + +- `opendance-windows` / `opendance-android` 的任何源码 +- 非当前任务的 `tasks/*.md` +- 完整 `contracts/` 目录(只引用锚点) +- `node_modules/`、`dist/`、构建产物、二进制、模型文件、上传存储 + +## 4. 上下文预算 + +| 项 | 建议值 | +|---|---| +| 单任务文件数 | ≤ 6 | +| 单任务代码行数 | ≤ 1500 | +| 单任务 Token | ≤ 12K | +| 契约引用 | 只引用锚点,不加载全文 | + +## 5. 目录结构 + +``` +opendance-web/ +├── project.yaml # 任务清单与状态 +├── DSH.md # 本文件 +├── tasks/ # 单任务说明书(001~010) +├── contracts/ # ★ 跨端契约(权威) +│ ├── VERSION # 契约版本(唯一来源) +│ ├── rest-api.yaml # OpenAPI 3.0 +│ ├── websocket-keypoints.md # 239 字节关键点帧 + 控制消息 +│ ├── data-models.md # 资源字段与 VRM 优先规则 +│ ├── skeleton-topology.md # MediaPipe 33 点拓扑 + 关节角 +│ ├── model-formats.md # 支持格式、预处理产物、上传限制 +│ ├── database.md # 现有 MySQL 连接与迁移约定 +│ └── open-source-credits.md # 开源声明锚点(三端共用) +├── web-server/ # NestJS 服务端 +│ ├── src/ # 源码(entities / modules / preprocess) +│ ├── admin/ # React 管理端(构建上下文) +│ ├── test/ # 单元测试 + 端到端冒烟 +│ ├── scripts/ # migrate.mjs / check-contracts.mjs +│ └── Dockerfile +├── deploy/ # ★ 部署(不含 MySQL) +│ ├── docker-compose.yml +│ ├── install.sh +│ └── .env.example +├── scripts/package-release.sh # 发布产物打包 +└── THIRD_PARTY.md +``` + +## 6. 硬性约束 + +1. **不部署 MySQL**。数据库为部署方提供的现有实例,连接信息只经环境变量注入。 +2. **`DB_SYNCHRONIZE` 生产必须为 `false`**,所有表结构变更走 migration;每个 + migration 必须可逆。 +3. **绝不把凭证写入仓库**。`.env` 已在 `.gitignore` 中,只提交 `.env.example`。 +4. **来源五元组强制必填**:`source_type` / `source_name` / `source_url` / `license` + / `author`。任何资产入库缺一即 400。 +5. **契约先行**:改接口先改 `contracts/`,再提升 `contracts/VERSION`,同步 + `web-server/src/config/contract-versions.ts`,最后才改实现。 + `npm run contracts:check` 在构建前拦截漂移。 +6. **不使用 SMPL** 及其衍生拓扑(许可风险),统一 MediaPipe 33 点。 +7. **上传文件永不使用原始文件名作为路径**,一律 `sha256` 去重后归档。 + +## 7. 每任务的执行步骤 + +```bash +# 1. 静态与单元校验 +cd web-server +npm run contracts:check +npm run typecheck +npm run test + +# 2. 构建 +npm run build + +# 3. 端到端冒烟(需数据库可连通) +npm run start:prod & +npm run test:smoke + +# 4. 数据库迁移 +npm run migration:run + +# 5. 提交 +git checkout -b task/T-WEB-0XX +git add . && git commit -m "feat(web): " +git push origin task/T-WEB-0XX +# Gitea 创建 PR → 合并到 dev +``` + +## 8. 环境变量 + +见 `web-server/.env.example` 与 `deploy/.env.example`。生产关键项: + +``` +DB_HOST DB_PORT DB_USER DB_PASS DB_NAME +DB_SYNCHRONIZE=false +STORAGE_DIR=/data/storage +UPLOAD_MAX_BYTES=536870912 +ASSIMP_BIN=assimp # 可选 +CONTRACTS_DIR=/contracts # 可选,读取实时契约版本 +``` + +## 9. 上下文超标时的拆分策略 + +| 超标原因 | 拆分方式 | +|---|---| +| 单任务涉及多个端点 | 按端点拆 | +| 单文件过大 | 按类/函数拆,先生成接口再分实现 | +| 契约全文加载 | 改用锚点引用 | +| 需要参考其他项目 | 只读契约,不读源码 | +| UI 与逻辑耦合 | UI 任务只调用已定义接口 | +| 测试与实现同任务 | 测试单独任务,共享接口摘要 | diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..e498e68 --- /dev/null +++ b/README.txt @@ -0,0 +1,149 @@ +================================================================ +OpenDance Studio —— Web 管理服务端 v0.1.0 使用说明 +================================================================ + +本服务端是 OpenDance Studio 三端中的第一端,负责管理歌曲、VRM 模型与 +动作来源,是 Windows 客户端与 Android 辅助 App 的唯一数据源与契约权威。 + +重要:本服务端 **不部署、不管理 MySQL**。数据库为部署方提供的现有实例, +连接信息通过 .env 注入。 + +---------------------------------------------------------------- +一、快速部署(Docker Compose) +---------------------------------------------------------------- + +前置要求 + - Linux 服务器,已安装 Docker 与 docker compose + - 一个可连通的现有 MySQL / MariaDB 实例,并已创建数据库与账号: + + CREATE DATABASE IF NOT EXISTS dpendance + CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + CREATE USER IF NOT EXISTS 'dpendance'@'%' IDENTIFIED BY '<密码>'; + GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP, REFERENCES + ON dpendance.* TO 'dpendance'@'%'; + FLUSH PRIVILEGES; + + (本项目不会创建数据库本身,也不需要 GRANT OPTION / FILE / SUPER 等管理权限) + +部署步骤 + cd deploy + cp .env.example .env + vi .env # 填写 DB_HOST / DB_PORT / DB_USER / DB_PASS / DB_NAME + ./install.sh + +脚本会依次:校验 .env → 构建镜像 → 执行 TypeORM 迁移 → 启动服务 → 健康检查。 + +部署完成后 + API: http://:3000 + Admin: http://:8080 + 健康检查: http://:3000/health + +停止服务 + ./uninstall.sh # 保留上传数据卷 + ./uninstall.sh -v # 连数据卷一起删除 + +---------------------------------------------------------------- +二、本地开发 +---------------------------------------------------------------- + +后端 + cd web-server + cp .env.example .env # 填写 DB_* + npm install + npm run migration:run # 首次需要 + npm run start:dev # http://localhost:3000 + +管理前端 + cd web-server/admin + npm install + npm run dev # http://localhost:5173(/api 与 /health 代理到 3000) + +---------------------------------------------------------------- +三、验证 +---------------------------------------------------------------- + + cd web-server + npm run contracts:check # 契约版本一致性 + npm run typecheck # TypeScript 严格检查 + npm run test # 31 项单元测试 + npm run build # 生产构建 + npm run start:prod & # 启动 + npm run test:smoke # 57 项端到端冒烟(需要数据库可连通) + + cd ../admin && npm run build # 管理端构建 + +---------------------------------------------------------------- +四、环境变量 +---------------------------------------------------------------- + + DB_HOST / DB_PORT / DB_USER / DB_PASS / DB_NAME + 现有 MySQL 实例连接信息(必填) + + DB_SYNCHRONIZE + 必须为 false。生产环境启动时若检测到 true 会直接中止启动。 + 所有表结构变更通过 TypeORM migration 执行。 + + DB_LOGGING 是否打印 SQL(默认 false) + PORT API 监听端口(默认 3000) + STORAGE_DIR 上传文件存储目录(默认 ./storage,容器内 /data/storage) + UPLOAD_MAX_BYTES 单文件上限,默认 536870912(512 MiB) + ASSIMP_BIN 可选:Assimp 可执行文件,FBX/glTF 动作预处理需要 + CONTRACTS_DIR 可选:契约目录,设置后 /health 反映实时契约版本 + +---------------------------------------------------------------- +五、主要 API +---------------------------------------------------------------- + + GET /health 健康检查(含数据库连通性) + GET /api/songs 曲目列表(分页 / 检索) + POST /api/songs 新建曲目 + GET /api/songs/:id 曲目详情 + PATCH /api/songs/:id 更新曲目 + DELETE /api/songs/:id 删除曲目(软删除) + POST /api/songs/:id/audio 上传音频 + GET /api/motions 动作列表 + POST /api/motions 上传动作 + 来源信息 + POST /api/motions/:id/preprocess BVH/FBX → MediaPipe33 关键点 + GET /api/motions/:id/keypoints 下载关键点序列(NDJSON) + GET /api/models 模型列表 + POST /api/models 上传模型 + 来源信息(VRM 优先) + POST /api/models/:id/primary 设为全局默认模型 + GET /api/reviews 审核流水 / 待审列表 + POST /api/reviews/:type/:id/approve 审核通过 + POST /api/reviews/:type/:id/reject 审核驳回(必须填意见) + GET /api/catalog/export 客户端一次性拉取已发布目录 + GET /api/catalog/version 契约版本 + 目录指纹 + +完整契约(OpenAPI 3.0)见 contracts/rest-api.yaml。 + +上传文件通过 /static////.ext 访问, +相同内容只落盘一次(sha256 去重)。 + +---------------------------------------------------------------- +六、目录结构 +---------------------------------------------------------------- + + project.yaml 任务清单与状态 + DSH.md 本项目 DSH 执行约束 + tasks/ 单任务说明书(T-WEB-001 ~ 010) + contracts/ 跨端契约(权威,改接口先改这里) + web-server/ NestJS 服务端源码 + web-server/admin/ React 管理端 + deploy/ Docker Compose 部署(不含 MySQL) + THIRD_PARTY.md 第三方开源项目声明 + +---------------------------------------------------------------- +七、安全提示 +---------------------------------------------------------------- + +1. .env 已被 .gitignore 忽略,禁止提交;仓库中只保留 .env.example。 +2. 数据库账号使用最小权限,不授予 GRANT OPTION / FILE / SUPER。 +3. 本版本未内置管理端登录认证,请部署在内网或由反向代理提供鉴权。 +4. 上传的模型与动作必须填写来源五元组并通过审核后才对客户端可见。 +5. 未使用 SMPL 拓扑,统一 MediaPipe 33 点,规避非商用许可限制。 + +---------------------------------------------------------------- +八、第三方开源声明 +---------------------------------------------------------------- + +见 THIRD_PARTY.md,或访问 http://:8080/ 的「关于」页面。 diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md new file mode 100644 index 0000000..f4bf59f --- /dev/null +++ b/THIRD_PARTY.md @@ -0,0 +1,124 @@ +# 第三方开源项目声明 + +**OpenDance Studio Web 管理服务端 v0.1.0** + +本软件使用了以下开源项目,特此声明。完整锚点见 `contracts/open-source-credits.md` +(三端共用,任一端新增依赖须回到本项目更新该表)。 + +## 1. MediaPipe + +- 版权:Copyright 2023 Google LLC +- 许可证:Apache License 2.0 +- 源链接:https://github.com/google-ai-edge/mediapipe +- 使用端:Android 辅助 App(本项目仅使用其 **33 点骨骼拓扑定义**作为契约, + 不包含其代码) + +## 2. Three.js + +- 许可证:MIT +- 源链接:https://github.com/mrdoob/three.js +- 使用端:Web 管理端(模型 3D 预览) + +## 3. @pixiv/three-vrm + +- 许可证:MIT +- 源链接:https://github.com/pixiv/three-vrm +- 使用端:Web 管理端(VRM 加载与渲染) + +## 4. Assimp + +- 许可证:BSD-3-Clause +- 源链接:https://github.com/assimp/assimp +- 使用端:Web 服务端(**可选**外部依赖,FBX/glTF 动作 → BVH 转换)、Windows 客户端 + +## 5. Godot Engine + +- 版权:Copyright Godot Engine contributors +- 许可证:MIT +- 源链接:https://github.com/godotengine/godot +- 使用端:Windows 客户端 + +## 6. godot-vrm + +- 许可证:MIT +- 源链接:https://github.com/V-Sekai/godot-vrm +- 使用端:Windows 客户端 + +## 7. NestJS + +- 许可证:MIT +- 源链接:https://github.com/nestjs/nest +- 使用端:Web 服务端 + +## 8. TypeORM + +- 许可证:MIT +- 源链接:https://github.com/typeorm/typeorm +- 使用端:Web 服务端 + +## 9. OkHttp + +- 许可证:Apache License 2.0 +- 源链接:https://github.com/square/okhttp +- 使用端:Android 辅助 App + +## 10. bvh + +- 许可证:MIT +- 源链接:https://github.com/aimoonchen/bvh +- 使用端:Web 服务端(BVH 解析参考实现)、Android 辅助 App + +--- + +## 补充声明(实际引入的构建期/运行时依赖) + +以下为文档模板 10 项之外、本项目实际引入的依赖,一并声明以保证完整性。 + +## 11. React + +- 许可证:MIT +- 源链接:https://github.com/facebook/react +- 使用端:Web 管理端 + +## 12. Vite + +- 许可证:MIT +- 源链接:https://github.com/vitejs/vite +- 使用端:Web 管理端(构建工具) + +## 13. mysql2 + +- 许可证:MIT +- 源链接:https://github.com/sidorares/node-mysql2 +- 使用端:Web 服务端(MySQL/MariaDB 驱动) + +## 14. class-validator / class-transformer + +- 许可证:MIT +- 源链接:https://github.com/typestack/class-validator , + https://github.com/typestack/class-transformer +- 使用端:Web 服务端(请求校验与转换) + +## 15. CameraX + +- 许可证:Apache License 2.0 +- 源链接:https://developer.android.com/jetpack/androidx/releases/camera +- 使用端:Android 辅助 App + +## 16. Jetpack Compose + +- 许可证:Apache License 2.0 +- 源链接:https://developer.android.com/jetpack/compose +- 使用端:Android 辅助 App + +--- + +## 许可合规说明 + +1. 本项目自身代码以 MIT 许可发布。 +2. 上述依赖的完整许可证文本随各依赖包的 `LICENSE` 文件分发;发布产物中 + `web-server/node_modules/` 内已包含各依赖的许可证副本。 +3. **未使用 SMPL 模型及其衍生拓扑**,统一采用 MediaPipe 33 点拓扑, + 以规避 SMPL 的非商用许可限制。 +4. 上传的模型与动作资产必须填写来源五元组(来源类型、名称、链接、许可、作者), + 并在管理端审核界面展示、由审核流程校验,拒绝侵权内容入库。 diff --git a/project.yaml b/project.yaml new file mode 100644 index 0000000..9da27f6 --- /dev/null +++ b/project.yaml @@ -0,0 +1,71 @@ +project: opendance-web-server +version: 0.1.0 +repository: opendance-web +default_branch: main +contracts_version: 0.1.0 + +# 契约变更流程(文档 §7.2): +# 任何跨端契约变更必须先在本项目修改 contracts/ 并提升版本, +# 再在 opendance-windows / opendance-android 创建适配任务。 + +database: + # 使用现有实例,本项目不部署、不管理 MySQL + managed_by: external + engine: MySQL 兼容(当前部署为 MariaDB 10.11.19) + name: dpendance + charset: utf8mb4 + migrations: TypeORM migration + synchronize: forbidden + +tasks: + - id: T-WEB-001 + file: tasks/001-scaffold.md + scope: NestJS 骨架 + 连接现有 MySQL + status: done + - id: T-WEB-002 + file: tasks/002-db-migration.md + scope: TypeORM 迁移生成 opendance 表结构 + status: done + - id: T-WEB-003 + file: tasks/003-song-crud.md + scope: /api/songs CRUD + status: done + - id: T-WEB-004 + file: tasks/004-motion-upload.md + scope: 动作数据上传 + 来源字段 + status: done + - id: T-WEB-005 + file: tasks/005-model-upload.md + scope: 模型上传 + VRM 优先 + 来源字段 + status: done + - id: T-WEB-006 + file: tasks/006-keypoint-preprocess.md + scope: BVH/FBX → 关键点序列预处理 + status: done + - id: T-WEB-007 + file: tasks/007-upload-review.md + scope: 上传审核流程 + status: done + - id: T-WEB-008 + file: tasks/008-admin-ui-songs.md + scope: 歌曲管理界面 + status: done + - id: T-WEB-009 + file: tasks/009-admin-ui-models.md + scope: 模型管理界面 + 来源展示 + status: done + - id: T-WEB-010 + file: tasks/010-deploy.md + scope: 部署脚本(不部署 MySQL) + status: done + +deliverable: + artifact: opendance-web-v0.1.0.tar.gz + packaging: Docker Compose(不含 MySQL) + includes: + - web-server/ # NestJS 源码 + 构建产物 + - admin/ # React 管理端构建产物 + - deploy/ # docker-compose.yml / install.sh / .env.example + - contracts/ # 当前版本契约快照 + - THIRD_PARTY.md # 开源声明 + - README.txt # 使用说明 diff --git a/scripts/package-release.sh b/scripts/package-release.sh new file mode 100755 index 0000000..5864a19 --- /dev/null +++ b/scripts/package-release.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# ============================================================ +# 打包发布产物:opendance-web-v0.1.0.tar.gz +# +# 产物结构(文档 §4.4): +# web-server/ NestJS 源码 + 构建产物 +# admin/ React 管理端构建产物 +# deploy/ docker-compose.yml / install.sh / .env.example +# contracts/ 当前版本契约快照 +# THIRD_PARTY.md 开源项目声明 +# README.txt 使用说明 +# +# 用法:./scripts/package-release.sh [版本号] +# ============================================================ +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +VERSION="${1:-0.1.0}" +NAME="opendance-web-v${VERSION}" +OUT_DIR="$REPO_ROOT/dist-release" +STAGE="$OUT_DIR/$NAME" + +log() { printf '\033[1;34m[package]\033[0m %s\n' "$*"; } +die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; } + +# ---------- 1. 契约一致性 ---------- +log "校验契约版本一致性..." +( cd web-server && node scripts/check-contracts.mjs ) + +# ---------- 2. 后端构建 ---------- +log "构建后端..." +( cd web-server && npm run build ) + +# ---------- 3. 管理端构建 ---------- +if [ -d web-server/admin/node_modules ]; then + log "构建管理端..." + ( cd web-server/admin && npm run build ) +else + log "警告:web-server/admin/node_modules 不存在,跳过管理端构建(产物将缺少 admin/)" +fi + +# ---------- 4. 组织产物目录 ---------- +log "整理产物目录 $STAGE ..." +rm -rf "$STAGE" +mkdir -p "$STAGE" + +# 后端源码 + 构建产物(排除依赖、测试、存储、密钥) +rsync -a --delete \ + --exclude 'node_modules' \ + --exclude 'storage' \ + --exclude 'data' \ + --exclude '.env' \ + --exclude '.env.local' \ + --exclude 'admin' \ + web-server/ "$STAGE/web-server/" + +# 管理端构建产物(文档要求 admin/ 与 web-server/ 平级) +if [ -d web-server/admin/dist ]; then + mkdir -p "$STAGE/admin" + cp -r web-server/admin/dist/. "$STAGE/admin/" + cp web-server/admin/nginx.conf "$STAGE/admin/" 2>/dev/null || true +else + log "警告:未找到 web-server/admin/dist,产物不含 admin/" +fi + +cp -r deploy "$STAGE/deploy" +rm -f "$STAGE/deploy/.env" +cp -r contracts "$STAGE/contracts" +cp THIRD_PARTY.md README.txt project.yaml DSH.md "$STAGE/" +cp -r tasks "$STAGE/tasks" + +# ---------- 5. 打包 ---------- +log "生成压缩包..." +( cd "$OUT_DIR" && tar -czf "$NAME.tar.gz" "$NAME" ) + +log "完成:$OUT_DIR/$NAME.tar.gz" +echo +echo "产物内容:" +tar -tzf "$OUT_DIR/$NAME.tar.gz" | head -20 +echo " ...(共 $(tar -tzf "$OUT_DIR/$NAME.tar.gz" | wc -l) 项)" +echo +echo "部署方式:解压后进入 deploy/,复制 .env.example 为 .env 并填写数据库信息,执行 ./install.sh" diff --git a/tasks/001-scaffold.md b/tasks/001-scaffold.md new file mode 100644 index 0000000..f9279c5 --- /dev/null +++ b/tasks/001-scaffold.md @@ -0,0 +1,63 @@ +# T-WEB-001 骨架 + 连接现有 MySQL + +- **状态**:done +- **依赖**:无 + +## 目标 + +建立 NestJS 服务端骨架,并**连接部署方提供的现有 MySQL 实例**。本项目不部署、 +不管理 MySQL。 + +## load + +``` +DSH.md +contracts/database.md +web-server/package.json +web-server/src/main.ts +web-server/src/app.module.ts +web-server/src/config/configuration.ts +web-server/src/db/data-source.ts +``` + +## 交付物 + +| 文件 | 说明 | +|---|---| +| `web-server/package.json` | NestJS 11 + TypeORM 0.3 + mysql2 依赖与脚本 | +| `web-server/src/main.ts` | 启动引导:环境校验、全局校验管道、CORS、静态资源、优雅关闭 | +| `web-server/src/app.module.ts` | 根模块,`TypeOrmModule.forRootAsync` 由 `AppConfig` 注入连接信息 | +| `web-server/src/config/configuration.ts` | 类型化配置层,唯一读取 `process.env` 的位置 | +| `web-server/src/config/config.module.ts` | 全局配置模块 | +| `web-server/src/db/data-source.ts` | TypeORM DataSource(CLI + 运行期共用),`synchronize: false` | +| `web-server/src/health/health.controller.ts` | `GET /health`,真实探测数据库连通性 | +| `web-server/.env.example` | 环境变量样例 | + +## 关键实现 + +- `synchronize` 在代码中恒为 `false`,不读取环境变量覆盖,从结构上杜绝生产误开。 +- `validateEnv()` 在启动时检查必填项,生产环境检测到 `DB_SYNCHRONIZE=true` 直接 + 以退出码 1 中止。 +- `/health` 返回数据库版本、库名、待执行迁移数、存储目录、Assimp 可用性、契约版本; + 数据库不可用时返回 **503**。 + +## 验收 + +```bash +cd web-server +cp .env.example .env # 填写 DB_* 后 +npm install +npm run start:prod +curl -s http://localhost:3000/health +``` + +期望:`status=ok`、`database.connected=true`、`database.server` 形如 +`10.11.19-MariaDB-...`。 + +> **环境差异记录**:文档假设 MySQL 8.x,实际部署实例为 **MariaDB 10.11.19**。 +> 已确认 TypeORM `mysql` 驱动(mysql2)兼容,SQL 限制在两者共同子集内, +> 详见 `contracts/database.md`。 + +## 结果 + +- 已连通 `192.168.150.10:3306/dpendance`,`/health` 返回 200 且 `connected=true`。 diff --git a/tasks/002-db-migration.md b/tasks/002-db-migration.md new file mode 100644 index 0000000..b6609b0 --- /dev/null +++ b/tasks/002-db-migration.md @@ -0,0 +1,57 @@ +# T-WEB-002 TypeORM 迁移生成表结构 + +- **状态**:done +- **依赖**:T-WEB-001 + +## 目标 + +用 TypeORM migration 在现有实例的 `dpendance` 库中建立全部表结构。不使用 +`DB_SYNCHRONIZE`,迁移必须可逆。 + +## load + +``` +DSH.md +contracts/database.md +contracts/data-models.md +web-server/src/entities/*.entity.ts +web-server/src/migrations/*.ts +web-server/src/db/data-source.ts +``` + +## 表结构 + +| 表 | 用途 | 关键列 | +|---|---|---| +| `assets` | 上传文件元数据 | `sha256` 唯一索引(去重)、`stored_path`、`size` | +| `songs` | 曲目 | 来源五元组、`status`、`deleted_at` | +| `motions` | 动作数据 | `song_id` FK(ON DELETE SET NULL)、预处理产物列 | +| `models` | 角色模型 | `format`、`vrm_version`、`is_primary` | +| `review_records` | 审核流水 | `(target_type, target_id)` 复合索引 | + +统一约定(`contracts/database.md`): + +- 表名 `snake_case` 复数;主键 `BIGINT AUTO_INCREMENT` +- 时间列 `DATETIME`;软删除 `deleted_at` +- Tags 使用 `simple-json`(MariaDB 下落 LONGTEXT,规避 JSON 类型差异) + +## 交付物 + +- `web-server/src/entities/{asset,song,motion,model,review-record}.entity.ts` +- `web-server/src/migrations/1789382903060-InitSchema.ts`(由 `migration:generate` + 对真实库生成,保证与实体零漂移) +- `web-server/src/db/data-source.ts` 的 `migrations` 通配配置 + +## 验收 + +```bash +cd web-server +npm run migration:run # 1 migrations were executed successfully +npm run migration:show # [X] 1 InitSchema1789382903060 +npm run migration:revert && npm run migration:run # 验证可逆 +``` + +## 结果 + +- 迁移已在 `dpendance` 库执行成功,创建 5 张业务表 + `typeorm_migrations`。 +- `down()` 完整删除索引、外键与表,可逆性通过 `migration:revert` 验证。 diff --git a/tasks/003-song-crud.md b/tasks/003-song-crud.md new file mode 100644 index 0000000..b7c6882 --- /dev/null +++ b/tasks/003-song-crud.md @@ -0,0 +1,52 @@ +# T-WEB-003 /api/songs CRUD + +- **状态**:done +- **依赖**:T-WEB-002 + +## 目标 + +曲目的增删改查与分页检索,字段语义符合 `contracts/data-models.md` §2。 + +## load + +``` +DSH.md +contracts/rest-api.yaml # 锚点:#/paths/~1api~1songs +contracts/data-models.md # 锚点:§1 通用约定、§2 Song +web-server/src/songs/* +web-server/src/common/source.dto.ts +web-server/src/common/pagination.ts +``` + +## 端点 + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/songs` | 分页 + `q`/`artist`/`status` 过滤 | +| POST | `/api/songs` | 新建(来源五元组必填) | +| GET | `/api/songs/:id` | 详情 | +| PATCH | `/api/songs/:id` | 更新(来源需五元组同时提交) | +| DELETE | `/api/songs/:id` | **软删除** | +| POST | `/api/songs/:id/audio` | 上传音频(multipart,字段名 `file`) | +| POST | `/api/songs/:id/cover` | 上传封面 | + +## 关键实现 + +- `likePattern()` 转义 `%` `_` `\`,用户输入不会被当作 LIKE 模式。 +- `assertSourceTupleComplete()` 实现「全有或全无」:PATCH 只给部分来源字段 → 400。 +- PATCH 直接改 `status` 时校验状态机;非法流转 → 409 并提示改用 `/api/reviews`。 +- `toDtos()` 批量解析 asset → URL,避免 N+1。 + +## 验收 + +```bash +npm run test:smoke +``` + +冒烟脚本 [1] 段落覆盖:创建 201、id 为 number、来源回显、tags 往返、bpm 为 number、 +分页结构、模糊查询命中、音频上传后 `audioUrl` 以 `/static/` 开头、PATCH 生效、 +部分来源字段 400、软删除后不再出现在列表。 + +## 结果 + +全部通过(见 `web-server/test/smoke.mjs`)。 diff --git a/tasks/004-motion-upload.md b/tasks/004-motion-upload.md new file mode 100644 index 0000000..3cb70cb --- /dev/null +++ b/tasks/004-motion-upload.md @@ -0,0 +1,56 @@ +# T-WEB-004 动作数据上传 + 来源字段 + +- **状态**:done +- **依赖**:T-WEB-002 + +## 目标 + +上传动作文件(BVH / FBX / glTF / GLB / keypoints-json),**强制填写来源五元组**。 + +## load + +``` +DSH.md +contracts/rest-api.yaml # 锚点:#/paths/~1api~1motions +contracts/data-models.md # 锚点:§3 Motion +contracts/model-formats.md # 锚点:§2 动作格式、§3 上传限制 +web-server/src/motions/* +web-server/src/storage/storage.service.ts +``` + +## 端点 + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/motions` | 分页 + `q`/`songId`/`format`/`status` 过滤 | +| POST | `/api/motions` | multipart 上传 | +| GET | `/api/motions/:id` | 详情 | +| PATCH | `/api/motions/:id` | 更新元数据(含 `detachSong`) | +| DELETE | `/api/motions/:id` | 软删除 | + +## 存储策略(`contracts/model-formats.md` §3) + +``` +${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext} +``` + +- 上传先落 `${STORAGE_DIR}/tmp`(multer diskStorage),避免大文件占用内存 +- `StorageService` 流式计算 sha256;**相同内容只落盘一次**,复用 `assets` 记录 +- 并发同文件上传靠 `assets.sha256` 唯一索引兜底,冲突后回退到已存在记录 +- 路径永不使用原始文件名;`absolutePath()` 拒绝逃逸存储根目录 + +## 校验 + +- 扩展名白名单:`.bvh .fbx .gltf .glb .json`,其他 → 400 +- 来源五元组缺任一项 → 400 +- `songId` 指向不存在的曲目 → 400 +- 单文件上限 `UPLOAD_MAX_BYTES`(默认 512 MiB) + +## 验收 + +```bash +npm run test:smoke +``` + +冒烟 [2] 段落:上传 201、自动识别 `format=bvh`、`fileSize` 记录、缺来源 400、 +`evil.exe` 400。 diff --git a/tasks/005-model-upload.md b/tasks/005-model-upload.md new file mode 100644 index 0000000..6d572e8 --- /dev/null +++ b/tasks/005-model-upload.md @@ -0,0 +1,61 @@ +# T-WEB-005 模型上传 + VRM 优先 + 来源字段 + +- **状态**:done +- **依赖**:T-WEB-002 + +## 目标 + +上传角色模型,**VRM 优先**,识别 VRM 版本,并维护全局默认模型。 + +## load + +``` +DSH.md +contracts/rest-api.yaml # 锚点:#/paths/~1api~1models +contracts/data-models.md # 锚点:§4 Model、§4.1 VRM 优先规则 +contracts/model-formats.md # 锚点:§1 角色模型 +web-server/src/models/* +web-server/src/models/vrm-inspector.ts +``` + +## 端点 + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/models` | 分页 + `q`/`format`/`status`/`primaryOnly` | +| POST | `/api/models` | multipart 上传(`file` + 名称 + 来源五元组 + `isPrimary`) | +| GET | `/api/models/:id` | 详情 | +| PATCH | `/api/models/:id` | 更新 | +| DELETE | `/api/models/:id` | 软删除 | +| POST | `/api/models/:id/primary` | 设为全局默认模型 | + +## VRM 优先规则(`contracts/data-models.md` §4.1,已实现于 `resolveDefaultModelId()`) + +``` +1. 显式 modelId(客户端侧) +2. format='vrm' 且 published 且 is_primary=1 +3. format='vrm' 且 published,按 id 升序取第一个 +4. published,按 (format='vrm' DESC, id ASC) 取第一个 +5. 无 → 客户端使用内置占位模型 +``` + +`POST /:id/primary` 在**同一事务**内先清零其余 `is_primary`,保证互斥。 + +## VRM 版本识别 + +`vrm-inspector.ts` 直接读取 GLB 头部 JSON chunk,不依赖完整 3D 库: + +- `extensions.VRMC_vrm` → VRM **1.0**(取 `specVersion`) +- `extensions.VRM` → VRM **0.x** +- 两者皆无 → `null`(普通 GLB) + +## 验收 + +```bash +npm run test # test/vrm-inspector.spec.ts,6 例 +npm run test:smoke # 冒烟 [4] 段落 +``` + +覆盖:真实 GLB 构造下识别 1.0 / 0.0 / null、文本 glTF 不误判、过小文件报错、 +非 JSON 首 chunk 报错、上传 201、`isPrimary` 置位、缺来源 400、按格式过滤列表、 +列表含来源信息(管理端展示用)。 diff --git a/tasks/006-keypoint-preprocess.md b/tasks/006-keypoint-preprocess.md new file mode 100644 index 0000000..6794bcb --- /dev/null +++ b/tasks/006-keypoint-preprocess.md @@ -0,0 +1,67 @@ +# T-WEB-006 BVH/FBX → 关键点序列预处理 + +- **状态**:done +- **依赖**:T-WEB-004 + +## 目标 + +把上传的 3D 动作数据转换为与 Android 端输出**同构**的 MediaPipe 33 点关键点序列, +使 Windows 端 DTW 引擎只有一条输入通路。 + +## load + +``` +DSH.md +contracts/model-formats.md # 锚点:§2 动作格式、§2.1 预处理产物、§2.2 关节名映射 +contracts/skeleton-topology.md # 锚点:§1 33 点、§3 父节点表、§5 关节角 +web-server/src/preprocess/* +``` + +## 端点 + +| 方法 | 路径 | 说明 | +|---|---|---| +| POST | `/api/motions/:id/preprocess` | 执行预处理,返回更新后的 Motion | +| GET | `/api/motions/:id/keypoints` | 下载 NDJSON 关键点序列 | + +## 实现分层 + +| 文件 | 职责 | +|---|---| +| `bvh-parser.ts` | 纯 TS BVH 词法/语法解析 + 逐帧前向运动学(FK) | +| `math3d.ts` | 向量/矩阵工具、旋转合成、关节角余弦 | +| `joint-map.ts` | BVH 关节名 → MediaPipe 点索引的默认映射表 | +| `retarget.ts` | 单帧解算 35 项(33 点 + neck/pelvis 虚拟关节)→ 骨盆归零 → 自适应缩放 → 投影 | +| `keypoint-codec.ts` | 239 字节二进制帧编解码、NDJSON 序列化/解析、关键点 JSON 校验 | +| `assimp.ts` | 可选外部 Assimp 调用(FBX/glTF → BVH) | +| `preprocess.service.ts` | 编排、落盘、状态与错误记录 | + +## 拓扑与缩放(契约冻结) + +- 33 点顺序与父节点表来自 `contracts/skeleton-topology.md`,虚拟关节 + `neck=33`、`pelvis=34` 只参与关节角计算,**不进入输出序列**。 +- 投影:`x_img = 0.5 + x·s`,`y_img = 0.5 − y·s`,`z_img = −z·s`, + `s = 0.35 / max(|x|,|y|,|z|)`(按整段动作自适应,保证不出画)。 +- 置信度分层:直接映射 **1.0**、按层级推算 **0.5**、无法推算 **0.0**。 + +## 格式支持 + +| 格式 | 处理 | +|---|---| +| `.bvh` | 内置解析器,无外部依赖 | +| `.json` | 直通校验与规范化(须符合 `mediapipe33@0.1.0`) | +| `.fbx` `.gltf` `.glb` | 调用可选 Assimp 转 BVH;未安装 Assimp 时写入可读错误并置 `preprocessState=failed` | + +## 验收 + +```bash +npm run test # 25 例:解析 / FK / 重定向 / 编解码 / 拓扑 / 契约版本 +npm run test:smoke # 冒烟 [3] 段落 +``` + +覆盖:60 帧样例经预处理后 **60 帧 × 33 点 × 9 关节角**、坐标落在 +MediaPipe 归一化范围、置信度分层、时间戳按帧率推进、序列随时间变化、 +NDJSON 往返、解码器拒绝错误长度与 magic。 + +样例数据由 `test/fixtures/generate-sample-bvh.mjs` 生成(60 帧挥手 + 深蹲, +21 关节 / 63 通道),可复现。 diff --git a/tasks/007-upload-review.md b/tasks/007-upload-review.md new file mode 100644 index 0000000..385cb9a --- /dev/null +++ b/tasks/007-upload-review.md @@ -0,0 +1,55 @@ +# T-WEB-007 上传审核流程 + +- **状态**:done +- **依赖**:T-WEB-003 / 004 / 005 + +## 目标 + +对歌曲、动作、模型三类资产实施统一的状态机与审核流水,确保只有审核通过的资产 +被客户端消费。 + +## load + +``` +DSH.md +contracts/rest-api.yaml # 锚点:#/paths/~1api~1reviews +contracts/data-models.md # 锚点:§1.2 PublishStatus、§5 ReviewRecord +contracts/database.md # 锚点:状态机 +web-server/src/reviews/* +``` + +## 状态机(契约冻结) + +``` +draft ──submit──▶ pending ──approve──▶ published + │ + └──reject──▶ rejected ──submit──▶ pending +``` + +| 方法 | 路径 | 说明 | +|---|---|---| +| GET | `/api/reviews` | 不传 `status` → 审核流水;传 `status` → 该状态待审资产(三类合并) | +| GET | `/api/reviews/:targetType/:id/history` | 单个资产的审核历史 | +| POST | `/api/reviews/:targetType/:id/submit` | → `pending` | +| POST | `/api/reviews/:targetType/:id/approve` | → `published` | +| POST | `/api/reviews/:targetType/:id/reject` | → `rejected`,**必须填写意见** | +| POST | `/api/reviews/:targetType/:id/withdraw` | → `draft` | + +## 关键实现 + +- `STATUS_TRANSITIONS` 是状态机的唯一来源;非法流转一律 **409**, + 重复审核同一状态同样 409。 +- 驳回未填 `comment` → **400**(合规要求:拒绝侵权内容需留痕)。 +- 每次成功流转都写一条 `review_records`,记录 `fromStatus` / `toStatus` / + `reviewer` / `comment`。 +- 三类资产的直连 PATCH 也会校验状态机,避免绕过审核流程。 +- `/api/catalog/export` 与 `/api/catalog/version` 只导出 `published`。 + +## 验收 + +```bash +npm run test:smoke +``` + +冒烟 [5] 段落:submit → pending、待审列表包含、驳回无意见 400、approve → published、 +重复审核 409、流水恰好 2 条。 diff --git a/tasks/008-admin-ui-songs.md b/tasks/008-admin-ui-songs.md new file mode 100644 index 0000000..e46e319 --- /dev/null +++ b/tasks/008-admin-ui-songs.md @@ -0,0 +1,53 @@ +# T-WEB-008 歌曲管理界面 + +- **状态**:done +- **依赖**:T-WEB-003 + +## 目标 + +React 管理端的歌曲管理页面:列表、检索、新建/编辑、音频上传与试听、删除。 + +## load + +``` +DSH.md +contracts/rest-api.yaml # 锚点:#/paths/~1api~1songs、#/components/schemas/Song +contracts/data-models.md # 锚点:§2 Song +web-server/admin/src/** +web-server/admin/package.json +``` + +## 交付物 + +`web-server/admin/`(React 18 + Vite + TypeScript): + +| 路径 | 说明 | +|---|---| +| `src/pages/SongsPage.tsx` | 歌曲列表 + 分页 + 检索(`q` / `status` / `artist`) | +| `src/components/SongForm*` | 新建/编辑表单,含来源五元组 | +| `src/api/client.ts` | fetch 封装;非 2xx 抛 `ApiError`,兼容 Nest 的 `message: string[]` | +| `src/api/types.ts` | 与契约对齐的 TypeScript 类型 | +| `src/styles.css` | 无 UI 框架,纯 CSS | + +## 页面能力 + +- 分页表格 + 关键词/状态/歌手过滤 +- 新建与编辑:曲名、歌手、专辑、语言、时长(秒输入 → 毫秒提交)、BPM、 + 难度 1–5、标签(逗号分隔)、状态、**来源五元组** +- 音频上传(`POST /api/songs/:id/audio`,字段名 `file`)与 `