feat: MBTI头像与用户规则链路升级,三端页面与接口同步
Made-with: Cursor
This commit is contained in:
163
soul-api/internal/handler/order_webhook.go
Normal file
163
soul-api/internal/handler/order_webhook.go
Normal file
@@ -0,0 +1,163 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user