Files
dygou_go/seckill.go
T
2026-06-19 12:10:55 +08:00

202 lines
6.0 KiB
Go

package main
import (
"errors"
"fmt"
"strings"
"sync"
"time"
)
// 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
}
// runAttempt 按抓包链路执行一次兑换: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)
limitCSRF, err := s.client.GenerateCSRF(attempt.Account.Cookie)
if err != nil {
result.Message = "失败|生成CSRF失败: " + err.Error()
return result
}
limit, err := s.client.BatchExchangeLimit(attempt.Account.Cookie, attempt.Product.CommodityID, limitCSRF)
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)
preCSRF, err := s.client.GenerateCSRF(attempt.Account.Cookie)
if err != nil {
result.Message = "失败|生成预兑换CSRF失败: " + err.Error()
return result
}
if err := s.client.PreExchange(attempt.Account.Cookie, attempt.Product.CommodityID, preCSRF); err != nil {
result.Message = "失败|预兑换失败: " + err.Error()
return result
}
exchangeCSRF, err := s.client.GenerateCSRF(attempt.Account.Cookie)
if err != nil {
result.Message = "失败|生成兑换CSRF失败: " + err.Error()
return result
}
exchangeInfo, err := s.client.Exchange(attempt.Account.Cookie, attempt.Product.CommodityID, exchangeCSRF)
if err != nil {
result.Message = "失败|" + err.Error()
return result
}
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()
serverTime, err := s.client.ServerTime()
if err != nil {
logEvent("定时", "warn", "定时使用降级时间: %v", err)
}
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)
s.scheduleMu.Lock()
if s.schedule != nil {
s.schedule.Stop()
}
s.schedule = time.AfterFunc(remain, func() {
logEvent("定时", "success", "定时触发: 本地=%s", time.Now().In(cst).Format("15:04:05.000"))
s.runNow(ap)
})
s.scheduleMu.Unlock()
logEvent("定时", "success", "定时已设置: 目标=%s 触发=%s 提前=%dms",
targetTime.In(cst).Format("2006-01-02 15:04:05.000"),
triggerTime.In(cst).Format("2006-01-02 15:04:05.000"),
req.Advance,
)
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"),
}, nil
}
// cancelSchedule 停止尚未触发的定时任务。
func (s *appState) cancelSchedule() bool {
s.scheduleMu.Lock()
defer s.scheduleMu.Unlock()
if s.schedule == nil {
return false
}
stopped := s.schedule.Stop()
s.schedule = nil
return stopped
}