PAYATHON 2026

手机网站支付能正常拉起,但没有回调怎么办?

支付小周

结论

支付页面能正常拉起,只能说明下单和跳转流程基本正常,并不代表异步通知配置没有问题。支付完成后收不到回调,可以依次检查 notify_url 配置、公网连通性、HTTP 响应状态、签名验证和业务处理日志。

如果这里的“手机网站支付”是指支付宝手机网站支付,还要分清两个地址的用途:

  • return_url 是支付完成后浏览器跳转的同步地址,不能用来可靠判断支付是否成功。
  • notify_url 是支付宝服务器发送支付结果的异步通知地址。订单状态应以异步通知或主动查询结果为准。
  • 异步通知处理成功后,接口应直接返回纯文本 success,否则支付平台可能继续重试。

先确认回调地址是否配置正确

创建支付请求时,需要明确传入公网可访问的 notify_url,不要把 return_url 当作异步回调地址。

AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();

request.setReturnUrl("https://www.example.com/pay/return");
request.setNotifyUrl("https://api.example.com/pay/alipay/notify");

request.setBizContent("""
{
  "out_trade_no": "ORDER_202609130001",
  "total_amount": "99.00",
  "subject": "订单支付",
  "product_code": "QUICK_WAP_WAY"
}
""");

排查时要核对实际提交给支付平台的请求参数,不能只看配置文件。常见问题有:

  • 没有设置 notify_url,或者传入的是空字符串。
  • 修改配置后没有重启应用,程序仍在使用旧地址。
  • 测试环境和生产环境读取了不同的配置。
  • 回调地址拼写错误、路径缺少前缀或域名解析异常。
  • notify_url 使用了 localhost、127.0.0.1、局域网 IP 或临时内网地址。
  • 网关、负载均衡或反向代理改写了回调路径。

检查回调接口能否从公网访问

支付平台的服务器需要主动请求 notify_url。这个地址通常应满足以下条件:

  • 使用公网域名。
  • DNS 能够正常解析。
  • HTTPS 证书有效,证书链完整。
  • 服务器监听端口已经开放。
  • 防火墙、安全组、WAF 和网关没有拦截支付平台的请求。
  • 接口不要求登录,也不依赖浏览器 Cookie。
  • 接口没有验证码、CSRF 校验或前端鉴权。
  • HTTP 请求不会被错误重定向。

可以从外部网络测试接口是否可达:

curl -i -X POST \
  "https://api.example.com/pay/alipay/notify" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "out_trade_no=test"

这个命令只能测试网络和路由,无法模拟一条能够通过验签的真实支付通知。测试时主要看下面这些结果:

  • 404:回调路径不存在或路由配置错误。
  • 401、403:请求被鉴权、CSRF、WAF 或访问控制拦截。
  • 301、302:请求发生了重定向,建议将 notify_url 直接配置为最终地址。
  • 405:接口不接受 POST 请求。
  • 500:回调处理代码出现异常。
  • 请求超时:可能是网络、防火墙、数据库或业务处理时间过长。

如果服务部署在反向代理后面,还要查看 Nginx、网关和应用服务器的访问日志,确定请求停在哪一层。

确认接口按表单方式接收参数

支付宝异步通知通常通过 POST 表单参数发送,不是 JSON 请求体。接口如果只用 @RequestBody 接收 JSON,可能无法取得通知参数。

Spring Boot 可以这样接收参数并验签:

