Files
stock-checker/auto_order.go
2025-12-12 03:59:18 +08:00

433 lines
13 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// auto_order.go
package main
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/playwright-community/playwright-go"
)
type OrderConfig struct {
URL string `json:"url"` // 商品页 URL
Cookies []Cookie `json:"cookies"` // 登录 Cookie
MaxPrice float64 `json:"max_price"` // 最高价格
TargetLocation string `json:"target_location"` // 目标机房(如 "LA"
StockKeyword string `json:"stock_keyword"` // 缺货关键字(如 "Out of Stock"
Timeout int `json:"timeout"` // 超时时间(秒)
}
type Cookie struct {
Name string `json:"name"`
Value string `json:"value"`
Domain string `json:"domain"`
Path string `json:"path"`
}
type OrderResult struct {
Success bool `json:"success"`
Message string `json:"message"`
OrderID string `json:"order_id"`
Price string `json:"price"`
Location string `json:"location"`
Screenshot string `json:"screenshot"`
}
func main() {
// 从命令行参数或配置文件读取
configFile := "order_config.json"
if len(os.Args) > 1 {
configFile = os.Args[1]
}
config, err := loadConfig(configFile)
if err != nil {
log.Fatal("加载配置失败:", err)
}
result := executeOrder(config)
// 输出结果
output, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(output))
// 保存结果到文件
os.WriteFile("order_result.json", output, 0644)
if !result.Success {
os.Exit(1)
}
}
func loadConfig(filename string) (*OrderConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var config OrderConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
// 设置默认值
if config.Timeout == 0 {
config.Timeout = 60
}
if config.StockKeyword == "" {
config.StockKeyword = "Out of Stock"
}
return &config, nil
}
func executeOrder(config *OrderConfig) *OrderResult {
result := &OrderResult{Success: false}
// 启动 Playwright
pw, err := playwright.Run()
if err != nil {
result.Message = fmt.Sprintf("启动 Playwright 失败: %v", err)
return result
}
defer pw.Stop()
// 启动浏览器(使用隐身模式 + 反检测)
browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(true),
Args: []string{
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--no-sandbox",
},
})
if err != nil {
result.Message = fmt.Sprintf("启动浏览器失败: %v", err)
return result
}
defer browser.Close()
// 创建上下文
context, err := browser.NewContext(playwright.BrowserNewContextOptions{
UserAgent: playwright.String(randomUserAgent()),
Viewport: &playwright.Size{
Width: 1920,
Height: 1080,
},
Locale: playwright.String("en-US"),
TimezoneId: playwright.String("America/New_York"),
JavaScriptEnabled: playwright.Bool(true),
})
if err != nil {
result.Message = fmt.Sprintf("创建上下文失败: %v", err)
return result
}
defer context.Close()
// 注入 Cookie
for _, c := range config.Cookies {
context.AddCookies(playwright.BrowserContextAddCookiesOptionsCookies{
Name: playwright.String(c.Name),
Value: playwright.String(c.Value),
Domain: playwright.String(c.Domain),
Path: playwright.String(c.Path),
})
}
// 创建页面
page, err := context.NewPage()
if err != nil {
result.Message = fmt.Sprintf("创建页面失败: %v", err)
return result
}
// 注入反检测脚本
page.AddInitScript(playwright.Script{
Content: playwright.String(`
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
window.chrome = {runtime: {}};
`),
})
// 设置超时
page.SetDefaultTimeout(float64(config.Timeout * 1000))
// 执行下单流程
return processOrder(page, config)
}
func processOrder(page playwright.Page, config *OrderConfig) *OrderResult {
result := &OrderResult{Success: false}
log.Println("步骤 1: 访问商品页...")
if _, err := page.Goto(config.URL, playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateNetworkidle,
}); err != nil {
result.Message = fmt.Sprintf("访问失败: %v", err)
return result
}
// 随机等待(模拟人类行为)
randomSleep(1000, 3000)
log.Println("步骤 2: 检查库存...")
outOfStockCount, _ := page.Locator(fmt.Sprintf("text=/%s/i", config.StockKeyword)).Count()
if outOfStockCount > 0 {
result.Message = "商品无货"
return result
}
log.Println("步骤 3: 检查价格...")
priceText := ""
priceSelectors := []string{
".price", ".product-price", "[class*='price']",
"span:has-text('$')", "div:has-text('$')",
}
for _, selector := range priceSelectors {
if text, err := page.Locator(selector).First().InnerText(); err == nil {
priceText = text
break
}
}
if priceText != "" {
price := extractPrice(priceText)
result.Price = fmt.Sprintf("$%.2f", price)
if config.MaxPrice > 0 && price > config.MaxPrice {
result.Message = fmt.Sprintf("价格超出预算: $%.2f > $%.2f", price, config.MaxPrice)
return result
}
log.Printf("价格: $%.2f ✓", price)
}
log.Println("步骤 4: 检查机房位置...")
if config.TargetLocation != "" {
locationText := ""
locationSelectors := []string{
".location", ".datacenter", "[class*='location']",
"text=/Los Angeles/i", "text=/LA/i",
}
for _, selector := range locationSelectors {
if text, err := page.Locator(selector).First().InnerText(); err == nil {
locationText = text
break
}
}
result.Location = locationText
if locationText != "" && !strings.Contains(strings.ToLower(locationText), strings.ToLower(config.TargetLocation)) {
result.Message = fmt.Sprintf("机房不匹配: %s (需要: %s)", locationText, config.TargetLocation)
// 截图保存(用于调试)
screenshotPath := fmt.Sprintf("wrong_location_%d.png", time.Now().Unix())
page.Screenshot(playwright.PageScreenshotOptions{
Path: playwright.String(screenshotPath),
FullPage: playwright.Bool(true),
})
result.Screenshot = screenshotPath
return result
}
log.Printf("机房: %s ✓", locationText)
}
log.Println("步骤 5: 点击购买按钮...")
randomSleep(500, 1500)
orderButtonSelectors := []string{
"button:has-text('Order Now')",
"button:has-text('Add to Cart')",
"a:has-text('Order Now')",
".order-button",
"#order-button",
}
clicked := false
for _, selector := range orderButtonSelectors {
if err := page.Locator(selector).First().Click(); err == nil {
clicked = true
log.Printf("点击按钮: %s", selector)
break
}
}
if !clicked {
result.Message = "未找到购买按钮"
return result
}
// 等待页面跳转
randomSleep(2000, 4000)
log.Println("步骤 6: 等待购物车/结账页...")
cartURLPatterns := []string{"**/cart**", "**/checkout**", "**/order**"}
currentURL := page.URL()
isCartPage := false
for _, pattern := range cartURLPatterns {
if strings.Contains(currentURL, strings.ReplaceAll(pattern, "**", "")) {
isCartPage = true
break
}
}
if !isCartPage {
// 尝试等待 URL 变化
time.Sleep(3 * time.Second)
currentURL = page.URL()
log.Printf("当前页面: %s", currentURL)
}
log.Println("步骤 7: 确认订单(不支付)...")
// 寻找"确认订单"按钮(但不是"支付"按钮)
confirmButtonSelectors := []string{
"button:has-text('Confirm Order')",
"button:has-text('Place Order')",
"button:has-text('Complete Order'):not(:has-text('Pay'))",
".btn-confirm",
}
confirmed := false
for _, selector := range confirmButtonSelectors {
if count, _ := page.Locator(selector).Count(); count > 0 {
// 检查按钮文本,确保不包含 "Pay"
btnText, _ := page.Locator(selector).First().InnerText()
if !strings.Contains(strings.ToLower(btnText), "pay") {
randomSleep(1000, 2000)
if err := page.Locator(selector).First().Click(); err == nil {
confirmed = true
log.Printf("点击确认按钮: %s", selector)
break
}
}
}
}
if !confirmed {
log.Println("未找到确认按钮,可能已经自动确认或需要手动处理")
}
// 等待订单完成
randomSleep(3000, 5000)
log.Println("步骤 8: 提取订单信息...")
// 尝试提取订单号
orderIDSelectors := []string{
".order-id", ".invoice-id", "[class*='order']",
"text=/Order ID/i", "text=/Invoice #/i",
}
for _, selector := range orderIDSelectors {
if text, err := page.Locator(selector).First().InnerText(); err == nil {
result.OrderID = extractOrderID(text)
if result.OrderID != "" {
break
}
}
}
// 从 URL 提取订单号
if result.OrderID == "" {
currentURL := page.URL()
if id := extractOrderIDFromURL(currentURL); id != "" {
result.OrderID = id
}
}
log.Println("步骤 9: 截图保存...")
screenshotPath := fmt.Sprintf("order_success_%d.png", time.Now().Unix())
page.Screenshot(playwright.PageScreenshotOptions{
Path: playwright.String(screenshotPath),
FullPage: playwright.Bool(true),
})
result.Screenshot = screenshotPath
// 成功
result.Success = true
result.Message = "下单成功(待支付)"
if result.OrderID != "" {
log.Printf("✅ 订单号: %s", result.OrderID)
} else {
log.Println("⚠️ 未能提取订单号,请查看截图")
}
return result
}
// === 辅助函数 ===
func randomUserAgent() string {
agents := []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
}
return agents[rand.Intn(len(agents))]
}
func randomSleep(minMs, maxMs int) {
duration := minMs + rand.Intn(maxMs-minMs)
time.Sleep(time.Duration(duration) * time.Millisecond)
}
func extractPrice(text string) float64 {
// 提取价格:$15.60 或 15.60
text = strings.ReplaceAll(text, "$", "")
text = strings.ReplaceAll(text, ",", "")
text = strings.TrimSpace(text)
price, _ := strconv.ParseFloat(text, 64)
return price
}
func extractOrderID(text string) string {
// 提取订单号Order ID: 12345 或 #12345
text = strings.TrimSpace(text)
parts := strings.Fields(text)
for _, part := range parts {
part = strings.Trim(part, "#:")
if len(part) > 3 && len(part) < 20 {
// 检查是否全是数字或字母数字组合
if isAlphanumeric(part) {
return part
}
}
}
return ""
}
func extractOrderIDFromURL(url string) string {
// 从 URL 提取订单号:/invoice/12345 或 /order/12345
parts := strings.Split(url, "/")
for i, part := range parts {
if (part == "invoice" || part == "order") && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}
func isAlphanumeric(s string) bool {
for _, r := range s {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) {
return false
}
}
return true
}