Files
rootandClaude Opus 5.5 7974a42301 Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 11:07:42 +08:00

85 lines
1.6 KiB
Go

package apilimitHandler
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
type counter struct {
Count int
WindowStart time.Time
}
type RouteHourlyLimiter struct {
mu sync.Mutex
limit int
window time.Duration
counters map[string]*counter
}
func NewRouteHourlyLimiter(limit int) *RouteHourlyLimiter {
l := &RouteHourlyLimiter{
limit: limit,
window: time.Hour,
counters: make(map[string]*counter),
}
go l.cleaner()
return l
}
func (l *RouteHourlyLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 用 FullPath 做“接口维度”,例如 /api/user/:id
route := c.FullPath()
if route == "" {
// 路由未匹配时兜底
route = c.Request.URL.Path
}
now := time.Now()
l.mu.Lock()
defer l.mu.Unlock()
item, ok := l.counters[route]
if !ok || now.Sub(item.WindowStart) >= l.window {
l.counters[route] = &counter{
Count: 1,
WindowStart: now,
}
c.Next()
return
}
if item.Count >= l.limit {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
"code": http.StatusTooManyRequests,
"message": "rate limit exceeded: max 100 requests per route per hour",
})
return
}
item.Count++
c.Next()
}
}
// 定时清理过期窗口,避免 map 无限增长
func (l *RouteHourlyLimiter) cleaner() {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
for now := range ticker.C {
l.mu.Lock()
for k, v := range l.counters {
if now.Sub(v.WindowStart) >= l.window {
delete(l.counters, k)
}
}
l.mu.Unlock()
}
}