docs(web): 任务说明、DSH 约束与交付文档
This commit is contained in:
@@ -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`。
|
||||
@@ -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` 验证。
|
||||
@@ -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`)。
|
||||
@@ -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。
|
||||
@@ -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、按格式过滤列表、
|
||||
列表含来源信息(管理端展示用)。
|
||||
@@ -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 通道),可复现。
|
||||
@@ -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 条。
|
||||
@@ -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`)与 `<audio controls>` 试听
|
||||
- 删除需二次确认(软删除)
|
||||
- 客户端校验:曲名/歌手与来源五项必填,`sourceUrl` 必须以 `http` 开头
|
||||
- 加载中 / 空列表 / 请求失败三态均有明确反馈,绝不出现白屏
|
||||
|
||||
## 验收
|
||||
|
||||
```bash
|
||||
cd web-server/admin
|
||||
npm run build # tsc -b && vite build 必须成功
|
||||
```
|
||||
|
||||
## 构建与运行
|
||||
|
||||
- 开发:`npm run dev`(5173,`/api` 与 `/health` 代理到 `localhost:3000`)
|
||||
- 生产:多阶段 Dockerfile 构建后由 nginx 托管,`/api` 反代到 `api:3000`
|
||||
- `VITE_API_BASE` 可覆盖 API 基地址;留空表示同源
|
||||
@@ -0,0 +1,49 @@
|
||||
# T-WEB-009 模型管理界面 + 来源展示
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-WEB-005
|
||||
|
||||
## 目标
|
||||
|
||||
React 管理端的模型管理页面:列表、上传、**来源信息完整展示**、VRM 优先设置、
|
||||
3D 预览。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
contracts/rest-api.yaml # 锚点:#/paths/~1api~1models
|
||||
contracts/data-models.md # 锚点:§4 Model、§4.1 VRM 优先规则
|
||||
web-server/admin/src/pages/ModelsPage.tsx
|
||||
web-server/admin/src/components/VrmPreview.tsx
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
| 路径 | 说明 |
|
||||
|---|---|
|
||||
| `src/pages/ModelsPage.tsx` | 模型列表 + 上传表单 + 设为默认 |
|
||||
| `src/components/VrmPreview.tsx` | three.js + @pixiv/three-vrm 的 3D 预览面板 |
|
||||
|
||||
## 页面能力
|
||||
|
||||
- 表格列:预览缩略图、名称、格式(VRM 高亮徽章)、VRM 版本、是否默认、
|
||||
**来源五元组**、许可、状态、操作
|
||||
- **来源展示(本任务验收重点)**:整行展示 `sourceType` + `sourceName` + `author`
|
||||
+ `license`,`sourceUrl` 可点击外链跳转
|
||||
- 上传:`POST /api/models`(multipart `file` + 名称 + `isPrimary` + 来源五元组 +
|
||||
状态),客户端校验与歌曲页一致
|
||||
- 「设为默认」→ `POST /api/models/:id/primary`,成功后刷新列表以反映互斥结果
|
||||
- 3D 预览:`GLTFLoader` + `VRMLoaderPlugin` 加载 `fileUrl`,
|
||||
支持鼠标拖拽旋转与滚轮缩放,正确处理 VRM 0.x / 1.0 元数据差异、
|
||||
未选择模型、加载失败三种状态,卸载时释放资源
|
||||
|
||||
## 验收
|
||||
|
||||
```bash
|
||||
cd web-server/admin
|
||||
npm run build # 必须成功
|
||||
```
|
||||
|
||||
另由 `npm run test:smoke`([4] 段落)验证后端确实在列表接口返回完整来源字段,
|
||||
供本页面展示。
|
||||
@@ -0,0 +1,87 @@
|
||||
# T-WEB-010 部署脚本(不部署 MySQL)
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-WEB-001 ~ 009
|
||||
|
||||
## 目标
|
||||
|
||||
Web 服务端可在干净 Linux 服务器上**一键部署**,连接**现有 MySQL 实例**。
|
||||
部署脚本**不创建、不管理 MySQL 容器**。
|
||||
|
||||
## load
|
||||
|
||||
```
|
||||
DSH.md
|
||||
contracts/database.md
|
||||
deploy/docker-compose.yml
|
||||
deploy/install.sh
|
||||
deploy/.env.example
|
||||
web-server/Dockerfile
|
||||
web-server/admin/Dockerfile
|
||||
```
|
||||
|
||||
## 交付物
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `deploy/docker-compose.yml` | 两个服务:`api`(NestJS)+ `admin`(nginx 静态站点)。**无 mysql 服务** |
|
||||
| `deploy/install.sh` | 前置检查 → 构建 → 迁移 → 启动 → 健康检查 |
|
||||
| `deploy/uninstall.sh` | 停止服务(默认保留数据卷) |
|
||||
| `deploy/.env.example` | 部署环境变量样例 |
|
||||
| `web-server/Dockerfile` | 多阶段构建,非 root 运行,内置 HEALTHCHECK |
|
||||
| `web-server/admin/Dockerfile` | 多阶段:Node 构建 → nginx 托管 |
|
||||
| `scripts/package-release.sh` | 生成 `opendance-web-v0.1.0.tar.gz` |
|
||||
|
||||
## compose 要点
|
||||
|
||||
- 数据库连接全部来自 `.env`;`DB_SYNCHRONIZE` 在 compose 中**硬编码为 `"false"`**
|
||||
- `api` 的 `CONTRACTS_DIR=/contracts`,只读挂载仓库 `contracts/`,
|
||||
使 `/health` 与 `/api/catalog/version` 能反映实时契约版本
|
||||
- 上传文件持久化到命名卷 `opendance-storage` → `/data/storage`
|
||||
- `admin` 依赖 `api` 的健康状态(`condition: service_healthy`)
|
||||
- `admin` 的 nginx 将 `/api/` 与 `/health` 反代到 `http://api:3000`
|
||||
|
||||
## 迁移执行方式
|
||||
|
||||
`install.sh` 调用 `docker compose run --rm api npm run migration:run`。
|
||||
|
||||
`web-server/scripts/migrate.mjs` 会自动选择执行方式:
|
||||
|
||||
- 存在 `dist/db/data-source.js` → `typeorm -d dist/db/data-source.js migration:run`
|
||||
(生产镜像已 `npm prune --omit=dev`,无需 ts-node)
|
||||
- 否则 → `typeorm-ts-node-commonjs -d src/db/data-source.ts migration:run`(开发)
|
||||
|
||||
因此同一条命令在本地与容器内行为一致。
|
||||
|
||||
## 验收(文档 §4.4 T-WEB-010)
|
||||
|
||||
```bash
|
||||
cp deploy/.env.example deploy/.env # 填写 DB_*
|
||||
./deploy/install.sh
|
||||
```
|
||||
|
||||
| 验收项 | 状态 |
|
||||
|---|---|
|
||||
| 干净 Linux 服务器执行 `deploy/install.sh` | ✅ 脚本就绪(本机无 Docker,未能实机执行,见下) |
|
||||
| 连接现有 MySQL 成功 | ✅ 已用真实实例验证(`/health` 200) |
|
||||
| TypeORM migration 执行成功,表结构完整 | ✅ 5 张业务表已创建并验证 |
|
||||
| `curl http://localhost:3000/health` 通过 | ✅ 返回 200 + `connected: true` |
|
||||
| Admin 页面可访问 | ✅ 前端 `npm run build` 通过,nginx 配置就绪 |
|
||||
| 不创建、不管理 MySQL 容器 | ✅ compose 中无 mysql 服务,镜像内无数据库 |
|
||||
|
||||
> **环境限制说明**:本次执行环境的宿主机**未安装 Docker**(无 `docker` /
|
||||
> `docker-compose`,无 `/var/run/docker.sock`),因此 `install.sh` 的
|
||||
> `docker compose up` 路径未能实机跑通。已通过的等价验证:
|
||||
> 应用以 `node dist/main.js` 直连真实数据库启动,`/health` 200,
|
||||
> `npm run migration:run` 成功,端到端冒烟 57 项全通过。
|
||||
> 需要在有 Docker 的机器上补做一次 `install.sh` 实机验收。
|
||||
|
||||
## 发布产物
|
||||
|
||||
```bash
|
||||
./scripts/package-release.sh
|
||||
# → dist-release/opendance-web-v0.1.0.tar.gz
|
||||
```
|
||||
|
||||
内容:`web-server/`(源码 + dist)、`admin/`(构建产物)、`deploy/`、
|
||||
`contracts/` 快照、`THIRD_PARTY.md`、`README.txt`。
|
||||
Reference in New Issue
Block a user