Check direct payment callback currency, provider, and paid amount before settling Alipay or WxPay top-ups.
201 lines
6.9 KiB
Go
201 lines
6.9 KiB
Go
package controller
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"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 WxpayPaymentRequest struct {
|
|
Amount int64 `json:"amount"`
|
|
PromoCodeId int `json:"promo_code_id"`
|
|
TradeType string `json:"trade_type"`
|
|
OpenID string `json:"openid"`
|
|
}
|
|
|
|
func CreateWxpayPayment(c *gin.Context) {
|
|
if !operation_setting.IsPaymentComplianceConfirmed() {
|
|
common.ApiErrorI18n(c, i18n.MsgPaymentComplianceRequired)
|
|
return
|
|
}
|
|
if !setting.Wxpay.Enabled {
|
|
common.ApiErrorMsg(c, "微信支付未启用")
|
|
return
|
|
}
|
|
|
|
var req WxpayPaymentRequest
|
|
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("WXPAY-%d-%d-%s", id, time.Now().UnixMilli(), common.GetRandomString(6))
|
|
topUp := &model.TopUp{
|
|
UserId: id,
|
|
Amount: amount,
|
|
Money: payMoney,
|
|
TradeNo: tradeNo,
|
|
PaymentMethod: model.PaymentMethodWxpay,
|
|
PaymentProvider: model.PaymentProviderWxpay,
|
|
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
|
|
}
|
|
|
|
wxpayProvider, err := provider.NewWxpay(setting.Wxpay)
|
|
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/wxpay"
|
|
if setting.Wxpay.NotifyURL != "" {
|
|
notifyURL = setting.Wxpay.NotifyURL
|
|
}
|
|
returnURL := callbackAddr + "/console/topup?show_history=true"
|
|
if setting.Wxpay.ReturnURL != "" {
|
|
returnURL = setting.Wxpay.ReturnURL
|
|
}
|
|
|
|
payAmount := decimal.NewFromFloat(payMoney).Mul(decimal.NewFromInt(100)).Round(0).IntPart()
|
|
resp, err := wxpayProvider.CreatePayment(c.Request.Context(), &payment.PaymentRequest{
|
|
OrderID: tradeNo,
|
|
Amount: int(payAmount),
|
|
Currency: "CNY",
|
|
Description: fmt.Sprintf("Top up %d credits", req.Amount),
|
|
ReturnURL: returnURL,
|
|
NotifyURL: notifyURL,
|
|
ClientIP: c.ClientIP(),
|
|
TradeType: req.TradeType,
|
|
OpenID: req.OpenID,
|
|
})
|
|
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 WxpayNotify(c *gin.Context) {
|
|
wxpayProvider, err := provider.NewWxpay(setting.Wxpay)
|
|
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()))
|
|
wxpayNotifyFail(c, "支付配置错误")
|
|
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()))
|
|
wxpayNotifyFail(c, "读取失败")
|
|
return
|
|
}
|
|
status, err := wxpayProvider.VerifyNotification(c.Request.Context(), body, collectWxpayHeaders(c))
|
|
if err != nil {
|
|
logger.LogWarn(c.Request.Context(), fmt.Sprintf("微信支付 异步通知验签或解密失败 path=%q client_ip=%s error=%q", c.Request.RequestURI, c.ClientIP(), err.Error()))
|
|
wxpayNotifyFail(c, "验签失败")
|
|
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()))
|
|
wxpayNotifySuccess(c)
|
|
return
|
|
}
|
|
|
|
LockOrder(status.ProviderOrderID)
|
|
defer UnlockOrder(status.ProviderOrderID)
|
|
if err := model.ValidateTopUpPaymentAmount(status.ProviderOrderID, model.PaymentProviderWxpay, status.Amount, status.Currency); err != nil {
|
|
logger.LogWarn(c.Request.Context(), fmt.Sprintf("微信支付 异步通知金额校验失败 trade_no=%s amount=%d currency=%s client_ip=%s error=%q", status.ProviderOrderID, status.Amount, status.Currency, c.ClientIP(), err.Error()))
|
|
wxpayNotifyFail(c, "金额校验失败")
|
|
return
|
|
}
|
|
if err := model.RechargeWxpay(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()))
|
|
wxpayNotifyFail(c, "处理失败")
|
|
return
|
|
}
|
|
logger.LogInfo(c.Request.Context(), fmt.Sprintf("微信支付 充值成功 trade_no=%s client_ip=%s", status.ProviderOrderID, c.ClientIP()))
|
|
wxpayNotifySuccess(c)
|
|
}
|
|
|
|
func collectWxpayHeaders(c *gin.Context) map[string]string {
|
|
headers := map[string]string{}
|
|
for _, key := range []string{
|
|
"Wechatpay-Timestamp",
|
|
"Wechatpay-Nonce",
|
|
"Wechatpay-Signature",
|
|
"Wechatpay-Serial",
|
|
} {
|
|
headers[key] = c.GetHeader(key)
|
|
}
|
|
return headers
|
|
}
|
|
|
|
func wxpayNotifySuccess(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
|
|
}
|
|
|
|
func wxpayNotifyFail(c *gin.Context, message string) {
|
|
c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": message})
|
|
}
|