package middleware import ( "net/http" "sync" "time" "fashionapi/internal/config" "github.com/gin-gonic/gin" ) // rateLimiter 基于令牌桶的按 IP 限流(进程内,单实例足够;多实例需换集中式如 redis)。 // // 设计取舍:公开接口「首页公开、翻页需登录」已天然抬高批量枚举门槛;本中间件作为 // 兜底,按客户端 IP 限制总量,兜住 casual 爬虫与突发流量,避免单 IP 打垮服务。 type rateLimiter struct { mu sync.Mutex visitors map[string]*visitor rate float64 // 每秒补充令牌数 burst float64 // 桶容量 lastSeen map[string]time.Time } type visitor struct { tokens float64 last time.Time } func newRateLimiter(rps, burst int) *rateLimiter { rl := &rateLimiter{ visitors: make(map[string]*visitor), lastSeen: make(map[string]time.Time), rate: float64(rps), burst: float64(burst), } go rl.cleanup(10 * time.Minute) return rl } // cleanup 周期清理长时间无活动的客户端,避免 map 无限增长。 func (rl *rateLimiter) cleanup(interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for range ticker.C { rl.mu.Lock() for ip, t := range rl.lastSeen { if time.Since(t) > 3*interval { delete(rl.visitors, ip) delete(rl.lastSeen, ip) } } rl.mu.Unlock() } } func (rl *rateLimiter) allow(ip string) bool { rl.mu.Lock() defer rl.mu.Unlock() now := time.Now() v, ok := rl.visitors[ip] if !ok { // 首次访问直接发放 burst-1 个令牌(允许一个突发请求),后续按速率补充 rl.visitors[ip] = &visitor{tokens: rl.burst - 1, last: now} rl.lastSeen[ip] = now return true } elapsed := now.Sub(v.last).Seconds() v.tokens += elapsed * rl.rate if v.tokens > rl.burst { v.tokens = rl.burst } v.last = now rl.lastSeen[ip] = now if v.tokens >= 1 { v.tokens-- return true } return false } // RateLimit 按 IP 的速率限制中间件。Enabled=false 时为 noop(便于灰度与本地开发)。 func RateLimit(cfg config.RateLimitConfig) gin.HandlerFunc { if !cfg.Enabled || cfg.RPS <= 0 { return func(c *gin.Context) { c.Next() } } rl := newRateLimiter(cfg.RPS, cfg.Burst) return func(c *gin.Context) { if !rl.allow(c.ClientIP()) { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ "error": "too many requests", }) return } c.Next() } }