支付宝“电脑网站支付”成功后未收到callback通知
结论
“电脑网站支付”完成后,网站余额不应依靠浏览器跳转页面来更新。入账应以支付宝服务器发送到 notify_url 的异步通知为准。
排查时主要确认以下事项:
- 创建支付请求时是否正确设置了
notify_url。 notify_url能否从公网直接访问。- 支付宝请求是否已经到达服务器,却被网关、鉴权、重定向或程序异常拦截。
- 通知验签和订单校验是否正确。
- 处理成功后是否向支付宝返回纯文本
success。 - 对未收到通知或处理失败的订单,是否通过
alipay.trade.query主动补查。
return_url 只是用户浏览器的同步跳转地址。如果用户关闭页面、网络中断或浏览器拦截跳转,该地址可能不会被访问,因此不能作为确认支付成功和余额入账的依据。
先区分两个通知地址
电脑网站支付通常会用到两个地址:
return_url:支付完成后,支付宝引导用户浏览器跳回网站。notify_url:支付宝服务器向商户服务器发送异步POST通知。
到账逻辑应放在 notify_url 中。典型流程如下:
创建本地待支付订单
↓
调用 alipay.trade.page.pay
↓
用户在支付宝完成付款
↓
支付宝服务器 POST notify_url
↓
网站验签并核对订单
↓
更新订单和用户余额
↓
返回 success
即使 return_url 正常跳转,也不能只凭跳转参数认定支付成功。
检查支付请求是否设置了 notify_url
以支付宝 Java SDK 的常见调用方式为例:
AlipayClient alipayClient = new DefaultAlipayClient(
GATEWAY_URL,
APP_ID,
MERCHANT_PRIVATE_KEY,
"json",
"UTF-8",
ALIPAY_PUBLIC_KEY,
"RSA2"
);
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setReturnUrl("https://www.example.com/payment/return");
request.setNotifyUrl("https://www.example.com/api/alipay/notify");
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo("ORDER_202609080001");
model.setTotalAmount("100.00");
model.setSubject("账户充值");
model.setProductCode("FAST_INSTANT_TRADE_PAY");
request.setBizModel(model);
AlipayTradePagePayResponse response =
alipayClient.pageExecute(request, "POST");
String paymentForm = response.getBody();
检查时应以实际提交给支付宝的请求为准,不能只看配置文件:
notify_url是否为空。- 地址是否拼写错误。
- 测试环境和生产环境是否使用了不同配置。
- URL 是否仍然指向旧域名、内网域名或临时地址。
- 是否只设置了
return_url,却没有设置notify_url。
不同语言和 SDK 版本的方法名可能略有差异,但支付请求必须正确传递 notify_url。
确认通知地址可以被公网访问
支付宝服务器必须能直接访问 notify_url。以下地址通常不能用于正式的异步通知:
http://localhost/alipay/notify
http://127.0.0.1/alipay/notify
http://192.168.1.10/alipay/notify
通知接口还需要满足以下条件:
- 域名解析正确。
- 端口允许公网访问。
- HTTPS 证书有效,证书链完整。
- 防火墙、WAF、CDN 和安全组没有拦截请求。
- 接口不要求登录态、Cookie、验证码或 CSRF Token。
- 请求不会被强制跳转到登录页。
- 路由支持
POST。 - 接口可以读取表单参数,而不是只接收 JSON。
- 响应时间足够短,不在通知请求中执行耗时任务。
可以先查看 Web 服务器访问日志,搜索通知路径:
/api/alipay/notify
根据日志结果继续排查:
- 完全没有访问记录:先检查
notify_url、公网连通性、DNS、防火墙和支付请求参数。 - 有访问记录但没有入账:继续检查 HTTP 状态码、应用日志、验签、订单校验、数据库事务和响应内容。
使用 curl 只能确认接口是否可以访问,无法模拟有效的支付宝通知,因为伪造参数不能通过支付宝签名验证。例如:
curl -i -X POST https://www.example.com/api/alipay/notify \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'test=connectivity'
业务代码将这个请求判定为验签失败属于正常现象。
正确读取并验证异步通知
支付宝异步通知通常以 application/x-www-form-urlencoded 格式提交。服务端应读取全部表单参数,再使用支付宝公钥验签。
下面是一个简化的 Spring MVC 示例:
@PostMapping(
value = "/api/alipay/notify",
produces = "text/plain;charset=UTF-8"
)
@ResponseBody
public String alipayNotify(HttpServletRequest request) {
try {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry
: request.getParameterMap().entrySet()) {
String value = String.join(",", entry.getValue());
params.put(entry.getKey(), value);
}
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 outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
if (!APP_ID.equals(appId)) {
return "failure";
}
if (!EXPECTED_SELLER_ID.equals(sellerId)) {
return "failure";
}
boolean paid =
"TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus);
if (!paid) {
return "success";
}
paymentService.confirmPayment(
outTradeNo,
tradeNo,
new BigDecimal(totalAmount)
);
return "success";
} catch (Exception e) {
// 应记录异常及订单号,但不要记录私钥等敏感信息
return "failure";
}
}
实际项目还需要核对这些信息:
app_id是否为本系统使用的支付宝应用。seller_id是否属于预期收款方。out_trade_no是否存在于本地订单表。total_amount是否与本地订单金额完全一致。- 币种和业务类型是否符合订单预期。
trade_no是否已经绑定其他本地订单。trade_status是否表示支付成功。
比较金额时不要使用 double 或 float,应使用 Java 的 BigDecimal 等定点小数类型:
BigDecimal notifiedAmount = new BigDecimal(totalAmount);
BigDecimal orderAmount = order.getAmount();
if (notifiedAmount.compareTo(orderAmount) != 0) {
throw new IllegalStateException("支付金额不一致");
}
验签时必须使用支付宝公钥,不能使用应用私钥或应用公钥。如果应用采用支付宝公钥证书模式,应使用对应的证书验签方式,不能直接沿用普通公钥模式的配置。
必须保证幂等入账
异步通知可能重复发送。同一通知到达两次时,系统不能给用户增加两次余额。
可以在同一个数据库事务中完成订单更新和余额变更,并通过条件更新或唯一约束保证幂等:
UPDATE recharge_order
SET status = 'PAID',
alipay_trade_no = :trade_no,
paid_at = CURRENT_TIMESTAMP
WHERE out_trade_no = :out_trade_no
AND status = 'PENDING';
只有受影响行数为 1 时,才执行余额增加:
UPDATE user_account
SET balance = balance + :amount
WHERE user_id = :user_id;
更稳妥的做法是把订单状态变更、账户流水写入和余额更新放在同一事务中,同时为支付流水设置唯一键,例如:
UNIQUE (alipay_trade_no)
如果订单已经是 PAID,说明此前处理成功,应直接返回 success,不要再次入账。
响应内容必须符合要求
成功处理通知后,应直接返回:
success
不要返回 HTML、JSON 或附带其他文字。例如,下面这些响应都不合适:
{"status":"success"}
<html>success</html>
success processed
还要检查框架是否自动包装响应,以及反向代理是否将请求重定向到其他页面。常见问题包括:
- 返回
302,跳转到登录页。 - Spring 接口没有添加
@ResponseBody,导致success被当作模板名称。 - 程序已经完成处理,但随后抛出异常,最终返回
500。 - 返回成功前执行邮件、消息推送等耗时操作,导致请求超时。
- 数据库事务尚未提交,程序就提前返回成功。
应先完成验签、订单校验和持久化,再返回 success。邮件、消息推送等非关键任务可以交给消息队列或后台任务处理。
使用交易查询进行补偿
系统不能只依赖异步通知确认订单。对于长时间处于 PENDING 状态的订单,可以调用 alipay.trade.query 主动查询。
Java 示例:
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo("ORDER_202609080001");
request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
String tradeStatus = response.getTradeStatus();
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
// 核对订单号、金额和收款方后,按幂等方式补记支付结果
}
}
交易查询适用于以下情况:
- 用户支付后已经返回网站,但本地订单仍是待支付状态。
- 异步通知因临时网络故障未能及时处理。
- 通知已经到达,但当时数据库或应用发生异常。
- 定时任务扫描到长期未完成的充值订单。
处理查询结果时,仍然要校验金额、订单号和收款方,并保证补记操作幂等。
推荐的排查顺序
按以下顺序检查,通常可以较快找到问题:
- 在支付请求日志中确认
notify_url的实际值。 - 确认该地址能够从公网访问,并支持匿名
POST。 - 查看 Nginx、网关、CDN 或负载均衡日志,确认通知是否到达。
- 查看接口返回的 HTTP 状态码,确认是否发生重定向。
- 检查接口是否按表单方式读取参数。
- 检查支付宝公钥、字符集和签名类型是否与应用配置一致。
- 检查
app_id、seller_id、订单号和金额校验。 - 检查数据库事务、订单状态条件和账户流水。
- 确认处理成功后只返回纯文本
success。 - 使用
alipay.trade.query查询问题订单,并按幂等方式补偿。
不要通过关闭验签、信任前端跳转参数或只凭 trade_status 入账来处理通知问题。这些做法会带来伪造支付和重复充值的风险。
备注:内容仅供参考。
