Add Alipay settings, RSA2 provider implementation, create/return/notify handlers, and provider-specific top-up settlement.
190 lines
6.6 KiB
Go
190 lines
6.6 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/i18n"
|
|
"github.com/QuantumNous/new-api/logger"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/payment"
|
|
"github.com/QuantumNous/new-api/payment/provider"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
type AlipayPaymentRequest struct {
|
|
Amount int64 `json:"amount"`
|
|
PromoCodeId int `json:"promo_code_id"`
|
|
TradeType string `json:"trade_type"`
|
|
}
|
|
|
|
func CreateAlipayPayment(c *gin.Context) {
|
|
if !operation_setting.IsPaymentComplianceConfirmed() {
|
|
common.ApiErrorI18n(c, i18n.MsgPaymentComplianceRequired)
|
|
return
|
|
}
|
|
if !setting.Alipay.Enabled {
|
|
common.ApiErrorMsg(c, "支付宝支付未启用")
|
|
return
|
|
}
|
|
|
|
var req AlipayPaymentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
common.ApiErrorMsg(c, "参数错误")
|
|
return
|
|
}
|
|
if req.Amount < getMinTopup() {
|
|
common.ApiErrorMsg(c, fmt.Sprintf("充值数量不能小于 %d", getMinTopup()))
|
|
return
|
|
}
|
|
|
|
id := c.GetInt("id")
|
|
group, err := model.GetUserGroup(id, true)
|
|
if err != nil {
|
|
common.ApiErrorMsg(c, "获取用户分组失败")
|
|
return
|
|
}
|
|
payMoney := getPayMoney(req.Amount, group)
|
|
if payMoney < 0.01 {
|
|
common.ApiErrorMsg(c, "充值金额过低")
|
|
return
|
|
}
|
|
|
|
amount := req.Amount
|
|
if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens {
|
|
dAmount := decimal.NewFromInt(req.Amount)
|
|
dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
|
amount = dAmount.Div(dQuotaPerUnit).IntPart()
|
|
if amount < 1 {
|
|
amount = 1
|
|
}
|
|
}
|
|
if !validateTopUpPromoCodeForOrder(c, id, req.PromoCodeId, amount) {
|
|
return
|
|
}
|
|
|
|
tradeNo := fmt.Sprintf("ALIPAY-%d-%d-%s", id, time.Now().UnixMilli(), common.GetRandomString(6))
|
|
topUp := &model.TopUp{
|
|
UserId: id,
|
|
Amount: amount,
|
|
Money: payMoney,
|
|
TradeNo: tradeNo,
|
|
PaymentMethod: model.PaymentMethodAlipay,
|
|
PaymentProvider: model.PaymentProviderAlipay,
|
|
PromoCodeId: req.PromoCodeId,
|
|
CreateTime: time.Now().Unix(),
|
|
Status: common.TopUpStatusPending,
|
|
}
|
|
if err := topUp.Insert(); err != nil {
|
|
logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 创建充值订单失败 user_id=%d trade_no=%s amount=%d error=%q", id, tradeNo, req.Amount, err.Error()))
|
|
common.ApiErrorMsg(c, "创建订单失败")
|
|
return
|
|
}
|
|
|
|
alipayProvider, err := provider.NewAlipay(setting.Alipay)
|
|
if err != nil {
|
|
logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 provider 初始化失败 user_id=%d trade_no=%s error=%q", id, tradeNo, err.Error()))
|
|
topUp.Status = common.TopUpStatusFailed
|
|
_ = topUp.Update()
|
|
common.ApiErrorMsg(c, "支付配置错误")
|
|
return
|
|
}
|
|
|
|
callbackAddr := service.GetCallbackAddress()
|
|
notifyURL := callbackAddr + "/api/payment/webhook/alipay"
|
|
if setting.Alipay.NotifyURL != "" {
|
|
notifyURL = setting.Alipay.NotifyURL
|
|
}
|
|
returnURL := callbackAddr + "/api/payment/alipay/return"
|
|
if setting.Alipay.ReturnURL != "" {
|
|
returnURL = setting.Alipay.ReturnURL
|
|
}
|
|
|
|
resp, err := alipayProvider.CreatePayment(c.Request.Context(), &payment.PaymentRequest{
|
|
OrderID: tradeNo,
|
|
Amount: int(math.Round(payMoney * 100)),
|
|
Currency: "CNY",
|
|
Description: fmt.Sprintf("Top up %d credits", req.Amount),
|
|
ReturnURL: returnURL,
|
|
NotifyURL: notifyURL,
|
|
TradeType: req.TradeType,
|
|
})
|
|
if err != nil {
|
|
logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 拉起支付失败 user_id=%d trade_no=%s error=%q", id, tradeNo, err.Error()))
|
|
topUp.Status = common.TopUpStatusFailed
|
|
_ = topUp.Update()
|
|
common.ApiErrorMsg(c, "拉起支付失败")
|
|
return
|
|
}
|
|
|
|
logger.LogInfo(c.Request.Context(), fmt.Sprintf("支付宝 充值订单创建成功 user_id=%d trade_no=%s amount=%d money=%.2f trade_type=%s", id, tradeNo, req.Amount, payMoney, req.TradeType))
|
|
common.ApiSuccess(c, gin.H{
|
|
"payment_url": resp.PaymentURL,
|
|
"qrcode": resp.QRCode,
|
|
"order_id": tradeNo,
|
|
})
|
|
}
|
|
|
|
func AlipayReturn(c *gin.Context) {
|
|
alipayProvider, err := provider.NewAlipay(setting.Alipay)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"message": "error", "data": "支付配置错误"})
|
|
return
|
|
}
|
|
if _, err := alipayProvider.VerifyNotification(c.Request.Context(), []byte(c.Request.URL.Query().Encode()), nil); err != nil {
|
|
logger.LogWarn(c.Request.Context(), fmt.Sprintf("支付宝 同步回调验签失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
|
|
c.JSON(http.StatusBadRequest, gin.H{"message": "error", "data": "验签失败"})
|
|
return
|
|
}
|
|
redirectURL := setting.Alipay.ReturnURL
|
|
if redirectURL == "" {
|
|
redirectURL = paymentReturnPath("/console/topup?show_history=true")
|
|
}
|
|
c.Redirect(http.StatusFound, redirectURL)
|
|
}
|
|
|
|
func AlipayNotify(c *gin.Context) {
|
|
alipayProvider, err := provider.NewAlipay(setting.Alipay)
|
|
if err != nil {
|
|
logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 provider 初始化失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
|
|
c.String(http.StatusOK, "fail")
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err != nil {
|
|
logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 异步通知读取失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
|
|
c.String(http.StatusOK, "fail")
|
|
return
|
|
}
|
|
status, err := alipayProvider.VerifyNotification(c.Request.Context(), body, nil)
|
|
if err != nil {
|
|
logger.LogWarn(c.Request.Context(), fmt.Sprintf("支付宝 异步通知验签失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
|
|
c.String(http.StatusOK, "fail")
|
|
return
|
|
}
|
|
if status.Status != payment.OrderStatusSuccess {
|
|
logger.LogInfo(c.Request.Context(), fmt.Sprintf("支付宝 异步通知忽略非成功状态 trade_no=%s status=%s client_ip=%s", status.ProviderOrderID, status.Status, c.ClientIP()))
|
|
c.String(http.StatusOK, "success")
|
|
return
|
|
}
|
|
|
|
LockOrder(status.ProviderOrderID)
|
|
defer UnlockOrder(status.ProviderOrderID)
|
|
if err := model.RechargeAlipay(status.ProviderOrderID, c.ClientIP()); err != nil {
|
|
logger.LogError(c.Request.Context(), fmt.Sprintf("支付宝 充值处理失败 trade_no=%s client_ip=%s error=%q", status.ProviderOrderID, c.ClientIP(), err.Error()))
|
|
c.String(http.StatusOK, "fail")
|
|
return
|
|
}
|
|
logger.LogInfo(c.Request.Context(), fmt.Sprintf("支付宝 充值成功 trade_no=%s client_ip=%s", status.ProviderOrderID, c.ClientIP()))
|
|
c.String(http.StatusOK, "success")
|
|
}
|