PAYATHON 2026

Single html page application 如何接收支付回调参数

支付小周

结论

如果支付网关会把用户浏览器重定向到 index.html?status=success&order_id=123,可以用 JavaScript 的 URLSearchParams 读取 URL 中的查询参数。

如果 callback 指的是支付网关向指定地址发送的服务器回调(Webhook),仅有静态 index.html 的应用无法可靠接收和处理。你需要提供后端接口或 Serverless Function,用来接收回调、验证签名并更新订单状态。

读取浏览器回跳参数

假设支付完成后的跳转地址是:

https://example.com/index.html?status=success&order_id=123&transaction_id=abc456

页面可以这样读取参数:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>支付结果</title>
</head>
<body>
  <div id="payment-result">正在查询支付结果……</div>

  <script>
    const params = new URLSearchParams(window.location.search);

    const status = params.get("status");
    const orderId = params.get("order_id");
    const transactionId = params.get("transaction_id");

    console.log({
      status,
      orderId,
      transactionId
    });

    const resultElement = document.getElementById("payment-result");

    if (!orderId) {
      resultElement.textContent = "缺少订单号,无法查询支付结果";
    } else {
      resultElement.textContent = `订单 ${orderId} 正在确认支付状态`;
    }
  </script>
</body>
</html>

参数不存在时,URLSearchParams.get() 会返回 null。如果同一个参数可能出现多次,可以使用:

const values = params.getAll("item");

如果支付网关把参数放在 URL Hash 中,例如:

https://example.com/index.html#status=success&order_id=123

则需要读取 window.location.hash:

const hashParams = new URLSearchParams(
  window.location.hash.replace(/^#/, "")
);

const status = hashParams.get("status");
const orderId = hashParams.get("order_id");

究竟使用查询字符串还是 Hash,要以支付网关的回跳协议为准。

不要直接相信 URL 中的支付状态

用户可以自行修改浏览器地址:

?status=success&order_id=123

所以,前端读到 status=success 不能证明支付已经成功。正确的处理流程是:

  1. 前端从回跳 URL 中取得订单号。
  2. 前端向自己的后端查询订单状态。
  3. 后端根据经过验证的支付回调,或通过支付网关查询接口,返回真实状态。
  4. 前端根据后端返回的结果显示成功、失败或处理中。

前端示例:

async function loadPaymentStatus(orderId) {
  const response = await fetch(
    `/api/orders/${encodeURIComponent(orderId)}/payment-status`,
    {
      method: "GET",
      credentials: "include"
    }
  );

  if (!response.ok) {
    throw new Error("无法查询支付状态");
  }

  return response.json();
}

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

if (orderId) {
  loadPaymentStatus(orderId)
    .then((result) => {
      document.getElementById("payment-result").textContent =
        result.paid ? "支付成功" : "支付尚未确认";
    })
    .catch(() => {
      document.getElementById("payment-result").textContent =
        "支付结果查询失败,请稍后重试";
    });
}

服务器回调需要后端接口

支付系统通常会提供两类地址:

  • return_url:支付完成后,用户浏览器跳回的商户页面。
  • callback_url、notify_url 或 webhook_url:支付网关主动请求的商户服务器地址。

不同支付网关使用的名称可能不同,具体要以接口文档为准。

服务器回调通常通过 POST 请求发送,内容可能包括 JSON、表单数据、签名和时间戳。静态 HTML 不是服务器程序,既无法读取请求 Body,也不能安全地保存密钥或验证签名。

可以建立类似下面的接口:

POST https://example.com/api/payment/callback

下面是一个简化的 Node.js/Express 结构示例。签名算法和字段名称必须按照实际支付网关的文档实现:

import express from "express";

const app = express();

app.use(express.json());

app.post("/api/payment/callback", async (req, res) => {
  const signature = req.get("X-Payment-Signature");
  const payload = req.body;

  const valid = verifyGatewaySignature(payload, signature);

  if (!valid) {
    return res.status(401).send("invalid signature");
  }

  const { order_id, status, transaction_id } = payload;

  // 应确保重复回调不会导致重复记账
  await updateOrderPaymentStatus({
    orderId: order_id,
    status,
    transactionId: transaction_id
  });

  return res.status(200).send("ok");
});

如果不想维护传统服务器,也可以使用云平台提供的 Serverless Function、Edge Function 或 API 服务。不过,这些仍然属于后端逻辑,不能全部放进 index.html。

推荐的完整流程

index.html 发起支付
        ↓
支付网关处理支付
        ↓
支付网关通知后端 callback URL
        ↓
后端验证签名并更新订单
        ↓
浏览器跳回 index.html?order_id=123
        ↓
index.html 请求后端查询订单状态
        ↓
页面展示最终支付结果

服务器回调和浏览器回跳的到达顺序并不固定。用户返回页面时,服务器回调可能还没有处理完成。前端因此需要支持“处理中”状态,并在短时间内适度轮询:

async function waitForPaymentResult(orderId, maxAttempts = 5) {
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const result = await loadPaymentStatus(orderId);

    if (result.status === "paid" || result.status === "failed") {
      return result;
    }

    await new Promise((resolve) => setTimeout(resolve, 2000));
  }

  return { status: "pending" };
}

注意事项

  • 不要把支付密钥、签名私钥或网关 Secret 写进 HTML 或前端 JavaScript。
  • 不要只根据 URL 中的 status 修改订单或发放商品。
  • 回调接口必须验证支付网关签名,同时核对订单号、金额、币种和商户信息。
  • 回调处理需要具备幂等性,因为支付网关可能重复发送通知。
  • 在页面中显示参数时应使用 textContent,不要直接拼接到 innerHTML,以免引入 XSS 风险。
  • 如果应用使用 /payment/result 这样的前端路由,静态服务器需要配置回退到 index.html,否则直接访问该地址可能返回 404。
  • 支付结果应以服务端验证后的记录为准。浏览器回跳参数只用于帮助页面定位订单和改善用户体验。

备注:内容仅供参考。