H5唤醒支付宝App支付后取消支付未回调指定地址
结论
H5 唤醒支付宝 App 后,用户点击“取消支付”通常不会触发支付成功回调。notify_url 用于接收服务端异步通知,return_url 用于支付流程完成后的同步跳转,两者都不能作为取消按钮的可靠回调。
建议按以下方式处理:
- 创建支付请求时配置
quit_url,让用户中途退出支付后返回业务页面。 - 同时配置
return_url和notify_url,分别处理同步跳转和服务端异步通知。 - H5 页面重新可见后,由前端请求业务服务端查询订单状态。
- 服务端以支付宝异步通知或
alipay.trade.query的查询结果为准,不能根据前端跳转直接判断支付成功或取消。
为什么取消支付后没有回调
支付页面跳转和支付结果通知是两套不同的机制:
return_url:同步跳转地址。用户完成支付流程后,支付宝可能将页面跳转到该地址。notify_url:异步通知地址。支付宝服务器通过该地址向商户服务器通知交易状态。quit_url:用户在支付过程中主动退出时的返回地址。alipay.trade.query:商户主动查询订单当前状态的接口。
用户点击取消时,订单可能仍处于等待支付状态,例如 WAIT_BUYER_PAY。这不代表支付失败,也不一定会产生可供业务处理的异步通知。因此,业务端不能只等待 notify_url,也不能假设支付宝 App 一定会通过 return_url 返回原页面。
能否自动回到浏览器,还会受到支付宝客户端版本、手机系统、默认浏览器、Universal Link/App Link、Scheme 和用户操作路径等因素影响。即使配置了退出地址,也要保留“返回页面后主动查单”的兜底逻辑。
配置退出地址
使用 alipay.trade.wap.pay 时,如果接口支持,可以在业务参数中设置 quit_url:
{
"out_trade_no": "ORDER_202609130001",
"total_amount": "99.00",
"subject": "订单支付",
"product_code": "QUICK_WAP_WAY",
"quit_url": "https://pay.example.com/alipay/cancel?order_no=ORDER_202609130001"
}
同步返回地址和异步通知地址通常通过请求公共参数配置:
return_url=https://pay.example.com/alipay/return
notify_url=https://api.example.com/payment/alipay/notify
不同 SDK 对 return_url、notify_url 和业务参数的设置方式可能不同,具体应以当前使用的支付宝 SDK 和接口文档为准。不要将 quit_url 放在 SDK 无法识别的位置。
退出地址必须是支付宝客户端能够访问的公网 HTTP 或 HTTPS 地址,不能使用:
localhost
127.0.0.1
file://...
建议使用 HTTPS,并正确编码订单号等 URL 查询参数。
退出页面不能直接判定订单已取消
quit_url 只能说明用户离开了当前支付流程,不能证明订单已经关闭。用户可能已经完成付款,只是页面跳转和服务端通知之间存在时间差。
退出页面加载后,应立即向业务服务端查询订单状态:
<script>
const params = new URLSearchParams(location.search);
const orderNo = params.get("order_no");
async function refreshPaymentStatus() {
if (!orderNo) return;
const response = await fetch(
`/api/orders/${encodeURIComponent(orderNo)}/payment-status`,
{
credentials: "include",
cache: "no-store"
}
);
if (!response.ok) {
throw new Error("查询支付状态失败");
}
const result = await response.json();
if (result.status === "PAID") {
location.replace(`/orders/${encodeURIComponent(orderNo)}/success`);
return;
}
if (result.status === "CLOSED") {
location.replace(`/orders/${encodeURIComponent(orderNo)}/closed`);
return;
}
// 未支付不一定等于已取消,可显示“继续支付”或“取消订单”按钮。
document.querySelector("#payment-status").textContent = "订单尚未支付";
}
refreshPaymentStatus().catch(() => {
document.querySelector("#payment-status").textContent =
"暂时无法确认支付结果,请稍后刷新";
});
</script>
前端接口返回的状态应来自商户服务端数据库。必要时,服务端可以再调用 alipay.trade.query 向支付宝查单。不要让浏览器携带商户私钥直接调用支付宝接口。
页面重新可见时主动查单
有些情况下,用户需要手动从支付宝切回浏览器。此时可以监听 visibilitychange、pageshow 和 focus,在页面恢复后刷新订单状态:
let querying = false;
async function queryAfterResume() {
if (querying || document.visibilityState === "hidden") {
return;
}
querying = true;
try {
await refreshPaymentStatus();
} finally {
querying = false;
}
}
document.addEventListener("visibilitychange", queryAfterResume);
window.addEventListener("pageshow", queryAfterResume);
window.addEventListener("focus", queryAfterResume);
这些事件可能连续触发,因此需要避免重复请求。事件触发只能说明页面恢复可见,不能证明用户已经支付或取消。
服务端异步通知处理
确认支付成功时,应优先采用支付宝服务端的异步通知。处理 notify_url 时,至少需要完成以下检查:
- 使用支付宝公钥验证签名。
- 校验
app_id是否属于当前应用。 - 校验
out_trade_no是否存在且属于当前商户。 - 校验订单金额、币种和收款方信息。
- 根据
trade_status更新订单。 - 保证处理幂等,避免同一通知重复到达时重复发货或记账。
- 处理成功后,按照接口要求返回成功响应。
伪代码如下:
@PostMapping("/payment/alipay/notify")
public String handleNotify(HttpServletRequest request) {
Map<String, String> params = extractAlipayParams(request);
if (!verifyAlipaySignature(params)) {
return "failure";
}
Order order = orderService.findByOrderNo(params.get("out_trade_no"));
if (order == null) {
return "failure";
}
if (!order.getAmount().toPlainString().equals(params.get("total_amount"))) {
return "failure";
}
String tradeStatus = params.get("trade_status");
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
orderService.markPaidIdempotently(
order.getOrderNo(),
params.get("trade_no")
);
}
return "success";
}
代码中的验签方法、参数提取方式和金额比较规则,应按照所用支付宝 SDK 实现。生产环境中不要自行拼装签名算法。
主动查单与关闭订单
如果用户返回后订单仍未支付,服务端可以调用 alipay.trade.query 查询订单:
{
"out_trade_no": "ORDER_202609130001"
}
可以根据查询结果分别处理:
- 查询结果为支付成功:将商户订单更新为已支付。
- 查询结果仍为等待支付:页面显示“继续支付”或“取消订单”。
- 查询结果为交易关闭:将商户订单更新为已关闭。
- 暂时查询不到:不要直接判断为支付失败,可以稍后重试。
如果业务要求用户点击“取消订单”后立即失效,应由商户服务端执行取消流程,并在确认订单尚未支付后调用对应的关闭交易接口。不能因为用户进入了 quit_url 就直接关闭订单,否则可能与稍晚到达的支付成功通知发生冲突。
注意事项
return_url携带的参数也必须验签。即使验签通过,仍建议通过服务端订单状态确认支付结果。notify_url必须能够被支付宝服务器访问,不能依赖登录状态、Cookie 或前端跨域配置。- 不要在前端回调页面直接执行发货、充值或发券等操作。
- “用户退出支付页”“订单未支付”和“订单已关闭”是三种不同的状态,不能混为一谈。
- 异步通知、主动查单和关闭订单都要做好并发控制与幂等处理。
- 如果已经配置
quit_url,但仍然无法返回,应检查该参数是否实际进入签名请求、URL 是否能从公网访问,以及当前 SDK 和接口是否支持此参数。H5 页面无法完全控制客户端是否自动切回原浏览器,因此还必须保留用户手动返回后的查单机制。
备注:内容仅供参考。
