添加 auto_order.go

This commit is contained in:
XOF
2025-12-12 03:59:18 +08:00
parent 06f8cada47
commit afefd5f468

432
auto_order.go Normal file
View File

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