抢购逻辑优化

This commit is contained in:
yml2213
2026-06-26 01:14:23 +08:00
parent e6116fd8f0
commit 4ae587d76b
6 changed files with 8814 additions and 8 deletions
+1 -1
View File
@@ -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"
+211 -6
View File
@@ -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
View File
@@ -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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff