第一版
This commit is contained in:
113
services/auth.go
Normal file
113
services/auth.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"go-taobao/config"
|
||||
"go-taobao/libs"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
)
|
||||
|
||||
// Auth 认证服务,管理登录和 Cookie
|
||||
type Auth struct {
|
||||
browser *libs.BrowserInfo
|
||||
cfg *config.RushConfig
|
||||
}
|
||||
|
||||
// NewAuth 创建认证服务
|
||||
func NewAuth(cfg *config.RushConfig) *Auth {
|
||||
return &Auth{cfg: cfg}
|
||||
}
|
||||
|
||||
// Login 打开登录页面,等待用户扫码,保存 Cookie
|
||||
func (a *Auth) Login() error {
|
||||
var mgr libs.Browser
|
||||
a.browser = mgr.GetBrowser(a.cfg.AccountID, &libs.Fingerprint{})
|
||||
|
||||
page := a.browser.Page
|
||||
page.MustNavigate(config.LoginURL).MustWaitLoad()
|
||||
page.MustWaitNavigation()
|
||||
page.MustWaitLoad()
|
||||
page.MustWaitIdle()
|
||||
page.MustActivate()
|
||||
|
||||
log.Println("[登录] 请扫码登录...")
|
||||
|
||||
// 轮询等待登录成功(URL 变化表示登录完成)
|
||||
for {
|
||||
uri := page.MustEval("() => window.location.href").String()
|
||||
log.Printf("[登录] 当前 URL: %s", uri)
|
||||
if uri != config.LoginURL {
|
||||
log.Println("[登录] 登录成功!")
|
||||
break
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
|
||||
// 导航到购物车页面获取完整 Cookie
|
||||
page.MustNavigate(config.CartURL).MustWaitLoad()
|
||||
page.MustWaitNavigation()
|
||||
page.MustWaitLoad()
|
||||
page.MustWaitIdle()
|
||||
|
||||
// 提取并保存 Cookie
|
||||
cookies := a.extractCookies(page, config.CartURL)
|
||||
if err := a.SaveCookies(cookies); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[登录] Cookie 已保存到 %s", a.cfg.CookieFile)
|
||||
a.browser.Browser.MustClose()
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadCookies 从文件加载 Cookie
|
||||
func (a *Auth) LoadCookies() ([]*proto.NetworkCookieParam, error) {
|
||||
data, err := os.ReadFile(a.cfg.CookieFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cookies []*proto.NetworkCookieParam
|
||||
if err := json.Unmarshal(data, &cookies); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[认证] 已加载 %d 条 Cookie", len(cookies))
|
||||
return cookies, nil
|
||||
}
|
||||
|
||||
// SaveCookies 保存 Cookie 到文件
|
||||
func (a *Auth) SaveCookies(cookies []*proto.NetworkCookieParam) error {
|
||||
data, err := json.MarshalIndent(cookies, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(a.cfg.CookieFile, data, 0644)
|
||||
}
|
||||
|
||||
// InjectCookies 将 Cookie 注入浏览器
|
||||
func (a *Auth) InjectCookies(browser *rod.Browser, cookies []*proto.NetworkCookieParam) error {
|
||||
return browser.SetCookies(cookies)
|
||||
}
|
||||
|
||||
// extractCookies 从页面提取 Cookie
|
||||
func (a *Auth) extractCookies(page *rod.Page, url string) []*proto.NetworkCookieParam {
|
||||
cookieArr, _ := page.Cookies([]string{url})
|
||||
var cookies []*proto.NetworkCookieParam
|
||||
for _, cookie := range cookieArr {
|
||||
cookies = append(cookies, &proto.NetworkCookieParam{
|
||||
Name: cookie.Name,
|
||||
Value: cookie.Value,
|
||||
Domain: cookie.Domain,
|
||||
Path: cookie.Path,
|
||||
Expires: cookie.Expires,
|
||||
HTTPOnly: cookie.HTTPOnly,
|
||||
Secure: cookie.Secure,
|
||||
})
|
||||
}
|
||||
return cookies
|
||||
}
|
||||
132
services/cart.go
Normal file
132
services/cart.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
)
|
||||
|
||||
// Cart 购物车操作服务
|
||||
type Cart struct {
|
||||
page *rod.Page
|
||||
}
|
||||
|
||||
// NewCart 创建购物车服务
|
||||
func NewCart(page *rod.Page) *Cart {
|
||||
return &Cart{page: page}
|
||||
}
|
||||
|
||||
// Open 打开购物车页面并等待完全加载
|
||||
func (c *Cart) Open() error {
|
||||
log.Println("[购物车] 正在打开购物车页面...")
|
||||
c.page.MustNavigate("https://cart.taobao.com/cart.htm").MustWaitLoad()
|
||||
c.page.MustWaitIdle()
|
||||
log.Println("[购物车] 页面加载完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh 刷新购物车页面
|
||||
func (c *Cart) Refresh() error {
|
||||
log.Println("[购物车] 刷新购物车...")
|
||||
c.page.MustReload().MustWaitLoad()
|
||||
c.page.MustWaitIdle()
|
||||
log.Println("[购物车] 刷新完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectAll 全选购物车商品
|
||||
func (c *Cart) SelectAll() error {
|
||||
log.Println("[购物车] 尝试全选商品...")
|
||||
|
||||
// 淘宝购物车全选按钮,尝试多种选择器兼容
|
||||
selectors := []string{
|
||||
// 常见的全选 checkbox 选择器
|
||||
`#cart-operation-fixed > div.cartOperationLeft--JnN5kBCf > label > span.ant-checkbox.ant-checkbox-checked > input[type="checkbox"]`,
|
||||
}
|
||||
|
||||
for _, sel := range selectors {
|
||||
el, err := c.page.Timeout(3 * time.Second).Element(sel)
|
||||
if err == nil && el != nil {
|
||||
el.MustClick()
|
||||
log.Printf("[购物车] 已点击全选按钮: %s", sel)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底方案:通过 JS 全选
|
||||
log.Println("[购物车] CSS 选择器未匹配,尝试 JS 全选...")
|
||||
c.page.MustEval(`() => {
|
||||
// 查找所有未选中的 checkbox 并点击
|
||||
const checkboxes = document.querySelectorAll('input[type="checkbox"]:not(:checked)');
|
||||
checkboxes.forEach(cb => cb.click());
|
||||
}`)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClickCheckout 点击"结算"按钮
|
||||
func (c *Cart) ClickCheckout() error {
|
||||
log.Println("[购物车] 查找结算按钮...")
|
||||
|
||||
// 按稳定性排序: class 选择器 > 容器内查找 > 文本匹配
|
||||
selectors := []string{
|
||||
"div.trade-cart-btn-submit div.btn--QDjHtErD", // 精确 class 路径
|
||||
"div.btnWrap--hJoXF9T5 div.btn--QDjHtErD", // 备用 wrapper class
|
||||
"#settlementContainer_1 div.btn--QDjHtErD", // 容器 ID + class
|
||||
"div[class*='trade-cart-btn-submit'] div[class*='btn--']", // 模糊匹配
|
||||
}
|
||||
|
||||
for _, sel := range selectors {
|
||||
el, err := c.page.Timeout(3 * time.Second).Element(sel)
|
||||
if err == nil && el != nil {
|
||||
el.MustClick()
|
||||
log.Printf("[购物车] 已点击结算按钮: %s", sel)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 兆底: 通过文本内容查找结算按钮
|
||||
log.Println("[购物车] 尝试通过文本内容查找结算按钮...")
|
||||
el, err := c.page.Timeout(3*time.Second).ElementR("div", "结算")
|
||||
if err == nil && el != nil {
|
||||
el.MustClick()
|
||||
log.Println("[购物车] 已通过文本匹配点击结算按钮")
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("未找到结算按钮")
|
||||
}
|
||||
|
||||
// HoverCheckout 将鼠标悬停在结算按钮上(预热,减少点击延迟)
|
||||
func (c *Cart) HoverCheckout() {
|
||||
log.Println("[购物车] 预热:悬停结算按钮...")
|
||||
|
||||
selectors := []string{
|
||||
"div.trade-cart-btn-submit div.btn--QDjHtErD",
|
||||
"div.btnWrap--hJoXF9T5 div.btn--QDjHtErD",
|
||||
"#settlementContainer_1 div.btn--QDjHtErD",
|
||||
}
|
||||
|
||||
for _, sel := range selectors {
|
||||
el, err := c.page.Timeout(2 * time.Second).Element(sel)
|
||||
if err == nil && el != nil {
|
||||
el.MustHover()
|
||||
log.Printf("[购物车] 已悬停结算按钮: %s", sel)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Println("[购物车] 预热:未找到结算按钮")
|
||||
}
|
||||
|
||||
// DisableAnimations 注入 JS 禁用页面动画,加速交互
|
||||
func (c *Cart) DisableAnimations() {
|
||||
c.page.MustEval(`() => {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '*, *::before, *::after { transition: none !important; animation: none !important; }';
|
||||
document.head.appendChild(style);
|
||||
}`)
|
||||
log.Println("[购物车] 已禁用页面动画")
|
||||
}
|
||||
164
services/order.go
Normal file
164
services/order.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
)
|
||||
|
||||
// OrderResult 下单结果
|
||||
type OrderResult struct {
|
||||
Success bool
|
||||
Message string
|
||||
}
|
||||
|
||||
// Order 订单提交服务
|
||||
type Order struct {
|
||||
page *rod.Page
|
||||
}
|
||||
|
||||
// NewOrder 创建订单服务
|
||||
func NewOrder(page *rod.Page) *Order {
|
||||
return &Order{page: page}
|
||||
}
|
||||
|
||||
// 提交订单按钮选择器列表
|
||||
var submitSelectors = []string{
|
||||
"#submitOrder > div > div.btnBox--p9CumEtE > div.btn--QDjHtErD", // 淘宝新版提交按钮
|
||||
}
|
||||
|
||||
// WaitForConfirmPage 等待订单确认页加载
|
||||
func (o *Order) WaitForConfirmPage(timeout time.Duration) error {
|
||||
log.Println("[订单] 等待订单确认页加载...")
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
url := o.page.MustEval("() => window.location.href").String()
|
||||
if o.isConfirmPage(url) {
|
||||
log.Printf("[订单] 已进入订单确认页: %s", url)
|
||||
o.page.MustWaitLoad()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
return nil
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
return fmt.Errorf("等待订单确认页超时")
|
||||
}
|
||||
|
||||
// Refresh 刷新当前订单确认页
|
||||
func (o *Order) Refresh() {
|
||||
log.Println("[订单] 刷新订单确认页...")
|
||||
o.page.MustReload().MustWaitLoad()
|
||||
o.page.MustWaitIdle()
|
||||
log.Println("[订单] 刷新完成")
|
||||
}
|
||||
|
||||
// CanSubmit 检测当前订单页是否可以提交(按钮存在且未被禁用)
|
||||
func (o *Order) CanSubmit() bool {
|
||||
for _, sel := range submitSelectors {
|
||||
el, err := o.page.Timeout(1 * time.Second).Element(sel)
|
||||
if err != nil || el == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查按钮是否被禁用(class 含 disabled / 属性 disabled 等)
|
||||
className, _ := el.Attribute("class")
|
||||
if className != nil && strings.Contains(*className, "disabled") {
|
||||
log.Printf("[订单] 按钮存在但被禁用: %s", sel)
|
||||
return false
|
||||
}
|
||||
|
||||
disabledAttr, _ := el.Attribute("disabled")
|
||||
if disabledAttr != nil {
|
||||
log.Printf("[订单] 按钮存在但 disabled 属性: %s", sel)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Printf("[订单] ✅ 按钮可点击: %s", sel)
|
||||
return true
|
||||
}
|
||||
|
||||
// 还可以检查页面上是否有"该商品暂时无法购买"之类的提示
|
||||
pageText := o.page.MustEval(`() => document.body.innerText`).String()
|
||||
if strings.Contains(pageText, "无法购买") ||
|
||||
strings.Contains(pageText, "暂不可售") ||
|
||||
strings.Contains(pageText, "已下架") ||
|
||||
strings.Contains(pageText, "未开始") {
|
||||
log.Println("[订单] 页面提示商品暂时无法购买")
|
||||
return false
|
||||
}
|
||||
|
||||
log.Println("[订单] 未找到提交按钮")
|
||||
return false
|
||||
}
|
||||
|
||||
// RefreshUntilCanSubmit 循环刷新订单页,直到可以提交,返回时表示可以立即点击提交
|
||||
func (o *Order) RefreshUntilCanSubmit(interval time.Duration, maxAttempts int) bool {
|
||||
for i := 1; i <= maxAttempts; i++ {
|
||||
log.Printf("[订单] 第 %d/%d 次检测是否可购买...", i, maxAttempts)
|
||||
|
||||
if o.CanSubmit() {
|
||||
log.Println("[订单] ✅ 商品可购买!")
|
||||
return true
|
||||
}
|
||||
|
||||
if i < maxAttempts {
|
||||
time.Sleep(interval)
|
||||
o.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[订单] 已达最大检测次数,商品仍不可购买")
|
||||
return false
|
||||
}
|
||||
|
||||
// Submit 点击"提交订单"按钮
|
||||
func (o *Order) Submit() *OrderResult {
|
||||
log.Println("[订单] 点击提交订单...")
|
||||
|
||||
for _, sel := range submitSelectors {
|
||||
el, err := o.page.Timeout(2 * time.Second).Element(sel)
|
||||
if err == nil && el != nil {
|
||||
el.MustClick()
|
||||
log.Printf("[订单] 已点击提交按钮: %s", sel)
|
||||
time.Sleep(1 * time.Second)
|
||||
return o.checkResult()
|
||||
}
|
||||
}
|
||||
|
||||
return &OrderResult{
|
||||
Success: false,
|
||||
Message: "未找到提交订单按钮",
|
||||
}
|
||||
}
|
||||
|
||||
// checkResult 检查下单结果
|
||||
func (o *Order) checkResult() *OrderResult {
|
||||
time.Sleep(2 * time.Second)
|
||||
url := o.page.MustEval("() => window.location.href").String()
|
||||
log.Printf("[订单] 提交后 URL: %s", url)
|
||||
|
||||
if strings.Contains(url, "cashier") ||
|
||||
strings.Contains(url, "pay") ||
|
||||
strings.Contains(url, "alipay") {
|
||||
return &OrderResult{
|
||||
Success: true,
|
||||
Message: "抢购成功!请尽快完成付款",
|
||||
}
|
||||
}
|
||||
|
||||
return &OrderResult{
|
||||
Success: false,
|
||||
Message: fmt.Sprintf("提交后仍在: %s", url),
|
||||
}
|
||||
}
|
||||
|
||||
// isConfirmPage 判断 URL 是否为订单确认页
|
||||
func (o *Order) isConfirmPage(url string) bool {
|
||||
return strings.Contains(url, "buy.taobao.com") ||
|
||||
strings.Contains(url, "buyertrade")
|
||||
}
|
||||
112
services/timer.go
Normal file
112
services/timer.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"go-taobao/config"
|
||||
"go-taobao/utils"
|
||||
)
|
||||
|
||||
// Timer 定时触发器,精确控制抢购时机
|
||||
type Timer struct {
|
||||
cfg *config.RushConfig
|
||||
timeSync *utils.TimeSync
|
||||
}
|
||||
|
||||
// NewTimer 创建定时器
|
||||
func NewTimer(cfg *config.RushConfig, ts *utils.TimeSync) *Timer {
|
||||
return &Timer{cfg: cfg, timeSync: ts}
|
||||
}
|
||||
|
||||
// WaitUntilPreload 等待到预加载时间点(目标时间 - PreloadBefore)
|
||||
// 返回后应立即开始预加载购物车
|
||||
func (t *Timer) WaitUntilPreload() {
|
||||
preloadTime := t.cfg.TargetTime.Add(-t.cfg.PreloadBefore)
|
||||
remaining := t.timeSync.UntilTarget(preloadTime)
|
||||
|
||||
if remaining <= 0 {
|
||||
log.Println("[定时器] 已过预加载时间,立即执行")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[定时器] 距离预加载还有 %v,等待中...", remaining.Round(time.Second))
|
||||
t.sleepUntil(preloadTime)
|
||||
log.Println("[定时器] 预加载时间到!")
|
||||
}
|
||||
|
||||
// WaitUntilRush 等待到抢购时间点(目标时间 - SubmitAdvance)
|
||||
// 使用 Sleep + 自旋忙等 的混合策略确保毫秒级精度
|
||||
func (t *Timer) WaitUntilRush() {
|
||||
// 实际触发时间 = 目标时间 - 提前量(补偿网络延迟)
|
||||
rushTime := t.cfg.TargetTime.Add(-t.cfg.SubmitAdvance)
|
||||
remaining := t.timeSync.UntilTarget(rushTime)
|
||||
|
||||
if remaining <= 0 {
|
||||
log.Println("[定时器] 已过抢购时间,立即执行")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[定时器] 距离抢购还有 %v (含 %v 提前量)",
|
||||
remaining.Round(time.Millisecond), t.cfg.SubmitAdvance)
|
||||
|
||||
t.sleepUntil(rushTime)
|
||||
|
||||
serverNow := t.timeSync.Now()
|
||||
diff := serverNow.Sub(t.cfg.TargetTime)
|
||||
log.Printf("[定时器] 触发! 服务器时间: %s (偏差: %v)",
|
||||
serverNow.Format("15:04:05.000"), diff)
|
||||
}
|
||||
|
||||
// sleepUntil 混合等待策略:先 Sleep 粗等,最后 3 秒自旋忙等
|
||||
func (t *Timer) sleepUntil(target time.Time) {
|
||||
// 阶段1: 距离目标 >5s 时,用 Sleep 粗等(省 CPU)
|
||||
for {
|
||||
remaining := t.timeSync.UntilTarget(target)
|
||||
if remaining <= 3*time.Second {
|
||||
break
|
||||
}
|
||||
// 每次 sleep 剩余时间的一半,逐步逼近
|
||||
sleepDur := remaining / 2
|
||||
if sleepDur > 1*time.Second {
|
||||
sleepDur = 1 * time.Second
|
||||
}
|
||||
time.Sleep(sleepDur)
|
||||
}
|
||||
|
||||
// 阶段2: 最后 3 秒,锁定 OS 线程 + 自旋忙等(最高精度)
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
log.Println("[定时器] 进入自旋忙等模式...")
|
||||
for t.timeSync.Now().Before(target) {
|
||||
// 自旋,不让出 CPU
|
||||
runtime.Gosched() // 偶尔让出一下,避免完全饿死其他 goroutine
|
||||
}
|
||||
}
|
||||
|
||||
// PrintCountdown 持续打印倒计时日志(在单独的 goroutine 中运行)
|
||||
func (t *Timer) PrintCountdown(done <-chan struct{}) {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
remaining := t.timeSync.UntilTarget(t.cfg.TargetTime)
|
||||
if remaining <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
hours := int(remaining.Hours())
|
||||
mins := int(remaining.Minutes()) % 60
|
||||
secs := int(remaining.Seconds()) % 60
|
||||
log.Printf("[倒计时] %02d:%02d:%02d (服务器时间: %s)",
|
||||
hours, mins, secs,
|
||||
t.timeSync.Now().Format("15:04:05"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user