From a03ba5d39645948f1c48ca0cfccc74c6edd1ccbf Mon Sep 17 00:00:00 2001 From: huang Date: Fri, 22 May 2026 12:09:50 +0800 Subject: [PATCH] =?UTF-8?q?task:=20=E5=AE=8C=E6=88=90=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E5=AE=9D=E5=9B=9E=E8=B0=83=E5=AE=89=E5=85=A8=EF=BC=88=E9=AA=8C?= =?UTF-8?q?=E7=AD=BE=E3=80=81=E5=A4=9A=E7=BB=B4=E5=BA=A6=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E3=80=81=E4=B8=9A=E5=8A=A1=E5=88=86=E5=8F=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/callback/payment.go | 209 ++++++++++++++++++++------- 1 file changed, 158 insertions(+), 51 deletions(-) diff --git a/internal/handler/callback/payment.go b/internal/handler/callback/payment.go index 67eeb03..89520ae 100644 --- a/internal/handler/callback/payment.go +++ b/internal/handler/callback/payment.go @@ -3,8 +3,8 @@ package callback import ( "context" "fmt" - "math" "net/http" + "net/url" "strconv" "strings" @@ -17,6 +17,7 @@ import ( 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" @@ -184,74 +185,180 @@ func (h *PaymentHandler) wechatV2SuccessResponse(c *fiber.Ctx) error { 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 支付宝回调 +// 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, "请求参数解析失败") + 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, "回调数据格式错误") } - if req.OrderNo == "" { + // 未验签前只读取 out_trade_no,用于回溯创建支付单时所用的配置 + outTradeNo := values.Get("out_trade_no") + if outTradeNo == "" { + h.logger.Error("支付宝回调:out_trade_no 为空") return errors.New(errors.CodeInvalidParam, "订单号不能为空") } - ctx := c.UserContext() - - // 优先取 buyer_pay_amount(买家实付,扣除优惠券后),回退到 total_amount - rawAmount := req.BuyerPayAmount - if rawAmount == "" { - rawAmount = req.TotalAmount + // 按 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, "支付配置不可用") } - paidAmount := parseYuanToFen(rawAmount) - handled, err := h.dispatchPaymentRecordCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, "", paidAmount) + // 使用支付宝公钥验签(DecodeNotification 内部完成签名校验) + notification, err := alipay.DecodeNotification(ctx, cfg, values) if err != nil { - return err + h.logger.Error("支付宝回调:验签失败", + zap.String("out_trade_no", outTradeNo), + zap.Uint("config_id", cfg.ID), + zap.Error(err), + ) + return errors.New(errors.CodeWechatCallbackInvalid, "支付宝回调验签失败") } - if handled { + + // 校验 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") } - // 按订单号前缀分发 - switch { - case strings.HasPrefix(req.OrderNo, "ORD"): - if err := h.orderService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentMethodAlipay, paidAmount); err != nil { - return err + // 查询支付记录 + 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, "支付记录不存在") } - case strings.HasPrefix(req.OrderNo, constants.AssetRechargeOrderPrefix): - if h.rechargeOrderService != nil { - return h.rechargeOrderService.HandlePaymentCallback(ctx, req.OrderNo, model.PaymentByAlipay, "") + 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, "处理支付宝支付回调失败") } - 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 - } + 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: - return errors.New(errors.CodeInvalidParam, "未知订单号前缀") + h.logger.Error("支付宝回调:未知支付记录类型", + zap.String("out_trade_no", outTradeNo), + zap.String("order_type", payment.OrderType), + ) + return errors.New(errors.CodeInternalError, "未知支付记录类型") } return c.SendString("success")