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
+}