diff --git a/internal/client/client.go b/internal/client/client.go index a4207ba..f1b4797 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -138,6 +138,10 @@ func (c *DouyuClient) BestServerTimeCalibration(rounds int, maxWait time.Duratio Fallback: true, }, fmt.Errorf("斗鱼时间校准失败: %w", lastErr) } + var calErr error + if lastErr != nil { + calErr = fmt.Errorf("斗鱼时间校准部分失败(已使用最优结果): %w", lastErr) + } return types.TimeCalibration{ Offset: best.Offset, ErrorBound: best.ErrorBound, @@ -146,7 +150,7 @@ func (c *DouyuClient) BestServerTimeCalibration(rounds int, maxWait time.Duratio CreatedAt: time.Now(), Source: best.Source, Fallback: best.Fallback, - }, lastErr + }, calErr } func betterTimeSample(candidate, current types.TimeSample) bool { diff --git a/internal/seckill/scheduler.go b/internal/seckill/scheduler.go index f27f46d..834b125 100644 --- a/internal/seckill/scheduler.go +++ b/internal/seckill/scheduler.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "time" "github.com/golang/freetype/truetype" @@ -30,7 +31,7 @@ const ( timeCalibrateWait = 1200 * time.Millisecond resyncBeforeTrigger = 10 * time.Second seckillMaxDuration = 75 * time.Second - behaviorCooldown = 8 * time.Second + behaviorCooldown = 5 * time.Second clickFastCooldown = 3 * time.Second ) @@ -77,9 +78,11 @@ type Scheduler struct { schedule *time.Timer warmupSchedule *time.Timer resyncSchedule *time.Timer + warmupGen int64 warmupsMu sync.RWMutex warmups map[string]types.WarmupData + warmupRunMu sync.Mutex attemptCacheMu sync.RWMutex attemptCache map[string]*types.AttemptCache // 激进模式缓存 @@ -580,36 +583,30 @@ func (s *Scheduler) executeRound(attempts []types.ExchangeAttempt, settings runS key := fmt.Sprintf("%s_第%d包", result.Account, result.Attempt) s.setResult(key, result.Message) - mu.Lock() - outcome.attempts++ - mu.Unlock() - if result.OK { logger.LogAccountEvent("抢购", "success", result.Account, "成功: 账号=%s 商品=%s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message) s.enqueueSuccessImage(attempt.Account, attempt.Product, result.RoleName) - - mu.Lock() - outcome.successAccounts[attempt.Account.Name] = true - mu.Unlock() } else { logger.LogAccountEvent("抢购", "info", result.Account, "尝试: 账号=%s 商品=%s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message) + } + + mu.Lock() + outcome.attempts++ + if result.OK { + outcome.successAccounts[attempt.Account.Name] = true + } else { if cooldown := retryCooldownDuration(result.Message); cooldown > 0 { - mu.Lock() outcome.cooldownAccounts[attempt.Account.Name] = time.Now().Add(jitterDuration(cooldown)) - mu.Unlock() } terminalAccount, terminalProduct, reason := classifyRetryStop(result.Message) - if terminalAccount || terminalProduct { - mu.Lock() - if terminalAccount { - outcome.stoppedAccounts[attempt.Account.Name] = reason - } - if terminalProduct { - outcome.stoppedProducts[attempt.Product.CommodityID] = reason - } - mu.Unlock() + if terminalAccount { + outcome.stoppedAccounts[attempt.Account.Name] = reason + } + if terminalProduct { + outcome.stoppedProducts[attempt.Product.CommodityID] = reason } } + mu.Unlock() }(attempt) } @@ -654,6 +651,12 @@ func retryCooldownDuration(message string) time.Duration { return behaviorCooldown case strings.Contains(message, "点击太快"): return clickFastCooldown + case strings.Contains(message, "操作频繁"): + return behaviorCooldown + case strings.Contains(message, "请求过多"): + return behaviorCooldown + case strings.Contains(message, "请求太频繁"): + return behaviorCooldown default: return 0 } @@ -795,6 +798,9 @@ func (s *Scheduler) clearAttemptCache() { } func (s *Scheduler) warmupAttempts(attempts []types.ExchangeAttempt) { + s.warmupRunMu.Lock() + defer s.warmupRunMu.Unlock() + attempts = uniqueWarmupAttempts(attempts) if len(attempts) == 0 { return @@ -1053,7 +1059,7 @@ func shouldFallbackStandard(errMsg string) bool { switch { case strings.Contains(lower, "csrf"): return true - case strings.Contains(lower, "token"): + case strings.Contains(lower, "cvl_csrf_token"): return true case strings.Contains(lower, "preexchange"): return true @@ -1094,9 +1100,9 @@ func (s *Scheduler) ScheduleRun(req types.SeckillRequest) (map[string]any, error } ap := utils.CloneStringMap(req.AccountProducts) - s.clearWarmups() s.scheduleMu.Lock() s.stopSchedulesLocked() + s.clearWarmups() s.scheduleMu.Unlock() s.scheduleTimers(ap, targetTime, triggerTime, calibration, false) @@ -1156,7 +1162,11 @@ func (s *Scheduler) scheduleTimers(accountProducts map[string]string, targetTime s.scheduleMu.Lock() defer s.scheduleMu.Unlock() + gen := atomic.AddInt64(&s.warmupGen, 1) s.replaceTimerLocked(&s.warmupSchedule, warmupRemain, func() { + if atomic.LoadInt64(&s.warmupGen) != gen { + return + } logger.LogEvent("预热", "success", "预热触发: 本地=%s", time.Now().In(constants.CST).Format("15:04:05.000")) settings := s.settingsSnapshot() attempts := s.buildAttempts(accountProducts, settings.attempts)