// 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 }