mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 03:43:39 +00:00
Improve subscription payment safety and data integrity by handling user/URL lookup failures, fixing Stripe subscription mode, persisting quota reset fields, and correcting subscription delta accounting and DB timestamp casting. Refine the UI with stricter custom duration validation, accurate currency rounding, conditional Epay labeling, rollback on preference update failure, and shared subscription formatting helpers plus clearer component naming.
173 lines
4.9 KiB
Go
173 lines
4.9 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/Calcium-Ion/go-epay/epay"
|
|
"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"
|
|
)
|
|
|
|
type SubscriptionEpayPayRequest struct {
|
|
PlanId int `json:"plan_id"`
|
|
PaymentMethod string `json:"payment_method"`
|
|
}
|
|
|
|
func SubscriptionRequestEpay(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
|
|
}
|
|
}
|
|
|
|
callBackAddress := service.GetCallbackAddress()
|
|
returnUrl, err := url.Parse(callBackAddress + "/api/subscription/epay/return")
|
|
if err != nil {
|
|
common.ApiErrorMsg(c, "回调地址配置错误")
|
|
return
|
|
}
|
|
notifyUrl, err := url.Parse(callBackAddress + "/api/subscription/epay/notify")
|
|
if err != nil {
|
|
common.ApiErrorMsg(c, "回调地址配置错误")
|
|
return
|
|
}
|
|
|
|
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
|
|
tradeNo = fmt.Sprintf("SUBUSR%dNO%s", userId, tradeNo)
|
|
|
|
client := GetEpayClient()
|
|
if client == nil {
|
|
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "当前管理员未配置支付信息"})
|
|
return
|
|
}
|
|
|
|
uri, params, err := client.Purchase(&epay.PurchaseArgs{
|
|
Type: req.PaymentMethod,
|
|
ServiceTradeNo: tradeNo,
|
|
Name: fmt.Sprintf("SUB:%s", plan.Title),
|
|
Money: strconv.FormatFloat(plan.PriceAmount, 'f', 2, 64),
|
|
Device: epay.PC,
|
|
NotifyUrl: notifyUrl,
|
|
ReturnUrl: returnUrl,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "拉起支付失败"})
|
|
return
|
|
}
|
|
|
|
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 {
|
|
c.JSON(http.StatusOK, gin.H{"message": "error", "data": "创建订单失败"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"message": "success", "data": params, "url": uri})
|
|
}
|
|
|
|
func SubscriptionEpayNotify(c *gin.Context) {
|
|
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{})
|
|
|
|
client := GetEpayClient()
|
|
if client == nil {
|
|
_, _ = c.Writer.Write([]byte("fail"))
|
|
return
|
|
}
|
|
verifyInfo, err := client.Verify(params)
|
|
if err == nil && verifyInfo.VerifyStatus {
|
|
_, _ = c.Writer.Write([]byte("success"))
|
|
} else {
|
|
_, _ = c.Writer.Write([]byte("fail"))
|
|
return
|
|
}
|
|
|
|
if verifyInfo.TradeStatus != epay.StatusTradeSuccess {
|
|
return
|
|
}
|
|
|
|
LockOrder(verifyInfo.ServiceTradeNo)
|
|
defer UnlockOrder(verifyInfo.ServiceTradeNo)
|
|
|
|
if err := model.CompleteSubscriptionOrder(verifyInfo.ServiceTradeNo, common.GetJsonString(verifyInfo)); err != nil {
|
|
// do not fail webhook response after signature verified
|
|
return
|
|
}
|
|
}
|
|
|
|
// SubscriptionEpayReturn handles browser return after payment.
|
|
// It verifies the payload and completes the order, then redirects to console.
|
|
func SubscriptionEpayReturn(c *gin.Context) {
|
|
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{})
|
|
|
|
client := GetEpayClient()
|
|
if client == nil {
|
|
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
|
|
return
|
|
}
|
|
verifyInfo, err := client.Verify(params)
|
|
if err != nil || !verifyInfo.VerifyStatus {
|
|
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=fail")
|
|
return
|
|
}
|
|
if verifyInfo.TradeStatus == epay.StatusTradeSuccess {
|
|
LockOrder(verifyInfo.ServiceTradeNo)
|
|
defer UnlockOrder(verifyInfo.ServiceTradeNo)
|
|
_ = model.CompleteSubscriptionOrder(verifyInfo.ServiceTradeNo, common.GetJsonString(verifyInfo))
|
|
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=success")
|
|
return
|
|
}
|
|
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup?pay=pending")
|
|
}
|