2026-08-02 23:24:32 +08:00
package payment
import (
"crypto/sha256"
"encoding/hex"
"errors"
"net/http"
"strconv"
2026-08-03 16:03:44 +08:00
"strings"
2026-08-02 23:24:32 +08:00
"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 . Channel != channel || order . Amount != amount {
return errors . New ( "payment identity or amount mismatch" )
}
2026-08-03 16:03:44 +08:00
if strings . TrimSpace ( tradeNo ) == "" {
return errors . New ( "channel trade number is required" )
}
if order . PaymentStatus == StatusPaid {
if order . ChannelTradeNo != tradeNo {
return errors . New ( "duplicate callback trade number mismatch" )
}
return nil
}
2026-08-02 23:24:32 +08:00
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" :
2026-08-03 16:03:44 +08:00
result := tx . Model ( & models . EcOrder { } ) . Where ( "identity = ? AND order_status = ?" , order . BusinessIdentity , 16 ) . Updates ( map [ string ] any { "order_status" : 18 , "paid_at" : & now } )
return requireSingleBusinessUpdate ( result )
2026-08-02 23:24:32 +08:00
case "gasorder" :
2026-08-03 16:03:44 +08:00
return requireSingleBusinessUpdate ( tx . Model ( & models . GasorderBasic { } ) . Where ( "identity = ? AND order_status IN ?" , order . BusinessIdentity , [ ] int { 16 , 18 } ) . Update ( "order_status" , 35 ) )
2026-08-02 23:24:32 +08:00
case "recharge" :
return completeRecharge ( tx , order , now )
}
return errors . New ( "unsupported payment business" )
} )
}
2026-08-03 16:03:44 +08:00
func requireSingleBusinessUpdate ( result * gorm . DB ) error {
if result . Error != nil {
return result . Error
}
if result . RowsAffected != 1 {
return errors . New ( "payment business state conflict" )
}
return nil
}
2026-08-02 23:24:32 +08:00
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
}