Files
opendance-web/tasks/015-scene-management.md
T
zijunchiang f3b97fec53 fix(scenes): 场景表不可用不再拖垮整站(生产事故修复)+ 诊断脚本
现场(真实部署 v0.7.2):数据库缺 scenes 表,于是
  /api/scenes、/api/scenes/:id、/api/catalog/export、/api/catalog/version、
  /api/reviews?status=pending 全部 500,而 /api/songs、/api/motions、/api/models
  正常。后果不是"场景不可用",而是客户端拉不到任何目录(Windows/Android 全废)、
管理端待审列表打不开。`/health` 却显示 database.connected=true、
migrationsPending=0 —— 因为 DataSource.showMigrations() 只比较**代码里加载到的**
迁移;部署的 dist 若缺 AddScenes.js,该迁移不在比较集合里,于是"没有待执行迁移"
但表不存在(最隐蔽的一种不一致)。

修复思路:契约与文档都写明「场景是可选资源」(没有可用场景时客户端回退内置空
场景),所以场景表的问题不允许影响与场景无关的功能 —— 改为降级:

- ScenesService 新增 queryScenes() 包装:数据库层面失败时返回空结果,**只记一次
  日志**(避免刷屏),原因写入 diagnostics();下一次查询成功即自动复位;
- ReviewsService.listByStatus() 对场景查询单独 catch,待审列表照常返回其它资源;
- /health 新增 scenes: { available, error } —— 降级但绝不静默;
- HealthModule 增加 imports: [ScenesModule](HealthController 注入了 ScenesService,
  Nest 的 DI 按模块解析,漏了会直接 UnknownDependenciesException;
  scripts/check-bootstrap.mjs 已能拦住这一类)。

新增 web-server/scripts/diagnose-scenes.mjs(默认只读):打印运行期真实连的库、
表清单、typeorm_migrations 记录、**代码实际加载的迁移**、scenes 实际列 vs 实体
期望列的逐列 diff;`--fix` 仅在缺表时用迁移里同一份 DDL 幂等补表。

顺带修正 models.controller.ts 的误导性注释:模型上传**不接受** isPrimary/status
(契约:上传一律 pending,默认模型由审核通过时带 setPrimary=true 或
POST /api/models/:id/primary 设定)。test/smoke.mjs 此前正是按错误预期断言
"isPrimary 已置位",已改为:上传断言 pending + isPrimary=false → 审核通过时带
setPrimary=true → 再断言 isPrimary=true;同时给目录导出/指纹等检查补上失败细节
(HTTP 状态 + 响应体),并把待审列表查询的 pageSize 放大到 200(避免库里待审项多
时新条目被挤出第一页)。

文档:README.md/.txt 增加 v0.7.3 修订说明;tasks/015-scene-management.md 增
「缺陷修复记录二」;开发文档 §4.12 补充硬性要求(场景不可用必须降级 + /health 暴露
+ showMigrations 的陷阱说明)。

测试:新增 test/scenes-degradation.spec.ts(6 项);npm run verify 通过
(契约六项一致、35 条路由、21 文件 / 276 项测试、nest build、启动冒烟);
发布产物解压后 npm ci --omit=dev + node dist/main.js 实测打印「应用版本 v0.7.3」。

注意:Gitea 附件上传通道仍故障(1 KB 也瞬时 500),因此本版**尚未上传 Release**,
产物先在本地交付(sha256 见 SHA256SUMS)。
2026-09-17 03:24:56 +08:00

