/** * 位置更新频率限制中间件 * * 功能描述: * - 限制用户位置更新的频率,防止过度请求 * - 基于用户ID和时间窗口的限流算法 * - 支持动态配置和监控统计 * - 提供优雅的限流响应和错误处理 * * 职责分离: * - 频率控制:实现基于时间窗口的请求限制 * - 用户隔离:每个用户独立的限流计数 * - 配置管理:支持动态调整限流参数 * - 监控统计:记录限流事件和性能指标 * * 技术实现: * - 滑动窗口算法:精确控制请求频率 * - 内存缓存:高性能的计数器存储 * - 异步处理:不阻塞正常请求流程 * - 错误恢复:处理异常情况的降级策略 * * 最近修改: * - 2026-01-08: 代码重构 - 提取魔法数字为常量,优化代码质量 (修改者: moyin) * * @author moyin * @version 1.1.0 * @since 2026-01-08 * @lastModified 2026-01-08 */ import { Injectable, Logger } from '@nestjs/common'; /** * 扩展的WebSocket接口 */ interface ExtendedWebSocket extends WebSocket { id: string; userId?: string; } /** * 限流配置接口 */ interface RateLimitConfig { /** 时间窗口(毫秒) */ windowMs: number; /** 窗口内最大请求数 */ maxRequests: number; /** 是否启用限流 */ enabled: boolean; /** 限流消息 */ message: string; } /** * 用户限流状态 */ interface UserRateLimit { /** 请求时间戳列表 */ requests: number[]; /** 最后更新时间 */ lastUpdate: number; /** 总请求数 */ totalRequests: number; /** 被限流次数 */ limitedCount: number; } /** * 限流统计信息 */ export interface RateLimitStats { /** 总请求数 */ totalRequests: number; /** 被限流请求数 */ limitedRequests: number; /** 活跃用户数 */ activeUsers: number; /** 限流率 */ limitRate: number; /** 统计时间戳 */ timestamp: number; } @Injectable() export class RateLimitMiddleware { private readonly logger = new Logger(RateLimitMiddleware.name); /** 默认时间窗口(毫秒) */ private static readonly DEFAULT_WINDOW_MS = 1000; /** 默认最大请求数 */ private static readonly DEFAULT_MAX_REQUESTS = 10; /** 清理间隔(毫秒) */ private static readonly CLEANUP_INTERVAL = 60000; /** 统计更新间隔(毫秒) */ private static readonly STATS_UPDATE_INTERVAL = 10000; /** 窗口数据保留倍数 */ private static readonly WINDOW_RETENTION_MULTIPLIER = 10; /** 时间转换常量 */ private static readonly MILLISECONDS_PER_SECOND = 1000; /** 用户限流状态缓存 */ private readonly userLimits = new Map(); /** 默认配置 */ private config: RateLimitConfig = { windowMs: RateLimitMiddleware.DEFAULT_WINDOW_MS, maxRequests: RateLimitMiddleware.DEFAULT_MAX_REQUESTS, enabled: true, message: '位置更新频率过高,请稍后重试', }; /** 统计信息 */ private stats: RateLimitStats = { totalRequests: 0, limitedRequests: 0, activeUsers: 0, limitRate: 0, timestamp: Date.now(), }; constructor() { // 定期清理过期的限流记录 setInterval(() => { this.cleanupExpiredRecords(); }, RateLimitMiddleware.CLEANUP_INTERVAL); // 定期更新统计信息 setInterval(() => { this.updateStats(); }, RateLimitMiddleware.STATS_UPDATE_INTERVAL); } /** * 检查用户是否被限流 * * @param userId 用户ID * @param socketId Socket连接ID * @returns 是否允许请求 */ checkRateLimit(userId: string, socketId: string): boolean { if (!this.config.enabled) { return true; } const now = Date.now(); this.stats.totalRequests++; // 获取或创建用户限流状态 let userLimit = this.userLimits.get(userId); if (!userLimit) { userLimit = { requests: [], lastUpdate: now, totalRequests: 0, limitedCount: 0, }; this.userLimits.set(userId, userLimit); } // 清理过期的请求记录 const windowStart = now - this.config.windowMs; userLimit.requests = userLimit.requests.filter(timestamp => timestamp > windowStart); // 检查是否超过限制 if (userLimit.requests.length >= this.config.maxRequests) { userLimit.limitedCount++; this.stats.limitedRequests++; this.logger.warn('用户位置更新被限流', { userId, socketId, requestCount: userLimit.requests.length, maxRequests: this.config.maxRequests, windowMs: this.config.windowMs, timestamp: new Date().toISOString(), }); return false; } // 记录请求 userLimit.requests.push(now); userLimit.totalRequests++; userLimit.lastUpdate = now; return true; } /** * 处理限流异常 * * @param client WebSocket客户端 * @param userId 用户ID */ handleRateLimit(client: ExtendedWebSocket, userId: string): void { const error = { type: 'error', code: 'RATE_LIMIT_EXCEEDED', message: this.config.message, details: { windowMs: this.config.windowMs, maxRequests: this.config.maxRequests, retryAfter: Math.ceil(this.config.windowMs / RateLimitMiddleware.MILLISECONDS_PER_SECOND), }, timestamp: Date.now(), }; if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ event: 'error', data: error })); } this.logger.debug('发送限流错误响应', { userId, socketId: client.id, error, }); } /** * 获取用户限流状态 * * @param userId 用户ID * @returns 用户限流状态 */ getUserRateLimit(userId: string): UserRateLimit | null { return this.userLimits.get(userId) || null; } /** * 获取限流统计信息 * * @returns 统计信息 */ getStats(): RateLimitStats { return { ...this.stats }; } /** * 更新限流配置 * * @param newConfig 新配置 */ updateConfig(newConfig: Partial): void { this.config = { ...this.config, ...newConfig }; this.logger.log('限流配置已更新', { config: this.config, timestamp: new Date().toISOString(), }); } /** * 重置用户限流状态 * * @param userId 用户ID */ resetUserLimit(userId: string): void { this.userLimits.delete(userId); this.logger.debug('重置用户限流状态', { userId, timestamp: new Date().toISOString(), }); } /** * 清理所有限流记录 */ clearAllLimits(): void { this.userLimits.clear(); this.stats = { totalRequests: 0, limitedRequests: 0, activeUsers: 0, limitRate: 0, timestamp: Date.now(), }; this.logger.log('清理所有限流记录', { timestamp: new Date().toISOString(), }); } /** * 清理过期的限流记录 * * @private */ private cleanupExpiredRecords(): void { const now = Date.now(); const expireTime = now - (this.config.windowMs * RateLimitMiddleware.WINDOW_RETENTION_MULTIPLIER); let cleanedCount = 0; for (const [userId, userLimit] of this.userLimits.entries()) { if (userLimit.lastUpdate < expireTime) { this.userLimits.delete(userId); cleanedCount++; } } if (cleanedCount > 0) { this.logger.debug('清理过期限流记录', { cleanedCount, remainingUsers: this.userLimits.size, timestamp: new Date().toISOString(), }); } } /** * 更新统计信息 * * @private */ private updateStats(): void { this.stats.activeUsers = this.userLimits.size; this.stats.limitRate = this.stats.totalRequests > 0 ? (this.stats.limitedRequests / this.stats.totalRequests) * 100 : 0; this.stats.timestamp = Date.now(); } } /** * 位置更新限流装饰器 * * 使用示例: * ```typescript * @PositionUpdateRateLimit() * @SubscribeMessage('position_update') * async handlePositionUpdate(@ConnectedSocket() client: AuthenticatedSocket, @MessageBody() message: PositionUpdateMessage) { * // 处理位置更新 * } * ``` */ export function PositionUpdateRateLimit() { return function (_target: any, _propertyName: string, descriptor: PropertyDescriptor) { const method = descriptor.value; descriptor.value = async function (...args: any[]) { const client = args[0] as ExtendedWebSocket; const rateLimitMiddleware = new RateLimitMiddleware(); if (client.userId) { const allowed = rateLimitMiddleware.checkRateLimit(client.userId, client.id); if (!allowed) { rateLimitMiddleware.handleRateLimit(client, client.userId); return; } } return method.apply(this, args); }; }; }