PAYATHON 2026

扫码支付成功后无法收到 notify_url 回调通知

支付老李

结论

notify_url 是支付平台服务器主动发起异步通知的地址。外部浏览器能打开它,不等于支付平台可以成功发送回调。当前地址使用公网 IP、HTTP 和非标准端口 11111,还要逐项检查端口放行、反向代理、路由重定向、请求方法和签名处理等环节。

return_url 是浏览器端的同步跳转,只用于改善用户体验,不能作为支付成功的可靠依据。扫码支付时,如果用户在另一台设备上付款、中途关闭页面,或者支付产品本身不保证同步跳转,浏览器都可能不会进入 return_url。订单最终状态必须以异步通知或主动查询结果为准。

先确认 notify_url 能否真正接收回调

回调地址至少要满足以下条件:

  • 支付平台服务器可以从公网直接访问。
  • 接口支持平台要求的请求方法,支付宝通知通常使用 POST。
  • 接口不依赖登录态、Cookie、验证码或前端 JavaScript。
  • 请求不会被 CSRF、WAF、鉴权中间件或 IP 白名单拦截。
  • 地址不会发生 301、302、307、308 跳转。
  • 接口能在规定时间内返回平台要求的纯文本响应。
  • 服务端防火墙、安全组、NAT 和容器端口映射均配置正确。
  • 即使回调处理失败,服务端也会保留原始请求日志。

浏览器访问通常发出的是 GET 请求,而支付通知一般是携带表单参数的 POST 请求,两者不能视为同一种测试。

可以从公网机器模拟请求:

curl -i -X POST \
  'http://47.57.236.213:11111/paymentNotify/alipayNotify/' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'out_trade_no=TEST001&trade_status=TRADE_SUCCESS'

重点查看响应状态是否为 200,以及请求过程中是否发生跳转:

curl -v -X POST \
  'http://47.57.236.213:11111/paymentNotify/alipayNotify/' \
  --data 'test=1'

如果响应中出现 Location,说明接口发生了重定向。需要特别确认路径末尾的 / 是否与服务端路由完全一致。部分框架会把:

/paymentNotify/alipayNotify

重定向到:

/paymentNotify/alipayNotify/

支付平台未必会按预期跟随重定向。即使跟随,原始请求方法或请求体也可能发生变化。配置的地址应直接命中最终处理接口。

检查端口、安全组和反向代理

11111 是非标准 HTTP 端口。即使浏览器能够访问,也不能直接排除网络配置问题,还需要检查:

  1. 云服务器安全组是否允许公网访问 TCP 11111。
  2. 操作系统防火墙是否放行该端口。
  3. 应用是否监听 0.0.0.0:11111,而非仅监听 127.0.0.1。
  4. Docker 或其他容器环境是否正确映射端口。
  5. Nginx、网关或 WAF 是否允许匿名 POST 请求。
  6. 回调路径是否被统一登录、CSRF 或权限过滤器拦截。
  7. 服务是否会因为缺少特定请求头而返回 403、404 或 405。

更稳妥的部署方式是使用已备案或符合业务要求的域名,通过 HTTPS 的 443 端口接收通知,再由 Nginx 转发给内部应用:

