- 抢购调度重构:参考 dygou_go 将 sniper 拆分为独立子模块(runner/run_control/retry/exchange/classify/config/summary/category/helpers);移除全局自适应降速与轮次上限,采用账号独立循环、统一账号级退避、单请求并发闸门与首发错峰 - 预热与建连优化:默认关闭预热(避免单账号超时连坐整机开抢),开启时转为后台非阻塞协程;WebSocket 握手超时从 10s 降至 1500ms - 双时间日志:logger 增加 SetServerOffset,校准后日志前缀自动渲染「本地时间(H虎牙估算时间)」,实现跨机器绝对毫秒对齐 - 默认配置调优:版本提升至 v0.5.0,默认线程调为 2(缓解多实例并发限流),前端自动平滑迁移 - 验证方式:go test ./... -count=1 全通,npm run build 构建正常,单测覆盖双时间与预热配置
188 lines
4.7 KiB
Go
188 lines
4.7 KiB
Go
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
|
|
}
|
|
}
|