合并远程main并保留和平小店功能

This commit is contained in:
yml2213
2026-08-09 10:52:56 +08:00
3 changed files with 113 additions and 63 deletions
+19 -17
View File
@@ -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 正式兑换一个商品。和平小店需要商品支付类型/价格,因此应优先走这个入口。
+38 -27
View File
@@ -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,
+56 -19
View File
@@ -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 {