PAYATHON 2026

PC端扫码支付成功但未收到异步通知

支付小周

结论

扫码支付成功,只能说明交易已经完成,不能说明商户服务器一定收到了异步通知,更不能说明通知已处理成功。网站配置了证书,也不代表支付平台能通过公网访问 notify_url。

订单号 2023050922001417181438452261 从格式看更像支付宝交易号 trade_no。仅凭这个号码无法判断支付宝是否发送过通知,还要查看支付宝侧的通知记录、网关访问日志和应用日志。常见原因包括:

  • 最终支付请求中没有 notify_url;
  • 通知地址无法从公网访问,或者被跳转、鉴权、防火墙拦截;
  • 网关收到了通知,但应用没有记录,或处理过程中失败;
  • 验签参数、支付宝公钥或字符集配置有误;
  • 接口没有返回纯文本 success,支付宝因此判定通知失败;
  • 业务处理已经完成,但本地订单更新事务回滚,或状态判断有误。

问题解决前,应主动调用交易查询接口确认支付状态并补单,不能只依赖异步通知。

第一步:确认订单号类型

支付宝支付通常会涉及两个订单号:

  • out_trade_no:商户生成的订单号;
  • trade_no:支付宝生成的交易号。

2023050922001417181438452261 更可能是 trade_no。排查时还要找到对应的 out_trade_no,因为本地订单通常通过 out_trade_no 关联。

可以使用 alipay.trade.query 查询:

{
  "trade_no": "2023050922001417181438452261"
}

也可以使用商户订单号:

{
  "out_trade_no": "商户订单号"
}

主要查看返回的 trade_status:

  • TRADE_SUCCESS:支付成功;
  • TRADE_FINISHED:交易完成;
  • WAIT_BUYER_PAY:等待付款;
  • TRADE_CLOSED:交易关闭。

如果查询结果是 TRADE_SUCCESS 或 TRADE_FINISHED,但本地订单仍显示未支付,就应执行幂等补单。

第二步:确认最终请求中确实包含 notify_url

不能只看代码中的配置项。应记录实际提交给支付宝网关的请求参数,确认其中包含正确的 notify_url。

例如:

notify_url=https://pay.example.com/alipay/notify

通知地址应满足以下条件:

  • 可以从公网访问;
  • 不使用 localhost、127.0.0.1 或内网地址;
  • 不依赖 Cookie、登录状态或人工认证;
  • 不包含 URL Fragment,例如 #notify;
  • 尽量不使用 HTTP 301、302 或 307 跳转;
  • HTTPS 证书完整有效,域名与证书匹配;
  • 网关、WAF、防火墙和反向代理允许支付宝发送 POST 请求;
  • 接口可以接收 application/x-www-form-urlencoded 表单参数。

如果使用 SDK,notify_url 通常属于公共请求参数,不应误放到 biz_content 中。

第三步:检查 Web 服务器访问日志

先确认通知有没有到达服务器,这一步可以把问题分成两类。

以 Nginx 为例:

grep "/alipay/notify" /var/log/nginx/access.log

需要查看:

  • 是否出现 POST 请求;
  • HTTP 状态码是否为 200;
  • 请求是否被重定向到登录页或 HTTPS 地址;
  • 是否返回 403、404、405、499、500、502 或 504;
  • 请求是否被限流、WAF 或 CSRF 校验拦截。

如果访问日志里完全没有相关请求,应检查:

  1. 实际支付请求是否包含 notify_url;
  2. 通知地址能否从公网访问;
  3. 支付宝侧是否有通知发送记录或失败记录;
  4. 域名解析、证书链、防火墙和反向代理配置是否正确。

如果访问日志中有请求,但业务日志没有记录,问题通常出在网关转发、路由、请求方法限制或应用启动阶段。

第四步:检查接口是否错误拦截通知

异步通知是服务器之间发送的 POST 请求,不会携带用户浏览器中的登录状态。以下中间件可能会误拦截通知:

  • 登录认证;
  • CSRF 校验;
  • Referer 校验;
  • User-Agent 白名单;
  • IP 白名单;
  • JSON 请求体强制校验;
  • 全局参数过滤;
  • 防重放策略。

