PAYATHON 2026

沙箱测试中支付完成后如何自动跳转到回调地址?

支付老李

明确结论

如果“支付完成”页面来自支付平台的沙箱环境,商户代码通常无法自动点击“已完成支付”按钮,也不能强制跳过这个页面。能否自动跳转,取决于支付平台的沙箱流程和接口能力。

建议按以下方式处理:

  • 通过异步支付通知(Webhook/notify URL)确认支付结果并更新订单。
  • 通过同步返回地址(return URL)接收支付平台的浏览器跳转。
  • 如果同步返回页由商户控制,可以先查询订单状态,确认支付成功后再自动跳转。
  • 如果中间页由支付平台控制,只能检查平台是否提供“自动返回”配置。平台没有提供这项能力时,商户无法绕过该页面。

为什么还需要点击按钮

支付流程通常包含两类地址,它们的用途不同:

  • 异步通知地址:支付平台服务器主动请求商户服务器,用来可靠地通知支付结果。
  • 同步返回地址:用户支付后,浏览器跳转到商户页面,用来展示结果并改善用户体验。

页面上的“已完成支付”按钮通常属于同步返回流程。沙箱可能会保留人工确认步骤,用于模拟不同终端、浏览器拦截或支付状态延迟等情况。

订单是否支付成功,不能以浏览器有没有顺利跳转为准。用户可能关闭页面或遇到网络中断,支付平台的异步通知也可能早于浏览器返回。

推荐处理方式

1. 配置异步通知地址

创建支付订单时,把异步通知地址设置为可从公网访问的 HTTPS 地址,例如:

https://merchant.example.com/api/payment/notify

收到通知后,应验证签名、核对订单信息,并以幂等方式更新订单状态。

下面的代码只展示通用处理结构。签名字段、验签算法和响应格式应以实际支付平台的接口文档为准。

app.post('/api/payment/notify', async (req, res) => {
  const payload = req.body;

  const signatureValid = verifyPaymentSignature(payload);
  if (!signatureValid) {
    return res.status(400).send('invalid signature');
  }

  const order = await findOrder(payload.out_trade_no);
  if (!order) {
    return res.status(404).send('order not found');
  }

  if (String(order.amount) !== String(payload.total_amount)) {
    return res.status(400).send('amount mismatch');
  }

  if (payload.trade_status === 'SUCCESS') {
    // 必须保证重复通知不会造成重复发货、重复入账
    await markOrderPaidOnce(order.id, payload.trade_no);
  }

  // 按支付平台要求返回成功响应
  return res.send('success');
});

2. 配置同步返回地址

如果支付接口支持 return_url 或类似参数,可以将其指向商户自己的支付结果页:

https://merchant.example.com/payment/result?orderNo=ORDER_123

不同支付产品使用的参数名称可能不同,不一定是 return_url,具体应以当前接口文档为准。

3. 在商户结果页查询状态并自动跳转

如果浏览器最终进入了商户控制的页面,可以轮询商户服务器中的订单状态。异步通知将订单更新为支付成功后,再自动跳转到业务页面:

<script>
  const orderNo = new URLSearchParams(location.search).get('orderNo');
  let attempts = 0;
  const maxAttempts = 20;

  async function checkPaymentStatus() {
    attempts += 1;

    const response = await fetch(
      `/api/orders/${encodeURIComponent(orderNo)}/payment-status`,
      { credentials: 'same-origin' }
    );

    if (!response.ok) {
      throw new Error('查询支付状态失败');
    }

    const result = await response.json();

    if (result.status === 'PAID') {
      location.replace(
        `/payment/success?orderNo=${encodeURIComponent(orderNo)}`
      );
      return;
    }

    if (attempts < maxAttempts) {
      setTimeout(checkPaymentStatus, 1500);
    } else {
      document.querySelector('#status').textContent =
        '支付结果仍在确认中,请稍后刷新订单页面。';
    }
  }

  checkPaymentStatus().catch(() => {
    document.querySelector('#status').textContent =
      '暂时无法查询支付结果,请稍后查看订单。';
  });
</script>

<p id="status">正在确认支付结果,请稍候……</p>

订单状态接口应读取商户服务器中保存的订单状态,不能直接相信浏览器传入的“支付成功”参数:

app.get('/api/orders/:orderNo/payment-status', async (req, res) => {
  const order = await findOrderForCurrentUser(
    req.params.orderNo,
    req.user.id
  );

  if (!order) {
    return res.status(404).json({ message: 'order not found' });
  }

  return res.json({
    status: order.paymentStatus
  });
});

如果当前停留在支付平台页面

先确认页面域名以及该页面由谁控制:

  • 如果是商户自己的页面,可以加入订单状态查询和自动跳转逻辑。
  • 如果是支付平台托管的沙箱页面,商户 JavaScript 无法控制,也不应使用模拟点击或浏览器自动化来绕过。
  • 检查支付产品后台或下单接口中是否有“自动返回”“同步跳转”或“支付后返回”等配置。
  • 如果文档中没有相关配置,应把手动点击当作沙箱交互流程,重点检查异步通知能否正常更新订单。

注意事项

  1. 回调与跳转不是一回事。 服务端异步通知不依赖用户操作,浏览器跳转则可能失败,也可能根本不会发生。

  2. 不要根据 URL 参数直接判断支付成功。 返回参数可能被修改,至少需要验签。更稳妥的做法是读取服务端订单状态,必要时主动调用支付查询接口。

  3. 通知处理必须幂等。 支付平台可能重复发送通知,重复通知不能导致重复发货、重复记账或重复发放权益。

  4. 核对关键字段。 除了验签,还要核对商户号、订单号、金额、币种和应用标识等信息。

  5. 不要依靠前端自动跳转完成业务操作。 发货、开通服务等操作应由服务端支付状态驱动。

  6. 沙箱和正式环境可能不同。 是否保留确认按钮、能否自动返回以及何时返回,都应以所用支付产品当前的官方文档和实际接口行为为准。

备注:内容仅供参考。