diff --git a/.gitignore b/.gitignore
index c17652a21a33..483c053500b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ web/dist
.env
one-api
new-api
+new-api-local
/__debug_bin*
.DS_Store
tiktoken_cache
diff --git a/controller/subscription_payment_xunhupay.go b/controller/subscription_payment_xunhupay.go
new file mode 100644
index 000000000000..8783f6905407
--- /dev/null
+++ b/controller/subscription_payment_xunhupay.go
@@ -0,0 +1,247 @@
+package controller
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/QuantumNous/new-api/common"
+ "github.com/QuantumNous/new-api/model"
+ "github.com/QuantumNous/new-api/service"
+ "github.com/QuantumNous/new-api/setting/operation_setting"
+ "github.com/QuantumNous/new-api/setting/system_setting"
+ "github.com/gin-gonic/gin"
+ "github.com/samber/lo"
+)
+
+func SubscriptionRequestXunhuPay(c *gin.Context) {
+ var req SubscriptionEpayPayRequest
+ if err := c.ShouldBindJSON(&req); err != nil || req.PlanId <= 0 {
+ common.ApiErrorMsg(c, "参数错误")
+ return
+ }
+
+ plan, err := model.GetSubscriptionPlanById(req.PlanId)
+ if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+ if !plan.Enabled {
+ common.ApiErrorMsg(c, "套餐未启用")
+ return
+ }
+ if plan.PriceAmount < 0.01 {
+ common.ApiErrorMsg(c, "套餐金额过低")
+ return
+ }
+ if !operation_setting.ContainsPayMethod(req.PaymentMethod) {
+ common.ApiErrorMsg(c, "支付方式不存在")
+ return
+ }
+
+ userId := c.GetInt("id")
+ if plan.MaxPurchasePerUser > 0 {
+ count, err := model.CountUserSubscriptionsByPlan(userId, plan.Id)
+ if err != nil {
+ common.ApiError(c, err)
+ return
+ }
+ if count >= int64(plan.MaxPurchasePerUser) {
+ common.ApiErrorMsg(c, "已达到该套餐购买上限")
+ return
+ }
+ }
+
+ if operation_setting.XunhuPayAppId == "" || operation_setting.XunhuPayAppSecret == "" || operation_setting.XunhuPayGateway == "" {
+ common.ApiErrorMsg(c, "当前管理员未配置虎皮椒支付信息")
+ return
+ }
+
+ callBackAddress := service.GetCallbackAddress()
+ returnUrl := callBackAddress + "/api/subscription/xunhupay/return"
+ notifyUrl := callBackAddress + "/api/subscription/xunhupay/notify"
+
+ tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
+ tradeNo = fmt.Sprintf("SUBUSR%dNO%s", userId, tradeNo)
+
+ paymentType := req.PaymentMethod
+ if paymentType == "wxpay" {
+ paymentType = "wechat"
+ }
+
+ nowTime := fmt.Sprintf("%d", time.Now().Unix())
+ totalFee := strconv.FormatFloat(plan.PriceAmount, 'f', 2, 64)
+
+ params := map[string]string{
+ "version": "1.1",
+ "appid": operation_setting.XunhuPayAppId,
+ "trade_order_id": tradeNo,
+ "total_fee": totalFee,
+ "title": fmt.Sprintf("SUB:%s", plan.Title),
+ "time": nowTime,
+ "notify_url": notifyUrl,
+ "return_url": returnUrl,
+ "nonce_str": common.GetRandomString(16),
+ "type": paymentType,
+ }
+ params["hash"] = generateXunhuHash(params, operation_setting.XunhuPayAppSecret)
+
+ order := &model.SubscriptionOrder{
+ UserId: userId,
+ PlanId: plan.Id,
+ Money: plan.PriceAmount,
+ TradeNo: tradeNo,
+ PaymentMethod: req.PaymentMethod,
+ CreateTime: time.Now().Unix(),
+ Status: common.TopUpStatusPending,
+ }
+ if err := order.Insert(); err != nil {
+ common.ApiErrorMsg(c, "创建订单失败")
+ return
+ }
+
+ // Build gateway URL
+ gateway := operation_setting.XunhuPayGateway
+ if !strings.HasSuffix(gateway, "/") && !strings.HasSuffix(gateway, "do.html") {
+ gateway += "/"
+ }
+ if !strings.HasSuffix(gateway, "do.html") {
+ gateway += "payment/do.html"
+ }
+
+ client := service.GetHttpClient()
+ formData := url.Values{}
+ for k, v := range params {
+ formData.Set(k, v)
+ }
+
+ resp, err := client.PostForm(gateway, formData)
+ if err != nil {
+ _ = model.ExpireSubscriptionOrder(tradeNo)
+ common.ApiErrorMsg(c, "网络请求失败,无法拉起支付")
+ return
+ }
+ defer resp.Body.Close()
+
+ var result struct {
+ Errcode int `json:"errcode"`
+ Errmsg string `json:"errmsg"`
+ Url string `json:"url"`
+ Hash string `json:"hash"`
+ }
+ if err = common.DecodeJson(resp.Body, &result); err != nil {
+ _ = model.ExpireSubscriptionOrder(tradeNo)
+ common.ApiErrorMsg(c, "支付网关返回格式错误")
+ return
+ }
+ if result.Errcode != 0 {
+ _ = model.ExpireSubscriptionOrder(tradeNo)
+ common.ApiErrorMsg(c, "拉起支付失败: "+result.Errmsg)
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "success", "url": result.Url})
+}
+
+func SubscriptionXunhuPayNotify(c *gin.Context) {
+ var params map[string]string
+
+ if c.Request.Method == "POST" {
+ if err := c.Request.ParseForm(); err != nil {
+ _, _ = c.Writer.Write([]byte("fail"))
+ return
+ }
+ params = lo.Reduce(lo.Keys(c.Request.PostForm), func(r map[string]string, t string, i int) map[string]string {
+ r[t] = c.Request.PostForm.Get(t)
+ return r
+ }, map[string]string{})
+ } else {
+ params = lo.Reduce(lo.Keys(c.Request.URL.Query()), func(r map[string]string, t string, i int) map[string]string {
+ r[t] = c.Request.URL.Query().Get(t)
+ return r
+ }, map[string]string{})
+ }
+
+ if len(params) == 0 {
+ _, _ = c.Writer.Write([]byte("fail"))
+ return
+ }
+
+ reqHash := params["hash"]
+ if reqHash == "" {
+ _, _ = c.Writer.Write([]byte("fail"))
+ return
+ }
+
+ sign := generateXunhuHash(params, operation_setting.XunhuPayAppSecret)
+ if sign != reqHash {
+ _, _ = c.Writer.Write([]byte("fail"))
+ return
+ }
+
+ if params["status"] == "OD" {
+ tradeNo := params["trade_order_id"]
+ LockOrder(tradeNo)
+ defer UnlockOrder(tradeNo)
+
+ if err := model.CompleteSubscriptionOrder(tradeNo, common.GetJsonString(params)); err != nil {
+ _, _ = c.Writer.Write([]byte("fail"))
+ return
+ }
+ }
+ _, _ = c.Writer.Write([]byte("success"))
+}
+
+func SubscriptionXunhuPayReturn(c *gin.Context) {
+ var params map[string]string
+
+ if c.Request.Method == "POST" {
+ if err := c.Request.ParseForm(); err != nil {
+ c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ return
+ }
+ params = lo.Reduce(lo.Keys(c.Request.PostForm), func(r map[string]string, t string, i int) map[string]string {
+ r[t] = c.Request.PostForm.Get(t)
+ return r
+ }, map[string]string{})
+ } else {
+ params = lo.Reduce(lo.Keys(c.Request.URL.Query()), func(r map[string]string, t string, i int) map[string]string {
+ r[t] = c.Request.URL.Query().Get(t)
+ return r
+ }, map[string]string{})
+ }
+
+ if len(params) == 0 {
+ c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ return
+ }
+
+ reqHash := params["hash"]
+ if reqHash == "" {
+ c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ return
+ }
+
+ sign := generateXunhuHash(params, operation_setting.XunhuPayAppSecret)
+ if sign != reqHash {
+ c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ return
+ }
+
+ if params["status"] == "OD" {
+ tradeNo := params["trade_order_id"]
+ LockOrder(tradeNo)
+ defer UnlockOrder(tradeNo)
+
+ if err := model.CompleteSubscriptionOrder(tradeNo, common.GetJsonString(params)); err != nil {
+ c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
+ return
+ }
+ c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=success")
+ return
+ }
+ c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=pending")
+}
diff --git a/controller/topup.go b/controller/topup.go
index e7a392a4d31d..b40093d4e5c6 100644
--- a/controller/topup.go
+++ b/controller/topup.go
@@ -78,24 +78,69 @@ func GetTopUpInfo(c *gin.Context) {
}
}
+ enableEpay := operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != ""
+ enableXunhuPay := operation_setting.XunhuPayAppId != "" && operation_setting.XunhuPayAppSecret != "" && operation_setting.XunhuPayGateway != ""
+
+ // 如果启用了虎皮椒支付,根据 XunhuPayMethod 自动注入对应支付方式
+ if enableXunhuPay {
+ xunhuMethod := operation_setting.XunhuPayMethod
+ if xunhuMethod == "" {
+ xunhuMethod = "both"
+ }
+ minTopupStr := strconv.Itoa(operation_setting.MinTopUp)
+ if xunhuMethod == "alipay" || xunhuMethod == "both" {
+ hasAlipay := false
+ for _, m := range payMethods {
+ if m["type"] == "alipay" {
+ hasAlipay = true
+ break
+ }
+ }
+ if !hasAlipay {
+ payMethods = append(payMethods, map[string]string{
+ "name": "支付宝",
+ "type": "alipay",
+ "min_topup": minTopupStr,
+ })
+ }
+ }
+ if xunhuMethod == "wxpay" || xunhuMethod == "both" {
+ hasWxpay := false
+ for _, m := range payMethods {
+ if m["type"] == "wxpay" {
+ hasWxpay = true
+ break
+ }
+ }
+ if !hasWxpay {
+ payMethods = append(payMethods, map[string]string{
+ "name": "微信支付",
+ "type": "wxpay",
+ "min_topup": minTopupStr,
+ })
+ }
+ }
+ }
+
data := gin.H{
- "enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "",
- "enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
- "enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]",
- "enable_waffo_topup": enableWaffo,
+ "enable_online_topup": enableEpay || enableXunhuPay,
+ "enable_xunhupay_topup": enableXunhuPay,
+ "enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "",
+ "enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]",
+ "enable_waffo_topup": enableWaffo,
"waffo_pay_methods": func() interface{} {
if enableWaffo {
return setting.GetWaffoPayMethods()
}
return nil
}(),
- "creem_products": setting.CreemProducts,
- "pay_methods": payMethods,
- "min_topup": operation_setting.MinTopUp,
- "stripe_min_topup": setting.StripeMinTopUp,
- "waffo_min_topup": setting.WaffoMinTopUp,
- "amount_options": operation_setting.GetPaymentSetting().AmountOptions,
- "discount": operation_setting.GetPaymentSetting().AmountDiscount,
+ "creem_products": setting.CreemProducts,
+ "pay_methods": payMethods,
+ "min_topup": operation_setting.MinTopUp,
+ "stripe_min_topup": setting.StripeMinTopUp,
+ "waffo_min_topup": setting.WaffoMinTopUp,
+ "amount_options": operation_setting.GetPaymentSetting().AmountOptions,
+ "discount": operation_setting.GetPaymentSetting().AmountDiscount,
}
common.ApiSuccess(c, data)
}
@@ -463,4 +508,3 @@ func AdminCompleteTopUp(c *gin.Context) {
}
common.ApiSuccess(c, nil)
}
-
diff --git a/controller/topup_xunhupay.go b/controller/topup_xunhupay.go
new file mode 100644
index 000000000000..895447782bc2
--- /dev/null
+++ b/controller/topup_xunhupay.go
@@ -0,0 +1,262 @@
+package controller
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "log"
+ "net/url"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/QuantumNous/new-api/common"
+ "github.com/QuantumNous/new-api/logger"
+ "github.com/QuantumNous/new-api/model"
+ "github.com/QuantumNous/new-api/service"
+ "github.com/QuantumNous/new-api/setting/operation_setting"
+ "github.com/QuantumNous/new-api/setting/system_setting"
+ "github.com/gin-gonic/gin"
+ "github.com/samber/lo"
+ "github.com/shopspring/decimal"
+)
+
+type XunhuPayRequest struct {
+ Amount int64 `json:"amount"`
+ PaymentMethod string `json:"payment_method"`
+}
+
+func GetXunhuPayMoney(amount int64, group string) float64 {
+ return getPayMoney(amount, group)
+}
+
+func GetXunhuPayMinTopup() int64 {
+ return getMinTopup()
+}
+
+func generateXunhuHash(data map[string]string, appSecret string) string {
+ keys := make([]string, 0, len(data))
+ for k := range data {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ var sb strings.Builder
+ for _, k := range keys {
+ if k == "hash" || data[k] == "" {
+ continue
+ }
+ if sb.Len() > 0 {
+ sb.WriteString("&")
+ }
+ sb.WriteString(k)
+ sb.WriteString("=")
+ sb.WriteString(data[k])
+ }
+ sb.WriteString(appSecret)
+
+ h := md5.New()
+ h.Write([]byte(sb.String()))
+ return hex.EncodeToString(h.Sum(nil))
+}
+
+func RequestXunhuPay(c *gin.Context) {
+ var req XunhuPayRequest
+ err := c.ShouldBindJSON(&req)
+ if err != nil {
+ c.JSON(200, gin.H{"message": "error", "data": "参数错误"})
+ return
+ }
+ if req.Amount < GetXunhuPayMinTopup() {
+ c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", GetXunhuPayMinTopup())})
+ return
+ }
+
+ id := c.GetInt("id")
+ group, err := model.GetUserGroup(id, true)
+ if err != nil {
+ c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"})
+ return
+ }
+ payMoney := GetXunhuPayMoney(req.Amount, group)
+ if payMoney < 0.01 {
+ c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
+ return
+ }
+
+ if !operation_setting.ContainsPayMethod(req.PaymentMethod) {
+ c.JSON(200, gin.H{"message": "error", "data": "支付方式不存在"})
+ return
+ }
+
+ callBackAddress := service.GetCallbackAddress()
+ returnUrl := system_setting.ServerAddress + "/console/log"
+ notifyUrl := callBackAddress + "/api/user/xunhupay/notify"
+
+ tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
+ tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
+
+ if operation_setting.XunhuPayAppId == "" || operation_setting.XunhuPayAppSecret == "" || operation_setting.XunhuPayGateway == "" {
+ c.JSON(200, gin.H{"message": "error", "data": "当前管理员未配置虎皮椒支付信息"})
+ return
+ }
+
+ paymentType := req.PaymentMethod
+ if paymentType == "wxpay" {
+ paymentType = "wechat"
+ }
+
+ nowTime := fmt.Sprintf("%d", time.Now().Unix())
+ totalFee := strconv.FormatFloat(payMoney, 'f', 2, 64)
+
+ params := map[string]string{
+ "version": "1.1",
+ "appid": operation_setting.XunhuPayAppId,
+ "trade_order_id": tradeNo,
+ "total_fee": totalFee,
+ "title": fmt.Sprintf("TUC%d", req.Amount),
+ "time": nowTime,
+ "notify_url": notifyUrl,
+ "return_url": returnUrl,
+ "nonce_str": common.GetRandomString(16),
+ "type": paymentType,
+ }
+ params["hash"] = generateXunhuHash(params, operation_setting.XunhuPayAppSecret)
+
+ // Send request to gateway
+ gateway := operation_setting.XunhuPayGateway
+ if !strings.HasSuffix(gateway, "/") && !strings.HasSuffix(gateway, "do.html") {
+ gateway += "/"
+ }
+ if !strings.HasSuffix(gateway, "do.html") {
+ gateway += "payment/do.html"
+ }
+
+ client := service.GetHttpClient()
+ formData := url.Values{}
+ for k, v := range params {
+ formData.Set(k, v)
+ }
+
+ resp, err := client.PostForm(gateway, formData)
+ if err != nil {
+ c.JSON(200, gin.H{"message": "error", "data": "网络请求失败,无法拉起微信/支付宝支付"})
+ return
+ }
+ defer resp.Body.Close()
+
+ var result struct {
+ Errcode int `json:"errcode"`
+ Errmsg string `json:"errmsg"`
+ Url string `json:"url"`
+ Hash string `json:"hash"`
+ }
+ err = common.DecodeJson(resp.Body, &result)
+ if err != nil {
+ c.JSON(200, gin.H{"message": "error", "data": "支付网关返回格式错误"})
+ return
+ }
+ if result.Errcode != 0 {
+ c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败: " + result.Errmsg})
+ return
+ }
+
+ amount := req.Amount
+ if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
+ dAmount := decimal.NewFromInt(int64(amount))
+ dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
+ amount = dAmount.Div(dQuotaPerUnit).IntPart()
+ }
+ topUp := &model.TopUp{
+ UserId: id,
+ Amount: amount,
+ Money: payMoney,
+ TradeNo: tradeNo,
+ PaymentMethod: req.PaymentMethod,
+ CreateTime: time.Now().Unix(),
+ Status: "pending",
+ }
+ err = topUp.Insert()
+ if err != nil {
+ c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"})
+ return
+ }
+
+ // Data here is the form hidden inputs, not needed for url redirect mode. Just return URL.
+ c.JSON(200, gin.H{"message": "success", "url": result.Url})
+}
+
+func XunhuPayNotify(c *gin.Context) {
+ var params map[string]string
+
+ if c.Request.Method == "POST" {
+ if err := c.Request.ParseForm(); err != nil {
+ log.Println("虎皮椒回调POST解析失败:", err)
+ _, _ = c.Writer.Write([]byte("fail"))
+ return
+ }
+ params = lo.Reduce(lo.Keys(c.Request.PostForm), func(r map[string]string, t string, i int) map[string]string {
+ r[t] = c.Request.PostForm.Get(t)
+ return r
+ }, map[string]string{})
+ } else {
+ params = lo.Reduce(lo.Keys(c.Request.URL.Query()), func(r map[string]string, t string, i int) map[string]string {
+ r[t] = c.Request.URL.Query().Get(t)
+ return r
+ }, map[string]string{})
+ }
+
+ if len(params) == 0 {
+ log.Println("虎皮椒回调参数为空")
+ _, _ = c.Writer.Write([]byte("fail"))
+ return
+ }
+
+ reqHash := params["hash"]
+ if reqHash == "" {
+ _, _ = c.Writer.Write([]byte("fail"))
+ return
+ }
+
+ sign := generateXunhuHash(params, operation_setting.XunhuPayAppSecret)
+ if sign != reqHash {
+ log.Println("虎皮椒回调签名验证失败")
+ _, _ = c.Writer.Write([]byte("fail"))
+ return
+ }
+
+ status := params["status"]
+ if status == "OD" {
+ tradeNo := params["trade_order_id"]
+ LockOrder(tradeNo)
+ defer UnlockOrder(tradeNo)
+ topUp := model.GetTopUpByTradeNo(tradeNo)
+ if topUp == nil {
+ log.Printf("虎皮椒回调未找到订单: %v", tradeNo)
+ _, _ = c.Writer.Write([]byte("success"))
+ return
+ }
+ if topUp.Status == "pending" {
+ topUp.Status = "success"
+ err := topUp.Update()
+ if err != nil {
+ log.Printf("虎皮椒回调更新订单失败: %v", topUp)
+ return
+ }
+ dAmount := decimal.NewFromInt(int64(topUp.Amount))
+ dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
+ quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart())
+ err = model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true)
+ if err != nil {
+ log.Printf("虎皮椒回调更新用户失败: %v", topUp)
+ return
+ }
+ model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用虎皮椒充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money))
+ }
+ _, _ = c.Writer.Write([]byte("success"))
+ } else {
+ log.Printf("虎皮椒未完成回调状态: %s", status)
+ _, _ = c.Writer.Write([]byte("success"))
+ }
+}
diff --git a/model/option.go b/model/option.go
index efa8c01daa7b..1d63a7f31d16 100644
--- a/model/option.go
+++ b/model/option.go
@@ -77,6 +77,10 @@ func InitOptionMap() {
common.OptionMap["CustomCallbackAddress"] = ""
common.OptionMap["EpayId"] = ""
common.OptionMap["EpayKey"] = ""
+ common.OptionMap["XunhuPayAppId"] = operation_setting.XunhuPayAppId
+ common.OptionMap["XunhuPayAppSecret"] = operation_setting.XunhuPayAppSecret
+ common.OptionMap["XunhuPayGateway"] = operation_setting.XunhuPayGateway
+ common.OptionMap["XunhuPayMethod"] = operation_setting.XunhuPayMethod
common.OptionMap["Price"] = strconv.FormatFloat(operation_setting.Price, 'f', -1, 64)
common.OptionMap["USDExchangeRate"] = strconv.FormatFloat(operation_setting.USDExchangeRate, 'f', -1, 64)
common.OptionMap["MinTopUp"] = strconv.Itoa(operation_setting.MinTopUp)
@@ -341,6 +345,14 @@ func updateOptionMap(key string, value string) (err error) {
system_setting.WorkerValidKey = value
case "PayAddress":
operation_setting.PayAddress = value
+ case "XunhuPayAppId":
+ operation_setting.XunhuPayAppId = value
+ case "XunhuPayAppSecret":
+ operation_setting.XunhuPayAppSecret = value
+ case "XunhuPayGateway":
+ operation_setting.XunhuPayGateway = value
+ case "XunhuPayMethod":
+ operation_setting.XunhuPayMethod = value
case "Chats":
err = setting.UpdateChatsByJsonString(value)
case "AutoGroups":
diff --git a/router/api-router.go b/router/api-router.go
index acc2241b515c..f9805e23ff05 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -64,6 +64,8 @@ func SetApiRouter(router *gin.Engine) {
userRoute.GET("/logout", controller.Logout)
userRoute.POST("/epay/notify", controller.EpayNotify)
userRoute.GET("/epay/notify", controller.EpayNotify)
+ userRoute.POST("/xunhupay/notify", controller.XunhuPayNotify)
+ userRoute.GET("/xunhupay/notify", controller.XunhuPayNotify)
userRoute.GET("/groups", controller.GetUserGroups)
selfRoute := userRoute.Group("/")
@@ -86,6 +88,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.GET("/topup/self", controller.GetUserTopUps)
selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp)
selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestEpay)
+ selfRoute.POST("/xunhupay/pay", middleware.CriticalRateLimit(), controller.RequestXunhuPay)
selfRoute.POST("/amount", controller.RequestAmount)
selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay)
selfRoute.POST("/stripe/amount", controller.RequestStripeAmount)
@@ -141,6 +144,7 @@ func SetApiRouter(router *gin.Engine) {
subscriptionRoute.GET("/self", controller.GetSubscriptionSelf)
subscriptionRoute.PUT("/self/preference", controller.UpdateSubscriptionPreference)
subscriptionRoute.POST("/epay/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestEpay)
+ subscriptionRoute.POST("/xunhupay/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestXunhuPay)
subscriptionRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestStripePay)
subscriptionRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestCreemPay)
}
@@ -165,6 +169,10 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/subscription/epay/notify", controller.SubscriptionEpayNotify)
apiRouter.GET("/subscription/epay/return", controller.SubscriptionEpayReturn)
apiRouter.POST("/subscription/epay/return", controller.SubscriptionEpayReturn)
+ apiRouter.POST("/subscription/xunhupay/notify", controller.SubscriptionXunhuPayNotify)
+ apiRouter.GET("/subscription/xunhupay/notify", controller.SubscriptionXunhuPayNotify)
+ apiRouter.GET("/subscription/xunhupay/return", controller.SubscriptionXunhuPayReturn)
+ apiRouter.POST("/subscription/xunhupay/return", controller.SubscriptionXunhuPayReturn)
optionRoute := apiRouter.Group("/option")
optionRoute.Use(middleware.RootAuth())
{
diff --git a/setting/operation_setting/xunhupay_setting.go b/setting/operation_setting/xunhupay_setting.go
new file mode 100644
index 000000000000..4d5f6abdcb53
--- /dev/null
+++ b/setting/operation_setting/xunhupay_setting.go
@@ -0,0 +1,8 @@
+package operation_setting
+
+// 虎皮椒支付独立配置
+var XunhuPayAppId = ""
+var XunhuPayAppSecret = ""
+var XunhuPayGateway = ""
+// XunhuPayMethod 控制用户端显示的支付方式:alipay / wxpay / both(默认)
+var XunhuPayMethod = "both"
diff --git a/web/src/components/settings/PaymentSetting.jsx b/web/src/components/settings/PaymentSetting.jsx
index 928d58a74bb0..71b6678232f4 100644
--- a/web/src/components/settings/PaymentSetting.jsx
+++ b/web/src/components/settings/PaymentSetting.jsx
@@ -24,6 +24,7 @@ import SettingsPaymentGateway from '../../pages/Setting/Payment/SettingsPaymentG
import SettingsPaymentGatewayStripe from '../../pages/Setting/Payment/SettingsPaymentGatewayStripe';
import SettingsPaymentGatewayCreem from '../../pages/Setting/Payment/SettingsPaymentGatewayCreem';
import SettingsPaymentGatewayWaffo from '../../pages/Setting/Payment/SettingsPaymentGatewayWaffo';
+import SettingsPaymentGatewayXunhu from '../../pages/Setting/Payment/SettingsPaymentGatewayXunhu';
import { API, showError, toBoolean } from '../../helpers';
import { useTranslation } from 'react-i18next';
@@ -42,6 +43,11 @@ const PaymentSetting = () => {
AmountOptions: '',
AmountDiscount: '',
+ XunhuPayAppId: '',
+ XunhuPayAppSecret: '',
+ XunhuPayGateway: '',
+ XunhuPayMethod: 'both',
+
StripeApiSecret: '',
StripeWebhookSecret: '',
StripePriceId: '',
@@ -147,6 +153,9 @@ const PaymentSetting = () => {