修复抢购流程多项问题

- executeRound 四次加锁合并为单次,减少锁竞争
- shouldFallbackStandard token 匹配从宽泛改为精确匹配 cvl_csrf_token
- retryCooldownDuration 新增操作频繁/请求过多/请求太频繁三种冷却匹配
- behaviorCooldown 从 8s 降为 5s
- BestServerTimeCalibration 部分失败时不再返回 error,仅全败才报错
- warmupSchedule 增加 gen 计数器+warmupRunMu 互斥锁,防止 resync 时新旧预热并发
- ScheduleRun 中 clearWarmups 移入锁内 stopSchedulesLocked 之后执行
This commit is contained in:
yml2213
2026-06-27 17:03:19 +08:00
parent 99885c1ddd
commit 76c942f10d
2 changed files with 37 additions and 23 deletions
+5 -1
View File
@@ -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 {
+32 -22
View File
@@ -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)