feat: integrate unified payment and wallet refunds
This commit is contained in:
129
backend/api/internal/logic/payment/callback.go
Normal file
129
backend/api/internal/logic/payment/callback.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package payment
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func digest(value string) string {
|
||||
sum := sha256.Sum256([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// AlipayNotify 验证支付宝证书签名、商户身份、金额和状态后幂等入账。
|
||||
func AlipayNotify(ctx *gin.Context) {
|
||||
client, err := alipayClient()
|
||||
if err != nil {
|
||||
ctx.String(http.StatusServiceUnavailable, "failure")
|
||||
return
|
||||
}
|
||||
if err = ctx.Request.ParseForm(); err != nil || client.VerifySign(ctx, ctx.Request.PostForm) != nil {
|
||||
ctx.String(http.StatusBadRequest, "failure")
|
||||
return
|
||||
}
|
||||
values := ctx.Request.PostForm
|
||||
amount, amountErr := strconv.ParseFloat(values.Get("total_amount"), 64)
|
||||
if amountErr != nil || values.Get("app_id") != config.Spec.Payment.Alipay.AppID || (values.Get("trade_status") != "TRADE_SUCCESS" && values.Get("trade_status") != "TRADE_FINISHED") {
|
||||
ctx.String(http.StatusBadRequest, "failure")
|
||||
return
|
||||
}
|
||||
err = complete(values.Get("out_trade_no"), values.Get("trade_no"), int64(amount*100+0.5), "alipay", digest(values.Encode()))
|
||||
if err != nil {
|
||||
ctx.String(http.StatusConflict, "failure")
|
||||
return
|
||||
}
|
||||
ctx.String(http.StatusOK, "success")
|
||||
}
|
||||
|
||||
// WechatNotify 使用微信平台证书验签并解密 API v3 通知后幂等入账。
|
||||
func WechatNotify(ctx *gin.Context) {
|
||||
if _, err := wechatClient(ctx); err != nil {
|
||||
ctx.JSON(http.StatusServiceUnavailable, gin.H{"code": "FAIL", "message": "channel unavailable"})
|
||||
return
|
||||
}
|
||||
visitor := downloader.MgrInstance().GetCertificateVisitor(config.Spec.Payment.Wechat.MerchantID)
|
||||
handler := notify.NewNotifyHandler(config.Spec.Payment.Wechat.APIv3Key, verifiers.NewSHA256WithRSAVerifier(visitor))
|
||||
transaction := new(payments.Transaction)
|
||||
if _, err := handler.ParseNotifyRequest(ctx, ctx.Request, transaction); err != nil || transaction.OutTradeNo == nil || transaction.Amount == nil || transaction.Amount.Total == nil || transaction.TradeState == nil || *transaction.TradeState != "SUCCESS" {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "invalid notification"})
|
||||
return
|
||||
}
|
||||
tradeNo := ""
|
||||
if transaction.TransactionId != nil {
|
||||
tradeNo = *transaction.TransactionId
|
||||
}
|
||||
if err := complete(*transaction.OutTradeNo, tradeNo, *transaction.Amount.Total, "wechat", digest(transaction.String())); err != nil {
|
||||
ctx.JSON(http.StatusConflict, gin.H{"code": "FAIL", "message": "payment conflict"})
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"})
|
||||
}
|
||||
|
||||
func complete(paymentNo, tradeNo string, amount int64, channel, callbackDigest string) error {
|
||||
return impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var order models.PaymentOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("payment_no = ?", paymentNo).First(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if order.PaymentStatus == StatusPaid {
|
||||
return nil
|
||||
}
|
||||
if order.Channel != channel || order.Amount != amount {
|
||||
return errors.New("payment identity or amount mismatch")
|
||||
}
|
||||
if time.Now().After(order.ExpiresAt) {
|
||||
return tx.Model(&order).Updates(map[string]any{"payment_status": 50, "channel_trade_no": tradeNo, "callback_digest": callbackDigest, "failure_code": "PAID_AFTER_EXPIRED"}).Error
|
||||
}
|
||||
now := time.Now()
|
||||
if err := tx.Model(&order).Updates(map[string]any{"payment_status": StatusPaid, "channel_trade_no": tradeNo, "callback_digest": callbackDigest, "paid_at": &now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
switch order.BusinessType {
|
||||
case "ec_order":
|
||||
return tx.Model(&models.EcOrder{}).Where("identity = ? AND order_status = ?", order.BusinessIdentity, 16).Updates(map[string]any{"order_status": 18, "paid_at": &now}).Error
|
||||
case "gasorder":
|
||||
return tx.Model(&models.GasorderBasic{}).Where("identity = ? AND order_status IN ?", order.BusinessIdentity, []int{16, 18}).Update("order_status", 35).Error
|
||||
case "recharge":
|
||||
return completeRecharge(tx, order, now)
|
||||
}
|
||||
return errors.New("unsupported payment business")
|
||||
})
|
||||
}
|
||||
|
||||
func completeRecharge(tx *gorm.DB, payment models.PaymentOrder, now time.Time) error {
|
||||
var recharge models.WalletRechargeOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND recharge_status = ?", payment.BusinessIdentity, 10).First(&recharge).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&wallet, recharge.WalletBasicID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
wallet.Balance += payment.Amount
|
||||
if err := tx.Model(&wallet).Update("balance", wallet.Balance).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&recharge).Updates(map[string]any{"recharge_status": 23, "completed_at": &now}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
date := now.In(time.Local)
|
||||
return tx.Create(&models.WalletRecord{Entity: models.Entity{Identity: models.NewIdentity(), Status: 1}, WalletBasicID: wallet.ID,
|
||||
RecordNo: "WR" + now.Format("20060102150405.000000"), RequestNo: "recharge:" + recharge.Identity, Direction: "income", TradeType: "recharge",
|
||||
Amount: payment.Amount, BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance, InTradeNo: recharge.RechargeNo,
|
||||
PayChannel: payment.Channel, OperatorIdentity: payment.UserIdentity, Ymd: int32(date.Year()*10000 + int(date.Month())*100 + date.Day()), Ym: int32(date.Year()*100 + int(date.Month()))}).Error
|
||||
}
|
||||
Reference in New Issue
Block a user