package payment import ( "crypto/sha256" "encoding/hex" "errors" "net/http" "strconv" "strings" "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") } 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 } 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": 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) case "gasorder": return requireSingleBusinessUpdate(tx.Model(&models.GasorderBasic{}).Where("identity = ? AND order_status IN ?", order.BusinessIdentity, []int{16, 18}).Update("order_status", 35)) case "recharge": return completeRecharge(tx, order, now) } return errors.New("unsupported payment business") }) } 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 } 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 }