@@ -373,6 +375,22 @@ const handleVerify = async () => {
flex-shrink: 0;
}
+.log-account {
+ display: inline-block;
+ max-width: 96px;
+ padding: 0 4px;
+ border: 1px solid var(--accent-border);
+ border-radius: 3px;
+ color: var(--accent);
+ font-size: 10px;
+ line-height: 1.4;
+ background: var(--accent-soft);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
.log-msg {
flex: 1;
min-width: 0;
diff --git a/frontend/src/components/modal/SettingsModal.vue b/frontend/src/components/modal/SettingsModal.vue
index 65173bc..3620ef4 100644
--- a/frontend/src/components/modal/SettingsModal.vue
+++ b/frontend/src/components/modal/SettingsModal.vue
@@ -156,14 +156,14 @@ defineExpose({ open, close })
标准模式:每次兑换都执行完整校验流程(CSRF→限制检查→预兑换→兑换)
- 激进模式:第一轮完整校验,后续轮次跳过限制检查和预兑换,直接调用兑换接口
+ 激进模式:账号通过限制检查和预兑换后,后续轮次跳过限制检查和预兑换,直接调用兑换接口
@@ -222,7 +222,7 @@ defineExpose({ open, close })
💡 抢购策略说明
- 抢购窗口:75 秒,自动循环重试
- - 默认:前 8 秒高频(180ms),之后逐渐降速
+ - 默认:前 4 秒 180ms,12 秒内 450ms,之后快速降速
- 尝试包数 > 1 时,每轮使用不同的包
diff --git a/frontend/src/composables/useLogs.ts b/frontend/src/composables/useLogs.ts
index 8183fff..a217511 100644
--- a/frontend/src/composables/useLogs.ts
+++ b/frontend/src/composables/useLogs.ts
@@ -6,6 +6,7 @@ export interface LogEntry {
time: string
level: string
category: string
+ account?: string
message: string
}
@@ -14,6 +15,7 @@ export interface LogMessage {
type: 's' | 'e' | 'i' | 'w' | 'd'
message: string
category?: string
+ account?: string
source: 'local' | 'server'
}
@@ -32,6 +34,19 @@ const resultSucceeded = (message: string): boolean => {
return /成功|兑换上限|已抢到/.test(String(message || ''))
}
+const formatServerLogTime = (time: string): string => {
+ const value = String(time || '')
+ const match = value.match(/\d{2}:\d{2}:\d{2}\.\d{3}/)
+ return match ? match[0] : value
+}
+
+const cleanServerLogMessage = (message: string): string => {
+ return String(message || '')
+ .replace(/^\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d{3})?\s*/i, '')
+ .replace(/\[[^\]]+\]\[(debug|info|success|warn|error)\]\s*/i, '')
+ .replace(/^\[账号:[^\]]+\]\s*/i, '')
+}
+
// 后端结果按“账号_第N包”返回,账号表按账号名展示,这里做一次归并。
const normalizeResultStatuses = (data: Record): Record => {
const next: Record = {}
@@ -80,14 +95,15 @@ export function useLogs() {
else if (item.level === 'warn') type = 'w'
else if (item.level === 'debug') type = 'd'
- // 清理日志前缀
- const cleanMsg = String(item.message || '').replace(/\[[^\]]+\]\[(info|success|warn|error)\]\s*/i, '')
+ // 清理后端结构化前缀,时间/分类/账号由独立字段展示。
+ const cleanMsg = cleanServerLogMessage(item.message)
logs.value.push({
- time: item.time || '',
+ time: formatServerLogTime(item.time || ''),
type,
message: cleanMsg,
category: item.category,
+ account: item.account,
source: 'server'
})
})
diff --git a/frontend/src/composables/useSettings.ts b/frontend/src/composables/useSettings.ts
index 318fdbd..55b6ffd 100644
--- a/frontend/src/composables/useSettings.ts
+++ b/frontend/src/composables/useSettings.ts
@@ -20,9 +20,10 @@ export interface Settings {
const App = window.go.main.App
const defaultRetryStages: RetryStage[] = [
- { maxElapsed: 8, interval: 130 },
- { maxElapsed: 25, interval: 260 },
- { maxElapsed: 75, interval: 900 }
+ { maxElapsed: 4, interval: 180 },
+ { maxElapsed: 12, interval: 450 },
+ { maxElapsed: 30, interval: 1200 },
+ { maxElapsed: 75, interval: 2500 }
]
const settings = ref({
diff --git a/internal/config/config.go b/internal/config/config.go
index 12137ce..2f30c6f 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -89,9 +89,10 @@ func defaultConfig() types.Config {
// defaultRetryStages 返回默认重试间隔阶段配置。
func defaultRetryStages() []types.RetryStage {
return []types.RetryStage{
- {MaxElapsed: 8, Interval: 130},
- {MaxElapsed: 25, Interval: 260},
- {MaxElapsed: 75, Interval: 900},
+ {MaxElapsed: 4, Interval: 180},
+ {MaxElapsed: 12, Interval: 450},
+ {MaxElapsed: 30, Interval: 1200},
+ {MaxElapsed: 75, Interval: 2500},
}
}
diff --git a/internal/logger/log_store.go b/internal/logger/log_store.go
index 3759166..b24a4c1 100644
--- a/internal/logger/log_store.go
+++ b/internal/logger/log_store.go
@@ -43,16 +43,22 @@ func (s *MemoryLogStore) Write(p []byte) (int, error) {
if strings.TrimSpace(line) == "" {
continue
}
- category, level := ParseLogMeta(line)
+ meta := ParseLogLine(line)
+ category, level := meta.Category, meta.Level
if level == "" {
level = DetectLogLevel(line)
}
+ logTime := meta.Time
+ if logTime == "" {
+ logTime = time.Now().In(constants.CST).Format("2006-01-02 15:04:05.000")
+ }
s.nextID++
s.entries = append(s.entries, types.LogEntry{
ID: s.nextID,
- Time: time.Now().In(constants.CST).Format("2006-01-02 15:04:05.000"),
+ Time: logTime,
Level: level,
Category: category,
+ Account: meta.Account,
Message: line,
})
if len(s.entries) > s.max {
diff --git a/internal/logger/logger.go b/internal/logger/logger.go
index 1495152..1662b13 100644
--- a/internal/logger/logger.go
+++ b/internal/logger/logger.go
@@ -14,6 +14,12 @@ import (
"douyu_go/internal/constants"
)
+const (
+ asyncLogBufferSize = 8192
+ logTimeLayout = "2006/01/02 15:04:05.000"
+ logEntryTimeLayout = "2006-01-02 15:04:05.000"
+)
+
// minLevel 日志最低等级,低于此等级的日志不输出。0=info, 1=warn, 2=error。
var minLevel atomic.Int32
@@ -83,8 +89,7 @@ func (w *AsyncLogWriter) run() {
_, _ = w.dst.Write(p)
}
if dropped := atomic.LoadUint64(&w.dropped); dropped > 0 {
- _, _ = fmt.Fprintf(w.dst, "%s [日志][warn] 日志队列已丢弃 %d 条,已优先保护抢购链路\n",
- time.Now().In(constants.CST).Format("2006/01/02 15:04:05.000000"), dropped)
+ _, _ = fmt.Fprintln(w.dst, formatLogLine(time.Now(), "日志", "warn", "", fmt.Sprintf("日志队列已丢弃 %d 条,已优先保护抢购链路", dropped)))
}
}
@@ -124,6 +129,9 @@ func (w *logFileWriter) Write(p []byte) (int, error) {
// InitLogging 初始化日志输出:日志文件和内存缓冲同时写入。
// 终端输出通过 logFileWriter 同步写入(Windows GUI 应用无控制台时自动跳过)。
func InitLogging(store *MemoryLogStore, logDir string) func() {
+ log.SetFlags(0)
+ log.SetPrefix("")
+
if logDir == "" {
logDir = constants.DefaultLogDir
}
@@ -132,27 +140,27 @@ func InitLogging(store *MemoryLogStore, logDir string) func() {
logDir = resolveAbsolutePath(logDir)
if err := os.MkdirAll(logDir, 0755); err != nil {
- async := NewAsyncLogWriter(store, 2048)
+ async := NewAsyncLogWriter(store, asyncLogBufferSize)
log.SetOutput(async)
- log.Printf("日志目录创建失败,仅写入内存: %v", err)
+ LogEvent("日志", "warn", "日志目录创建失败,仅写入内存: %v", err)
return func() { _ = async.Close() }
}
filePath := filepath.Join(logDir, "douyu_"+time.Now().In(constants.CST).Format("20060102_150405")+".log")
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
- async := NewAsyncLogWriter(store, 2048)
+ async := NewAsyncLogWriter(store, asyncLogBufferSize)
log.SetOutput(async)
- log.Printf("日志文件创建失败,仅写入内存: %v", err)
+ LogEvent("日志", "warn", "日志文件创建失败,仅写入内存: %v", err)
return func() { _ = async.Close() }
}
store.SetFilePath(filePath)
// 使用 logFileWriter 同时写入文件、内存和终端(终端失败时静默忽略)
multi := &logFileWriter{file: file, store: store}
- async := NewAsyncLogWriter(multi, 2048)
+ async := NewAsyncLogWriter(multi, asyncLogBufferSize)
log.SetOutput(async)
- log.Printf("日志文件已启用: %s", filePath)
+ LogEvent("日志", "success", "日志文件已启用: %s", filePath)
return func() {
_ = async.Close()
_ = file.Close()
@@ -161,6 +169,15 @@ func InitLogging(store *MemoryLogStore, logDir string) func() {
// LogEvent 输出带分类和级别的结构化文本日志。低于当前最低等级的日志会被丢弃。
func LogEvent(category, level, format string, args ...any) {
+ logStructuredEvent(category, level, "", format, args...)
+}
+
+// LogAccountEvent 输出带账号标签的结构化日志,便于前端按账号观察抢购链路。
+func LogAccountEvent(category, level, account, format string, args ...any) {
+ logStructuredEvent(category, level, account, format, args...)
+}
+
+func logStructuredEvent(category, level, account, format string, args ...any) {
if category == "" {
category = "系统"
}
@@ -170,24 +187,96 @@ func LogEvent(category, level, format string, args ...any) {
if levelPriority(level) < int(minLevel.Load()) {
return
}
- log.Printf("[%s][%s] %s", category, level, fmt.Sprintf(format, args...))
+ log.Print(formatLogLine(time.Now(), category, level, account, fmt.Sprintf(format, args...)))
+}
+
+func formatLogLine(now time.Time, category, level, account, message string) string {
+ ts := now.In(constants.CST).Format(logTimeLayout)
+ if account != "" {
+ return fmt.Sprintf("%s [%s][%s][账号:%s] %s", ts, category, level, account, message)
+ }
+ return fmt.Sprintf("%s [%s][%s] %s", ts, category, level, message)
+}
+
+// LogMeta 是从日志文本中解析出的结构化信息。
+type LogMeta struct {
+ Time string
+ Category string
+ Level string
+ Account string
+}
+
+// ParseLogLine 从标准日志行中提取毫秒时间、分类、级别和账号。
+func ParseLogLine(line string) LogMeta {
+ meta := LogMeta{
+ Time: parseLogTime(line),
+ }
+
+ idx := strings.Index(line, "[")
+ if idx < 0 {
+ meta.Account = parseAccountFromText(line)
+ return meta
+ }
+ category, rest, ok := cutBracket(line[idx:])
+ if !ok {
+ meta.Account = parseAccountFromText(line)
+ return meta
+ }
+ level, rest, ok := cutBracket(rest)
+ if !ok || !IsLogLevel(level) {
+ meta.Category = category
+ meta.Account = parseAccountFromText(line)
+ return meta
+ }
+
+ meta.Category = category
+ meta.Level = level
+ rest = strings.TrimLeft(rest, " \t")
+ tag, _, ok := cutBracket(rest)
+ if ok && strings.HasPrefix(tag, "账号:") {
+ meta.Account = strings.TrimSpace(strings.TrimPrefix(tag, "账号:"))
+ }
+ if meta.Account == "" {
+ meta.Account = parseAccountFromText(line)
+ }
+ return meta
+}
+
+func parseLogTime(line string) string {
+ if len(line) < len(logTimeLayout) {
+ return ""
+ }
+ raw := line[:len(logTimeLayout)]
+ t, err := time.ParseInLocation(logTimeLayout, raw, constants.CST)
+ if err != nil {
+ return ""
+ }
+ return t.Format(logEntryTimeLayout)
+}
+
+func parseAccountFromText(line string) string {
+ for _, marker := range []string{"账号=", "账号:"} {
+ idx := strings.Index(line, marker)
+ if idx < 0 {
+ continue
+ }
+ start := idx + len(marker)
+ rest := strings.TrimSpace(line[start:])
+ end := strings.IndexFunc(rest, func(r rune) bool {
+ return r == ' ' || r == '\t' || r == ']' || r == ',' || r == ',' || r == '|' || r == ';' || r == ';'
+ })
+ if end >= 0 {
+ rest = rest[:end]
+ }
+ return strings.TrimSpace(rest)
+ }
+ return ""
}
// ParseLogMeta 从标准日志行中提取类似 [网络][warn] 的分类和级别。
func ParseLogMeta(line string) (string, string) {
- idx := strings.Index(line, "[")
- if idx < 0 {
- return "", ""
- }
- category, rest, ok := cutBracket(line[idx:])
- if !ok {
- return "", ""
- }
- level, _, ok := cutBracket(rest)
- if !ok || !IsLogLevel(level) {
- return category, ""
- }
- return category, level
+ meta := ParseLogLine(line)
+ return meta.Category, meta.Level
}
func cutBracket(s string) (string, string, bool) {
diff --git a/internal/seckill/scheduler.go b/internal/seckill/scheduler.go
index fde7844..3699022 100644
--- a/internal/seckill/scheduler.go
+++ b/internal/seckill/scheduler.go
@@ -25,17 +25,21 @@ import (
const (
warmupBeforeTrigger = 2 * time.Second
warmupMaxAge = 30 * time.Second
+ qualifiedMaxAge = 30 * time.Second
timeCalibrateRounds = 3
timeCalibrateWait = 1200 * time.Millisecond
resyncBeforeTrigger = 10 * time.Second
seckillMaxDuration = 75 * time.Second
+ behaviorCooldown = 8 * time.Second
+ clickFastCooldown = 3 * time.Second
)
type roundOutcome struct {
- successAccounts map[string]bool
- stoppedAccounts map[string]string
- stoppedProducts map[string]string
- attempts int
+ successAccounts map[string]bool
+ stoppedAccounts map[string]string
+ stoppedProducts map[string]string
+ cooldownAccounts map[string]time.Time
+ attempts int
}
// Scheduler 抢购调度器。
@@ -295,12 +299,18 @@ func (s *Scheduler) RunNow(accountProducts map[string]string) {
successAccounts := make(map[string]bool)
stoppedAccounts := make(map[string]string)
stoppedProducts := make(map[string]string)
+ cooldownAccounts := make(map[string]time.Time)
totalAccounts := uniqueAttemptAccounts(allAttempts)
totalAttempts := 0
startTime := time.Now()
round := 0
- logger.LogEvent("抢购", "info", "开始抢购: %d 个账号,窗口=%v,前段高频后段降速", totalAccounts, seckillMaxDuration)
+ s.clearAttemptCache()
+ if s.seckillMode == types.SeckillModeAggressive {
+ s.initCacheFromWarmup(allAttempts)
+ }
+
+ logger.LogEvent("抢购", "info", "开始抢购: %d 个账号,窗口=%v,预检通过后直兑,前段高频后段降速", totalAccounts, seckillMaxDuration)
for {
elapsed := time.Since(startTime)
@@ -324,13 +334,21 @@ func (s *Scheduler) RunNow(accountProducts map[string]string) {
break
}
- // 激进模式每轮开始时清空缓存,强制重新获取 CSRF
- if s.seckillMode == types.SeckillModeAggressive {
- s.clearAttemptCache()
- // 第1轮时从预热数据初始化缓存
- if round == 1 {
- s.initCacheFromWarmup(roundAttempts)
+ now := time.Now()
+ roundAttempts, nextReady := filterCooldownAttempts(roundAttempts, cooldownAccounts, now)
+ if len(roundAttempts) == 0 {
+ sleepFor := nextReady.Sub(now)
+ if sleepFor <= 0 {
+ sleepFor = s.getRetryInterval(time.Since(startTime))
}
+ if sleepFor > remaining {
+ sleepFor = remaining
+ }
+ if sleepFor > 0 {
+ logger.LogEvent("抢购", "info", "第%d轮账号冷却中,等待=%v", round, utils.RoundDuration(sleepFor))
+ time.Sleep(sleepFor)
+ }
+ continue
}
logger.LogEvent("抢购", "info", "第%d轮: 待抢购 %d 个账号,本轮执行=%d,剩余时间=%v,模式=%s",
@@ -355,6 +373,13 @@ func (s *Scheduler) RunNow(accountProducts map[string]string) {
logger.LogEvent("抢购", "warn", "商品停止重试: %s,原因=%s", product, reason)
}
}
+ for account, until := range outcome.cooldownAccounts {
+ if until.After(cooldownAccounts[account]) {
+ cooldownAccounts[account] = until
+ logger.LogAccountEvent("抢购", "warn", account, "账号临时降频: 账号=%s 冷却至=%s",
+ account, until.In(constants.CST).Format("15:04:05.000"))
+ }
+ }
if len(outcome.successAccounts) > 0 {
logger.LogEvent("抢购", "success", "第%d轮成功: %d 个账号", round, len(outcome.successAccounts))
@@ -389,6 +414,22 @@ func (s *Scheduler) filterPendingAttempts(allAttempts []types.ExchangeAttempt, s
return pending
}
+func filterCooldownAttempts(attempts []types.ExchangeAttempt, cooldownAccounts map[string]time.Time, now time.Time) ([]types.ExchangeAttempt, time.Time) {
+ out := make([]types.ExchangeAttempt, 0, len(attempts))
+ var nextReady time.Time
+ for _, attempt := range attempts {
+ until := cooldownAccounts[attempt.Account.Name]
+ if until.After(now) {
+ if nextReady.IsZero() || until.Before(nextReady) {
+ nextReady = until
+ }
+ continue
+ }
+ out = append(out, attempt)
+ }
+ return out, nextReady
+}
+
// pickRoundAttempts 每轮每个账号最多执行一次,避免同账号并发过猛触发频控。
func (s *Scheduler) pickRoundAttempts(attempts []types.ExchangeAttempt, round int) []types.ExchangeAttempt {
if len(attempts) == 0 {
@@ -427,9 +468,10 @@ func (s *Scheduler) pickRoundAttempts(attempts []types.ExchangeAttempt, round in
// 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),
+ successAccounts: make(map[string]bool),
+ stoppedAccounts: make(map[string]string),
+ stoppedProducts: make(map[string]string),
+ cooldownAccounts: make(map[string]time.Time),
}
var mu sync.Mutex
@@ -459,14 +501,19 @@ func (s *Scheduler) executeRound(attempts []types.ExchangeAttempt) roundOutcome
mu.Unlock()
if result.OK {
- logger.LogEvent("抢购", "success", "成功: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
+ logger.LogAccountEvent("抢购", "success", result.Account, "成功: 账号=%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("抢购", "info", "尝试: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
+ logger.LogAccountEvent("抢购", "info", result.Account, "尝试: 账号=%s 商品=%s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
+ if cooldown := retryCooldownDuration(result.Message); cooldown > 0 {
+ mu.Lock()
+ outcome.cooldownAccounts[attempt.Account.Name] = time.Now().Add(jitterDuration(cooldown))
+ mu.Unlock()
+ }
terminalAccount, terminalProduct, isSuccess, reason := classifyRetryStop(result.Message)
if terminalAccount || terminalProduct {
mu.Lock()
@@ -479,7 +526,7 @@ func (s *Scheduler) executeRound(attempts []types.ExchangeAttempt) roundOutcome
// 兑换上限也算成功
if isSuccess {
outcome.successAccounts[attempt.Account.Name] = true
- logger.LogEvent("抢购", "success", "成功(兑换上限): %s %s 第%d包", result.Account, result.Product, result.Attempt)
+ logger.LogAccountEvent("抢购", "success", result.Account, "成功(兑换上限): 账号=%s 商品=%s 第%d包", result.Account, result.Product, result.Attempt)
go image.SaveSuccessImage(attempt.Account, attempt.Product, result.RoleName, s.font, s.paths.SuccessDir)
}
mu.Unlock()
@@ -524,6 +571,17 @@ func jitterDuration(base time.Duration) time.Duration {
return base + jitter
}
+func retryCooldownDuration(message string) time.Duration {
+ switch {
+ case strings.Contains(message, "用户行为异常"):
+ return behaviorCooldown
+ case strings.Contains(message, "点击太快"):
+ return clickFastCooldown
+ default:
+ return 0
+ }
+}
+
func classifyRetryStop(message string) (account bool, product bool, success bool, reason string) {
switch {
case strings.Contains(message, "商品本次活动库存已耗完"):
@@ -612,9 +670,10 @@ func (s *Scheduler) initCacheFromWarmup(attempts []types.ExchangeAttempt) {
s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{
CSRF: warmup.CSRF,
RoleName: warmup.RoleName,
+ Qualified: warmup.Qualified,
LastCheck: time.Now(),
})
- logger.LogEvent("抢购", "info", "从预热初始化缓存: 账号=%s", attempt.Account.Name)
+ logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "从预热初始化缓存: 账号=%s", attempt.Account.Name)
}
}
}
@@ -622,6 +681,10 @@ func (s *Scheduler) initCacheFromWarmup(attempts []types.ExchangeAttempt) {
func (s *Scheduler) setAttemptCache(accountName string, cache *types.AttemptCache) {
s.attemptCacheMu.Lock()
defer s.attemptCacheMu.Unlock()
+ if cache == nil {
+ delete(s.attemptCache, accountName)
+ return
+ }
s.attemptCache[accountName] = cache
}
@@ -631,13 +694,6 @@ func (s *Scheduler) clearAttemptCache() {
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
@@ -657,19 +713,19 @@ func (s *Scheduler) warmupAttempts(attempts []types.ExchangeAttempt) {
start := time.Now()
csrf, err := s.client.GenerateCSRF(attempt.Account.Cookie)
if err != nil {
- logger.LogEvent("预热", "warn", "CSRF预热失败: 账号=%s 商品=%s err=%v", attempt.Account.Name, attempt.Product.CommodityName, err)
+ logger.LogAccountEvent("预热", "warn", attempt.Account.Name, "CSRF预热失败: 账号=%s 商品=%s err=%v", attempt.Account.Name, attempt.Product.CommodityName, err)
return
}
roleName, err := s.client.RoleName(attempt.Account.Cookie)
if err != nil {
- logger.LogEvent("预热", "warn", "角色预热失败: 账号=%s err=%v", attempt.Account.Name, err)
+ logger.LogAccountEvent("预热", "warn", attempt.Account.Name, "角色预热失败: 账号=%s err=%v", attempt.Account.Name, err)
}
s.setWarmup(warmupKey(attempt), types.WarmupData{
CSRF: csrf,
RoleName: roleName,
CreatedAt: time.Now(),
})
- logger.LogEvent("预热", "success", "预热完成: 账号=%s 商品=%s 耗时=%s", attempt.Account.Name, attempt.Product.CommodityName, utils.RoundDuration(time.Since(start)))
+ logger.LogAccountEvent("预热", "success", attempt.Account.Name, "预热完成: 账号=%s 商品=%s 耗时=%s", attempt.Account.Name, attempt.Product.CommodityName, utils.RoundDuration(time.Since(start)))
}(attempt)
}
wg.Wait()
@@ -721,13 +777,13 @@ func (s *Scheduler) runAttempt(attempt types.ExchangeAttempt) types.ExchangeResu
Attempt: attempt.Index,
}
- logger.LogEvent("抢购", "info", "执行兑换链路: 账号=%s 商品=%s 第%d包", attempt.Account.Name, attempt.Product.CommodityName, attempt.Index)
+ logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "执行资格预检链路: 账号=%s 商品=%s 第%d包", attempt.Account.Name, attempt.Product.CommodityName, attempt.Index)
var roleName string
csrf := ""
if warmup, ok := s.warmupFor(attempt); ok {
csrf = warmup.CSRF
roleName = warmup.RoleName
- logger.LogEvent("抢购", "info", "使用预热CSRF: 账号=%s 商品=%s 预热距今=%s", attempt.Account.Name, attempt.Product.CommodityName, utils.RoundDuration(time.Since(warmup.CreatedAt)))
+ logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "使用预热CSRF: 账号=%s 商品=%s 预热距今=%s", attempt.Account.Name, attempt.Product.CommodityName, utils.RoundDuration(time.Since(warmup.CreatedAt)))
}
if csrf == "" {
var err error
@@ -736,7 +792,7 @@ func (s *Scheduler) runAttempt(attempt types.ExchangeAttempt) types.ExchangeResu
result.Message = "失败|生成CSRF失败: " + err.Error()
return result
}
- logger.LogEvent("抢购", "info", "CSRF已生成,本次兑换链路复用同一个 token: 账号=%s 商品=%s", attempt.Account.Name, attempt.Product.CommodityName)
+ logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "CSRF已生成,本次兑换链路复用同一个 token: 账号=%s 商品=%s", attempt.Account.Name, attempt.Product.CommodityName)
}
limit, err := s.client.BatchExchangeLimit(attempt.Account.Cookie, attempt.Product.CommodityID, &csrf)
@@ -748,15 +804,28 @@ func (s *Scheduler) runAttempt(attempt types.ExchangeAttempt) types.ExchangeResu
result.Message = "失败|当前商品限制兑换"
return result
}
- logger.LogEvent("抢购", "info", "批量限制通过: 账号=%s 商品=%s limit=%d", attempt.Account.Name, attempt.Product.CommodityName, limit)
+ logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "批量限制通过: 账号=%s 商品=%s limit=%d", attempt.Account.Name, attempt.Product.CommodityName, limit)
if err := s.client.PreExchange(attempt.Account.Cookie, attempt.Product.CommodityID, &csrf); err != nil {
result.Message = "失败|预兑换失败: " + err.Error()
return result
}
+ s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{
+ CSRF: csrf,
+ RoleName: roleName,
+ Qualified: true,
+ LastCheck: time.Now(),
+ })
+ logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "资格预检通过: 账号=%s 商品=%s,后续可直兑", attempt.Account.Name, attempt.Product.CommodityName)
exchangeInfo, err := s.client.Exchange(attempt.Account.Cookie, attempt.Product.CommodityID, &csrf)
if err != nil {
+ s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{
+ CSRF: csrf,
+ RoleName: roleName,
+ Qualified: true,
+ LastCheck: time.Now(),
+ })
result.Message = "失败|" + err.Error()
return result
}
@@ -764,7 +833,7 @@ func (s *Scheduler) runAttempt(attempt types.ExchangeAttempt) types.ExchangeResu
if roleName == "" {
roleName, err = s.client.RoleName(attempt.Account.Cookie)
if err != nil {
- logger.LogEvent("抢购", "warn", "%s 角色名查询失败: %v", attempt.Account.Name, err)
+ logger.LogAccountEvent("抢购", "warn", attempt.Account.Name, "角色名查询失败: 账号=%s err=%v", attempt.Account.Name, err)
}
}
@@ -780,6 +849,7 @@ func (s *Scheduler) runAttempt(attempt types.ExchangeAttempt) types.ExchangeResu
s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{
CSRF: csrf,
RoleName: roleName,
+ Qualified: true,
LastCheck: time.Now(),
})
@@ -794,23 +864,32 @@ func (s *Scheduler) runAttemptFast(attempt types.ExchangeAttempt) types.Exchange
Attempt: attempt.Index,
}
- // 获取缓存的 CSRF
- csrf := s.getCacheCSRF(attempt.Account.Name)
- if csrf == "" {
- // 没有缓存,回退到标准模式
- logger.LogEvent("抢购", "info", "激进模式回退标准: 账号=%s 原因=无缓存CSRF", attempt.Account.Name)
+ cache := s.getAttemptCache(attempt.Account.Name)
+ if cache == nil || cache.CSRF == "" || time.Since(cache.LastCheck) >= qualifiedMaxAge {
+ logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "激进模式执行资格预检: 账号=%s 原因=无缓存CSRF", attempt.Account.Name)
+ return s.runAttempt(attempt)
+ }
+ if !cache.Qualified {
+ logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "激进模式执行资格预检: 账号=%s 原因=未完成资格预检", attempt.Account.Name)
return s.runAttempt(attempt)
}
- logger.LogEvent("抢购", "info", "激进模式兑换: 账号=%s 商品=%s 第%d包", attempt.Account.Name, attempt.Product.CommodityName, attempt.Index)
+ csrf := cache.CSRF
+ logger.LogAccountEvent("抢购", "info", attempt.Account.Name, "激进模式直兑: 账号=%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 {
+ s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{
+ CSRF: csrf,
+ RoleName: cache.RoleName,
+ Qualified: true,
+ LastCheck: time.Now(),
+ })
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)
+ logger.LogAccountEvent("抢购", "warn", attempt.Account.Name, "激进模式回退标准: 账号=%s 原因=CSRF错误", attempt.Account.Name)
s.setAttemptCache(attempt.Account.Name, nil) // 清空缓存
return s.runAttempt(attempt)
}
@@ -819,7 +898,6 @@ func (s *Scheduler) runAttemptFast(attempt types.ExchangeAttempt) types.Exchange
}
// 获取角色名
- cache := s.getAttemptCache(attempt.Account.Name)
roleName := ""
if cache != nil {
roleName = cache.RoleName
@@ -828,13 +906,15 @@ func (s *Scheduler) runAttemptFast(attempt types.ExchangeAttempt) types.Exchange
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
+ logger.LogAccountEvent("抢购", "warn", attempt.Account.Name, "角色名查询失败: 账号=%s err=%v", attempt.Account.Name, err)
}
}
+ s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{
+ CSRF: csrf,
+ RoleName: roleName,
+ Qualified: true,
+ LastCheck: time.Now(),
+ })
result.OK = true
result.RoleName = roleName
diff --git a/internal/types/types.go b/internal/types/types.go
index e27ac45..c815fea 100644
--- a/internal/types/types.go
+++ b/internal/types/types.go
@@ -7,14 +7,14 @@ import (
// Config 应用配置。
type Config struct {
- TimeAPIURL string `json:"timeApiUrl"`
- TraceEnabled bool `json:"traceEnabled"`
- TraceMaxBody int `json:"traceMaxBody"`
- LogLevel string `json:"logLevel"` // debug / info / warn / error
- Concurrency int `json:"concurrency"` // 抢购并发数
- Attempts int `json:"attempts"` // 单号重试次数
- RetryStages []RetryStage `json:"retryStages"` // 重试间隔阶段配置
- SeckillMode SeckillMode `json:"seckillMode"` // 抢购模式:0=标准, 1=激进
+ TimeAPIURL string `json:"timeApiUrl"`
+ TraceEnabled bool `json:"traceEnabled"`
+ TraceMaxBody int `json:"traceMaxBody"`
+ LogLevel string `json:"logLevel"` // debug / info / warn / error
+ Concurrency int `json:"concurrency"` // 抢购并发数
+ Attempts int `json:"attempts"` // 单号重试次数
+ RetryStages []RetryStage `json:"retryStages"` // 重试间隔阶段配置
+ SeckillMode SeckillMode `json:"seckillMode"` // 抢购模式:0=标准, 1=激进
}
// RetryStage 重试间隔阶段配置。
@@ -106,6 +106,7 @@ type LogEntry struct {
Time string `json:"time"`
Level string `json:"level"`
Category string `json:"category"`
+ Account string `json:"account,omitempty"`
Message string `json:"message"`
}
@@ -175,6 +176,7 @@ func (c TimeCalibration) IsZero() bool {
type WarmupData struct {
CSRF string
RoleName string
+ Qualified bool
CreatedAt time.Time
}
@@ -182,5 +184,6 @@ type WarmupData struct {
type AttemptCache struct {
CSRF string
RoleName string
+ Qualified bool
LastCheck time.Time
}