合并 test/aggressive-mode 到 main
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
.idea
|
||||
.cscode
|
||||
*.iml
|
||||
logs/
|
||||
# logs/
|
||||
success_images/
|
||||
build/
|
||||
config.json
|
||||
|
||||
@@ -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 {
|
||||
@@ -533,6 +533,9 @@ func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, end
|
||||
|
||||
// GetVersion 返回应用版本号。
|
||||
func (a *App) GetVersion() string {
|
||||
if constants.BuildTime != "unknown" {
|
||||
return constants.Version + " (" + constants.BuildTime + ")"
|
||||
}
|
||||
return constants.Version
|
||||
}
|
||||
|
||||
@@ -563,10 +566,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}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,8 @@ dev_mode() {
|
||||
# 生产构建
|
||||
build_production() {
|
||||
local VERSION=$(get_version)
|
||||
info "开始生产构建 v${VERSION}"
|
||||
local BUILD_TIME=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
info "开始生产构建 v${VERSION} 构建时间=${BUILD_TIME}"
|
||||
|
||||
# 类型检查
|
||||
info "TypeScript 类型检查..."
|
||||
@@ -102,8 +103,8 @@ build_production() {
|
||||
npm run type-check
|
||||
cd ..
|
||||
|
||||
# Wails 构建
|
||||
wails build
|
||||
# Wails 构建,注入构建时间
|
||||
wails build -ldflags "-X 'douyu_go/internal/constants.BuildTime=${BUILD_TIME}'"
|
||||
|
||||
success "构建完成!"
|
||||
echo ""
|
||||
@@ -114,9 +115,12 @@ build_production() {
|
||||
# 构建 Windows 版本
|
||||
build_windows() {
|
||||
local VERSION=$(get_version)
|
||||
info "构建 Windows 版本 v${VERSION}"
|
||||
local BUILD_TIME=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local DATE_TAG=$(date '+%y.%-m.%-d')
|
||||
local OUTPUT_NAME="douyu_go_v${VERSION}_${DATE_TAG}.exe"
|
||||
info "构建 Windows 版本 v${VERSION} 构建时间=${BUILD_TIME}"
|
||||
|
||||
GOOS=windows GOARCH=amd64 wails build -platform windows/amd64 -o "douyu_go_v${VERSION}.exe"
|
||||
GOOS=windows GOARCH=amd64 wails build -platform windows/amd64 -o "${OUTPUT_NAME}" -ldflags "-X 'douyu_go/internal/constants.BuildTime=${BUILD_TIME}'"
|
||||
|
||||
success "Windows 构建完成!"
|
||||
echo ""
|
||||
@@ -127,9 +131,12 @@ build_windows() {
|
||||
# 构建 Linux 版本
|
||||
build_linux() {
|
||||
local VERSION=$(get_version)
|
||||
info "构建 Linux 版本 v${VERSION}"
|
||||
local BUILD_TIME=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
local DATE_TAG=$(date '+%y.%-m.%-d')
|
||||
local OUTPUT_NAME="douyu_go_v${VERSION}_${DATE_TAG}"
|
||||
info "构建 Linux 版本 v${VERSION} 构建时间=${BUILD_TIME}"
|
||||
|
||||
GOOS=linux GOARCH=amd64 wails build -platform linux/amd64 -o "douyu_go_v${VERSION}"
|
||||
GOOS=linux GOARCH=amd64 wails build -platform linux/amd64 -o "${OUTPUT_NAME}" -ldflags "-X 'douyu_go/internal/constants.BuildTime=${BUILD_TIME}'"
|
||||
|
||||
success "Linux 构建完成!"
|
||||
echo ""
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useVirtualizer } from '@tanstack/vue-virtual'
|
||||
import { computed } from 'vue'
|
||||
import { useAccounts } from '@/composables/useAccounts'
|
||||
import { useProducts } from '@/composables/useProducts'
|
||||
|
||||
@@ -15,21 +14,10 @@ const {
|
||||
|
||||
const { selectedProductName } = useProducts()
|
||||
|
||||
const parentRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const allSelected = computed(() =>
|
||||
accounts.value.length > 0 && selectedIndices.value.size === accounts.value.length
|
||||
)
|
||||
|
||||
const virtualizer = useVirtualizer(
|
||||
computed(() => ({
|
||||
count: accounts.value.length,
|
||||
getScrollElement: () => parentRef.value,
|
||||
estimateSize: () => 48,
|
||||
overscan: 5
|
||||
}))
|
||||
)
|
||||
|
||||
// 状态信息计算
|
||||
const getStatusInfo = (name: string, selected: boolean) => {
|
||||
const st = statusMap.value[name]
|
||||
@@ -85,7 +73,7 @@ const getResultMessage = (name: string) => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div ref="parentRef" class="table-wrap">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -104,24 +92,23 @@ const getResultMessage = (name: string) => {
|
||||
</tr>
|
||||
<template v-else>
|
||||
<tr
|
||||
v-for="item in virtualizer.getVirtualItems()"
|
||||
:key="String(item.key)"
|
||||
:style="{ height: `${item.size}px` }"
|
||||
v-for="(account, index) in accounts"
|
||||
:key="account.name"
|
||||
>
|
||||
<td class="select-cell">
|
||||
<label class="account-check" @click="toggleAccount(item.index)">
|
||||
<div class="check-box" :class="{ checked: selectedIndices.has(item.index) }">
|
||||
<span v-if="selectedIndices.has(item.index)">✓</span>
|
||||
<label class="account-check" @click="toggleAccount(index)">
|
||||
<div class="check-box" :class="{ checked: selectedIndices.has(index) }">
|
||||
<span v-if="selectedIndices.has(index)">✓</span>
|
||||
</div>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="account-name">{{ accounts[item.index].name }}</div>
|
||||
<div class="account-sub">{{ selectedIndices.has(item.index) ? '已加入任务' : '未选择' }}</div>
|
||||
<div class="account-name">{{ account.name }}</div>
|
||||
<div class="account-sub">{{ selectedIndices.has(index) ? '已加入任务' : '未选择' }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
v-if="accounts[item.index].cookie"
|
||||
v-if="account.cookie"
|
||||
class="status-badge status-success"
|
||||
>
|
||||
已加载
|
||||
@@ -134,18 +121,18 @@ const getResultMessage = (name: string) => {
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>{{ getScore(accounts[item.index].name) }}</td>
|
||||
<td>{{ getScore(account.name) }}</td>
|
||||
<td class="status-td">
|
||||
<span
|
||||
class="status-badge"
|
||||
:class="getStatusInfo(accounts[item.index].name, selectedIndices.has(item.index)).cls"
|
||||
:class="getStatusInfo(account.name, selectedIndices.has(index)).cls"
|
||||
>
|
||||
{{ getStatusInfo(accounts[item.index].name, selectedIndices.has(item.index)).text }}
|
||||
{{ getStatusInfo(account.name, selectedIndices.has(index)).text }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="account-sub" :title="getResultMessage(accounts[item.index].name)">
|
||||
{{ getResultMessage(accounts[item.index].name) }}
|
||||
<span class="account-sub" :title="getResultMessage(account.name)">
|
||||
{{ getResultMessage(account.name) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -241,7 +228,7 @@ tbody tr:last-child td {
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
transition: .1s;
|
||||
transition: background-color .15s ease;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ package constants
|
||||
import "time"
|
||||
|
||||
// Version 应用版本号,与 wails.json version 同步。
|
||||
const Version = "0.1.0"
|
||||
const Version = "0.1.1"
|
||||
|
||||
// BuildTime 构建时间,由编译时 ldflags 注入。
|
||||
var BuildTime = "unknown"
|
||||
|
||||
const (
|
||||
ManualID = "G4KA4Qnz4LDp7"
|
||||
|
||||
+199
-17
@@ -2,6 +2,7 @@ package seckill
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
@@ -47,6 +48,7 @@ type Scheduler struct {
|
||||
attempts int // 单号重试次数
|
||||
|
||||
retryStages []types.RetryStage // 重试间隔阶段配置
|
||||
seckillMode types.SeckillMode // 抢购模式
|
||||
|
||||
accountsMu sync.RWMutex
|
||||
accounts []types.Account
|
||||
@@ -64,10 +66,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 +80,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 +103,7 @@ func (s *Scheduler) UpdateSettings(concurrency, attempts int, retryStages []type
|
||||
if len(retryStages) > 0 {
|
||||
s.retryStages = retryStages
|
||||
}
|
||||
s.seckillMode = seckillMode
|
||||
}
|
||||
|
||||
// LoadAccounts 从账号文件加载账号。
|
||||
@@ -112,7 +120,8 @@ func (s *Scheduler) LoadAccounts() {
|
||||
logger.LogEvent("账号", "warn", "账号目录创建失败: %s: %v", dir, mkErr)
|
||||
}
|
||||
}
|
||||
hint := "# 格式: 账号名----密码----Cookie\n# 每行一个账号,粘贴后保存即可\n"
|
||||
// 使用 Windows 换行格式 \r\n
|
||||
hint := "# 格式: 账号名----密码----Cookie\r\n# 每行一个账号,粘贴后保存即可\r\n"
|
||||
if wrErr := os.WriteFile(path, []byte(hint), 0644); wrErr != nil {
|
||||
logger.LogEvent("账号", "warn", "账号文件创建失败: %s: %v", path, wrErr)
|
||||
} else {
|
||||
@@ -120,7 +129,8 @@ func (s *Scheduler) LoadAccounts() {
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
// 读取文件内容,自动转换换行格式为 Windows 格式
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
logger.LogEvent("账号", "error", "读取账号文件失败: %s: %v", path, err)
|
||||
s.accountsMu.Lock()
|
||||
@@ -128,10 +138,21 @@ func (s *Scheduler) LoadAccounts() {
|
||||
s.accountsMu.Unlock()
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// 检测并转换换行格式
|
||||
normalized, converted := s.normalizeLineEndings(path, content)
|
||||
if converted {
|
||||
// 保存转换后的内容
|
||||
if err := os.WriteFile(path, normalized, 0644); err != nil {
|
||||
logger.LogEvent("账号", "warn", "账号文件换行格式转换失败: %s: %v", path, err)
|
||||
} else {
|
||||
logger.LogEvent("账号", "info", "账号文件已自动转换为 Windows 换行格式: %s", path)
|
||||
}
|
||||
content = normalized
|
||||
}
|
||||
|
||||
var list []types.Account
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner := bufio.NewScanner(bytes.NewReader(content))
|
||||
scanner.Buffer(make([]byte, 1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
@@ -166,6 +187,33 @@ func (s *Scheduler) AccountCount() int {
|
||||
return len(s.accounts)
|
||||
}
|
||||
|
||||
// normalizeLineEndings 检测并转换换行格式为 Windows 格式(\r\n)。
|
||||
// 返回转换后的内容和是否进行了转换。
|
||||
func (s *Scheduler) normalizeLineEndings(path string, content []byte) ([]byte, bool) {
|
||||
// 检查是否已经是 Windows 格式(包含 \r\n)
|
||||
if bytes.Contains(content, []byte("\r\n")) {
|
||||
// 已经是 Windows 格式,检查是否还有单独的 \n
|
||||
// 先将所有 \r\n 替换为占位符,检查是否还有单独的 \n
|
||||
temp := bytes.ReplaceAll(content, []byte("\r\n"), []byte("__RN_PLACEHOLDER__"))
|
||||
if !bytes.Contains(temp, []byte("\n")) && !bytes.Contains(temp, []byte("\r")) {
|
||||
// 完全是 Windows 格式,不需要转换
|
||||
return content, false
|
||||
}
|
||||
// 混合格式,需要统一转换
|
||||
}
|
||||
|
||||
// 转换逻辑:先统一为 \n,再转换为 \r\n
|
||||
normalized := content
|
||||
// 1. 将 \r\n 替换为 \n
|
||||
normalized = bytes.ReplaceAll(normalized, []byte("\r\n"), []byte("\n"))
|
||||
// 2. 将单独的 \r 替换为 \n
|
||||
normalized = bytes.ReplaceAll(normalized, []byte("\r"), []byte("\n"))
|
||||
// 3. 将 \n 替换为 \r\n
|
||||
normalized = bytes.ReplaceAll(normalized, []byte("\n"), []byte("\r\n"))
|
||||
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
// AccountFilePath 返回账号文件路径。
|
||||
func (s *Scheduler) AccountFilePath() string {
|
||||
return s.paths.AccountFile
|
||||
@@ -276,8 +324,18 @@ 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()
|
||||
// 第1轮时从预热数据初始化缓存
|
||||
if round == 1 {
|
||||
s.initCacheFromWarmup(roundAttempts)
|
||||
}
|
||||
}
|
||||
|
||||
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 +443,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 +492,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 +597,47 @@ 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]
|
||||
}
|
||||
|
||||
// initCacheFromWarmup 从预热数据初始化激进模式缓存。
|
||||
func (s *Scheduler) initCacheFromWarmup(attempts []types.ExchangeAttempt) {
|
||||
for _, attempt := range attempts {
|
||||
if warmup, ok := s.warmupFor(attempt); ok {
|
||||
s.setAttemptCache(attempt.Account.Name, &types.AttemptCache{
|
||||
CSRF: warmup.CSRF,
|
||||
RoleName: warmup.RoleName,
|
||||
LastCheck: time.Now(),
|
||||
})
|
||||
logger.LogEvent("抢购", "info", "从预热初始化缓存: 账号=%s", attempt.Account.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +768,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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
2026/06/26 15:42:17 日志文件已启用: /Users/yml/codes/dygou_go/logs/douyu_20260626_154217.log
|
||||
2026/06/26 15:42:17 [配置][info] 配置加载完成: 内置默认配置
|
||||
2026/06/26 15:42:17 [字体][success] 字体加载完成: /System/Library/Fonts/Supplemental/Arial Unicode.ttf
|
||||
2026/06/26 15:42:17 [账号][success] 账号加载完成: 91 个,来源=/Users/yml/codes/dygou_go/data/user.txt
|
||||
2026/06/26 15:42:17 [时间][success] 服务端时间获取成功: 2026-06-26 15:42:18.879
|
||||
2026/06/26 15:42:17 [系统][success] 应用启动完成 | 账号: 91
|
||||
2026/06/26 15:42:18 [网络][info] 请求完成: 商品列表 GET /wgapi/ordnc/activity/peace/storehome 耗时=57ms
|
||||
2026/06/26 15:42:18 [商品][success] 商品刷新完成: 69 个,刷新时间=15:42:18.183
|
||||
2026/06/26 15:49:24 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=211ms
|
||||
2026/06/26 15:49:24 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=98ms
|
||||
2026/06/26 15:49:25 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=142ms
|
||||
2026/06/26 15:49:25 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=133ms
|
||||
2026/06/26 15:49:25 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=74ms
|
||||
2026/06/26 15:49:26 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=113ms
|
||||
2026/06/26 15:49:26 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=85ms
|
||||
2026/06/26 15:49:26 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=72ms
|
||||
2026/06/26 15:49:27 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=70ms
|
||||
2026/06/26 15:49:27 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=145ms
|
||||
2026/06/26 15:49:27 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=132ms
|
||||
2026/06/26 15:49:28 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=64ms
|
||||
2026/06/26 15:49:28 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=119ms
|
||||
2026/06/26 15:49:28 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=67ms
|
||||
2026/06/26 15:49:28 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=62ms
|
||||
2026/06/26 15:49:29 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=71ms
|
||||
2026/06/26 15:49:29 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=84ms
|
||||
2026/06/26 15:49:29 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=59ms
|
||||
2026/06/26 15:49:29 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=59ms
|
||||
2026/06/26 15:49:30 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=58ms
|
||||
2026/06/26 15:49:30 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=60ms
|
||||
2026/06/26 15:49:30 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=67ms
|
||||
2026/06/26 15:49:30 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=63ms
|
||||
2026/06/26 15:49:31 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=59ms
|
||||
2026/06/26 15:49:31 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=55ms
|
||||
2026/06/26 15:49:31 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=61ms
|
||||
2026/06/26 15:49:32 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=67ms
|
||||
2026/06/26 15:49:32 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=84ms
|
||||
2026/06/26 15:49:32 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=129ms
|
||||
2026/06/26 15:49:32 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=132ms
|
||||
2026/06/26 15:49:33 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=61ms
|
||||
2026/06/26 15:49:33 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=116ms
|
||||
2026/06/26 15:49:33 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=59ms
|
||||
2026/06/26 15:49:34 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=79ms
|
||||
2026/06/26 15:49:34 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=55ms
|
||||
2026/06/26 15:49:34 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=67ms
|
||||
2026/06/26 15:49:34 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=70ms
|
||||
2026/06/26 15:49:35 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=63ms
|
||||
2026/06/26 15:49:35 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=66ms
|
||||
2026/06/26 15:49:35 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=59ms
|
||||
2026/06/26 15:49:35 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=65ms
|
||||
2026/06/26 15:49:36 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=72ms
|
||||
2026/06/26 15:49:36 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=66ms
|
||||
2026/06/26 15:49:36 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=148ms
|
||||
2026/06/26 15:49:37 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=132ms
|
||||
2026/06/26 15:49:37 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=80ms
|
||||
2026/06/26 15:49:37 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=107ms
|
||||
2026/06/26 15:49:38 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=69ms
|
||||
2026/06/26 15:49:38 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=77ms
|
||||
2026/06/26 15:49:38 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=64ms
|
||||
2026/06/26 15:49:38 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=63ms
|
||||
2026/06/26 15:49:39 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=53ms
|
||||
2026/06/26 15:49:39 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=65ms
|
||||
2026/06/26 15:49:39 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=68ms
|
||||
2026/06/26 15:49:39 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=67ms
|
||||
2026/06/26 15:49:40 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=67ms
|
||||
2026/06/26 15:49:40 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=58ms
|
||||
2026/06/26 15:49:40 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=74ms
|
||||
2026/06/26 15:49:41 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=138ms
|
||||
2026/06/26 15:49:41 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=131ms
|
||||
2026/06/26 15:49:41 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=66ms
|
||||
2026/06/26 15:49:41 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=112ms
|
||||
2026/06/26 15:49:42 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=62ms
|
||||
2026/06/26 15:49:42 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=72ms
|
||||
2026/06/26 15:49:42 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=64ms
|
||||
2026/06/26 15:49:43 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=70ms
|
||||
2026/06/26 15:49:43 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=63ms
|
||||
2026/06/26 15:49:43 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=73ms
|
||||
2026/06/26 15:49:43 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=114ms
|
||||
2026/06/26 15:49:44 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=109ms
|
||||
2026/06/26 15:49:44 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=131ms
|
||||
2026/06/26 15:49:44 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=65ms
|
||||
2026/06/26 15:49:45 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=128ms
|
||||
2026/06/26 15:49:45 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=66ms
|
||||
2026/06/26 15:49:45 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=62ms
|
||||
2026/06/26 15:49:45 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=67ms
|
||||
2026/06/26 15:49:46 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=57ms
|
||||
2026/06/26 15:49:46 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=73ms
|
||||
2026/06/26 15:49:46 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=72ms
|
||||
2026/06/26 15:49:46 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=64ms
|
||||
2026/06/26 15:49:47 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=151ms
|
||||
2026/06/26 15:49:47 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=131ms
|
||||
2026/06/26 15:49:47 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=69ms
|
||||
2026/06/26 15:49:48 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=120ms
|
||||
2026/06/26 15:49:48 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=79ms
|
||||
2026/06/26 15:49:48 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=70ms
|
||||
2026/06/26 15:49:49 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=70ms
|
||||
2026/06/26 15:49:49 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=55ms
|
||||
2026/06/26 15:49:49 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=58ms
|
||||
2026/06/26 15:49:49 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=75ms
|
||||
2026/06/26 15:49:50 [网络][info] 请求完成: 兑换记录 GET /wgapi/ordnc/activity/peace/exchangeList 耗时=63ms
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed frontend
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "douyu_go",
|
||||
"outputfilename": "douyu_go",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"frontend:dir": "frontend",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
|
||||
Reference in New Issue
Block a user