server {
    listen 443 ssl;
    server_name pay.example.com;

    ssl_certificate     /path/to/fullchain.pem;
    ssl_certificate_key /path/to/private.key;

    location = /paymentNotify/alipayNotify/ {
        proxy_pass http://127.0.0.1:11111/paymentNotify/alipayNotify/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

对应配置可以改为:

https://pay.example.com/paymentNotify/alipayNotify/

是否强制使用 HTTPS、域名或特定端口,要根据实际支付产品的当前文档和商户后台限制判断。不过,从稳定性、安全性和证书校验等方面考虑,正式环境不宜直接使用公网 IP 加高位端口。

确认 notify_url 是否提交到了正确位置

需要确认 notify_url 已作为支付请求参数实际提交,而不是只存在于本地配置文件中,却没有传给 SDK 或 API。

生成支付请求前,可以记录以下信息:

out_trade_no
notify_url
return_url
product_code
请求接口名称
支付平台返回结果

不同支付产品对 return_url 和 notify_url 的支持范围可能不同。有些 SDK 通过独立方法设置通知地址,而不是把它放进业务参数 biz_content。

以常见的 Java SDK 调用方式为例,具体类名要以实际使用的接口为准:

AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();

request.setNotifyUrl(
    "https://pay.example.com/paymentNotify/alipayNotify/"
);
request.setReturnUrl(
    "https://www.example.com/pay/result/"
);

request.setBizContent(bizContent);

不要只在前端传递 notify_url。异步通知地址应由可信的服务端配置,避免客户端篡改。

回调接口必须接收表单参数

支付宝异步通知通常以 application/x-www-form-urlencoded 格式提交参数。如果接口只接收 JSON,例如仅使用 @RequestBody 解析 JSON,就可能无法取得通知参数。

Spring Boot 可以按表单参数接收:

@PostMapping(
    value = "/paymentNotify/alipayNotify/",
    consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public ResponseEntity<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));
        }
    });

    // 先记录通知到达情况。生产环境注意脱敏。
    log.info(
        "Alipay notify received, out_trade_no={}, trade_status={}",
        params.get("out_trade_no"),
        params.get("trade_status")
    );

    try {
        boolean signatureValid = verifyAlipaySignature(params);
        if (!signatureValid) {
            log.warn(
                "Invalid Alipay signature, out_trade_no={}",
                params.get("out_trade_no")
            );
            return ResponseEntity.ok("failure");
        }

        boolean processed = processPaymentNotify(params);
        return ResponseEntity.ok(processed ? "success" : "failure");
    } catch (Exception e) {
        log.error("Failed to process Alipay notify", e);
        return ResponseEntity.ok("failure");
    }
}

其中,verifyAlipaySignature 和 processPaymentNotify 是业务占位方法,必须结合实际 SDK 和商户配置实现,不能直接信任请求参数。

如果项目启用了 Spring Security,需要允许该接口匿名访问,并根据所用版本配置 CSRF 例外。下面仅展示配置思路:

http
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/paymentNotify/alipayNotify/").permitAll()
        .anyRequest().authenticated()
    )
    .csrf(csrf -> csrf
        .ignoringRequestMatchers("/paymentNotify/alipayNotify/")
    );

允许匿名访问不等于可以跳过验签。支付回调本身就需要匿名访问,其真实性要靠签名验证和订单校验来保证。

正确验签并处理订单

收到通知后,可以按以下顺序处理:

  1. 获取全部原始表单参数。
  2. 使用支付宝公钥和正确的签名算法验签。
  3. 校验 app_id 是否属于当前应用。
  4. 校验 seller_id 等收款方信息是否匹配。
  5. 校验 out_trade_no 是否存在。
  6. 校验通知金额是否与本地订单金额一致。
  7. 根据 trade_status 更新订单状态。
  8. 使用数据库事务保证订单更新一致。
  9. 做好幂等处理,允许同一通知重复到达。
  10. 处理成功后返回纯文本 success。

验签时必须使用支付平台传来的原始参数。不要提前转换字符集、重新拼接金额、修改空格或重新序列化参数,否则可能导致验签失败。

金额不要使用 double 直接比较:

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

BigDecimal orderAmount = order.getAmount();

if (notifiedAmount.compareTo(orderAmount) != 0) {
    throw new IllegalStateException("Payment amount mismatch");
}

订单更新需要具备幂等性:

