如何从第三方支付网站重定向回 React 应用
结论
React 应用可以把前端路由设置为支付完成后的返回地址,例如:
https://example.com/payment/result
但不能只根据浏览器重定向携带的参数判断支付是否成功。支付流程通常还需要一个后端异步通知地址,例如:
https://api.example.com/payments/webhook
前端返回地址用于展示支付结果。后端通知地址用于接收支付平台的签名通知、验证支付状态并更新订单。字段名称和回调规则要以支付平台的 API 文档为准。
推荐的处理流程
一般可以按以下流程处理:
- React 请求自己的后端创建支付订单。
- 后端调用第三方支付 API,生成支付链接或支付会话。
- React 将浏览器跳转到第三方支付页面。
- 用户完成支付后,支付平台向后端发送通知。
- 支付平台将用户重定向到 React 的结果页面。
- React 携带订单号向后端查询最终支付状态。
支付平台对这两个地址的命名可能不同:
return_url、success_url或redirect_url:浏览器返回 React 应用的地址。notify_url、callback_url或webhook_url:服务器接收支付结果通知的地址。
具体名称并不统一,请以接入平台的文档为准。
React 中发起支付
不要在 React 中保存支付平台的密钥。前端只调用自己的后端,支付订单由后端创建。
async function startPayment(orderId) {
const response = await fetch("/api/payments/create", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ orderId }),
});
if (!response.ok) {
throw new Error("创建支付订单失败");
}
const { paymentUrl } = await response.json();
window.location.assign(paymentUrl);
}
如果支付平台要求提交表单,而不是直接访问 URL,也可以按照平台文档动态提交表单。签名计算仍应在后端完成。
后端创建支付订单
下面用 Express 展示基本结构。接口名称和参数仅用于说明流程,需要替换成支付平台实际要求的字段。
import express from "express";
const app = express();
app.use(express.json());
app.post("/api/payments/create", async (req, res) => {
const { orderId } = req.body;
// 应先从数据库读取订单,并在后端确定金额。
const order = await findOrderById(orderId);
if (!order || order.status !== "pending") {
return res.status(400).json({ error: "订单不可支付" });
}
const paymentSession = await paymentProvider.createPayment({
merchantOrderId: order.id,
amount: order.amount,
returnUrl: `https://example.com/payment/result?orderId=${encodeURIComponent(order.id)}`,
notifyUrl: "https://api.example.com/payments/webhook",
});
res.json({
paymentUrl: paymentSession.paymentUrl,
});
});
金额、商品信息和订单状态必须由后端读取和校验,不能直接信任 React 提交的数据。
配置 React 返回页面
使用 React Router 时,可以添加一个支付结果路由:
import { BrowserRouter, Routes, Route } from "react-router-dom";
import PaymentResult from "./PaymentResult";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/payment/result" element={<PaymentResult />} />
</Routes>
</BrowserRouter>
);
}
结果页面从 URL 获取订单号,再向后端查询状态:
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
export default function PaymentResult() {
const [searchParams] = useSearchParams();
const [status, setStatus] = useState("loading");
const orderId = searchParams.get("orderId");
useEffect(() => {
if (!orderId) {
setStatus("invalid");
return;
}
fetch(`/api/payments/status?orderId=${encodeURIComponent(orderId)}`)
.then((response) => {
if (!response.ok) {
throw new Error("查询支付状态失败");
}
return response.json();
})
.then((data) => setStatus(data.status))
.catch(() => setStatus("error"));
}, [orderId]);
if (status === "loading") return <p>正在确认支付结果……</p>;
if (status === "paid") return <p>支付成功。</p>;
if (status === "pending") return <p>支付结果确认中,请稍后刷新。</p>;
if (status === "invalid") return <p>缺少订单信息。</p>;
return <p>暂时无法确认支付结果。</p>;
}
即使返回 URL 中带有 status=success,也不能直接把订单标记为已支付,因为用户可以自行修改 URL 参数。
后端接收支付通知
支付平台通常会向后端发送异步通知。后端收到通知后,需要验证签名,并向支付平台查询或核对交易信息:
app.post(
"/payments/webhook",
express.raw({ type: "application/json" }),
async (req, res) => {
try {
const event = paymentProvider.verifyWebhook(
req.body,
req.headers
);
if (event.type === "payment.succeeded") {
const payment = event.data;
await markOrderAsPaid({
orderId: payment.merchantOrderId,
transactionId: payment.transactionId,
amount: payment.amount,
});
}
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
}
);
接入时必须使用支付平台提供的签名验证方式。部分平台要求返回指定文本,而不是普通的 HTTP 200 响应。
callback URL 应该填写什么
如果支付平台要求填写用户支付后的跳转地址,应填写 React 中已经配置并可公开访问的路由:
https://example.com/payment/result
如果要求填写支付结果通知地址,则应填写后端接口:
https://api.example.com/payments/webhook
支付平台通常无法访问开发环境中的 http://localhost:3000,部分平台也只接受 HTTPS 地址。测试时可以使用平台提供的沙箱环境,并配合临时 HTTPS 隧道或已经部署的测试域名。
注意事项
- 使用 HTTPS,并在支付平台后台配置允许访问的域名。
- 支付密钥、签名密钥和商户私钥只能保存在后端。
- 回调处理应具备幂等性,避免重复通知导致重复入账或发货。
- 验证通知签名,并核对订单号、金额、币种和商户身份。
- 不能只依赖前端跳转。用户可能关闭页面,网络也可能中断,而后端通知仍可正常到达。
- 不要在返回 URL 中放入完整订单信息或敏感数据。
- 使用
BrowserRouter时,服务器必须把/payment/result等前端路由回退到index.html。否则用户从支付网站直接返回时可能遇到404。 - 如果后端还没有收到通知,结果页可以显示“确认中”,并在短时间内轮询订单状态。
备注:内容仅供参考。
