import ResponseUtil from '../utils/api_response.js'; /** * 内存速率限制 * 基于 IP + 接口路径维度,不引入 Redis */ const rateLimitStore = new Map(); // 每 60 秒清理一次过期记录 const cleanupTimer = setInterval(() => { const now = Date.now(); for (const [key, record] of rateLimitStore.entries()) { if (now - record.resetAt > record.windowMs) { rateLimitStore.delete(key); } } }, 60_000); cleanupTimer.unref(); /** * 创建速率限制中间件 * @param {number} max - 窗口内最大请求数 * @param {number} windowMs - 窗口时长(毫秒),默认 60 秒 */ function rateLimit(max = 10, windowMs = 60_000) { return async (ctx, next) => { const ip = ctx.ip || ctx.request.ip; const key = `${ip}:${ctx.path}`; const now = Date.now(); let record = rateLimitStore.get(key); if (!record || now - record.resetAt > windowMs) { record = { count: 0, resetAt: now, windowMs }; rateLimitStore.set(key, record); } record.count++; if (record.count > max) { const retryAfter = Math.ceil((record.resetAt + windowMs - now) / 1000); ctx.set('Retry-After', retryAfter); return ResponseUtil.error(ctx, `请求过于频繁,请 ${retryAfter} 秒后重试`, null, 429); } ctx.set('X-RateLimit-Limit', max); ctx.set('X-RateLimit-Remaining', Math.max(0, max - record.count)); await next(); }; } export { rateLimit };