Files
jiu/backend/internal/middleware/dailyquota.go
T
wangjia 290054dc8c feat(backend): 公开接口反爬轻量版——per-IP 日配额 + 列表收紧(todo #3)
DailyQuota 二级闸(单品 1000/日、店铺列表 300/日,0=关;出错放行不误伤);
店铺列表 page_size 上限 50→20(客户端固定传 20 无破坏);公开响应敏感字段
(cost/purchase_price/profit)零暴露回归测试。
明确不做:签名链接(QR 已印刷+UUIDv4 不可枚举)、滑块(杀零门槛分享)、登录墙。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 10:42:23 +08:00

49 lines
1.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:<scope>:<yyyymmdd>:<ip>"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())
}