From 754e3f1ca8581099f786a6a8db53a36eb3b4e9bf Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 9 Aug 2026 14:48:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=92=8C=E5=B9=B3=E5=B0=8F?= =?UTF-8?q?=E5=BA=97=E6=8A=A2=E8=B4=AD=E9=87=8D=E8=AF=95=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/seckill/scheduler.go | 63 +++++++++++++++++++++++------- internal/seckill/scheduler_test.go | 24 ++++++++++++ 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/internal/seckill/scheduler.go b/internal/seckill/scheduler.go index cea4b1d..55f9ed3 100644 --- a/internal/seckill/scheduler.go +++ b/internal/seckill/scheduler.go @@ -629,24 +629,27 @@ func (s *Scheduler) RunXiaodian(accountProducts map[string]string, attempts, del return } - stopped := make(map[string]bool) + stoppedAccounts := make(map[string]string) + cooldownAccounts := make(map[string]time.Time) + cooldownCount := make(map[string]int) start := time.Now() logger.LogEvent("抢购", "info", "和平小店抢购开始: %d 个账号,次数=%d,间隔=%dms", len(accounts), attempts, delay.Milliseconds()) - for round := 1; round <= attempts; round++ { + for round, waitBeforeRound := 1, false; round <= attempts; { if s.runStopped() { logger.LogEvent("抢购", "warn", "收到停止指令,和平小店抢购正在结束") break } - if round > 1 && s.interruptibleSleep(delay) { + if waitBeforeRound && s.interruptibleSleep(delay) { logger.LogEvent("抢购", "warn", "收到停止指令,和平小店抢购正在结束") break } + waitBeforeRound = false roundAttempts := make([]types.ExchangeAttempt, 0, len(accounts)) for _, account := range accounts { - if stopped[account.Name] { + if stoppedAccounts[account.Name] != "" { continue } productID := accountProducts[account.Name] @@ -663,24 +666,55 @@ func (s *Scheduler) RunXiaodian(accountProducts map[string]string, attempts, del if len(roundAttempts) == 0 { break } + now := time.Now() + roundAttempts, nextReady := filterCooldownAttempts(roundAttempts, cooldownAccounts, now) + if len(roundAttempts) == 0 { + waitFor := nextReady.Sub(now) + if waitFor > 0 { + logger.LogEvent("抢购", "info", "和平小店第%d/%d轮账号冷却中,等待=%v", round, attempts, utils.RoundDuration(waitFor)) + if s.interruptibleSleep(waitFor) { + logger.LogEvent("抢购", "warn", "收到停止指令,和平小店抢购正在结束") + break + } + } + continue + } outcome := s.executeXiaodianRound(roundAttempts, settings) for name := range outcome.successAccounts { - stopped[name] = true + stoppedAccounts[name] = "已抢到" } - for name := range outcome.stoppedAccounts { - stopped[name] = true + for name, reason := range outcome.stoppedAccounts { + if stoppedAccounts[name] == "" { + stoppedAccounts[name] = reason + logger.LogAccountEvent("抢购", "warn", name, "账号停止重试: 账号=%s 原因=%s", name, reason) + } } - logger.LogEvent("抢购", "info", "和平小店第%d/%d轮完成: 成功=%d,停止=%d,剩余账号=%d", - round, attempts, len(outcome.successAccounts), len(outcome.stoppedAccounts), len(accounts)-len(stopped)) + for name, until := range outcome.cooldownAccounts { + if stoppedAccounts[name] != "" || !until.After(cooldownAccounts[name]) { + continue + } + cooldownAccounts[name] = until + cooldownCount[name]++ + logger.LogAccountEvent("抢购", "warn", name, "账号临时降频: 账号=%s 冷却至=%s (第%d次)", + name, until.In(constants.CST).Format("15:04:05.000"), cooldownCount[name]) + if cooldownCount[name] >= maxCooldownCount { + stoppedAccounts[name] = "冷却次数过多" + logger.LogAccountEvent("抢购", "warn", name, "账号停止重试: 账号=%s 原因=冷却次数过多(%d次)", name, cooldownCount[name]) + } + } + logger.LogEvent("抢购", "info", "和平小店第%d/%d轮完成: 成功=%d,账号停止=%d,冷却=%d,剩余账号=%d", + round, attempts, len(outcome.successAccounts), len(outcome.stoppedAccounts), len(outcome.cooldownAccounts), len(accounts)-len(stoppedAccounts)) - if len(stopped) >= len(accounts) { + if len(stoppedAccounts) >= len(accounts) { break } + round++ + waitBeforeRound = true } logger.LogEvent("抢购", "info", "和平小店抢购结束: 成功或停止=%d/%d,耗时=%v", - len(stopped), len(accounts), utils.RoundDuration(time.Since(start))) + len(stoppedAccounts), len(accounts), utils.RoundDuration(time.Since(start))) } func normalizeXiaodianRun(attempts, delayMs, fallbackAttempts int) (int, time.Duration) { @@ -926,14 +960,13 @@ func classifyRetryStop(message string) (account bool, product bool, reason strin func classifyXiaodianRetryStop(message string) (account bool, product bool, reason string) { switch { + case strings.Contains(message, "余额不足"), + strings.Contains(message, "点券不足"): + return true, false, "点券余额不足" case strings.Contains(message, "已拥有该道具"), strings.Contains(message, "已经拥有该道具"), strings.Contains(message, "已拥有该商品"): return true, false, "账号已拥有该道具" - case strings.Contains(message, "已达限量"), - strings.Contains(message, "已达本次活动兑换上限"), - strings.Contains(message, "兑换上限"): - return true, false, "已达兑换限量" case strings.Contains(message, "未绑定账号"): return true, false, "账号未绑定角色" case strings.Contains(message, "商品不存在"), diff --git a/internal/seckill/scheduler_test.go b/internal/seckill/scheduler_test.go index 8df7e21..6796ba7 100644 --- a/internal/seckill/scheduler_test.go +++ b/internal/seckill/scheduler_test.go @@ -121,6 +121,30 @@ func TestClassifyXiaodianRetryStopOwnedItem(t *testing.T) { } } +func TestClassifyXiaodianRetryStop(t *testing.T) { + cases := []struct { + message string + account bool + product bool + reason string + }{ + {"不好意思,您的余额不足~", true, false, "点券余额不足"}, + {"点券不足,请充值后重试", true, false, "点券余额不足"}, + {"未绑定账号", true, false, "账号未绑定角色"}, + {"商品不存在", false, true, "商品不可兑换"}, + {"已达限量", false, false, ""}, + {"已达本次活动兑换上限", false, false, ""}, + {"库存不足", false, false, ""}, + } + for _, tc := range cases { + account, product, reason := classifyXiaodianRetryStop(tc.message) + if account != tc.account || product != tc.product || reason != tc.reason { + t.Errorf("classifyXiaodianRetryStop(%q) = (%v,%v,%q), want (%v,%v,%q)", + tc.message, account, product, reason, tc.account, tc.product, tc.reason) + } + } +} + func TestFilterCooldownAttempts(t *testing.T) { attempts := makeAttempts("a", "b", "c") now := time.Now()