package callback import ( "context" "net/http" "net/url" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp/fasthttpadaptor" "go.uber.org/zap" agentrechargeApp "github.com/break/junhong_cmp_fiber/internal/application/agentrecharge" "github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog" "github.com/break/junhong_cmp_fiber/internal/model" orderService "github.com/break/junhong_cmp_fiber/internal/service/order" rechargeService "github.com/break/junhong_cmp_fiber/internal/service/recharge" rechargeOrderSvc "github.com/break/junhong_cmp_fiber/internal/service/recharge_order" "github.com/break/junhong_cmp_fiber/internal/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/alipay" "github.com/break/junhong_cmp_fiber/pkg/auditcontext" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/fuiou" pkgmiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/break/junhong_cmp_fiber/pkg/wechat" "gorm.io/gorm" ) // AgentRechargeServiceInterface 代理充值服务接口 type AgentRechargeServiceInterface interface { HandlePaymentCallback(ctx context.Context, rechargeNo string, paymentMethod string, paymentTransactionID string, paidAmount int64) error } // WechatConfigServiceInterface 支付配置服务接口 type WechatConfigServiceInterface interface { GetActiveConfig(ctx context.Context) (*model.WechatConfig, error) GetConfigForCallback(ctx context.Context, orderNo string) (*model.WechatConfig, error) } type PaymentHandler struct { orderService *orderService.Service rechargeService *rechargeService.Service rechargeOrderService *rechargeOrderSvc.Service agentRechargeService AgentRechargeServiceInterface wechatPayment wechat.PaymentServiceInterface wechatConfigService WechatConfigServiceInterface paymentStore *postgres.PaymentStore agentPaymentConfirm *agentrechargeApp.ConfirmOnlinePaymentService integration *integrationlog.Repository logger *zap.Logger } func NewPaymentHandler( orderService *orderService.Service, rechargeService *rechargeService.Service, rechargeOrderService *rechargeOrderSvc.Service, agentRechargeService AgentRechargeServiceInterface, wechatPayment wechat.PaymentServiceInterface, wechatConfigService WechatConfigServiceInterface, paymentStore *postgres.PaymentStore, agentPaymentConfirm *agentrechargeApp.ConfirmOnlinePaymentService, integration *integrationlog.Repository, logger *zap.Logger, ) *PaymentHandler { return &PaymentHandler{ orderService: orderService, rechargeService: rechargeService, rechargeOrderService: rechargeOrderService, agentRechargeService: agentRechargeService, wechatPayment: wechatPayment, wechatConfigService: wechatConfigService, paymentStore: paymentStore, agentPaymentConfirm: agentPaymentConfirm, integration: integration, logger: logger, } } type verifiedPaymentCallback struct { PaymentNo string PaymentMethod string TransactionID string Amount int64 ConfigID uint MerchantIdentity string PaidAt time.Time Provider string RawPayload []byte ContentType string } // WechatPayCallback 微信支付回调(带签名验证) // POST /api/callback/wechat-pay func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error { body := c.Body() ctx := c.UserContext() // 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置 orderNo, err := wechat.PeekOrderNo(body) if err != nil { h.logger.Error("微信回调:预解析订单号失败", zap.Error(err)) return errors.New(errors.CodeWechatCallbackInvalid, "回调数据格式错误") } cfg, err := h.wechatConfigService.GetConfigForCallback(ctx, orderNo) if err != nil || cfg == nil { h.logger.Error("微信回调:加载支付配置失败", zap.String("order_no", orderNo), zap.Error(err), ) return errors.New(errors.CodeWechatCallbackInvalid, "微信支付服务未配置") } ctx = paymentCallbackContext(ctx, constants.IntegrationProviderWechatPay) switch cfg.ProviderType { case model.ProviderTypeWechatV2: paymentSvc := wechat.NewPaymentV2Service(cfg.OaAppID, cfg.WxMchID, cfg.WxAPIV2Key, cfg.WxNotifyURL, h.logger) result, err := paymentSvc.VerifyCallback(body) if err != nil { return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "微信 v2 回调验签失败") } if result.TradeState != "SUCCESS" { if err := h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{ PaymentNo: result.OutTradeNo, TransactionID: result.TransactionID, Provider: constants.IntegrationProviderWechatPay, RawPayload: body, ContentType: c.Get("Content-Type"), }, result.TradeState); err != nil { return err } return h.wechatV2SuccessResponse(c) } // TotalFee 为字符串格式的分,解析失败则降级为 0(后续 handlePaymentCallback 会记录日志) totalFee, _ := strconv.ParseInt(result.TotalFee, 10, 64) paidAt, _ := time.ParseInLocation("20060102150405", result.SuccessTime, time.Local) if err := h.dispatchWechatCallback(ctx, verifiedPaymentCallback{ PaymentNo: result.OutTradeNo, PaymentMethod: model.PaymentMethodWechat, TransactionID: result.TransactionID, Amount: totalFee, ConfigID: cfg.ID, MerchantIdentity: cfg.WxMchID, PaidAt: paidAt, Provider: constants.IntegrationProviderWechatPay, RawPayload: body, ContentType: c.Get("Content-Type"), }); err != nil { return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败") } case model.ProviderTypeWechat: if h.wechatPayment == nil { return errors.New(errors.CodeWechatCallbackInvalid, "微信 v3 支付未初始化") } var httpReq http.Request fasthttpadaptor.ConvertRequest(c.Context(), &httpReq, true) _, err := h.wechatPayment.HandlePaymentNotify(&httpReq, func(result *wechat.PaymentNotifyResult) error { if result.TradeState != "SUCCESS" { return h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{ PaymentNo: result.OutTradeNo, TransactionID: result.TransactionID, Provider: constants.IntegrationProviderWechatPay, RawPayload: body, ContentType: c.Get("Content-Type"), }, result.TradeState) } paidAt, _ := time.Parse(time.RFC3339, result.SuccessTime) return h.dispatchWechatCallback(ctx, verifiedPaymentCallback{ PaymentNo: result.OutTradeNo, PaymentMethod: model.PaymentMethodWechat, TransactionID: result.TransactionID, Amount: result.TotalAmount, ConfigID: cfg.ID, MerchantIdentity: cfg.WxMchID, PaidAt: paidAt, Provider: constants.IntegrationProviderWechatPay, RawPayload: body, ContentType: c.Get("Content-Type"), }) }) if err != nil { return errors.Wrap(errors.CodeWechatCallbackInvalid, err, "处理微信支付回调失败") } default: return errors.New(errors.CodeWechatCallbackInvalid, "不支持的支付渠道类型") } return h.wechatV2SuccessResponse(c) } func (h *PaymentHandler) dispatchPaymentRecordCallback(ctx context.Context, callback verifiedPaymentCallback) (bool, error) { if h.paymentStore != nil { payment, err := h.paymentStore.GetByPaymentNo(ctx, callback.PaymentNo) if err == nil { log, err := h.recordPaymentCallback(ctx, callback, payment) if err != nil { return true, err } var processErr error switch payment.OrderType { case model.PaymentOrderTypePackage: processErr = h.orderService.HandlePaymentRecordCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID, callback.Amount) case model.PaymentOrderTypeRecharge: if h.rechargeOrderService != nil { processErr = h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID) } else { processErr = errors.New(errors.CodeInternalError, "充值订单服务未配置") } case model.PaymentOrderTypeAgentRecharge: return true, h.confirmAgentRechargePayment(ctx, callback, log) default: processErr = errors.New(errors.CodeInvalidStatus, "未知支付记录类型") } completion := integrationlog.Completion{Result: constants.IntegrationResultSuccess, ResponseSummary: map[string]any{"confirmed": true}} if processErr != nil { completion.Result = constants.IntegrationResultFailed completion.SafeProviderMessage = "支付回调业务确认失败" completion.ResponseSummary = map[string]any{"confirmed": false} } else if current, currentErr := h.paymentStore.GetByPaymentNo(ctx, callback.PaymentNo); currentErr == nil { completion.StateChanged = payment.Status != model.PaymentRecordStatusPaid && current.Status == model.PaymentRecordStatusPaid } h.completePaymentCallbackLog(ctx, log, completion) return true, processErr } if err != gorm.ErrRecordNotFound { return true, errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") } } return false, nil } // dispatchWechatCallback 优先按支付单分发,旧单号继续按前缀兼容处理。 func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, callback verifiedPaymentCallback) error { handled, err := h.dispatchPaymentRecordCallback(ctx, callback) if handled || err != nil { return err } return h.dispatchLegacyPaymentCallback(ctx, callback) } func (h *PaymentHandler) dispatchLegacyPaymentCallback(ctx context.Context, callback verifiedPaymentCallback) error { if h.integration == nil { return errors.New(errors.CodeInvalidStatus, "支付回调 Integration Log 接缝未配置") } resourceKey, correlationID := callback.PaymentNo, callback.PaymentNo idempotencyKey := callback.TransactionID if idempotencyKey == "" { idempotencyKey = callback.PaymentNo } log, _, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{ IdempotencyKey: idempotencyKey, Provider: callback.Provider, Operation: constants.IntegrationOperationPaymentCallback, ExternalID: callback.TransactionID, ResourceType: constants.IntegrationResourceTypePayment, ResourceKey: &resourceKey, RawPayload: callback.RawPayload, ContentType: callback.ContentType, RequestID: pkgmiddleware.GetRequestIDFromContext(ctx), CorrelationID: &correlationID, }) if err != nil { return err } if err := h.preparePaymentCallbackRetry(ctx, log); err != nil { return err } var processErr error switch { case strings.HasPrefix(callback.PaymentNo, "ORD"): processErr = h.orderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.Amount) case strings.HasPrefix(callback.PaymentNo, constants.AssetRechargeOrderPrefix): if h.rechargeOrderService != nil { processErr = h.rechargeOrderService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID) } else { processErr = errors.New(errors.CodeInternalError, "充值订单服务未配置") } case strings.HasPrefix(callback.PaymentNo, constants.AgentRechargeOrderPrefix): if h.agentRechargeService != nil { processErr = h.agentRechargeService.HandlePaymentCallback(ctx, callback.PaymentNo, callback.PaymentMethod, callback.TransactionID, callback.Amount) } else { processErr = errors.New(errors.CodeInternalError, "代理充值服务未配置") } default: processErr = errors.New(errors.CodeInvalidStatus, "未知订单号前缀") } completion := integrationlog.Completion{Result: constants.IntegrationResultSuccess, ResponseSummary: map[string]any{"confirmed": processErr == nil}} if processErr != nil { completion.Result = constants.IntegrationResultFailed completion.SafeProviderMessage = "旧支付回调业务确认失败" } h.completePaymentCallbackLog(ctx, log, completion) return processErr } func (h *PaymentHandler) confirmAgentRechargePayment(ctx context.Context, callback verifiedPaymentCallback, log *model.IntegrationLog) error { if h.agentPaymentConfirm == nil || h.integration == nil { return errors.New(errors.CodeInternalError, "代理充值支付回调能力未配置") } correlationID := callback.PaymentNo ctx = auditcontext.With(ctx, auditcontext.Context{CorrelationID: correlationID}) linkage := auditcontext.From(ctx) // 富友本质是微信支付上游通道,回调渠道归一化为业务方式 wechat 后再进入确认用例。 paymentMethod := callback.PaymentMethod if paymentMethod == model.ProviderTypeFuiou { paymentMethod = constants.RechargeMethodWechat } result, confirmErr := h.agentPaymentConfirm.Execute(ctx, agentrechargeApp.ConfirmOnlinePaymentCommand{ PaymentNo: callback.PaymentNo, PaymentMethod: paymentMethod, ConfigID: callback.ConfigID, MerchantIdentity: callback.MerchantIdentity, ThirdPartyTradeNo: callback.TransactionID, Amount: callback.Amount, PaidAt: callback.PaidAt, RequestID: linkage.RequestID, CorrelationID: correlationID, ParentEventID: linkage.ParentEventID, }) if confirmErr != nil { h.logger.Error("代理充值支付确认失败", zap.String("integration_id", log.IntegrationID), zap.String("payment_no", callback.PaymentNo), zap.Error(confirmErr), ) h.completePaymentCallbackLog(ctx, log, integrationlog.Completion{ Result: constants.IntegrationResultFailed, SafeProviderMessage: "代理充值支付确认失败", ResponseSummary: map[string]any{"confirmed": false}, }) return confirmErr } if log.Result == constants.IntegrationResultPending { resolvedResourceID := strconv.FormatUint(uint64(result.PaymentID), 10) _, err := h.integration.Complete(ctx, log.IntegrationID, integrationlog.Completion{ Result: constants.IntegrationResultSuccess, ProviderCode: "SUCCESS", ResponseSummary: map[string]any{"confirmed": true, "already_confirmed": result.AlreadyConfirmed}, StateChanged: !result.AlreadyConfirmed, ResourceID: &resolvedResourceID, }) if err != nil { return err } } return nil } func (h *PaymentHandler) recordPaymentCallback(ctx context.Context, callback verifiedPaymentCallback, payment *model.Payment) (*model.IntegrationLog, error) { if h.integration == nil || payment == nil { return nil, errors.New(errors.CodeInvalidStatus, "支付回调 Integration Log 接缝未配置") } resourceID, resourceKey, correlationID := strconv.FormatUint(uint64(payment.ID), 10), payment.PaymentNo, payment.PaymentNo idempotencyKey := callback.TransactionID if idempotencyKey == "" { idempotencyKey = callback.PaymentNo } ctx = auditcontext.With(ctx, auditcontext.Context{CorrelationID: correlationID}) log, _, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{ IdempotencyKey: idempotencyKey, Provider: callback.Provider, Operation: constants.IntegrationOperationPaymentCallback, ExternalID: callback.TransactionID, ResourceType: constants.IntegrationResourceTypePayment, ResourceID: &resourceID, ResourceKey: &resourceKey, RawPayload: callback.RawPayload, ContentType: callback.ContentType, RequestID: pkgmiddleware.GetRequestIDFromContext(ctx), CorrelationID: &correlationID, }) if err != nil { return nil, err } if err := h.preparePaymentCallbackRetry(ctx, log); err != nil { return nil, err } return log, nil } func (h *PaymentHandler) preparePaymentCallbackRetry(ctx context.Context, log *model.IntegrationLog) error { if log == nil || log.Result != constants.IntegrationResultFailed { return nil } claimed, err := h.integration.ClaimFailedInbound(ctx, log.IntegrationID) if err != nil { return err } if claimed { log.Result = constants.IntegrationResultPending } return nil } func (h *PaymentHandler) recordIgnoredPaymentCallback(ctx context.Context, callback verifiedPaymentCallback, providerCode string) error { if h.integration == nil { return errors.New(errors.CodeInvalidStatus, "支付回调 Integration Log 接缝未配置") } resourceKey, correlationID := callback.PaymentNo, callback.PaymentNo idempotencyKey := callback.PaymentNo + ":" + providerCode log, _, err := h.integration.RecordInbound(ctx, integrationlog.InboundAttempt{ IdempotencyKey: idempotencyKey, Provider: callback.Provider, Operation: constants.IntegrationOperationPaymentCallback, ExternalID: callback.TransactionID, ResourceType: constants.IntegrationResourceTypePayment, ResourceKey: &resourceKey, RawPayload: callback.RawPayload, ContentType: callback.ContentType, RequestID: pkgmiddleware.GetRequestIDFromContext(ctx), CorrelationID: &correlationID, }) if err != nil || log.Result != constants.IntegrationResultPending { return err } _, err = h.integration.Complete(ctx, log.IntegrationID, integrationlog.Completion{ Result: constants.IntegrationResultIgnored, ProviderCode: providerCode, ResponseSummary: map[string]any{"confirmed": false}, }) return err } func (h *PaymentHandler) completePaymentCallbackLog(ctx context.Context, log *model.IntegrationLog, completion integrationlog.Completion) { if log == nil || log.Result != constants.IntegrationResultPending { return } if _, err := h.integration.Complete(ctx, log.IntegrationID, completion); err != nil { h.logger.Error("支付回调 Integration Log 终结失败", zap.String("integration_id", log.IntegrationID), zap.Error(err), ) } } // wechatV2SuccessResponse 返回微信 v2 要求的 XML 格式成功响应 // v2 API 要求 XML 格式的 return_code=SUCCESS,否则微信会持续重试回调 func (h *PaymentHandler) wechatV2SuccessResponse(c *fiber.Ctx) error { c.Set("Content-Type", "text/xml; charset=utf-8") return c.SendString(``) } // AlipayCallback 支付宝异步回调(带验签与多维度安全校验) // POST /api/callback/alipay func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error { ctx := c.UserContext() // 从原始请求体解析 form 参数(x-www-form-urlencoded) values, err := url.ParseQuery(string(c.Body())) if err != nil { h.logger.Error("支付宝回调:解析 form 参数失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam, "回调数据格式错误") } // 未验签前只读取 out_trade_no,用于回溯创建支付单时所用的配置 outTradeNo := values.Get("out_trade_no") if outTradeNo == "" { h.logger.Error("支付宝回调:out_trade_no 为空") return errors.New(errors.CodeInvalidParam, "订单号不能为空") } // 按 payment_config_id 加载创建支付单时所用的配置(支持已停用配置) cfg, err := h.wechatConfigService.GetConfigForCallback(ctx, outTradeNo) if err != nil || cfg == nil { h.logger.Error("支付宝回调:加载支付配置失败", zap.String("out_trade_no", outTradeNo), zap.Error(err), ) return errors.New(errors.CodeWechatCallbackInvalid, "支付配置不可用") } ctx = paymentCallbackContext(ctx, constants.IntegrationProviderAlipay) // 使用支付宝公钥验签(DecodeNotification 内部完成签名校验) notification, err := alipay.DecodeNotification(ctx, cfg, values) if err != nil { h.logger.Error("支付宝回调:验签失败", zap.String("out_trade_no", outTradeNo), zap.Uint("config_id", cfg.ID), zap.Error(err), ) return errors.New(errors.CodeWechatCallbackInvalid, "支付宝回调验签失败") } // 校验 app_id 与配置一致,防止跨商户混用 if notification.AppId != cfg.AliAppID { h.logger.Error("支付宝回调:app_id 不匹配", zap.String("out_trade_no", outTradeNo), zap.String("notify_app_id", notification.AppId), zap.String("config_app_id", cfg.AliAppID), ) return errors.New(errors.CodeWechatCallbackInvalid, "支付宝 app_id 校验失败") } // 非终态交易(如 WAIT_BUYER_PAY、TRADE_CLOSED)直接返回 success,不推进业务 tradeStatus := string(notification.TradeStatus) if tradeStatus != "TRADE_SUCCESS" && tradeStatus != "TRADE_FINISHED" { h.logger.Info("支付宝回调:非成功交易状态,忽略", zap.String("out_trade_no", outTradeNo), zap.String("trade_status", tradeStatus), ) if err := h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{ PaymentNo: outTradeNo, TransactionID: notification.TradeNo, Provider: constants.IntegrationProviderAlipay, RawPayload: c.Body(), ContentType: c.Get("Content-Type"), }, tradeStatus); err != nil { return err } return c.SendString("success") } // 查询支付记录 payment, err := h.paymentStore.GetByPaymentNo(ctx, outTradeNo) if err != nil { if err == gorm.ErrRecordNotFound { h.logger.Error("支付宝回调:支付记录不存在", zap.String("out_trade_no", outTradeNo), ) return errors.New(errors.CodeWechatCallbackInvalid, "支付记录不存在") } h.logger.Error("支付宝回调:查询支付记录失败", zap.String("out_trade_no", outTradeNo), zap.Error(err), ) return errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") } // 校验支付方式必须为 alipay,防止其他通道的 payment_no 被误用 if payment.PaymentMethod != model.PaymentByAlipay { h.logger.Error("支付宝回调:支付方式不匹配", zap.String("out_trade_no", outTradeNo), zap.String("payment_method", payment.PaymentMethod), ) return errors.New(errors.CodeWechatCallbackInvalid, "支付通道校验失败") } // 校验 payment_config_id 与当前配置一致;旧数据为空时兼容并记录 warn if payment.PaymentConfigID == nil { h.logger.Warn("支付宝回调:payment_config_id 为空,跳过配置 ID 校验(旧数据兼容)", zap.String("out_trade_no", outTradeNo), zap.Uint("config_id", cfg.ID), ) } else if *payment.PaymentConfigID != cfg.ID { h.logger.Error("支付宝回调:payment_config_id 不匹配", zap.String("out_trade_no", outTradeNo), zap.Uint("payment_config_id", *payment.PaymentConfigID), zap.Uint("callback_config_id", cfg.ID), ) return errors.New(errors.CodeWechatCallbackInvalid, "支付配置 ID 校验失败") } // 校验金额:使用 total_amount(原始订单金额)而非 buyer_pay_amount, // 避免优惠券/积分场景导致内部订单金额被错误记低 notifyAmountFen, err := alipay.YuanToFen(notification.TotalAmount) if err != nil { h.logger.Error("支付宝回调:total_amount 转换失败", zap.String("out_trade_no", outTradeNo), zap.String("total_amount", notification.TotalAmount), zap.Error(err), ) return errors.New(errors.CodeWechatCallbackInvalid, "支付金额格式错误") } if notifyAmountFen != payment.Amount { h.logger.Error("支付宝回调:金额不一致", zap.String("out_trade_no", outTradeNo), zap.Int64("notify_amount_fen", notifyAmountFen), zap.Int64("payment_amount_fen", payment.Amount), ) return errors.New(errors.CodeWechatCallbackInvalid, "支付金额校验失败") } callback := verifiedPaymentCallback{ PaymentNo: outTradeNo, PaymentMethod: model.PaymentByAlipay, TransactionID: notification.TradeNo, Amount: notifyAmountFen, ConfigID: cfg.ID, MerchantIdentity: notification.AppId, Provider: constants.IntegrationProviderAlipay, RawPayload: c.Body(), ContentType: c.Get("Content-Type"), } if payment.OrderType == model.PaymentOrderTypeAgentRecharge { paidAt, parseErr := time.ParseInLocation("2006-01-02 15:04:05", notification.GmtPayment, time.Local) if parseErr != nil { h.logger.Error("支付宝回调:付款时间格式无效", zap.String("out_trade_no", outTradeNo), zap.Error(parseErr), ) return errors.New(errors.CodeWechatCallbackInvalid, "支付宝付款时间格式错误") } callback.PaidAt = paidAt } if handled, dispatchErr := h.dispatchPaymentRecordCallback(ctx, callback); dispatchErr != nil { h.logger.Error("支付宝回调:确认支付失败", zap.String("out_trade_no", outTradeNo), zap.Error(dispatchErr)) return errors.Wrap(errors.CodeInternalError, dispatchErr, "处理支付宝支付回调失败") } else if !handled { return errors.New(errors.CodeInternalError, "支付记录分发失败") } return c.SendString("success") } // FuiouPayCallback 富友支付回调(带签名验证) // POST /api/callback/fuiou-pay func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error { body, bodySource := fuiouCallbackPayload(c) if strings.TrimSpace(string(body)) == "" { return errors.New(errors.CodeFuiouCallbackInvalid, "回调请求体为空") } ctx := c.UserContext() c.Set("Content-Type", "text/plain; charset=utf-8") // 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置 preNotify, err := fuiou.ParseNotify(body) if err != nil { h.logger.Error("富友回调:预解析失败", zap.Error(err), zap.String("payload_source", bodySource), zap.Int("raw_body_len", len(c.Body())), zap.Int("payload_len", len(body)), zap.String("content_type", c.Get("Content-Type")), zap.String("query", string(c.Request().URI().QueryString())), ) return c.Send(fuiou.BuildNotifyFailResponse("parse failed")) } cfg, err := h.wechatConfigService.GetConfigForCallback(ctx, preNotify.MchntOrderNo) if err != nil || cfg == nil { h.logger.Error("富友回调:加载支付配置失败", zap.String("order_no", preNotify.MchntOrderNo), zap.Error(err), ) return c.Send(fuiou.BuildNotifyFailResponse("payment config unavailable")) } ctx = paymentCallbackContext(ctx, model.ProviderTypeFuiou) if cfg.ProviderType != model.ProviderTypeFuiou || strings.TrimSpace(preNotify.InsCd) != strings.TrimSpace(cfg.FyInsCd) || strings.TrimSpace(preNotify.MchntCd) != strings.TrimSpace(cfg.FyMchntCd) { h.logger.Error("富友回调:支付配置与回调商户不匹配", zap.String("order_no", preNotify.MchntOrderNo), zap.Uint("config_id", cfg.ID), zap.String("config_name", cfg.Name), zap.String("callback_ins_cd", preNotify.InsCd), zap.String("config_ins_cd", cfg.FyInsCd), zap.String("callback_mchnt_cd", preNotify.MchntCd), zap.String("config_mchnt_cd", cfg.FyMchntCd), zap.String("provider_type", cfg.ProviderType), ) return c.Send(fuiou.BuildNotifyFailResponse("payment config mismatch")) } client, err := fuiou.NewClient( cfg.FyInsCd, cfg.FyMchntCd, cfg.FyTermID, cfg.FyAPIURL, cfg.FyNotifyURL, cfg.FyPrivateKey, cfg.FyPublicKey, h.logger, ) if err != nil { h.logger.Error("富友回调:构造客户端失败", zap.Error(err)) return c.Send(fuiou.BuildNotifyFailResponse("client init failed")) } notify, err := client.VerifyNotify(body) if err != nil { if notify != nil { h.logger.Warn("富友回调:非成功结果", zap.String("result_code", notify.ResultCode), zap.String("result_msg", notify.ResultMsg)) if recordErr := h.recordIgnoredPaymentCallback(ctx, verifiedPaymentCallback{ PaymentNo: notify.MchntOrderNo, TransactionID: notify.TransactionId, Provider: constants.IntegrationProviderFuiou, RawPayload: body, ContentType: c.Get("Content-Type"), }, notify.ResultCode); recordErr != nil { return c.Send(fuiou.BuildNotifyFailResponse("integration log failed")) } return c.Send(fuiou.BuildNotifySuccessResponse()) } h.logger.Error("富友回调:验签或解析失败", zap.Error(err), zap.String("order_no", preNotify.MchntOrderNo), zap.Uint("config_id", cfg.ID), zap.String("config_name", cfg.Name), zap.String("callback_ins_cd", preNotify.InsCd), zap.String("config_ins_cd", cfg.FyInsCd), zap.String("callback_mchnt_cd", preNotify.MchntCd), zap.String("config_mchnt_cd", cfg.FyMchntCd), ) return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) } orderNo := notify.MchntOrderNo // OrderAmt 为字符串格式的分,解析失败则降级为 0 orderAmt, _ := strconv.ParseInt(notify.OrderAmt, 10, 64) paidAt, _ := time.ParseInLocation("20060102150405", notify.TxnFinTs, time.Local) if handled, err := h.dispatchPaymentRecordCallback(ctx, verifiedPaymentCallback{ PaymentNo: orderNo, PaymentMethod: "fuiou", TransactionID: notify.TransactionId, Amount: orderAmt, ConfigID: cfg.ID, MerchantIdentity: cfg.FyMchntCd, PaidAt: paidAt, Provider: constants.IntegrationProviderFuiou, RawPayload: body, ContentType: c.Get("Content-Type"), }); err != nil { return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) } else if handled { return c.Send(fuiou.BuildNotifySuccessResponse()) } if err := h.dispatchLegacyPaymentCallback(ctx, verifiedPaymentCallback{ PaymentNo: orderNo, PaymentMethod: model.ProviderTypeFuiou, TransactionID: notify.TransactionId, Amount: orderAmt, Provider: constants.IntegrationProviderFuiou, RawPayload: body, ContentType: c.Get("Content-Type"), }); err != nil { return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) } return c.Send(fuiou.BuildNotifySuccessResponse()) } func paymentCallbackContext(ctx context.Context, provider string) context.Context { return auditcontext.With(ctx, auditcontext.Context{ ActorKind: constants.AuditActorExternalSystem, ActorID: provider, ActorName: provider, Source: constants.AuditSourceCallback, }) } // fuiouCallbackPayload 提取富友回调载荷,兼容 body、form req 和 query req 三种来源。 func fuiouCallbackPayload(c *fiber.Ctx) ([]byte, string) { if req := strings.TrimSpace(c.FormValue("req")); req != "" { return []byte(req), "form:req" } if req := strings.TrimSpace(c.Query("req")); req != "" { return []byte(req), "query:req" } return c.Body(), "body" }