支付宝 H5 收银台“完成支付”的具体含义是什么?
明确结论
这里的“完成支付”,更准确地说,是用户结束了支付宝收银台中的支付操作,进入前台回跳阶段。它并不等于“支付宝已经成功扣款”。
跳转到 return_url,只说明浏览器从支付宝页面返回了商家页面,不能据此确认交易成功。支付失败、交易仍在处理中或出现异常时,都不能只根据是否回跳来判断结果。反过来,即使支付已经成功,用户直接关闭页面、网络中断或回跳被拦截,也可能导致商家收不到 return_url 请求。
最终支付结果应主要以支付宝发送到 notify_url 的服务端异步通知为准。必要时,再调用 alipay.trade.query 主动查询确认。
为什么不能依赖 return_url
return_url 是前台同步跳转地址,主要用于改善用户体验,比如展示“正在确认支付结果”,或者引导用户返回订单详情页。它有几个天然限制:
- 用户可能在支付后直接关闭支付宝或浏览器,导致回跳没有发生。
- 浏览器跳转可能因网络、系统 WebView 或页面状态而失败。
- URL 参数可能被用户修改,未经验证签名不能信任。
- 收到回跳,并不代表交易状态已经最终确定。
- 支付失败后是否回跳取决于具体的收银台流程,不能假定所有失败场景都会访问
return_url。
因此,业务系统应把 return_url 理解为“用户回来了”,而不是“钱到账了”。
正确的处理流程
支付结果确认流程可以这样设计:
- 创建支付订单时,同时传入
return_url和notify_url。 - 用户从支付宝回到
return_url后,前端只展示“支付结果确认中”。 - 商家后端验证同步返回参数的签名,但不直接据此发货或充值。
- 商家后端等待并处理支付宝发送到
notify_url的异步通知。 - 收到通知后完成验签,并核对订单号、商户身份、金额等业务字段。
- 确认交易状态成功后,再把本地订单更新为已支付。
- 如果异步通知还没有到达,可以调用
alipay.trade.query查询交易状态。 - 订单状态更新必须具备幂等性,避免重复通知引发重复发货。
三者的职责可以概括为:
return_url:负责页面跳转和用户体验
notify_url:负责服务端支付结果通知
alipay.trade.query:负责主动补查和状态确认
return_url 应如何处理
前台回跳页面不应直接显示“支付成功”,也不能因为参数中出现了 trade_no、out_trade_no 等字段,就修改订单状态。
比较稳妥的做法是让页面调用商家自己的订单查询接口:
async function checkPaymentResult(outTradeNo) {
const response = await fetch(
`/api/orders/${encodeURIComponent(outTradeNo)}/payment-status`,
{
credentials: "include"
}
);
if (!response.ok) {
throw new Error("查询订单状态失败");
}
const result = await response.json();
if (result.status === "PAID") {
window.location.href = `/payment/success?orderNo=${encodeURIComponent(outTradeNo)}`;
return;
}
if (result.status === "CLOSED") {
window.location.href = `/payment/failed?orderNo=${encodeURIComponent(outTradeNo)}`;
return;
}
// WAITING 表示商家服务端尚未确认最终结果
document.querySelector("#payment-status").textContent =
"支付结果确认中,请稍候刷新";
}
这里查询的是商家数据库中保存的订单状态,而不是让浏览器自行认定支付宝交易已经成功。
异步通知处理示例
下面是一个简化的 Java 示例。具体类名和方法签名,应以项目实际使用的支付宝 Java SDK 版本为准。
@PostMapping("/alipay/notify")
@ResponseBody
public String handleAlipayNotify(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,
charset,
signType
);
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");
// 以下值必须与商家创建订单时保存的数据进行比较
Order order = orderService.findByOrderNo(outTradeNo);
if (order == null) {
return "failure";
}
if (!expectedAppId.equals(appId)
|| !expectedSellerId.equals(sellerId)
|| new BigDecimal(totalAmount).compareTo(order.getAmount()) != 0) {
return "failure";
}
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
// 必须采用幂等更新,重复通知不能重复发货或重复记账
orderService.markPaidIfUnpaid(outTradeNo, tradeNo);
} else if ("TRADE_CLOSED".equals(tradeStatus)) {
orderService.markClosedIfUnpaid(outTradeNo);
}
// 只有通知已被正确处理后才返回 success
return "success";
} catch (Exception e) {
return "failure";
}
}
验签只能确认通知来自可信来源,不能替代业务校验。至少还要核对:
app_id是否属于当前应用;seller_id是否属于当前收款商户;out_trade_no是否对应本系统创建的订单;total_amount是否与订单应付金额一致;- 订单是否已经处理,避免重复记账;
- 通知中的交易状态是否符合当前业务的成功条件。
主动查询支付结果
如果用户已经返回商家页面,但异步通知尚未处理完成,可以由商家服务端调用 alipay.trade.query:
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent("""
{
"out_trade_no": "%s"
}
""".formatted(outTradeNo));
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (!response.isSuccess()) {
// 查询失败不等于支付失败,应记录错误并稍后重试
return;
}
String tradeStatus = response.getTradeStatus();
switch (tradeStatus) {
case "TRADE_SUCCESS":
case "TRADE_FINISHED":
orderService.markPaidIfUnpaid(
outTradeNo,
response.getTradeNo()
);
break;
case "TRADE_CLOSED":
orderService.markClosedIfUnpaid(outTradeNo);
break;
case "WAIT_BUYER_PAY":
// 尚未完成付款,保持待支付状态
break;
default:
// 未知状态不要擅自认定成功或失败
break;
}
常见交易状态可以这样理解:
WAIT_BUYER_PAY:等待买家付款;TRADE_SUCCESS:交易支付成功;TRADE_FINISHED:交易结束,通常不能再进行常规退款操作;TRADE_CLOSED:交易关闭。
实际可用的状态及其业务含义,应以当前接入产品和支付宝开放平台文档为准。
需要特别注意的事项
不要把回跳当作成功凭证
下面这些判断都不可靠:
访问了 return_url,所以支付成功
URL 中有 trade_no,所以支付成功
浏览器显示支付宝成功页,所以支付成功
用户截图显示已付款,所以支付成功
发货、充值、开通会员等业务状态,必须由商家服务端判断。
异步通知可能重复到达
支付宝可能重复发送通知,因此商家的处理逻辑必须保证幂等。例如,可以使用条件更新:
UPDATE orders
SET status = 'PAID',
alipay_trade_no = ?
WHERE order_no = ?
AND status = 'UNPAID';
只有第一次更新成功时,才能执行后续发货。不能每收到一次通知就重新发货。
查询失败不等于支付失败
调用 alipay.trade.query 时,如果发生超时、网络错误或系统错误,只能说明本次查询没有得到可靠结果,不能立即把订单标记为失败。此时应保留订单原状态,再根据业务策略重试或定时补单。
前台参数也必须验签
如果业务需要读取 return_url 中的参数,应由服务端按照支付宝规则验签。即使验签通过,前台结果也更适合用于页面展示和触发查询,不能取代异步通知或主动查询。
因此,这里的“完成支付”不应被严格理解为“成功扣款”,它描述的是收银台流程结束后的前台回跳行为。商家确认最终支付结果时,应以经过验签和业务校验的异步通知为主,以 alipay.trade.query 作为补充。前端展示和履约操作,则应始终以商家服务端保存的订单状态为依据。
备注:内容仅供参考。