@PostMapping(
    value = "/pay/alipay/notify",
    consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public String alipayNotify(HttpServletRequest request) {
    Map<String, String> params = new HashMap<>();

    request.getParameterMap().forEach((key, values) -> {
        if (values != null && values.length > 0) {
            params.put(key, String.join(",", values));
        }
    });

    try {
        boolean verified = AlipaySignature.rsaCheckV1(
            params,
            alipayPublicKey,
            "UTF-8",
            "RSA2"
        );

        if (!verified) {
            return "failure";
        }

        String appId = params.get("app_id");
        String outTradeNo = params.get("out_trade_no");
        String tradeNo = params.get("trade_no");
        String tradeStatus = params.get("trade_status");
        String totalAmount = params.get("total_amount");
        String sellerId = params.get("seller_id");

        // 校验 app_id、seller_id、订单号、订单金额等业务字段。
        // 确认无误后,以幂等方式更新订单状态。

        if ("TRADE_SUCCESS".equals(tradeStatus)
                || "TRADE_FINISHED".equals(tradeStatus)) {
            markOrderPaidIdempotently(outTradeNo, tradeNo, totalAmount);
        }

        return "success";
    } catch (AlipayApiException e) {
        // 记录异常,但不要记录私钥等敏感信息。
        return "failure";
    }
}

示例中的公钥、字符集和签名类型必须与实际接入配置一致。不同 SDK 版本的方法签名可能不同,应以项目当前使用的 SDK 接口为准。

排查签名配置

请求已经到达,但验签失败,也可能被误判为“没有回调”。在不泄露敏感信息的前提下,日志中应记录:

  • 回调到达时间。
  • out_trade_no。
  • trade_no。
  • trade_status。
  • app_id。
  • sign_type。
  • 验签结果。
  • 异常类型和堆栈。
  • 最终返回的 HTTP 状态码和响应正文。

检查时注意以下问题:

  • 使用的是支付宝公钥,而不是应用公钥。
  • 当前环境使用的支付宝公钥与对应应用一致。
  • 没有混用沙箱环境和生产环境的密钥。
  • 使用公钥证书模式时,验签方式与证书模式匹配。
  • charset 和 sign_type 与接入配置一致。
  • 验签前没有擅自删除、改名或重新编码通知参数。
  • 没有对已经解码的参数再次执行 URL Decode。
  • 网关、中间件或字符过滤器没有修改参数值。

不能因为验签失败就跳过验签。关闭验签后,攻击者可能伪造支付成功通知。

正确处理并返回 success

回调业务处理成功后,接口应返回以下纯文本:

success

不要返回 JSON、HTML 页面或其他经过包装的内容。例如,下面这种响应通常不符合要求:

{"code":200,"message":"success"}

如果项目统一使用响应包装器,需要把支付回调接口排除在包装逻辑之外。同时检查异常处理器、字符编码过滤器和网关有没有改写响应内容。

Spring Boot 可以明确指定返回类型:

@PostMapping(
    value = "/pay/alipay/notify",
    produces = MediaType.TEXT_PLAIN_VALUE
)
@ResponseBody
public String notify(HttpServletRequest request) {
    // 验签和业务处理
    return "success";
}

只有验签通过、订单信息核对无误,并且业务状态已经可靠保存后,才能返回 success。如果订单状态保存失败,应返回 failure 或非成功响应,让支付平台按其重试机制再次发送通知。

检查业务代码是否中途失败

即使回调请求已经到达,下面这些问题仍可能导致订单没有更新:

  • 数据库连接失败或事务回滚。
  • 根据 out_trade_no 找不到订单。
  • 使用 double 比较金额,产生精度误差。
  • 订单已经处理,重复更新时抛出异常。
  • 消息队列发送失败后直接终止请求。
  • 回调线程等待第三方接口,最终超时。
  • 全局异常处理器吞掉了业务异常。
  • 订单更新成功后,又被后续逻辑回滚。

金额应使用 BigDecimal 比较:

BigDecimal notifiedAmount = new BigDecimal(params.get("total_amount"));
BigDecimal orderAmount = order.getPayAmount();

if (notifiedAmount.compareTo(orderAmount) != 0) {
    throw new IllegalStateException("支付金额与订单金额不一致");
}

回调处理还要支持幂等。支付平台可能重复发送同一笔交易的通知,程序不能因此重复发货、充值或记账。

@Transactional
public void markOrderPaidIdempotently(
        String outTradeNo,
        String tradeNo,
        String totalAmount
) {
    Order order = orderRepository.findByOrderNoForUpdate(outTradeNo)
        .orElseThrow(() -> new IllegalStateException("订单不存在"));

    if (order.isPaid()) {
        return;
    }

    BigDecimal notifiedAmount = new BigDecimal(totalAmount);
    if (notifiedAmount.compareTo(order.getPayAmount()) != 0) {
        throw new IllegalStateException("支付金额不一致");
    }

    order.markPaid(tradeNo);
    orderRepository.save(order);
}

使用主动查询补偿订单状态

网络抖动、服务发布或短暂故障都可能让异步通知延迟,因此订单状态不能只依赖单次回调。对于长时间停留在“支付中”的订单,可以调用支付查询接口进行补偿确认。

以支付宝交易查询为例:

AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent("""
{
  "out_trade_no": "ORDER_202609130001"
}
""");

AlipayTradeQueryResponse response = alipayClient.execute(request);

if (response.isSuccess()) {
    String tradeStatus = response.getTradeStatus();

    if ("TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus)) {
        // 再次核对订单号、金额、收款方等信息后,幂等更新订单。
    }
}

主动查询可用于以下情况:

  • 用户已经返回商户页面,但本地订单仍显示未支付。
  • 异步通知暂时没有到达。
  • 回调处理失败,需要定时补偿。
  • 客服需要确认某笔订单的实际支付状态。

用户通过 return_url 返回前端页面后,可以显示“支付结果确认中”,再由后端查询本地订单状态。不要根据前端参数直接把订单改成已支付。

推荐的排查顺序

  1. 查看支付平台是否记录了异步通知及其处理结果。
  2. 核对实际支付请求中的 notify_url。
  3. 从公网测试域名、证书、端口和路由。
  4. 查看 CDN、WAF、Nginx、网关和应用访问日志。
  5. 确认接口能够接收 POST 表单参数。
  6. 检查请求是否已经到达,但验签失败。
  7. 检查订单校验、事务和幂等处理。
  8. 确认处理成功后返回的是纯文本 success。
  9. 对仍处于处理状态的订单调用交易查询接口补偿。

如果服务器各层日志中都找不到回调请求,先检查 notify_url 和公网连通性。如果日志中有请求,但订单没有更新,则应检查验签、业务校验、事务和接口响应内容。

备注:内容仅供参考。