添加 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