- 抢购内核改为"每账号独立重试循环",移除轮次屏障:旧实现在 3 个中签账号卡住 60 秒支付退避时,另外 31 个账号空转 39 秒,还出现过"某一轮只跑 1 个账号、其余整轮没被尝试" - 锁单退避改为读取配置的"重试间隔策略"(同一账号连续失败成倍放大,封顶 5 秒), 删掉硬编码 30 秒退避与"只重试一次" - 锁单失败不再提前停止:限流(活动火爆/网络繁忙)继续重试;售罄也继续降频探测 (别人锁单后支付失败或锁单过期,库存会回流),只在窗口结束时按售罄/限流/普通失败分类上报 - 支付重试按斗鱼错误码决策:1644430001/1644400002/1644400014 视为"订单已受理、结果未定" 并快重试(300ms 起),取代按文案把"订单操作进行中"误判成频控的 60 秒退避; 不再设次数上限,只受 120 秒预算与锁单过期约束,超出预算如实报"支付结果未知" - 已锁订单在 75 秒窗口结束后仍会支付完,不再受窗口约束 - 多实例:为每个实例分配独占的 WebView2 用户数据目录。Wails 默认所有实例共用 %APPDATA%\<可执行文件名>,而一个数据目录只能有一个浏览器进程,导致偶发开不出新实例、 必须全部关掉重开;实例占位因此提前到 wails.Run 之前完成 - 多实例:占位失败不再静默退回默认目录(会让两个进程共享 accounts.txt/config.json、 同秒启动还会写同一个日志文件),改为进程唯一的临时实例目录并记录降级原因 - 多实例:单个槽位锁被瞬时占用(杀软/索引器)时短暂重试,仍失败则继续试下一个槽位 而不是整个分配失败;日志文件改为独占创建,同秒启动自动追加 -2/-3 后缀 - 新增 %LOCALAPPDATA%\douyu-go\startup.log,记录每次启动与失败原因, 便于排查"窗口起不来"这类连应用日志都不会生成的问题 - 修复预热缓存日志每个账号打两遍、"账号尝试"计数误导(改为真实锁单/支付/直接兑换次数) - 修复前端把含"成功"字样的失败文案显示成抢到(改为只认"成功|"前缀) - 修复 jitterDuration 在基准小于 10ms 时 rand.Int63n(0) 会 panic 的隐患 - 设置页文案与作用域同步:重试间隔策略明确作用于锁单重试、"尝试包数"明确仅用于和平小店
108 lines
3.4 KiB
Go
108 lines
3.4 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestAcquireInstancePathsSeparatesConcurrentInstances(t *testing.T) {
|
|
baseDir := t.TempDir()
|
|
t.Setenv("DOUYU_DATA_DIR", baseDir)
|
|
t.Setenv("DOUYU_CONFIG", "")
|
|
t.Setenv("DOUYU_USERS", "")
|
|
t.Setenv("DOUYU_LOG_DIR", "")
|
|
t.Setenv("DOUYU_SUCCESS_DIR", "")
|
|
t.Setenv("devserver", "")
|
|
t.Setenv("frontenddevserverurl", "")
|
|
|
|
first, err := AcquireInstancePaths()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer first.Release()
|
|
if first.Name() != "默认实例" || first.Paths().DataDir != baseDir {
|
|
t.Fatalf("first instance = %q, %q", first.Name(), first.Paths().DataDir)
|
|
}
|
|
|
|
second, err := AcquireInstancePaths()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer second.Release()
|
|
wantSecondDir := filepath.Join(baseDir, "instances", "instance-2")
|
|
if second.Name() != "实例 2" || second.Paths().DataDir != wantSecondDir {
|
|
t.Fatalf("second instance = %q, %q", second.Name(), second.Paths().DataDir)
|
|
}
|
|
if second.Paths().LegacyConfigFile != "" || second.Paths().LegacyAccountFile != "" {
|
|
t.Fatal("附加实例不应迁移默认实例或旧工作目录数据")
|
|
}
|
|
|
|
first.Release()
|
|
reused, err := AcquireInstancePaths()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer reused.Release()
|
|
if reused.Name() != "默认实例" || reused.Paths().DataDir != baseDir {
|
|
t.Fatalf("reused instance = %q, %q", reused.Name(), reused.Paths().DataDir)
|
|
}
|
|
}
|
|
|
|
// TestAcquireInstancePathsNeverSharesDefaultDir 保证实例占位失败时也不会退回默认目录。
|
|
// 退回默认目录会让两个进程共享 accounts.txt/config.json,同秒启动还会写进同一个日志文件。
|
|
func TestAcquireInstancePathsNeverSharesDefaultDir(t *testing.T) {
|
|
baseDir := t.TempDir()
|
|
// 用一个普通文件占住 <dataDir>,让 <dataDir>/instances/.locks 无法创建。
|
|
blocked := filepath.Join(baseDir, "blocked")
|
|
if err := os.WriteFile(blocked, []byte("x"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("DOUYU_DATA_DIR", blocked)
|
|
|
|
lease, err := AcquireInstancePaths()
|
|
if lease == nil {
|
|
t.Fatal("实例占位必须始终返回可用目录")
|
|
}
|
|
defer lease.Release()
|
|
if err == nil {
|
|
t.Fatal("降级路径应当把原因返回给上层记录")
|
|
}
|
|
if lease.Name() != "临时实例" {
|
|
t.Fatalf("降级实例名 = %q, want 临时实例", lease.Name())
|
|
}
|
|
if lease.Paths().DataDir == blocked || !strings.Contains(lease.Paths().DataDir, "instance-tmp-") {
|
|
t.Fatalf("降级目录必须与本进程绑定且独立: %q", lease.Paths().DataDir)
|
|
}
|
|
}
|
|
|
|
// TestInstanceWebviewDirsAreIsolated WebView2 一个用户数据目录只能有一个浏览器进程,
|
|
// 多个实例共用会让第二个实例偶发创建环境失败(必须全部关掉才能重开)。
|
|
func TestInstanceWebviewDirsAreIsolated(t *testing.T) {
|
|
baseDir := t.TempDir()
|
|
t.Setenv("DOUYU_DATA_DIR", baseDir)
|
|
|
|
first, err := AcquireInstancePaths()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer first.Release()
|
|
second, err := AcquireInstancePaths()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer second.Release()
|
|
|
|
firstDir, secondDir := first.WebviewUserDataDir(), second.WebviewUserDataDir()
|
|
if firstDir == "" || secondDir == "" {
|
|
t.Fatalf("WebView2 目录不能为空: %q %q", firstDir, secondDir)
|
|
}
|
|
if firstDir == secondDir {
|
|
t.Fatalf("两个实例的 WebView2 目录不能相同: %q", firstDir)
|
|
}
|
|
if !strings.HasSuffix(firstDir, "webview2") {
|
|
t.Fatalf("WebView2 目录应在实例数据目录下: %q", firstDir)
|
|
}
|
|
}
|