package middleware import ( "fmt" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/wangjia/jiu/backend/config" "github.com/wangjia/jiu/backend/internal/ratelimit" ) // DailyQuota 同一 IP 的自然日累计配额——分钟级限流之上的第二道反爬闸。 // key = "dq:::",TTL 26h(跨午夜/时钟偏移裕量,键按日期 // 自然分片,旧键过期自灭)。limit<=0 或总开关关闭时放行;计数器出错(redis // 降级窗口等)也放行——宁可漏限不误伤真实客户。 func DailyQuota(scope string, limit func() int) gin.HandlerFunc { return func(c *gin.Context) { lim := limit() if !config.C.RateLimit.Enabled || lim <= 0 { c.Next() return } ip := c.ClientIP() if ip == "" { c.Next() return } now := time.Now() key := fmt.Sprintf("dq:%s:%s:%s", scope, now.Format("20060102"), ip) n, err := ratelimit.Default().Counter().Incr(key, 26*time.Hour) if err == nil && n > int64(lim) { c.Header("Retry-After", strconv.Itoa(secondsToMidnight(now))) c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "今日访问次数已达上限,请明日再试"}) return } c.Next() } } // secondsToMidnight 距下一个本地零点的秒数(日配额的自然重置时刻)。 func secondsToMidnight(now time.Time) int { next := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, 1) return int(next.Sub(now).Seconds()) }