优化抢购流程日志和复验性能
This commit is contained in:
@@ -43,6 +43,15 @@ type App struct {
|
||||
closeTrace func()
|
||||
}
|
||||
|
||||
const exchangeLookupConcurrency = 8
|
||||
|
||||
type exchangeLookupResult struct {
|
||||
Account types.Account
|
||||
Records []client.ExchangeRecord
|
||||
Err error
|
||||
EmptyCookie bool
|
||||
}
|
||||
|
||||
// NewApp 创建 App 实例。
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
@@ -228,14 +237,7 @@ func (a *App) VerifyExchange(accountName string) map[string]any {
|
||||
if err != nil {
|
||||
return map[string]any{"success": false, "error": "查询失败: " + err.Error()}
|
||||
}
|
||||
|
||||
// 过滤当前活动的记录
|
||||
var matched []client.ExchangeRecord
|
||||
for _, r := range records {
|
||||
if r.CommodityID != "" {
|
||||
matched = append(matched, r)
|
||||
}
|
||||
}
|
||||
matched := filterExchangeRecords(records)
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
@@ -244,43 +246,31 @@ func (a *App) VerifyExchange(accountName string) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyAllExchange 批量查询兑换记录,带间隔避免请求过快。
|
||||
// VerifyAllExchange 批量查询兑换记录,使用有限并发减少等待时间。
|
||||
func (a *App) VerifyAllExchange() map[string]any {
|
||||
accounts := a.scheduler.AccountSnapshot()
|
||||
results := make(map[string]any)
|
||||
lookups := a.lookupExchangeRecords("复验", accounts)
|
||||
|
||||
for i, account := range accounts {
|
||||
if i > 0 {
|
||||
// 每个请求间隔 200ms,避免太快
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
if account.Cookie == "" {
|
||||
results[account.Name] = map[string]any{
|
||||
for _, lookup := range lookups {
|
||||
if lookup.EmptyCookie {
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": false,
|
||||
"error": "Cookie 为空",
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
records, err := a.client.ExchangeList(account.Cookie)
|
||||
if err != nil {
|
||||
results[account.Name] = map[string]any{
|
||||
if lookup.Err != nil {
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": false,
|
||||
"error": "查询失败: " + err.Error(),
|
||||
"error": "查询失败: " + lookup.Err.Error(),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 过滤当前活动的记录
|
||||
var matched []client.ExchangeRecord
|
||||
for _, r := range records {
|
||||
if r.CommodityID != "" {
|
||||
matched = append(matched, r)
|
||||
}
|
||||
}
|
||||
|
||||
results[account.Name] = map[string]any{
|
||||
matched := filterExchangeRecords(lookup.Records)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": true,
|
||||
"records": matched,
|
||||
"count": len(matched),
|
||||
@@ -296,49 +286,38 @@ func (a *App) VerifyAndExport(commodityID string) map[string]any {
|
||||
results := make(map[string]any)
|
||||
hitAccounts := make([]string, 0)
|
||||
missAccounts := make([]string, 0)
|
||||
lookups := a.lookupExchangeRecords("复验", accounts)
|
||||
|
||||
for i, account := range accounts {
|
||||
if i > 0 {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
if account.Cookie == "" {
|
||||
missAccounts = append(missAccounts, account.Name)
|
||||
results[account.Name] = map[string]any{
|
||||
for _, lookup := range lookups {
|
||||
if lookup.EmptyCookie {
|
||||
missAccounts = append(missAccounts, lookup.Account.Name)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": false,
|
||||
"error": "Cookie 为空",
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
records, err := a.client.ExchangeList(account.Cookie)
|
||||
if err != nil {
|
||||
missAccounts = append(missAccounts, account.Name)
|
||||
results[account.Name] = map[string]any{
|
||||
if lookup.Err != nil {
|
||||
missAccounts = append(missAccounts, lookup.Account.Name)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": false,
|
||||
"error": "查询失败: " + err.Error(),
|
||||
"error": "查询失败: " + lookup.Err.Error(),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否抢到了指定商品
|
||||
hit := false
|
||||
for _, r := range records {
|
||||
if r.CommodityID == commodityID {
|
||||
hit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
hit := hasCommodityRecord(lookup.Records, commodityID, 0, 0)
|
||||
if hit {
|
||||
hitAccounts = append(hitAccounts, account.Name)
|
||||
results[account.Name] = map[string]any{
|
||||
hitAccounts = append(hitAccounts, lookup.Account.Name)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": true,
|
||||
"hit": true,
|
||||
}
|
||||
} else {
|
||||
missAccounts = append(missAccounts, account.Name)
|
||||
results[account.Name] = map[string]any{
|
||||
missAccounts = append(missAccounts, lookup.Account.Name)
|
||||
results[lookup.Account.Name] = map[string]any{
|
||||
"success": true,
|
||||
"hit": false,
|
||||
}
|
||||
@@ -346,15 +325,110 @@ func (a *App) VerifyAndExport(commodityID string) map[string]any {
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"hitCount": len(hitAccounts),
|
||||
"missCount": len(missAccounts),
|
||||
"hitAccounts": hitAccounts,
|
||||
"missAccounts": missAccounts,
|
||||
"details": results,
|
||||
"success": true,
|
||||
"hitCount": len(hitAccounts),
|
||||
"missCount": len(missAccounts),
|
||||
"hitAccounts": hitAccounts,
|
||||
"missAccounts": missAccounts,
|
||||
"details": results,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) lookupExchangeRecords(category string, accounts []types.Account) []exchangeLookupResult {
|
||||
results := make([]exchangeLookupResult, len(accounts))
|
||||
if len(accounts) == 0 {
|
||||
return results
|
||||
}
|
||||
|
||||
workerCount := exchangeLookupConcurrency
|
||||
if len(accounts) < workerCount {
|
||||
workerCount = len(accounts)
|
||||
}
|
||||
start := time.Now()
|
||||
logger.LogEvent(category, "info", "兑换记录批量查询开始: 账号=%d 并发=%d", len(accounts), workerCount)
|
||||
|
||||
jobs := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
for worker := 0; worker < workerCount; worker++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for idx := range jobs {
|
||||
account := accounts[idx]
|
||||
lookup := exchangeLookupResult{Account: account}
|
||||
if strings.TrimSpace(account.Cookie) == "" {
|
||||
lookup.EmptyCookie = true
|
||||
results[idx] = lookup
|
||||
continue
|
||||
}
|
||||
records, err := a.client.ExchangeList(account.Cookie)
|
||||
lookup.Records = records
|
||||
lookup.Err = err
|
||||
results[idx] = lookup
|
||||
}
|
||||
}()
|
||||
}
|
||||
for idx := range accounts {
|
||||
jobs <- idx
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
|
||||
errors := 0
|
||||
empty := 0
|
||||
for _, result := range results {
|
||||
if result.EmptyCookie {
|
||||
empty++
|
||||
}
|
||||
if result.Err != nil {
|
||||
errors++
|
||||
}
|
||||
}
|
||||
level := "success"
|
||||
if errors > 0 {
|
||||
level = "warn"
|
||||
}
|
||||
logger.LogEvent(category, level, "兑换记录批量查询完成: 账号=%d 空CK=%d 失败=%d 耗时=%s",
|
||||
len(accounts), empty, errors, time.Since(start).Round(time.Millisecond))
|
||||
return results
|
||||
}
|
||||
|
||||
func filterExchangeRecords(records []client.ExchangeRecord) []client.ExchangeRecord {
|
||||
matched := make([]client.ExchangeRecord, 0, len(records))
|
||||
for _, r := range records {
|
||||
if r.CommodityID != "" {
|
||||
matched = append(matched, r)
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
func hasCommodityRecord(records []client.ExchangeRecord, commodityID string, startTime, endTime int64) bool {
|
||||
if commodityID == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range records {
|
||||
if r.CommodityID != commodityID {
|
||||
continue
|
||||
}
|
||||
if startTime == 0 && endTime == 0 {
|
||||
return true
|
||||
}
|
||||
recordTime, err := strconv.ParseInt(r.Time, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if startTime > 0 && recordTime < startTime {
|
||||
continue
|
||||
}
|
||||
if endTime > 0 && recordTime > endTime {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExportAccounts 导出指定账号到文件。
|
||||
func (a *App) ExportAccounts(accountNames []string, filename string) map[string]any {
|
||||
if len(accountNames) == 0 {
|
||||
@@ -437,40 +511,18 @@ func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, end
|
||||
accounts := a.scheduler.AccountSnapshot()
|
||||
hitAccounts := make([]string, 0)
|
||||
missAccounts := make([]string, 0)
|
||||
lookups := a.lookupExchangeRecords("整理", accounts)
|
||||
|
||||
for i, account := range accounts {
|
||||
if i > 0 {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
if account.Cookie == "" {
|
||||
missAccounts = append(missAccounts, account.Name)
|
||||
for _, lookup := range lookups {
|
||||
if lookup.EmptyCookie || lookup.Err != nil {
|
||||
missAccounts = append(missAccounts, lookup.Account.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
records, err := a.client.ExchangeList(account.Cookie)
|
||||
if err != nil {
|
||||
missAccounts = append(missAccounts, account.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否在指定时间段内抢到了指定商品
|
||||
hit := false
|
||||
for _, r := range records {
|
||||
if r.CommodityID == commodityID {
|
||||
// 检查时间范围
|
||||
recordTime, _ := strconv.ParseInt(r.Time, 10, 64)
|
||||
if startTime == 0 || (recordTime >= startTime && recordTime <= endTime) {
|
||||
hit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hit {
|
||||
hitAccounts = append(hitAccounts, account.Name)
|
||||
if hasCommodityRecord(lookup.Records, commodityID, startTime, endTime) {
|
||||
hitAccounts = append(hitAccounts, lookup.Account.Name)
|
||||
} else {
|
||||
missAccounts = append(missAccounts, account.Name)
|
||||
missAccounts = append(missAccounts, lookup.Account.Name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,9 @@ const handleVerify = async () => {
|
||||
>
|
||||
<template v-if="log.source === 'server'">
|
||||
<span class="log-prefix">[后端]</span>
|
||||
<span v-if="log.time" class="log-time">[{{ log.time }}]</span>
|
||||
<span v-if="log.category" class="log-cat">{{ log.category }}</span>
|
||||
<span v-if="log.account" class="log-account">{{ log.account }}</span>
|
||||
<span class="log-msg">{{ log.message }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -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;
|
||||
|
||||
@@ -156,14 +156,14 @@ defineExpose({ open, close })
|
||||
<label class="form-label">抢购模式</label>
|
||||
<select v-model.number="formData.seckillMode" class="form-select">
|
||||
<option :value="0">标准模式(每次完整校验)</option>
|
||||
<option :value="1">激进模式(复用校验结果)</option>
|
||||
<option :value="1">激进模式(预检后直兑)</option>
|
||||
</select>
|
||||
<div class="form-hint">
|
||||
<template v-if="formData.seckillMode === 0">
|
||||
标准模式:每次兑换都执行完整校验流程(CSRF→限制检查→预兑换→兑换)
|
||||
</template>
|
||||
<template v-else>
|
||||
激进模式:第一轮完整校验,后续轮次跳过限制检查和预兑换,直接调用兑换接口
|
||||
激进模式:账号通过限制检查和预兑换后,后续轮次跳过限制检查和预兑换,直接调用兑换接口
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,7 +222,7 @@ defineExpose({ open, close })
|
||||
<div class="info-title">💡 抢购策略说明</div>
|
||||
<ul class="info-list">
|
||||
<li>抢购窗口:<strong>75 秒</strong>,自动循环重试</li>
|
||||
<li>默认:前 8 秒高频(180ms),之后逐渐降速</li>
|
||||
<li>默认:前 4 秒 180ms,12 秒内 450ms,之后快速降速</li>
|
||||
<li>尝试包数 > 1 时,每轮使用不同的包</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -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<string, string>): Record<string, AccountStatus> => {
|
||||
const next: Record<string, AccountStatus> = {}
|
||||
@@ -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'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<Settings>({
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+111
-22
@@ -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) {
|
||||
|
||||
+126
-46
@@ -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
|
||||
|
||||
+11
-8
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user