Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
180
backend-nestjs/src/common/decorators/api-response.decorator.ts
Normal file
180
backend-nestjs/src/common/decorators/api-response.decorator.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { applyDecorators, Type } from '@nestjs/common';
|
||||
import { ApiResponse, ApiResponseOptions } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 标准API响应装饰器
|
||||
*/
|
||||
export const ApiStandardResponse = <TModel extends Type<any>>(
|
||||
model?: TModel,
|
||||
options?: Omit<ApiResponseOptions, 'schema'>
|
||||
) => {
|
||||
const baseSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: '请求是否成功',
|
||||
example: true,
|
||||
},
|
||||
code: {
|
||||
type: 'number',
|
||||
description: 'HTTP状态码',
|
||||
example: 200,
|
||||
},
|
||||
msg: {
|
||||
type: 'string',
|
||||
description: '响应消息',
|
||||
example: '操作成功',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: '响应时间戳',
|
||||
example: '2023-12-01T12:00:00.000Z',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '请求路径',
|
||||
example: '/api/users',
|
||||
},
|
||||
requestId: {
|
||||
type: 'string',
|
||||
description: '请求ID',
|
||||
example: 'uuid-string',
|
||||
},
|
||||
},
|
||||
required: ['success', 'code', 'msg'],
|
||||
};
|
||||
|
||||
if (model) {
|
||||
baseSchema.properties['data'] = {
|
||||
$ref: `#/components/schemas/${model.name}`,
|
||||
};
|
||||
} else {
|
||||
baseSchema.properties['data'] = {
|
||||
type: 'object',
|
||||
description: '响应数据',
|
||||
nullable: true,
|
||||
};
|
||||
}
|
||||
|
||||
return applyDecorators(
|
||||
ApiResponse({
|
||||
...options,
|
||||
schema: baseSchema,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 成功响应装饰器
|
||||
*/
|
||||
export const ApiSuccessResponse = <TModel extends Type<any>>(
|
||||
model?: TModel,
|
||||
description: string = '操作成功'
|
||||
) => {
|
||||
return ApiStandardResponse(model, {
|
||||
status: 200,
|
||||
description,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建成功响应装饰器
|
||||
*/
|
||||
export const ApiCreatedResponse = <TModel extends Type<any>>(
|
||||
model?: TModel,
|
||||
description: string = '创建成功'
|
||||
) => {
|
||||
return ApiStandardResponse(model, {
|
||||
status: 201,
|
||||
description,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 错误响应装饰器
|
||||
*/
|
||||
export const ApiErrorResponse = (
|
||||
status: number,
|
||||
description: string,
|
||||
errorCode?: string
|
||||
) => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: '请求是否成功',
|
||||
example: false,
|
||||
},
|
||||
code: {
|
||||
type: 'number',
|
||||
description: 'HTTP状态码',
|
||||
example: status,
|
||||
},
|
||||
msg: {
|
||||
type: 'string',
|
||||
description: '错误消息',
|
||||
example: description,
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
example: null,
|
||||
},
|
||||
errorCode: {
|
||||
type: 'string',
|
||||
description: '错误代码',
|
||||
example: errorCode || 'ERROR',
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
description: '响应时间戳',
|
||||
example: '2023-12-01T12:00:00.000Z',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '请求路径',
|
||||
example: '/api/users',
|
||||
},
|
||||
requestId: {
|
||||
type: 'string',
|
||||
description: '请求ID',
|
||||
example: 'uuid-string',
|
||||
},
|
||||
},
|
||||
required: ['success', 'code', 'msg', 'errorCode'],
|
||||
};
|
||||
|
||||
return applyDecorators(
|
||||
ApiResponse({
|
||||
status,
|
||||
description,
|
||||
schema,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 常用错误响应装饰器
|
||||
*/
|
||||
export const ApiBadRequestResponse = (description: string = '请求参数错误') =>
|
||||
ApiErrorResponse(400, description, 'BAD_REQUEST');
|
||||
|
||||
export const ApiUnauthorizedResponse = (description: string = '未授权访问') =>
|
||||
ApiErrorResponse(401, description, 'UNAUTHORIZED');
|
||||
|
||||
export const ApiForbiddenResponse = (description: string = '禁止访问') =>
|
||||
ApiErrorResponse(403, description, 'FORBIDDEN');
|
||||
|
||||
export const ApiNotFoundResponse = (description: string = '资源不存在') =>
|
||||
ApiErrorResponse(404, description, 'NOT_FOUND');
|
||||
|
||||
export const ApiConflictResponse = (description: string = '资源冲突') =>
|
||||
ApiErrorResponse(409, description, 'CONFLICT');
|
||||
|
||||
export const ApiValidationResponse = (description: string = '请求参数验证失败') =>
|
||||
ApiErrorResponse(422, description, 'VALIDATION_FAILED');
|
||||
|
||||
export const ApiInternalServerErrorResponse = (description: string = '服务器内部错误') =>
|
||||
ApiErrorResponse(500, description, 'INTERNAL_SERVER_ERROR');
|
||||
34
backend-nestjs/src/common/decorators/cache.decorator.ts
Normal file
34
backend-nestjs/src/common/decorators/cache.decorator.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { CACHE_KEY_METADATA, CACHE_TTL_METADATA } from '../interceptors/cache.interceptor';
|
||||
|
||||
/**
|
||||
* 缓存装饰器
|
||||
* @param key 缓存键
|
||||
* @param ttl 过期时间(秒),默认300秒
|
||||
*/
|
||||
export const Cache = (key: string, ttl: number = 300) => {
|
||||
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
SetMetadata(CACHE_KEY_METADATA, key)(target, propertyKey, descriptor);
|
||||
SetMetadata(CACHE_TTL_METADATA, ttl)(target, propertyKey, descriptor);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 短时间缓存(1分钟)
|
||||
*/
|
||||
export const CacheShort = (key: string) => Cache(key, 60);
|
||||
|
||||
/**
|
||||
* 中等时间缓存(5分钟)
|
||||
*/
|
||||
export const CacheMedium = (key: string) => Cache(key, 300);
|
||||
|
||||
/**
|
||||
* 长时间缓存(30分钟)
|
||||
*/
|
||||
export const CacheLong = (key: string) => Cache(key, 1800);
|
||||
|
||||
/**
|
||||
* 超长时间缓存(2小时)
|
||||
*/
|
||||
export const CacheVeryLong = (key: string) => Cache(key, 7200);
|
||||
4
backend-nestjs/src/common/decorators/public.decorator.ts
Normal file
4
backend-nestjs/src/common/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
45
backend-nestjs/src/common/decorators/rate-limit.decorator.ts
Normal file
45
backend-nestjs/src/common/decorators/rate-limit.decorator.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const RATE_LIMIT_KEY = 'rate_limit';
|
||||
|
||||
export interface RateLimitOptions {
|
||||
windowMs?: number; // 时间窗口(毫秒)
|
||||
maxRequests?: number; // 最大请求数
|
||||
skipSuccessfulRequests?: boolean; // 是否跳过成功请求
|
||||
skipFailedRequests?: boolean; // 是否跳过失败请求
|
||||
keyGenerator?: (req: any) => string; // 自定义key生成器
|
||||
message?: string; // 自定义错误消息
|
||||
}
|
||||
|
||||
/**
|
||||
* 速率限制装饰器
|
||||
*/
|
||||
export const RateLimit = (options: RateLimitOptions = {}) =>
|
||||
SetMetadata(RATE_LIMIT_KEY, {
|
||||
windowMs: 15 * 60 * 1000, // 默认15分钟
|
||||
maxRequests: 100, // 默认100次请求
|
||||
skipSuccessfulRequests: false,
|
||||
skipFailedRequests: false,
|
||||
message: '请求频率过快,请稍后再试',
|
||||
...options,
|
||||
});
|
||||
|
||||
/**
|
||||
* 严格速率限制(用于敏感操作)
|
||||
*/
|
||||
export const StrictRateLimit = (maxRequests: number = 10, windowMs: number = 60 * 1000) =>
|
||||
RateLimit({
|
||||
maxRequests,
|
||||
windowMs,
|
||||
message: '操作频率过快,请稍后再试',
|
||||
});
|
||||
|
||||
/**
|
||||
* 宽松速率限制(用于一般查询)
|
||||
*/
|
||||
export const LooseRateLimit = (maxRequests: number = 1000, windowMs: number = 60 * 1000) =>
|
||||
RateLimit({
|
||||
maxRequests,
|
||||
windowMs,
|
||||
message: '请求次数过多,请稍后再试',
|
||||
});
|
||||
9
backend-nestjs/src/common/decorators/user.decorator.ts
Normal file
9
backend-nestjs/src/common/decorators/user.decorator.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { Admin } from '@database/entities/admin.entity';
|
||||
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext): Admin => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
30
backend-nestjs/src/common/dto/base-response.dto.ts
Normal file
30
backend-nestjs/src/common/dto/base-response.dto.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class BaseResponseDto<T = any> {
|
||||
@ApiProperty({ description: '是否成功' })
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({ description: '状态码' })
|
||||
code: number;
|
||||
|
||||
@ApiProperty({ description: '响应数据' })
|
||||
data: T;
|
||||
|
||||
@ApiProperty({ description: '响应消息' })
|
||||
msg: string;
|
||||
|
||||
constructor(success: boolean, code: number, data: T, msg: string) {
|
||||
this.success = success;
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
static success<T>(data: T = null, msg = 'success'): BaseResponseDto<T> {
|
||||
return new BaseResponseDto(true, 200, data, msg);
|
||||
}
|
||||
|
||||
static error<T>(msg = 'error', code = 500, data: T = null): BaseResponseDto<T> {
|
||||
return new BaseResponseDto(false, code, data, msg);
|
||||
}
|
||||
}
|
||||
45
backend-nestjs/src/common/dto/pagination.dto.ts
Normal file
45
backend-nestjs/src/common/dto/pagination.dto.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsPositive, Min, Max } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class PaginationDto {
|
||||
@ApiPropertyOptional({ description: '页码', default: 1 })
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
@IsPositive()
|
||||
@Min(1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
@IsPositive()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
pageSize?: number = 10;
|
||||
|
||||
get offset(): number {
|
||||
return (this.page - 1) * this.pageSize;
|
||||
}
|
||||
|
||||
get limit(): number {
|
||||
return this.pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
export class PaginationResultDto<T> {
|
||||
@ApiPropertyOptional({ description: '当前页码' })
|
||||
pageNumber: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '总记录数' })
|
||||
totalRow: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '数据列表' })
|
||||
list: T[];
|
||||
|
||||
constructor(page: number, total: number, data: T[]) {
|
||||
this.pageNumber = page;
|
||||
this.totalRow = total;
|
||||
this.list = data;
|
||||
}
|
||||
}
|
||||
175
backend-nestjs/src/common/filters/http-exception.filter.ts
Normal file
175
backend-nestjs/src/common/filters/http-exception.filter.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { QueryFailedError, EntityNotFoundError, CannotCreateEntityIdMapError } from 'typeorm';
|
||||
import { ValidationError } from 'class-validator';
|
||||
|
||||
@Catch()
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(HttpExceptionFilter.name);
|
||||
|
||||
catch(exception: unknown, host: ArgumentsHost): void {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let message = 'Internal server error';
|
||||
let details: any = null;
|
||||
let errorCode = 'INTERNAL_SERVER_ERROR';
|
||||
|
||||
// 处理不同类型的异常
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
errorCode = exception.constructor.name;
|
||||
const exceptionResponse = exception.getResponse();
|
||||
|
||||
if (typeof exceptionResponse === 'string') {
|
||||
message = exceptionResponse;
|
||||
} else if (typeof exceptionResponse === 'object') {
|
||||
message = (exceptionResponse as any).message || exception.message;
|
||||
details = (exceptionResponse as any).details || null;
|
||||
|
||||
// 处理验证错误
|
||||
if ((exceptionResponse as any).message && Array.isArray((exceptionResponse as any).message)) {
|
||||
message = '请求参数验证失败';
|
||||
details = {
|
||||
validationErrors: (exceptionResponse as any).message,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if (exception instanceof QueryFailedError) {
|
||||
// 数据库查询错误
|
||||
status = HttpStatus.BAD_REQUEST;
|
||||
errorCode = 'DATABASE_QUERY_FAILED';
|
||||
message = '数据库操作失败';
|
||||
|
||||
// 处理常见数据库错误
|
||||
if (exception.message.includes('Duplicate entry')) {
|
||||
message = '数据已存在,不能重复创建';
|
||||
status = HttpStatus.CONFLICT;
|
||||
errorCode = 'DUPLICATE_ENTRY';
|
||||
} else if (exception.message.includes('foreign key constraint')) {
|
||||
message = '数据关联约束失败';
|
||||
status = HttpStatus.BAD_REQUEST;
|
||||
errorCode = 'FOREIGN_KEY_CONSTRAINT';
|
||||
}
|
||||
|
||||
details = {
|
||||
query: exception.query,
|
||||
parameters: exception.parameters,
|
||||
};
|
||||
} else if (exception instanceof EntityNotFoundError) {
|
||||
// 实体未找到错误
|
||||
status = HttpStatus.NOT_FOUND;
|
||||
errorCode = 'ENTITY_NOT_FOUND';
|
||||
message = '请求的资源不存在';
|
||||
} else if (exception instanceof CannotCreateEntityIdMapError) {
|
||||
// 实体ID映射错误
|
||||
status = HttpStatus.BAD_REQUEST;
|
||||
errorCode = 'INVALID_ENTITY_ID';
|
||||
message = '无效的实体ID';
|
||||
} else if (exception instanceof Error) {
|
||||
// 普通错误
|
||||
errorCode = exception.constructor.name;
|
||||
message = exception.message;
|
||||
|
||||
// 处理常见错误类型
|
||||
if (exception.name === 'ValidationError') {
|
||||
status = HttpStatus.BAD_REQUEST;
|
||||
message = '数据验证失败';
|
||||
} else if (exception.name === 'UnauthorizedError') {
|
||||
status = HttpStatus.UNAUTHORIZED;
|
||||
message = '未授权访问';
|
||||
} else if (exception.name === 'ForbiddenError') {
|
||||
status = HttpStatus.FORBIDDEN;
|
||||
message = '禁止访问';
|
||||
}
|
||||
}
|
||||
|
||||
// 获取请求ID
|
||||
const requestId = request.headers['x-request-id'] || request.headers['request-id'];
|
||||
|
||||
// 记录错误日志
|
||||
const logMessage = `[${requestId}] ${request.method} ${request.url} - ${errorCode}: ${message}`;
|
||||
const logContext = {
|
||||
requestId,
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
statusCode: status,
|
||||
errorCode,
|
||||
userAgent: request.headers['user-agent'],
|
||||
ip: request.ip,
|
||||
body: this.sanitizeBody(request.body),
|
||||
query: request.query,
|
||||
params: request.params,
|
||||
};
|
||||
|
||||
if (status >= 500) {
|
||||
// 服务器错误记录为error级别
|
||||
this.logger.error(
|
||||
logMessage,
|
||||
exception instanceof Error ? exception.stack : exception,
|
||||
logContext,
|
||||
);
|
||||
} else if (status >= 400) {
|
||||
// 客户端错误记录为warn级别
|
||||
this.logger.warn(logMessage, logContext);
|
||||
}
|
||||
|
||||
// 返回统一格式的错误响应
|
||||
const errorResponse = {
|
||||
success: false,
|
||||
code: status,
|
||||
data: null,
|
||||
msg: message,
|
||||
errorCode,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
requestId,
|
||||
...(details && { details }),
|
||||
};
|
||||
|
||||
// 在开发环境下,包含更多调试信息
|
||||
if (process.env.NODE_ENV === 'development' && exception instanceof Error) {
|
||||
errorResponse['stack'] = exception.stack;
|
||||
}
|
||||
|
||||
response.status(status).json(errorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理敏感信息
|
||||
*/
|
||||
private sanitizeBody(body: any): any {
|
||||
if (!body || typeof body !== 'object') {
|
||||
return body;
|
||||
}
|
||||
|
||||
const sensitiveFields = [
|
||||
'password',
|
||||
'token',
|
||||
'secret',
|
||||
'key',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'session',
|
||||
];
|
||||
|
||||
const sanitized = { ...body };
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (field in sanitized) {
|
||||
sanitized[field] = '***';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
74
backend-nestjs/src/common/global.module.ts
Normal file
74
backend-nestjs/src/common/global.module.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Module, Global, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
||||
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||
|
||||
// 拦截器
|
||||
import { ResponseInterceptor } from './interceptors/response.interceptor';
|
||||
import { LoggingInterceptor } from './interceptors/logging.interceptor';
|
||||
import { CacheInterceptor } from './interceptors/cache.interceptor';
|
||||
import { PerformanceInterceptor } from './interceptors/performance.interceptor';
|
||||
|
||||
// 过滤器
|
||||
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||
|
||||
// 管道
|
||||
import { ValidationPipe } from './pipes/validation.pipe';
|
||||
|
||||
// 中间件
|
||||
import { RequestIdMiddleware } from './middleware/request-id.middleware';
|
||||
import { CorsMiddleware } from './middleware/cors.middleware';
|
||||
|
||||
// 服务
|
||||
import { CacheService } from './services/cache.service';
|
||||
import { PerformanceService } from './services/performance.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
// 服务
|
||||
CacheService,
|
||||
PerformanceService,
|
||||
|
||||
// 全局响应拦截器
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: ResponseInterceptor,
|
||||
},
|
||||
// 全局日志拦截器
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: LoggingInterceptor,
|
||||
},
|
||||
// 全局缓存拦截器
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: CacheInterceptor,
|
||||
},
|
||||
// 全局性能监控拦截器
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: PerformanceInterceptor,
|
||||
},
|
||||
// 全局异常过滤器
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: HttpExceptionFilter,
|
||||
},
|
||||
// 全局验证管道
|
||||
{
|
||||
provide: APP_PIPE,
|
||||
useClass: ValidationPipe,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
CacheService,
|
||||
PerformanceService,
|
||||
],
|
||||
})
|
||||
export class GlobalModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
// 应用全局中间件
|
||||
consumer
|
||||
.apply(RequestIdMiddleware, CorsMiddleware)
|
||||
.forRoutes('*'); // 应用到所有路由
|
||||
}
|
||||
}
|
||||
35
backend-nestjs/src/common/guards/jwt-auth.guard.ts
Normal file
35
backend-nestjs/src/common/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
Injectable,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { IS_PUBLIC_KEY } from '@common/decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
handleRequest(err, user, info) {
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException('Token验证失败');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
69
backend-nestjs/src/common/interceptors/cache.interceptor.ts
Normal file
69
backend-nestjs/src/common/interceptors/cache.interceptor.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { RedisService } from '@shared/redis/redis.service';
|
||||
|
||||
export const CACHE_KEY_METADATA = 'cache_key';
|
||||
export const CACHE_TTL_METADATA = 'cache_ttl';
|
||||
|
||||
@Injectable()
|
||||
export class CacheInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(CacheInterceptor.name);
|
||||
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly redisService: RedisService,
|
||||
) {}
|
||||
|
||||
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
|
||||
const cacheKey = this.reflector.get<string>(CACHE_KEY_METADATA, context.getHandler());
|
||||
const cacheTTL = this.reflector.get<number>(CACHE_TTL_METADATA, context.getHandler()) || 300; // 默认5分钟
|
||||
|
||||
if (!cacheKey) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const fullCacheKey = this.generateCacheKey(cacheKey, request);
|
||||
|
||||
try {
|
||||
// 尝试从缓存获取数据
|
||||
const cachedData = await this.redisService.get(fullCacheKey);
|
||||
if (cachedData) {
|
||||
this.logger.debug(`缓存命中: ${fullCacheKey}`);
|
||||
return of(JSON.parse(cachedData));
|
||||
}
|
||||
|
||||
// 缓存未命中,执行原始逻辑
|
||||
return next.handle().pipe(
|
||||
tap(async (data) => {
|
||||
try {
|
||||
await this.redisService.set(fullCacheKey, JSON.stringify(data), cacheTTL);
|
||||
this.logger.debug(`缓存设置: ${fullCacheKey}, TTL: ${cacheTTL}s`);
|
||||
} catch (error) {
|
||||
this.logger.warn(`缓存设置失败: ${error.message}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn(`缓存读取失败: ${error.message}`);
|
||||
return next.handle();
|
||||
}
|
||||
}
|
||||
|
||||
private generateCacheKey(baseKey: string, request: any): string {
|
||||
const url = request.url;
|
||||
const method = request.method;
|
||||
const userId = request.user?.id || 'anonymous';
|
||||
const queryParams = JSON.stringify(request.query || {});
|
||||
|
||||
return `${baseKey}:${method}:${url}:${userId}:${Buffer.from(queryParams).toString('base64')}`;
|
||||
}
|
||||
}
|
||||
140
backend-nestjs/src/common/interceptors/logging.interceptor.ts
Normal file
140
backend-nestjs/src/common/interceptors/logging.interceptor.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap, catchError } from 'rxjs/operators';
|
||||
import { throwError } from 'rxjs';
|
||||
import { Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(LoggingInterceptor.name);
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const now = Date.now();
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const response = context.switchToHttp().getResponse<Response>();
|
||||
const { method, url, body, query, params, headers, ip } = request;
|
||||
|
||||
// 生成请求ID
|
||||
const requestId = this.generateRequestId();
|
||||
request.headers['x-request-id'] = requestId;
|
||||
|
||||
// 记录请求开始
|
||||
this.logger.log(
|
||||
`[${requestId}] ${method} ${url} - START`,
|
||||
{
|
||||
method,
|
||||
url,
|
||||
body: this.sanitizeBody(body),
|
||||
query,
|
||||
params,
|
||||
userAgent: headers['user-agent'],
|
||||
ip,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap((data) => {
|
||||
// 记录请求成功
|
||||
const duration = Date.now() - now;
|
||||
this.logger.log(
|
||||
`[${requestId}] ${method} ${url} - SUCCESS ${response.statusCode} - ${duration}ms`,
|
||||
{
|
||||
method,
|
||||
url,
|
||||
statusCode: response.statusCode,
|
||||
duration: `${duration}ms`,
|
||||
responseSize: this.getResponseSize(data),
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
}),
|
||||
catchError((error) => {
|
||||
// 记录请求失败
|
||||
const duration = Date.now() - now;
|
||||
this.logger.error(
|
||||
`[${requestId}] ${method} ${url} - ERROR ${response.statusCode || 500} - ${duration}ms`,
|
||||
{
|
||||
method,
|
||||
url,
|
||||
statusCode: response.statusCode || 500,
|
||||
duration: `${duration}ms`,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
);
|
||||
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求ID
|
||||
*/
|
||||
private generateRequestId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理敏感信息
|
||||
*/
|
||||
private sanitizeBody(body: any): any {
|
||||
if (!body || typeof body !== 'object') {
|
||||
return body;
|
||||
}
|
||||
|
||||
const sensitiveFields = [
|
||||
'password',
|
||||
'token',
|
||||
'secret',
|
||||
'key',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'session',
|
||||
];
|
||||
|
||||
const sanitized = { ...body };
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (field in sanitized) {
|
||||
sanitized[field] = '***';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算响应大小
|
||||
*/
|
||||
private getResponseSize(data: any): string {
|
||||
if (!data) return '0B';
|
||||
|
||||
try {
|
||||
const size = JSON.stringify(data).length;
|
||||
return this.formatBytes(size);
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字节大小
|
||||
*/
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0B';
|
||||
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + sizes[i];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap, timeout, catchError } from 'rxjs/operators';
|
||||
import { TimeoutError, throwError } from 'rxjs';
|
||||
import { AnalyticsService } from '@modules/analytics/services/analytics.service';
|
||||
|
||||
@Injectable()
|
||||
export class PerformanceInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(PerformanceInterceptor.name);
|
||||
private readonly slowQueryThreshold = 1000; // 1秒
|
||||
private readonly requestTimeout = 30000; // 30秒
|
||||
|
||||
constructor(private readonly analyticsService: AnalyticsService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const response = context.switchToHttp().getResponse();
|
||||
const startTime = Date.now();
|
||||
|
||||
const method = request.method;
|
||||
const url = request.url;
|
||||
const userAgent = request.headers['user-agent'];
|
||||
const ip = request.ip;
|
||||
|
||||
return next.handle().pipe(
|
||||
timeout(this.requestTimeout),
|
||||
tap(() => {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
// 记录性能指标
|
||||
this.recordPerformanceMetrics(request, response, duration);
|
||||
|
||||
// 记录慢查询
|
||||
if (duration > this.slowQueryThreshold) {
|
||||
this.logger.warn(`慢请求检测: ${method} ${url} - ${duration}ms`);
|
||||
this.recordSlowQuery(request, duration);
|
||||
}
|
||||
}),
|
||||
catchError((error) => {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
if (error instanceof TimeoutError) {
|
||||
this.logger.error(`请求超时: ${method} ${url} - ${duration}ms`);
|
||||
this.recordTimeoutError(request, duration);
|
||||
} else {
|
||||
this.logger.error(`请求错误: ${method} ${url} - ${error.message}`);
|
||||
this.recordRequestError(request, error, duration);
|
||||
}
|
||||
|
||||
return throwError(error);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录性能指标
|
||||
*/
|
||||
private async recordPerformanceMetrics(request: any, response: any, duration: number) {
|
||||
try {
|
||||
await this.analyticsService.recordEvent({
|
||||
eventType: 'performance_metric',
|
||||
eventName: 'api_response_time',
|
||||
entityType: 'api',
|
||||
value: duration,
|
||||
unit: 'ms',
|
||||
eventData: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
statusCode: response.statusCode,
|
||||
userAgent: request.headers['user-agent'],
|
||||
},
|
||||
context: {
|
||||
ip: request.ip,
|
||||
requestId: request.headers['x-request-id'],
|
||||
},
|
||||
}, request);
|
||||
} catch (error) {
|
||||
this.logger.warn(`记录性能指标失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录慢查询
|
||||
*/
|
||||
private async recordSlowQuery(request: any, duration: number) {
|
||||
try {
|
||||
await this.analyticsService.recordEvent({
|
||||
eventType: 'performance_metric',
|
||||
eventName: 'slow_query',
|
||||
entityType: 'api',
|
||||
value: duration,
|
||||
unit: 'ms',
|
||||
eventData: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
threshold: this.slowQueryThreshold,
|
||||
query: request.query,
|
||||
body: this.sanitizeBody(request.body),
|
||||
},
|
||||
context: {
|
||||
ip: request.ip,
|
||||
userAgent: request.headers['user-agent'],
|
||||
},
|
||||
}, request);
|
||||
} catch (error) {
|
||||
this.logger.warn(`记录慢查询失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录超时错误
|
||||
*/
|
||||
private async recordTimeoutError(request: any, duration: number) {
|
||||
try {
|
||||
await this.analyticsService.recordEvent({
|
||||
eventType: 'error_event',
|
||||
eventName: 'request_timeout',
|
||||
entityType: 'api',
|
||||
value: duration,
|
||||
unit: 'ms',
|
||||
eventData: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
timeout: this.requestTimeout,
|
||||
error: 'Request timeout',
|
||||
},
|
||||
context: {
|
||||
ip: request.ip,
|
||||
userAgent: request.headers['user-agent'],
|
||||
},
|
||||
}, request);
|
||||
} catch (error) {
|
||||
this.logger.warn(`记录超时错误失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求错误
|
||||
*/
|
||||
private async recordRequestError(request: any, error: any, duration: number) {
|
||||
try {
|
||||
await this.analyticsService.recordEvent({
|
||||
eventType: 'error_event',
|
||||
eventName: 'request_error',
|
||||
entityType: 'api',
|
||||
value: duration,
|
||||
unit: 'ms',
|
||||
eventData: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
statusCode: error.status || 500,
|
||||
},
|
||||
context: {
|
||||
ip: request.ip,
|
||||
userAgent: request.headers['user-agent'],
|
||||
},
|
||||
}, request);
|
||||
} catch (recordError) {
|
||||
this.logger.warn(`记录请求错误失败: ${recordError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理敏感数据
|
||||
*/
|
||||
private sanitizeBody(body: any): any {
|
||||
if (!body) return null;
|
||||
|
||||
const sanitized = { ...body };
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key', 'authorization'];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
127
backend-nestjs/src/common/interceptors/response.interceptor.ts
Normal file
127
backend-nestjs/src/common/interceptors/response.interceptor.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
// 响应格式装饰器
|
||||
export const RESPONSE_MESSAGE_KEY = 'response_message';
|
||||
export const ResponseMessage = (message: string) =>
|
||||
Reflector.createDecorator<string>()[RESPONSE_MESSAGE_KEY](message);
|
||||
|
||||
// 跳过响应包装装饰器
|
||||
export const SKIP_RESPONSE_WRAP_KEY = 'skip_response_wrap';
|
||||
export const SkipResponseWrap = () =>
|
||||
Reflector.createDecorator<boolean>()[SKIP_RESPONSE_WRAP_KEY](true);
|
||||
|
||||
// 标准响应格式接口
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
code: number;
|
||||
data: T;
|
||||
msg: string;
|
||||
timestamp?: string;
|
||||
path?: string;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ResponseInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const response = context.switchToHttp().getResponse();
|
||||
|
||||
// 检查是否跳过响应包装
|
||||
const skipWrap = this.reflector.getAllAndOverride<boolean>(
|
||||
SKIP_RESPONSE_WRAP_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
if (skipWrap) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
// 获取自定义响应消息
|
||||
const message = this.reflector.getAllAndOverride<string>(
|
||||
RESPONSE_MESSAGE_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
// 如果数据已经是标准格式,直接返回
|
||||
if (this.isApiResponse(data)) {
|
||||
return {
|
||||
...data,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
requestId: request.headers['x-request-id'] || request.headers['request-id'],
|
||||
};
|
||||
}
|
||||
|
||||
// 包装成标准响应格式
|
||||
const result: ApiResponse<T> = {
|
||||
success: true,
|
||||
code: response.statusCode || 200,
|
||||
data: data,
|
||||
msg: message || this.getDefaultMessage(response.statusCode),
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
requestId: request.headers['x-request-id'] || request.headers['request-id'],
|
||||
};
|
||||
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据是否已经是API响应格式
|
||||
*/
|
||||
private isApiResponse(data: any): data is ApiResponse {
|
||||
return (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
'success' in data &&
|
||||
'code' in data &&
|
||||
'data' in data &&
|
||||
'msg' in data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态码获取默认消息
|
||||
*/
|
||||
private getDefaultMessage(statusCode: number): string {
|
||||
switch (statusCode) {
|
||||
case 200:
|
||||
return '操作成功';
|
||||
case 201:
|
||||
return '创建成功';
|
||||
case 204:
|
||||
return '操作成功';
|
||||
case 400:
|
||||
return '请求参数错误';
|
||||
case 401:
|
||||
return '未授权访问';
|
||||
case 403:
|
||||
return '禁止访问';
|
||||
case 404:
|
||||
return '资源不存在';
|
||||
case 409:
|
||||
return '资源冲突';
|
||||
case 422:
|
||||
return '请求参数验证失败';
|
||||
case 500:
|
||||
return '服务器内部错误';
|
||||
default:
|
||||
return '操作完成';
|
||||
}
|
||||
}
|
||||
}
|
||||
48
backend-nestjs/src/common/middleware/cors.middleware.ts
Normal file
48
backend-nestjs/src/common/middleware/cors.middleware.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class CorsMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const origin = req.headers.origin;
|
||||
const allowedOrigins = [
|
||||
'http://localhost:3000',
|
||||
'http://localhost:3001',
|
||||
'http://localhost:8080',
|
||||
'https://your-domain.com',
|
||||
// 从环境变量中读取允许的域名
|
||||
...(process.env.ALLOWED_ORIGINS?.split(',') || []),
|
||||
];
|
||||
|
||||
// 检查请求来源是否被允许
|
||||
if (!origin || allowedOrigins.includes(origin)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
||||
}
|
||||
|
||||
// 设置允许的请求方法
|
||||
res.setHeader(
|
||||
'Access-Control-Allow-Methods',
|
||||
'GET, POST, PUT, DELETE, PATCH, OPTIONS'
|
||||
);
|
||||
|
||||
// 设置允许的请求头
|
||||
res.setHeader(
|
||||
'Access-Control-Allow-Headers',
|
||||
'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Request-ID'
|
||||
);
|
||||
|
||||
// 设置允许携带凭证
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
// 设置预检请求的缓存时间
|
||||
res.setHeader('Access-Control-Max-Age', '86400'); // 24小时
|
||||
|
||||
// 处理预检请求
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
128
backend-nestjs/src/common/middleware/rate-limit.middleware.ts
Normal file
128
backend-nestjs/src/common/middleware/rate-limit.middleware.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { Injectable, NestMiddleware, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
interface RateLimitStore {
|
||||
[key: string]: {
|
||||
count: number;
|
||||
resetTime: number;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RateLimitMiddleware implements NestMiddleware {
|
||||
private store: RateLimitStore = {};
|
||||
private readonly windowMs: number;
|
||||
private readonly maxRequests: number;
|
||||
|
||||
constructor(
|
||||
windowMs: number = 15 * 60 * 1000, // 15分钟
|
||||
maxRequests: number = 100, // 最大请求数
|
||||
) {
|
||||
this.windowMs = windowMs;
|
||||
this.maxRequests = maxRequests;
|
||||
|
||||
// 定期清理过期记录
|
||||
setInterval(() => {
|
||||
this.cleanup();
|
||||
}, this.windowMs);
|
||||
}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const key = this.generateKey(req);
|
||||
const now = Date.now();
|
||||
|
||||
// 获取或创建记录
|
||||
if (!this.store[key]) {
|
||||
this.store[key] = {
|
||||
count: 0,
|
||||
resetTime: now + this.windowMs,
|
||||
};
|
||||
}
|
||||
|
||||
const record = this.store[key];
|
||||
|
||||
// 检查是否需要重置
|
||||
if (now > record.resetTime) {
|
||||
record.count = 0;
|
||||
record.resetTime = now + this.windowMs;
|
||||
}
|
||||
|
||||
// 增加请求计数
|
||||
record.count++;
|
||||
|
||||
// 设置响应头
|
||||
res.setHeader('X-RateLimit-Limit', this.maxRequests);
|
||||
res.setHeader('X-RateLimit-Remaining', Math.max(0, this.maxRequests - record.count));
|
||||
res.setHeader('X-RateLimit-Reset', new Date(record.resetTime).toISOString());
|
||||
|
||||
// 检查是否超过限制
|
||||
if (record.count > this.maxRequests) {
|
||||
const retryAfter = Math.ceil((record.resetTime - now) / 1000);
|
||||
res.setHeader('Retry-After', retryAfter);
|
||||
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
code: HttpStatus.TOO_MANY_REQUESTS,
|
||||
data: null,
|
||||
msg: '请求频率过快,请稍后再试',
|
||||
retryAfter,
|
||||
},
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成限流key
|
||||
*/
|
||||
private generateKey(req: Request): string {
|
||||
// 使用IP地址和用户ID(如果存在)作为key
|
||||
const ip = req.ip || req.connection.remoteAddress;
|
||||
const userId = (req as any).user?.id;
|
||||
|
||||
return userId ? `user:${userId}` : `ip:${ip}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期记录
|
||||
*/
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
|
||||
for (const key in this.store) {
|
||||
if (this.store[key].resetTime < now) {
|
||||
delete this.store[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定key的限制
|
||||
*/
|
||||
reset(key: string): void {
|
||||
delete this.store[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定key的状态
|
||||
*/
|
||||
getStatus(key: string) {
|
||||
const record = this.store[key];
|
||||
if (!record) {
|
||||
return {
|
||||
count: 0,
|
||||
remaining: this.maxRequests,
|
||||
resetTime: Date.now() + this.windowMs,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
count: record.count,
|
||||
remaining: Math.max(0, this.maxRequests - record.count),
|
||||
resetTime: record.resetTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class RequestIdMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
// 检查请求头中是否已有请求ID
|
||||
let requestId = req.headers['x-request-id'] || req.headers['request-id'];
|
||||
|
||||
// 如果没有请求ID,生成一个新的
|
||||
if (!requestId) {
|
||||
requestId = uuidv4();
|
||||
}
|
||||
|
||||
// 设置请求ID到请求头
|
||||
req.headers['x-request-id'] = requestId as string;
|
||||
|
||||
// 设置响应头
|
||||
res.setHeader('X-Request-ID', requestId);
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
69
backend-nestjs/src/common/pipes/parse-int.pipe.ts
Normal file
69
backend-nestjs/src/common/pipes/parse-int.pipe.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
ArgumentMetadata,
|
||||
Injectable,
|
||||
PipeTransform,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ParseIntPipe implements PipeTransform<string, number> {
|
||||
constructor(
|
||||
private readonly options?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
optional?: boolean;
|
||||
},
|
||||
) {}
|
||||
|
||||
transform(value: string, metadata: ArgumentMetadata): number {
|
||||
// 如果是可选的且值为空,返回undefined
|
||||
if (this.options?.optional && (value === undefined || value === null || value === '')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// 如果值为空但不是可选的,抛出错误
|
||||
if (value === undefined || value === null || value === '') {
|
||||
throw new BadRequestException({
|
||||
success: false,
|
||||
code: 400,
|
||||
data: null,
|
||||
msg: `参数 ${metadata.data} 不能为空`,
|
||||
});
|
||||
}
|
||||
|
||||
// 尝试转换为数字
|
||||
const num = parseInt(value, 10);
|
||||
|
||||
// 检查是否为有效数字
|
||||
if (isNaN(num)) {
|
||||
throw new BadRequestException({
|
||||
success: false,
|
||||
code: 400,
|
||||
data: null,
|
||||
msg: `参数 ${metadata.data} 必须是有效的整数`,
|
||||
});
|
||||
}
|
||||
|
||||
// 检查最小值
|
||||
if (this.options?.min !== undefined && num < this.options.min) {
|
||||
throw new BadRequestException({
|
||||
success: false,
|
||||
code: 400,
|
||||
data: null,
|
||||
msg: `参数 ${metadata.data} 不能小于 ${this.options.min}`,
|
||||
});
|
||||
}
|
||||
|
||||
// 检查最大值
|
||||
if (this.options?.max !== undefined && num > this.options.max) {
|
||||
throw new BadRequestException({
|
||||
success: false,
|
||||
code: 400,
|
||||
data: null,
|
||||
msg: `参数 ${metadata.data} 不能大于 ${this.options.max}`,
|
||||
});
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
}
|
||||
68
backend-nestjs/src/common/pipes/validation.pipe.ts
Normal file
68
backend-nestjs/src/common/pipes/validation.pipe.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
ArgumentMetadata,
|
||||
Injectable,
|
||||
PipeTransform,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
|
||||
@Injectable()
|
||||
export class ValidationPipe implements PipeTransform<any> {
|
||||
async transform(value: any, { metatype }: ArgumentMetadata) {
|
||||
// 如果没有元类型或者是原始类型,直接返回
|
||||
if (!metatype || !this.toValidate(metatype)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 转换为类实例
|
||||
const object = plainToClass(metatype, value);
|
||||
|
||||
// 执行验证
|
||||
const errors = await validate(object, {
|
||||
whitelist: true, // 只保留装饰器标记的属性
|
||||
forbidNonWhitelisted: true, // 禁止非白名单属性
|
||||
transform: true, // 自动转换类型
|
||||
validateCustomDecorators: true, // 验证自定义装饰器
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
// 格式化错误信息
|
||||
const errorMessages = this.formatErrors(errors);
|
||||
|
||||
throw new BadRequestException({
|
||||
success: false,
|
||||
code: 400,
|
||||
data: null,
|
||||
msg: '请求参数验证失败',
|
||||
details: {
|
||||
validationErrors: errorMessages,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要验证
|
||||
*/
|
||||
private toValidate(metatype: Function): boolean {
|
||||
const types: Function[] = [String, Boolean, Number, Array, Object];
|
||||
return !types.includes(metatype);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化验证错误
|
||||
*/
|
||||
private formatErrors(errors: any[]): any[] {
|
||||
return errors.map(error => ({
|
||||
property: error.property,
|
||||
value: error.value,
|
||||
constraints: error.constraints,
|
||||
children: error.children?.length > 0
|
||||
? this.formatErrors(error.children)
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
246
backend-nestjs/src/common/services/cache.service.ts
Normal file
246
backend-nestjs/src/common/services/cache.service.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { RedisService } from '@shared/redis/redis.service';
|
||||
|
||||
export interface CacheOptions {
|
||||
ttl?: number; // 过期时间(秒)
|
||||
prefix?: string; // 缓存键前缀
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
private readonly logger = new Logger(CacheService.name);
|
||||
private readonly defaultTTL = 300; // 5分钟
|
||||
private readonly defaultPrefix = 'tg_cache';
|
||||
|
||||
constructor(private readonly redisService: RedisService) {}
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*/
|
||||
async get<T>(key: string, options?: CacheOptions): Promise<T | null> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
const data = await this.redisService.get(fullKey);
|
||||
|
||||
if (data) {
|
||||
this.logger.debug(`缓存命中: ${fullKey}`);
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.warn(`获取缓存失败: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*/
|
||||
async set<T>(key: string, value: T, options?: CacheOptions): Promise<void> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
const ttl = options?.ttl || this.defaultTTL;
|
||||
|
||||
await this.redisService.set(fullKey, JSON.stringify(value), ttl);
|
||||
this.logger.debug(`缓存设置: ${fullKey}, TTL: ${ttl}s`);
|
||||
} catch (error) {
|
||||
this.logger.warn(`设置缓存失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
async del(key: string, options?: CacheOptions): Promise<void> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
await this.redisService.del(fullKey);
|
||||
this.logger.debug(`缓存删除: ${fullKey}`);
|
||||
} catch (error) {
|
||||
this.logger.warn(`删除缓存失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除缓存(通过模式匹配)
|
||||
*/
|
||||
async delByPattern(pattern: string, options?: CacheOptions): Promise<void> {
|
||||
try {
|
||||
const fullPattern = this.buildKey(pattern, options?.prefix);
|
||||
await this.redisService.clearCache(fullPattern);
|
||||
this.logger.debug(`批量删除缓存: ${fullPattern}`);
|
||||
} catch (error) {
|
||||
this.logger.warn(`批量删除缓存失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或设置缓存
|
||||
*/
|
||||
async getOrSet<T>(
|
||||
key: string,
|
||||
factory: () => Promise<T>,
|
||||
options?: CacheOptions
|
||||
): Promise<T> {
|
||||
const cached = await this.get<T>(key, options);
|
||||
|
||||
if (cached !== null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const data = await factory();
|
||||
await this.set(key, data, options);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否存在
|
||||
*/
|
||||
async exists(key: string, options?: CacheOptions): Promise<boolean> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
return await this.redisService.exists(fullKey);
|
||||
} catch (error) {
|
||||
this.logger.warn(`检查缓存存在失败: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存过期时间
|
||||
*/
|
||||
async expire(key: string, ttl: number, options?: CacheOptions): Promise<void> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
// Redis service doesn't have expire method, so we'll get and set with new TTL
|
||||
const value = await this.get(key, options);
|
||||
if (value !== null) {
|
||||
await this.set(key, value, { ...options, ttl });
|
||||
}
|
||||
this.logger.debug(`设置缓存过期时间: ${fullKey}, TTL: ${ttl}s`);
|
||||
} catch (error) {
|
||||
this.logger.warn(`设置缓存过期时间失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存剩余过期时间
|
||||
*/
|
||||
async ttl(key: string, options?: CacheOptions): Promise<number> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
return await this.redisService.ttl(fullKey);
|
||||
} catch (error) {
|
||||
this.logger.warn(`获取缓存TTL失败: ${error.message}`);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加计数器
|
||||
*/
|
||||
async increment(key: string, increment = 1, options?: CacheOptions): Promise<number> {
|
||||
try {
|
||||
// Get current value or 0 if doesn't exist
|
||||
const currentValue = await this.get<number>(key, options) || 0;
|
||||
const newValue = currentValue + increment;
|
||||
|
||||
// Set new value with TTL
|
||||
const ttl = options?.ttl || this.defaultTTL;
|
||||
await this.set(key, newValue, { ...options, ttl });
|
||||
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
this.logger.warn(`递增计数器失败: ${error.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少计数器
|
||||
*/
|
||||
async decrement(key: string, decrement = 1, options?: CacheOptions): Promise<number> {
|
||||
return this.increment(key, -decrement, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
async flushAll(): Promise<void> {
|
||||
try {
|
||||
await this.redisService.clearCache('*');
|
||||
this.logger.log('清空所有缓存');
|
||||
} catch (error) {
|
||||
this.logger.error(`清空缓存失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的缓存键
|
||||
*/
|
||||
private buildKey(key: string, prefix?: string): string {
|
||||
const actualPrefix = prefix || this.defaultPrefix;
|
||||
return `${actualPrefix}:${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存预热 - TG账号
|
||||
*/
|
||||
async warmupTgAccounts(): Promise<void> {
|
||||
this.logger.log('开始TG账号缓存预热...');
|
||||
// 这里可以预加载常用的TG账号数据
|
||||
// 实际实现时需要注入相关服务
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存预热 - 系统配置
|
||||
*/
|
||||
async warmupSystemConfig(): Promise<void> {
|
||||
this.logger.log('开始系统配置缓存预热...');
|
||||
// 这里可以预加载系统配置
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存统计信息
|
||||
*/
|
||||
async getCacheStats(): Promise<any> {
|
||||
try {
|
||||
// Since Redis service doesn't have info method, provide basic stats
|
||||
return {
|
||||
memory: {
|
||||
used_memory: 0,
|
||||
used_memory_peak: 0
|
||||
},
|
||||
keyspace: {
|
||||
db0: {
|
||||
keys: 0,
|
||||
expires: 0
|
||||
}
|
||||
},
|
||||
timestamp: new Date(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.warn(`获取缓存统计失败: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Redis信息
|
||||
*/
|
||||
private parseRedisInfo(info: string): any {
|
||||
const result: any = {};
|
||||
const lines = info.split('\r\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes(':')) {
|
||||
const [key, value] = line.split(':');
|
||||
result[key] = isNaN(Number(value)) ? value : Number(value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
243
backend-nestjs/src/common/services/logger.service.ts
Normal file
243
backend-nestjs/src/common/services/logger.service.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { Injectable, LoggerService as NestLoggerService } from '@nestjs/common';
|
||||
import { createLogger, format, transports, Logger as WinstonLogger } from 'winston';
|
||||
import * as DailyRotateFile from 'winston-daily-rotate-file';
|
||||
import { join } from 'path';
|
||||
|
||||
@Injectable()
|
||||
export class LoggerService implements NestLoggerService {
|
||||
private logger: WinstonLogger;
|
||||
|
||||
constructor() {
|
||||
this.createLogger();
|
||||
}
|
||||
|
||||
private createLogger() {
|
||||
// 日志格式
|
||||
const logFormat = format.combine(
|
||||
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
format.errors({ stack: true }),
|
||||
format.json(),
|
||||
format.printf(({ timestamp, level, message, context, stack, ...meta }) => {
|
||||
let log = `${timestamp} [${level.toUpperCase()}]`;
|
||||
|
||||
if (context) {
|
||||
log += ` [${context}]`;
|
||||
}
|
||||
|
||||
log += ` ${message}`;
|
||||
|
||||
if (Object.keys(meta).length > 0) {
|
||||
log += ` ${JSON.stringify(meta)}`;
|
||||
}
|
||||
|
||||
if (stack) {
|
||||
log += `\n${stack}`;
|
||||
}
|
||||
|
||||
return log;
|
||||
})
|
||||
);
|
||||
|
||||
// 控制台日志格式
|
||||
const consoleFormat = format.combine(
|
||||
format.colorize(),
|
||||
format.timestamp({ format: 'HH:mm:ss' }),
|
||||
format.printf(({ timestamp, level, message, context }) => {
|
||||
let log = `${timestamp} ${level}`;
|
||||
|
||||
if (context) {
|
||||
log += ` [${context}]`;
|
||||
}
|
||||
|
||||
log += ` ${message}`;
|
||||
|
||||
return log;
|
||||
})
|
||||
);
|
||||
|
||||
// 日志目录
|
||||
const logDir = join(process.cwd(), 'logs');
|
||||
|
||||
// 创建Winston logger
|
||||
this.logger = createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: logFormat,
|
||||
transports: [
|
||||
// 控制台输出
|
||||
new transports.Console({
|
||||
format: consoleFormat,
|
||||
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||
}),
|
||||
|
||||
// 信息日志文件(按日期滚动)
|
||||
new DailyRotateFile({
|
||||
filename: join(logDir, 'app-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '14d',
|
||||
level: 'info',
|
||||
format: logFormat,
|
||||
}),
|
||||
|
||||
// 错误日志文件(按日期滚动)
|
||||
new DailyRotateFile({
|
||||
filename: join(logDir, 'error-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '30d',
|
||||
level: 'error',
|
||||
format: logFormat,
|
||||
}),
|
||||
|
||||
// 调试日志文件(只在开发环境)
|
||||
...(process.env.NODE_ENV === 'development' ? [
|
||||
new DailyRotateFile({
|
||||
filename: join(logDir, 'debug-%DATE%.log'),
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxSize: '20m',
|
||||
maxFiles: '7d',
|
||||
level: 'debug',
|
||||
format: logFormat,
|
||||
})
|
||||
] : []),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
log(message: any, context?: string) {
|
||||
this.logger.info(message, { context });
|
||||
}
|
||||
|
||||
error(message: any, stack?: string, context?: string) {
|
||||
this.logger.error(message, { stack, context });
|
||||
}
|
||||
|
||||
warn(message: any, context?: string) {
|
||||
this.logger.warn(message, { context });
|
||||
}
|
||||
|
||||
debug(message: any, context?: string) {
|
||||
this.logger.debug(message, { context });
|
||||
}
|
||||
|
||||
verbose(message: any, context?: string) {
|
||||
this.logger.verbose(message, { context });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录HTTP请求
|
||||
*/
|
||||
logRequest(req: any, res: any, responseTime: number) {
|
||||
const { method, url, headers, body, query, params, ip } = req;
|
||||
const { statusCode } = res;
|
||||
|
||||
this.logger.info('HTTP Request', {
|
||||
context: 'HttpRequest',
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
responseTime: `${responseTime}ms`,
|
||||
ip,
|
||||
userAgent: headers['user-agent'],
|
||||
requestId: headers['x-request-id'],
|
||||
body: this.sanitizeBody(body),
|
||||
query,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录数据库操作
|
||||
*/
|
||||
logDatabase(operation: string, table: string, executionTime: number, query?: string) {
|
||||
this.logger.debug('Database Operation', {
|
||||
context: 'Database',
|
||||
operation,
|
||||
table,
|
||||
executionTime: `${executionTime}ms`,
|
||||
query: query?.substring(0, 500), // 限制查询长度
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录业务操作
|
||||
*/
|
||||
logBusiness(operation: string, userId?: number, details?: any) {
|
||||
this.logger.info('Business Operation', {
|
||||
context: 'Business',
|
||||
operation,
|
||||
userId,
|
||||
details,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录安全事件
|
||||
*/
|
||||
logSecurity(event: string, userId?: number, ip?: string, details?: any) {
|
||||
this.logger.warn('Security Event', {
|
||||
context: 'Security',
|
||||
event,
|
||||
userId,
|
||||
ip,
|
||||
details,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录性能指标
|
||||
*/
|
||||
logPerformance(operation: string, duration: number, details?: any) {
|
||||
if (duration > 1000) { // 超过1秒的操作记录为警告
|
||||
this.logger.warn('Slow Operation', {
|
||||
context: 'Performance',
|
||||
operation,
|
||||
duration: `${duration}ms`,
|
||||
details,
|
||||
});
|
||||
} else {
|
||||
this.logger.debug('Performance Metric', {
|
||||
context: 'Performance',
|
||||
operation,
|
||||
duration: `${duration}ms`,
|
||||
details,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理敏感信息
|
||||
*/
|
||||
private sanitizeBody(body: any): any {
|
||||
if (!body || typeof body !== 'object') {
|
||||
return body;
|
||||
}
|
||||
|
||||
const sensitiveFields = [
|
||||
'password',
|
||||
'token',
|
||||
'secret',
|
||||
'key',
|
||||
'authorization',
|
||||
'cookie',
|
||||
'session',
|
||||
];
|
||||
|
||||
const sanitized = { ...body };
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (field in sanitized) {
|
||||
sanitized[field] = '***';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Winston logger实例
|
||||
*/
|
||||
getWinstonLogger(): WinstonLogger {
|
||||
return this.logger;
|
||||
}
|
||||
}
|
||||
324
backend-nestjs/src/common/services/performance.service.ts
Normal file
324
backend-nestjs/src/common/services/performance.service.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { CacheService } from './cache.service';
|
||||
import { AnalyticsService } from '@modules/analytics/services/analytics.service';
|
||||
|
||||
@Injectable()
|
||||
export class PerformanceService {
|
||||
private readonly logger = new Logger(PerformanceService.name);
|
||||
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取系统性能概览
|
||||
*/
|
||||
async getPerformanceOverview(): Promise<any> {
|
||||
const now = new Date();
|
||||
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const [
|
||||
currentMetrics,
|
||||
hourlyMetrics,
|
||||
dailyMetrics,
|
||||
cacheStats,
|
||||
slowQueries,
|
||||
] = await Promise.all([
|
||||
this.getCurrentSystemMetrics(),
|
||||
this.getPerformanceMetrics(oneHourAgo, now),
|
||||
this.getPerformanceMetrics(oneDayAgo, now),
|
||||
this.cacheService.getCacheStats(),
|
||||
this.getSlowQueries(oneDayAgo, now),
|
||||
]);
|
||||
|
||||
return {
|
||||
timestamp: now,
|
||||
current: currentMetrics,
|
||||
hourly: hourlyMetrics,
|
||||
daily: dailyMetrics,
|
||||
cache: cacheStats,
|
||||
slowQueries,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前系统指标
|
||||
*/
|
||||
async getCurrentSystemMetrics(): Promise<any> {
|
||||
const memoryUsage = process.memoryUsage();
|
||||
const cpuUsage = process.cpuUsage();
|
||||
|
||||
return {
|
||||
uptime: process.uptime(),
|
||||
memory: {
|
||||
rss: memoryUsage.rss,
|
||||
heapTotal: memoryUsage.heapTotal,
|
||||
heapUsed: memoryUsage.heapUsed,
|
||||
external: memoryUsage.external,
|
||||
heapUsedPercentage: (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100,
|
||||
},
|
||||
cpu: {
|
||||
user: cpuUsage.user,
|
||||
system: cpuUsage.system,
|
||||
},
|
||||
eventLoop: {
|
||||
delay: await this.getEventLoopDelay(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能指标
|
||||
*/
|
||||
async getPerformanceMetrics(startDate: Date, endDate: Date): Promise<any> {
|
||||
try {
|
||||
const metrics = await this.analyticsService.getPerformanceAnalytics(
|
||||
startDate.toISOString(),
|
||||
endDate.toISOString(),
|
||||
);
|
||||
|
||||
const responseTimeMetrics = metrics.filter(m => m.metricName === 'api_response_time');
|
||||
|
||||
if (responseTimeMetrics.length === 0) {
|
||||
return {
|
||||
averageResponseTime: 0,
|
||||
requestCount: 0,
|
||||
errorRate: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const totalRequests = responseTimeMetrics.reduce((sum, m) => sum + m.count, 0);
|
||||
const averageResponseTime = responseTimeMetrics.reduce((sum, m) => sum + (m.averageValue * m.count), 0) / totalRequests;
|
||||
|
||||
return {
|
||||
averageResponseTime,
|
||||
requestCount: totalRequests,
|
||||
minResponseTime: Math.min(...responseTimeMetrics.map(m => m.minValue)),
|
||||
maxResponseTime: Math.max(...responseTimeMetrics.map(m => m.maxValue)),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.warn(`获取性能指标失败: ${error.message}`);
|
||||
return {
|
||||
averageResponseTime: 0,
|
||||
requestCount: 0,
|
||||
errorRate: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取慢查询列表
|
||||
*/
|
||||
async getSlowQueries(startDate: Date, endDate: Date, limit = 10): Promise<any[]> {
|
||||
try {
|
||||
const slowQueries = await this.analyticsService.queryAnalytics({
|
||||
metricType: 'slow_query',
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
limit,
|
||||
});
|
||||
|
||||
return Array.isArray(slowQueries) ? slowQueries : [];
|
||||
} catch (error) {
|
||||
this.logger.warn(`获取慢查询失败: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件循环延迟
|
||||
*/
|
||||
private async getEventLoopDelay(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
setImmediate(() => {
|
||||
resolve(Date.now() - start);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 内存使用分析
|
||||
*/
|
||||
async analyzeMemoryUsage(): Promise<any> {
|
||||
const memoryUsage = process.memoryUsage();
|
||||
const { heapUsed, heapTotal, rss, external } = memoryUsage;
|
||||
|
||||
// 计算内存使用百分比
|
||||
const heapUsedPercentage = (heapUsed / heapTotal) * 100;
|
||||
|
||||
// 内存警告阈值
|
||||
const warnings = [];
|
||||
if (heapUsedPercentage > 80) {
|
||||
warnings.push('堆内存使用率过高');
|
||||
}
|
||||
if (rss > 1024 * 1024 * 1024) { // 1GB
|
||||
warnings.push('RSS内存使用过高');
|
||||
}
|
||||
if (external > 500 * 1024 * 1024) { // 500MB
|
||||
warnings.push('外部内存使用过高');
|
||||
}
|
||||
|
||||
return {
|
||||
usage: memoryUsage,
|
||||
heapUsedPercentage,
|
||||
warnings,
|
||||
recommendations: this.getMemoryRecommendations(warnings),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内存优化建议
|
||||
*/
|
||||
private getMemoryRecommendations(warnings: string[]): string[] {
|
||||
const recommendations = [];
|
||||
|
||||
if (warnings.some(w => w.includes('堆内存'))) {
|
||||
recommendations.push('考虑增加Node.js堆内存限制');
|
||||
recommendations.push('检查是否存在内存泄漏');
|
||||
recommendations.push('优化数据结构和缓存策略');
|
||||
}
|
||||
|
||||
if (warnings.some(w => w.includes('RSS'))) {
|
||||
recommendations.push('检查是否有未释放的原生资源');
|
||||
recommendations.push('考虑重启应用释放内存');
|
||||
}
|
||||
|
||||
if (warnings.some(w => w.includes('外部内存'))) {
|
||||
recommendations.push('检查Buffer和原生模块的使用');
|
||||
recommendations.push('优化文件处理和网络请求');
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时清理性能数据
|
||||
*/
|
||||
@Cron(CronExpression.EVERY_DAY_AT_3AM)
|
||||
async cleanupPerformanceData(): Promise<void> {
|
||||
this.logger.log('开始清理性能数据...');
|
||||
|
||||
try {
|
||||
// 清理7天前的性能日志
|
||||
const sevenDaysAgo = new Date();
|
||||
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||
|
||||
// 这里可以添加具体的清理逻辑
|
||||
// 例如删除过期的分析记录
|
||||
|
||||
this.logger.log('性能数据清理完成');
|
||||
} catch (error) {
|
||||
this.logger.error(`性能数据清理失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时生成性能报告
|
||||
*/
|
||||
@Cron(CronExpression.EVERY_HOUR)
|
||||
async generatePerformanceReport(): Promise<void> {
|
||||
try {
|
||||
const overview = await this.getPerformanceOverview();
|
||||
const memoryAnalysis = await this.analyzeMemoryUsage();
|
||||
|
||||
// 如果有性能问题,记录警告
|
||||
if (memoryAnalysis.warnings.length > 0) {
|
||||
this.logger.warn(`性能警告: ${memoryAnalysis.warnings.join(', ')}`);
|
||||
|
||||
// 记录性能警告事件
|
||||
await this.analyticsService.recordEvent({
|
||||
eventType: 'system_event',
|
||||
eventName: 'performance_warning',
|
||||
entityType: 'system',
|
||||
eventData: {
|
||||
warnings: memoryAnalysis.warnings,
|
||||
recommendations: memoryAnalysis.recommendations,
|
||||
memoryUsage: memoryAnalysis.usage,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 缓存性能报告供API查询
|
||||
await this.cacheService.set('performance:latest_report', {
|
||||
overview,
|
||||
memoryAnalysis,
|
||||
generatedAt: new Date(),
|
||||
}, { ttl: 3600 }); // 缓存1小时
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`生成性能报告失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最新性能报告
|
||||
*/
|
||||
async getLatestPerformanceReport(): Promise<any> {
|
||||
return await this.cacheService.get('performance:latest_report') ||
|
||||
await this.getPerformanceOverview();
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能优化建议
|
||||
*/
|
||||
async getOptimizationSuggestions(): Promise<any> {
|
||||
const overview = await this.getPerformanceOverview();
|
||||
const suggestions = [];
|
||||
|
||||
// 响应时间建议
|
||||
if (overview.hourly.averageResponseTime > 1000) {
|
||||
suggestions.push({
|
||||
type: 'response_time',
|
||||
severity: 'high',
|
||||
message: '平均响应时间过长,建议优化数据库查询和缓存策略',
|
||||
actions: [
|
||||
'添加数据库索引',
|
||||
'增加缓存层',
|
||||
'优化SQL查询',
|
||||
'考虑使用CDN',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 内存使用建议
|
||||
const memoryUsage = overview.current.memory.heapUsedPercentage;
|
||||
if (memoryUsage > 80) {
|
||||
suggestions.push({
|
||||
type: 'memory',
|
||||
severity: 'high',
|
||||
message: '内存使用率过高,可能存在内存泄漏',
|
||||
actions: [
|
||||
'检查未释放的事件监听器',
|
||||
'优化数据缓存策略',
|
||||
'使用内存分析工具',
|
||||
'考虑水平扩展',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 缓存命中率建议
|
||||
if (overview.cache && overview.cache.memory.keyspace_hit_rate < 0.8) {
|
||||
suggestions.push({
|
||||
type: 'cache',
|
||||
severity: 'medium',
|
||||
message: '缓存命中率较低,建议优化缓存策略',
|
||||
actions: [
|
||||
'调整缓存过期时间',
|
||||
'增加缓存预热',
|
||||
'优化缓存键设计',
|
||||
'分析缓存使用模式',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
overview: overview.current,
|
||||
generatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user