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 不能证明支付已经成功。正确的处理流程是:
- 前端从回跳 URL 中取得订单号。
- 前端向自己的后端查询订单状态。
- 后端根据经过验证的支付回调,或通过支付网关查询接口,返回真实状态。
- 前端根据后端返回的结果显示成功、失败或处理中。
前端示例:
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。 - 支付结果应以服务端验证后的记录为准。浏览器回跳参数只用于帮助页面定位订单和改善用户体验。
备注:内容仅供参考。
