feat:实现位置广播系统
- 添加位置广播核心控制器和服务 - 实现健康检查和位置同步功能 - 添加WebSocket实时位置更新支持 - 完善位置广播的测试覆盖
This commit is contained in:
348
src/business/location_broadcast/rate_limit.middleware.ts
Normal file
348
src/business/location_broadcast/rate_limit.middleware.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* 位置更新频率限制中间件
|
||||
*
|
||||
* 功能描述:
|
||||
* - 限制用户位置更新的频率,防止过度请求
|
||||
* - 基于用户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';
|
||||
import { Socket } from 'socket.io';
|
||||
|
||||
/**
|
||||
* 限流配置接口
|
||||
*/
|
||||
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<string, UserRateLimit>();
|
||||
|
||||
/** 默认配置 */
|
||||
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: Socket, 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(),
|
||||
};
|
||||
|
||||
client.emit('error', 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<RateLimitConfig>): 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 Socket & { userId?: string };
|
||||
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);
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user