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 }