合并远程main并保留和平小店功能
This commit is contained in:
+19
-17
@@ -1118,12 +1118,7 @@ func (c *DouyuClient) BatchExchangeLimit(cookie, commodityID string, csrf *strin
|
|||||||
if csrf != nil {
|
if csrf != nil {
|
||||||
currentCSRF = *csrf
|
currentCSRF = *csrf
|
||||||
}
|
}
|
||||||
var result struct {
|
var result types.APIResponse
|
||||||
types.APIResponse
|
|
||||||
Data struct {
|
|
||||||
Limit string `json:"limit"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
if err := c.postMultipart(c.Activity.BatchLimitURL, cookie, csrf, "批量限制", map[string]string{
|
if err := c.postMultipart(c.Activity.BatchLimitURL, cookie, csrf, "批量限制", map[string]string{
|
||||||
"manualID": c.Activity.ManualID,
|
"manualID": c.Activity.ManualID,
|
||||||
"commodityID": commodityID,
|
"commodityID": commodityID,
|
||||||
@@ -1135,9 +1130,15 @@ func (c *DouyuClient) BatchExchangeLimit(cookie, commodityID string, csrf *strin
|
|||||||
if result.Error != 0 {
|
if result.Error != 0 {
|
||||||
return 0, fmt.Errorf("批量限制接口失败: %s", result.Msg)
|
return 0, fmt.Errorf("批量限制接口失败: %s", result.Msg)
|
||||||
}
|
}
|
||||||
limit, err := strconv.Atoi(result.Data.Limit)
|
var data struct {
|
||||||
|
Limit string `json:"limit"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(result.Data, &data); err != nil {
|
||||||
|
return 0, fmt.Errorf("批量限制数据解析失败: %w", err)
|
||||||
|
}
|
||||||
|
limit, err := strconv.Atoi(data.Limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("批量限制值无效: %q", result.Data.Limit)
|
return 0, fmt.Errorf("批量限制值无效: %q", data.Limit)
|
||||||
}
|
}
|
||||||
return limit, nil
|
return limit, nil
|
||||||
}
|
}
|
||||||
@@ -1175,14 +1176,7 @@ func (c *DouyuClient) Exchange(cookie, commodityID string, csrf *string) (string
|
|||||||
if c.Activity.IsXiaodian {
|
if c.Activity.IsXiaodian {
|
||||||
return c.xiaodianExchange(cookie, types.Product{CommodityID: commodityID})
|
return c.xiaodianExchange(cookie, types.Product{CommodityID: commodityID})
|
||||||
}
|
}
|
||||||
var result struct {
|
var result types.APIResponse
|
||||||
types.APIResponse
|
|
||||||
Data struct {
|
|
||||||
ExchangeID int `json:"exchangeId"`
|
|
||||||
ExchangeNum string `json:"exchangeNum"`
|
|
||||||
CommodityTyp int `json:"commodityType"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
if c.Activity.UseCTN {
|
if c.Activity.UseCTN {
|
||||||
// 电竞手册:用 ctn(acf_ccn) 鉴权,multipart 表单(与浏览器抓包一致)
|
// 电竞手册:用 ctn(acf_ccn) 鉴权,multipart 表单(与浏览器抓包一致)
|
||||||
ctn := ""
|
ctn := ""
|
||||||
@@ -1234,7 +1228,15 @@ func (c *DouyuClient) Exchange(cookie, commodityID string, csrf *string) (string
|
|||||||
}
|
}
|
||||||
return "", fmt.Errorf("兑换失败: %s", result.Msg)
|
return "", fmt.Errorf("兑换失败: %s", result.Msg)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("exchangeId=%d,num=%s", result.Data.ExchangeID, result.Data.ExchangeNum), nil
|
var data struct {
|
||||||
|
ExchangeID int `json:"exchangeId"`
|
||||||
|
ExchangeNum string `json:"exchangeNum"`
|
||||||
|
CommodityTyp int `json:"commodityType"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(result.Data, &data); err != nil {
|
||||||
|
return "", fmt.Errorf("兑换数据解析失败: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("exchangeId=%d,num=%s", data.ExchangeID, data.ExchangeNum), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExchangeProduct 正式兑换一个商品。和平小店需要商品支付类型/价格,因此应优先走这个入口。
|
// ExchangeProduct 正式兑换一个商品。和平小店需要商品支付类型/价格,因此应优先走这个入口。
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package seckill
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@ func (s *Scheduler) ScheduleRun(req types.SeckillRequest) (types.ScheduleResult,
|
|||||||
calibration := s.calibrateDouyuTime("定时")
|
calibration := s.calibrateDouyuTime("定时")
|
||||||
serverTime := calibration.EstimatedServerNow()
|
serverTime := calibration.EstimatedServerNow()
|
||||||
|
|
||||||
triggerTime := targetTime.Add(-internalAdvance)
|
advance := dynamicAdvance(calibration)
|
||||||
|
triggerTime := targetTime.Add(-advance)
|
||||||
remain := triggerTime.Sub(serverTime)
|
remain := triggerTime.Sub(serverTime)
|
||||||
if remain <= 0 {
|
if remain <= 0 {
|
||||||
return types.ScheduleResult{}, errors.New("目标时间已过")
|
return types.ScheduleResult{}, errors.New("目标时间已过")
|
||||||
@@ -52,7 +54,8 @@ func (s *Scheduler) ScheduleRun(req types.SeckillRequest) (types.ScheduleResult,
|
|||||||
s.scheduleMu.Lock()
|
s.scheduleMu.Lock()
|
||||||
s.replaceTimerLocked(&s.resyncSchedule, resyncRemain, func() {
|
s.replaceTimerLocked(&s.resyncSchedule, resyncRemain, func() {
|
||||||
newCalibration := s.calibrateDouyuTime("临近")
|
newCalibration := s.calibrateDouyuTime("临近")
|
||||||
s.scheduleTimers(ap, targetTime, triggerTime, newCalibration, req.Attempts, req.DelayMs, true)
|
newTriggerTime := targetTime.Add(-dynamicAdvance(newCalibration))
|
||||||
|
s.scheduleTimers(ap, targetTime, newTriggerTime, newCalibration, req.Attempts, req.DelayMs, true)
|
||||||
})
|
})
|
||||||
s.scheduleMu.Unlock()
|
s.scheduleMu.Unlock()
|
||||||
|
|
||||||
@@ -64,7 +67,7 @@ func (s *Scheduler) ScheduleRun(req types.SeckillRequest) (types.ScheduleResult,
|
|||||||
logger.LogEvent("定时", "success", "定时已设置: 目标=%s 触发=%s 提前=%dms 斗鱼估算=%s 本地偏移=%dms 误差<=%dms RTT=%dms 预热=%s 复校=%s",
|
logger.LogEvent("定时", "success", "定时已设置: 目标=%s 触发=%s 提前=%dms 斗鱼估算=%s 本地偏移=%dms 误差<=%dms RTT=%dms 预热=%s 复校=%s",
|
||||||
targetTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
targetTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||||
triggerTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
triggerTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||||
internalAdvance.Milliseconds(),
|
advance.Milliseconds(),
|
||||||
serverTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
serverTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||||
calibration.Offset.Milliseconds(),
|
calibration.Offset.Milliseconds(),
|
||||||
calibration.ErrorBound.Milliseconds(),
|
calibration.ErrorBound.Milliseconds(),
|
||||||
@@ -73,7 +76,7 @@ func (s *Scheduler) ScheduleRun(req types.SeckillRequest) (types.ScheduleResult,
|
|||||||
time.Now().Add(resyncRemain).In(constants.CST).Format("15:04:05.000"),
|
time.Now().Add(resyncRemain).In(constants.CST).Format("15:04:05.000"),
|
||||||
)
|
)
|
||||||
|
|
||||||
remainSecond := int(triggerTime.Sub(serverTime).Seconds())
|
remainSecond := int(math.Ceil(triggerTime.Sub(serverTime).Seconds()))
|
||||||
if remainSecond < 0 {
|
if remainSecond < 0 {
|
||||||
remainSecond = 0
|
remainSecond = 0
|
||||||
}
|
}
|
||||||
@@ -95,26 +98,27 @@ func (s *Scheduler) scheduleTimers(accountProducts map[string]string, targetTime
|
|||||||
if remain < 0 {
|
if remain < 0 {
|
||||||
remain = 0
|
remain = 0
|
||||||
}
|
}
|
||||||
warmupRemain := remain - warmupBeforeTrigger
|
|
||||||
if warmupRemain < 0 {
|
|
||||||
warmupRemain = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
s.scheduleMu.Lock()
|
s.scheduleMu.Lock()
|
||||||
defer s.scheduleMu.Unlock()
|
defer s.scheduleMu.Unlock()
|
||||||
gen := atomic.AddInt64(&s.warmupGen, 1)
|
if !resync {
|
||||||
s.replaceTimerLocked(&s.warmupSchedule, warmupRemain, func() {
|
warmupRemain := remain - warmupBeforeTrigger
|
||||||
if atomic.LoadInt64(&s.warmupGen) != gen {
|
if warmupRemain < 0 {
|
||||||
return
|
warmupRemain = 0
|
||||||
}
|
}
|
||||||
if s.clientSnapshot().Activity.IsXiaodian {
|
gen := atomic.AddInt64(&s.warmupGen, 1)
|
||||||
return
|
s.replaceTimerLocked(&s.warmupSchedule, warmupRemain, func() {
|
||||||
}
|
if atomic.LoadInt64(&s.warmupGen) != gen {
|
||||||
logger.LogEvent("预热", "success", "预热触发: 本地=%s", time.Now().In(constants.CST).Format("15:04:05.000"))
|
return
|
||||||
settings := s.settingsSnapshot()
|
}
|
||||||
attempts := s.buildAttempts(accountProducts, settings.attempts)
|
if s.clientSnapshot().Activity.IsXiaodian {
|
||||||
s.warmupAttempts(attempts)
|
return
|
||||||
})
|
}
|
||||||
|
logger.LogEvent("预热", "success", "预热触发: 本地=%s", time.Now().In(constants.CST).Format("15:04:05.000"))
|
||||||
|
settings := s.settingsSnapshot()
|
||||||
|
attempts := s.buildAttempts(accountProducts, settings.attempts)
|
||||||
|
s.warmupAttempts(attempts)
|
||||||
|
})
|
||||||
|
}
|
||||||
s.replaceTimerLocked(&s.schedule, remain, func() {
|
s.replaceTimerLocked(&s.schedule, remain, func() {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
estimatedServer := now.Add(calibration.Offset)
|
estimatedServer := now.Add(calibration.Offset)
|
||||||
@@ -134,14 +138,13 @@ func (s *Scheduler) scheduleTimers(accountProducts map[string]string, targetTime
|
|||||||
s.RunNow(accountProducts)
|
s.RunNow(accountProducts)
|
||||||
})
|
})
|
||||||
if resync {
|
if resync {
|
||||||
logger.LogEvent("定时", "success", "临近复校已应用: 目标=%s 触发=%s 斗鱼估算=%s 本地偏移=%dms 误差<=%dms RTT=%dms 预热=%s",
|
logger.LogEvent("定时", "success", "临近复校已应用: 目标=%s 触发=%s 斗鱼估算=%s 偏移=%dms 误差<=%dms RTT=%dms",
|
||||||
targetTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
targetTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||||
triggerTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
triggerTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||||
serverTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
serverTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||||
calibration.Offset.Milliseconds(),
|
calibration.Offset.Milliseconds(),
|
||||||
calibration.ErrorBound.Milliseconds(),
|
calibration.ErrorBound.Milliseconds(),
|
||||||
calibration.RTT.Milliseconds(),
|
calibration.RTT.Milliseconds())
|
||||||
time.Now().Add(warmupRemain).In(constants.CST).Format("15:04:05.000"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,11 +167,19 @@ func (s *Scheduler) calibrateDouyuTime(label string) types.TimeCalibration {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogEvent("时间", "warn", "%s校准降级: %v", label, err)
|
logger.LogEvent("时间", "warn", "%s校准降级: %v", label, err)
|
||||||
}
|
}
|
||||||
logger.SetDouyuOffset(calibration.Offset)
|
if !calibration.Fallback {
|
||||||
logger.LogEvent("时间", "success", "%s校准完成: 斗鱼估算=%s 偏移=%dms 误差<=%dms RTT=%dms 采样=%d 来源=%s",
|
s.recordCalibrationOffset(calibration.Offset)
|
||||||
|
}
|
||||||
|
smoothed := s.smoothedOffset()
|
||||||
|
if smoothed == 0 && calibration.Fallback {
|
||||||
|
smoothed = calibration.Offset
|
||||||
|
}
|
||||||
|
calibration.Offset = smoothed
|
||||||
|
logger.SetDouyuOffset(smoothed)
|
||||||
|
logger.LogEvent("时间", "success", "%s校准完成: 斗鱼估算=%s 平滑偏移=%dms 误差<=%dms RTT=%dms 采样=%d 来源=%s",
|
||||||
label,
|
label,
|
||||||
calibration.EstimatedServerNow().In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
calibration.EstimatedServerNow().In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
||||||
calibration.Offset.Milliseconds(),
|
smoothed.Milliseconds(),
|
||||||
calibration.ErrorBound.Milliseconds(),
|
calibration.ErrorBound.Milliseconds(),
|
||||||
calibration.RTT.Milliseconds(),
|
calibration.RTT.Milliseconds(),
|
||||||
calibration.Samples,
|
calibration.Samples,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package seckill
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,18 +19,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
warmupBeforeTrigger = 2 * time.Second
|
warmupBeforeTrigger = 2 * time.Second
|
||||||
warmupMaxAge = 30 * time.Second
|
warmupMaxAge = 30 * time.Second
|
||||||
qualifiedMaxAge = 30 * time.Second
|
qualifiedMaxAge = 30 * time.Second
|
||||||
timeCalibrateRounds = 3
|
timeCalibrateRounds = 3
|
||||||
timeCalibrateWait = 1200 * time.Millisecond
|
timeCalibrateWait = 1200 * time.Millisecond
|
||||||
resyncBeforeTrigger = 10 * time.Second
|
resyncBeforeTrigger = 3 * time.Second
|
||||||
seckillMaxDuration = 75 * time.Second
|
calibrationHistorySize = 3
|
||||||
behaviorCooldown = 5 * time.Second
|
seckillMaxDuration = 75 * time.Second
|
||||||
clickFastCooldown = 3 * time.Second
|
behaviorCooldown = 5 * time.Second
|
||||||
internalAdvance = 50 * time.Millisecond
|
clickFastCooldown = 3 * time.Second
|
||||||
maxCooldownCount = 5
|
minInternalAdvance = 50 * time.Millisecond
|
||||||
xiaodianDefaultDelay = 500 * time.Millisecond
|
maxCooldownCount = 5
|
||||||
|
xiaodianDefaultDelay = 500 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
type roundOutcome struct {
|
type roundOutcome struct {
|
||||||
@@ -94,6 +96,9 @@ type Scheduler struct {
|
|||||||
|
|
||||||
productsMu sync.RWMutex
|
productsMu sync.RWMutex
|
||||||
products map[string]types.Product
|
products map[string]types.Product
|
||||||
|
|
||||||
|
calibrationMu sync.Mutex
|
||||||
|
calibrationOffsets []time.Duration // 最近N次校准偏移,用于中位数平滑
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetStateChangeCallback 设置运行状态变化回调。
|
// SetStateChangeCallback 设置运行状态变化回调。
|
||||||
@@ -131,13 +136,14 @@ func New(cli *client.DouyuClient, paths types.AppPaths, font *truetype.Font, con
|
|||||||
1,
|
1,
|
||||||
256,
|
256,
|
||||||
),
|
),
|
||||||
concurrency: concurrency,
|
concurrency: concurrency,
|
||||||
attempts: attempts,
|
attempts: attempts,
|
||||||
retryStages: cloneRetryStages(retryStages),
|
retryStages: cloneRetryStages(retryStages),
|
||||||
seckillMode: normalizeSeckillMode(seckillMode),
|
seckillMode: normalizeSeckillMode(seckillMode),
|
||||||
results: make(map[string]string),
|
results: make(map[string]string),
|
||||||
attemptCache: make(map[string]*types.AttemptCache),
|
attemptCache: make(map[string]*types.AttemptCache),
|
||||||
products: make(map[string]types.Product),
|
products: make(map[string]types.Product),
|
||||||
|
calibrationOffsets: make([]time.Duration, 0, calibrationHistorySize),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -939,6 +945,37 @@ func classifyXiaodianRetryStop(message string) (account bool, product bool, reas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordCalibrationOffset 记录校准偏移,保留最近N次用于中位数平滑。
|
||||||
|
func (s *Scheduler) recordCalibrationOffset(offset time.Duration) {
|
||||||
|
s.calibrationMu.Lock()
|
||||||
|
defer s.calibrationMu.Unlock()
|
||||||
|
s.calibrationOffsets = append(s.calibrationOffsets, offset)
|
||||||
|
if len(s.calibrationOffsets) > calibrationHistorySize {
|
||||||
|
s.calibrationOffsets = s.calibrationOffsets[len(s.calibrationOffsets)-calibrationHistorySize:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// smoothedOffset 返回最近校准偏移的中位数,过滤跳变异常。
|
||||||
|
func (s *Scheduler) smoothedOffset() time.Duration {
|
||||||
|
s.calibrationMu.Lock()
|
||||||
|
defer s.calibrationMu.Unlock()
|
||||||
|
if len(s.calibrationOffsets) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
sorted := append([]time.Duration(nil), s.calibrationOffsets...)
|
||||||
|
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
|
||||||
|
return sorted[len(sorted)/2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// dynamicAdvance 根据校准误差边界和 RTT 计算动态提前量,最少 50ms。
|
||||||
|
func dynamicAdvance(calibration types.TimeCalibration) time.Duration {
|
||||||
|
advance := calibration.ErrorBound + calibration.RTT/2
|
||||||
|
if advance < minInternalAdvance {
|
||||||
|
advance = minInternalAdvance
|
||||||
|
}
|
||||||
|
return advance
|
||||||
|
}
|
||||||
|
|
||||||
func uniqueAttemptAccounts(attempts []types.ExchangeAttempt) int {
|
func uniqueAttemptAccounts(attempts []types.ExchangeAttempt) int {
|
||||||
seen := make(map[string]struct{}, len(attempts))
|
seen := make(map[string]struct{}, len(attempts))
|
||||||
for _, attempt := range attempts {
|
for _, attempt := range attempts {
|
||||||
|
|||||||
Reference in New Issue
Block a user