168 lines
6.3 KiB
TypeScript
168 lines
6.3 KiB
TypeScript
|
|
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<void> {
|
|||
|
|
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<void> {
|
|||
|
|
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<void> {
|
|||
|
|
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<void> {
|
|||
|
|
const app = await NestFactory.create<NestExpressApplication>(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();
|