// Package middleware holds gin-agnostic-ish HTTP cross-cutting concerns // (currently: per-IP rate limiting) shared by the v2 gateway router. package middleware import ( "net/http" "sync" "time" "github.com/gin-gonic/gin" ) // bucket 单个 IP 的令牌桶状态。 type bucket struct { mu sync.Mutex tokens float64 updated time.Time } // IPRateLimiter 简单内存令牌桶,按客户端 IP 分桶限速——单实例部署足够(jiu 反馈波, // P3 v2 限流纵深)。多实例横向扩展需换外置存储(Redis INCR+EXPIRE 等)统一计数, // 当前 pay 只跑单进程,内存方案够用且零额外依赖。 type IPRateLimiter struct { mu sync.Mutex buckets map[string]*bucket rate float64 // 每秒补充的令牌数 burst float64 // 桶容量(=突发上限,取每分钟额度) sweepCounter uint64 } // NewIPRateLimiter 按每分钟额度构造限流器;perMinute<=0 时按 30 兜底(与 config 里 // "零值=默认开、默认 30/min" 的约定一致,调用方无需重复判空)。 func NewIPRateLimiter(perMinute int) *IPRateLimiter { if perMinute <= 0 { perMinute = 30 } return &IPRateLimiter{ buckets: make(map[string]*bucket), rate: float64(perMinute) / 60.0, burst: float64(perMinute), } } // allow 令牌桶判定:按经过时间匀速补充令牌(封顶 burst),够 1 个则放行并扣 1。 func (l *IPRateLimiter) allow(ip string) bool { now := time.Now() l.mu.Lock() b, ok := l.buckets[ip] if !ok { b = &bucket{tokens: l.burst, updated: now} l.buckets[ip] = b } l.sweepCounter++ if l.sweepCounter%1000 == 0 { l.sweepLocked(now) } l.mu.Unlock() b.mu.Lock() defer b.mu.Unlock() elapsed := now.Sub(b.updated).Seconds() b.tokens += elapsed * l.rate if b.tokens > l.burst { b.tokens = l.burst } b.updated = now if b.tokens < 1 { return false } b.tokens-- return true } // sweepLocked 惰性回收长期不活跃(>10min 无请求)的 IP 桶,防长期运行的单实例 // 在"短连接、海量不同 IP"场景下内存无界增长。调用方须已持 l.mu。 func (l *IPRateLimiter) sweepLocked(now time.Time) { for ip, b := range l.buckets { b.mu.Lock() stale := now.Sub(b.updated) > 10*time.Minute b.mu.Unlock() if stale { delete(l.buckets, ip) } } } // Gin 返回 gin 中间件:超速回 429,不中断其它路由。 func (l *IPRateLimiter) Gin() gin.HandlerFunc { return func(c *gin.Context) { if !l.allow(c.ClientIP()) { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ "code": "rate_limited", "message": "请求过于频繁,请稍后重试", }) return } c.Next() } }