Преглед изворни кода

完善 订单处理,以及 优化 ui

Little Write пре 3 месеци
родитељ
комит
a7d6a8b0d0

+ 228 - 29
controller/topup_creem.go

@@ -2,6 +2,9 @@ package controller
 
 import (
 	"bytes"
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -17,11 +20,30 @@ import (
 )
 
 const (
-	PaymentMethodCreem = "creem"
+	PaymentMethodCreem   = "creem"
+	CreemSignatureHeader = "creem-signature"
 )
 
 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 {
 	ProductId     string `json:"product_id"`
 	PaymentMethod string `json:"payment_method"`
@@ -75,41 +97,65 @@ func (*CreemAdaptor) RequestPay(c *gin.Context, req *CreemPayRequest) {
 	id := c.GetInt("id")
 	user, _ := model.GetUserById(id, false)
 
+	// 生成唯一的订单引用ID
 	reference := fmt.Sprintf("creem-api-ref-%d-%d-%s", user.Id, time.Now().UnixMilli(), randstr.String(4))
 	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{
 		UserId:     id,
-		Amount:     selectedProduct.Quota,
-		Money:      selectedProduct.Price,
+		Amount:     selectedProduct.Quota, // 充值额度
+		Money:      selectedProduct.Price, // 支付金额
 		TradeNo:    referenceId,
 		CreateTime: time.Now().Unix(),
 		Status:     common.TopUpStatusPending,
 	}
 	err = topUp.Insert()
 	if err != nil {
+		log.Printf("创建Creem订单失败: %v", err)
 		c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
 		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{
 		"message": "success",
 		"data": gin.H{
 			"checkout_url": checkoutUrl,
+			"order_id":     referenceId,
 		},
 	})
 }
 
 func RequestCreemPay(c *gin.Context) {
 	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 {
 		c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
 		return
@@ -117,6 +163,68 @@ func RequestCreemPay(c *gin.Context) {
 	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 string `json:"type"`
 	Data struct {
@@ -127,38 +235,122 @@ type CreemWebhookData struct {
 }
 
 func CreemWebhook(c *gin.Context) {
-	// 解析 webhook 数据
-	var webhookData CreemWebhookData
-	if err := c.ShouldBindJSON(&webhookData); err != nil {
-		log.Printf("解析Creem Webhook参数失败: %v\n", err)
+	// 读取body内容用于打印,同时保留原始数据供后续使用
+	bodyBytes, err := io.ReadAll(c.Request.Body)
+	if err != nil {
+		log.Printf("读取Creem Webhook请求body失败: %v", err)
+		c.AbortWithStatus(http.StatusBadRequest)
+		return
+	}
+
+	// 获取签名头
+	signature := c.GetHeader(CreemSignatureHeader)
+
+	// 打印请求信息用于调试
+	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
 	}
 
-	// 检查事件类型
-	if webhookData.Type != "checkout.completed" {
-		log.Printf("忽略Creem Webhook事件类型: %s", webhookData.Type)
+	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)
 		return
 	}
 
-	// 获取引用ID
-	referenceId := webhookData.Data.RequestId
+	// 获取引用ID(这是我们创建订单时传递的request_id)
+	referenceId := event.Object.RequestId
 	if referenceId == "" {
 		log.Println("Creem Webhook缺少request_id字段")
 		c.AbortWithStatus(http.StatusBadRequest)
 		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 {
-		log.Println("Creem充值处理失败:", err.Error(), referenceId)
+		log.Printf("Creem充值处理失败: %s, 订单号: %s", err.Error(), referenceId)
 		c.AbortWithStatus(http.StatusInternalServerError)
 		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)
 }
 
@@ -185,20 +377,23 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
 	apiUrl := "https://api.creem.io/v1/checkouts"
 	if setting.CreemTestMode {
 		apiUrl = "https://test-api.creem.io/v1/checkouts"
+		log.Printf("使用Creem测试环境: %s", apiUrl)
 	}
 
-	// 构建请求数据
+	// 构建请求数据,确保包含用户邮箱
 	requestData := CreemCheckoutRequest{
 		ProductId: product.ProductId,
-		RequestId: referenceId,
+		RequestId: referenceId, // 这个作为订单ID传递给Creem
 		Customer: struct {
 			Email string `json:"email"`
 		}{
-			Email: email,
+			Email: email, // 用户邮箱会在支付页面预填充
 		},
 		Metadata: map[string]string{
 			"username":     username,
 			"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("x-api-key", setting.CreemApiKey)
 
+	log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s",
+		apiUrl, product.ProductId, email, referenceId)
+
 	// 发送请求
 	client := &http.Client{
 		Timeout: 30 * time.Second,
@@ -227,7 +425,6 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
 		return "", fmt.Errorf("发送HTTP请求失败: %v", err)
 	}
 	defer resp.Body.Close()
-	log.Printf(" creem req host: %s, key %s req 【%s】", apiUrl, setting.CreemApiKey, jsonData)
 
 	// 读取响应
 	body, err := io.ReadAll(resp.Body)
@@ -235,6 +432,8 @@ func genCreemLink(referenceId string, product *CreemProduct, email string, usern
 		return "", fmt.Errorf("读取响应失败: %v", err)
 	}
 
+	log.Printf("Creem API响应 - 状态码: %d, 响应体: %s", resp.StatusCode, string(body))
+
 	// 检查响应状态
 	if resp.StatusCode != http.StatusOK {
 		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 未返回支付链接")
 	}
 
-	log.Printf("Creem 支付链接创建成功: %s, 订单ID: %s", referenceId, checkoutResp.Id)
+	log.Printf("Creem 支付链接创建成功 - 订单号: %s, 支付链接: %s", referenceId, checkoutResp.CheckoutUrl)
 	return checkoutResp.CheckoutUrl, nil
 }

+ 3 - 0
model/option.go

@@ -84,6 +84,7 @@ func InitOptionMap() {
 	common.OptionMap["CreemApiKey"] = setting.CreemApiKey
 	common.OptionMap["CreemProducts"] = setting.CreemProducts
 	common.OptionMap["CreemTestMode"] = strconv.FormatBool(setting.CreemTestMode)
+	common.OptionMap["CreemWebhookSecret"] = setting.CreemWebhookSecret
 	common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
 	common.OptionMap["Chats"] = setting.Chats2JsonString()
 	common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
@@ -335,6 +336,8 @@ func updateOptionMap(key string, value string) (err error) {
 		setting.CreemProducts = value
 	case "CreemTestMode":
 		setting.CreemTestMode = value == "true"
+	case "CreemWebhookSecret":
+		setting.CreemWebhookSecret = value
 	case "TopupGroupRatio":
 		err = common.UpdateTopupGroupRatioByJSONString(value)
 	case "GitHubClientId":

+ 25 - 3
model/topup.go

@@ -99,7 +99,7 @@ func Recharge(referenceId string, customerId string) (err error) {
 	return nil
 }
 
-func RechargeCreem(referenceId string) (err error) {
+func RechargeCreem(referenceId string, customerEmail string, customerName string) (err error) {
 	if referenceId == "" {
 		return errors.New("未提供支付单号")
 	}
@@ -131,7 +131,29 @@ func RechargeCreem(referenceId string) (err error) {
 
 		// Creem 直接使用 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 {
 			return err
 		}
@@ -143,7 +165,7 @@ func RechargeCreem(referenceId string) (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
 }

+ 1 - 0
setting/payment_creem.go

@@ -3,3 +3,4 @@ package setting
 var CreemApiKey = ""
 var CreemProducts = "[]"
 var CreemTestMode = false
+var CreemWebhookSecret = ""

+ 1 - 0
web/src/components/settings/PaymentSetting.js

@@ -28,6 +28,7 @@ const PaymentSetting = () => {
     StripeMinTopUp: 1,
 
     CreemApiKey: '',
+    CreemWebhookSecret: '',
     CreemProducts: '[]',
   });
 

+ 356 - 342
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js

@@ -1,373 +1,387 @@
 import React, { useEffect, useState, useRef } from 'react';
 import {
-  Banner,
-  Button,
-  Form,
-  Row,
-  Col,
-  Typography,
-  Spin,
-  Table,
-  Modal,
-  Input,
-  InputNumber,
-  Select,
+    Banner,
+    Button,
+    Form,
+    Row,
+    Col,
+    Typography,
+    Spin,
+    Table,
+    Modal,
+    Input,
+    InputNumber,
+    Select,
 } from '@douyinfe/semi-ui';
 const { Text } = Typography;
 import {
-  API,
-  showError,
-  showSuccess,
+    API,
+    showError,
+    showSuccess,
 } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
 import { Plus, Trash2 } from 'lucide-react';
 
 export default function SettingsPaymentGatewayCreem(props) {
-  const { t } = useTranslation();
-  const [loading, setLoading] = useState(false);
-  const [inputs, setInputs] = useState({
-    CreemApiKey: '',
-    CreemProducts: '[]',
-    CreemTestMode: false,
-  });
-  const [originInputs, setOriginInputs] = useState({});
-  const [products, setProducts] = useState([]);
-  const [showProductModal, setShowProductModal] = useState(false);
-  const [editingProduct, setEditingProduct] = useState(null);
-  const [productForm, setProductForm] = useState({
-    name: '',
-    productId: '',
-    price: 0,
-    quota: 0,
-    currency: 'USD',
-  });
-  const formApiRef = useRef(null);
+    const { t } = useTranslation();
+    const [loading, setLoading] = useState(false);
+    const [inputs, setInputs] = useState({
+        CreemApiKey: '',
+        CreemWebhookSecret: '',
+        CreemProducts: '[]',
+        CreemTestMode: false,
+    });
+    const [originInputs, setOriginInputs] = useState({});
+    const [products, setProducts] = useState([]);
+    const [showProductModal, setShowProductModal] = useState(false);
+    const [editingProduct, setEditingProduct] = useState(null);
+    const [productForm, setProductForm] = useState({
+        name: '',
+        productId: '',
+        price: 0,
+        quota: 0,
+        currency: 'USD',
+    });
+    const formApiRef = useRef(null);
 
-  useEffect(() => {
-    if (props.options && formApiRef.current) {
-      const currentInputs = {
-        CreemApiKey: props.options.CreemApiKey || '',
-        CreemProducts: props.options.CreemProducts || '[]',
-        CreemTestMode: props.options.CreemTestMode === 'true',
-      };
-      setInputs(currentInputs);
-      setOriginInputs({ ...currentInputs });
-      formApiRef.current.setValues(currentInputs);
-      
-      // Parse products
-      try {
-        const parsedProducts = JSON.parse(currentInputs.CreemProducts);
-        setProducts(parsedProducts);
-      } catch (e) {
-        setProducts([]);
-      }
-    }
-  }, [props.options]);
+    useEffect(() => {
+        if (props.options && formApiRef.current) {
+            const currentInputs = {
+                CreemApiKey: props.options.CreemApiKey || '',
+                CreemWebhookSecret: props.options.CreemWebhookSecret || '',
+                CreemProducts: props.options.CreemProducts || '[]',
+                CreemTestMode: props.options.CreemTestMode === 'true',
+            };
+            setInputs(currentInputs);
+            setOriginInputs({ ...currentInputs });
+            formApiRef.current.setValues(currentInputs);
 
-  const handleFormChange = (values) => {
-    setInputs(values);
-  };
+            // Parse products
+            try {
+                const parsedProducts = JSON.parse(currentInputs.CreemProducts);
+                setProducts(parsedProducts);
+            } catch (e) {
+                setProducts([]);
+            }
+        }
+    }, [props.options]);
 
-  const submitCreemSetting = async () => {
-    setLoading(true);
-    try {
-      const options = [];
+    const handleFormChange = (values) => {
+        setInputs(values);
+    };
 
-      if (inputs.CreemApiKey && inputs.CreemApiKey !== '') {
-        options.push({ key: 'CreemApiKey', value: inputs.CreemApiKey });
-      }
-      
-      // Save test mode setting
-      options.push({ key: 'CreemTestMode', value: inputs.CreemTestMode ? 'true' : 'false' });
-      
-      // Save products as JSON string
-      options.push({ key: 'CreemProducts', value: JSON.stringify(products) });
+    const submitCreemSetting = async () => {
+        setLoading(true);
+        try {
+            const options = [];
 
-      // 发送请求
-      const requestQueue = options.map(opt =>
-        API.put('/api/option/', {
-          key: opt.key,
-          value: opt.value,
-        })
-      );
+            if (inputs.CreemApiKey && inputs.CreemApiKey !== '') {
+                options.push({ key: 'CreemApiKey', value: inputs.CreemApiKey });
+            }
 
-      const results = await Promise.all(requestQueue);
+            if (inputs.CreemWebhookSecret && inputs.CreemWebhookSecret !== '') {
+                options.push({ key: 'CreemWebhookSecret', value: inputs.CreemWebhookSecret });
+            }
 
-      // 检查所有请求是否成功
-      const errorResults = results.filter(res => !res.data.success);
-      if (errorResults.length > 0) {
-        errorResults.forEach(res => {
-          showError(res.data.message);
-        });
-      } else {
-        showSuccess(t('更新成功'));
-        // 更新本地存储的原始值
-        setOriginInputs({ ...inputs });
-        props.refresh?.();
-      }
-    } catch (error) {
-      showError(t('更新失败'));
-    }
-    setLoading(false);
-  };
+            // Save test mode setting
+            options.push({ key: 'CreemTestMode', value: inputs.CreemTestMode ? 'true' : 'false' });
 
-  const openProductModal = (product = null) => {
-    if (product) {
-      setEditingProduct(product);
-      setProductForm({ ...product });
-    } else {
-      setEditingProduct(null);
-      setProductForm({
-        name: '',
-        productId: '',
-        price: 0,
-        quota: 0,
-        currency: 'USD',
-      });
-    }
-    setShowProductModal(true);
-  };
+            // Save products as JSON string
+            options.push({ key: 'CreemProducts', value: JSON.stringify(products) });
 
-  const closeProductModal = () => {
-    setShowProductModal(false);
-    setEditingProduct(null);
-    setProductForm({
-      name: '',
-      productId: '',
-      price: 0,
-      quota: 0,
-      currency: 'USD',
-    });
-  };
+            // 发送请求
+            const requestQueue = options.map(opt =>
+                API.put('/api/option/', {
+                    key: opt.key,
+                    value: opt.value,
+                })
+            );
+
+            const results = await Promise.all(requestQueue);
 
-  const saveProduct = () => {
-    if (!productForm.name || !productForm.productId || productForm.price <= 0 || productForm.quota <= 0 || !productForm.currency) {
-      showError(t('请填写完整的产品信息'));
-      return;
-    }
+            // 检查所有请求是否成功
+            const errorResults = results.filter(res => !res.data.success);
+            if (errorResults.length > 0) {
+                errorResults.forEach(res => {
+                    showError(res.data.message);
+                });
+            } else {
+                showSuccess(t('更新成功'));
+                // 更新本地存储的原始值
+                setOriginInputs({ ...inputs });
+                props.refresh?.();
+            }
+        } catch (error) {
+            showError(t('更新失败'));
+        }
+        setLoading(false);
+    };
 
-    let newProducts = [...products];
-    if (editingProduct) {
-      // 编辑现有产品
-      const index = newProducts.findIndex(p => p.productId === editingProduct.productId);
-      if (index !== -1) {
-        newProducts[index] = { ...productForm };
-      }
-    } else {
-      // 添加新产品
-      if (newProducts.find(p => p.productId === productForm.productId)) {
-        showError(t('产品ID已存在'));
-        return;
-      }
-      newProducts.push({ ...productForm });
-    }
+    const openProductModal = (product = null) => {
+        if (product) {
+            setEditingProduct(product);
+            setProductForm({ ...product });
+        } else {
+            setEditingProduct(null);
+            setProductForm({
+                name: '',
+                productId: '',
+                price: 0,
+                quota: 0,
+                currency: 'USD',
+            });
+        }
+        setShowProductModal(true);
+    };
 
-    setProducts(newProducts);
-    closeProductModal();
-  };
+    const closeProductModal = () => {
+        setShowProductModal(false);
+        setEditingProduct(null);
+        setProductForm({
+            name: '',
+            productId: '',
+            price: 0,
+            quota: 0,
+            currency: 'USD',
+        });
+    };
 
-  const deleteProduct = (productId) => {
-    const newProducts = products.filter(p => p.productId !== productId);
-    setProducts(newProducts);
-  };
+    const saveProduct = () => {
+        if (!productForm.name || !productForm.productId || productForm.price <= 0 || productForm.quota <= 0 || !productForm.currency) {
+            showError(t('请填写完整的产品信息'));
+            return;
+        }
 
-  const columns = [
-    {
-      title: t('产品名称'),
-      dataIndex: 'name',
-      key: 'name',
-    },
-    {
-      title: t('产品ID'),
-      dataIndex: 'productId',
-      key: 'productId',
-    },
-    {
-      title: t('价格'),
-      dataIndex: 'price',
-      key: 'price',
-      render: (price, record) => `${record.currency === 'EUR' ? '€' : '$'}${price}`,
-    },
-    {
-      title: t('充值额度'),
-      dataIndex: 'quota',
-      key: 'quota',
-    },
-    {
-      title: t('操作'),
-      key: 'action',
-      render: (_, record) => (
-        <div className='flex gap-2'>
-          <Button
-            type='tertiary'
-            size='small'
-            onClick={() => openProductModal(record)}
-          >
-            {t('编辑')}
-          </Button>
-          <Button
-            type='danger'
-            theme='borderless'
-            size='small'
-            icon={<Trash2 size={14} />}
-            onClick={() => deleteProduct(record.productId)}
-          />
-        </div>
-      ),
-    },
-  ];
+        let newProducts = [...products];
+        if (editingProduct) {
+            // 编辑现有产品
+            const index = newProducts.findIndex(p => p.productId === editingProduct.productId);
+            if (index !== -1) {
+                newProducts[index] = { ...productForm };
+            }
+        } else {
+            // 添加新产品
+            if (newProducts.find(p => p.productId === productForm.productId)) {
+                showError(t('产品ID已存在'));
+                return;
+            }
+            newProducts.push({ ...productForm });
+        }
 
-  return (
-    <Spin spinning={loading}>
-      <Form
-        initValues={inputs}
-        onValueChange={handleFormChange}
-        getFormApi={(api) => (formApiRef.current = api)}
-      >
-        <Form.Section text={t('Creem 设置')}>
-          <Text>
-            Creem 是一个简单的支付处理平台,支持固定金额的产品销售。请在
-            <a
-              href='https://creem.io'
-              target='_blank'
-              rel='noreferrer'
-            >
-              Creem 官网
-            </a>
-            创建账户并获取 API 密钥。
-            <br />
-          </Text>
-          <Banner
-            type='info'
-            description={t('Creem 只支持预设的固定金额产品,不支持自定义金额充值')}
-          />
-          
-          <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}>
-              <Form.Input
-                field='CreemApiKey'
-                label={t('API 密钥')}
-                placeholder={t('creem_xxx 的 Creem API 密钥,敏感信息不显示')}
-                type='password'
-              />
-            </Col>
-            <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-              <Form.Switch
-                field='CreemTestMode'
-                label={t('测试模式')}
-                extraText={t('启用后将使用 Creem 测试环境,可使用测试卡号 4242 4242 4242 4242 进行测试')}
-              />
-            </Col>
-          </Row>
+        setProducts(newProducts);
+        closeProductModal();
+    };
+
+    const deleteProduct = (productId) => {
+        const newProducts = products.filter(p => p.productId !== productId);
+        setProducts(newProducts);
+    };
 
-          <div style={{ marginTop: 24 }}>
-            <div className='flex justify-between items-center mb-4'>
-              <Text strong>{t('产品配置')}</Text>
-              <Button
-                type='primary'
-                icon={<Plus size={16} />}
-                onClick={() => openProductModal()}
-              >
-                {t('添加产品')}
-              </Button>
-            </div>
-            
-            <Table
-              columns={columns}
-              dataSource={products}
-              pagination={false}
-              empty={
-                <div className='text-center py-8'>
-                  <Text type='tertiary'>{t('暂无产品配置')}</Text>
+    const columns = [
+        {
+            title: t('产品名称'),
+            dataIndex: 'name',
+            key: 'name',
+        },
+        {
+            title: t('产品ID'),
+            dataIndex: 'productId',
+            key: 'productId',
+        },
+        {
+            title: t('价格'),
+            dataIndex: 'price',
+            key: 'price',
+            render: (price, record) => `${record.currency === 'EUR' ? '€' : '$'}${price}`,
+        },
+        {
+            title: t('充值额度'),
+            dataIndex: 'quota',
+            key: 'quota',
+        },
+        {
+            title: t('操作'),
+            key: 'action',
+            render: (_, record) => (
+                <div className='flex gap-2'>
+                    <Button
+                        type='tertiary'
+                        size='small'
+                        onClick={() => openProductModal(record)}
+                    >
+                        {t('编辑')}
+                    </Button>
+                    <Button
+                        type='danger'
+                        theme='borderless'
+                        size='small'
+                        icon={<Trash2 size={14} />}
+                        onClick={() => deleteProduct(record.productId)}
+                    />
                 </div>
-              }
-            />
-          </div>
+            ),
+        },
+    ];
 
-          <Button onClick={submitCreemSetting} style={{ marginTop: 16 }}>
-            {t('更新 Creem 设置')}
-          </Button>
-        </Form.Section>
-      </Form>
+    return (
+        <Spin spinning={loading}>
+            <Form
+                initValues={inputs}
+                onValueChange={handleFormChange}
+                getFormApi={(api) => (formApiRef.current = api)}
+            >
+                <Form.Section text={t('Creem 设置')}>
+                    <Text>
+                        Creem 是一个简单的支付处理平台,支持固定金额的产品销售。请在
+                        <a
+                            href='https://creem.io'
+                            target='_blank'
+                            rel='noreferrer'
+                        >
+                            Creem 官网
+                        </a>
+                        创建账户并获取 API 密钥。
+                        <br />
+                    </Text>
+                    <Banner
+                        type='info'
+                        description={t('Creem 只支持预设的固定金额产品,不支持自定义金额充值')}
+                    />
+
+                    <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
+                        <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                            <Form.Input
+                                field='CreemApiKey'
+                                label={t('API 密钥')}
+                                placeholder={t('creem_xxx 的 Creem API 密钥,敏感信息不显示')}
+                                type='password'
+                            />
+                        </Col>
+                        <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
+                                field='CreemTestMode'
+                                label={t('测试模式')}
+                                extraText={t('启用后将使用 Creem Test Mode')}
+                            />
+                        </Col>
+                    </Row>
+
+                    <div style={{ marginTop: 24 }}>
+                        <div className='flex justify-between items-center mb-4'>
+                            <Text strong>{t('产品配置')}</Text>
+                            <Button
+                                type='primary'
+                                icon={<Plus size={16} />}
+                                onClick={() => openProductModal()}
+                            >
+                                {t('添加产品')}
+                            </Button>
+                        </div>
 
-      {/* 产品配置模态框 */}
-      <Modal
-        title={editingProduct ? t('编辑产品') : t('添加产品')}
-        visible={showProductModal}
-        onOk={saveProduct}
-        onCancel={closeProductModal}
-        maskClosable={false}
-        size='small'
-        centered
-      >
-        <div className='space-y-4'>
-          <div>
-            <Text strong className='block mb-2'>
-              {t('产品名称')}
-            </Text>
-            <Input
-              value={productForm.name}
-              onChange={(value) => setProductForm({ ...productForm, name: value })}
-              placeholder={t('例如:基础套餐')}
-              size='large'
-            />
-          </div>
-          <div>
-            <Text strong className='block mb-2'>
-              {t('产品ID')}
-            </Text>
-            <Input
-              value={productForm.productId}
-              onChange={(value) => setProductForm({ ...productForm, productId: value })}
-              placeholder={t('例如:prod_6I8rBerHpPxyoiU9WK4kot')}
-              size='large'
-              disabled={!!editingProduct}
-            />
-          </div>
-          <div>
-            <Text strong className='block mb-2'>
-              {t('货币')}
-            </Text>
-            <Select
-              value={productForm.currency}
-              onChange={(value) => setProductForm({ ...productForm, currency: value })}
-              size='large'
-              className='w-full'
+                        <Table
+                            columns={columns}
+                            dataSource={products}
+                            pagination={false}
+                            empty={
+                                <div className='text-center py-8'>
+                                    <Text type='tertiary'>{t('暂无产品配置')}</Text>
+                                </div>
+                            }
+                        />
+                    </div>
+
+                    <Button onClick={submitCreemSetting} style={{ marginTop: 16 }}>
+                        {t('更新 Creem 设置')}
+                    </Button>
+                </Form.Section>
+            </Form>
+
+            {/* 产品配置模态框 */}
+            <Modal
+                title={editingProduct ? t('编辑产品') : t('添加产品')}
+                visible={showProductModal}
+                onOk={saveProduct}
+                onCancel={closeProductModal}
+                maskClosable={false}
+                size='small'
+                centered
             >
-              <Select.Option value='USD'>USD (美元)</Select.Option>
-              <Select.Option value='EUR'>EUR (欧元)</Select.Option>
-            </Select>
-          </div>
-          <div>
-            <Text strong className='block mb-2'>
-              {t('价格')} ({productForm.currency === 'EUR' ? '欧元' : '美元'})
-            </Text>
-            <InputNumber
-              value={productForm.price}
-              onChange={(value) => setProductForm({ ...productForm, price: value })}
-              placeholder={t('例如:4.99')}
-              min={0.01}
-              precision={2}
-              size='large'
-              className='w-full'
-            />
-          </div>
-          <div>
-            <Text strong className='block mb-2'>
-              {t('充值额度')}
-            </Text>
-            <InputNumber
-              value={productForm.quota}
-              onChange={(value) => setProductForm({ ...productForm, quota: value })}
-              placeholder={t('例如:100000')}
-              min={1}
-              precision={0}
-              size='large'
-              className='w-full'
-            />
-          </div>
-        </div>
-      </Modal>
-    </Spin>
-  );
+                <div className='space-y-4'>
+                    <div>
+                        <Text strong className='block mb-2'>
+                            {t('产品名称')}
+                        </Text>
+                        <Input
+                            value={productForm.name}
+                            onChange={(value) => setProductForm({ ...productForm, name: value })}
+                            placeholder={t('例如:基础套餐')}
+                            size='large'
+                        />
+                    </div>
+                    <div>
+                        <Text strong className='block mb-2'>
+                            {t('产品ID')}
+                        </Text>
+                        <Input
+                            value={productForm.productId}
+                            onChange={(value) => setProductForm({ ...productForm, productId: value })}
+                            placeholder={t('例如:prod_6I8rBerHpPxyoiU9WK4kot')}
+                            size='large'
+                            disabled={!!editingProduct}
+                        />
+                    </div>
+                    <div>
+                        <Text strong className='block mb-2'>
+                            {t('货币')}
+                        </Text>
+                        <Select
+                            value={productForm.currency}
+                            onChange={(value) => setProductForm({ ...productForm, currency: value })}
+                            size='large'
+                            className='w-full'
+                        >
+                            <Select.Option value='USD'>USD (美元)</Select.Option>
+                            <Select.Option value='EUR'>EUR (欧元)</Select.Option>
+                        </Select>
+                    </div>
+                    <div>
+                        <Text strong className='block mb-2'>
+                            {t('价格')} ({productForm.currency === 'EUR' ? '欧元' : '美元'})
+                        </Text>
+                        <InputNumber
+                            value={productForm.price}
+                            onChange={(value) => setProductForm({ ...productForm, price: value })}
+                            placeholder={t('例如:4.99')}
+                            min={0.01}
+                            precision={2}
+                            size='large'
+                            className='w-full'
+                        />
+                    </div>
+                    <div>
+                        <Text strong className='block mb-2'>
+                            {t('充值额度')}
+                        </Text>
+                        <InputNumber
+                            value={productForm.quota}
+                            onChange={(value) => setProductForm({ ...productForm, quota: value })}
+                            placeholder={t('例如:100000')}
+                            min={1}
+                            precision={0}
+                            size='large'
+                            className='w-full'
+                        />
+                    </div>
+                </div>
+            </Modal>
+        </Spin>
+    );
 }