手机网站支付成功后支付宝服务器未通知应用服务器
结论
付款成功,并不表示应用服务器已经完成异步通知处理。问题一般出在以下几个环节:
- 下单时没有正确传入
notify_url。 - 支付宝无法访问通知地址。
- 应用服务器收到了通知,但在验签、参数解析或业务处理时发生异常。
- 应用服务器处理完成后,没有返回纯文本
success,支付宝因此判定通知失败并重试。 - 业务只通过前台跳转地址
return_url更新订单,混淆了同步跳转和异步通知。
建议先调用 alipay.trade.query 查询交易状态,确认交易号 2020090322001438751425798479 和商户订单号 UZFB20200903215430707613676 的实际状态,再检查通知日志、网络访问记录及验签结果。这笔交易发生在 2020 年,能否继续查询,要看支付宝的数据保留情况和商户账号权限。如果历史日志已经清理,仅凭现有信息无法判断具体在哪个环节失败。
先核实支付宝侧交易状态
通过手机网站支付接口 alipay.trade.wap.pay 创建交易后,可以调用 alipay.trade.query 主动查询订单。查询时传入商户订单号或支付宝交易号即可:
{
"out_trade_no": "UZFB20200903215430707613676"
}
也可以传入:
{
"trade_no": "2020090322001438751425798479"
}
主要检查以下返回字段:
trade_statustrade_noout_trade_nototal_amountbuyer_logon_id
常见交易状态如下:
WAIT_BUYER_PAY:等待付款。TRADE_SUCCESS:付款成功。TRADE_FINISHED:交易完成。TRADE_CLOSED:交易关闭。
如果查询结果是 TRADE_SUCCESS 或 TRADE_FINISHED,但本地订单仍显示未支付,应根据查询结果补偿订单,并继续排查异步通知链路。
检查 notify_url 是否正确
调用 alipay.trade.wap.pay 时,应通过公共请求参数 notify_url 传递异步通知地址,不能将它放入 biz_content:
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setNotifyUrl("https://pay.example.com/alipay/notify");
request.setReturnUrl("https://www.example.com/pay/result");
request.setBizContent(bizContent);
需要逐项确认:
notify_url是公网可访问的完整地址。- 地址不是
localhost、127.0.0.1、局域网 IP,也不是只能通过 VPN 访问的域名。 - 该地址可以直接处理支付宝发来的
POST请求,不依赖浏览器 Cookie、登录状态或 Session。 - 通知地址不会触发
301、302等重定向。 - HTTPS 证书有效,证书链完整。
- 网关、WAF、防火墙、反向代理及 IP 白名单没有拦截支付宝请求。
- 路由不会被 CSRF 校验、登录鉴权或验证码机制阻断。
可以在公网环境中测试通知地址是否可达:
curl -i -X POST "https://pay.example.com/alipay/notify" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "test=1"
这项测试只能检查公网入口的连通性,不能模拟真实的支付宝签名。
检查服务器是否实际收到通知
排查”没有通知”时,需要先分清具体情况:
- Web 服务器完全没有访问记录:先检查
notify_url、DNS、证书、防火墙和网关。 - Web 服务器有访问记录,但应用没有业务日志:检查反向代理转发、路由、请求方法、CSRF 及异常处理。
- 应用有通知日志,但订单没有更新:检查参数解析、验签和业务校验。
还要结合以下信息一起检查:
- Nginx、Apache 或负载均衡的访问日志。
- 应用程序入口日志。
- 验签失败日志。
- 数据库更新异常和事务回滚日志。
- 接口最终返回的 HTTP 状态码及响应正文。
支付宝异步通知通常以 application/x-www-form-urlencoded 格式提交,应用不能只读取 JSON 请求体。
正确验签和处理通知
下面的示例展示了 Java 环境中的主要处理流程。支付宝 SDK 版本不同,具体类名和方法签名可能也不同,应以项目实际使用的 SDK 为准。
@PostMapping("/alipay/notify")
@ResponseBody
public String alipayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
params.put(entry.getKey(), String.join(",", entry.getValue()));
}
try {
boolean verified = AlipaySignature.rsaCheckV1(
params,
ALIPAY_PUBLIC_KEY,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
String appId = params.get("app_id");
String sellerId = params.get("seller_id");
String tradeNo = params.get("trade_no");
String outTradeNo = params.get("out_trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
if (!EXPECTED_APP_ID.equals(appId)) {
return "failure";
}
if (!EXPECTED_SELLER_ID.equals(sellerId)) {
return "failure";
}
Order order = orderService.findByOrderNo(outTradeNo);
if (order == null) {
return "failure";
}
BigDecimal notifiedAmount = new BigDecimal(totalAmount);
if (order.getPayAmount().compareTo(notifiedAmount) != 0) {
return "failure";
}
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
orderService.markPaidIdempotently(outTradeNo, tradeNo);
}
return "success";
} catch (Exception e) {
logger.error("Failed to process Alipay notification", e);
return "failure";
}
}
验签时需要注意:
- 使用支付宝公钥,不要使用应用私钥或应用公钥。
sign_type通常是RSA2,但应与实际请求及应用配置保持一致。- 字符集通常是
UTF-8,也必须与接口配置一致。 - 应将支付宝提交的全部参数交给 SDK 验签。
- 验签前不要对参数值额外执行 URL 解码、HTML 转义或字符替换。
- 如果使用支付宝公钥证书模式,应采用相应的证书验签方式,不能直接使用普通公钥模式的代码。
必须校验业务参数
验签成功只能证明通知来自持有对应签名私钥的一方。应用还要确认这笔交易确实属于当前商户,至少检查以下内容:
app_id是否与当前应用的 App ID 一致。seller_id是否与当前收款账号一致。out_trade_no是否存在于本地订单系统。total_amount是否与本地订单金额完全一致。trade_status是否属于允许更新支付状态的值。trade_no是否已经被其他订单使用。
比较金额时,应使用 BigDecimal 或以最小货币单位表示的整数,不要使用 double:
BigDecimal expected = new BigDecimal("99.90");
BigDecimal actual = new BigDecimal(totalAmount);
if (expected.compareTo(actual) != 0) {
return "failure";
}
正确返回 success
通知处理成功后,响应正文应直接返回:
success
不要返回 JSON、HTML 页面或附加说明。下面这些响应都不符合要求:
{"status":"success"}
success!
<html><body>success</body></html>
还要确认 HTTP 状态码正常,并且框架、网关或调试组件没有在响应中追加其他内容。
如果应用没有返回支付宝认可的成功响应,支付宝通常会按照通知机制重试。具体重试次数和时间安排,应以相应产品当前的官方文档为准。订单更新也不能只依赖通知重试。
订单处理必须支持幂等
支付宝可能对同一笔交易重复发送通知,因此订单更新必须支持幂等。可以为商户订单号和支付宝交易号建立唯一约束,并在事务中检查订单当前状态:
UPDATE payment_order
SET
pay_status = 'PAID',
alipay_trade_no = :tradeNo,
paid_at = :paidAt
WHERE
order_no = :outTradeNo
AND pay_status = 'UNPAID';
如果受影响的行数是 0,应继续判断订单是否已经处理成功,不能直接重复发货或重复增加余额。
支付通知可以按以下顺序处理:
- 解析参数并记录必要日志。
- 验证支付宝签名。
- 校验商户、订单和金额。
- 在数据库事务中幂等更新订单。
- 提交事务。
- 返回纯文本
success。
只有数据库事务成功提交后,才能返回 success。如果先返回成功,再更新订单,后续处理一旦失败,支付宝无法得知应用侧操作尚未完成。
建立主动查询补偿机制
支付系统不能完全依赖异步通知。用户从 return_url 返回,或者订单长时间停留在”支付中”状态时,应用可以调用 alipay.trade.query 主动确认交易状态。
建议采用以下补偿流程:
- 用户返回支付结果页时,前端只显示”正在确认支付结果”。
- 应用服务器查询本地订单状态。
- 如果本地订单仍未支付,调用
alipay.trade.query。 - 查询结果确认支付成功后,执行与异步通知相同的幂等更新逻辑。
- 定时扫描超过一定时间仍处于”支付中”的订单,再次查询并补偿。
return_url 是浏览器同步跳转地址。用户可能关闭页面、断网或阻止跳转,所以不能把它作为可靠的支付成功依据,也不能依赖它完成发货。
针对该订单的处理建议
对于交易号 2020090322001438751425798479、商户订单号 UZFB20200903215430707613676,可以按以下顺序处理:
- 在支付宝商户后台核对该交易是否属于当前应用和当前收款账号。
- 调用
alipay.trade.query查询最终交易状态及支付金额。 - 检查 2020-09-03 21 前后的网关访问日志、应用日志和数据库日志。
- 如果交易已经成功,且金额与商户身份均匹配,通过幂等补单流程修复本地订单。
- 核对当时下单请求实际提交的
notify_url,不要只查看当前配置。 - 增加通知原始参数摘要、验签结果、订单更新结果及响应内容日志。
- 增加主动查询和超时订单补偿任务,避免以后再次因通知链路异常而漏单。
历史交易的日志和查询能力可能已经受到数据保留期限的影响。如果无法还原当时的请求,应根据商户后台账单、交易查询结果和资金流水进行人工核对,不能仅凭用户提供的支付截图直接修改订单状态。
备注:内容仅供参考。
