feat(web): T-WEB-002 TypeORM 迁移生成表结构(5 表,可逆)
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { AssetKind } from '../common/enums';
|
||||
import { bigintNumber } from '../common/transformers';
|
||||
/**
|
||||
* 上传文件元数据。
|
||||
* 契约 database.md:表名 snake_case、主键 BIGINT、时间 DATETIME。
|
||||
* 相同 sha256 的文件只落盘一次,多条业务记录共享同一 asset。
|
||||
*/
|
||||
@Entity('assets')
|
||||
@Index('idx_assets_sha256', ['sha256'], { unique: true })
|
||||
@Index('idx_assets_kind', ['kind'])
|
||||
export class Asset {
|
||||
/**
|
||||
* BIGINT 主键。DataSource 已设置 supportBigNumbers + bigNumberStrings=false,
|
||||
* mysql2 在数值可被 double 精确表示时返回 number,因此这里不需要 transformer
|
||||
* (TypeORM 的 PrimaryGeneratedColumn 不支持 transformer 选项)。
|
||||
*/
|
||||
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 32 })
|
||||
kind!: AssetKind;
|
||||
|
||||
@Column({ name: 'original_name', type: 'varchar', length: 255 })
|
||||
originalName!: string;
|
||||
|
||||
/** 相对 STORAGE_DIR 的路径,永不使用原始文件名 */
|
||||
@Column({ name: 'stored_path', type: 'varchar', length: 500 })
|
||||
storedPath!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 128, default: 'application/octet-stream' })
|
||||
mime!: string;
|
||||
|
||||
@Column({ type: 'bigint', transformer: bigintNumber })
|
||||
size!: number;
|
||||
|
||||
@Column({ type: 'char', length: 64 })
|
||||
sha256!: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'datetime' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Asset } from './asset.entity';
|
||||
import { CharacterModel } from './model.entity';
|
||||
import { Motion } from './motion.entity';
|
||||
import { ReviewRecord } from './review-record.entity';
|
||||
import { Song } from './song.entity';
|
||||
|
||||
export { Asset, CharacterModel, Motion, ReviewRecord, Song };
|
||||
|
||||
/** 显式列出实体:glob 在 ts-node 与编译产物下行为不一致,显式最稳 */
|
||||
export const ENTITIES = [Asset, Song, Motion, CharacterModel, ReviewRecord];
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { bigintNumber, boolTinyint } from '../common/transformers';
|
||||
import { ModelFormat, PublishStatus, SourceType } from '../common/enums';
|
||||
|
||||
/** 角色模型 —— 契约 data-models.md §4(VRM 优先) */
|
||||
@Entity('models')
|
||||
@Index('idx_models_status', ['status'])
|
||||
@Index('idx_models_format', ['format'])
|
||||
@Index('idx_models_is_primary', ['isPrimary'])
|
||||
export class CharacterModel {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 200 })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 16 })
|
||||
format!: ModelFormat;
|
||||
|
||||
@Column({ name: 'vrm_version', type: 'varchar', length: 16, nullable: true })
|
||||
vrmVersion!: string | null;
|
||||
|
||||
/** 全局默认模型;服务端保证同一时刻至多一条为 true(见 ModelsService.setPrimary) */
|
||||
@Column({ name: 'is_primary', type: 'tinyint', width: 1, default: 0, transformer: boolTinyint })
|
||||
isPrimary!: boolean;
|
||||
|
||||
@Column({ name: 'file_asset_id', type: 'bigint', nullable: true, transformer: bigintNumber })
|
||||
fileAssetId!: number | null;
|
||||
|
||||
@Column({ name: 'thumbnail_asset_id', type: 'bigint', nullable: true, transformer: bigintNumber })
|
||||
thumbnailAssetId!: number | 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.Draft })
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { bigintNumber, decimalNumber } from '../common/transformers';
|
||||
import { MotionFormat, PreprocessState, PublishStatus, SourceType } from '../common/enums';
|
||||
import { Song } from './song.entity';
|
||||
|
||||
/** 动作数据 —— 契约 data-models.md §3 */
|
||||
@Entity('motions')
|
||||
@Index('idx_motions_song_id', ['songId'])
|
||||
@Index('idx_motions_status', ['status'])
|
||||
@Index('idx_motions_format', ['format'])
|
||||
export class Motion {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'song_id', type: 'bigint', nullable: true, transformer: bigintNumber })
|
||||
songId!: number | null;
|
||||
|
||||
@ManyToOne(() => Song, { nullable: true, onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'song_id' })
|
||||
song?: Song | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 200 })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 24 })
|
||||
format!: MotionFormat;
|
||||
|
||||
@Column({ name: 'file_asset_id', type: 'bigint', nullable: true, transformer: bigintNumber })
|
||||
fileAssetId!: number | null;
|
||||
|
||||
@Column({ type: 'decimal', precision: 8, scale: 3, nullable: true, transformer: decimalNumber })
|
||||
fps!: number | null;
|
||||
|
||||
@Column({ name: 'frame_count', type: 'int', nullable: true })
|
||||
frameCount!: number | null;
|
||||
|
||||
@Column({ name: 'duration_ms', type: 'int', nullable: true })
|
||||
durationMs!: number | null;
|
||||
|
||||
// ---- 预处理产物(T-WEB-006)----
|
||||
@Column({ name: 'keypoint_format', type: 'varchar', length: 32, nullable: true })
|
||||
keypointFormat!: string | null;
|
||||
|
||||
@Column({ name: 'keypoint_asset_id', type: 'bigint', nullable: true, transformer: bigintNumber })
|
||||
keypointAssetId!: number | null;
|
||||
|
||||
@Column({ name: 'keypoint_frames', type: 'int', nullable: true })
|
||||
keypointFrames!: number | null;
|
||||
|
||||
@Column({ name: 'preprocess_state', type: 'varchar', length: 16, default: PreprocessState.None })
|
||||
preprocessState!: PreprocessState;
|
||||
|
||||
@Column({ name: 'preprocess_error', type: 'text', nullable: true })
|
||||
preprocessError!: 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.Draft })
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { bigintNumber } from '../common/transformers';
|
||||
import { PublishStatus, ReviewAction, TargetType } from '../common/enums';
|
||||
|
||||
/** 审核流水 —— 契约 data-models.md §5 */
|
||||
@Entity('review_records')
|
||||
@Index('idx_review_target', ['targetType', 'targetId'])
|
||||
@Index('idx_review_created_at', ['createdAt'])
|
||||
export class ReviewRecord {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||
id!: number;
|
||||
|
||||
@Column({ name: 'target_type', type: 'varchar', length: 16 })
|
||||
targetType!: TargetType;
|
||||
|
||||
@Column({ name: 'target_id', type: 'bigint', transformer: bigintNumber })
|
||||
targetId!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 16 })
|
||||
action!: ReviewAction;
|
||||
|
||||
@Column({ name: 'from_status', type: 'varchar', length: 16 })
|
||||
fromStatus!: PublishStatus;
|
||||
|
||||
@Column({ name: 'to_status', type: 'varchar', length: 16 })
|
||||
toStatus!: PublishStatus;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, nullable: true })
|
||||
reviewer!: string | null;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
comment!: string | null;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at', type: 'datetime' })
|
||||
createdAt!: Date;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
DeleteDateColumn,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { bigintNumber, decimalNumber } from '../common/transformers';
|
||||
import { PublishStatus, SourceType } from '../common/enums';
|
||||
|
||||
/** 曲目 —— 契约 data-models.md §2、database.md 表清单 */
|
||||
@Entity('songs')
|
||||
@Index('idx_songs_title', ['title'])
|
||||
@Index('idx_songs_artist', ['artist'])
|
||||
@Index('idx_songs_status', ['status'])
|
||||
export class Song {
|
||||
@PrimaryGeneratedColumn({ type: 'bigint' })
|
||||
id!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 200 })
|
||||
title!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 200 })
|
||||
artist!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 200, nullable: true })
|
||||
album!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 32, nullable: true })
|
||||
language!: string | null;
|
||||
|
||||
@Column({ name: 'duration_ms', type: 'int', nullable: true })
|
||||
durationMs!: number | null;
|
||||
|
||||
@Column({ type: 'decimal', precision: 7, scale: 2, nullable: true, transformer: decimalNumber })
|
||||
bpm!: number | null;
|
||||
|
||||
@Column({ type: 'tinyint', nullable: true })
|
||||
difficulty!: number | null;
|
||||
|
||||
/** simple-json 在 MariaDB 上落 LONGTEXT,避免 JSON 类型兼容性差异 */
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
tags!: string[] | null;
|
||||
|
||||
@Column({ name: 'cover_asset_id', type: 'bigint', nullable: true, transformer: bigintNumber })
|
||||
coverAssetId!: number | null;
|
||||
|
||||
@Column({ name: 'audio_asset_id', type: 'bigint', nullable: true, transformer: bigintNumber })
|
||||
audioAssetId!: number | 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.Draft })
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user