- 抢购调度重构:参考 dygou_go 将 sniper 拆分为独立子模块(runner/run_control/retry/exchange/classify/config/summary/category/helpers);移除全局自适应降速与轮次上限,采用账号独立循环、统一账号级退避、单请求并发闸门与首发错峰 - 预热与建连优化:默认关闭预热(避免单账号超时连坐整机开抢),开启时转为后台非阻塞协程;WebSocket 握手超时从 10s 降至 1500ms - 双时间日志:logger 增加 SetServerOffset,校准后日志前缀自动渲染「本地时间(H虎牙估算时间)」,实现跨机器绝对毫秒对齐 - 默认配置调优:版本提升至 v0.5.0,默认线程调为 2(缓解多实例并发限流),前端自动平滑迁移 - 验证方式:go test ./... -count=1 全通,npm run build 构建正常,单测覆盖双时间与预热配置
89 lines
2.6 KiB
Go
89 lines
2.6 KiB
Go
package sniper
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func normalizeConfig(cfg Config) Config {
|
|
cfg.Category = strings.TrimSpace(cfg.Category)
|
|
if cfg.maxAttempts <= 0 && cfg.OpenAt.IsZero() {
|
|
// 非窗口模式必须有个有限轮次,否则会变成死循环。
|
|
cfg.maxAttempts = 1
|
|
}
|
|
// 窗口模式由 deadline 兜底,maxAttempts<=0 表示一直尝试到窗口结束。
|
|
if cfg.Delay <= 0 {
|
|
cfg.Delay = 150 * time.Millisecond
|
|
}
|
|
if cfg.Concurrency <= 0 {
|
|
cfg.Concurrency = 1
|
|
}
|
|
if cfg.TooFrequentBackoff <= 0 {
|
|
cfg.TooFrequentBackoff = maxDuration(cfg.Delay*2, defaultRetryBackoff)
|
|
}
|
|
if cfg.ErrorBackoff <= 0 {
|
|
cfg.ErrorBackoff = maxDuration(cfg.Delay, defaultRetryBackoff)
|
|
}
|
|
// 首发错峰给了 spread 就至少要有 1ms 步长,否则等于没启用。
|
|
if cfg.FirstWaveSpread > 0 && cfg.FirstWaveMaxStep <= 0 {
|
|
cfg.FirstWaveMaxStep = DefaultFirstWaveMaxStep
|
|
}
|
|
// 积分不足是终态:积分不会自己变多,继续打只是浪费请求,默认停手。
|
|
if !cfg.ContinueOnNotEnough {
|
|
cfg.StopOnNotEnough = true
|
|
}
|
|
// 售罄刻意不给默认值:库存可能因别人取消 / 锁单过期而回流,默认继续探测到
|
|
// 窗口结束;需要「售罄即停」时显式设置 StopOnSoldOut。
|
|
if !cfg.ListBeforeExchange {
|
|
cfg.DirectExchange = true
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
// normalizeTargets 去掉空目标与重复目标。
|
|
//
|
|
// 除了完全相同的 target,还按 pid / 名称去重:界面选品会同时提交 pid、名称,
|
|
// 以及 targetText 里的同一个 pid,不去重就会对同一奖品重复兑换(实测踩过)。
|
|
func normalizeTargets(targets []Target) []Target {
|
|
out := make([]Target, 0, len(targets))
|
|
seenKey := map[string]bool{}
|
|
seenPID := map[int32]bool{}
|
|
seenName := map[string]bool{}
|
|
for _, target := range targets {
|
|
target.Name = strings.TrimSpace(target.Name)
|
|
if target.PID <= 0 && target.Name == "" && !target.Auto {
|
|
continue
|
|
}
|
|
if target.PID > 0 && seenPID[target.PID] {
|
|
continue
|
|
}
|
|
if target.Name != "" && seenName[target.Name] {
|
|
continue
|
|
}
|
|
key := targetKey(target)
|
|
if seenKey[key] {
|
|
continue
|
|
}
|
|
seenKey[key] = true
|
|
if target.PID > 0 {
|
|
seenPID[target.PID] = true
|
|
}
|
|
if target.Name != "" {
|
|
seenName[target.Name] = true
|
|
}
|
|
out = append(out, target)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func shouldDirectExchange(target Target, cfg Config) bool {
|
|
// 设置了价格/库存门槛时必须先查列表,否则直兑会绕过过滤条件,
|
|
// 商品临时涨价或库存不足时仍会直接下单。
|
|
return cfg.DirectExchange &&
|
|
!cfg.ListBeforeExchange &&
|
|
target.PID > 0 &&
|
|
!target.Auto &&
|
|
target.MaxPrice <= 0 &&
|
|
target.MinLeft <= 0
|
|
}
|