"""接口限流:Redis 固定窗口计数(Nginx 层之外的第二道,对齐《02-技术文档》4.5)""" from datetime import datetime import redis.asyncio as aioredis from app.core import errors from app.core.response import BizException async def hit(redis: aioredis.Redis, key: str, limit: int, window_seconds: int, message: str | None = None) -> None: """固定窗口计数,超限抛 1005(HTTP 429)。key 需自带维度与窗口标识。""" n = await redis.incr(key) if n == 1: await redis.expire(key, window_seconds) if n > limit: raise BizException(errors.RATE_LIMITED, message, http_status=429) def window_key(window_seconds: int) -> str: """窗口标识:分钟 %Y%m%d%H%M / 小时 %Y%m%d%H / 天 %Y%m%d""" now = datetime.now() if window_seconds >= 86400: return f"{now:%Y%m%d}" if window_seconds >= 3600: return f"{now:%Y%m%d%H}" return f"{now:%Y%m%d%H%M}" async def hit_subject( redis: aioredis.Redis, subject: str, action: str, limit: int, window_seconds: int, message: str | None = None ) -> None: """按主体(用户ID/设备ID/IP)+ 动作限流:rl:{action}:{subject}:{窗口}""" await hit(redis, f"rl:{action}:{subject}:{window_key(window_seconds)}", limit, window_seconds, message)