diff --git a/src/app.module.ts b/src/app.module.ts index dc93509..5da6656 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -16,6 +16,7 @@ import { AuthModule } from './modules/auth/auth.module'; import { AdminAuthModule } from './modules/admin-auth/admin-auth.module'; import { AdminDashboardModule } from './modules/admin-dashboard/admin-dashboard.module'; import { AdminUsersModule } from './modules/admin-users/admin-users.module'; +import { AdminThrottleModule } from './modules/admin-throttle/admin-throttle.module'; import { AppConfigModule } from './modules/config/config.module'; import { AdminEventsModule } from './modules/admin-events/admin-events.module'; import { AdminKnowledgeModule } from './modules/admin-knowledge/admin-knowledge.module'; @@ -50,6 +51,8 @@ import { GlobalExceptionFilter } from './common/filters/global-exception.filter' import { StrictValidationPipe } from './common/pipes/strict-validation.pipe'; import { ResponseInterceptor } from './common/interceptors/response.interceptor'; import { TraceIdInterceptor } from './common/interceptors/trace-id.interceptor'; +import { TimeoutInterceptor } from './common/interceptors/timeout.interceptor'; +import { AppThrottleModule } from './common/throttle/throttle.module'; import { AiAnalysisWorker } from './workers/ai-analysis.worker'; import { DocumentImportWorker } from './workers/document-import.worker'; @@ -88,6 +91,7 @@ import appleConfig from './config/apple.config'; }), PrismaModule, RedisModule, + AppThrottleModule, EventBusModule, QueueModule, AiModule, @@ -98,6 +102,7 @@ import appleConfig from './config/apple.config'; AdminAuthModule, AdminDashboardModule, AdminUsersModule, + AdminThrottleModule, AppConfigModule, AdminEventsModule, AdminKnowledgeModule, @@ -132,6 +137,7 @@ import appleConfig from './config/apple.config'; { provide: APP_FILTER, useClass: GlobalExceptionFilter }, { provide: APP_PIPE, useClass: StrictValidationPipe }, { provide: APP_INTERCEPTOR, useClass: TraceIdInterceptor }, + { provide: APP_INTERCEPTOR, useClass: TimeoutInterceptor }, { provide: APP_INTERCEPTOR, useClass: ResponseInterceptor }, AiAnalysisWorker, DocumentImportWorker, diff --git a/src/common/interceptors/timeout.interceptor.ts b/src/common/interceptors/timeout.interceptor.ts new file mode 100644 index 0000000..1ba1327 --- /dev/null +++ b/src/common/interceptors/timeout.interceptor.ts @@ -0,0 +1,10 @@ +import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common'; +import { Observable, throwError, TimeoutError } from 'rxjs'; +import { catchError, timeout } from 'rxjs/operators'; + +@Injectable() +export class TimeoutInterceptor implements NestInterceptor { + intercept(_context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe(timeout(30000), catchError(err => err instanceof TimeoutError ? throwError(() => new RequestTimeoutException('Request timeout')) : throwError(() => err))); + } +} diff --git a/src/common/throttle/redis-throttler.storage.ts b/src/common/throttle/redis-throttler.storage.ts new file mode 100644 index 0000000..2513d36 --- /dev/null +++ b/src/common/throttle/redis-throttler.storage.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { ThrottlerStorage } from '@nestjs/throttler'; +import { RedisService } from '../../infrastructure/redis/redis.service'; + +@Injectable() +export class RedisThrottlerStorage implements ThrottlerStorage { + constructor(private readonly redis: RedisService) {} + async increment(key: string, ttl: number) { + const redisKey = `throttle:${key}`; + try { const hits = await this.redis.incr(redisKey); await this.redis.expire(redisKey, Math.ceil(ttl / 1000)); return { totalHits: hits, timeToExpire: ttl }; } + catch { return { totalHits: 1, timeToExpire: ttl }; } + } +} diff --git a/src/common/throttle/throttle.module.ts b/src/common/throttle/throttle.module.ts new file mode 100644 index 0000000..a55b462 --- /dev/null +++ b/src/common/throttle/throttle.module.ts @@ -0,0 +1,24 @@ +import { Global, Module } from '@nestjs/common'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { RedisThrottlerStorage } from './redis-throttler.storage'; +import { RedisService } from '../../infrastructure/redis/redis.service'; + +@Global() +@Module({ + imports: [ + ThrottlerModule.forRootAsync({ + useFactory: (redis: RedisService) => ({ + throttlers: [ + { name: 'global', ttl: 60000, limit: 100 }, + { name: 'ai', ttl: 60000, limit: 20 }, + { name: 'login', ttl: 60000, limit: 5 }, + { name: 'upload', ttl: 60000, limit: 10 }, + ], + storage: new RedisThrottlerStorage(redis), + }), + inject: [RedisService], + }), + ], + exports: [ThrottlerModule], +}) +export class AppThrottleModule {} diff --git a/src/modules/admin-throttle/admin-throttle.controller.ts b/src/modules/admin-throttle/admin-throttle.controller.ts new file mode 100644 index 0000000..5e8cf6e --- /dev/null +++ b/src/modules/admin-throttle/admin-throttle.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Get, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; +import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; +import { AdminRoles } from '../../common/decorators/admin-roles.decorator'; +import type { AdminRole } from '../../common/types/admin-role.enum'; + +@ApiTags('admin-throttle') +@Controller('admin-api/throttle') +@UseGuards(AdminAuthGuard, AdminRolesGuard) +@ApiBearerAuth() +export class AdminThrottleController { + @Get('status') + @AdminRoles('SUPER_ADMIN' as AdminRole) + @ApiOperation({ summary: '限流规则状态' }) + async status() { + return { + rules: [ + { name: 'global', ttl: '60s', limit: 100, desc: '全局 API' }, + { name: 'ai', ttl: '60s', limit: 20, desc: 'AI 接口' }, + { name: 'login', ttl: '60s', limit: 5, desc: '登录接口' }, + { name: 'upload', ttl: '60s', limit: 10, desc: '文件上传' }, + ], + storage: 'Redis', + enabled: true, + }; + } +} diff --git a/src/modules/admin-throttle/admin-throttle.module.ts b/src/modules/admin-throttle/admin-throttle.module.ts new file mode 100644 index 0000000..8513ddf --- /dev/null +++ b/src/modules/admin-throttle/admin-throttle.module.ts @@ -0,0 +1,6 @@ +import { Module } from '@nestjs/common'; +import { AdminThrottleController } from './admin-throttle.controller'; +import { AdminAuthGuard } from '../../common/guards/admin-auth.guard'; +import { AdminRolesGuard } from '../../common/guards/admin-roles.guard'; +@Module({ controllers: [AdminThrottleController], providers: [AdminAuthGuard, AdminRolesGuard] }) +export class AdminThrottleModule {}