添加 auto_order.go
This commit is contained in:
432
auto_order.go
Normal file
432
auto_order.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user