feat(security): helmet + CORS 白名单 + 限流 + 定时清理 + 时间契约 Unix 毫秒

- helmet 安全头 + CORS_ORIGIN 白名单 + Swagger 仅非生产
- @nestjs/throttler 全局限流(60s/100 次)
- @nestjs/schedule 定时清理过期 token 与旧日志
- API 时间统一 Unix 毫秒(TransformInterceptor 递归转换)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 16:54:08 +08:00
parent 606585c3bd
commit ba1ae43e39
6 changed files with 136 additions and 15 deletions

View File

@ -34,11 +34,14 @@
"@nestjs/core": "^12.0.1", "@nestjs/core": "^12.0.1",
"@nestjs/jwt": "^12.0.2", "@nestjs/jwt": "^12.0.2",
"@nestjs/platform-express": "^12.0.1", "@nestjs/platform-express": "^12.0.1",
"@nestjs/schedule": "^12.0.2",
"@nestjs/swagger": "^12.0.2", "@nestjs/swagger": "^12.0.2",
"@nestjs/throttler": "^6.7.1",
"@prisma/client": "^6.19.3", "@prisma/client": "^6.19.3",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.15.1", "class-validator": "^0.15.1",
"helmet": "^8.3.0",
"ioredis": "^6.0.0", "ioredis": "^6.0.0",
"nestjs-pino": "^5.3.1", "nestjs-pino": "^5.3.1",
"pino": "^10.4.0", "pino": "^10.4.0",

62
pnpm-lock.yaml generated
View File

