From afefd5f468822bc52b9213a9fcd722ea41174b93 Mon Sep 17 00:00:00 2001 From: XOF Date: Fri, 12 Dec 2025 03:59:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20auto=5Forder.go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- auto_order.go | 432 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 auto_order.go diff --git a/auto_order.go b/auto_order.go new file mode 100644 index 0000000..78a0bf5 --- /dev/null +++ b/auto_order.go @@ -0,0 +1,432 @@ +// 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 +}