mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-05-01 05:51:46 +00:00
完善 订单处理,以及 优化 ui
This commit is contained in:
@@ -2,6 +2,9 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -18,10 +21,29 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
PaymentMethodCreem = "creem"
|
PaymentMethodCreem = "creem"
|
||||||
|
CreemSignatureHeader = "creem-signature"
|
||||||
)
|
)
|
||||||
|
|
||||||
var creemAdaptor = &CreemAdaptor{}
|
var creemAdaptor = &CreemAdaptor{}
|
||||||
|
|
||||||
|
// 生成HMAC-SHA256签名
|
||||||
|
func generateCreemSignature(payload string, secret string) string {
|
||||||
|
h := hmac.New(sha256.New, []byte(secret))
|
||||||
|
h.Write([]byte(payload))
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证Creem webhook签名
|
||||||
|
func verifyCreemSignature(payload string, signature string, secret string) bool {
|
||||||
|
if secret == "" {
|
||||||
|
log.Printf("Creem webhook secret未配置,跳过签名验证")
|
||||||
|
return true // 如果没有配置secret,跳过验证
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSignature := generateCreemSignature(payload, secret)
|
||||||
|
return hmac.Equal([]byte(signature), []byte(expectedSignature))
|
||||||
|
}
|
||||||
|
|
||||||
type CreemPayRequest struct {
|
type CreemPayRequest struct {
|
||||||
ProductId string `json:"product_id"`
|
ProductId string `json:"product_id"`
|
||||||
PaymentMethod string `json:"payment_method"`
|
PaymentMethod string `json:"payment_method"`
|
||||||
@@ -75,41 +97,65 @@ func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
|
|||||||
id := c.GetInt("id")
|
id := c.GetInt("id")
|
||||||
user, _ := model.GetUserById(id, false)
|
user, _ := model.GetUserById(id, false)
|
||||||
|
|
||||||
|
// 生成唯一的订单引用ID
|
||||||
reference := fmt.Sprintf("creem-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
|
reference := fmt.Sprintf("creem-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
|
||||||
referenceId := "ref_" + common.Sha1([]byte(reference))
|
referenceId := "ref_" + common.Sha1([]byte(reference))
|
||||||
|
|
||||||
checkoutUrl, err := genCreemLink(referenceId, selectedProduct, user.Email, user.Username)
|
// 先创建订单记录,使用产品配置的金额和充值额度
|
||||||
if err != nil {
|
|
||||||
log.Println("获取Creem支付链接失败", err)
|
|
||||||
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
topUp := &model.TopUp{
|
topUp := &model.TopUp{
|
||||||
UserId: id,
|
UserId: id,
|
||||||
Amount: selectedProduct.Quota,
|
Amount: selectedProduct.Quota, // 充值额度
|
||||||
Money: selectedProduct.Price,
|
Money: selectedProduct.Price, // 支付金额
|
||||||
TradeNo: referenceId,
|
TradeNo: referenceId,
|
||||||
CreateTime: time.Now().Unix(),
|
CreateTime: time.Now().Unix(),
|
||||||
Status: common.TopUpStatusPending,
|
Status: common.TopUpStatusPending,
|
||||||
}
|
}
|
||||||
err = topUp.Insert()
|
err = topUp.Insert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("创建Creem订单失败: %v", err)
|
||||||
c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
|
c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建支付链接,传入用户邮箱
|
||||||
|
checkoutUrl, err := genCreemLink(referenceId, selectedProduct, user.Email, user.Username)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("获取Creem支付链接失败: %v", err)
|
||||||
|
c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Creem订单创建成功 - 用户ID: %d, 订单号: %s, 产品: %s, 充值额度: %d, 支付金额: %.2f",
|
||||||
|
id, referenceId, selectedProduct.Name, selectedProduct.Quota, selectedProduct.Price)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"message": "success",
|
"message": "success",
|
||||||
"data": gin.H{
|
"data": gin.H{
|
||||||
"checkout_url": checkoutUrl,
|
"checkout_url": checkoutUrl,
|
||||||
|
"order_id": referenceId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func RequestCreemPay(c *gin.Context) {
|
func RequestCreemPay(c *gin.Context) {
|
||||||
var req CreemPayRequest
|
var req CreemPayRequest
|
||||||
err := c.ShouldBindJSON(&req)
|
|
||||||
|
// 读取body内容用于打印,同时保留原始数据供后续使用
|
||||||
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("读取请求body失败: %v", err)
|
||||||
|
c.JSON(200, gin.H{"message": "error", "data": "读取请求失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印body内容
|
||||||
|
log.Printf("creem pay request body: %s", string(bodyBytes))
|
||||||
|
|
||||||
|
// 重新设置body供后续的ShouldBindJSON使用
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
|
||||||
|
err = c.ShouldBindJSON(&req)
|
||||||
|
log.Printf(" json body is %+v", req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
|
c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
|
||||||
return
|
return
|
||||||
@@ -117,6 +163,68 @@ func RequestCreemPay(c *gin.Context) {
|
|||||||
creemAdaptor.RequestPay(c, &req)
|
creemAdaptor.RequestPay(c, &req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 新的Creem Webhook结构体,匹配实际的webhook数据格式
|
||||||
|
type CreemWebhookEvent struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
EventType string `json:"eventType"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
Object struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
RequestId string `json:"request_id"`
|
||||||
|
Order struct {
|
||||||
|
Object string `json:"object"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Customer string `json:"customer"`
|
||||||
|
Product string `json:"product"`
|
||||||
|
Amount int `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
SubTotal int `json:"sub_total"`
|
||||||
|
TaxAmount int `json:"tax_amount"`
|
||||||
|
AmountDue int `json:"amount_due"`
|
||||||
|
AmountPaid int `json:"amount_paid"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Transaction string `json:"transaction"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
} `json:"order"`
|
||||||
|
Product struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Price int `json:"price"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
BillingType string `json:"billing_type"`
|
||||||
|
BillingPeriod string `json:"billing_period"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TaxMode string `json:"tax_mode"`
|
||||||
|
TaxCategory string `json:"tax_category"`
|
||||||
|
DefaultSuccessUrl *string `json:"default_success_url"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
} `json:"product"`
|
||||||
|
Units int `json:"units"`
|
||||||
|
Customer struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
} `json:"customer"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Metadata map[string]string `json:"metadata"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
} `json:"object"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留旧的结构体作为兼容
|
||||||
type CreemWebhookData struct {
|
type CreemWebhookData struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Data struct {
|
Data struct {
|
||||||
@@ -127,38 +235,122 @@ type CreemWebhookData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CreemWebhook(c *gin.Context) {
|
func CreemWebhook(c *gin.Context) {
|
||||||
// 解析 webhook 数据
|
// 读取body内容用于打印,同时保留原始数据供后续使用
|
||||||
var webhookData CreemWebhookData
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||||
if err := c.ShouldBindJSON(&webhookData); err != nil {
|
if err != nil {
|
||||||
log.Printf("解析Creem Webhook参数失败: %v\n", err)
|
log.Printf("读取Creem Webhook请求body失败: %v", err)
|
||||||
c.AbortWithStatus(http.StatusBadRequest)
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查事件类型
|
// 获取签名头
|
||||||
if webhookData.Type != "checkout.completed" {
|
signature := c.GetHeader(CreemSignatureHeader)
|
||||||
log.Printf("忽略Creem Webhook事件类型: %s", webhookData.Type)
|
|
||||||
|
// 打印请求信息用于调试
|
||||||
|
log.Printf("Creem Webhook - URI: %s, Query: %s", c.Request.RequestURI, c.Request.URL.RawQuery)
|
||||||
|
log.Printf("Creem Webhook - Signature: %s", signature)
|
||||||
|
log.Printf("Creem Webhook - Body: %s", string(bodyBytes))
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
if !verifyCreemSignature(string(bodyBytes), signature, setting.CreemWebhookSecret) {
|
||||||
|
log.Printf("Creem Webhook签名验证失败")
|
||||||
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Creem Webhook签名验证成功")
|
||||||
|
|
||||||
|
// 重新设置body供后续的ShouldBindJSON使用
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewReader(bodyBytes))
|
||||||
|
|
||||||
|
// 解析新格式的webhook数据
|
||||||
|
var webhookEvent CreemWebhookEvent
|
||||||
|
if err := c.ShouldBindJSON(&webhookEvent); err != nil {
|
||||||
|
log.Printf("解析Creem Webhook参数失败: %v", err)
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Creem Webhook解析成功 - EventType: %s, EventId: %s", webhookEvent.EventType, webhookEvent.Id)
|
||||||
|
|
||||||
|
// 根据事件类型处理不同的webhook
|
||||||
|
switch webhookEvent.EventType {
|
||||||
|
case "checkout.completed":
|
||||||
|
handleCheckoutCompleted(c, &webhookEvent)
|
||||||
|
default:
|
||||||
|
log.Printf("忽略Creem Webhook事件类型: %s", webhookEvent.EventType)
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理支付完成事件
|
||||||
|
func handleCheckoutCompleted(c *gin.Context, event *CreemWebhookEvent) {
|
||||||
|
// 验证订单状态
|
||||||
|
if event.Object.Order.Status != "paid" {
|
||||||
|
log.Printf("订单状态不是已支付: %s, 跳过处理", event.Object.Order.Status)
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取引用ID
|
// 获取引用ID(这是我们创建订单时传递的request_id)
|
||||||
referenceId := webhookData.Data.RequestId
|
referenceId := event.Object.RequestId
|
||||||
if referenceId == "" {
|
if referenceId == "" {
|
||||||
log.Println("Creem Webhook缺少request_id字段")
|
log.Println("Creem Webhook缺少request_id字段")
|
||||||
c.AbortWithStatus(http.StatusBadRequest)
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理支付完成事件
|
// 验证订单类型,目前只处理一次性付款
|
||||||
err := model.RechargeCreem(referenceId)
|
if event.Object.Order.Type != "onetime" {
|
||||||
|
log.Printf("暂不支持的订单类型: %s, 跳过处理", event.Object.Order.Type)
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录详细的支付信息
|
||||||
|
log.Printf("处理Creem支付完成 - 订单号: %s, Creem订单ID: %s, 支付金额: %d %s, 客户邮箱: %s, 产品: %s",
|
||||||
|
referenceId,
|
||||||
|
event.Object.Order.Id,
|
||||||
|
event.Object.Order.AmountPaid,
|
||||||
|
event.Object.Order.Currency,
|
||||||
|
event.Object.Customer.Email,
|
||||||
|
event.Object.Product.Name)
|
||||||
|
|
||||||
|
// 查询本地订单确认存在
|
||||||
|
topUp := model.GetTopUpByTradeNo(referenceId)
|
||||||
|
if topUp == nil {
|
||||||
|
log.Printf("Creem充值订单不存在: %s", referenceId)
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if topUp.Status != common.TopUpStatusPending {
|
||||||
|
log.Printf("Creem充值订单状态错误: %s, 当前状态: %s", referenceId, topUp.Status)
|
||||||
|
c.Status(http.StatusOK) // 已处理过的订单,返回成功避免重复处理
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理充值,传入客户邮箱和姓名信息
|
||||||
|
customerEmail := event.Object.Customer.Email
|
||||||
|
customerName := event.Object.Customer.Name
|
||||||
|
|
||||||
|
// 防护性检查,确保邮箱和姓名不为空字符串
|
||||||
|
if customerEmail == "" {
|
||||||
|
log.Printf("警告:Creem回调中客户邮箱为空 - 订单号: %s", referenceId)
|
||||||
|
}
|
||||||
|
if customerName == "" {
|
||||||
|
log.Printf("警告:Creem回调中客户姓名为空 - 订单号: %s", referenceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := model.RechargeCreem(referenceId, customerEmail, customerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Creem充值处理失败:", err.Error(), referenceId)
|
log.Printf("Creem充值处理失败: %s, 订单号: %s", err.Error(), referenceId)
|
||||||
c.AbortWithStatus(http.StatusInternalServerError)
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Creem充值成功: %s", referenceId)
|
log.Printf("Creem充值成功 - 订单号: %s, 充值额度: %d, 支付金额: %.2f, 客户邮箱: %s, 客户姓名: %s",
|
||||||
|
referenceId, topUp.Amount, topUp.Money, customerEmail, customerName)
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,20 +377,23 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
|
|||||||
apiUrl := "https://api.creem.io/v1/checkouts"
|
apiUrl := "https://api.creem.io/v1/checkouts"
|
||||||
if setting.CreemTestMode {
|
if setting.CreemTestMode {
|
||||||
apiUrl = "https://test-api.creem.io/v1/checkouts"
|
apiUrl = "https://test-api.creem.io/v1/checkouts"
|
||||||
|
log.Printf("使用Creem测试环境: %s", apiUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建请求数据
|
// 构建请求数据,确保包含用户邮箱
|
||||||
requestData := CreemCheckoutRequest{
|
requestData := CreemCheckoutRequest{
|
||||||
ProductId: product.ProductId,
|
ProductId: product.ProductId,
|
||||||
RequestId: referenceId,
|
RequestId: referenceId, // 这个作为订单ID传递给Creem
|
||||||
Customer: struct {
|
Customer: struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
}{
|
}{
|
||||||
Email: email,
|
Email: email, // 用户邮箱会在支付页面预填充
|
||||||
},
|
},
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]string{
|
||||||
"username": username,
|
"username": username,
|
||||||
"reference_id": referenceId,
|
"reference_id": referenceId,
|
||||||
|
"product_name": product.Name,
|
||||||
|
"quota": fmt.Sprintf("%d", product.Quota),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +413,9 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
|
|||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("x-api-key", setting.CreemApiKey)
|
req.Header.Set("x-api-key", setting.CreemApiKey)
|
||||||
|
|
||||||
|
log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s",
|
||||||
|
apiUrl, product.ProductId, email, referenceId)
|
||||||
|
|
||||||
// 发送请求
|
// 发送请求
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
@@ -227,7 +425,6 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
|
|||||||
return "", fmt.Errorf("发送HTTP请求失败: %v", err)
|
return "", fmt.Errorf("发送HTTP请求失败: %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
log.Printf(" creem req host: %s, key %s req 【%s】", apiUrl, setting.CreemApiKey, jsonData)
|
|
||||||
|
|
||||||
// 读取响应
|
// 读取响应
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
@@ -235,6 +432,8 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
|
|||||||
return "", fmt.Errorf("读取响应失败: %v", err)
|
return "", fmt.Errorf("读取响应失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Creem API响应 - 状态码: %d, 响应体: %s", resp.StatusCode, string(body))
|
||||||
|
|
||||||
// 检查响应状态
|
// 检查响应状态
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return "", fmt.Errorf("Creem API 返回错误状态 %d: %s", resp.StatusCode, string(body))
|
return "", fmt.Errorf("Creem API 返回错误状态 %d: %s", resp.StatusCode, string(body))
|
||||||
@@ -251,6 +450,6 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
|
|||||||
return "", fmt.Errorf("Creem API 未返回支付链接")
|
return "", fmt.Errorf("Creem API 未返回支付链接")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Creem 支付链接创建成功: %s, 订单ID: %s", referenceId, checkoutResp.Id)
|
log.Printf("Creem 支付链接创建成功 - 订单号: %s, 支付链接: %s", referenceId, checkoutResp.CheckoutUrl)
|
||||||
return checkoutResp.CheckoutUrl, nil
|
return checkoutResp.CheckoutUrl, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["CreemApiKey"] = setting.CreemApiKey
|
common.OptionMap["CreemApiKey"] = setting.CreemApiKey
|
||||||
common.OptionMap["CreemProducts"] = setting.CreemProducts
|
common.OptionMap["CreemProducts"] = setting.CreemProducts
|
||||||
common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode)
|
common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode)
|
||||||
|
common.OptionMap["CreemWebhookSecret"] = setting.CreemWebhookSecret
|
||||||
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
|
||||||
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
||||||
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
|
||||||
@@ -335,6 +336,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
setting.CreemProducts = value
|
setting.CreemProducts = value
|
||||||
case "CreemTestMode":
|
case "CreemTestMode":
|
||||||
setting.CreemTestMode = value == "true"
|
setting.CreemTestMode = value == "true"
|
||||||
|
case "CreemWebhookSecret":
|
||||||
|
setting.CreemWebhookSecret = value
|
||||||
case "TopupGroupRatio":
|
case "TopupGroupRatio":
|
||||||
err = common.UpdateTopupGroupRatioByJSONString(value)
|
err = common.UpdateTopupGroupRatioByJSONString(value)
|
||||||
case "GitHubClientId":
|
case "GitHubClientId":
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func Recharge(referenceId string, customerId string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RechargeCreem(referenceId string) (err error) {
|
func RechargeCreem(referenceId string, customerEmail string, customerName string) (err error) {
|
||||||
if referenceId == "" {
|
if referenceId == "" {
|
||||||
return errors.New("未提供支付单号")
|
return errors.New("未提供支付单号")
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,29 @@ func RechargeCreem(referenceId string) (err error) {
|
|||||||
|
|
||||||
// Creem 直接使用 Amount 作为充值额度
|
// Creem 直接使用 Amount 作为充值额度
|
||||||
quota = float64(topUp.Amount)
|
quota = float64(topUp.Amount)
|
||||||
err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quota)).Error
|
|
||||||
|
// 构建更新字段,优先使用邮箱,如果邮箱为空则使用用户名
|
||||||
|
updateFields := map[string]interface{}{
|
||||||
|
"quota": gorm.Expr("quota + ?", quota),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有客户邮箱,尝试更新用户邮箱(仅当用户邮箱为空时)
|
||||||
|
if customerEmail != "" {
|
||||||
|
// 先检查用户当前邮箱是否为空
|
||||||
|
var user User
|
||||||
|
err = tx.Where("id = ?", topUp.UserId).First(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户邮箱为空,则更新为支付时使用的邮箱
|
||||||
|
if user.Email == "" {
|
||||||
|
updateFields["email"] = customerEmail
|
||||||
|
fmt.Printf("更新用户邮箱:用户ID %d, 新邮箱 %s\n", topUp.UserId, customerEmail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Updates(updateFields).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -143,7 +165,7 @@ func RechargeCreem(referenceId string) (err error) {
|
|||||||
return errors.New("充值失败," + err.Error())
|
return errors.New("充值失败," + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", common.FormatQuota(int(quota)), topUp.Money))
|
RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f,客户邮箱:%s", common.FormatQuota(int(quota)), topUp.Money, customerEmail))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ package setting
|
|||||||
var CreemApiKey = ""
|
var CreemApiKey = ""
|
||||||
var CreemProducts = "[]"
|
var CreemProducts = "[]"
|
||||||
var CreemTestMode = false
|
var CreemTestMode = false
|
||||||
|
var CreemWebhookSecret = ""
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const PaymentSetting = () => {
|
|||||||
StripeMinTopUp: 1,
|
StripeMinTopUp: 1,
|
||||||
|
|
||||||
CreemApiKey: '',
|
CreemApiKey: '',
|
||||||
|
CreemWebhookSecret: '',
|
||||||
CreemProducts: '[]',
|
CreemProducts: '[]',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export default function SettingsPaymentGatewayCreem(props) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
CreemApiKey: '',
|
CreemApiKey: '',
|
||||||
|
CreemWebhookSecret: '',
|
||||||
CreemProducts: '[]',
|
CreemProducts: '[]',
|
||||||
CreemTestMode: false,
|
CreemTestMode: false,
|
||||||
});
|
});
|
||||||
@@ -47,6 +48,7 @@ export default function SettingsPaymentGatewayCreem(props) {
|
|||||||
if (props.options && formApiRef.current) {
|
if (props.options && formApiRef.current) {
|
||||||
const currentInputs = {
|
const currentInputs = {
|
||||||
CreemApiKey: props.options.CreemApiKey || '',
|
CreemApiKey: props.options.CreemApiKey || '',
|
||||||
|
CreemWebhookSecret: props.options.CreemWebhookSecret || '',
|
||||||
CreemProducts: props.options.CreemProducts || '[]',
|
CreemProducts: props.options.CreemProducts || '[]',
|
||||||
CreemTestMode: props.options.CreemTestMode === 'true',
|
CreemTestMode: props.options.CreemTestMode === 'true',
|
||||||
};
|
};
|
||||||
@@ -77,6 +79,10 @@ export default function SettingsPaymentGatewayCreem(props) {
|
|||||||
options.push({ key: 'CreemApiKey', value: inputs.CreemApiKey });
|
options.push({ key: 'CreemApiKey', value: inputs.CreemApiKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inputs.CreemWebhookSecret && inputs.CreemWebhookSecret !== '') {
|
||||||
|
options.push({ key: 'CreemWebhookSecret', value: inputs.CreemWebhookSecret });
|
||||||
|
}
|
||||||
|
|
||||||
// Save test mode setting
|
// Save test mode setting
|
||||||
options.push({ key: 'CreemTestMode', value: inputs.CreemTestMode ? 'true' : 'false' });
|
options.push({ key: 'CreemTestMode', value: inputs.CreemTestMode ? 'true' : 'false' });
|
||||||
|
|
||||||
@@ -243,7 +249,7 @@ export default function SettingsPaymentGatewayCreem(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
||||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
field='CreemApiKey'
|
field='CreemApiKey'
|
||||||
label={t('API 密钥')}
|
label={t('API 密钥')}
|
||||||
@@ -251,11 +257,19 @@ export default function SettingsPaymentGatewayCreem(props) {
|
|||||||
type='password'
|
type='password'
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||||
|
<Form.Input
|
||||||
|
field='CreemWebhookSecret'
|
||||||
|
label={t('Webhook 密钥')}
|
||||||
|
placeholder={t('用于验证 Webhook 请求的密钥,敏感信息不显示')}
|
||||||
|
type='password'
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
||||||
<Form.Switch
|
<Form.Switch
|
||||||
field='CreemTestMode'
|
field='CreemTestMode'
|
||||||
label={t('测试模式')}
|
label={t('测试模式')}
|
||||||
extraText={t('启用后将使用 Creem 测试环境,可使用测试卡号 4242 4242 4242 4242 进行测试')}
|
extraText={t('启用后将使用 Creem Test Mode')}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
Reference in New Issue
Block a user