支付宝接口突然掉单,付款后网站后台仍显示未付款
结论
这种情况通常不是支付宝”掉单”,而是网站没有正确处理支付宝发送到 notify_url 的异步通知。
不要直接使用网上找到的支付宝 IP 白名单。支付宝的通知出口地址可能发生变化,固定白名单很容易再次拦截正常请求。只有防火墙策略明确要求限制来源时,才应使用支付宝开放平台当前官方文档或技术支持提供的地址范围,不能沿用历史列表。
排查时,先确认 notify_url 能否从公网访问,是否被 WAF 或安全插件拦截,验签是否成功,以及处理完成后是否按要求返回纯文本 success。
为什么支付宝显示付款成功,网站仍显示未付款
支付宝完成收款后,网站还需要单独更新订单。整个过程如下:
- 用户在支付宝完成付款。
- 支付宝服务器向商户配置的
notify_url发送异步通知。 - 网站验证签名、金额、商户身份和订单状态。
- 网站更新本地订单,并向支付宝返回
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。只有验签通过,或者主动查询确认交易成功,并且金额、应用和订单信息全部核对一致后,才能补记支付状态。
还可以增加定时补偿任务:
- 找出创建超过一定时间、状态仍为未支付的订单。
- 调用
alipay.trade.query。 - 核对交易状态、订单号和金额。
- 通过统一的幂等入账逻辑更新订单。
- 记录补单原因和查询结果。
是否需要配置 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、金额校验和数据库更新是否失败。只有确定请求在网络层遭到拦截后,才需要继续处理防火墙或白名单问题。
备注:内容仅供参考。
