Acknowledge unrecoverable direct payment callback mismatches, tolerate missing affiliate users during rebate processing, and make channel monitor runner shutdown wait for checks safely.
206 lines
7.2 KiB
Go
206 lines
7.2 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"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()))
|
|
if errors.Is(err, model.ErrTopUpNotFound) || errors.Is(err, model.ErrTopUpAmountMismatch) || errors.Is(err, model.ErrTopUpCurrencyMismatch) || errors.Is(err, model.ErrPaymentMethodMismatch) {
|
|
wxpayNotifySuccess(c)
|
|
} else {
|
|
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})
|
|
}
|