Files
soul-yongping/soul-api/internal/handler/order_webhook.go
2026-03-24 01:22:50 +08:00

164 lines
4.3 KiB
Go

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
}