PAYATHON 2026

APP支付成功后未收到异步回调

支付阿杰

结论

外部浏览器能访问回调地址,不代表支付宝异步通知一定能正常送达。首先确认创建 APP 支付订单时,是否通过 notify_url 传入了这个完整地址:

https://test.hybtco.com/zsnc/s20UserClient/mall/aliPayNotify.do

“授权回调地址”和“支付异步通知地址”是两回事。开放平台后台配置的授权回调地址,通常不会自动用于 APP 支付的异步通知。服务端调用支付接口时,需要明确设置 notify_url。

如果支付请求已经正确设置了该参数,再依次检查网关访问日志、HTTP 状态码、重定向、TLS/WAF、参数解析、验签和响应内容。

先检查支付请求

APP 支付订单应由服务端创建,并在 alipay.trade.app.pay 请求中设置通知地址。Java SDK 示例:

AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();

AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo("1678950734-Test0011472");
model.setTotalAmount("0.01");
model.setSubject("订单支付");
model.setProductCode("QUICK_MSECURITY_PAY");

request.setBizModel(model);
request.setNotifyUrl(
    "https://test.hybtco.com/zsnc/s20UserClient/mall/aliPayNotify.do"
);

AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
String orderString = response.getBody();

检查服务端最终提交给支付宝的请求,不要只看客户端保存的配置:

  • 请求中是否有 notify_url。
  • 地址是否与当前测试环境一致。
  • 是否误填了 OAuth 授权回调地址、同步跳转地址或旧域名。
  • 配置是否被覆盖,例如测试配置被生产配置替换。
  • URL 是否被错误地二次编码。
  • 当前支付订单是否确实由这段服务端代码创建。

修改 notify_url 只会影响此后创建的新订单。已经创建并支付的订单,不会因为后来修改了配置而自动更换通知地址。

根据访问日志定位问题层级

同时查看 CDN、负载均衡、Nginx 和应用日志,并用订单号搜索:

1678950734-Test0011472

根据日志结果,可以按下面几种情况排查。

完全没有请求记录

这通常说明请求没有到达入口层。需要检查:

  • 创建支付订单时没有传递 notify_url,或者传入了其他地址。
  • DNS 解析异常,尤其是 IPv6 解析错误。
  • HTTPS 证书过期、证书链不完整或域名不匹配。
  • 服务器只允许特定来源 IP。
  • CDN、防火墙或 WAF 拦截了支付宝的 POST 请求。
  • 回调地址只能通过公司内网、VPN 或特定 Host 访问。
  • 网络入口不支持外部 IPv4/IPv6 访问。

不要只用浏览器发起 GET 请求来验证。异步通知通常使用表单格式的 POST 请求,两种请求经过的安全规则可能不同。

可以从外部环境测试该路由是否接受 POST:

curl -i -X POST \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'out_trade_no=1678950734-Test0011472&trade_status=TRADE_SUCCESS' \
  'https://test.hybtco.com/zsnc/s20UserClient/mall/aliPayNotify.do'

这个请求没有支付宝签名,只能用来检查网络、路由和请求解析,不能验证完整的支付逻辑。

有请求,但返回 301、302、307 或 308

这说明回调地址发生了重定向,例如:

HTTP → HTTPS
旧域名 → 新域名
未带斜杠 → 带斜杠
登录页或统一认证页

应将 notify_url 直接设置为最终处理地址,并确保接口不依赖登录、Cookie、验证码或其他浏览器交互。

有请求,但返回 403 或 404

需要检查:

  • WAF、CSRF、防爬虫或来源校验是否拦截了请求。
  • Spring Security 等安全组件是否要求 CSRF Token。
  • 反向代理配置的路径是否与应用路径一致。
  • 接口是否只允许 GET,没有接收 POST。
  • 域名对应的虚拟主机是否正确。
  • 发布后的实际 Context Path 是否仍是 /zsnc。

异步通知接口通常需要免登录,并对该路径合理处理 CSRF。免登录或豁免 CSRF 不等于跳过支付宝签名验证。

有请求,但返回 500 或超时

查看应用异常日志,常见原因有:

  • 把表单参数当成 JSON 解析。
  • 字符集处理不一致。
  • 验签时使用了错误的支付宝公钥。
  • 数据库事务异常或连接池耗尽。
  • 业务逻辑执行时间过长。
  • 重复通知引发唯一键冲突。
  • 接口先调用耗时的第三方服务,导致响应超时。

回调入口应尽快完成验签、订单校验和本地状态更新。耗时操作可以在事务提交后交给消息队列或异步任务。

正确接收并验签

支付宝异步通知通常以 application/x-www-form-urlencoded 格式提交,不要直接按 JSON 请求体读取。Spring MVC 示例:

