PAYATHON 2026

支付宝接口突然掉单,付款后网站后台仍显示未付款

支付小周

结论

这种情况通常不是支付宝”掉单”,而是网站没有正确处理支付宝发送到 notify_url 的异步通知。

不要直接使用网上找到的支付宝 IP 白名单。支付宝的通知出口地址可能发生变化,固定白名单很容易再次拦截正常请求。只有防火墙策略明确要求限制来源时,才应使用支付宝开放平台当前官方文档或技术支持提供的地址范围,不能沿用历史列表。

排查时,先确认 notify_url 能否从公网访问,是否被 WAF 或安全插件拦截,验签是否成功,以及处理完成后是否按要求返回纯文本 success。

为什么支付宝显示付款成功,网站仍显示未付款

支付宝完成收款后,网站还需要单独更新订单。整个过程如下:

  1. 用户在支付宝完成付款。
  2. 支付宝服务器向商户配置的 notify_url 发送异步通知。
  3. 网站验证签名、金额、商户身份和订单状态。
  4. 网站更新本地订单,并向支付宝返回 success。

第 2~4 步中只要有一步失败,支付宝侧仍可能显示交易成功,网站订单却不会自动变为已付款。

return_url 只是付款后浏览器跳转的同步地址,不能作为可靠的入账依据。用户可能关闭页面,也可能遇到网络中断,跳转参数本身还可能被伪造。订单状态应以异步通知或主动查询接口返回的结果为准。

排查步骤

1. 检查异步通知地址

先确认支付宝应用实际配置的 notify_url 满足以下条件:

  • 可以从公网访问,不是内网地址或 localhost。
  • 使用有效的 HTTP 或 HTTPS 地址。
  • HTTPS 证书完整且未过期,服务器可以正常完成 TLS 握手。
  • 访问时不需要登录、验证码或 Cookie。
  • 不会异常跳转到其他地址,例如从 HTTP 跳到 HTTPS、登录页或 CDN 验证页。
  • 可以接收支付宝使用的 POST 请求。
  • 请求处理时间不会过长。

可以在 Web 服务器访问日志中搜索通知路径,例如:

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

如果完全找不到请求记录,继续检查域名解析、CDN、负载均衡、安全组、防火墙和 WAF。如果有请求记录,则查看响应状态码、应用日志和验签结果。

2. 检查服务器拦截规则

如果问题是最近突然出现的,先确认最近两天是否有过这些变更:

  • 开启或升级了 WAF、CDN、防火墙或安全插件。
  • 修改了 Nginx、Apache 或反向代理规则。
  • 强制启用了登录验证、CSRF 校验或 Referer 校验。
  • 更换了域名、证书、服务器或负载均衡节点。
  • 修改了支付宝公钥、应用私钥、应用 ID 或签名算法。
  • PHP、Java、支付插件或支付宝 SDK 升级后出现兼容问题。
  • 数据库更新订单时出现异常、锁等待或事务回滚。

支付通知是服务器之间的请求,通常不会携带网站登录状态和 CSRF Token。因此,通知接口一般需要排除 CSRF 检查,但支付宝签名验证仍然必不可少。

例如,Spring Security 项目可以只忽略支付通知路径的 CSRF 校验,具体写法取决于所用版本:

http.csrf(csrf -> csrf
    .ignoringRequestMatchers("/payment/alipay/notify")
);

这并不等于关闭整个网站的 CSRF 防护。

3. 检查通知处理结果

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

success

不要返回 JSON、HTML 页面、调试信息,也不要让模板在内容中加入额外空格。处理失败时不能返回 success,否则支付宝会认为通知已经被正确接收。

下面这些响应都不符合要求:

{"status":"success"}
<html><body>success</body></html>

正确响应是纯文本:

success

4. 检查验签配置

验签时逐项确认:

  • 使用的是”支付宝公钥”,不是商户应用公钥。
  • app_id 与当前应用一致。
  • charset 通常为 UTF-8。
  • sign_type 通常为 RSA2,但要以实际配置为准。
  • 验签前没有擅自修改、拼接或删除请求参数。
  • 公钥没有丢失换行、复制不完整,也没有使用其他环境的密钥。
  • 沙箱环境和正式环境的配置没有混用。

