package callback import ( "context" "fmt" "net/http" "net/url" "strconv" "strings" "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp/fasthttpadaptor" "go.uber.org/zap" "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/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/fuiou" "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) 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 logger *zap.Logger } func NewPaymentHandler( orderService *orderService.Service, rechargeService *rechargeService.Service, rechargeOrderService *rechargeOrderSvc.Service, agentRechargeService AgentRechargeServiceInterface, wechatPayment wechat.PaymentServiceInterface, wechatConfigService WechatConfigServiceInterface, paymentStore *postgres.PaymentStore, logger *zap.Logger, ) *PaymentHandler { return &PaymentHandler{ orderService: orderService, rechargeService: rechargeService, rechargeOrderService: rechargeOrderService, agentRechargeService: agentRechargeService, wechatPayment: wechatPayment, wechatConfigService: wechatConfigService, paymentStore: paymentStore, logger: logger, } } // WechatPayCallback 微信支付回调(带签名验证) // POST /api/callback/wechat-pay func (h *PaymentHandler) WechatPayCallback(c *fiber.Ctx) error { body := c.Body() ctx := context.Background() // 预解析订单号(不验签),用于按 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, "微信支付服务未配置") } 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" { return h.wechatV2SuccessResponse(c) } // TotalFee 为字符串格式的分,解析失败则降级为 0(后续 handlePaymentCallback 会记录日志) totalFee, _ := strconv.ParseInt(result.TotalFee, 10, 64) if err := h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, totalFee); 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 nil } return h.dispatchWechatCallback(ctx, result.OutTradeNo, result.TransactionID, result.TotalAmount) }) 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, paymentNo, paymentMethod, transactionID string, paidAmount int64) (bool, error) { if h.paymentStore != nil { payment, err := h.paymentStore.GetByPaymentNo(ctx, paymentNo) if err == nil { switch payment.OrderType { case model.PaymentOrderTypePackage: return true, h.orderService.HandlePaymentRecordCallback(ctx, paymentNo, paymentMethod, transactionID, paidAmount) case model.PaymentOrderTypeRecharge: if h.rechargeOrderService != nil { return true, h.rechargeOrderService.HandlePaymentCallback(ctx, paymentNo, paymentMethod, transactionID) } return true, fmt.Errorf("充值订单服务未配置,无法处理支付单: %s", paymentNo) default: return true, fmt.Errorf("未知支付记录类型: %s", payment.OrderType) } } if err != gorm.ErrRecordNotFound { return true, errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") } } return false, nil } // dispatchWechatCallback 优先按支付单分发,旧单号继续按前缀兼容处理。 func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, transactionID string, paidAmount int64) error { handled, err := h.dispatchPaymentRecordCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID, paidAmount) if handled || err != nil { return err } switch { case strings.HasPrefix(outTradeNo, "ORD"): return h.orderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, paidAmount) case strings.HasPrefix(outTradeNo, constants.AssetRechargeOrderPrefix): if h.rechargeOrderService != nil { return h.rechargeOrderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentByWechat, transactionID) } return fmt.Errorf("充值订单服务未配置,无法处理订单: %s", outTradeNo) case strings.HasPrefix(outTradeNo, constants.AgentRechargeOrderPrefix): if h.agentRechargeService != nil { return h.agentRechargeService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentMethodWechat, transactionID) } return fmt.Errorf("代理充值服务未配置,无法处理订单: %s", outTradeNo) default: return fmt.Errorf("未知订单号前缀: %s", outTradeNo) } } // 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, "支付配置不可用") } // 使用支付宝公钥验签(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), ) 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, "支付金额校验失败") } // 写入支付宝交易流水号 if err := h.paymentStore.UpdatePaymentInfo(ctx, payment.ID, notification.TradeNo, nil); err != nil { h.logger.Error("支付宝回调:写入 third_party_trade_no 失败", zap.String("out_trade_no", outTradeNo), zap.String("trade_no", notification.TradeNo), zap.Error(err), ) // 不中断业务,继续分发(幂等业务层会处理状态) } // 按支付单 order_type 分发业务 switch payment.OrderType { case model.PaymentOrderTypePackage: if err := h.orderService.HandlePaymentRecordCallback(ctx, outTradeNo, model.PaymentByAlipay, notification.TradeNo, payment.Amount); err != nil { h.logger.Error("支付宝回调:推进套餐订单失败", zap.String("out_trade_no", outTradeNo), zap.String("trade_no", notification.TradeNo), zap.Error(err), ) return errors.Wrap(errors.CodeInternalError, err, "处理支付宝支付回调失败") } h.logger.Info("支付宝回调:套餐订单支付成功", zap.String("out_trade_no", outTradeNo), zap.String("trade_no", notification.TradeNo), zap.String("order_type", payment.OrderType), ) case model.PaymentOrderTypeRecharge: if h.rechargeOrderService == nil { h.logger.Error("支付宝回调:充值订单服务未配置", zap.String("out_trade_no", outTradeNo), ) return errors.New(errors.CodeInternalError, "充值订单服务未配置") } if err := h.rechargeOrderService.HandlePaymentCallback(ctx, outTradeNo, model.PaymentByAlipay, notification.TradeNo); err != nil { h.logger.Error("支付宝回调:推进充值订单失败", zap.String("out_trade_no", outTradeNo), zap.String("trade_no", notification.TradeNo), zap.Error(err), ) return errors.Wrap(errors.CodeInternalError, err, "处理支付宝充值回调失败") } h.logger.Info("支付宝回调:充值订单支付成功", zap.String("out_trade_no", outTradeNo), zap.String("trade_no", notification.TradeNo), zap.String("order_type", payment.OrderType), ) default: h.logger.Error("支付宝回调:未知支付记录类型", zap.String("out_trade_no", outTradeNo), zap.String("order_type", payment.OrderType), ) 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")) } 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)) 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) if handled, err := h.dispatchPaymentRecordCallback(ctx, orderNo, "fuiou", notify.TransactionId, orderAmt); err != nil { return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) } else if handled { return c.Send(fuiou.BuildNotifySuccessResponse()) } switch { case strings.HasPrefix(orderNo, "ORD"): if err := h.orderService.HandlePaymentCallback(ctx, orderNo, "fuiou", orderAmt); err != nil { return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) } case strings.HasPrefix(orderNo, constants.AssetRechargeOrderPrefix): if h.rechargeOrderService != nil { if err := h.rechargeOrderService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil { return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) } return c.Send(fuiou.BuildNotifySuccessResponse()) } return c.Send(fuiou.BuildNotifyFailResponse("充值订单服务未配置")) case strings.HasPrefix(orderNo, constants.AgentRechargeOrderPrefix): if h.agentRechargeService != nil { if err := h.agentRechargeService.HandlePaymentCallback(ctx, orderNo, "fuiou", notify.TransactionId); err != nil { return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) } } default: return c.Send(fuiou.BuildNotifyFailResponse("unknown order prefix")) } return c.Send(fuiou.BuildNotifySuccessResponse()) } // 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" }