Files
dygou_go/internal/seckill/attempt.go
T

275 lines
8.9 KiB
Go

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)
}