Gatsby v2 如何连接缺少 JavaScript 版本的在线支付网关?
明确结论
可以集成,但 C#.NET、PHP、VB.NET 或 JSP 的支付脚本不能直接放在 Gatsby 项目中运行。
Gatsby v2 构建后主要是运行在浏览器中的静态前端,这些支付模块则依赖对应的服务器运行环境。需要单独增加一个后端支付接口,由后端使用支付网关支持的任意语言调用支付模块。Gatsby 负责提交捐赠请求、跳转到支付页面,并展示支付结果。
推荐的连接方式
完整流程可以这样设计:
- 用户在 Gatsby 页面填写捐赠金额。
- Gatsby 请求自己的后端接口,例如
POST /api/payments。 - 后端验证金额并生成唯一订单号。
- 后端通过支付网关提供的 PHP、C#.NET 或 JSP 模块创建支付请求。
- 支付网关返回支付地址、表单参数或交易令牌。
- 浏览器跳转到支付网关的托管页面,完成信用卡付款。
- 支付网关通过服务器回调把支付结果通知给后端。
- 后端验证回调签名并更新订单状态。
- Gatsby 向后端查询订单状态,然后展示结果。
使用 PHP、C#.NET 还是 JSP,与 Gatsby 没有直接关系。具体选择取决于服务器环境、团队经验,以及支付网关为哪种语言提供了更完整的示例。
Gatsby 前端示例
假设后端提供 POST /api/payments,并返回支付网关的跳转地址:
import React, { useState } from "react";
export default function DonationForm() {
const [amount, setAmount] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
async function handleSubmit(event) {
event.preventDefault();
setSubmitting(true);
setError("");
try {
const response = await fetch(
"https://api.example.com/api/payments",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
amount
})
}
);
if (!response.ok) {
throw new Error("创建支付请求失败");
}
const result = await response.json();
if (!result.redirectUrl) {
throw new Error("后端没有返回支付地址");
}
window.location.assign(result.redirectUrl);
} catch (err) {
setError(err.message);
setSubmitting(false);
}
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="amount">捐赠金额</label>
<input
id="amount"
type="number"
min="1"
step="0.01"
value={amount}
onChange={event => setAmount(event.target.value)}
required
/>
<button type="submit" disabled={submitting}>
{submitting ? "正在处理..." : "前往支付"}
</button>
{error && <p role="alert">{error}</p>}
</form>
);
}
在生产环境中,通常会把 API 地址放入 Gatsby 环境变量:
GATSBY_API_BASE_URL=https://api.example.com
调用时使用:
const response = await fetch(
`${process.env.GATSBY_API_BASE_URL}/api/payments`,
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ amount })
}
);
以 GATSBY_ 开头的变量会被打包进浏览器代码,只能用于保存公开配置。商户密钥、签名密钥和网关密码都不能放在这些变量中。
PHP 后端示意
如果使用支付网关提供的 PHP 模块,后端接口可以参考下面的结构。由于这里没有指定具体的支付网关及其参数,代码中的类名、方法名和字段都是占位示例,实际使用时需要替换成网关文档规定的值。
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: https://www.example.com');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Methods: POST, OPTIONS');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$amount = filter_var($input['amount'] ?? null, FILTER_VALIDATE_FLOAT);
if ($amount === false || $amount <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Invalid amount']);
exit;
}
$orderId = bin2hex(random_bytes(16));
require_once __DIR__ . '/payment-gateway-sdk.php';
try {
// 以下 API 名称仅用于展示集成结构。
$gateway = new PaymentGateway([
'merchantId' => getenv('PAYMENT_MERCHANT_ID'),
'secretKey' => getenv('PAYMENT_SECRET_KEY')
]);
$payment = $gateway->createPayment([
'orderId' => $orderId,
'amount' => number_format($amount, 2, '.', ''),
'currency' => 'MAD',
'returnUrl' => 'https://www.example.com/payment/result',
'callbackUrl' => 'https://api.example.com/api/payment-callback'
]);
// 应先把 orderId、金额和初始状态保存到数据库。
echo json_encode([
'orderId' => $orderId,
'redirectUrl' => $payment->getRedirectUrl()
]);
} catch (Throwable $exception) {
http_response_code(502);
echo json_encode(['error' => 'Unable to initialize payment']);
}
有些支付网关不会返回简单的 redirectUrl,而是要求浏览器把隐藏表单提交到网关。此时,后端应返回支付地址和经过签名的字段,再由 Gatsby 创建并提交表单:
function submitGatewayForm(action, fields) {
const form = document.createElement("form");
form.method = "POST";
form.action = action;
Object.entries(fields).forEach(([name, value]) => {
const input = document.createElement("input");
input.type = "hidden";
input.name = name;
input.value = String(value);
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
至于后端应返回哪些字段、哪些字段需要参与签名,必须以支付网关的集成文档为准。
支付回调比返回页面更重要
用户付款后,支付网关可能会把浏览器重定向回 Gatsby 页面,但不能只凭这次跳转判断支付成功。用户可能在跳转前关闭页面,攻击者也可能伪造查询参数。
后端应提供单独的回调地址,并进行以下处理:
<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '';
// verifySignature 及签名头名称应替换为网关规定的实现。
if (!verifySignature($payload, $signature, getenv('PAYMENT_SECRET_KEY'))) {
http_response_code(400);
exit('Invalid signature');
}
$event = json_decode($payload, true);
$orderId = $event['orderId'] ?? '';
$status = $event['status'] ?? '';
// 还应核对订单号、商户号、币种和金额。
if ($status === 'PAID') {
// 使用幂等更新,避免重复回调造成重复记账。
markOrderAsPaid($orderId);
}
http_response_code(200);
echo 'OK';
不同网关使用的回调字段、签名算法、成功状态名称和应答格式并不相同,不能直接照搬上面的占位名称。
部署方案
后端不必与 Gatsby 部署在同一台服务器上,可以采用以下方案:
- Gatsby 静态站点配合独立的 PHP、ASP.NET 或 Java 服务。
- Gatsby 配合支持相应运行时的 Serverless Function。
- Gatsby 前端部署在 CDN,支付接口部署在自己的服务器或云平台。
- 如果网关只提供旧版 SDK,也可以不使用 SDK,但网关必须提供完整且受支持的 HTTP API 文档。
如果前后端使用不同域名,例如前端是 www.example.com,后端是 api.example.com,后端需要正确配置 CORS,并且只允许可信站点访问。CORS 不能代替鉴权,后端仍需验证所有请求数据。
安全注意事项
- 商户密钥和签名密钥只能保存在后端。
- 不要让 Gatsby 直接接收或处理银行卡号、安全码等敏感信息。
- 优先使用支付网关托管的支付页面,以缩小 PCI DSS 合规范围。
- 后端必须验证金额。如果捐赠项目设有固定金额,还应由后端重新计算。
- 使用 HTTPS,并验证支付网关的回调签名。
- 处理回调时,核对订单号、商户号、金额、币种和交易状态。
- 为每笔支付生成不可预测的唯一订单号。
- 回调处理必须具备幂等性,因为支付网关可能重复发送通知。
- 不要仅凭 Gatsby 返回页中的 URL 参数将订单标记为已支付。
- 保存网关交易号和必要的审计记录,但不要记录银行卡敏感数据。
- 正式上线前,在网关提供的测试环境中完成支付成功、失败、取消、超时和重复回调测试。
问题并不是 Gatsby 能否执行 PHP 或 C#.NET 脚本,而是要划分好前后端职责。Gatsby 发起业务请求并展示结果,服务器端则使用网关支持的语言调用支付接口、保管密钥,并确认交易状态。
备注:内容仅供参考。
