package sniper import ( "context" "errors" "fmt" "time" "huya-go/internal/huya" ) // emitSkippedDuplicate 在跳过同一奖品的重复兑换时给出明确结果,便于界面观察。 // // 每个「账号 + 奖品」只提示一次:窗口模式下每轮都会跳过同一个奖品, // 不记录会刷出一整个窗口的重复提示(但请求数不会增加)。 func (s *runState) emitSkippedDuplicate(emit func(Result), loop int, uid int64, index int, prizeID int64, target Target) { if s.markDuplicateNotified(uid, prizeID) { s.recordOutcome(CategoryAlreadyDone) } // 同一奖品已由本账号兑换成功,这个 target 不可能再成功,直接标记完成, // 避免多名称目标反复空转。 s.markDone(uid, index) if emit == nil { return } emit(Result{ Loop: loop, UID: uid, TargetIndex: index, Target: target, Action: "skip_duplicate", Category: CategoryAlreadyDone, Message: "该奖品本账号已兑换成功,跳过重复兑换", }) } // markDuplicateNotified 记录并返回是否是首次提示。 func (s *runState) markDuplicateNotified(uid, prizeID int64) bool { s.mu.Lock() defer s.mu.Unlock() if s.duplicateNotified[uid] == nil { s.duplicateNotified[uid] = map[int64]bool{} } if s.duplicateNotified[uid][prizeID] { return false } s.duplicateNotified[uid][prizeID] = true return true } func (r *Runner) exchangeTarget(ctx context.Context, client huya.ActivityClient, state *runState, account Account, target Target, index int, loop int, sid int32, prize *huya.PrizeInfo) Result { pid := target.PID if prize != nil { pid = int32(prize.ID) } result := Result{ Loop: loop, UID: account.UID, TargetIndex: index, Target: target, Action: "exchange", Prize: prize, } if !state.acquire(ctx) { result.Action = actionExchangeCanceled result.Category = CategoryError result.Message = "任务已取消,未发起兑换" result.Error = ctx.Err().Error() return result } // 排队等额度期间窗口可能已经结束,不能把请求补发到窗口外。 if state.windowExpired() { state.release() result.Action = actionExchangeCanceled result.Category = CategoryError result.Message = "窗口已结束,未发起兑换" return result } started := time.Now() exchange, err := client.ScoreExchangePrize(ctx, account.UID, account.Cookie, sid, pid) state.release() state.recordRequest(reqExchange, time.Since(started), err) result.Exchange = exchange result.Category = ClassifyExchange(exchange, err) result.Message = ExchangeMessage(exchange) if err != nil { result.Action = "exchange_error" result.Error = err.Error() if result.Message == "" { result.Message = "兑换请求失败" } } return result } func (r *Runner) applyExchangeDecision(state *runState, account Account, index int, cfg Config, result Result, cancel context.CancelFunc) Result { state.recordOutcome(result.Category) if result.Category == CategorySuccess { state.markSuccessUID(account.UID) } switch result.Category { case CategorySuccess: if cfg.StopAccountAfterSuccess { state.markAccountDone(account.UID) result.Done = true } else { state.markDone(account.UID, index) } if cfg.StopAfterSuccess { cancel() } case CategoryNotEnough, CategoryAlreadyDone: if cfg.StopOnNotEnough { state.markDone(account.UID, index) result.Done = true } case CategoryOffShelf: // 已下架 / 奖品不存在:本窗口内不会再出现,直接终止该 target。 state.markTargetDone(index) result.Done = true case CategorySoldOut: if cfg.StopOnSoldOut { state.markTargetDone(index) result.Done = true } } return result } func (r *Runner) refreshAfterSuccess(state *runState, client huya.ActivityClient, account Account, target Target, index int, loop int, cfg Config, result Result, emit func(Result)) { if !cfg.RefreshAfterSuccess || emit == nil || result.Category != CategorySuccess { return } state.refresh.Add(1) go func() { defer state.refresh.Done() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 刷新请求同样走并发闸门,但用拥有 5 秒预算的独立 ctx。 score, err := func() (*huya.GetUserScoreResp, error) { if !state.acquire(ctx) { return nil, ctx.Err() } defer state.release() started := time.Now() resp, err := client.QueryUserScore(ctx, account.UID, account.Cookie, cfg.SID) state.recordRequest(reqScore, time.Since(started), err) return resp, err }() scoreResult := Result{ Loop: loop, UID: account.UID, TargetIndex: index, Target: target, Action: "refresh_score", Category: CategoryReady, Message: "刷新积分完成", Score: score, } if err != nil { scoreResult.Category = CategoryError scoreResult.Message = "刷新积分失败" scoreResult.Error = err.Error() } else if score != nil { scoreResult.Message = fmt.Sprintf("剩余积分: %d", score.AvailableScore()) } emit(scoreResult) records, err := func() (*huya.GetUserPrizeRecordsResp, error) { if !state.acquire(ctx) { return nil, ctx.Err() } defer state.release() started := time.Now() resp, err := client.GetUserPrizeRecords(ctx, account.UID, account.Cookie, cfg.SID) state.recordRequest(reqRecord, time.Since(started), err) return resp, err }() recordResult := Result{ Loop: loop, UID: account.UID, TargetIndex: index, Target: target, Action: "refresh_records", Category: CategoryReady, Message: "刷新兑换记录完成", Records: records, } if err != nil { recordResult.Category = CategoryError recordResult.Message = "刷新兑换记录失败" recordResult.Error = err.Error() } emit(recordResult) }() } // listForAccount 返回本账号本轮要用的奖品列表快照。 // // SharePrizeList 打开时多账号复用同一份快照:TTL 内第一个账号查完,其余账号直接复用 // (单飞:同一时刻只有一个账号在查,避免并发重复查询)。关闭时各查各的。 func (r *Runner) listForAccount(ctx context.Context, client huya.ActivityClient, account Account, cfg Config, state *runState) *listSnapshot { fetch := func() *listSnapshot { if !state.acquire(ctx) { return &listSnapshot{err: ctx.Err()} } if state.windowExpired() { state.release() return &listSnapshot{err: errors.New("窗口已结束")} } started := time.Now() resp, err := client.GetActPrizeList(ctx, account.UID, account.Cookie, cfg.SID) state.release() state.recordRequest(reqList, time.Since(started), err) return &listSnapshot{resp: resp, err: err} } if !cfg.SharePrizeList { return fetch() } return state.sharedListOnce(ctx, fetch) }