ADD: auto order
This commit is contained in:
195
main.go
195
main.go
@@ -29,6 +29,12 @@ type Task struct {
|
|||||||
NotifyEnabled bool `json:"notify_enabled"`
|
NotifyEnabled bool `json:"notify_enabled"`
|
||||||
Order int `json:"order"`
|
Order int `json:"order"`
|
||||||
History []HistoryItem `json:"history"`
|
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 {
|
type Config struct {
|
||||||
@@ -44,6 +50,16 @@ type HistoryItem struct {
|
|||||||
Time time.Time `json:"time"`
|
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 (
|
var (
|
||||||
tasks = make(map[string]*Task)
|
tasks = make(map[string]*Task)
|
||||||
config = &Config{Interval: 60, Timeout: 20, NotifyEnabled: true}
|
config = &Config{Interval: 60, Timeout: 20, NotifyEnabled: true}
|
||||||
@@ -51,6 +67,8 @@ var (
|
|||||||
configMu sync.RWMutex
|
configMu sync.RWMutex
|
||||||
authToken = os.Getenv("AUTH_TOKEN")
|
authToken = os.Getenv("AUTH_TOKEN")
|
||||||
client *http.Client
|
client *http.Client
|
||||||
|
orderLocks = make(map[string]*sync.Mutex)
|
||||||
|
orderLocksMu sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -564,6 +582,9 @@ func checkTask(task *Task) {
|
|||||||
wasInStock := task.InStock
|
wasInStock := task.InStock
|
||||||
wasNotified := task.Notified
|
wasNotified := task.Notified
|
||||||
taskNotifyEnabled := task.NotifyEnabled
|
taskNotifyEnabled := task.NotifyEnabled
|
||||||
|
taskAutoOrder := task.AutoOrder
|
||||||
|
orderConfigPath := task.OrderConfigPath
|
||||||
|
|
||||||
task.Status = status
|
task.Status = status
|
||||||
task.InStock = inStock
|
task.InStock = inStock
|
||||||
task.LastCheck = now
|
task.LastCheck = now
|
||||||
@@ -586,8 +607,15 @@ func checkTask(task *Task) {
|
|||||||
globalNotifyEnabled := config.NotifyEnabled
|
globalNotifyEnabled := config.NotifyEnabled
|
||||||
configMu.RUnlock()
|
configMu.RUnlock()
|
||||||
|
|
||||||
|
// 有货且之前无货且未通知过
|
||||||
if globalNotifyEnabled && taskNotifyEnabled && inStock && !wasInStock && !wasNotified {
|
if globalNotifyEnabled && taskNotifyEnabled && inStock && !wasInStock && !wasNotified {
|
||||||
notify(task.Name + " 有货了!", task.URL)
|
notify(task.Name + " 有货了!", task.URL)
|
||||||
|
|
||||||
|
// 自动下单(异步执行,不阻塞监控)
|
||||||
|
if taskAutoOrder && orderConfigPath != "" {
|
||||||
|
go executeAutoOrder(task)
|
||||||
|
}
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
task.Notified = true
|
task.Notified = true
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
@@ -674,6 +702,173 @@ func handleTestNotification(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
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() {
|
func updateClient() {
|
||||||
configMu.RLock()
|
configMu.RLock()
|
||||||
timeout := config.Timeout
|
timeout := config.Timeout
|
||||||
|
|||||||
Reference in New Issue
Block a user