feat(scenes): 场景管理后端 + 契约 0.5.0/0.7.0/0.4.0(T-WEB-015)

契约先行(三端逐字节一致,已同步 windows/android):
- rest-api -> 0.5.0:新增 GET/POST /api/scenes、GET/PATCH/DELETE /api/scenes/{id}
- data-models -> 0.7.0:新增 §7 Scene 与 7.1 SceneConfig(物理/环境/出生点/相机)
- database -> 0.4.0:新增 scenes 表(config/stats 用 MEDIUMTEXT,迁移可逆)

后端:
- entities/scene.entity.ts + 迁移 AddScenes1789650000000(scenes 表)
- scenes/scene-config.ts:SceneConfig 定义 + 默认值 + **边界收敛**(越界夹紧、
  非法枚举回退),保证客户端拿到的参数一定可解释;服务端只存参数不跑物理
- scenes/scenes.service.ts:上传(.glb/.gltf 直接导入;.zip/.pmx/.fbx 转 GLB,
  场景不做 humanoid 重绑定、不转 VRM)、列表/详情/更新/软删除、缩略图复用模型
  光栅化器、分析复用模型检查器、config 同时接受 JSON 字符串与平铺字段
- scenes/scenes.controller.ts + scenes.module.ts(app.module 注册)
- 审核复用 /api/reviews/:type/:id:TargetType.Scene 接入待审列表与状态流转
- catalog/export 增 scenes[],目录指纹纳入场景变更
- 枚举:TargetType.Scene、AssetKind.Scene / SceneThumbnail

测试:新增 test/scene-config.spec.ts 6 项(默认值/夹紧/枚举回退/出生点上限/颜色/
序列化);规模 19 文件 / 243 项;check-routes 35 条一致。版本 0.6.0。

注:管理端场景页(GLB 预览 + 物理参数可视化)与 Windows 场景选择留待下一轮。
This commit is contained in:
opendance
2026-09-17 00:27:08 +08:00
parent aad60cbd2e
commit 584b7eaca9
25 changed files with 1020 additions and 35 deletions
+95
View File
@@ -0,0 +1,95 @@
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { bigintNumber } from '../common/transformers';
import { PublishStatus, SourceType } from '../common/enums';
import type { SceneConfig } from '../scenes/scene-config';
/** JSON 文本列(MEDIUMTEXT):TEXT 的 64KB 上限在模型 stats 上已经踩过坑 */
const jsonText = {
to: (value: unknown): string | null => (value === null || value === undefined ? null : JSON.stringify(value)),
from: (value: string | null): unknown => {
if (value === null || value === undefined || value === '') return null;
try {
return JSON.parse(value);
} catch {
return null;
}
},
};
/**
* 场景 —— 契约 data-models.md §7(T-WEB-015)。
*
* **以 GLB 为主**(与模型区分:场景不做 humanoid 重绑定、不转 VRM);
* `config` 存物理/环境/出生点/相机参数(重力、风向动态等),
* **服务端只存参数不跑物理**,仿真在客户端(Godot 内置物理)。
*/
@Entity('scenes')
@Index('idx_scenes_status', ['status'])
export class Scene {
@PrimaryGeneratedColumn({ type: 'bigint' })
id!: number;
@Column({ type: 'varchar', length: 200 })
name!: string;
/** 场景 GLB 资产 */
@Column({ name: 'glb_asset_id', type: 'bigint', nullable: true, transformer: bigintNumber })
glbAssetId!: number | null;
@Column({ name: 'thumbnail_asset_id', type: 'bigint', nullable: true, transformer: bigintNumber })
thumbnailAssetId!: number | null;
/** 物理 / 环境 / 出生点 / 相机(见 scenes/scene-config.ts) */
@Column({ name: 'config', type: 'mediumtext', nullable: true, transformer: jsonText })
config!: SceneConfig | null;
/** 场景 GLB 的分析结果(面数 / 网格 / 材质 / warnings,复用模型检查器) */
@Column({ name: 'stats', type: 'mediumtext', nullable: true, transformer: jsonText })
stats!: Record<string, unknown> | null;
@Column({ name: 'file_size', type: 'bigint', nullable: true, transformer: bigintNumber })
fileSize!: number | null;
@Column({ name: 'file_sha256', type: 'varchar', length: 64, nullable: true })
fileSha256!: string | null;
/** 由 `.zip` / `.pmx` / `.fbx` 转换而来时的说明 */
@Column({ name: 'conversion_note', type: 'varchar', length: 500, nullable: true })
conversionNote!: string | null;
// ---- 来源五元组(必填)----
@Column({ name: 'source_type', type: 'varchar', length: 32 })
sourceType!: SourceType;
@Column({ name: 'source_name', type: 'varchar', length: 200 })
sourceName!: string;
@Column({ name: 'source_url', type: 'varchar', length: 500 })
sourceUrl!: string;
@Column({ type: 'varchar', length: 100 })
license!: string;
@Column({ type: 'varchar', length: 200 })
author!: string;
@Column({ type: 'varchar', length: 16, default: PublishStatus.Pending })
status!: PublishStatus;
@CreateDateColumn({ name: 'created_at', type: 'datetime' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'datetime' })
updatedAt!: Date;
@DeleteDateColumn({ name: 'deleted_at', type: 'datetime', nullable: true })
deletedAt!: Date | null;
}