package handler import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strings" "time" "soul-api/internal/database" "soul-api/internal/model" "gorm.io/gorm" ) func loadOrderWebhookURL(db *gorm.DB) string { keys := []string{"order_paid_webhook_url", "ckb_lead_webhook_url"} for _, key := range keys { var cfg model.SystemConfig if err := db.Where("config_key = ?", key).First(&cfg).Error; err != nil { continue } var webhookURL string if len(cfg.ConfigValue) > 0 { _ = json.Unmarshal(cfg.ConfigValue, &webhookURL) } webhookURL = strings.TrimSpace(webhookURL) if webhookURL != "" && strings.HasPrefix(webhookURL, "http") { return webhookURL } } return "" } func pushPaidOrderWebhook(db *gorm.DB, order *model.Order) error { if order == nil || order.OrderSN == "" { return fmt.Errorf("empty order") } if order.WebhookPushStatus == "sent" { return nil } webhookURL := loadOrderWebhookURL(db) if webhookURL == "" { return nil } var user model.User _ = db.Select("id,nickname,phone,open_id").Where("id = ?", order.UserID).First(&user).Error productName := order.ProductType if order.Description != nil && strings.TrimSpace(*order.Description) != "" { productName = strings.TrimSpace(*order.Description) } status := "" if order.Status != nil { status = *order.Status } if status == "" { status = "paid" } text := "💰 用户购买成功(实时推送)" text += fmt.Sprintf("\n订单号: %s", order.OrderSN) if user.Nickname != nil && strings.TrimSpace(*user.Nickname) != "" { text += fmt.Sprintf("\n用户: %s", strings.TrimSpace(*user.Nickname)) } if user.Phone != nil && strings.TrimSpace(*user.Phone) != "" { text += fmt.Sprintf("\n手机: %s", strings.TrimSpace(*user.Phone)) } text += fmt.Sprintf("\n商品: %s", productName) text += fmt.Sprintf("\n金额: %.2f", order.Amount) text += fmt.Sprintf("\n状态: %s", status) if order.PayTime != nil { text += fmt.Sprintf("\n支付时间: %s", order.PayTime.Format("2006-01-02 15:04:05")) } else { text += fmt.Sprintf("\n支付时间: %s", time.Now().Format("2006-01-02 15:04:05")) } var payload []byte if strings.Contains(webhookURL, "qyapi.weixin.qq.com") { payload, _ = json.Marshal(map[string]interface{}{ "msgtype": "text", "text": map[string]string{"content": text}, }) } else { payload, _ = json.Marshal(map[string]interface{}{ "msg_type": "text", "content": map[string]string{"text": text}, }) } resp, err := http.Post(webhookURL, "application/json", bytes.NewReader(payload)) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode >= 400 { return fmt.Errorf("webhook status=%d", resp.StatusCode) } return nil } func markOrderWebhookResult(db *gorm.DB, orderSn string, sent bool, pushErr error) { if orderSn == "" { return } updates := map[string]interface{}{ "webhook_push_attempts": gorm.Expr("COALESCE(webhook_push_attempts, 0) + 1"), "updated_at": time.Now(), } if sent { now := time.Now() updates["webhook_push_status"] = "sent" updates["webhook_pushed_at"] = now updates["webhook_push_error"] = "" } else { errText := "" if pushErr != nil { errText = strings.TrimSpace(pushErr.Error()) } if len(errText) > 500 { errText = errText[:500] } updates["webhook_push_status"] = "failed" updates["webhook_push_error"] = errText } _ = db.Model(&model.Order{}).Where("order_sn = ?", orderSn).Updates(updates).Error } // RetryPendingPaidOrderWebhooks 扫描未推送成功的已支付订单并补推。 func RetryPendingPaidOrderWebhooks(ctx context.Context, limit int) (retried, sent int, err error) { if limit <= 0 { limit = 200 } if limit > 2000 { limit = 2000 } db := database.DB() var rows []model.Order if err := db.Where( "status IN ? AND COALESCE(webhook_push_status,'') <> ?", []string{"paid", "completed"}, "sent", ).Order("pay_time ASC, created_at ASC").Limit(limit).Find(&rows).Error; err != nil { return 0, 0, err } for i := range rows { select { case <-ctx.Done(): return retried, sent, ctx.Err() default: } retried++ pushErr := pushPaidOrderWebhook(db, &rows[i]) if pushErr == nil { sent++ markOrderWebhookResult(db, rows[i].OrderSN, true, nil) } else { markOrderWebhookResult(db, rows[i].OrderSN, false, pushErr) } } return retried, sent, nil }