例如,在 Spring Security 中,应结合项目的实际配置,对通知路径放行认证和 CSRF:

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

通知接口仍然要验证支付宝签名。放行登录认证和 CSRF,并不等于跳过安全校验。

第五步:按原始表单参数验签

通知参数通常来自 POST 表单。不要先把参数转换成业务对象,再重新拼接用于验签,否则可能造成字段遗漏、编码变化或空值处理不一致。

Java 示例:

@PostMapping(
    value = "/alipay/notify",
    consumes = "application/x-www-form-urlencoded"
)
public String alipayNotify(HttpServletRequest request) {
    Map<String, String> params = new HashMap<>();

    request.getParameterMap().forEach((key, values) -> {
        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");

        // 校验 app_id、商户订单号、金额和收款方信息。
        // 确认无误后,以 out_trade_no 为幂等键更新订单。
        processPayment(outTradeNo, tradeNo, tradeStatus, totalAmount, appId);

        return "success";
    } catch (Exception e) {
        // 记录异常及必要的排查信息,避免记录敏感数据。
        return "failure";
    }
}

示例中的 alipayPublicKey 指支付宝公钥,不是以下任何一种密钥:

  • 应用公钥;
  • 应用私钥;
  • 网站 HTTPS 证书公钥。

如果使用支付宝公钥证书模式,应按照相应 SDK 的证书模式完成初始化和验签,不能与普通公钥模式混用。

第六步:完整校验业务参数

验签通过后,不能马上把订单改为支付成功。至少还要校验:

  • app_id 是否属于当前应用;
  • out_trade_no 是否存在;
  • total_amount 是否与本地订单金额一致;
  • 收款方标识是否符合当前商户配置;
  • trade_status 是否属于业务允许的成功状态;
  • trade_no 是否已经绑定到其他订单。

金额应使用十进制金额类型或最小货币单位进行比较,不要直接用浮点数判断:

BigDecimal notifiedAmount = new BigDecimal(totalAmount);
BigDecimal orderAmount = order.getPayAmount();

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

第七步:正确返回 success

只有验签、订单校验和本地状态更新全部成功后,接口才能返回:

success

响应体最好只有小写纯文本 success。不要返回:

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

也不要返回 HTML 页面、调试信息或包含其他内容的字符串。

Spring 示例:

@PostMapping("/alipay/notify")
@ResponseBody
public String notify(HttpServletRequest request) {
    // 验签并处理业务
    return "success";
}

如果本地处理失败,应返回 failure 或其他非成功结果,让支付平台按照自身机制重新发送通知。具体重试次数和时间应以当前支付宝官方规则及后台记录为准,不要根据固定时间推测。

第八步:保证订单处理幂等

异步通知可能重复到达,主动查询和异步通知也可能同时更新同一笔订单,因此需要以订单号进行幂等控制。

UPDATE payment_order
SET
    pay_status = 'PAID',
    trade_no = :tradeNo,
    paid_at = CURRENT_TIMESTAMP
WHERE
    out_trade_no = :outTradeNo
    AND pay_status = 'UNPAID';

如果受影响行数为 0,应重新查询订单:

  • 订单已经是 PAID:按成功处理,不要重复发货;
  • 订单不存在:记录异常并返回失败;
  • 金额或交易号存在冲突:停止处理并告警。

临时补救方案

对于当前订单,应立即通过 alipay.trade.query 主动查询。查询确认支付成功后:

  1. 校验商户订单号、金额、应用和收款方;
  2. 通过幂等事务把本地订单补记为已支付;
  3. 触发尚未执行的后续业务;
  4. 保存补单来源、支付宝交易号和查询结果摘要;
  5. 继续排查异步通知链路,避免后续订单再次漏单。

长期可增加定时对账或补偿任务,扫描用户已经付款但本地仍为待支付状态的订单,再通过交易查询接口确认状态。异步通知仍是主要处理入口,主动查询和账单对账用于容错。

备注:内容仅供参考。