diff --git a/main.go b/main.go index 41b9cb3..1aae865 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,12 @@ type Task struct { NotifyEnabled bool `json:"notify_enabled"` Order int `json:"order"` History []HistoryItem `json:"history"` + // 新增:自动下单相关 + AutoOrder bool `json:"auto_order"` // 是否启用自动下单 + OrderConfigPath string `json:"order_config"` // 下单配置路径 + Ordering bool `json:"ordering"` // 是否正在下单中 + LastOrderTime time.Time `json:"last_order_time"` // 最后下单时间 + OrderSuccess bool `json:"order_success"` // 是否下单成功过 } type Config struct { @@ -44,6 +50,16 @@ type HistoryItem struct { Time time.Time `json:"time"` } +// OrderResult 结构体 +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"` +} + var ( tasks = make(map[string]*Task) config = &Config{Interval: 60, Timeout: 20, NotifyEnabled: true} @@ -51,6 +67,8 @@ var ( configMu sync.RWMutex authToken = os.Getenv("AUTH_TOKEN") client *http.Client + orderLocks = make(map[string]*sync.Mutex) + orderLocksMu sync.Mutex ) func main() { @@ -564,6 +582,9 @@ func checkTask(task *Task) { wasInStock := task.InStock wasNotified := task.Notified taskNotifyEnabled := task.NotifyEnabled + taskAutoOrder := task.AutoOrder + orderConfigPath := task.OrderConfigPath + task.Status = status task.InStock = inStock task.LastCheck = now @@ -586,8 +607,15 @@ func checkTask(task *Task) { globalNotifyEnabled := config.NotifyEnabled configMu.RUnlock() + // 有货且之前无货且未通知过 if globalNotifyEnabled && taskNotifyEnabled && inStock && !wasInStock && !wasNotified { notify(task.Name + " 有货了!", task.URL) + + // 自动下单(异步执行,不阻塞监控) + if taskAutoOrder && orderConfigPath != "" { + go executeAutoOrder(task) + } + mu.Lock() task.Notified = true mu.Unlock() @@ -674,6 +702,173 @@ func handleTestNotification(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } +// 获取任务专属的锁 +func getOrderLock(taskID string) *sync.Mutex { + orderLocksMu.Lock() + defer orderLocksMu.Unlock() + + if _, exists := orderLocks[taskID]; !exists { + orderLocks[taskID] = &sync.Mutex{} + } + return orderLocks[taskID] +} + +func executeAutoOrder(task *Task) { + // 获取该任务的专属锁 + taskLock := getOrderLock(task.ID) + + // 尝试获取锁,如果获取失败说明已经有下单任务在执行 + if !taskLock.TryLock() { + log.Printf("[%s] 已有下单任务正在执行,跳过", task.Name) + return + } + defer taskLock.Unlock() + + // 检查是否已经下单成功过 + mu.RLock() + if task.OrderSuccess { + mu.RUnlock() + log.Printf("[%s] 该任务已成功下单,跳过", task.Name) + return + } + + // 检查最近是否下过单(防止频繁重试) + if !task.LastOrderTime.IsZero() && time.Since(task.LastOrderTime) < 5*time.Minute { + mu.RUnlock() + log.Printf("[%s] 距离上次下单不足5分钟,跳过", task.Name) + return + } + mu.RUnlock() + + // 标记为下单中 + mu.Lock() + task.Ordering = true + task.LastOrderTime = time.Now() + mu.Unlock() + saveTasks() + + log.Printf("[%s] 开始自动下单...", task.Name) + + // 重试逻辑 + maxRetries := 3 + var lastResult *OrderResult + + for attempt := 1; attempt <= maxRetries; attempt++ { + log.Printf("[%s] 第 %d/%d 次尝试下单", task.Name, attempt, maxRetries) + + result := runOrder(task) + lastResult = result + + if result.Success { + // 下单成功 + mu.Lock() + task.Ordering = false + task.OrderSuccess = true + mu.Unlock() + saveTasks() + + msg := fmt.Sprintf("✅ 自动下单成功!\n商品: %s\n订单号: %s\n价格: %s\n机房: %s\n截图: %s\n尝试次数: %d", + task.Name, result.OrderID, result.Price, result.Location, result.Screenshot, attempt) + notify("🎉 "+task.Name+" 下单成功", msg) + log.Printf("[%s] %s", task.Name, msg) + + return + } + + // 下单失败,判断是否需要重试 + log.Printf("[%s] 第 %d 次尝试失败: %s", task.Name, attempt, result.Message) + + // 判断失败原因,某些情况不需要重试 + if shouldSkipRetry(result.Message) { + log.Printf("[%s] 失败原因不适合重试,停止", task.Name) + break + } + + // 如果不是最后一次,等待后重试 + if attempt < maxRetries { + waitTime := time.Duration(attempt*5) * time.Second // 递增等待:5s, 10s, 15s + log.Printf("[%s] 等待 %v 后重试...", task.Name, waitTime) + time.Sleep(waitTime) + } + } + + // 所有重试都失败 + mu.Lock() + task.Ordering = false + mu.Unlock() + saveTasks() + + msg := fmt.Sprintf("❌ 自动下单失败(已重试 %d 次)\n商品: %s\n最后错误: %s\n截图: %s", + maxRetries, task.Name, lastResult.Message, lastResult.Screenshot) + notify("⚠️ "+task.Name+" 下单失败", msg) + log.Printf("[%s] %s", task.Name, msg) +} +// 执行单次下单 +func runOrder(task *Task) *OrderResult { + result := &OrderResult{Success: false} + + // 检查配置文件是否存在 + if task.OrderConfigPath == "" { + result.Message = "未配置下单配置文件" + return result + } + + if _, err := os.Stat(task.OrderConfigPath); os.IsNotExist(err) { + result.Message = fmt.Sprintf("配置文件不存在: %s", task.OrderConfigPath) + return result + } + + // 执行下单脚本 + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "./auto_order", task.OrderConfigPath) + output, err := cmd.CombinedOutput() + + if ctx.Err() == context.DeadlineExceeded { + result.Message = "下单超时(120秒)" + return result + } + + if err != nil { + result.Message = fmt.Sprintf("执行失败: %v, 输出: %s", err, string(output)) + log.Printf("[%s] 命令执行错误: %v", task.Name, err) + log.Printf("[%s] 命令输出: %s", task.Name, string(output)) + return result + } + + // 解析结果 + if err := json.Unmarshal(output, result); err != nil { + result.Message = fmt.Sprintf("解析结果失败: %v, 原始输出: %s", err, string(output)) + log.Printf("[%s] JSON 解析错误: %v", task.Name, err) + log.Printf("[%s] 原始输出: %s", task.Name, string(output)) + return result + } + + return result +} +// 判断是否应该跳过重试 +func shouldSkipRetry(message string) bool { + // 这些情况不需要重试 + skipKeywords := []string{ + "价格超出预算", + "机房不匹配", + "未配置", + "配置文件不存在", + "Cookie", + "认证失败", + } + + messageLower := strings.ToLower(message) + for _, keyword := range skipKeywords { + if strings.Contains(messageLower, strings.ToLower(keyword)) { + return true + } + } + + return false +} + func updateClient() { configMu.RLock() timeout := config.Timeout