web-01-api/src/main.ts

168 lines
6.3 KiB
TypeScript
Raw Normal View History

2026-06-22 10:26:29 +08:00
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 '父节点IDNULL=根节点',
\`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_GUARDJwtAuthGuard 需要 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();