手机网站支付能正常拉起,但没有回调怎么办?
结论
支付页面能正常拉起,只能说明下单和跳转流程基本正常,并不代表异步通知配置没有问题。支付完成后收不到回调,可以依次检查 notify_url 配置、公网连通性、HTTP 响应状态、签名验证和业务处理日志。
如果这里的“手机网站支付”是指支付宝手机网站支付,还要分清两个地址的用途:
return_url是支付完成后浏览器跳转的同步地址,不能用来可靠判断支付是否成功。notify_url是支付宝服务器发送支付结果的异步通知地址。订单状态应以异步通知或主动查询结果为准。- 异步通知处理成功后,接口应直接返回纯文本
success,否则支付平台可能继续重试。
先确认回调地址是否配置正确
创建支付请求时,需要明确传入公网可访问的 notify_url,不要把 return_url 当作异步回调地址。
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setReturnUrl("https://www.example.com/pay/return");
request.setNotifyUrl("https://api.example.com/pay/alipay/notify");
request.setBizContent("""
{
"out_trade_no": "ORDER_202609130001",
"total_amount": "99.00",
"subject": "订单支付",
"product_code": "QUICK_WAP_WAY"
}
""");
排查时要核对实际提交给支付平台的请求参数,不能只看配置文件。常见问题有:
- 没有设置
notify_url,或者传入的是空字符串。 - 修改配置后没有重启应用,程序仍在使用旧地址。
- 测试环境和生产环境读取了不同的配置。
- 回调地址拼写错误、路径缺少前缀或域名解析异常。
notify_url使用了localhost、127.0.0.1、局域网 IP 或临时内网地址。- 网关、负载均衡或反向代理改写了回调路径。
检查回调接口能否从公网访问
支付平台的服务器需要主动请求 notify_url。这个地址通常应满足以下条件:
- 使用公网域名。
- DNS 能够正常解析。
- HTTPS 证书有效,证书链完整。
- 服务器监听端口已经开放。
- 防火墙、安全组、WAF 和网关没有拦截支付平台的请求。
- 接口不要求登录,也不依赖浏览器 Cookie。
- 接口没有验证码、CSRF 校验或前端鉴权。
- HTTP 请求不会被错误重定向。
可以从外部网络测试接口是否可达:
curl -i -X POST \
"https://api.example.com/pay/alipay/notify" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data "out_trade_no=test"
这个命令只能测试网络和路由,无法模拟一条能够通过验签的真实支付通知。测试时主要看下面这些结果:
404:回调路径不存在或路由配置错误。401、403:请求被鉴权、CSRF、WAF 或访问控制拦截。301、302:请求发生了重定向,建议将notify_url直接配置为最终地址。405:接口不接受POST请求。500:回调处理代码出现异常。- 请求超时:可能是网络、防火墙、数据库或业务处理时间过长。
如果服务部署在反向代理后面,还要查看 Nginx、网关和应用服务器的访问日志,确定请求停在哪一层。
确认接口按表单方式接收参数
支付宝异步通知通常通过 POST 表单参数发送,不是 JSON 请求体。接口如果只用 @RequestBody 接收 JSON,可能无法取得通知参数。
Spring Boot 可以这样接收参数并验签:
@PostMapping(
value = "/pay/alipay/notify",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
)
public String alipayNotify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0) {
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");
String sellerId = params.get("seller_id");
// 校验 app_id、seller_id、订单号、订单金额等业务字段。
// 确认无误后,以幂等方式更新订单状态。
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
markOrderPaidIdempotently(outTradeNo, tradeNo, totalAmount);
}
return "success";
} catch (AlipayApiException e) {
// 记录异常,但不要记录私钥等敏感信息。
return "failure";
}
}
示例中的公钥、字符集和签名类型必须与实际接入配置一致。不同 SDK 版本的方法签名可能不同,应以项目当前使用的 SDK 接口为准。
排查签名配置
请求已经到达,但验签失败,也可能被误判为“没有回调”。在不泄露敏感信息的前提下,日志中应记录:
- 回调到达时间。
out_trade_no。trade_no。trade_status。app_id。sign_type。- 验签结果。
- 异常类型和堆栈。
- 最终返回的 HTTP 状态码和响应正文。
检查时注意以下问题:
- 使用的是支付宝公钥,而不是应用公钥。
- 当前环境使用的支付宝公钥与对应应用一致。
- 没有混用沙箱环境和生产环境的密钥。
- 使用公钥证书模式时,验签方式与证书模式匹配。
charset和sign_type与接入配置一致。- 验签前没有擅自删除、改名或重新编码通知参数。
- 没有对已经解码的参数再次执行 URL Decode。
- 网关、中间件或字符过滤器没有修改参数值。
不能因为验签失败就跳过验签。关闭验签后,攻击者可能伪造支付成功通知。
正确处理并返回 success
回调业务处理成功后,接口应返回以下纯文本:
success
不要返回 JSON、HTML 页面或其他经过包装的内容。例如,下面这种响应通常不符合要求:
{"code":200,"message":"success"}
如果项目统一使用响应包装器,需要把支付回调接口排除在包装逻辑之外。同时检查异常处理器、字符编码过滤器和网关有没有改写响应内容。
Spring Boot 可以明确指定返回类型:
@PostMapping(
value = "/pay/alipay/notify",
produces = MediaType.TEXT_PLAIN_VALUE
)
@ResponseBody
public String notify(HttpServletRequest request) {
// 验签和业务处理
return "success";
}
只有验签通过、订单信息核对无误,并且业务状态已经可靠保存后,才能返回 success。如果订单状态保存失败,应返回 failure 或非成功响应,让支付平台按其重试机制再次发送通知。
检查业务代码是否中途失败
即使回调请求已经到达,下面这些问题仍可能导致订单没有更新:
- 数据库连接失败或事务回滚。
- 根据
out_trade_no找不到订单。 - 使用
double比较金额,产生精度误差。 - 订单已经处理,重复更新时抛出异常。
- 消息队列发送失败后直接终止请求。
- 回调线程等待第三方接口,最终超时。
- 全局异常处理器吞掉了业务异常。
- 订单更新成功后,又被后续逻辑回滚。
金额应使用 BigDecimal 比较:
BigDecimal notifiedAmount = new BigDecimal(params.get("total_amount"));
BigDecimal orderAmount = order.getPayAmount();
if (notifiedAmount.compareTo(orderAmount) != 0) {
throw new IllegalStateException("支付金额与订单金额不一致");
}
回调处理还要支持幂等。支付平台可能重复发送同一笔交易的通知,程序不能因此重复发货、充值或记账。
@Transactional
public void markOrderPaidIdempotently(
String outTradeNo,
String tradeNo,
String totalAmount
) {
Order order = orderRepository.findByOrderNoForUpdate(outTradeNo)
.orElseThrow(() -> new IllegalStateException("订单不存在"));
if (order.isPaid()) {
return;
}
BigDecimal notifiedAmount = new BigDecimal(totalAmount);
if (notifiedAmount.compareTo(order.getPayAmount()) != 0) {
throw new IllegalStateException("支付金额不一致");
}
order.markPaid(tradeNo);
orderRepository.save(order);
}
使用主动查询补偿订单状态
网络抖动、服务发布或短暂故障都可能让异步通知延迟,因此订单状态不能只依赖单次回调。对于长时间停留在“支付中”的订单,可以调用支付查询接口进行补偿确认。
以支付宝交易查询为例:
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent("""
{
"out_trade_no": "ORDER_202609130001"
}
""");
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
String tradeStatus = response.getTradeStatus();
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
// 再次核对订单号、金额、收款方等信息后,幂等更新订单。
}
}
主动查询可用于以下情况:
- 用户已经返回商户页面,但本地订单仍显示未支付。
- 异步通知暂时没有到达。
- 回调处理失败,需要定时补偿。
- 客服需要确认某笔订单的实际支付状态。
用户通过 return_url 返回前端页面后,可以显示“支付结果确认中”,再由后端查询本地订单状态。不要根据前端参数直接把订单改成已支付。
推荐的排查顺序
- 查看支付平台是否记录了异步通知及其处理结果。
- 核对实际支付请求中的
notify_url。 - 从公网测试域名、证书、端口和路由。
- 查看 CDN、WAF、Nginx、网关和应用访问日志。
- 确认接口能够接收
POST表单参数。 - 检查请求是否已经到达,但验签失败。
- 检查订单校验、事务和幂等处理。
- 确认处理成功后返回的是纯文本
success。 - 对仍处于处理状态的订单调用交易查询接口补偿。
如果服务器各层日志中都找不到回调请求,先检查 notify_url 和公网连通性。如果日志中有请求,但订单没有更新,则应检查验签、业务校验、事务和接口响应内容。
备注:内容仅供参考。
