package sniper import ( "time" "huya-go/internal/huya" ) // MinTriggerAdvance 是触发提前量的下限。 // // 动态提前量只补偿「请求在途时间 + 时钟误差」,正常情况下就是几十毫秒; // 下限 50ms 是为了在时钟校准异常(误差边界为 0)时仍然留一点余量。 const MinTriggerAdvance = 50 * time.Millisecond const ( // WarmupLead 是预热连接相对开抢时刻的提前量。 // // 提前 10 秒开始预热连接:为 30+ 账号留足 3~5 秒平滑建连缓冲,开抢前 5 秒全部就绪; // 既享受长连接 0 延迟首发,又能在开抢前形成静默隔离带,绝不阻塞整点发车。 WarmupLead = 10 * time.Second // ResyncLead 是「临近复校」相对开抢时刻的提前量。 // // 提前 15 秒复校时钟:在预热前完成服务端时钟校准,排定精确的预热与触发时刻。 ResyncLead = 15 * time.Second ) // TriggerPlan 描述一次定时抢购的关键时刻(全部是「本机时钟」下的时刻)。 type TriggerPlan struct { // Advance 是相对开抢时刻的提前量(含 RTT/2 与时钟误差补偿)。 Advance time.Duration // OpenAt 是开抢时刻换算到本机时钟后的时刻。 OpenAt time.Time // WarmupAt 是应当开始预热连接的时刻。 WarmupAt time.Time // ResyncAt 是应当复校服务端时钟的时刻。 ResyncAt time.Time // TriggerAt 是应当开始发请求的时刻。 TriggerAt time.Time // WindowEnd 是抢购窗口结束时刻。 WindowEnd time.Time } // PlanTrigger 计算定时抢购的预热 / 复校 / 触发 / 窗口结束时刻。 // // target 是界面上填写的时间,按「服务端时间」理解;offset 是「服务端 - 本机」的偏差。 // advance 默认取 calibration.ErrorBound + calibration.RTT/2(参考 dygou_go 的 // dynamicAdvance):固定 2500ms 的提前量既浪费又伤身——2026-10-16 实测,开抢前那 2.5 秒 // 打出去的请求全部被「活动太火爆」挡回,等于先把账号的令牌桶烧掉。 // overrideLead > 0 时沿用旧行为(兼容界面里显式填写的提前量)。 func PlanTrigger(target time.Time, calibration huya.TimeCalibration, overrideLead time.Duration, window time.Duration) TriggerPlan { advance := overrideLead if advance <= 0 { advance = calibration.ErrorBound + calibration.RTT/2 if advance < MinTriggerAdvance { advance = MinTriggerAdvance } } if window <= 0 { window = DefaultWindowSeconds * time.Second } openAt := target.Add(-calibration.Offset) return TriggerPlan{ Advance: advance, OpenAt: openAt, WarmupAt: openAt.Add(-WarmupLead), ResyncAt: openAt.Add(-ResyncLead), TriggerAt: openAt.Add(-advance), WindowEnd: openAt.Add(window), } } // WindowDuration 把「窗口秒数」配置换算成时长;<=0 时取默认值。 func WindowDuration(seconds int) time.Duration { if seconds <= 0 { return DefaultWindowSeconds * time.Second } return time.Duration(seconds) * time.Second }