package sniper import ( "context" "errors" "time" ) func newRunState(accounts []Account, targetCount, concurrency int, cfg Config) *runState { if concurrency <= 0 { concurrency = 1 } state := &runState{ startedAt: time.Now(), accounts: make(map[int64]*accountState, len(accounts)), targetDone: make([]bool, targetCount), exchanged: make(map[int64]map[int64]bool, len(accounts)), duplicateNotified: make(map[int64]map[int64]bool, len(accounts)), firstWaveSent: make(map[int64]bool, len(accounts)), sem: make(chan struct{}, concurrency), stats: runStats{ outcomes: map[string]int{}, successUIDs: map[int64]bool{}, requests: map[requestKind]*requestStat{}, accounts: map[int64]*AccountOutcome{}, }, } for _, account := range accounts { state.accounts[account.UID] = &accountState{targets: make([]targetState, targetCount)} } state.configureWindow(cfg, len(accounts)) return state } // configureWindow 按配置初始化窗口与首发错峰参数。 // // 只有 OpenAt 非零才是窗口模式;正常 GUI 抢购始终是窗口模式。 func (s *runState) configureWindow(cfg Config, accountCount int) { if cfg.OpenAt.IsZero() { return } window := time.Duration(cfg.WindowSeconds) * time.Second if cfg.WindowSeconds <= 0 { window = DefaultWindowSeconds * time.Second } if cfg.windowOverride > 0 { window = cfg.windowOverride } startAhead := cfg.StartAhead if startAhead <= 0 { startAhead = DefaultStartAhead } s.openAt = cfg.OpenAt s.startAhead = startAhead s.windowLen = window s.deadline = cfg.OpenAt.Add(window) if cfg.FirstWaveSpread > 0 { step := cfg.FirstWaveSpread / time.Duration(maxInt(accountCount-1, 1)) maxStep := cfg.FirstWaveMaxStep if maxStep <= 0 { maxStep = DefaultFirstWaveMaxStep } if step > maxStep { step = maxStep } if step < 0 { step = 0 } s.firstWaveSpread = cfg.FirstWaveSpread s.firstWaveStep = step } } // windowed 返回是否处于窗口模式。 func (s *runState) windowed() bool { return !s.deadline.IsZero() } // windowExpired 返回窗口是否已经结束(非窗口模式永远为 false)。 func (s *runState) windowExpired() bool { if s.deadline.IsZero() { return false } return !time.Now().Before(s.deadline) } // windowStart 返回窗口循环应当开始发请求的时刻(开抢时刻减去先手缓冲)。 func (s *runState) windowStart() time.Time { return s.openAt.Add(-s.startAhead) } // needsPrefire 返回是否还欠一轮「开抢前预打」。 // // 只有距离窗口起点还有一段时间才值得单独预打一轮:正常情况下触发时刻(开抢 − advance) // 与窗口起点(开抢 − StartAhead)几乎重合,此时再预打一轮就等于把首发齐射打两遍。 func (s *runState) needsPrefire() bool { if s.openAt.IsZero() || s.prefireDone { return false } return time.Now().Add(prefireMinGap).Before(s.windowStart()) } func (s *runState) markPrefire() { s.prefireDone = true } // clipToDeadline 把等待时间裁剪到窗口结束,保证不会睡过窗口。 func (s *runState) clipToDeadline(delay time.Duration) time.Duration { if s.deadline.IsZero() { return delay } remaining := time.Until(s.deadline) if remaining <= 0 { return 0 } if delay > remaining { return remaining } return delay } // noteAttempt 记录所有账号里跑过的最大轮次(用于汇总日志)。 func (s *runState) noteAttempt(round int) { s.mu.Lock() defer s.mu.Unlock() if round > s.maxAttemptCount { s.maxAttemptCount = round } } // maxAttempt 返回所有账号里跑过的最大轮次。 func (s *runState) maxAttempt() int { s.mu.Lock() defer s.mu.Unlock() return s.maxAttemptCount } // accountFinished 返回该账号是否已经没有可尝试的目标(全部成功或进入终态)。 func (s *runState) accountFinished(uid int64, targetCount int) bool { s.mu.Lock() defer s.mu.Unlock() account := s.accounts[uid] if account == nil || account.done { return true } for index := 0; index < targetCount; index++ { if index < len(s.targetDone) && s.targetDone[index] { continue } if index < len(account.targets) && !account.targets[index].Done { return false } } return true } // sharedListOnce 保证同一时刻只有一个账号去查询奖品列表,其余账号等它完成后复用结果。 // // 只缓存成功的快照:失败结果不缓存,否则一次抖动会让所有账号一起失败。 func (s *runState) sharedListOnce(ctx context.Context, fetch func() *listSnapshot) *listSnapshot { s.listMu.Lock() if s.listSnapshot != nil && time.Since(s.listCachedAt) <= sharedListTTL { snapshot := s.listSnapshot s.listMu.Unlock() return snapshot } if inflight := s.listInflight; inflight != nil { s.listMu.Unlock() select { case <-inflight: case <-ctx.Done(): return &listSnapshot{err: ctx.Err()} } s.listMu.Lock() snapshot := s.listSnapshot s.listMu.Unlock() if snapshot != nil { return snapshot } return &listSnapshot{err: errors.New("奖品列表响应为空")} } inflight := make(chan struct{}) s.listInflight = inflight s.listMu.Unlock() snapshot := fetch() s.listMu.Lock() if snapshot != nil && snapshot.err == nil && snapshot.resp != nil { s.listSnapshot = snapshot s.listCachedAt = time.Now() } s.listInflight = nil s.listMu.Unlock() close(inflight) return snapshot } // takeFirstWaveDelay 领取本账号的首发错峰等待时间,每个账号只生效一次。 func (s *runState) takeFirstWaveDelay(uid int64) time.Duration { if s.firstWaveStep <= 0 { return 0 } s.mu.Lock() defer s.mu.Unlock() if s.firstWaveSent[uid] { return 0 } s.firstWaveSent[uid] = true slot := s.firstWaveUsed s.firstWaveUsed++ return time.Duration(slot) * s.firstWaveStep } // noteOutcome 记录一个账号最近一次兑换类结果,供任务结束时的逐账号结局输出。 func (s *runState) noteOutcome(result Result) { switch result.Action { case "exchange", "exchange_error", "skip_duplicate", "not_found": default: return } s.mu.Lock() defer s.mu.Unlock() if s.stats.accounts == nil { s.stats.accounts = map[int64]*AccountOutcome{} } outcome := s.stats.accounts[result.UID] if outcome == nil { outcome = &AccountOutcome{UID: result.UID} s.stats.accounts[result.UID] = outcome } if result.Action != "skip_duplicate" { outcome.Attempts++ } outcome.Category = result.Category outcome.Message = result.Message if result.Category == CategorySuccess || result.Done { outcome.Done = true } } // requestKind 标识一次在途请求的类型,用于汇总日志里的请求次数。 type requestKind int const ( reqList requestKind = iota reqExchange reqScore reqRecord ) // String 返回接口类型的中文名,用于「接口统计」日志。 func (k requestKind) String() string { switch k { case reqList: return "奖品列表" case reqExchange: return "兑换" case reqScore: return "积分" case reqRecord: return "兑换记录" default: return "未知" } } // acquire 申请一个请求额度;ctx 结束时会返回 false,调用方应直接放弃这次请求。 func (s *runState) acquire(ctx context.Context) bool { if s.sem == nil { return ctx.Err() == nil } select { case s.sem <- struct{}{}: return true case <-ctx.Done(): return false } } func (s *runState) release() { if s.sem == nil { return } <-s.sem } // recordRequest 记录一次请求的次数、耗时与错误。 // // 除了原来的分类计数,还累计耗时/失败/最后错误,任务结束时输出「接口统计」行, // 这样复盘时能一眼看出是「请求打不出去(慢/超时)」还是「打出去被拒(限流)」。 func (s *runState) recordRequest(kind requestKind, cost time.Duration, err error) { s.mu.Lock() defer s.mu.Unlock() switch kind { case reqList: s.stats.listReqs++ case reqExchange: s.stats.exchangeReqs++ case reqScore: s.stats.scoreReqs++ case reqRecord: s.stats.recordReqs++ } if s.stats.requests == nil { s.stats.requests = map[requestKind]*requestStat{} } stat := s.stats.requests[kind] if stat == nil { stat = &requestStat{} s.stats.requests[kind] = stat } stat.observe(cost, err) } // recordOutcome 累计一条结局分类,供任务结束时的汇总日志使用。 func (s *runState) recordOutcome(category string) { if category == "" { return } s.mu.Lock() defer s.mu.Unlock() if s.stats.outcomes == nil { s.stats.outcomes = map[string]int{} } s.stats.outcomes[category]++ } func (s *runState) markSuccessUID(uid int64) { s.mu.Lock() defer s.mu.Unlock() if s.stats.successUIDs == nil { s.stats.successUIDs = map[int64]bool{} } s.stats.successUIDs[uid] = true } func (s *runState) snapshotStats() runStats { s.mu.Lock() defer s.mu.Unlock() out := runStats{ listReqs: s.stats.listReqs, exchangeReqs: s.stats.exchangeReqs, scoreReqs: s.stats.scoreReqs, recordReqs: s.stats.recordReqs, outcomes: make(map[string]int, len(s.stats.outcomes)), successUIDs: make(map[int64]bool, len(s.stats.successUIDs)), requests: make(map[requestKind]*requestStat, len(s.stats.requests)), accounts: make(map[int64]*AccountOutcome, len(s.stats.accounts)), } for key, value := range s.stats.outcomes { out.outcomes[key] = value } for uid := range s.stats.successUIDs { out.successUIDs[uid] = true } for kind, stat := range s.stats.requests { if stat == nil { continue } copied := *stat out.requests[kind] = &copied } for uid, outcome := range s.stats.accounts { if outcome == nil { continue } copied := *outcome out.accounts[uid] = &copied } return out } // prizeExchanged 判断该账号是否已经成功兑换过这个奖品。 func (s *runState) prizeExchanged(uid, prizeID int64) bool { if prizeID <= 0 { return false } s.mu.Lock() defer s.mu.Unlock() return s.exchanged[uid][prizeID] } func (s *runState) markPrizeExchanged(uid, prizeID int64) { if prizeID <= 0 { return } s.mu.Lock() defer s.mu.Unlock() if s.exchanged[uid] == nil { s.exchanged[uid] = map[int64]bool{} } s.exchanged[uid][prizeID] = true } func (s *runState) activeTargets(uid int64, targetCount int) []int { s.mu.Lock() defer s.mu.Unlock() account := s.accounts[uid] active := make([]int, 0, targetCount) if account == nil || account.done { return active } for i := 0; i < targetCount; i++ { if i < len(s.targetDone) && s.targetDone[i] { continue } if i >= len(account.targets) || account.targets[i].Done { continue } active = append(active, i) } return active } func (s *runState) markDone(uid int64, index int) { s.mu.Lock() defer s.mu.Unlock() if account := s.accounts[uid]; account != nil && index >= 0 && index < len(account.targets) { account.targets[index].Done = true } } // markAccountDone 一次性结束该账号的所有目标。 func (s *runState) markAccountDone(uid int64) { s.mu.Lock() defer s.mu.Unlock() if account := s.accounts[uid]; account != nil { account.done = true for index := range account.targets { account.targets[index].Done = true } } } func (s *runState) markTargetDone(index int) { s.mu.Lock() defer s.mu.Unlock() if index >= 0 && index < len(s.targetDone) { s.targetDone[index] = true } } func (s *runState) allDone() bool { s.mu.Lock() defer s.mu.Unlock() for index, globallyDone := range s.targetDone { if globallyDone { continue } for _, account := range s.accounts { if account == nil || account.done { continue } if index < len(account.targets) && !account.targets[index].Done { return false } } } return true }