PAYATHON 2026

Gatsby v2 如何连接缺少 JavaScript 版本的在线支付网关?

支付阿杰

明确结论

可以集成,但 C#.NET、PHP、VB.NET 或 JSP 的支付脚本不能直接放在 Gatsby 项目中运行。

Gatsby v2 构建后主要是运行在浏览器中的静态前端,这些支付模块则依赖对应的服务器运行环境。需要单独增加一个后端支付接口,由后端使用支付网关支持的任意语言调用支付模块。Gatsby 负责提交捐赠请求、跳转到支付页面,并展示支付结果。

推荐的连接方式

完整流程可以这样设计:

  1. 用户在 Gatsby 页面填写捐赠金额。
  2. Gatsby 请求自己的后端接口,例如 POST /api/payments。
  3. 后端验证金额并生成唯一订单号。
  4. 后端通过支付网关提供的 PHP、C#.NET 或 JSP 模块创建支付请求。
  5. 支付网关返回支付地址、表单参数或交易令牌。
  6. 浏览器跳转到支付网关的托管页面,完成信用卡付款。
  7. 支付网关通过服务器回调把支付结果通知给后端。
  8. 后端验证回调签名并更新订单状态。
  9. 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 发起业务请求并展示结果,服务器端则使用网关支持的语言调用支付接口、保管密钥,并确认交易状态。

备注:内容仅供参考。