feat(backend): 限流/登录失败锁状态外置 Redis(todo #2)

新包 internal/ratelimit:内存/redis(GCRA+Lua) 双实现 + 出错逐调用降级内存
(fail-open 到内存不 fail-closed)。REDIS_ADDR 空=内存模式,行为与既往一致;
配置后跨重启保状态、支持多实例。miniredis 全覆盖测试,零真实外部依赖。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-05 10:42:23 +08:00
parent e850a1987c
commit 3c687e5e0b
15 changed files with 894 additions and 169 deletions
+13
View File
@@ -14,6 +14,7 @@ type Config struct {
Storage StorageConfig
Session SessionConfig
RateLimit RateLimitConfig
Redis RedisConfig
Pay PayConfig
}
@@ -68,6 +69,14 @@ type RateLimitConfig struct {
PublicReadPerMin int `mapstructure:"public_read_per_min"` // 其余公开读接口(单品/release),按 IP
ShopRPS int `mapstructure:"shop_rps"` // 认证流量每店每秒,按 shop_id
ShopBurst int `mapstructure:"shop_burst"` // 认证流量每店突发
// 日配额(分钟级限流之上的第二道反爬闸,0=关闭该闸):同一 IP 每自然日累计上限
DailyProductPerIP int `mapstructure:"daily_product_per_ip"` // 单品详情(API + OG 页同池)
DailyShopListPerIP int `mapstructure:"daily_shoplist_per_ip"` // 店铺公开商品列表
}
// RedisConfig 限流/登录锁状态外置。Addr 为空 = 内存模式(单实例,重启清零)。
type RedisConfig struct {
Addr string `mapstructure:"addr"` // 如 127.0.0.1:6379
}
type StorageConfig struct {
@@ -99,6 +108,7 @@ func Load() {
_ = viper.BindEnv("pay.base_url", "PAY_BASE_URL")
_ = viper.BindEnv("pay.secret", "PAY_SECRET")
_ = viper.BindEnv("pay.return_url", "PAY_RETURN_URL")
_ = viper.BindEnv("redis.addr", "REDIS_ADDR")
// 默认值
viper.SetDefault("server.port", "8080")
@@ -123,6 +133,9 @@ func Load() {
viper.SetDefault("ratelimit.public_read_per_min", 60)
viper.SetDefault("ratelimit.shop_rps", 20)
viper.SetDefault("ratelimit.shop_burst", 40)
viper.SetDefault("ratelimit.daily_product_per_ip", 1000)
viper.SetDefault("ratelimit.daily_shoplist_per_ip", 300)
viper.SetDefault("redis.addr", "") // 空=内存模式
viper.SetDefault("database.max_idle_conns", 10)
viper.SetDefault("database.max_open_conns", 100)
viper.SetDefault("storage.upload_dir", "./uploads/images")