80 lines
3.0 KiB
Go
80 lines
3.0 KiB
Go
package sniper
|
||
|
||
import (
|
||
"time"
|
||
|
||
"huya-go/internal/huya"
|
||
)
|
||
|
||
// MinTriggerAdvance 是触发提前量的下限。
|
||
//
|
||
// 动态提前量只补偿「请求在途时间 + 时钟误差」,正常情况下就是几十毫秒;
|
||
// 下限 50ms 是为了在时钟校准异常(误差边界为 0)时仍然留一点余量。
|
||
const MinTriggerAdvance = 50 * time.Millisecond
|
||
|
||
const (
|
||
// WarmupLead 是预热连接相对开抢时刻的提前量。
|
||
//
|
||
// 2 秒足够建连 + 握手(实测单账号建连约 300ms),又不会让连接闲置太久:
|
||
// 旧实现一启动就预热、然后干等几十分钟,连接是否还活着只能靠运气。
|
||
WarmupLead = 2 * time.Second
|
||
// ResyncLead 是「临近复校」相对开抢时刻的提前量。
|
||
//
|
||
// 只做一次校准是不够的:pc-1/pc-2/pc-3 三台机器的本机偏移分别是 ≈0 / -370ms / +900ms,
|
||
// 而且本机时钟在等待期间还会漂移。触发前 3 秒再校准一次,能把这部分误差压掉。
|
||
ResyncLead = 3 * 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
|
||
}
|