package callback import ( "context" "fmt" "math" "net/http" "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/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" ) // 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) } // dispatchWechatCallback 按订单号前缀将支付结果分发到对应业务 func (h *PaymentHandler) dispatchWechatCallback(ctx context.Context, outTradeNo, transactionID string, paidAmount int64) error { 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(``) } // parseYuanToFen 将支付宝元为单位的金额字符串转换为分 // 支付宝回调金额字段(buyer_pay_amount / total_amount)为字符串格式的元,如 "2.00" // 使用 math.Round 避免浮点运算精度误差 func parseYuanToFen(yuan string) int64 { if yuan == "" { return 0 } f, err := strconv.ParseFloat(yuan, 64) if err != nil { return 0 } return int64(math.Round(f * 100)) } type AlipayCallbackRequest struct { OrderNo string `json:"out_trade_no" form:"out_trade_no"` BuyerPayAmount string `json:"buyer_pay_amount" form:"buyer_pay_amount"` TotalAmount string `json:"total_amount" form:"total_amount"` } // AlipayCallback 支付宝回调 // POST /api/callback/alipay func (h *PaymentHandler) AlipayCallback(c *fiber.Ctx) error { var req AlipayCallbackRequest if err := c.BodyParser(&req); err != nil { return errors.New(errors.CodeInvalidParam, "请求参数解析失败") } if req.OrderNo == "" { return errors.New(errors.CodeInvalidParam, "订单号不能为空") } ctx := c.UserContext() // 优先取 buyer_pay_amount(买家实付,扣除优惠券后),回退到 total_amount rawAmount := req.BuyerPayAmount if rawAmount == "" { rawAmount = req.TotalAmount } paidAmount := parseYuanToFen(rawAmount) // 按订单号前缀分发 switch { case strings.HasPrefix(req.OrderNo, "ORD"): if err := h.orderService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, paidAmount); err != nil { return err } case strings.HasPrefix(req.OrderNo, constants.AssetRechargeOrderPrefix): if h.rechargeOrderService != nil { return h.rechargeOrderService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentByAlipay, "") } return fmt.Errorf("充值订单服务未配置,无法处理订单: %s", req.OrderNo) case strings.HasPrefix(req.OrderNo, constants.AgentRechargeOrderPrefix): if h.agentRechargeService != nil { if err := h.agentRechargeService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, ""); err != nil { return err } } default: return errors.New(errors.CodeInvalidParam, "未知订单号前缀") } return c.SendString("success") } // FuiouPayCallback 富友支付回调(带签名验证) // POST /api/callback/fuiou-pay func (h *PaymentHandler) FuiouPayCallback(c *fiber.Ctx) error { body := c.Body() if len(body) == 0 { return errors.New(errors.CodeFuiouCallbackInvalid, "回调请求体为空") } ctx := c.UserContext() c.Set("Content-Type", "text/xml; charset=gbk") // 预解析订单号(不验签),用于按 payment_config_id 加载创建订单时所用的配置 preNotify, err := fuiou.ParseNotify(body) if err != nil { h.logger.Error("富友回调:预解析失败", zap.Error(err)) 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")) } 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)) return c.Send(fuiou.BuildNotifyFailResponse(err.Error())) } orderNo := notify.MchntOrderNo // OrderAmt 为字符串格式的分,解析失败则降级为 0 orderAmt, _ := strconv.ParseInt(notify.OrderAmt, 10, 64) 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()) }