task: 完成支付宝回调安全(验签、多维度校验、业务分发)

This commit is contained in:
2026-05-22 12:09:50 +08:00
parent 56adc30409
commit b988f99a95

View File

@@ -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(`<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>`)
}
// 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")