PAYATHON 2026

H5唤醒支付宝App支付后取消支付未回调指定地址

支付老李

结论

H5 唤醒支付宝 App 后,用户点击“取消支付”通常不会触发支付成功回调。notify_url 用于接收服务端异步通知,return_url 用于支付流程完成后的同步跳转,两者都不能作为取消按钮的可靠回调。

建议按以下方式处理:

  1. 创建支付请求时配置 quit_url,让用户中途退出支付后返回业务页面。
  2. 同时配置 return_url 和 notify_url,分别处理同步跳转和服务端异步通知。
  3. H5 页面重新可见后,由前端请求业务服务端查询订单状态。
  4. 服务端以支付宝异步通知或 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 时,至少需要完成以下检查:

  1. 使用支付宝公钥验证签名。
  2. 校验 app_id 是否属于当前应用。
  3. 校验 out_trade_no 是否存在且属于当前商户。
  4. 校验订单金额、币种和收款方信息。
  5. 根据 trade_status 更新订单。
  6. 保证处理幂等,避免同一通知重复到达时重复发货或记账。
  7. 处理成功后,按照接口要求返回成功响应。

伪代码如下:

@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 页面无法完全控制客户端是否自动切回原浏览器,因此还必须保留用户手动返回后的查单机制。

备注:内容仅供参考。