@Transactional
public boolean markOrderPaid(String orderNo, String tradeNo) {
    Order order = orderRepository.findByOrderNoForUpdate(orderNo);

    if (order == null) {
        return false;
    }

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

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

支付平台可能因为网络超时、没有收到正确响应或内部重试机制而重复发送通知,所以回调处理不能建立在“通知只会到达一次”的假设上。

返回内容必须符合平台要求

业务处理成功后,需要返回平台要求的确认文本。对于常见的支付宝异步通知,通常返回:

success

响应应为纯文本,不要返回 JSON、HTML 页面或统一响应对象。例如,下面的响应可能不符合要求:

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

还要检查全局响应包装器是否会把字符串自动转换成 JSON,或者在响应中追加其他内容。接口的最终响应最好类似:

HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8

success

如果处理失败,可以按平台协议返回 failure,由平台根据规则重试。具体重试次数和时间间隔,应以当前支付产品文档为准。

分层记录日志

排查时,要先分清“请求没有到达服务器”和“请求已经到达,但业务处理失败”。

建议至少保留四层日志:

  • 云安全组或负载均衡访问日志。
  • Nginx、网关或 WAF 访问日志。
  • 应用入口日志。
  • 验签和订单处理日志。

如果 Nginx 访问日志中完全找不到回调请求,主要检查:

  • 发起支付请求时是否实际携带了 notify_url。
  • 该地址是否允许支付平台从公网访问。
  • 端口是否被网络策略拦截。
  • 商户后台或支付产品是否限制回调地址格式。
  • 请求是否发往其他环境或旧地址。

如果 Nginx 有记录,但应用没有记录,主要检查反向代理、路由和容器映射。

如果应用已经收到请求,但订单没有更新,主要检查:

  • 参数是否正确解析。
  • 验签所用的公钥、字符集和签名类型是否正确。
  • 当前应用环境是否与支付应用配置匹配。
  • 金额或商户身份校验是否失败。
  • 数据库事务是否回滚。
  • 接口是否在业务完成前出现异常或超时。

日志可以记录订单号、交易号、状态和验签结果,但不要完整记录私钥、授权信息或其他敏感数据。

为什么 return_url 偶尔不跳转

return_url 是用户浏览器端的同步跳转,与服务端异步通知属于两条不同的链路。以下情况都可能导致浏览器不跳转:

  • 用户在手机上扫码付款,但显示二维码的电脑页面没有建立可靠的状态轮询。
  • 用户支付后直接关闭支付宝或浏览器页面。
  • 网络中断或页面被拦截。
  • 支付产品不保证当前场景会触发同步跳转。
  • return_url 页面无法从用户当前使用的网络访问。
  • 前端端口 4300 没有对公网开放。
  • 页面路由依赖开发服务器配置,刷新后返回 404。
  • 前端应用加载失败、出现 JavaScript 异常,或者被浏览器安全策略阻止。
  • 当前 API 没有正确提交 return_url,或者该 API 不支持此参数。

当前地址是:

http://47.57.236.213:4300/pay/result/

这个地址看起来像直接暴露在公网的前端服务端口。正式环境建议使用 Nginx,通过 HTTPS 443 对外提供页面,例如:

https://www.example.com/pay/result/

如果使用 Vue、React 等单页应用,还需要配置 History 路由回退:

location / {
    try_files $uri $uri/ /index.html;
}

return_url 页面不要直接认定支付成功

用户进入 return_url 后,页面不应只根据 URL 参数显示“支付成功”。更可靠的做法是从 URL 中取得商户订单号,再调用自己的服务端查询订单状态:

const params = new URLSearchParams(window.location.search);
const orderNo = params.get("out_trade_no");

async function refreshOrderStatus() {
  const response = await fetch(
    `/api/orders/${encodeURIComponent(orderNo)}/payment-status`
  );

  const result = await response.json();

  if (result.status === "PAID") {
    renderPaid();
  } else {
    renderPending();
  }
}

如果服务端查询到本地订单仍处于待支付状态,可以进行短时间轮询。必要时,再由服务端调用支付平台的订单查询接口核实:

const timer = setInterval(async () => {
  const status = await queryPaymentStatus();

  if (status === "PAID") {
    clearInterval(timer);
    renderPaid();
  }
}, 2000);

setTimeout(() => {
  clearInterval(timer);
}, 30000);

轮询间隔和持续时间要根据业务量及接口承载能力设置,避免产生高频请求。

推荐的最终流程

完整的支付状态处理流程可以这样设计:

  1. 服务端创建本地订单。
  2. 服务端向支付平台发起支付请求,同时提交公网可访问的 notify_url。
  3. 用户完成付款。
  4. 支付平台通过 notify_url 通知服务端。
  5. 服务端验签、核对订单,并以幂等方式更新状态。
  6. return_url 页面只负责展示,并向服务端查询订单状态。
  7. 如果异步通知延迟或丢失,服务端主动查询订单状态进行补偿。
  8. 定时任务对长时间处于“支付中”的订单执行对账或状态修复。

解决这个问题的重点并非强制保证 return_url 每次都能跳转,而是确保 notify_url 可以访问,回调接口能正确验签并按要求应答,同时建立主动查询和补偿机制。这样,即使用户没有回到结果页,订单状态也不会丢失。

备注:内容仅供参考。