202 lines
6.0 KiB
Go
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
|
|
}
|