Initial commit: Telegram Management System
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:
你的用户名
2025-11-04 15:37:50 +08:00
commit 237c7802e5
3674 changed files with 525172 additions and 0 deletions

View 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');

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

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View 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: '请求次数过多,请稍后再试',
});

View 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;
},
);

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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('*'); // 应用到所有路由
}
}

View 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;
}
}

View 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')}`;
}
}

View 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];
}
}

View File

@@ -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;
}
}

View 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 '操作完成';
}
}
}

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

View 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,
};
}
}

View File

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

View 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;
}
}

View 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,
}));
}
}

View 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;
}
}

View 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;
}
}

View 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(),
};
}
}