package pay import ( "net/http" "time" "encoding/json" "bindbox-game/configs" "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/repository/mysql/model" "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" "github.com/wechatpay-apiv3/wechatpay-go/utils" ) type notifyAck struct { Code string `json:"code"` Message string `json:"message"` } // WechatNotify 微信支付回调通知处理 // 入参:微信官方通知,验签并解密resource,推进订单状态为已支付 // 幂等:若订单已为已支付则直接ACK func (h *handler) WechatNotify() core.HandlerFunc { return func(ctx core.Context) { c := configs.Get() if c.WechatPay.ApiV3Key == "" { ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete")) return } var handler *notify.Handler if c.WechatPay.PublicKeyID != "" && c.WechatPay.PublicKeyPath != "" { pubKey, err := utils.LoadPublicKeyWithPath(c.WechatPay.PublicKeyPath) if err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150001, err.Error())) return } handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAPubkeyVerifier(c.WechatPay.PublicKeyID, *pubKey)) } else { if c.WechatPay.MchID == "" || c.WechatPay.SerialNo == "" || c.WechatPay.PrivateKeyPath == "" { ctx.AbortWithError(core.Error(http.StatusBadRequest, 150000, "wechat pay config incomplete")) return } mchPrivateKey, err := utils.LoadPrivateKeyWithPath(c.WechatPay.PrivateKeyPath) if err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150002, err.Error())) return } if err := downloader.MgrInstance().RegisterDownloaderWithPrivateKey(ctx.RequestContext(), mchPrivateKey, c.WechatPay.SerialNo, c.WechatPay.MchID, c.WechatPay.ApiV3Key); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150003, err.Error())) return } certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(c.WechatPay.MchID) handler = notify.NewNotifyHandler(c.WechatPay.ApiV3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor)) } var transaction payments.Transaction notification, err := handler.ParseNotifyRequest(ctx.RequestContext(), ctx.Request(), &transaction) if err != nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, err.Error())) return } // 事件去重处理 var existed *model.PaymentNotifyEvents if notification != nil && notification.ID != "" { existed, _ = h.readDB.PaymentNotifyEvents.WithContext(ctx.RequestContext()).Where(h.readDB.PaymentNotifyEvents.NotifyID.Eq(notification.ID)).First() if existed != nil && existed.Processed { ctx.Payload(¬ifyAck{Code: "SUCCESS", Message: "OK"}) return } } if transaction.OutTradeNo == nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "missing out_trade_no")) return } paidAt := time.Now() if transaction.SuccessTime != nil { if t, err := time.Parse(time.RFC3339, *transaction.SuccessTime); err == nil { paidAt = t } } rawStr := func() string { b, _ := json.Marshal(transaction); return string(b) }() order, err := h.readDB.Orders.WithContext(ctx.RequestContext()).Where(h.readDB.Orders.OrderNo.Eq(*transaction.OutTradeNo)).First() if err != nil || order == nil { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "order not found for out_trade_no")) return } tx := &model.PaymentTransactions{ OrderID: order.ID, OrderNo: *transaction.OutTradeNo, Channel: "wechat_jsapi", TransactionID: func() string { if transaction.TransactionId!=nil { return *transaction.TransactionId }; return "" }(), AmountTotal: func() int64 { if transaction.Amount!=nil && transaction.Amount.Total!=nil { return *transaction.Amount.Total }; return 0 }(), SuccessTime: paidAt, Raw: rawStr, } if err := h.writeDB.PaymentTransactions.WithContext(ctx.RequestContext()).Create(tx); err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150006, err.Error())) return } // 记录事件 if notification != nil && notification.ID != "" { if existed == nil { _ = h.writeDB.PaymentNotifyEvents.WithContext(ctx.RequestContext()).Create(&model.PaymentNotifyEvents{ NotifyID: notification.ID, ResourceType: notification.ResourceType, EventType: notification.EventType, Summary: notification.Summary, Raw: rawStr, Processed: false, }) } } _, err = h.writeDB.Orders.WithContext(ctx.RequestContext()).Where(h.writeDB.Orders.OrderNo.Eq(*transaction.OutTradeNo), h.writeDB.Orders.Status.Eq(1)).Updates(map[string]any{ h.writeDB.Orders.Status.ColumnName().String(): 2, h.writeDB.Orders.PaidAt.ColumnName().String(): paidAt, }) if err != nil { ctx.AbortWithError(core.Error(http.StatusInternalServerError, 150005, err.Error())) return } // 标记事件处理完成 if notification != nil && notification.ID != "" { _, _ = h.writeDB.PaymentNotifyEvents.WithContext(ctx.RequestContext()).Where(h.readDB.PaymentNotifyEvents.NotifyID.Eq(notification.ID)).Updates(map[string]any{ h.writeDB.PaymentNotifyEvents.Processed.ColumnName().String(): true, }) } ctx.Payload(¬ifyAck{Code: "SUCCESS", Message: "OK"}) } }