feat(web): T-WEB-001 NestJS 骨架 + 连接现有 MySQL 实例
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AppConfig, configuration } from './configuration';
|
||||
|
||||
/**
|
||||
* 全局配置模块:AppConfig 在所有模块中可直接注入。
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
envFilePath: ['.env.local', '.env'],
|
||||
cache: true,
|
||||
}),
|
||||
],
|
||||
providers: [AppConfig],
|
||||
exports: [AppConfig],
|
||||
})
|
||||
export class AppConfigModule {}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { resolve } from 'node:path';
|
||||
import { resolveContractVersions } from './contract-versions';
|
||||
|
||||
export interface DbConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
pass: string;
|
||||
name: string;
|
||||
synchronize: boolean;
|
||||
logging: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型化的配置访问层。
|
||||
* 所有环境变量只在此处读取,业务代码不直接触碰 process.env。
|
||||
*/
|
||||
@Injectable()
|
||||
export class AppConfig {
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
get nodeEnv(): string {
|
||||
return this.config.get<string>('nodeEnv') ?? 'development';
|
||||
}
|
||||
|
||||
get isProduction(): boolean {
|
||||
return this.nodeEnv === 'production';
|
||||
}
|
||||
|
||||
get port(): number {
|
||||
return this.config.get<number>('port') ?? 3000;
|
||||
}
|
||||
|
||||
get storageDir(): string {
|
||||
return this.config.get<string>('storageDir') ?? resolve(process.cwd(), 'storage');
|
||||
}
|
||||
|
||||
get uploadMaxBytes(): number {
|
||||
return this.config.get<number>('uploadMaxBytes') ?? 512 * 1024 * 1024;
|
||||
}
|
||||
|
||||
get assimpBin(): string {
|
||||
return this.config.get<string>('assimpBin') ?? 'assimp';
|
||||
}
|
||||
|
||||
get contractsDir(): string | undefined {
|
||||
return this.config.get<string>('contractsDir') || undefined;
|
||||
}
|
||||
|
||||
get contractVersions(): Record<string, string> {
|
||||
return resolveContractVersions(this.contractsDir);
|
||||
}
|
||||
|
||||
get db(): DbConfig {
|
||||
return {
|
||||
host: this.config.get<string>('db.host') ?? '127.0.0.1',
|
||||
port: this.config.get<number>('db.port') ?? 3306,
|
||||
user: this.config.get<string>('db.user') ?? 'opendance_app',
|
||||
pass: this.config.get<string>('db.pass') ?? '',
|
||||
name: this.config.get<string>('db.name') ?? 'opendance',
|
||||
synchronize: this.config.get<boolean>('db.synchronize') ?? false,
|
||||
logging: this.config.get<boolean>('db.logging') ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function bool(value: string | undefined, fallback = false): boolean {
|
||||
if (value === undefined || value === '') return fallback;
|
||||
return value === 'true' || value === '1';
|
||||
}
|
||||
|
||||
/** @nestjs/config 的 load 工厂 */
|
||||
export const configuration = () => ({
|
||||
nodeEnv: process.env.NODE_ENV ?? 'development',
|
||||
port: Number(process.env.PORT ?? 3000),
|
||||
storageDir: process.env.STORAGE_DIR ?? resolve(process.cwd(), 'storage'),
|
||||
uploadMaxBytes: Number(process.env.UPLOAD_MAX_BYTES ?? 512 * 1024 * 1024),
|
||||
assimpBin: process.env.ASSIMP_BIN ?? 'assimp',
|
||||
contractsDir: process.env.CONTRACTS_DIR ?? '',
|
||||
db: {
|
||||
host: process.env.DB_HOST ?? '127.0.0.1',
|
||||
port: Number(process.env.DB_PORT ?? 3306),
|
||||
user: process.env.DB_USER ?? 'opendance_app',
|
||||
pass: process.env.DB_PASS ?? '',
|
||||
name: process.env.DB_NAME ?? 'opendance',
|
||||
synchronize: bool(process.env.DB_SYNCHRONIZE, false),
|
||||
logging: bool(process.env.DB_LOGGING, false),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 生产环境下禁止 DB_SYNCHRONIZE=true —— 契约 database.md 明确要求。
|
||||
* 返回错误信息数组,供 main.ts 启动时快速失败。
|
||||
*/
|
||||
export function validateEnv(env: Record<string, unknown>): string[] {
|
||||
const problems: string[] = [];
|
||||
const db = (env.db ?? {}) as Record<string, unknown>;
|
||||
|
||||
if (!db.host) problems.push('DB_HOST 未设置');
|
||||
if (!db.user) problems.push('DB_USER 未设置');
|
||||
if (!db.name) problems.push('DB_NAME 未设置');
|
||||
if (env.nodeEnv === 'production' && db.synchronize === true) {
|
||||
problems.push('生产环境禁止 DB_SYNCHRONIZE=true,请使用 TypeORM migration');
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
/**
|
||||
* 编译进代码的契约版本常量。
|
||||
* 与 `contracts/VERSION` 必须一致;`npm run contracts:check` 在构建前校验。
|
||||
* 之所以编译进代码:Docker 构建上下文是 web-server/,contracts/ 不在其中。
|
||||
*/
|
||||
export const CONTRACT_VERSIONS: Readonly<Record<string, string>> = Object.freeze({
|
||||
'rest-api': '0.1.0',
|
||||
'websocket-keypoints': '0.1.0',
|
||||
'data-models': '0.1.0',
|
||||
'skeleton-topology': '0.1.0',
|
||||
'model-formats': '0.1.0',
|
||||
database: '0.1.0',
|
||||
});
|
||||
|
||||
/** 解析 `contracts/VERSION`(`key: value` 每行一条,`#` 起注释) */
|
||||
export function parseVersionFile(content: string): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) continue;
|
||||
const idx = line.indexOf(':');
|
||||
if (idx <= 0) continue;
|
||||
const key = line.slice(0, idx).trim();
|
||||
const value = line.slice(idx + 1).trim();
|
||||
if (key && value) out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行期契约版本。设置 `CONTRACTS_DIR` 时读取实时文件(部署可挂载 contracts/),
|
||||
* 否则回落到编译常量。
|
||||
*/
|
||||
export function resolveContractVersions(contractsDir?: string): Record<string, string> {
|
||||
if (!contractsDir) return { ...CONTRACT_VERSIONS };
|
||||
const file = join(contractsDir, 'VERSION');
|
||||
if (!existsSync(file)) return { ...CONTRACT_VERSIONS };
|
||||
try {
|
||||
const parsed = parseVersionFile(readFileSync(file, 'utf8'));
|
||||
return Object.keys(parsed).length > 0 ? parsed : { ...CONTRACT_VERSIONS };
|
||||
} catch {
|
||||
return { ...CONTRACT_VERSIONS };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user