PAYATHON 2026

安卓钉钉内手机网站支付成功后不跳转 return_url

支付小周

结论

这类问题通常不是 return_url 配置错误,也不代表支付失败。更常见的原因是安卓钉钉内置 WebView、支付宝 App 和业务页面之间的返回链路没有完整恢复。

return_url 只是支付宝收银台结束后的前端跳转地址,不能作为支付成功的可靠依据。安卓钉钉调起支付宝后,支付宝可能只能回到钉钉原来的 WebView 页面,无法继续执行从收银台结果页跳转到 return_url 的流程。此时,页面会停留在支付发起页,也不会收到任何前端回调。

支付结果应以服务端 notify_url 异步通知或主动查询接口为准。要让安卓端获得接近苹果端的体验,可以增加支付中间页和订单状态轮询,必要时引导用户通过系统浏览器完成支付。

原因分析

手机网站支付的页面链路大致如下:

业务页面
  → 支付宝手机网站收银台
  → 支付宝 App
  → 返回收银台结果页
  → 跳转 return_url

在安卓钉钉中,业务页面运行在钉钉内置 WebView 里。调起支付宝 App 后,系统需要恢复支付宝、钉钉和原 WebView 页面之间的上下文。不同宿主 App、WebView 内核和系统版本,对外部应用跳转、URL Scheme 和页面栈恢复的处理并不完全相同。

如果支付宝完成支付后只恢复了钉钉中的原页面,没有恢复支付宝收银台页面,后续的结果页展示和 return_url 跳转也就不会执行。

这也能解释不同环境下的表现差异:

  • iOS 钉钉处理页面返回链路的方式不同;
  • 手机浏览器通常有更完整的页面栈;
  • 安卓钉钉内置 WebView 可能无法按相同方式恢复支付宝收银台页面。

实际表现还可能受到钉钉版本、支付宝版本、Android 系统和默认浏览器设置的影响。没有对应版本和复现信息时,不能直接认定这是某个特定版本的缺陷。

推荐解决方案

1. 通过服务端异步通知确认支付结果

调用 alipay.trade.wap.pay 时,同时配置 notify_url 和 return_url:

AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();

request.setNotifyUrl("https://www.example.com/alipay/notify");
request.setReturnUrl("https://www.example.com/payment/result");

request.setBizContent("""
{
  "out_trade_no": "ORDER_202609090001",
  "total_amount": "99.00",
  "subject": "订单支付",
  "product_code": "QUICK_WAP_WAY"
}
""");

String form = alipayClient.pageExecute(request).getBody();

两者用途不同:

  • notify_url 接收支付宝服务端发送的异步通知;
  • return_url 只处理支付后的页面跳转;
  • 订单是否支付成功,必须根据异步通知的验签结果判断,不能看浏览器有没有跳回。

处理异步通知时,至少需要:

  1. 验证支付宝签名;
  2. 校验 app_id、out_trade_no、金额和收款方等字段;
  3. 判断 trade_status;
  4. 幂等更新订单;
  5. 处理成功后返回纯文本 success。

示例逻辑:

@PostMapping("/alipay/notify")
public String notify(HttpServletRequest request) throws Exception {
    Map<String, String> params = new HashMap<>();

    request.getParameterMap().forEach((key, values) -> {
        if (values != null && values.length > 0) {
            params.put(key, values[0]);
        }
    });

    boolean verified = AlipaySignature.rsaCheckV1(
        params,
        alipayPublicKey,
        "UTF-8",
        "RSA2"
    );

    if (!verified) {
        return "failure";
    }

    String outTradeNo = params.get("out_trade_no");
    String tradeStatus = params.get("trade_status");
    String totalAmount = params.get("total_amount");

    // 还应校验 app_id、seller_id 或收款方信息,以及订单金额。
    if ("TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus)) {
        paymentService.markPaidIdempotently(outTradeNo, totalAmount);
        return "success";
    }

    return "failure";
}

收到通知后,不要跳过校验直接将订单改为已支付。

2. 在支付发起页轮询订单状态

安卓钉钉可能会回到原页面,因此可以在页面重新获得焦点或恢复可见时查询订单状态。服务端确认订单已支付后,再由业务页面跳转到结果页。

<script>
const orderNo = "ORDER_202609090001";
let pollingTimer = null;
let pollingCount = 0;
const maxPollingCount = 30;

async function checkPaymentStatus() {
  try {
    const response = await fetch(
      `/api/orders/${encodeURIComponent(orderNo)}/payment-status`,
      {
        method: "GET",
        credentials: "include",
        cache: "no-store"
      }
    );

    if (!response.ok) {
      return;
    }

    const result = await response.json();

    if (result.status === "PAID") {
      stopPolling();
      location.replace(
        `/payment/result?orderNo=${encodeURIComponent(orderNo)}`
      );
    }
  } catch (error) {
    // 网络暂时不可用时保留页面,等待下一次查询。
  }
}

function startPolling() {
  if (pollingTimer !== null) {
    return;
  }

  checkPaymentStatus();

  pollingTimer = window.setInterval(() => {
    pollingCount += 1;

    if (pollingCount >= maxPollingCount) {
      stopPolling();
      return;
    }

    checkPaymentStatus();
  }, 2000);
}