下面是 Java Servlet/Spring 场景的示意代码,类名基于支付宝 Java SDK。具体依赖版本和异常类型应以项目当前使用的 SDK 为准:

@PostMapping(
    value = "/payment/alipay/notify",
    produces = "text/plain;charset=UTF-8"
)
@ResponseBody
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 appId = params.get("app_id");
        String tradeStatus = params.get("trade_status");
        String outTradeNo = params.get("out_trade_no");
        String tradeNo = params.get("trade_no");
        String totalAmount = params.get("total_amount");

        if (!expectedAppId.equals(appId)) {
            return "failure";
        }

        if (!"TRADE_SUCCESS".equals(tradeStatus)
                && !"TRADE_FINISHED".equals(tradeStatus)) {
            return "success";
        }

        boolean updated = paymentService.confirmPayment(
            outTradeNo,
            tradeNo,
            new BigDecimal(totalAmount)
        );

        return updated ? "success" : "failure";
    } catch (Exception e) {
        logger.error("Failed to process Alipay notification", e);
        return "failure";
    }
}

confirmPayment 不能只根据 out_trade_no 修改订单状态,还要核对本地订单金额、商户身份等重要数据。通知中实际包含哪些字段,取决于使用的支付宝产品和接口,应以收到的参数及对应接口文档为准。

5. 保证重复通知不会重复入账

支付宝可能多次发送同一笔交易的通知,因此处理程序必须保证幂等。可以根据本地订单号和支付宝交易号建立约束,并在事务中完成订单更新。

示意 SQL:

UPDATE orders
SET
    payment_status = 'PAID',
    alipay_trade_no = :trade_no,
    paid_at = CURRENT_TIMESTAMP
WHERE
    order_no = :out_trade_no
    AND payment_status = 'UNPAID'
    AND payable_amount = :total_amount;

如果订单已经支付,再次收到合法通知时仍应返回 success,但不能重复增加余额、库存或积分。

6. 对异常订单主动查单

对于支付宝已经收款、本地状态仍是未支付的订单,可以调用支付宝交易查询接口主动核实。常见接口名称为:

alipay.trade.query

查询时可以使用本地订单号 out_trade_no 或支付宝交易号 trade_no。只有验签通过,或者主动查询确认交易成功,并且金额、应用和订单信息全部核对一致后,才能补记支付状态。

还可以增加定时补偿任务:

  1. 找出创建超过一定时间、状态仍为未支付的订单。
  2. 调用 alipay.trade.query。
  3. 核对交易状态、订单号和金额。
  4. 通过统一的幂等入账逻辑更新订单。
  5. 记录补单原因和查询结果。

是否需要配置 IP 白名单

多数情况下不需要,也不应优先使用固定 IP 白名单。

如果服务器当前只允许指定来源 IP 访问通知接口,可以先暂时取消该路径的来源 IP 限制,改为通过验签确认请求真实性。与单纯判断来源 IP 相比,签名验证更可靠。

如果合规或内部安全规范明确要求使用白名单,需要注意:

  • 只使用支付宝开放平台当前官方渠道公布或确认的地址范围。
  • 区分正式环境和沙箱环境。
  • 不要使用论坛、旧博客或历史工单中的 IP 列表。
  • 地址范围发生变化后及时更新。
  • 即使配置了 IP 白名单,也不能省略签名验证。
  • 如果前面使用了 CDN 或反向代理,要确认防火墙看到的是支付宝源地址,还是代理节点地址。
  • 不要直接信任客户端提交的 X-Forwarded-For,除非请求确实来自受信任的反向代理。

建议增加的日志

通知日志至少应记录以下信息:

request_time
request_id
remote_addr
app_id
out_trade_no
trade_no
trade_status
total_amount
sign_type
signature_verified
http_status
order_update_result
error_message

不要把应用私钥、完整签名密钥或其他敏感凭证写入日志。如果需要保留通知原始参数,也要按照业务的数据安全要求进行脱敏并控制访问权限。

根据目前的现象,先检查通知接口最近是否返回过 403、404、5xx,或者没有返回纯文本 success。然后再检查支付宝公钥、app_id、金额校验和数据库更新是否失败。只有确定请求在网络层遭到拦截后,才需要继续处理防火墙或白名单问题。

备注:内容仅供参考。