- 兑换请求增加独立的 RequestTimeout 上下文超时熔断(默认 1500ms),防止微服务卡死吃掉 5 秒 - 剥离 CategoryRoleUnbound(角色未绑定/抖动),不再计入 sessionStreak 判死阈值,杜绝误杀有效账号 - 将 CategoryRoleUnbound 与 CategoryTooFrequent 纳入 sessionAliveCategory,正常回包可重置会话计数 - 补充单测 TestRunTargetsRoleUnboundDoesNotStopAccount 与 TestExchangeTargetFastTimeout
92 lines
2.7 KiB
Go
92 lines
2.7 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 = 120 * time.Millisecond
|
|
}
|
|
if cfg.Concurrency <= 0 {
|
|
cfg.Concurrency = 30
|
|
}
|
|
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.RequestTimeout <= 0 {
|
|
cfg.RequestTimeout = DefaultRequestTimeout
|
|
}
|
|
// 积分不足是终态:积分不会自己变多,继续打只是浪费请求,默认停手。
|
|
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
|
|
}
|