web-01-api/src/main.ts
2026-06-22 10:26:29 +08:00

168 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();