import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { ValidationPipe, Logger, type INestApplication } from '@nestjs/common'; import { SwaggerModule } from '@nestjs/swagger'; import { WinstonModule } from 'nest-winston'; import * as winston from 'winston'; import { DataSource } from 'typeorm'; import { AppModule } from './app.module'; import { swaggerOptions } from './config/swagger.config'; import { AllExceptionsFilter } from './common/filters'; import { TransformInterceptor } from './common/interceptors'; /** * 启动期自动升级:确保 admin 表存在 role 字段(角色权限控制所需)。 * - 通过 information_schema 检测,缺失时自动 ALTER TABLE 添加 * - 默认 'normal',并将 id=1 的初始管理员置为 'super_admin' * - 已有 role 字段时跳过,幂等 */ async function ensureAdminRoleColumn(app: INestApplication): Promise { const dataSource = app.get(DataSource); const rows = (await dataSource.query( `SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin' AND COLUMN_NAME = 'role'`, )) as Array<{ cnt: number | string }>; const exists = Number(rows?.[0]?.cnt ?? 0) > 0; if (exists) return; Logger.warn('检测到 admin 表缺少 role 字段,正在自动升级数据库...', 'Migration'); await dataSource.query( "ALTER TABLE `admin` ADD COLUMN `role` varchar(20) NOT NULL DEFAULT 'normal' " + "COMMENT '角色:super_admin 超级管理员 / normal 普通管理员' AFTER `avatar`", ); await dataSource.query( "UPDATE `admin` SET `role` = 'super_admin' WHERE `id` = 1", ); Logger.log('admin 表 role 字段已自动升级完成,初始 admin 已置为超级管理员', 'Migration'); } /** * 启动期自动升级:确保 manual 表存在(使用手册模块)。 * - 表不存在时自动 CREATE TABLE * - 已存在则跳过,幂等 */ async function ensureManualTable(app: INestApplication): Promise { const dataSource = app.get(DataSource); const rows = (await dataSource.query( `SELECT COUNT(*) AS cnt FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'manual'`, )) as Array<{ cnt: number | string }>; const exists = Number(rows?.[0]?.cnt ?? 0) > 0; if (exists) return; Logger.warn('检测到 manual 表不存在,正在自动创建...', 'Migration'); await dataSource.query( `CREATE TABLE \`manual\` ( \`id\` int unsigned NOT NULL AUTO_INCREMENT, \`parent_id\` int unsigned DEFAULT NULL COMMENT '父节点ID,NULL=根节点', \`title\` varchar(200) NOT NULL COMMENT '标题', \`type\` tinyint(1) NOT NULL DEFAULT 1 COMMENT '类型:0=目录节点 1=文档节点', \`content\` longtext COMMENT '文档正文(目录节点为空)', \`content_format\` varchar(10) NOT NULL DEFAULT 'html' COMMENT '正文格式:html=富文本 markdown=Markdown', \`sort\` int NOT NULL DEFAULT 0 COMMENT '排序值', \`is_show\` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否显示 1是0否', \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, \`updated_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (\`id\`), KEY \`idx_parent\` (\`parent_id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='使用手册'`, ); Logger.log('manual 表已自动创建完成', 'Migration'); } /** * 启动期自动升级:确保 manual 表存在 content_format 字段(持久化正文格式)。 * - 仅在 manual 表已存在但缺少该字段时执行 * - 幂等 */ async function ensureManualContentFormatColumn( app: INestApplication, ): Promise { const dataSource = app.get(DataSource); const rows = (await dataSource.query( `SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'manual' AND COLUMN_NAME = 'content_format'`, )) as Array<{ cnt: number | string }>; const exists = Number(rows?.[0]?.cnt ?? 0) > 0; if (exists) return; Logger.warn( '检测到 manual 表缺少 content_format 字段,正在自动升级数据库...', 'Migration', ); await dataSource.query( "ALTER TABLE `manual` ADD COLUMN `content_format` varchar(10) NOT NULL DEFAULT 'html' " + "COMMENT '正文格式:html=富文本 markdown=Markdown' AFTER `content`", ); Logger.log( 'manual 表 content_format 字段已自动升级完成', 'Migration', ); } async function bootstrap(): Promise { const app = await NestFactory.create(AppModule, { logger: WinstonModule.createLogger({ transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.timestamp(), winston.format.colorize(), winston.format.printf(({ level, message, timestamp }) => { return `${timestamp} ${level}: ${message}`; }), ), }), ], }), }); app.setGlobalPrefix('api'); // 全局管道(DTO + class-validator) // 注意:app.module.ts 的 APP_PIPE/APP_FILTER/APP_INTERCEPTOR 已删除, // 这里统一注册避免重复执行;APP_GUARD(JwtAuthGuard 需要 DI)保留在 app.module.ts。 app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: false, }), ); // 全局过滤器 + 响应拦截器 app.useGlobalFilters(new AllExceptionsFilter()); app.useGlobalInterceptors(new TransformInterceptor()); // 跨域放行(前端 3000 -> 后端 3001) app.enableCors({ origin: true, credentials: true, }); // Swagger 文档 const document = SwaggerModule.createDocument(app, swaggerOptions); SwaggerModule.setup('api-docs', app, document); // 启动期自动迁移:补齐 admin.role 字段、确保 manual 表存在 await ensureAdminRoleColumn(app); await ensureManualTable(app); await ensureManualContentFormatColumn(app); const port = parseInt(process.env.PORT ?? '3001', 10); await app.listen(port); Logger.log(`🚀 后端服务已启动: http://localhost:${port}`, 'Bootstrap'); Logger.log(`📘 Swagger 文档地址: http://localhost:${port}/api-docs`, 'Bootstrap'); } void bootstrap();