new-api/controller/payment_wxpay.go
zizi b4ee983d1d fix: validate direct payment callback amounts
Check direct payment callback currency, provider, and paid amount before settling Alipay or WxPay top-ups.
2026-05-20 15:49:08 +08:00

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})
}