feat: 新增激进抢购模式
- 添加 SeckillMode 类型(标准/激进) - 激进模式:第一轮完整校验,后续复用 CSRF 直接兑换 - 每轮开始时清空缓存,强制重新获取 CSRF - 设置面板添加模式切换 - 缓存 30 秒有效期,CSRF 错误时自动回退标准模式
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -82,6 +82,7 @@ func defaultConfig() types.Config {
|
||||
Concurrency: constants.DefaultConcurrency,
|
||||
Attempts: constants.DefaultAttempts,
|
||||
RetryStages: defaultRetryStages(),
|
||||
SeckillMode: types.SeckillModeNormal,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+136
-13
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user