Skip to content

Commit dfbadf0

Browse files
committed
fix(admin): one-click clean rate-limited now clears premium 5h + usage_exhausted
CleanRateLimited used CleanByRuntimeStatus("rate_limited"), which has two quirks designed for the auto-cleanup sweep: 1. premium 5h rate-limited accounts are skipped — they self-recover in ~5h so deleting them wastes accounts 2. only matches RuntimeStatus == "rate_limited", not "usage_exhausted" (Free 7d window full) The UI's "限流" filter is broader (covers usage_exhausted + premium 5h), so users see accounts that the one-click cleaner won't touch. Users already worked around this by selecting all of them and running batch delete, which goes through per-id deletion and bypasses the runtime-status gate entirely. Fix: new CleanRateLimitedManual that - matches both rate_limited AND usage_exhausted - does NOT skip premium 5h (manual click is an explicit user intent) - still skips Locked accounts - logs the deletion event with reason "manual_clean" instead of "auto_clean" CleanByRuntimeStatus is unchanged and still serves runAutoCleanupSweep.
1 parent 3ec56d8 commit dfbadf0

3 files changed

Lines changed: 97 additions & 3 deletions

File tree

admin/handler.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5077,9 +5077,14 @@ func (h *Handler) CleanBanned(c *gin.Context) {
50775077
h.cleanByStatus(c, "unauthorized")
50785078
}
50795079

5080-
// CleanRateLimited 清理限流(rate_limited)账号
5080+
// CleanRateLimited 一键清理所有限流账号(含 premium 5h、free 7d、usage_exhausted)
50815081
func (h *Handler) CleanRateLimited(c *gin.Context) {
5082-
h.cleanByStatus(c, "rate_limited")
5082+
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
5083+
defer cancel()
5084+
5085+
cleaned := h.store.CleanRateLimitedManual(ctx)
5086+
5087+
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("已清理 %d 个账号", cleaned), "cleaned": cleaned})
50835088
}
50845089

50855090
// CleanError 清理错误(error)账号

auth/premium_rate_limit_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,51 @@ func TestCleanByRuntimeStatusSkipsPremium5hRateLimitedAccount(t *testing.T) {
115115
t.Fatalf("AccountCount() = %d, want 1", store.AccountCount())
116116
}
117117
}
118+
119+
func TestCleanRateLimitedManualClearsAllRateLimitFlavors(t *testing.T) {
120+
premium := newPremium5hTestAccount("plus", time.Now().Add(20*time.Minute))
121+
premium.DBID = 1
122+
123+
// Free 7d 用尽 → RuntimeStatus = "usage_exhausted"
124+
exhausted := &Account{
125+
DBID: 2,
126+
AccessToken: "token-exhausted",
127+
PlanType: "free",
128+
Status: StatusReady,
129+
HealthTier: HealthTierHealthy,
130+
UsagePercent7d: 100,
131+
UsagePercent7dValid: true,
132+
Reset7dAt: time.Now().Add(48 * time.Hour),
133+
UsageUpdatedAt: time.Now().Add(-1 * time.Minute),
134+
}
135+
136+
// 普通正常账号 → 不应被清理
137+
healthy := &Account{
138+
DBID: 3,
139+
AccessToken: "token-healthy",
140+
PlanType: "plus",
141+
Status: StatusReady,
142+
HealthTier: HealthTierHealthy,
143+
}
144+
145+
// 锁定的限流账号 → 不应被清理
146+
lockedRL := newPremium5hTestAccount("plus", time.Now().Add(20*time.Minute))
147+
lockedRL.DBID = 4
148+
lockedRL.Locked = 1
149+
150+
store := &Store{accounts: []*Account{premium, exhausted, healthy, lockedRL}}
151+
152+
cleaned := store.CleanRateLimitedManual(context.Background())
153+
if cleaned != 2 {
154+
t.Fatalf("CleanRateLimitedManual() cleaned = %d, want 2 (premium + exhausted)", cleaned)
155+
}
156+
if store.AccountCount() != 2 {
157+
t.Fatalf("AccountCount() = %d, want 2 (healthy + locked stay)", store.AccountCount())
158+
}
159+
if store.FindByID(3) == nil {
160+
t.Fatal("healthy account should remain")
161+
}
162+
if store.FindByID(4) == nil {
163+
t.Fatal("locked rate-limited account should remain")
164+
}
165+
}

auth/store.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2521,7 +2521,9 @@ func (s *Store) Stop() {
25212521
s.wg.Wait()
25222522
}
25232523

2524-
// CleanByRuntimeStatus 按运行时状态清理账号
2524+
// CleanByRuntimeStatus 按运行时状态清理账号(用于自动清理流程)
2525+
// premium 5h 限流账号会被跳过,因为它们会在 5h 内自然恢复,无需删除。
2526+
// 手动一键清理请改用 CleanRateLimitedManual——它会清掉所有限流账号。
25252527
func (s *Store) CleanByRuntimeStatus(ctx context.Context, targetStatus string) int {
25262528
accounts := s.Accounts()
25272529
cleaned := 0
@@ -2556,6 +2558,45 @@ func (s *Store) CleanByRuntimeStatus(ctx context.Context, targetStatus string) i
25562558
return cleaned
25572559
}
25582560

2561+
// CleanRateLimitedManual 清理所有"限流"含义下的账号(用于手动一键清理)。
2562+
// 与 CleanByRuntimeStatus("rate_limited") 的区别:
2563+
// - 涵盖 RuntimeStatus 的全部限流相关值:rate_limited / usage_exhausted
2564+
// - 不跳过 premium 5h 限流:手动触发即代表用户明确意图删除
2565+
// - 锁定账号依然跳过(与所有清理流程一致)
2566+
func (s *Store) CleanRateLimitedManual(ctx context.Context) int {
2567+
accounts := s.Accounts()
2568+
cleaned := 0
2569+
2570+
for _, acc := range accounts {
2571+
if acc == nil {
2572+
continue
2573+
}
2574+
status := acc.RuntimeStatus()
2575+
if status != "rate_limited" && status != "usage_exhausted" {
2576+
continue
2577+
}
2578+
2579+
if atomic.LoadInt32(&acc.Locked) == 1 {
2580+
continue
2581+
}
2582+
2583+
if s.db != nil {
2584+
if err := s.db.SoftDeleteAccount(ctx, acc.DBID); err != nil {
2585+
log.Printf("[账号 %d] 手动清理限流账号失败: %v", acc.DBID, err)
2586+
continue
2587+
}
2588+
}
2589+
2590+
s.RemoveAccount(acc.DBID)
2591+
cleaned++
2592+
if s.db != nil {
2593+
s.db.InsertAccountEventAsync(acc.DBID, "deleted", "manual_clean")
2594+
}
2595+
}
2596+
2597+
return cleaned
2598+
}
2599+
25592600
// ==================== 最少连接调度 ====================
25602601

25612602
// Next 获取下一个可用账号(健康优先 + 低负载择优 + warm 公平调度)

0 commit comments

Comments
 (0)