@ -178,9 +178,15 @@ importers:
'@nestjs/platform-express': '@nestjs/platform-express':
specifier: ^12.0.1 specifier: ^12.0.1
version: 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0) version: 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0)
'@nestjs/schedule':
specifier: ^12.0.2
version: 12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)
'@nestjs/swagger': '@nestjs/swagger':
specifier: ^12.0.2 specifier: ^12.0.2
version: 12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(typescript@6.0.3) version: 12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(typescript@6.0.3)
'@nestjs/throttler':
specifier: ^6.7.1
version: 6.7.1(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(reflect-metadata@0.2.2)
'@prisma/client': '@prisma/client':
specifier: ^6.19.3 specifier: ^6.19.3
version: 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3) version: 6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3)
@ -193,6 +199,9 @@ importers:
class-validator: class-validator:
specifier: ^0.15.1 specifier: ^0.15.1
version: 0.15.1 version: 0.15.1
helmet:
specifier: ^8.3.0
version: 8.3.0
ioredis: ioredis:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0(supports-color@7.2.0) version: 6.0.0(supports-color@7.2.0)
@ -726,6 +735,13 @@ packages:
'@nestjs/common': ^12.0.0 '@nestjs/common': ^12.0.0
'@nestjs/core': ^12.0.0 '@nestjs/core': ^12.0.0
'@nestjs/schedule@12.0.2':
resolution: {integrity: sha512-5iAvJEtk0njbfTrG4JQr+SXw8ZrqgDM1wJH9nUs6tVLCX5pWGd1LJ0a8b089UT1FDeDWaThRRfSLixFz9zztcA==}
engines: {node: '>=20.19.0'}
peerDependencies:
'@nestjs/common': ^11.0.0 || ^12.0.0
'@nestjs/core': ^11.0.0 || ^12.0.0
'@nestjs/schematics@12.0.6': '@nestjs/schematics@12.0.6':
resolution: {integrity: sha512-2OG0BUhGPJqVvWFhiy+7oaQa4VQUvTS9Dw8YRfvweznl4KS3YyZrdPi4cVGuRAThoEIsbdfguZzSOv1+t8U1tg==} resolution: {integrity: sha512-2OG0BUhGPJqVvWFhiy+7oaQa4VQUvTS9Dw8YRfvweznl4KS3YyZrdPi4cVGuRAThoEIsbdfguZzSOv1+t8U1tg==}
engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0}
@ -770,6 +786,14 @@ packages:
'@nestjs/platform-express': '@nestjs/platform-express':
optional: true optional: true
'@nestjs/throttler@6.7.1':
resolution: {integrity: sha512-gAyY1AkU+cSK5DCjFOB43xax9oakameH4xZa9kAgqBqSBQZgmv3DZylLV8KyZNzOGhceL3kDVtby+CF8VAdmEA==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
'@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0
'@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0
reflect-metadata: ^0.1.13 || ^0.2.0
'@noble/hashes@1.8.0': '@noble/hashes@1.8.0':
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16} engines: {node: ^14.21.3 || >=16}
@ -1115,6 +1139,9 @@ packages:
'@types/jsonwebtoken@9.0.10': '@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/luxon@3.7.6':
resolution: {integrity: sha512-6KSjliQAXK8ZLgFQO4B7iE4Rpf/B3rItzCFuXezBY/XIAGxOdLd7vRNK9/StRPwiS/yJsVbB7HKpW46B8tcEuQ==}
'@types/methods@1.1.4': '@types/methods@1.1.4':
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
@ -1445,6 +1472,10 @@ packages:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
cron@4.4.0:
resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==}
engines: {node: '>=18.x'}
date-fns@4.4.0: date-fns@4.4.0:
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
@ -1705,6 +1736,10 @@ packages:
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
helmet@8.3.0:
resolution: {integrity: sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==}
engines: {node: '>=18.0.0'}
help-me@5.0.0: help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
@ -1929,6 +1964,10 @@ packages:
resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
engines: {node: '>=18'} engines: {node: '>=18'}
luxon@3.7.2:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
magic-string@0.30.21: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@ -2981,6 +3020,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@nestjs/schedule@12.0.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)':
dependencies:
'@nestjs/common': 12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
'@nestjs/core': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
cron: 4.4.0
'@nestjs/schematics@12.0.6(chokidar@5.0.0)(prettier@3.9.9)(typescript@6.0.3)': '@nestjs/schematics@12.0.6(chokidar@5.0.0)(prettier@3.9.9)(typescript@6.0.3)':
dependencies: dependencies:
'@angular-devkit/core': 22.2.0(chokidar@5.0.0) '@angular-devkit/core': 22.2.0(chokidar@5.0.0)
@ -3019,6 +3064,12 @@ snapshots:
optionalDependencies: optionalDependencies:
'@nestjs/platform-express': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0) '@nestjs/platform-express': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(supports-color@7.2.0)
'@nestjs/throttler@6.7.1(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(reflect-metadata@0.2.2)':
dependencies:
'@nestjs/common': 12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
'@nestjs/core': 12.1.2(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@12.1.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
reflect-metadata: 0.2.2
'@noble/hashes@1.8.0': {} '@noble/hashes@1.8.0': {}
'@oxc-project/types@0.153.0': {} '@oxc-project/types@0.153.0': {}
@ -3245,6 +3296,8 @@ snapshots:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/node': 24.19.1 '@types/node': 24.19.1
'@types/luxon@3.7.6': {}
'@types/methods@1.1.4': {} '@types/methods@1.1.4': {}
'@types/ms@2.1.0': {} '@types/ms@2.1.0': {}
@ -3570,6 +3623,11 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
vary: 1.1.2 vary: 1.1.2
cron@4.4.0:
dependencies:
'@types/luxon': 3.7.6
luxon: 3.7.2
date-fns@4.4.0: {} date-fns@4.4.0: {}
dateformat@4.6.3: {} dateformat@4.6.3: {}
@ -3859,6 +3917,8 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
helmet@8.3.0: {}
help-me@5.0.0: {} help-me@5.0.0: {}
html-escaper@2.0.2: {} html-escaper@2.0.2: {}
@ -4060,6 +4120,8 @@ snapshots:
is-unicode-supported: 2.1.0 is-unicode-supported: 2.1.0
yoctocolors: 2.2.0 yoctocolors: 2.2.0
luxon@3.7.2: {}
magic-string@0.30.21: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/sourcemap-codec': 1.6.0

View File

@ -11,6 +11,9 @@ import { BodyLoggingInterceptor } from './logging/body-logging.interceptor.js';
import { TransformInterceptor } from './common/interceptors/transform.interceptor.js'; import { TransformInterceptor } from './common/interceptors/transform.interceptor.js';
import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor.js'; import { AuditLogInterceptor } from './common/interceptors/audit-log.interceptor.js';
import { AllExceptionsFilter } from './common/filters/http-exception.filter.js'; import { AllExceptionsFilter } from './common/filters/http-exception.filter.js';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { ScheduleModule } from '@nestjs/schedule';
import { CleanupService } from './common/cleanup.service.js';
import { TenantsModule } from './tenants/tenants.module.js'; import { TenantsModule } from './tenants/tenants.module.js';
import { UsersModule } from './users/users.module.js'; import { UsersModule } from './users/users.module.js';
import { RolesModule } from './roles/roles.module.js'; import { RolesModule } from './roles/roles.module.js';
@ -25,6 +28,8 @@ import { AppService } from './app.service.js';
@Module({ @Module({
imports: [ imports: [
ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),
ScheduleModule.forRoot(),
createLoggerModule(), createLoggerModule(),
PrismaModule, PrismaModule,
RedisModule, RedisModule,
@ -42,6 +47,8 @@ import { AppService } from './app.service.js';
controllers: [AppController], controllers: [AppController],
providers: [ providers: [
AppService, AppService,
CleanupService,
{ provide: APP_GUARD, useClass: ThrottlerGuard },
{ provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: PermissionsGuard }, { provide: APP_GUARD, useClass: PermissionsGuard },
{ provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor }, { provide: APP_INTERCEPTOR, useClass: RequestContextInterceptor },

View File

@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import { PrismaService } from '../prisma/prisma.service.js';
/** 定时清理:过期/吊销 refresh token 与超过保留期的审计/登录日志。 */
@Injectable()
export class CleanupService {
constructor(
private readonly prisma: PrismaService,
@InjectPinoLogger(CleanupService.name) private readonly logger: PinoLogger,
) {}
/** 每天 03:07 执行。 */
@Cron('7 3 * * *')
async cleanup(): Promise<void> {
try {
const db = this.prisma.raw;
const tokens = await db.refreshToken.deleteMany({
where: { OR: [{ revokedAt: { not: null } }, { expireAt: { lt: new Date() } }] },
});
const cutoff = new Date(Date.now() - 90 * 24 * 3600 * 1000);
const audit = await db.auditLog.deleteMany({ where: { createdAt: { lt: cutoff } } });
const login = await db.loginLog.deleteMany({ where: { createdAt: { lt: cutoff } } });
this.logger.info(
{ refreshTokens: tokens.count, auditLogs: audit.count, loginLogs: login.count },
'定时清理完成',
);
} catch (err) {
this.logger.warn({ err }, '定时清理失败');
}
}
}

View File

@ -2,12 +2,24 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nes
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
/** 递归将 Date 转为 Unix 毫秒 number(API 时间契约:统一毫秒时间戳)。 */
function toUnixMs(value: unknown): unknown {
if (value instanceof Date) return value.getTime();
if (Array.isArray(value)) return value.map(toUnixMs);
if (value && typeof value === 'object') {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) out[k] = toUnixMs(v);
return out;
}
return value;
}
/** /**
* 统一响应封装:{ code, message, data },成功 code = 0。 * 统一响应封装:{ code, message, data },成功 code = 0;时间统一 Unix 毫秒。
*/ */
@Injectable() @Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, unknown> { export class TransformInterceptor<T> implements NestInterceptor<T, unknown> {
intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<unknown> { intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<unknown> {
return next.handle().pipe(map((data) => ({ code: 0, message: 'ok', data: data ?? null }))); return next.handle().pipe(map((data) => ({ code: 0, message: 'ok', data: toUnixMs(data ?? null) })));
} }
} }

View File

@ -3,29 +3,33 @@ import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { Logger } from 'nestjs-pino'; import { Logger } from 'nestjs-pino';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import helmet from 'helmet';
import { AppModule } from './app.module.js'; import { AppModule } from './app.module.js';
async function bootstrap(): Promise<void> { async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule, { bufferLogs: true }); const app = await NestFactory.create(AppModule, { bufferLogs: true });
app.setGlobalPrefix('api/v1'); app.setGlobalPrefix('api/v1');
app.enableCors(); app.use(helmet());
const corsOrigin = process.env.CORS_ORIGIN
? process.env.CORS_ORIGIN.split(',').map((s) => s.trim())
: ['http://localhost:5173'];
app.enableCors({ origin: corsOrigin, credentials: true });
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
app.useLogger(app.get(Logger)); app.useLogger(app.get(Logger));
const swaggerConfig = new DocumentBuilder() // Swagger 文档仅非生产环境开放(生产默认关闭)
.setTitle('系统管理 API') if (process.env.NODE_ENV !== 'production') {
.setDescription('投云科技系统管理 API 文档') const swaggerConfig = new DocumentBuilder()
.setVersion('1.0') .setTitle('系统管理 API')
.addBearerAuth() .setDescription('投云科技系统管理 API 文档')
.build(); .setVersion('1.0')
SwaggerModule.setup( .addBearerAuth()
'docs', .build();
app, SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, swaggerConfig));
SwaggerModule.createDocument(app, swaggerConfig), }
);
const port = Number(process.env.PORT ?? 3000); const port = Number(process.env.PORT ?? 3000);
await app.listen(port); await app.listen(port);
app.get(Logger).log(`服务启动成功,端口 ${port},Swagger 文档: http://localhost:${port}/docs`); app.get(Logger).log(`服务启动成功,端口 ${port}`);
} }
await bootstrap(); await bootstrap();