@PostMapping(
    value = "/zsnc/s20UserClient/mall/aliPayNotify.do",
    produces = "text/plain;charset=UTF-8"
)
public String alipayNotify(HttpServletRequest request) {
    Map<String, String> params = new HashMap<>();

    request.getParameterMap().forEach((name, values) -> {
        params.put(name, String.join(",", values));
    });

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

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

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

        // 1. 根据 outTradeNo 查询本地订单
        // 2. 校验 appId、sellerId、订单金额和订单归属
        // 3. 仅处理 TRADE_SUCCESS 或 TRADE_FINISHED
        // 4. 使用事务和唯一约束保证幂等
        // 5. 保存 tradeNo 及支付状态

        if ("TRADE_SUCCESS".equals(tradeStatus)
                || "TRADE_FINISHED".equals(tradeStatus)) {
            paymentService.confirmPayment(
                outTradeNo,
                tradeNo,
                totalAmount,
                appId,
                sellerId
            );
        }

        return "success";
    } catch (Exception e) {
        // 记录异常及可定位信息,但不要记录私钥等敏感数据
        return "failure";
    }
}

验签时需要确认:

  • 使用的是支付宝公钥,不是应用公钥或应用私钥。
  • sign_type、字符集和密钥模式与实际配置一致。
  • 验签参数来自支付宝提交的原始表单参数。
  • 验签前没有随意修改、重新编码或格式化参数值。
  • 证书模式与普通公钥模式的配置方式不同,需要与项目当前的接入方式一致。
  • 验签成功后,仍需校验 app_id、seller_id、out_trade_no、total_amount 等业务字段。

响应内容必须准确

处理成功后,响应体应直接返回:

success

不要返回 JSON、HTML 页面或统一响应对象。下面这些内容通常不符合要求:

{"code":0,"data":"success"}
<html><body>success</body></html>

还要防止框架把返回值当成视图名称。可以使用 @RestController、@ResponseBody,或者直接写入响应体,并确认最终 HTTP 状态码是 200。

业务处理失败时,应返回 failure 或非成功响应,让支付平台按其机制继续通知。重试次数和时间间隔可能随平台规则调整,应以当前官方文档和实际通知记录为准。

保证重复通知安全

支付宝可能重复发送异步通知,所以第二次收到通知不能直接当作异常。订单更新必须具备幂等性,例如:

UPDATE payment_order
SET
    pay_status = 'PAID',
    alipay_trade_no = ?
WHERE
    out_trade_no = ?
    AND pay_status = 'UNPAID';

也可以为支付宝交易号建立唯一约束:

CREATE UNIQUE INDEX uk_payment_alipay_trade_no
ON payment_order (alipay_trade_no);

如果订单已经处于已支付状态,应直接按处理成功对待,不能重复发货、增加余额或生成权益。

对当前订单进行补偿查询

对于这个订单:

out_trade_no = 1678950734-Test0011472

如果找不到任何异步通知记录,应由服务端调用 alipay.trade.query 主动查询交易状态。确认支付成功后,再通过幂等的补偿流程更新本地订单。

AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();

AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo("1678950734-Test0011472");

request.setBizModel(model);

AlipayTradeQueryResponse response = alipayClient.execute(request);

if (response.isSuccess()
        && ("TRADE_SUCCESS".equals(response.getTradeStatus())
        || "TRADE_FINISHED".equals(response.getTradeStatus()))) {
    paymentService.confirmPaymentByQuery(
        response.getOutTradeNo(),
        response.getTradeNo(),
        response.getTotalAmount()
    );
}

客户端显示“支付成功”只能用于界面提示,不能作为服务端发货或入账的依据。可靠的支付结果必须来自验签通过的异步通知,或者服务端主动查询。

推荐排查顺序

  1. 打印创建 APP 支付订单时的实际请求参数,确认其中包含正确的 notify_url。
  2. 创建新订单并重新支付,不要用修改配置前创建的旧订单验证。
  3. 在网关和应用访问日志中搜索 out_trade_no。
  4. 查看回调请求的 HTTP 状态码、耗时和响应体。
  5. 排除重定向、登录认证、CSRF、WAF 和 IP 白名单限制。
  6. 确认接口按表单参数接收通知,并使用正确的支付宝公钥验签。
  7. 确认处理成功时只返回纯文本 success。
  8. 使用 alipay.trade.query 核对漏通知订单,并建立定时补偿机制。

现有信息还不足以确定问题出在哪一层。应最先确认该地址是否真的通过 notify_url 写入了 alipay.trade.app.pay 请求,而不是只配置成了“授权回调地址”。

备注:内容仅供参考。