433 lines
13 KiB
Go
433 lines
13 KiB
Go
// 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
|
||
}
|