Files
dygou_go/seckill.go
T
2026-06-19 23:22:20 +08:00

381 lines
12 KiB
Go

package main
import (
"errors"
"fmt"
"strings"
"sync"
"time"
)
const (
warmupBeforeTrigger = 2 * time.Second
warmupMaxAge = 30 * time.Second
timeCalibrateRounds = 3
timeCalibrateWait = 1200 * time.Millisecond
resyncBeforeTrigger = 10 * time.Second
)
// runNow 立即执行一批抢购任务。
func (s *appState) runNow(accountProducts map[string]string) {
s.clearResults()
attempts := s.buildAttempts(accountProducts)
if len(attempts) == 0 {
logEvent("抢购", "warn", "没有可执行的抢购任务")
return
}
logEvent("抢购", "info", "开始抢购: %d 个请求", len(attempts))
limit := make(chan struct{}, defaultConcurrency)
var wg sync.WaitGroup
for _, attempt := range attempts {
wg.Add(1)
go func(attempt ExchangeAttempt) {
defer wg.Done()
limit <- struct{}{}
defer func() { <-limit }()
result := s.runAttempt(attempt)
key := fmt.Sprintf("%s_第%d包", result.Account, result.Attempt)
s.setResult(key, result.Message)
if result.OK {
logEvent("抢购", "success", "成功: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
go s.saveSuccessImage(attempt.Account, attempt.Product, result.RoleName)
} else {
logEvent("抢购", "error", "失败: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
}
}(attempt)
}
wg.Wait()
logEvent("抢购", "info", "抢购任务结束")
}
// buildAttempts 根据前端传入的账号到商品映射,生成具体兑换尝试。
func (s *appState) buildAttempts(accountProducts map[string]string) []ExchangeAttempt {
accounts := s.accountSnapshot()
products, _ := s.productSnapshot()
productByID := make(map[string]Product, len(products))
for _, p := range products {
productByID[p.CommodityID] = p
}
var attempts []ExchangeAttempt
for _, account := range accounts {
commodityID := accountProducts[account.Name]
if commodityID == "" {
continue
}
product := productByID[commodityID]
if product.CommodityID == "" {
product = Product{CommodityID: commodityID, CommodityName: commodityID}
}
for i := 1; i <= defaultAttempts; i++ {
attempts = append(attempts, ExchangeAttempt{
Account: account,
Product: product,
Index: i,
})
}
}
return attempts
}
func warmupKey(attempt ExchangeAttempt) string {
return fmt.Sprintf("%s|%s|%d", attempt.Account.Name, attempt.Product.CommodityID, attempt.Index)
}
func (s *appState) setWarmup(key string, data WarmupData) {
s.warmupsMu.Lock()
defer s.warmupsMu.Unlock()
if s.warmups == nil {
s.warmups = make(map[string]WarmupData)
}
s.warmups[key] = data
}
func (s *appState) warmupFor(attempt ExchangeAttempt) (WarmupData, bool) {
s.warmupsMu.RLock()
data, ok := s.warmups[warmupKey(attempt)]
s.warmupsMu.RUnlock()
if !ok || data.CreatedAt.IsZero() || time.Since(data.CreatedAt) > warmupMaxAge {
return WarmupData{}, false
}
return data, true
}
func (s *appState) clearWarmups() {
s.warmupsMu.Lock()
defer s.warmupsMu.Unlock()
s.warmups = make(map[string]WarmupData)
}
func (s *appState) warmupAttempts(attempts []ExchangeAttempt) {
if len(attempts) == 0 {
return
}
s.clearWarmups()
logEvent("预热", "info", "开始预热: %d 个请求", len(attempts))
limit := make(chan struct{}, defaultConcurrency)
var wg sync.WaitGroup
for _, attempt := range attempts {
wg.Add(1)
go func(attempt ExchangeAttempt) {
defer wg.Done()
limit <- struct{}{}
defer func() { <-limit }()
start := time.Now()
csrf, err := s.client.GenerateCSRF(attempt.Account.Cookie)
if err != nil {
logEvent("预热", "warn", "CSRF预热失败: 账号=%s 商品=%s err=%v", attempt.Account.Name, attempt.Product.CommodityName, err)
return
}
roleName, err := s.client.RoleName(attempt.Account.Cookie)
if err != nil {
logEvent("预热", "warn", "角色预热失败: 账号=%s err=%v", attempt.Account.Name, err)
}
s.setWarmup(warmupKey(attempt), WarmupData{
CSRF: csrf,
RoleName: roleName,
CreatedAt: time.Now(),
})
logEvent("预热", "success", "预热完成: 账号=%s 商品=%s 耗时=%s", attempt.Account.Name, attempt.Product.CommodityName, roundDuration(time.Since(start)))
}(attempt)
}
wg.Wait()
logEvent("预热", "info", "预热任务结束")
}
func (s *appState) calibrateDouyuTime(label string) TimeCalibration {
calibration, err := s.client.BestServerTimeCalibration(timeCalibrateRounds, timeCalibrateWait)
if err != nil {
logEvent("时间", "warn", "%s校准降级: %v", label, err)
}
logEvent("时间", "success", "%s校准完成: 斗鱼估算=%s 偏移=%dms 误差<=%dms RTT=%dms 采样=%d 来源=%s",
label,
calibration.EstimatedServerNow().In(cst).Format("2006-01-02 15:04:05.000"),
calibration.Offset.Milliseconds(),
calibration.ErrorBound.Milliseconds(),
calibration.RTT.Milliseconds(),
calibration.Samples,
calibration.Source)
return calibration
}
func (s *appState) replaceTimerLocked(timer **time.Timer, d time.Duration, fn func()) {
if *timer != nil {
(*timer).Stop()
}
*timer = time.AfterFunc(d, fn)
}
func (s *appState) stopSchedulesLocked() {
if s.schedule != nil {
s.schedule.Stop()
s.schedule = nil
}
if s.warmupSchedule != nil {
s.warmupSchedule.Stop()
s.warmupSchedule = nil
}
if s.resyncSchedule != nil {
s.resyncSchedule.Stop()
s.resyncSchedule = nil
}
}
// runAttempt 按抓包链路执行一次兑换:CSRF -> 限制 -> 预兑换 -> 正式兑换。
// 同一次抢购尝试只生成一个 CSRF,后续接口共用,减少临门请求耗时。
func (s *appState) runAttempt(attempt ExchangeAttempt) ExchangeResult {
result := ExchangeResult{
Account: attempt.Account.Name,
Product: attempt.Product.CommodityName,
Attempt: attempt.Index,
}
logEvent("抢购", "info", "执行兑换链路: 账号=%s 商品=%s 第%d包", attempt.Account.Name, attempt.Product.CommodityName, attempt.Index)
var roleName string
csrf := ""
if warmup, ok := s.warmupFor(attempt); ok {
csrf = warmup.CSRF
roleName = warmup.RoleName
logEvent("抢购", "info", "使用预热CSRF: 账号=%s 商品=%s 预热距今=%s", attempt.Account.Name, attempt.Product.CommodityName, roundDuration(time.Since(warmup.CreatedAt)))
}
if csrf == "" {
var err error
csrf, err = s.client.GenerateCSRF(attempt.Account.Cookie)
if err != nil {
result.Message = "失败|生成CSRF失败: " + err.Error()
return result
}
logEvent("抢购", "info", "CSRF已生成,本次兑换链路复用同一个 token: 账号=%s 商品=%s", attempt.Account.Name, attempt.Product.CommodityName)
}
limit, err := s.client.BatchExchangeLimit(attempt.Account.Cookie, attempt.Product.CommodityID, csrf)
if err != nil {
result.Message = "失败|批量限制检查失败: " + err.Error()
return result
}
if limit <= 0 {
result.Message = "失败|当前商品限制兑换"
return result
}
logEvent("抢购", "info", "批量限制通过: 账号=%s 商品=%s limit=%d", attempt.Account.Name, attempt.Product.CommodityName, limit)
if err := s.client.PreExchange(attempt.Account.Cookie, attempt.Product.CommodityID, csrf); err != nil {
result.Message = "失败|预兑换失败: " + err.Error()
return result
}
exchangeInfo, err := s.client.Exchange(attempt.Account.Cookie, attempt.Product.CommodityID, csrf)
if err != nil {
result.Message = "失败|" + err.Error()
return result
}
if roleName == "" {
roleName, err = s.client.RoleName(attempt.Account.Cookie)
if err != nil {
logEvent("抢购", "warn", "%s 角色名查询失败: %v", attempt.Account.Name, err)
}
}
result.OK = true
result.RoleName = roleName
if roleName != "" {
result.Message = "成功|" + roleName + "|" + exchangeInfo
} else {
result.Message = "成功|" + exchangeInfo
}
return result
}
// scheduleRun 注册一次定时抢购。
func (s *appState) scheduleRun(req SeckillRequest) (map[string]any, error) {
if len(req.AccountProducts) == 0 {
return nil, errors.New("请选择账号和商品")
}
if strings.TrimSpace(req.TargetTime) == "" {
return nil, errors.New("请设置目标时间")
}
targetLocal, err := time.ParseInLocation("2006-01-02 15:04:05", req.TargetTime, cst)
if err != nil {
return nil, fmt.Errorf("时间格式错误: %w", err)
}
targetTime := targetLocal.UTC()
calibration := s.calibrateDouyuTime("定时")
serverTime := calibration.EstimatedServerNow()
advance := time.Duration(req.Advance) * time.Millisecond
triggerTime := targetTime.Add(-advance)
remain := triggerTime.Sub(serverTime)
if remain <= 0 {
return nil, errors.New("目标时间已过")
}
ap := cloneStringMap(req.AccountProducts)
attempts := s.buildAttempts(ap)
s.clearWarmups()
s.scheduleMu.Lock()
s.stopSchedulesLocked()
s.scheduleMu.Unlock()
s.scheduleTimers(ap, attempts, targetTime, triggerTime, calibration, false)
resyncRemain := triggerTime.Sub(serverTime) - resyncBeforeTrigger
if resyncRemain < 0 {
resyncRemain = 0
}
s.scheduleMu.Lock()
s.replaceTimerLocked(&s.resyncSchedule, resyncRemain, func() {
newCalibration := s.calibrateDouyuTime("临近")
s.scheduleTimers(ap, attempts, targetTime, triggerTime, newCalibration, true)
})
s.scheduleMu.Unlock()
warmupRemain := remain - warmupBeforeTrigger
if warmupRemain < 0 {
warmupRemain = 0
}
logEvent("定时", "success", "定时已设置: 目标=%s 触发=%s 提前=%dms 斗鱼估算=%s 本地偏移=%dms 误差<=%dms RTT=%dms 预热=%s 复校=%s",
targetTime.In(cst).Format("2006-01-02 15:04:05.000"),
triggerTime.In(cst).Format("2006-01-02 15:04:05.000"),
req.Advance,
serverTime.In(cst).Format("2006-01-02 15:04:05.000"),
calibration.Offset.Milliseconds(),
calibration.ErrorBound.Milliseconds(),
calibration.RTT.Milliseconds(),
time.Now().Add(warmupRemain).In(cst).Format("15:04:05.000"),
time.Now().Add(resyncRemain).In(cst).Format("15:04:05.000"),
)
return map[string]any{
"success": true,
"targetTime": targetTime.In(cst).Format("2006-01-02 15:04:05"),
"triggerTime": triggerTime.In(cst).Format("2006-01-02 15:04:05.000"),
"advance": req.Advance,
"remainSecond": int(remain.Seconds()),
"serverTime": serverTime.In(cst).Format("2006-01-02 15:04:05.000"),
"offsetMs": calibration.Offset.Milliseconds(),
"errorBoundMs": calibration.ErrorBound.Milliseconds(),
"rttMs": calibration.RTT.Milliseconds(),
}, nil
}
func (s *appState) scheduleTimers(accountProducts map[string]string, attempts []ExchangeAttempt, targetTime, triggerTime time.Time, calibration TimeCalibration, resync bool) {
serverTime := calibration.EstimatedServerNow()
remain := triggerTime.Sub(serverTime)
if remain < 0 {
remain = 0
}
warmupRemain := remain - warmupBeforeTrigger
if warmupRemain < 0 {
warmupRemain = 0
}
s.scheduleMu.Lock()
defer s.scheduleMu.Unlock()
s.replaceTimerLocked(&s.warmupSchedule, warmupRemain, func() {
logEvent("预热", "success", "预热触发: 本地=%s", time.Now().In(cst).Format("15:04:05.000"))
s.warmupAttempts(attempts)
})
s.replaceTimerLocked(&s.schedule, remain, func() {
now := time.Now()
estimatedServer := now.Add(calibration.Offset)
logEvent("定时", "success", "定时触发: 本地=%s 斗鱼估算=%s 偏移=%dms 误差<=%dms",
now.In(cst).Format("15:04:05.000"),
estimatedServer.In(cst).Format("15:04:05.000"),
calibration.Offset.Milliseconds(),
calibration.ErrorBound.Milliseconds())
s.runNow(accountProducts)
})
if resync {
logEvent("定时", "success", "临近复校已应用: 目标=%s 触发=%s 斗鱼估算=%s 本地偏移=%dms 误差<=%dms RTT=%dms 预热=%s",
targetTime.In(cst).Format("2006-01-02 15:04:05.000"),
triggerTime.In(cst).Format("2006-01-02 15:04:05.000"),
serverTime.In(cst).Format("2006-01-02 15:04:05.000"),
calibration.Offset.Milliseconds(),
calibration.ErrorBound.Milliseconds(),
calibration.RTT.Milliseconds(),
time.Now().Add(warmupRemain).In(cst).Format("15:04:05.000"))
}
}
// cancelSchedule 停止尚未触发的定时任务。
func (s *appState) cancelSchedule() bool {
s.scheduleMu.Lock()
defer s.scheduleMu.Unlock()
stopped := false
if s.schedule != nil {
stopped = s.schedule.Stop()
}
s.stopSchedulesLocked()
s.clearWarmups()
return stopped
}