275 lines
8.9 KiB
Go
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)
|
|
}
|