function stopPolling() {
  if (pollingTimer !== null) {
    window.clearInterval(pollingTimer);
    pollingTimer = null;
  }
}

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible") {
    startPolling();
  }
});

window.addEventListener("focus", startPolling);
window.addEventListener("pageshow", startPolling);

startPolling();
</script>

前端查询的是业务服务端记录的订单状态,不能自行认定支付成功。业务服务端可以读取 notify_url 已经更新的状态。如果异步通知尚未到达,也可以在受控条件下调用支付宝订单查询接口补查。

3. 提供”我已完成支付”按钮

部分 Android WebView 恢复页面后,不一定能可靠触发 focus 或 visibilitychange。支付发起页还应提供手动查询入口:

<button type="button" id="check-payment">
  我已完成支付
</button>

<script>
document
  .getElementById("check-payment")
  .addEventListener("click", checkPaymentStatus);
</script>

页面可以同时显示明确提示:

支付完成后页面未自动跳转,请点击”我已完成支付”查询订单结果,请勿重复付款。

4. 使用独立的支付中间页

复杂的业务页面不适合直接承担支付返回逻辑。可以改用下面的结构:

订单页面
  → 支付中间页
  → 支付宝收银台
  → 支付中间页查询订单状态
  → 业务支付结果页

支付中间页只处理以下事务:

  • 保存订单号;
  • 提交支付宝返回的支付表单;
  • 页面恢复后查询订单状态;
  • 显示”支付处理中”和手动查询按钮;
  • 确认支付后跳转到业务结果页。

如果需要通过钉钉 JSAPI 打开独立页面,可以在钉钉环境初始化完成后调用 dd.biz.util.openLink,打开自有支付中间页,再由该页面提交支付宝支付表单:

dd.biz.util.openLink({
  url: "https://www.example.com/payment/bridge?orderNo=ORDER_202609090001",
  onSuccess() {},
  onFail(error) {
    location.href =
      "https://www.example.com/payment/bridge?orderNo=ORDER_202609090001";
  }
});

这种方式可以隔离页面导航栈,但无法保证所有安卓钉钉、支付宝和系统版本都能恢复支付宝结果页。服务端通知和订单查询机制仍然不能省略。

5. 必要时引导用户使用系统浏览器

如果业务必须展示支付宝原生支付结果页,并要求用户点击”完成”后进入 return_url,那么在内置 WebView 中通常无法完全保证这一流程。

可以检测安卓钉钉环境,并提示用户通过系统浏览器打开:

const ua = navigator.userAgent.toLowerCase();
const isAndroid = ua.includes("android");
const isDingTalk = ua.includes("dingtalk");

if (isAndroid && isDingTalk) {
  document.getElementById("browser-tip").hidden = false;
}

不建议只靠自定义 URL Scheme 强制拉起某个浏览器。不同厂商系统和浏览器的支持情况不同,这种操作也可能被钉钉的安全策略拦截。相对稳妥的方式是显示”在浏览器中打开”的操作说明,或者让用户复制支付链接。

return_url 页面的正确处理方式

即使页面成功进入 return_url,也不能只根据 URL 参数把订单显示为支付成功。结果页仍需向服务端查询订单状态:

async function loadResult() {
  const query = new URLSearchParams(location.search);
  const orderNo = query.get("orderNo");

  if (!orderNo) {
    return;
  }

  const response = await fetch(
    `/api/orders/${encodeURIComponent(orderNo)}/payment-status`,
    {
      credentials: "include",
      cache: "no-store"
    }
  );

  const result = await response.json();

  if (result.status === "PAID") {
    renderPaidResult(result);
  } else {
    renderProcessingResult();
  }
}

如果异步通知还没有到达,页面应显示”支付结果确认中”并进行短时间轮询,不要直接显示失败,也不要让用户立即重复付款。

注意事项

  • return_url 是同步页面跳转地址,不是支付成功的前端回调函数。
  • 页面没有跳转,不能说明支付失败。页面成功跳转,也不能单独证明支付成功。
  • notify_url 必须是支付宝服务器可以通过公网访问的 HTTPS 地址,不能依赖登录状态或前端 Cookie。
  • 支付宝可能重复发送异步通知,因此订单更新必须幂等。
  • 用户回到原页面后,应先查询原订单,不要立即创建新订单或再次发起扣款。
  • 支付表单应使用支付宝 SDK 生成的完整内容,不要自行删改隐藏字段、签名参数或编码。
  • 不要只根据 User-Agent 判断支付结果。环境识别只能用来选择交互方案。
  • 如果问题只出现在特定环境组合中,应记录 Android、钉钉、支付宝和 WebView 的版本,并分别测试安装支付宝和未安装支付宝的场景。
  • 如果必须追查具体的兼容性原因,需要携带复现时间、订单号、客户端版本和跳转链路日志,向钉钉及支付宝技术支持确认。仅凭页面停留现象,无法认定是哪一方的确定缺陷。

实际落地时,可以把支付宝自动跳转作为体验上的补充,把异步通知确认订单和页面恢复后主动查询设为主流程。即使安卓钉钉没有进入支付宝结果页,用户回到原页面后仍能看到正确的支付结果,整体体验也能与苹果端基本一致。

备注:内容仅供参考。