package sniper import ( "time" ) // sessionLostLimit 返回「连续会话失效多少次后停止该账号」的阈值。 func sessionLostLimit(cfg Config) int { if cfg.SessionLostLimit > 0 { return cfg.SessionLostLimit } return defaultSessionLostLimit } // completeRound 每个账号每轮只更新一次连续失败/会话失效计数。 // 多目标一轮同时失败不会被放大成多次失败;任意可信业务回包都会清零会话失效。 func (s *runState) completeRound(uid int64, categories []string, sessionLimit int) { s.mu.Lock() defer s.mu.Unlock() account := s.accounts[uid] if account == nil { return } hasSuccess := false hasRetry := false hasSessionLost := false hasSessionAlive := false for _, category := range categories { switch category { case CategorySuccess: hasSuccess = true hasSessionAlive = true case CategorySessionLost: hasSessionLost = true default: if sessionAliveCategory(category) { hasSessionAlive = true } if backoffCategory(category) { hasRetry = true } } } if hasSuccess { account.failStreak = 0 account.sessionStreak = 0 return } if hasRetry { account.failStreak++ } if hasSessionAlive { account.sessionStreak = 0 } else if hasSessionLost { account.sessionStreak++ } if sessionLimit > 0 && account.sessionStreak >= sessionLimit { account.done = true } } // roundDelay 返回本轮结束后该账号的统一等待时间:取本轮所有失败分类里 // 最长的退避值,不再为每个 target 单独维护下一次重试时间。 func (s *runState) roundDelay(uid int64, cfg Config, categories []string) time.Duration { delay := cfg.Delay if delay <= 0 { delay = 100 * time.Millisecond } seen := make(map[string]bool, len(categories)) for _, category := range categories { if category == "" || seen[category] || !backoffCategory(category) { continue } seen[category] = true if candidate := s.retryDelay(uid, cfg, category); candidate > delay { delay = candidate } } return delay } // retryDelay 计算当前账号在某个结局后的退避时间。 func (s *runState) retryDelay(uid int64, cfg Config, category string) time.Duration { s.mu.Lock() streak := 0 if account := s.accounts[uid]; account != nil { streak = account.failStreak } s.mu.Unlock() return computeRetryDelay(s.elapsed(), streak, cfg, category) } // computeRetryDelay 使用与 dygou_go 相同的简单阶梯: // 分类基础值 × 时间阶段 × 连续失败倍率,最后叠加抖动并封顶 1.5 秒。 func computeRetryDelay(elapsed time.Duration, failStreak int, cfg Config, category string) time.Duration { base := cfg.ErrorBackoff switch category { case CategoryTooFrequent: base = cfg.TooFrequentBackoff case CategoryNotOpen: base = maxDuration(cfg.Delay, 50*time.Millisecond) } if base <= 0 { base = defaultRetryBackoff } delay := time.Duration(stageMultiplier(elapsed, category)) * base if mult := streakMultiplier(failStreak); mult > 1 { delay *= time.Duration(mult) } if delay > maxRetryDelay { delay = maxRetryDelay } delay = jitterDuration(delay) if delay > maxRetryDelay { delay = maxRetryDelay } return delay } // elapsed 返回相对开抢时刻的已用时,用于按阶段取退避节奏。 func (s *runState) elapsed() time.Duration { if !s.openAt.IsZero() { elapsed := time.Since(s.openAt) if elapsed < 0 { return 0 } return elapsed } if s.startedAt.IsZero() { return 0 } return time.Since(s.startedAt) } // stageMultiplier 按已抢购时长返回退避倍率:开抢头 2 秒最激进,5 秒后放慢到 3 倍。 func stageMultiplier(elapsed time.Duration, category string) int { if backoffCategory(category) { switch { case elapsed < 2*time.Second: return 1 case elapsed < 5*time.Second: return 2 default: return 3 } } if elapsed >= 5*time.Second { return 2 } return 1 } // backoffCategory 返回该结局是否需要账号级退避。 func backoffCategory(category string) bool { switch category { case "", CategorySuccess, CategoryNotEnough, CategoryAlreadyDone, CategoryOffShelf: return false default: return true } } // streakMultiplier 把连续失败次数换算成翻倍系数:第 3 次起 ×2,第 4 次起 ×4。 func streakMultiplier(streak int) int { if streak <= 2 { return 1 } mult := 1 for i := 2; i < streak && mult < 4; i++ { mult <<= 1 } return mult } // sessionAliveCategory 返回该结局是否能证明当前账号的登录态仍然有效。 // 网络错误、协议异常和限流都不算。 func sessionAliveCategory(category string) bool { switch category { case CategorySuccess, CategoryNotOpen, CategorySoldOut, CategoryNotEnough, CategoryAlreadyDone, CategoryOffShelf, CategoryCondition, CategoryFailed: return true default: return false } }