mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-05-20 06:05:32 +00:00
Create the subscription order before initiating epay payment and expire it if the provider call fails, preventing orphaned transactions and improving reconciliation.
177 lines
5.1 KiB
Go
177 lines
5.1 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 {
|
|
common.ApiErrorMsg(c, "当前管理员未配置支付信息")
|
|
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 {
|
|
common.ApiErrorMsg(c, "创建订单失败")
|
|
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 {
|
|
_ = model.ExpireSubscriptionOrder(tradeNo)
|
|
common.ApiErrorMsg(c, "拉起支付失败")
|
|
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("fail"))
|
|
return
|
|
}
|
|
|
|
if verifyInfo.TradeStatus != epay.StatusTradeSuccess {
|
|
_, _ = c.Writer.Write([]byte("fail"))
|
|
return
|
|
}
|
|
|
|
LockOrder(verifyInfo.ServiceTradeNo)
|
|
defer UnlockOrder(verifyInfo.ServiceTradeNo)
|
|
|
|
if err := model.CompleteSubscriptionOrder(verifyInfo.ServiceTradeNo, common.GetJsonString(verifyInfo)); err != nil {
|
|
_, _ = c.Writer.Write([]byte("fail"))
|
|
return
|
|
}
|
|
|
|
_, _ = c.Writer.Write([]byte("success"))
|
|
}
|
|
|
|
// 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/subscription?pay=fail")
|
|
return
|
|
}
|
|
verifyInfo, err := client.Verify(params)
|
|
if err != nil || !verifyInfo.VerifyStatus {
|
|
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/subscription?pay=fail")
|
|
return
|
|
}
|
|
if verifyInfo.TradeStatus == epay.StatusTradeSuccess {
|
|
LockOrder(verifyInfo.ServiceTradeNo)
|
|
defer UnlockOrder(verifyInfo.ServiceTradeNo)
|
|
if err := model.CompleteSubscriptionOrder(verifyInfo.ServiceTradeNo, common.GetJsonString(verifyInfo)); err != nil {
|
|
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/subscription?pay=fail")
|
|
return
|
|
}
|
|
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/subscription?pay=success")
|
|
return
|
|
}
|
|
c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/subscription?pay=pending")
|
|
}
|