feat: 新增激进抢购模式

- 添加 SeckillMode 类型(标准/激进)
- 激进模式:第一轮完整校验,后续复用 CSRF 直接兑换
- 每轮开始时清空缓存,强制重新获取 CSRF
- 设置面板添加模式切换
- 缓存 30 秒有效期,CSRF 错误时自动回退标准模式
This commit is contained in:
yml2213
2026-06-26 13:58:54 +08:00
parent 385a6d882c
commit 59bd34b004
6 changed files with 181 additions and 20 deletions
+4 -4
View File
@@ -70,7 +70,7 @@ func (a *App) startup(ctx context.Context) {
a.client = client.New(a.cfg.TimeAPIURL, a.trace)
a.font = imgpkg.LoadFont(a.paths.FontFiles)
a.scheduler = seckill.New(a.client, a.paths, a.font, a.cfg.Concurrency, a.cfg.Attempts, a.cfg.RetryStages)
a.scheduler = seckill.New(a.client, a.paths, a.font, a.cfg.Concurrency, a.cfg.Attempts, a.cfg.RetryStages, a.cfg.SeckillMode)
a.scheduler.LoadAccounts()
if err := os.MkdirAll(a.paths.SuccessDir, 0755); err != nil {
@@ -563,10 +563,10 @@ func (a *App) SaveSettings(cfg types.Config) map[string]any {
// 运行时生效
a.cfg = cfg
logger.SetMinLevel(cfg.LogLevel)
a.scheduler.UpdateSettings(cfg.Concurrency, cfg.Attempts, cfg.RetryStages)
a.scheduler.UpdateSettings(cfg.Concurrency, cfg.Attempts, cfg.RetryStages, cfg.SeckillMode)
logger.LogEvent("配置", "success", "配置已保存并生效: 日志等级=%s 并发=%d 重试=%d 重试阶段=%d trace=%v",
cfg.LogLevel, cfg.Concurrency, cfg.Attempts, len(cfg.RetryStages), cfg.TraceEnabled)
logger.LogEvent("配置", "success", "配置已保存并生效: 日志等级=%s 并发=%d 重试=%d 重试阶段=%d 模式=%d trace=%v",
cfg.LogLevel, cfg.Concurrency, cfg.Attempts, len(cfg.RetryStages), cfg.SeckillMode, cfg.TraceEnabled)
return map[string]any{"success": true}
}
@@ -12,7 +12,8 @@ const formData = ref({
logLevel: 'info',
concurrency: 80,
attempts: 1,
retryStages: [] as RetryStage[]
retryStages: [] as RetryStage[],
seckillMode: 0
})
const open = async () => {
@@ -150,6 +151,23 @@ defineExpose({ open, close })
</div>
</div>
<!-- 抢购模式 -->
<div class="form-group">
<label class="form-label">抢购模式</label>
<select v-model.number="formData.seckillMode" class="form-select">
<option :value="0">标准模式(每次完整校验)</option>
<option :value="1">激进模式(复用校验结果)</option>
</select>
<div class="form-hint">
<template v-if="formData.seckillMode === 0">
标准模式:每次兑换都执行完整校验流程(CSRF→限制检查→预兑换→兑换)
</template>
<template v-else>
激进模式:第一轮完整校验,后续轮次跳过限制检查和预兑换,直接调用兑换接口
</template>
</div>
</div>
<!-- 重试间隔配置 -->
<div class="form-group">
<label class="form-label">重试间隔策略</label>
+5 -2
View File
@@ -14,6 +14,7 @@ export interface Settings {
concurrency: number
attempts: number
retryStages: RetryStage[]
seckillMode: number // 0=标准, 1=激进
}
const App = window.go.main.App
@@ -31,7 +32,8 @@ const settings = ref<Settings>({
logLevel: 'info',
concurrency: 80,
attempts: 1,
retryStages: [...defaultRetryStages]
retryStages: [...defaultRetryStages],
seckillMode: 0
})
export function useSettings() {
@@ -49,7 +51,8 @@ export function useSettings() {
attempts: cfg.attempts || 1,
retryStages: cfg.retryStages && cfg.retryStages.length > 0
? cfg.retryStages
: [...defaultRetryStages]
: [...defaultRetryStages],
seckillMode: cfg.seckillMode || 0
}
} catch (e: any) {
showToast('读取设置失败', e.message, 'error')
+1
View File
@@ -82,6 +82,7 @@ func defaultConfig() types.Config {
Concurrency: constants.DefaultConcurrency,
Attempts: constants.DefaultAttempts,
RetryStages: defaultRetryStages(),
SeckillMode: types.SeckillModeNormal,
}
}
+136 -13
View File
@@ -47,6 +47,7 @@ type Scheduler struct {
attempts int // 单号重试次数
retryStages []types.RetryStage // 重试间隔阶段配置
seckillMode types.SeckillMode // 抢购模式
accountsMu sync.RWMutex
accounts []types.Account
@@ -64,10 +65,13 @@ type Scheduler struct {
warmupsMu sync.RWMutex
warmups map[string]types.WarmupData
attemptCacheMu sync.RWMutex
attemptCache map[string]*types.AttemptCache // 激进模式缓存
}
// New 创建抢购调度器。
func New(cli *client.DouyuClient, paths types.AppPaths, font *truetype.Font, concurrency, attempts int, retryStages []types.RetryStage) *Scheduler {
func New(cli *client.DouyuClient, paths types.AppPaths, font *truetype.Font, concurrency, attempts int, retryStages []types.RetryStage, seckillMode types.SeckillMode) *Scheduler {
if concurrency <= 0 {
concurrency = constants.DefaultConcurrency
}
@@ -75,18 +79,20 @@ func New(cli *client.DouyuClient, paths types.AppPaths, font *truetype.Font, con
attempts = constants.DefaultAttempts
}
return &Scheduler{
client: cli,
paths: paths,
font: font,
concurrency: concurrency,
attempts: attempts,
retryStages: retryStages,
results: make(map[string]string),
client: cli,
paths: paths,
font: font,
concurrency: concurrency,
attempts: attempts,
retryStages: retryStages,
seckillMode: seckillMode,
results: make(map[string]string),
attemptCache: make(map[string]*types.AttemptCache),
}
}
// UpdateSettings 运行时更新并发数、重试次数和重试间隔。
func (s *Scheduler) UpdateSettings(concurrency, attempts int, retryStages []types.RetryStage) {
// UpdateSettings 运行时更新并发数、重试次数、重试间隔和抢购模式。
func (s *Scheduler) UpdateSettings(concurrency, attempts int, retryStages []types.RetryStage, seckillMode types.SeckillMode) {
if concurrency > 0 {
s.concurrency = concurrency
}
@@ -96,6 +102,7 @@ func (s *Scheduler) UpdateSettings(concurrency, attempts int, retryStages []type
if len(retryStages) > 0 {
s.retryStages = retryStages
}
s.seckillMode = seckillMode
}
// LoadAccounts 从账号文件加载账号。
@@ -276,8 +283,14 @@ func (s *Scheduler) RunNow(accountProducts map[string]string) {
break
}
logger.LogEvent("抢购", "info", "第%d轮: 待抢购 %d 个账号,本轮执行=%d,剩余时间=%v",
round, uniqueAttemptAccounts(pendingAttempts), len(roundAttempts), utils.RoundDuration(remaining))
// 激进模式每轮开始时清空缓存,强制重新获取 CSRF
if s.seckillMode == types.SeckillModeAggressive {
s.clearAttemptCache()
}
logger.LogEvent("抢购", "info", "第%d轮: 待抢购 %d 个账号,本轮执行=%d,剩余时间=%v,模式=%s",
round, uniqueAttemptAccounts(pendingAttempts), len(roundAttempts), utils.RoundDuration(remaining),
s.getModeName())
outcome := s.executeRound(roundAttempts)
totalAttempts += outcome.attempts
@@ -385,7 +398,14 @@ func (s *Scheduler) executeRound(attempts []types.ExchangeAttempt) roundOutcome
limit <- struct{}{}
defer func() { <-limit }()
result := s.runAttempt(attempt)
// 根据模式选择执行方法
var result types.ExchangeResult
if s.seckillMode == types.SeckillModeAggressive {
result = s.runAttemptFast(attempt)
} else {
result = s.runAttempt(attempt)
}
key := fmt.Sprintf("%s_第%d包", result.Account, result.Attempt)
s.setResult(key, result.Message)
@@ -427,6 +447,14 @@ func (s *Scheduler) executeRound(attempts []types.ExchangeAttempt) roundOutcome
return outcome
}
// getModeName 返回当前抢购模式的名称。
func (s *Scheduler) getModeName() string {
if s.seckillMode == types.SeckillModeAggressive {
return "激进"
}
return "标准"
}
// getRetryInterval 根据配置的重试阶段计算当前应该使用的重试间隔。
func (s *Scheduler) getRetryInterval(elapsed time.Duration) time.Duration {
elapsedSec := int(elapsed.Seconds())
@@ -524,6 +552,33 @@ func (s *Scheduler) clearWarmups() {
s.warmups = make(map[string]types.WarmupData)
}
// ========== 激进模式缓存管理 ==========
func (s *Scheduler) getAttemptCache(accountName string) *types.AttemptCache {
s.attemptCacheMu.RLock()
defer s.attemptCacheMu.RUnlock()
return s.attemptCache[accountName]
}
func (s *Scheduler) setAttemptCache(accountName string, cache *types.AttemptCache) {
s.attemptCacheMu.Lock()
defer s.attemptCacheMu.Unlock()
s.attemptCache[accountName] = cache
}
func (s *Scheduler) clearAttemptCache() {
s.attemptCacheMu.Lock()
defer s.attemptCacheMu.Unlock()
s.attemptCache = make(map[string]*types.AttemptCache)
}
func (s *Scheduler) getCacheCSRF(accountName string) string {
if cache := s.getAttemptCache(accountName); cache != nil && time.Since(cache.LastCheck) < 30*time.Second {
return cache.CSRF
}
return ""
}
func (s *Scheduler) warmupAttempts(attempts []types.ExchangeAttempt) {
if len(attempts) == 0 {
return
@@ -654,6 +709,74 @@ func (s *Scheduler) runAttempt(attempt types.ExchangeAttempt) types.ExchangeResu
}
}
result.OK = true
result.RoleName = roleName
if roleName != "" {
result.Message = "成功|" + roleName + "|" + exchangeInfo
} else {
result.Message = "成功|" + exchangeInfo
}
// 缓存 CSRF 和角色名(激进模式使用)
s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{
CSRF: csrf,
RoleName: roleName,
LastCheck: time.Now(),
})
return result
}
// runAttemptFast 激进模式的兑换逻辑,复用缓存的 CSRF,跳过 BatchExchangeLimit 和 PreExchange。
func (s *Scheduler) runAttemptFast(attempt types.ExchangeAttempt) types.ExchangeResult {
result := types.ExchangeResult{
Account: attempt.Account.Name,
Product: attempt.Product.CommodityName,
Attempt: attempt.Index,
}
// 获取缓存的 CSRF
csrf := s.getCacheCSRF(attempt.Account.Name)
if csrf == "" {
// 没有缓存,回退到标准模式
logger.LogEvent("抢购", "info", "激进模式回退标准: 账号=%s 原因=无缓存CSRF", attempt.Account.Name)
return s.runAttempt(attempt)
}
logger.LogEvent("抢购", "info", "激进模式兑换: 账号=%s 商品=%s 第%d包", attempt.Account.Name, attempt.Product.CommodityName, attempt.Index)
// 直接调用 Exchange
exchangeInfo, err := s.client.Exchange(attempt.Account.Cookie, attempt.Product.CommodityID, &csrf)
if err != nil {
errMsg := err.Error()
// 检查是否是 CSRF 相关错误,如果是则回退到标准模式
if strings.Contains(errMsg, "csrf") || strings.Contains(errMsg, "CSRF") || strings.Contains(errMsg, "token") {
logger.LogEvent("抢购", "warn", "激进模式回退标准: 账号=%s 原因=CSRF错误", attempt.Account.Name)
s.setAttemptCache(attempt.Account.Name, nil) // 清空缓存
return s.runAttempt(attempt)
}
result.Message = "失败|" + errMsg
return result
}
// 获取角色名
cache := s.getAttemptCache(attempt.Account.Name)
roleName := ""
if cache != nil {
roleName = cache.RoleName
}
if roleName == "" {
var err error
roleName, err = s.client.RoleName(attempt.Account.Cookie)
if err != nil {
logger.LogEvent("抢购", "warn", "%s 角色名查询失败: %v", attempt.Account.Name, err)
}
// 更新缓存
if cache != nil {
cache.RoleName = roleName
}
}
result.OK = true
result.RoleName = roleName
if roleName != "" {
+16
View File
@@ -14,6 +14,7 @@ type Config struct {
Concurrency int `json:"concurrency"` // 抢购并发数
Attempts int `json:"attempts"` // 单号重试次数
RetryStages []RetryStage `json:"retryStages"` // 重试间隔阶段配置
SeckillMode SeckillMode `json:"seckillMode"` // 抢购模式:0=标准, 1=激进
}
// RetryStage 重试间隔阶段配置。
@@ -22,6 +23,14 @@ type RetryStage struct {
Interval int `json:"interval"` // 重试间隔(ms)
}
// SeckillMode 抢购模式。
type SeckillMode int
const (
SeckillModeNormal SeckillMode = iota // 标准模式:每次完整校验
SeckillModeAggressive // 激进模式:复用校验结果
)
// AppPaths 运行时路径。
type AppPaths struct {
ConfigFile string
@@ -168,3 +177,10 @@ type WarmupData struct {
RoleName string
CreatedAt time.Time
}
// AttemptCache 单个账号的兑换缓存(激进模式使用)。
type AttemptCache struct {
CSRF string
RoleName string
LastCheck time.Time
}