Files
yml2213 46c06180a5 重构锁单支付重试内核并修复多实例启动,发布0.3.0
- 抢购内核改为"每账号独立重试循环",移除轮次屏障:旧实现在 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 的隐患
- 设置页文案与作用域同步:重试间隔策略明确作用于锁单重试、"尝试包数"明确仅用于和平小店
2026-10-03 19:09:19 +08:00

88 lines
2.7 KiB
Go

package main
import (
"embed"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
"github.com/wailsapp/wails/v2/pkg/options/windows"
"douyu_go/internal/app"
"douyu_go/internal/config"
)
//go:embed all:frontend/dist
var assets embed.FS
func main() {
// 实例槽位必须在 wails.Run 之前占用:WebView2 的用户数据目录要按实例区分,
// 而它只能在创建窗口之前设定。占位结果随后注入 App,避免重复占位。
lease, leaseErr := config.AcquireInstancePaths()
writeStartupTrace(fmt.Sprintf("启动: 实例=%s 数据目录=%s", lease.Name(), lease.Paths().DataDir), leaseErr)
application := app.New(lease, leaseErr)
appOptions := &options.App{
Title: "斗鱼抢购系统",
Width: 1600,
Height: 960,
MinWidth: 1100,
MinHeight: 700,
AssetServer: &assetserver.Options{
Assets: assets,
},
OnStartup: application.Startup,
OnShutdown: application.Shutdown,
Bind: []interface{}{
application,
},
}
// 每个实例一个独立的 WebView2 用户数据目录。Wails 默认用
// %APPDATA%\<可执行文件名>,所有实例共用,而 WebView2 的一个数据目录
// 只能有一个浏览器进程,第二个实例会偶发创建环境失败。
if webviewDir := lease.WebviewUserDataDir(); webviewDir != "" {
if err := os.MkdirAll(webviewDir, 0o700); err != nil {
writeStartupTrace("WebView2 用户数据目录创建失败: "+err.Error(), nil)
} else {
appOptions.Windows = &windows.Options{WebviewUserDataPath: webviewDir}
}
}
if err := wails.Run(appOptions); err != nil {
log.Printf("启动失败: %v", err)
writeStartupTrace("启动失败: "+err.Error(), nil)
os.Exit(1)
}
}
// writeStartupTrace 把每次启动/失败追加到 <用户缓存目录>/douyu-go/startup.log。
//
// GUI 应用没有控制台,而 WebView2 环境创建失败时 OnStartup 根本不会执行、
// 应用自己的日志文件也不会生成——那时只有这份文件能说明"窗口为什么没起来"。
func writeStartupTrace(line string, cause error) {
dir, err := os.UserCacheDir()
if err != nil || dir == "" {
return
}
path := filepath.Join(dir, "douyu-go", "startup.log")
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err != nil {
return
}
defer func() { _ = file.Close() }()
if cause != nil {
line = fmt.Sprintf("%s (原因: %v)", line, cause)
}
_, _ = fmt.Fprintf(file, "%s pid=%d %s\n", time.Now().Format("2006-01-02 15:04:05.000"), os.Getpid(), line)
}