diff --git a/internal/logic/billing/ratelimit.go b/internal/logic/billing/ratelimit.go index e7837b2..8d644c4 100644 --- a/internal/logic/billing/ratelimit.go +++ b/internal/logic/billing/ratelimit.go @@ -201,6 +201,7 @@ local function tryAcquire(i) local v = redis.call("DECR", acquired[j]) if v < 0 then redis.call("SET", acquired[j], 0) + redis.call("EXPIRE", acquired[j], expire) end end return false @@ -217,11 +218,14 @@ return 1 ` // releaseConcurrentLua 一次 EVAL 释放 4 级并发许可。 +// ARGV[1] = expire 秒数(用于 SET 后恢复 TTL) var releaseConcurrentLua = ` +local expire = tonumber(ARGV[1]) for i = 1, 4 do local v = redis.call("DECR", KEYS[i]) if v < 0 then redis.call("SET", KEYS[i], 0) + redis.call("EXPIRE", KEYS[i], expire) end end return 1 @@ -261,7 +265,8 @@ func ReleaseConcurrent(ctx context.Context, tenantID, userID, apiKeyID int64, mo keyKey := fmt.Sprintf("conc:key:%d", apiKeyID) g.Redis().Do(ctx, "EVAL", releaseConcurrentLua, 4, - tenantKey, modelKey, userKey, keyKey) + tenantKey, modelKey, userKey, keyKey, + 300) // EXPIRE 秒数 } // getTenantConcurrencyLimit 获取租户并发上限。 diff --git a/internal/logic/billing/wallet.go b/internal/logic/billing/wallet.go index 630f0a7..5210c9c 100644 --- a/internal/logic/billing/wallet.go +++ b/internal/logic/billing/wallet.go @@ -160,7 +160,9 @@ redis.call("HSET", prededuct_key, "amount", amount) redis.call("HSET", prededuct_key, "tenant_id", ARGV[4]) redis.call("HSET", prededuct_key, "model_name", ARGV[5]) redis.call("HSET", prededuct_key, "created_at", ARGV[6]) -redis.call("SADD", "prededuct_active:" .. ARGV[4], request_id) +local active_set = "prededuct_active:" .. ARGV[4] +redis.call("SADD", active_set, request_id) +redis.call("EXPIRE", active_set, 30 * 86400) redis.call("EXPIRE", prededuct_key, ttl) return 1 ` @@ -510,6 +512,8 @@ func rebuildPredeductFromDB(ctx context.Context, tenantID int64) { g.Redis().Do(ctx, "EXPIRE", predeductKey, remainingTTL) g.Redis().Do(ctx, "SADD", activeSetKey, t.RequestID) } + // 确保 active SET 有 TTL(30 天),过期后下次预扣时自动重建 + g.Redis().Do(ctx, "EXPIRE", activeSetKey, 30*86400) g.Log().Infof(ctx, "[PRE-DEDUCT] rebuilt %d active tracks from DB for tenant=%d", len(tracks), tenantID) } diff --git a/internal/logic/common/cache.go b/internal/logic/common/cache.go index 53b3527..371ed81 100644 --- a/internal/logic/common/cache.go +++ b/internal/logic/common/cache.go @@ -57,7 +57,9 @@ func (c *Cache) Set(ctx context.Context, key string, value any, ttl ...time.Dura if ttlSeconds > 0 { _, _ = g.Redis().Do(ctx, "SETEX", fullKey, ttlSeconds, string(jsonBytes)) } else { - _, _ = g.Redis().Do(ctx, "SET", fullKey, string(jsonBytes)) + // 兜底:ttl ≤ 0 时使用 24h 兜底 TTL,避免产生永久 key + g.Log().Warningf(ctx, "[Cache] TTL <= 0 for key=%s, using fallback 24h TTL", fullKey) + _, _ = g.Redis().Do(ctx, "SETEX", fullKey, 86400, string(jsonBytes)) } } diff --git a/internal/logic/common/verify_code.go b/internal/logic/common/verify_code.go index 33863ae..c346162 100644 --- a/internal/logic/common/verify_code.go +++ b/internal/logic/common/verify_code.go @@ -44,21 +44,18 @@ func SendVerifyCode(ctx context.Context, email string, purpose VerifyPurpose) er fmt.Sprintf("请 %d 秒后再试", secs)) } - // Rate limit: max 5 per hour + // Rate limit: max 5 per hour (atomic Lua: INCR + first-time EXPIRE) hourlyKey := fmt.Sprintf("verify:hourly:%s:%s", email, purpose) - hourlyCount, err := g.Redis().Do(ctx, "INCR", hourlyKey) + hourlyCount, err := g.Redis().Do(ctx, "EVAL", + `local count = redis.call("INCR", KEYS[1]) + if count == 1 then + redis.call("EXPIRE", KEYS[1], ARGV[1]) + end + return count`, + 1, hourlyKey, 3600) if err == nil && hourlyCount.Int() > 5 { return NewBusinessError(consts.CodeRateLimitExceeded, "每小时最多发送5次验证码") } - // Set the 1-hour window TTL only when it is missing. This keeps a fixed - // window (not a sliding one) while still repairing an orphaned key left by - // a crash between INCR and EXPIRE (TTL < 0 means no expiry or key absent). - if ttl, err := g.Redis().Do(ctx, "TTL", hourlyKey); err == nil && ttl.Int() < 0 { - if _, err := g.Redis().Do(ctx, "EXPIRE", hourlyKey, 3600); err != nil { - g.Log().Warningf(ctx, "failed to set hourly rate limit TTL: %v", err) - } - } - // Generate 6-digit code code, err := generateCode(6) if err != nil { diff --git a/internal/logic/tenant/oauth.go b/internal/logic/tenant/oauth.go index 6846f46..b262df7 100644 --- a/internal/logic/tenant/oauth.go +++ b/internal/logic/tenant/oauth.go @@ -85,11 +85,10 @@ func (s *sTenant) GetOAuthAuthorizeURL(ctx context.Context, req *v1.OAuthAuthori redirectURI := siteURL + fmt.Sprintf("/api/tenant/oauth/%s/callback", req.Provider) state := generateState() - _, err := g.Redis().Set(ctx, oauthStatePrefix+state, req.Provider) + _, err := g.Redis().Do(ctx, "SET", oauthStatePrefix+state, req.Provider, "EX", int64(oauthStateTTL.Seconds())) if err != nil { return nil, err } - _, _ = g.Redis().Expire(ctx, oauthStatePrefix+state, int64(oauthStateTTL.Seconds())) authorizeURL := provider.GetAuthorizeURL(clientID, redirectURI, state) return &v1.OAuthAuthorizeRes{ diff --git a/internal/logic/tenant/open_platform.go b/internal/logic/tenant/open_platform.go index a16605f..ac1ec6f 100644 --- a/internal/logic/tenant/open_platform.go +++ b/internal/logic/tenant/open_platform.go @@ -143,8 +143,8 @@ func (s *sTenant) OpenAppCreate(ctx context.Context, req *v1.OpenAppCreateReq) ( id, _ := result.LastInsertId() - // Store encrypted secret in Redis — 失败时回滚 DB 记录 - _, err = g.Redis().Do(ctx, "SET", fmt.Sprintf("open:secret:%d", id), encryptedSecret) + // Store encrypted secret in Redis(30 天 TTL,过期后从 DB 重新加载)— 失败时回滚 DB 记录 + _, err = g.Redis().Do(ctx, "SET", fmt.Sprintf("open:secret:%d", id), encryptedSecret, "EX", 30*86400) if err != nil { // 回滚:删除已插入的 DB 记录 _, _ = dao.OpnApps.Ctx(ctx).Where("id", id).Delete() @@ -235,8 +235,8 @@ func (s *sTenant) OpenAppResetSecret(ctx context.Context, req *v1.OpenAppResetSe return nil, err } - // 先写 Redis,成功后再更新 DB - _, err = g.Redis().Do(ctx, "SET", fmt.Sprintf("open:secret:%d", req.Id), encryptedSecret) + // 先写 Redis(30 天 TTL),成功后再更新 DB + _, err = g.Redis().Do(ctx, "SET", fmt.Sprintf("open:secret:%d", req.Id), encryptedSecret, "EX", 30*86400) if err != nil { return nil, fmt.Errorf("update app secret in Redis failed: %w", err) }