115 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# T-WEB-015 场景管理(新增模块)
- **状态**: done
- **依赖**: T-WEB-007(审核流程)
- **文档**: v1.6 §4.12
- **契约**: `rest-api 0.5.0`、`data-models 0.7.0`、`database 0.4.0`
## 目标
新增**场景**这一类资源:客户端跟练时的 3D 环境(GLB)+ 物理参数
(重力 / 风 / 地面 / 碰撞体 / 环境 / 出生点 / 相机)。
服务端**只存参数、不跑物理**,仿真在客户端(Windows 端用 Godot 内置物理)。
## 与模型的区别
| 项 | 模型 | 场景 |
|--|--|--|
| 主力格式 | **VRM**(humanoid 重绑定) | **GLB**(不做重绑定、不转 VRM) |
| 转换 | PMX/FBX/zip → GLB → VRM | `.glb`/`.gltf` 直接导入;`.zip`/`.pmx`/`.fbx` → GLB |
| 附加数据 | 骨骼 / 材质 / 缩略图 | **物理参数** `SceneConfig` |
## 交付物
| 文件 | 说明 |
|--|--|
| `contracts/data-models.md` §7 | `Scene` 与冻结的 `SceneConfig` 结构 |
| `contracts/rest-api.yaml` | `/api/scenes` 增删改查 + 审核复用 `/api/reviews/scene/:id/approve\|reject` + `catalog/export.scenes[]` |
| `migrations/1789650000000-AddScenes.ts` | `scenes` 表;`config` / `stats` 用 **MEDIUMTEXT** |
| `src/scenes/scene-config.ts` | `normalizeSceneConfig()`:缺项默认、越界夹紧、非法枚举回退,**永不抛错** |
| `src/scenes/{scenes.service,scene.serializer,scenes.controller,scenes.module}.ts` | 业务实现(上传一律 `pending`;删除时清理源文件/GLB/缩略图) |
| `src/models/model-inspector.ts` | 场景复用模型检查器做 GLB 分析 |
| `src/models/model-thumbnail.ts` | 场景复用模型光栅化器出缩略图 |
| `admin/src/pages/Scenes.tsx` | 列表 / 上传 / 审核 / 删除 / 物理参数编辑(编辑期即按同一规则夹紧) |
| `admin/src/lib/scene-physics.ts` | 与后端同规则的收敛(保存前就不会产生非法枚举) |
> **MEDIUMTEXT 的原因**:`models.stats` 曾在 TEXT(65,535 字节)上溢出
> (实测 70,980 字节 → MySQL 1406 → 500)。场景的 `stats` 与 `config` 同样可能很大,
> 因此**一开始就用 MEDIUMTEXT**,并保留 `shrinkStatsForStorage()` 兜底。
## 验收
| 验收项 | 结果 |
|--|--|
| 场景可上传(GLB 直传 / zip 自动解包 / PMX 转换) | ✅ 与模型同一套转换管线 |
| 物理参数可在管理端可视化编辑 | ✅ 重力 / 风 / 摩擦 / 弹性 / 碰撞体 / 环境 / 出生点 / 相机 |
| 非法参数不会入库 | ✅ 前后端同规则收敛;`test/scene-config.spec.ts` 9 项边界用例 |
| 客户端能拿到场景 | ✅ `catalog/export.scenes[]`;目录指纹纳入场景变更 |
| 路由与契约一致 | ✅ `check-routes` 共 **35** 条 |
## 缺陷修复记录(v0.7.2)
**缺陷**:`CatalogModule` 的 `imports` 里漏了 `ScenesModule`。
`CatalogService` 因为 `catalog/export` 要下发 `scenes[]` 而注入了 `ScenesService`,
但 Nest 的 DI 是**按模块**解析的 —— 不在同一个模块的 `imports` 里,就解析不到。
后果是 `node dist/main.js` 在 bootstrap 阶段直接抛
`UnknownDependenciesException: Nest can't resolve dependencies of the CatalogService (... ? ...)`,
**服务完全起不来**(v0.7.0 / v0.7.1 的产物都是这样)。
**为什么当时没被发现**:三层检查全都"绿"——
| 检查 | 为什么没抓到 |
|--|--|
| `nest build` / `tsc --noEmit` | 模块 import 缺失是**运行时** DI 问题,类型系统看不出来 |
| 258 项单元测试 | 它们直接 new 服务或测纯函数,**从不编译 `AppModule`** |
| 打包脚本的产物完整性校验 | 只校验文件清单、压缩包结构与 PE/魔数,不启动服务 |
**修复**:在 `CatalogModule.imports` 中补上 `ScenesModule`。
**防线**:新增 `scripts/check-bootstrap.mjs`,以正式部署方式
(`ExecStart=node dist/main.js`、**不带任何 npm 环境变量**、**不需要数据库**)
拉起真实产物,断言①无依赖解析类错误②启动日志里的应用版本等于 `package.json`
③启动确实推进到数据库连接阶段。已挂进 `npm run verify` 与
`scripts/package-release.sh`(构建后、打包前),并做过**反向验证**:
把 `ScenesModule` 删除后该检查立刻失败。
## 缺陷修复记录二(v0.7.3):场景表不可用会拖垮**整站**
**现场**(真实部署,Web v0.7.2):
| 请求 | 结果 |
|--|--|
| `GET /api/songs` / `/api/motions` / `/api/models` | 200 |
| `GET /api/scenes`、`/api/scenes/1`、`/api/scenes?status=draft` | **500** |
| `GET /api/catalog/export`、`/api/catalog/version` | **500** |
| `GET /api/reviews?status=pending`(待审列表) | **500** |
| `GET /api/reviews`(仅审核流水) | 200 |
`/health` 显示 `version 0.7.2`、`database.connected true`、`migrationsPending 0` ——
看起来一切正常,但**凡是要查 `scenes` 表的请求全部 500**。后果不是"场景不可用",
而是**客户端拉不到任何目录**(Windows/Android 双双不可用)、管理端待审列表打不开。
**为什么 `migrationsPending` 是 0 却有表缺失**:`DataSource.showMigrations()` 只比较
**代码里加载到的**迁移与数据库记录;如果部署的 `dist/migrations/` 里没有
`AddScenes.js`(或 glob 没匹配到),这个迁移根本不在比较集合里,于是"没有待执行迁移"
—— 表却不存在。这种"静默不一致"靠 `/health` 的 `pending` 是发现不了的。
**修复(降级,而不是继续让它 500)**:
契约与文档都写明**场景是可选资源**(没有可用场景时客户端回退内置空场景),
所以场景表的问题**不允许**影响与场景无关的功能:
1. `ScenesService.queryScenes()`:数据库层面失败 → 返回空结果、**只记一次日志**
(避免刷屏)、把原因写入 `diagnostics()`;下一次查询成功即自动复位;
2. `ReviewsService.listByStatus()`:场景查询单独 `catch`,待审列表照常返回歌曲/动作/模型;
3. `/health` 新增 `scenes: { available, error }` —— **降级但绝不静默**;
4. 新增诊断脚本 `web-server/scripts/diagnose-scenes.mjs`(默认只读):
打印运行期真实连的库、表清单、迁移记录、**代码实际加载的迁移**、
`scenes` 实际列 vs 实体期望列的逐列 diff;`--fix` 仅在缺表时用迁移里
同一份 DDL 幂等补表。
**测试**:`test/scenes-degradation.spec.ts`(6 项)锁定「表缺失 → 空场景而不是抛错」、
「原因被记录并被 `/health` 暴露」、「表恢复后自动复位」、「`catalog/export` 与
待审列表在降级下仍可用」。