diff --git a/app.go b/app.go index 94068ee..616a924 100644 --- a/app.go +++ b/app.go @@ -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} } diff --git a/frontend/src/components/modal/SettingsModal.vue b/frontend/src/components/modal/SettingsModal.vue index f460c7a..65173bc 100644 --- a/frontend/src/components/modal/SettingsModal.vue +++ b/frontend/src/components/modal/SettingsModal.vue @@ -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 }) + +
+ + +
+ + +
+
+
diff --git a/frontend/src/composables/useSettings.ts b/frontend/src/composables/useSettings.ts index 8118b05..318fdbd 100644 --- a/frontend/src/composables/useSettings.ts +++ b/frontend/src/composables/useSettings.ts @@ -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({ 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') diff --git a/internal/config/config.go b/internal/config/config.go index 99456e7..12137ce 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -82,6 +82,7 @@ func defaultConfig() types.Config { Concurrency: constants.DefaultConcurrency, Attempts: constants.DefaultAttempts, RetryStages: defaultRetryStages(), + SeckillMode: types.SeckillModeNormal, } } diff --git a/internal/seckill/scheduler.go b/internal/seckill/scheduler.go index ef90ba2..a3828a4 100644 --- a/internal/seckill/scheduler.go +++ b/internal/seckill/scheduler.go @@ -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 != "" { diff --git a/internal/types/types.go b/internal/types/types.go index dda4412..e27ac45 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -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 +}