抢购逻辑优化
This commit is contained in:
@@ -3,7 +3,7 @@ package constants
|
||||
import "time"
|
||||
|
||||
// Version 应用版本号,与 wails.json version 同步。
|
||||
const Version = "0.0.2"
|
||||
const Version = "0.0.3"
|
||||
|
||||
const (
|
||||
ManualID = "G4KA4Qnz4LDp7"
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -26,8 +27,16 @@ const (
|
||||
timeCalibrateRounds = 3
|
||||
timeCalibrateWait = 1200 * time.Millisecond
|
||||
resyncBeforeTrigger = 10 * time.Second
|
||||
seckillMaxDuration = 75 * time.Second
|
||||
)
|
||||
|
||||
type roundOutcome struct {
|
||||
successAccounts map[string]bool
|
||||
stoppedAccounts map[string]string
|
||||
stoppedProducts map[string]string
|
||||
attempts int
|
||||
}
|
||||
|
||||
// Scheduler 抢购调度器。
|
||||
type Scheduler struct {
|
||||
client *client.DouyuClient
|
||||
@@ -213,19 +222,151 @@ func (s *Scheduler) finishRun() {
|
||||
s.running = false
|
||||
}
|
||||
|
||||
// RunNow 立即执行一批抢购任务。
|
||||
// RunNow 立即执行抢购任务,持续循环直到超时或全部成功。
|
||||
func (s *Scheduler) RunNow(accountProducts map[string]string) {
|
||||
defer s.finishRun()
|
||||
s.ClearResults()
|
||||
attempts := s.buildAttempts(accountProducts)
|
||||
if len(attempts) == 0 {
|
||||
|
||||
allAttempts := s.buildAttempts(accountProducts)
|
||||
if len(allAttempts) == 0 {
|
||||
logger.LogEvent("抢购", "warn", "没有可执行的抢购任务")
|
||||
return
|
||||
}
|
||||
|
||||
logger.LogEvent("抢购", "info", "开始抢购: %d 个请求", len(attempts))
|
||||
successAccounts := make(map[string]bool)
|
||||
stoppedAccounts := make(map[string]string)
|
||||
stoppedProducts := make(map[string]string)
|
||||
totalAccounts := uniqueAttemptAccounts(allAttempts)
|
||||
totalAttempts := 0
|
||||
startTime := time.Now()
|
||||
round := 0
|
||||
|
||||
logger.LogEvent("抢购", "info", "开始抢购: %d 个账号,窗口=%v,前段高频后段降速", totalAccounts, seckillMaxDuration)
|
||||
|
||||
for {
|
||||
elapsed := time.Since(startTime)
|
||||
if elapsed >= seckillMaxDuration {
|
||||
logger.LogEvent("抢购", "info", "已达到抢购窗口 %v,停止抢购", seckillMaxDuration)
|
||||
break
|
||||
}
|
||||
|
||||
round++
|
||||
remaining := seckillMaxDuration - elapsed
|
||||
|
||||
pendingAttempts := s.filterPendingAttempts(allAttempts, stoppedAccounts, stoppedProducts)
|
||||
if len(pendingAttempts) == 0 {
|
||||
logger.LogEvent("抢购", "info", "所有账号都已成功或遇到终止条件,停止抢购")
|
||||
break
|
||||
}
|
||||
|
||||
roundAttempts := s.pickRoundAttempts(pendingAttempts, round)
|
||||
if len(roundAttempts) == 0 {
|
||||
logger.LogEvent("抢购", "info", "第%d轮无可执行账号,停止抢购", round)
|
||||
break
|
||||
}
|
||||
|
||||
logger.LogEvent("抢购", "info", "第%d轮: 待抢购 %d 个账号,本轮执行=%d,剩余时间=%v",
|
||||
round, uniqueAttemptAccounts(pendingAttempts), len(roundAttempts), utils.RoundDuration(remaining))
|
||||
|
||||
outcome := s.executeRound(roundAttempts)
|
||||
totalAttempts += outcome.attempts
|
||||
|
||||
for account := range outcome.successAccounts {
|
||||
successAccounts[account] = true
|
||||
stoppedAccounts[account] = "已抢到"
|
||||
}
|
||||
for account, reason := range outcome.stoppedAccounts {
|
||||
if stoppedAccounts[account] == "" {
|
||||
stoppedAccounts[account] = reason
|
||||
}
|
||||
}
|
||||
for product, reason := range outcome.stoppedProducts {
|
||||
if stoppedProducts[product] == "" {
|
||||
stoppedProducts[product] = reason
|
||||
logger.LogEvent("抢购", "warn", "商品停止重试: %s,原因=%s", product, reason)
|
||||
}
|
||||
}
|
||||
|
||||
if len(outcome.successAccounts) > 0 {
|
||||
logger.LogEvent("抢购", "success", "第%d轮成功: %d 个账号", round, len(outcome.successAccounts))
|
||||
}
|
||||
if len(stoppedAccounts) >= totalAccounts {
|
||||
logger.LogEvent("抢购", "info", "所有账号都已成功或停止重试,停止抢购")
|
||||
break
|
||||
}
|
||||
|
||||
retryInterval := seckillRetryInterval(time.Since(startTime))
|
||||
if retryInterval > 0 && time.Since(startTime)+retryInterval < seckillMaxDuration {
|
||||
time.Sleep(jitterDuration(retryInterval))
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogEvent("抢购", "info", "抢购任务结束: 成功=%d/%d,停止=%d,账号尝试=%d,耗时=%v",
|
||||
len(successAccounts), totalAccounts, len(stoppedAccounts)-len(successAccounts), totalAttempts, utils.RoundDuration(time.Since(startTime)))
|
||||
}
|
||||
|
||||
// filterPendingAttempts 过滤掉已终止账号和已确认无库存的商品。
|
||||
func (s *Scheduler) filterPendingAttempts(allAttempts []types.ExchangeAttempt, stoppedAccounts, stoppedProducts map[string]string) []types.ExchangeAttempt {
|
||||
var pending []types.ExchangeAttempt
|
||||
for _, attempt := range allAttempts {
|
||||
if stoppedAccounts[attempt.Account.Name] != "" {
|
||||
continue
|
||||
}
|
||||
if stoppedProducts[attempt.Product.CommodityID] != "" {
|
||||
continue
|
||||
}
|
||||
pending = append(pending, attempt)
|
||||
}
|
||||
return pending
|
||||
}
|
||||
|
||||
// pickRoundAttempts 每轮每个账号最多执行一次,避免同账号并发过猛触发频控。
|
||||
func (s *Scheduler) pickRoundAttempts(attempts []types.ExchangeAttempt, round int) []types.ExchangeAttempt {
|
||||
if len(attempts) == 0 {
|
||||
return nil
|
||||
}
|
||||
preferredIndex := 1
|
||||
if s.attempts > 0 {
|
||||
preferredIndex = ((round - 1) % s.attempts) + 1
|
||||
}
|
||||
|
||||
byAccount := make(map[string]types.ExchangeAttempt)
|
||||
fallback := make(map[string]types.ExchangeAttempt)
|
||||
order := make([]string, 0, len(attempts))
|
||||
for _, attempt := range attempts {
|
||||
name := attempt.Account.Name
|
||||
if _, ok := fallback[name]; !ok {
|
||||
fallback[name] = attempt
|
||||
order = append(order, name)
|
||||
}
|
||||
if attempt.Index == preferredIndex {
|
||||
byAccount[name] = attempt
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]types.ExchangeAttempt, 0, len(order))
|
||||
for _, name := range order {
|
||||
if attempt, ok := byAccount[name]; ok {
|
||||
out = append(out, attempt)
|
||||
} else {
|
||||
out = append(out, fallback[name])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// executeRound 执行一轮抢购,并返回成功和终止重试的账号。
|
||||
func (s *Scheduler) executeRound(attempts []types.ExchangeAttempt) roundOutcome {
|
||||
outcome := roundOutcome{
|
||||
successAccounts: make(map[string]bool),
|
||||
stoppedAccounts: make(map[string]string),
|
||||
stoppedProducts: make(map[string]string),
|
||||
}
|
||||
var mu sync.Mutex
|
||||
|
||||
limit := make(chan struct{}, s.concurrency)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, attempt := range attempts {
|
||||
wg.Add(1)
|
||||
go func(attempt types.ExchangeAttempt) {
|
||||
@@ -236,16 +377,80 @@ func (s *Scheduler) RunNow(accountProducts map[string]string) {
|
||||
result := s.runAttempt(attempt)
|
||||
key := fmt.Sprintf("%s_第%d包", result.Account, result.Attempt)
|
||||
s.setResult(key, result.Message)
|
||||
|
||||
mu.Lock()
|
||||
outcome.attempts++
|
||||
mu.Unlock()
|
||||
|
||||
if result.OK {
|
||||
logger.LogEvent("抢购", "success", "成功: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
|
||||
go image.SaveSuccessImage(attempt.Account, attempt.Product, result.RoleName, s.font, s.paths.SuccessDir)
|
||||
|
||||
mu.Lock()
|
||||
outcome.successAccounts[attempt.Account.Name] = true
|
||||
mu.Unlock()
|
||||
} else {
|
||||
logger.LogEvent("抢购", "error", "失败: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
|
||||
logger.LogEvent("抢购", "info", "尝试: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
|
||||
terminalAccount, terminalProduct, reason := classifyRetryStop(result.Message)
|
||||
if terminalAccount || terminalProduct {
|
||||
mu.Lock()
|
||||
if terminalAccount {
|
||||
outcome.stoppedAccounts[attempt.Account.Name] = reason
|
||||
}
|
||||
if terminalProduct {
|
||||
outcome.stoppedProducts[attempt.Product.CommodityID] = reason
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}(attempt)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
logger.LogEvent("抢购", "info", "抢购任务结束")
|
||||
return outcome
|
||||
}
|
||||
|
||||
func seckillRetryInterval(elapsed time.Duration) time.Duration {
|
||||
switch {
|
||||
case elapsed < 8*time.Second:
|
||||
return 180 * time.Millisecond
|
||||
case elapsed < 25*time.Second:
|
||||
return 350 * time.Millisecond
|
||||
default:
|
||||
return 900 * time.Millisecond
|
||||
}
|
||||
}
|
||||
|
||||
func jitterDuration(base time.Duration) time.Duration {
|
||||
if base <= 0 {
|
||||
return 0
|
||||
}
|
||||
// 加一点抖动,避免所有机器严格同频请求。
|
||||
jitter := time.Duration(rand.Int63n(int64(base / 3)))
|
||||
return base + jitter
|
||||
}
|
||||
|
||||
func classifyRetryStop(message string) (account bool, product bool, reason string) {
|
||||
switch {
|
||||
case strings.Contains(message, "商品本次活动库存已耗完"):
|
||||
return true, true, "商品库存已耗完"
|
||||
case strings.Contains(message, "您已达本次活动兑换上限"):
|
||||
return true, false, "账号已达兑换上限"
|
||||
case strings.Contains(message, "未绑定账号"):
|
||||
return true, false, "账号未绑定角色"
|
||||
case strings.Contains(message, "需要开通精英手册"):
|
||||
return true, false, "账号不满足精英手册条件"
|
||||
default:
|
||||
return false, false, ""
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueAttemptAccounts(attempts []types.ExchangeAttempt) int {
|
||||
seen := make(map[string]struct{}, len(attempts))
|
||||
for _, attempt := range attempts {
|
||||
seen[attempt.Account.Name] = struct{}{}
|
||||
}
|
||||
return len(seen)
|
||||
}
|
||||
|
||||
func (s *Scheduler) buildAttempts(accountProducts map[string]string) []types.ExchangeAttempt {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "douyu_go",
|
||||
"outputfilename": "douyu_go",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"frontend:dir": "frontend",
|
||||
"frontend:install": "",
|
||||
"frontend:build": "",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2930
File diff suppressed because it is too large
Load Diff
+3638
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user