package seckill import ( "strings" "time" "douyu_go/internal/logger" "douyu_go/internal/types" "douyu_go/internal/utils" ) // runAttempt 标准模式兑换:完整执行鉴权 token→BatchLimit→PreExchange→Exchange 链路。 func (s *Scheduler) runAttempt(attempt types.ExchangeAttempt) types.ExchangeResult { cli := s.clientSnapshot() // 最终防护:任何旧入口在和平小店活动下都必须使用 livebuy。 if cli.Activity.IsXiaodian { return s.runXiaodianAttempt(attempt) } result := types.ExchangeResult{ Account: attempt.Account.Name, Product: attempt.Product.CommodityName, Attempt: attempt.Index, } logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "执行资格预检链路: 账号=%s 商品=%s 第%d包", attempt.Account.Name, attempt.Product.CommodityName, attempt.Index) var roleName string csrf := "" if warmup, ok := s.warmupFor(attempt); ok { csrf = warmup.CSRF roleName = warmup.RoleName logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "使用预热鉴权token: 账号=%s 商品=%s 预热距今=%s", attempt.Account.Name, attempt.Product.CommodityName, utils.RoundDuration(time.Since(warmup.CreatedAt))) } if csrf == "" { var err error csrf, err = cli.GenerateCSRF(attempt.Account.Cookie) if err != nil { result.Message = "失败|生成鉴权token失败: " + err.Error() return result } logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "鉴权token已生成,本次兑换链路复用同一个 token: 账号=%s 商品=%s", attempt.Account.Name, attempt.Product.CommodityName) } limit, err := cli.BatchExchangeLimit(attempt.Account.Cookie, attempt.Product.CommodityID, &csrf) if err != nil { result.Message = "失败|批量限制检查失败: " + err.Error() return result } if limit <= 0 { result.Message = "失败|当前商品限制兑换" return result } logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "批量限制通过: 账号=%s 商品=%s limit=%d", attempt.Account.Name, attempt.Product.CommodityName, limit) if err := cli.PreExchange(attempt.Account.Cookie, attempt.Product.CommodityID, &csrf); err != nil { result.Message = "失败|预兑换失败: " + err.Error() return result } s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{ CSRF: csrf, RoleName: roleName, Qualified: true, LastCheck: time.Now(), }) logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "资格预检通过: 账号=%s 商品=%s,后续可直兑", attempt.Account.Name, attempt.Product.CommodityName) exchangeInfo, err := cli.ExchangeProduct(attempt.Account.Cookie, attempt.Product, &csrf) if err != nil { s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{ CSRF: csrf, RoleName: roleName, Qualified: true, LastCheck: time.Now(), }) result.RoleName = roleName result.Message = "失败|" + err.Error() return result } if roleName == "" { roleName, err = cli.RoleName(attempt.Account.Cookie) if err != nil { logger.LogAccountEvent("抢购", "warn", attempt.Account.Name, "角色名查询失败: 账号=%s err=%v", attempt.Account.Name, err) } } result.OK = true result.RoleName = roleName if roleName != "" { result.Message = "成功|" + roleName + "|" + exchangeInfo } else { result.Message = "成功|" + exchangeInfo } // 缓存鉴权 token 和角色名(激进模式使用) s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{ CSRF: csrf, RoleName: roleName, Qualified: true, LastCheck: time.Now(), }) return result } // runAttemptFast 激进模式的兑换逻辑,复用缓存的鉴权 token,跳过 BatchExchangeLimit 和 PreExchange。 func (s *Scheduler) runAttemptFast(attempt types.ExchangeAttempt) types.ExchangeResult { cli := s.clientSnapshot() if cli.Activity.IsXiaodian { return s.runXiaodianAttempt(attempt) } result := types.ExchangeResult{ Account: attempt.Account.Name, Product: attempt.Product.CommodityName, Attempt: attempt.Index, } cache := s.getAttemptCache(attempt.Account.Name) if cache == nil || cache.CSRF == "" || time.Since(cache.LastCheck) >= qualifiedMaxAge { logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "激进模式执行资格预检: 账号=%s 原因=无缓存鉴权token", attempt.Account.Name) return s.runAttempt(attempt) } if !cache.Qualified { logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "激进模式执行资格预检: 账号=%s 原因=未完成资格预检", attempt.Account.Name) return s.runAttempt(attempt) } csrf := cache.CSRF logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "激进模式直兑: 账号=%s 商品=%s 第%d包", attempt.Account.Name, attempt.Product.CommodityName, attempt.Index) // 直接调用 Exchange exchangeInfo, err := cli.ExchangeProduct(attempt.Account.Cookie, attempt.Product, &csrf) if err != nil { s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{ CSRF: csrf, RoleName: cache.RoleName, Qualified: true, LastCheck: time.Now(), }) result.RoleName = cache.RoleName errMsg := err.Error() if shouldFallbackStandard(errMsg) { logger.LogAccountEvent("抢购", "warn", attempt.Account.Name, "激进模式回退标准: 账号=%s 原因=%s", attempt.Account.Name, utils.TrimForLog(errMsg, 80)) s.setAttemptCache(attempt.Account.Name, nil) // 清空缓存 return s.runAttempt(attempt) } result.Message = "失败|" + errMsg return result } // 获取角色名 roleName := "" if cache != nil { roleName = cache.RoleName } if roleName == "" { var err error roleName, err = cli.RoleName(attempt.Account.Cookie) if err != nil { logger.LogAccountEvent("抢购", "warn", attempt.Account.Name, "角色名查询失败: 账号=%s err=%v", attempt.Account.Name, err) } } s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{ CSRF: csrf, RoleName: roleName, Qualified: true, LastCheck: time.Now(), }) result.OK = true result.RoleName = roleName if roleName != "" { result.Message = "成功|" + roleName + "|" + exchangeInfo } else { result.Message = "成功|" + exchangeInfo } return result } // runXiaodianAttempt 和平小店专用兑换:跳过手册鉴权、批量限制和预兑换,直接调用 livebuy。 func (s *Scheduler) runXiaodianAttempt(attempt types.ExchangeAttempt) types.ExchangeResult { cli := s.clientSnapshot() result := types.ExchangeResult{ Account: attempt.Account.Name, Product: attempt.Product.CommodityName, Attempt: attempt.Index, } if s.runStopped() { result.Message = "失败|已收到停止指令" return result } logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "和平小店直接兑换: 账号=%s 商品=%s 第%d次", attempt.Account.Name, attempt.Product.CommodityName, attempt.Index) exchangeInfo, err := cli.ExchangeProduct(attempt.Account.Cookie, attempt.Product, nil) if err != nil { result.Message = "失败|" + err.Error() return result } roleName, err := cli.RoleName(attempt.Account.Cookie) if err != nil { logger.LogAccountEvent("抢购", "warn", attempt.Account.Name, "角色名查询失败: 账号=%s err=%v", attempt.Account.Name, err) } result.OK = true result.RoleName = roleName if roleName != "" { result.Message = "成功|" + roleName + "|" + exchangeInfo } else { result.Message = "成功|" + exchangeInfo } return result } func shouldFallbackStandard(errMsg string) bool { lower := strings.ToLower(errMsg) switch { case strings.Contains(lower, "csrf"): return true case strings.Contains(lower, "cvl_csrf_token"): return true case strings.Contains(lower, "preexchange"): return true case strings.Contains(lower, "limit"): return true case strings.Contains(errMsg, "预兑换"): return true case strings.Contains(errMsg, "限制检查"): return true default: return false } } // ========== 激进模式缓存管理 ========== func (s *Scheduler) getAttemptCache(accountName string) *types.AttemptCache { s.attemptCacheMu.RLock() defer s.attemptCacheMu.RUnlock() return s.attemptCache[accountName] } // initCacheFromWarmup 从预热数据初始化激进模式缓存。 func (s *Scheduler) initCacheFromWarmup(attempts []types.ExchangeAttempt) { for _, attempt := range attempts { if warmup, ok := s.warmupFor(attempt); ok { s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{ CSRF: warmup.CSRF, RoleName: warmup.RoleName, Qualified: warmup.Qualified, LastCheck: time.Now(), }) logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "从预热初始化缓存: 账号=%s", attempt.Account.Name) } } } func (s *Scheduler) setAttemptCache(accountName string, cache *types.AttemptCache) { s.attemptCacheMu.Lock() defer s.attemptCacheMu.Unlock() if cache == nil { delete(s.attemptCache, accountName) return } s.attemptCache[accountName] = cache } func (s *Scheduler) clearAttemptCache() { s.attemptCacheMu.Lock() defer s.attemptCacheMu.Unlock() s.attemptCache = make(map[string]*types.AttemptCache) }