PAYATHON 2026

Android app callback 如何返回 Chrome 已打开的 tab?

支付老李

结论

通常不行。支付 App 通过 HTTP/HTTPS callback URL 返回 Chrome 时,实际上是在 Android 上发起了一个新的 Intent。是否复用已有 tab 由 Chrome 决定,网页和第三方支付 App 都无法可靠指定之前打开的某个 tab。

更可靠的做法是保留原支付页面,通过后端轮询、WebSocket,或在页面重新获得焦点时查询支付状态。支付 App 的 callback 只用于配合服务端确认状态,不应依赖它将用户准确带回原 tab。

推荐方案:由原页面查询支付结果

支付开始后,原 tab 定期向服务端查询订单状态。用户从支付 App 切回 Chrome 时,页面再立即查询一次。

const orderId = "ORDER_ID";
let timer;

async function checkPaymentStatus() {
  const response = await fetch(
    `/api/payments/${encodeURIComponent(orderId)}/status`,
    {
      credentials: "include",
      cache: "no-store"
    }
  );

  if (!response.ok) {
    throw new Error(`Status request failed: ${response.status}`);
  }

  const result = await response.json();

  if (result.status === "paid") {
    clearInterval(timer);
    window.location.replace(`/payment/success?orderId=${encodeURIComponent(orderId)}`);
  }
}

timer = setInterval(() => {
  checkPaymentStatus().catch(console.error);
}, 3000);

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible") {
    checkPaymentStatus().catch(console.error);
  }
});

window.addEventListener("focus", () => {
  checkPaymentStatus().catch(console.error);
});

后端必须根据支付平台的服务端通知或主动查询结果更新订单状态,不能只凭浏览器 callback 中的参数判断支付是否成功。

app.get("/api/payments/:orderId/status", async (req, res) => {
  const payment = await paymentRepository.findByOrderId(req.params.orderId);

  if (!payment || payment.userId !== req.user.id) {
    return res.sendStatus(404);
  }

  res.json({
    status: payment.status
  });
});

整个流程通常是:

  1. 网页向服务端创建订单并取得 orderId。
  2. 网页打开支付 App,同时保留当前页面。
  3. 支付平台向服务端发送支付结果通知。
  4. 服务端校验签名、金额、商户号和订单号,再更新订单状态。
  5. 用户回到原来的 Chrome tab。
  6. 原页面查询到支付成功后,显示结果或跳转到成功页。

即使 callback 打开了新 tab,原 tab 仍能获得正确的支付结果。

如果 callback 已经打开新 tab

如果新旧页面同源,callback 页面可以通过 BroadcastChannel 通知原 tab。原 tab 收到消息后,再更新状态或跳转:

// 原支付页面
const channel = new BroadcastChannel("payment-result");

channel.addEventListener("message", event => {
  const message = event.data;

  if (
    message?.type === "payment-completed" &&
    message.orderId === currentOrderId
  ) {
    checkPaymentStatus().catch(console.error);
  }
});

callback 页面负责发送通知:

const params = new URLSearchParams(window.location.search);
const orderId = params.get("orderId");

const channel = new BroadcastChannel("payment-result");

channel.postMessage({
  type: "payment-completed",
  orderId
});

channel.close();

document.body.textContent = "支付结果已同步,请返回之前的页面。";

如果需要考虑更广泛的兼容性,也可以使用 localStorage 和 storage 事件:

// callback 页面
localStorage.setItem(
  "payment-result",
  JSON.stringify({
    orderId,
    timestamp: Date.now()
  })
);
// 原支付页面
window.addEventListener("storage", event => {
  if (event.key !== "payment-result" || !event.newValue) {
    return;
  }

  const result = JSON.parse(event.newValue);

  if (result.orderId === currentOrderId) {
    checkPaymentStatus().catch(console.error);
  }
});

这些方法只能实现 tab 之间的通信,无法强制 Chrome 切换到指定 tab。

callback 页面还可以尝试调用:

window.close();

不过,浏览器通常只允许脚本关闭由脚本打开的窗口或 tab。Android Intent 创建的 tab 很可能无法关闭,所以这种方式只能作为辅助,不能作为流程依赖。

如果同时控制 Android App

即使 Android App 是自行开发的,也不能假设使用 Intent flags 就能准确找到某个 Chrome tab。Chrome 内部的 tab 由 Chrome 管理,FLAG_ACTIVITY_* 主要影响 Android Activity 和 task,并不等于选择浏览器中的特定 tab。

如果流程必须运行在受控的页面容器中,可以让自己的 Android App 使用 Chrome Custom Tabs 打开支付页,并由 App 处理 deep link 或 App Link 返回。这样可以控制应用级的导航流程,但解决的是自己的 App 如何与 Custom Tab 协作,无法接管用户在普通 Chrome 中已经打开的任意 tab。

注意事项

  • callback URL 应使用唯一且不可猜测的订单标识或 state,并由服务端校验其有效期和归属。
  • 不要根据 ?status=success 之类的前端参数直接认定支付成功。
  • 必须验证支付平台服务端通知的签名,并核对订单号、商户号、金额和币种。
  • callback 处理必须具备幂等性,避免重复回调造成重复入账或重复发货。
  • BroadcastChannel 和 localStorage 通信通常要求页面同源。不同子域也可能因 origin 不同而无法直接通信。
  • 移动浏览器会限制后台页面的定时器,因此需要结合 visibilitychange、focus 和服务端状态查询。
  • 不要依赖 window.opener。从 Android App 启动的新 tab 通常没有可用的 opener 关系。

可靠的方案不是强制 callback 复用 Chrome 中的旧 tab,而是让原 tab 能自行恢复并查询最终支付状态。新 tab 只需作为提示页或辅助通知页面。

备注:内容仅供参考。