PAYATHON 2026

PHP POST-CURL 无法提交表单到 EPayment Gateway

支付小周

结论

一个 HTML <form> 只能设置一个 action。捐款表单应提交到 create.php,由 create.php 校验参数并写入数据库。保存成功后,再向 PayDollar 提交支付参数。

对于 payForm.jsp 这类托管支付页面,更合适的方式通常不是由服务器通过 cURL 获取页面,而是返回一个自动提交的 HTML 表单,让用户的浏览器通过 POST 请求进入支付网关。

当前代码中还有一个会直接中断支付流程的问题。数据库写入后,代码立即执行了:

header("Location: index.php");

浏览器收到这个响应后会返回 index.php。虽然 header() 不会立即终止 PHP,但后续的 cURL 响应并未输出到浏览器,用户也就看不到支付页面。

正确的处理流程

可以按以下顺序处理:

  1. 捐款表单只提交到 create.php。
  2. create.php 校验表单数据。
  3. 创建唯一的 orderRef。
  4. 以”待支付”状态将订单写入数据库。
  5. 生成 PayDollar 要求的 secureHash。
  6. 输出包含支付参数的隐藏表单。
  7. 通过 JavaScript 自动将表单提交到 PayDollar。
  8. 支付完成后,根据网关回调或返回地址更新订单状态。

原始表单只保留一个 action:

<form action="create.php" method="post">
    <!-- 捐款人资料和金额字段 -->
    <button type="submit">Donate</button>
</form>

同一个表单不能设置两个 action,也不要嵌套两个 <form>。

保存数据后提交到 PayDollar

下面的示例省略了原有字段校验的细节,只展示保存数据库记录和跳转支付页面的主要结构。PayDollar 的参数值、币种代码和签名算法必须以实际商户接入文档为准。

<?php

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Method Not Allowed');
}

$CName  = trim($_POST['CName'] ?? '');
$Address = trim($_POST['Address'] ?? '');
$Phone  = trim($_POST['Phone'] ?? '');
$Amount = trim($_POST['Amount'] ?? '');

$errors = [];

if ($CName === '') {
    $errors[] = 'Please enter a valid name.';
}

if ($Address === '') {
    $errors[] = 'Please enter an address.';
}

if ($Phone === '' || !ctype_digit($Phone)) {
    $errors[] = 'Please enter a valid phone number.';
}

if ($Amount === '' || !ctype_digit($Amount) || (int) $Amount <= 0) {
    $errors[] = 'Please enter a valid amount.';
}

if ($errors) {
    http_response_code(422);
    exit(implode('<br>', array_map('htmlspecialchars', $errors)));
}

/*
 * 以下值必须替换为真实的商户配置。
 */
$merchantId = 'YOUR_MERCHANT_ID';
$orderRef   = bin2hex(random_bytes(16));
$currCode   = 'YOUR_CURRENCY_CODE';
$payType    = 'YOUR_PAY_TYPE';
$lang       = 'E';
$payMethod  = 'ALL';
$mpsMode    = 'NIL';

$pdo = Database::connect();
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

try {
    $sql = '
        INSERT INTO donation
            (CName, Address, Phone, Amount, orderRef, paymentStatus)
        VALUES
            (?, ?, ?, ?, ?, ?)
    ';

    $statement = $pdo->prepare($sql);
    $statement->execute([
        $CName,
        $Address,
        $Phone,
        $Amount,
        $orderRef,
        'pending'
    ]);
} finally {
    Database::disconnect();
}

/*
 * secureHash 不能随意填写。
 * 必须按照 PayDollar 当前接入文档规定的字段顺序、编码方式和算法生成。
 */
$secureHash = generatePayDollarSecureHash([
    'merchantId' => $merchantId,
    'orderRef'   => $orderRef,
    'currCode'   => $currCode,
    'amount'     => $Amount,
    'payType'    => $payType
]);

$paymentData = [
    'merchantId' => $merchantId,
    'orderRef'   => $orderRef,
    'amount'     => $Amount,
    'currCode'   => $currCode,
    'mpsMode'    => $mpsMode,
    'successUrl' => 'https://www.example.com/payment/success.php',
    'failUrl'    => 'https://www.example.com/payment/fail.php',
    'cancelUrl'  => 'https://www.example.com/payment/cancel.php',
    'payType'    => $payType,
    'lang'       => $lang,
    'payMethod'  => $payMethod,
    'secureHash' => $secureHash
];

?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Redirecting to payment gateway</title>
</head>
<body>
    <p>Redirecting to the payment gateway…</p>

    <form
        id="paydollar-form"
        action="https://test.paydollar.com/b2cDemo/eng/payment/payForm.jsp"
        method="post"
    >
        <?php foreach ($paymentData as $name => $value): ?>
            <input
                type="hidden"
                name="<?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>"
                value="<?= htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8') ?>"
            >
        <?php endforeach; ?>

        <noscript>
            <button type="submit">Continue to payment</button>
        </noscript>
    </form>

    <script>
        document.getElementById('paydollar-form').submit();
    </script>
</body>
</html>

此处不要再执行:

header("Location: index.php");

当前目标是让浏览器继续进入支付网关,而不是返回首页。

如果确实需要使用 cURL

只有 PayDollar 的目标接口明确支持服务器到服务器调用时,才应使用 cURL。调用时至少需要检查 HTTP 状态和 cURL 错误,并正确处理返回内容:

<?php

$paymentData = [
    'merchantId' => $merchantId,
    'orderRef'   => $orderRef,
    'amount'     => $Amount,
    'currCode'   => $currCode,
    'mpsMode'    => 'NIL',
    'successUrl' => 'https://www.example.com/payment/success.php',
    'failUrl'    => 'https://www.example.com/payment/fail.php',
    'cancelUrl'  => 'https://www.example.com/payment/cancel.php',
    'payType'    => $payType,
    'lang'       => 'E',
    'payMethod'  => 'ALL',
    'secureHash' => $secureHash
];

$ch = curl_init(
    'https://test.paydollar.com/b2cDemo/eng/payment/payForm.jsp'
);

curl_setopt_array($ch, [
    CURLOPT_POST            => true,
    CURLOPT_POSTFIELDS      => http_build_query($paymentData),
    CURLOPT_RETURNTRANSFER  => true,
    CURLOPT_CONNECTTIMEOUT  => 10,
    CURLOPT_TIMEOUT         => 30,
    CURLOPT_HTTPHEADER      => [
        'Content-Type: application/x-www-form-urlencoded'
    ]
]);

$response = curl_exec($ch);

if ($response === false) {
    $message = curl_error($ch);
    curl_close($ch);

    throw new RuntimeException('Payment request failed: ' . $message);
}

$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpStatus < 200 || $httpStatus >= 300) {
    throw new RuntimeException(
        'Payment gateway returned HTTP status ' . $httpStatus
    );
}

echo $response;

如果接口返回的是供用户操作的支付页面,cURL 只会让 PHP 服务器取得这段 HTML,并不会自动将用户的浏览器带到支付网关。直接输出返回的 HTML 还可能遇到相对资源地址、Cookie、跳转和安全策略等问题。因此,托管支付页一般应通过浏览器提交 POST 表单。

代码中的其他问题

缺少 amount

原来的 cURL 参数没有提交 amount,但问题描述明确要求传递支付金额,因此需要加入:

'amount' => $Amount

金额格式必须符合网关要求。有些接口要求 "100.00",有些接口使用以最小货币单位表示的整数。仅凭示例无法确定具体格式,必须查看 PayDollar 商户文档。

secureHash 不能写成固定字符串

下面这样的占位值无法生成有效签名:

'secureHash' => 'sth'

签名通常与 merchantId、orderRef、amount、currCode、payType 等字段有关。字段顺序、分隔方式、字符编码、密钥和摘要算法都必须严格遵循网关文档。商户密钥只能保存在服务器端,不能写入 HTML 或 JavaScript。

不能把返回页面当作支付成功证明

用户访问 successUrl 并不代表款项已经到账。浏览器返回地址可以被直接访问,也可能因为网络中断而未被访问。

应使用 PayDollar 提供的服务器端通知或交易查询接口,并完成以下检查:

  • 验证回调签名。
  • 核对 merchantId 和 orderRef。
  • 核对金额和币种。
  • 检查支付状态。
  • 防止重复处理同一条通知。
  • 验证通过后,再将数据库状态改为 paid。

先保存为待支付状态

跳转到支付页面前,不能把数据库记录标记为支付成功。建议至少保存以下字段:

orderRef
amount
currency
paymentStatus = pending
createdAt
gatewayTransactionId

支付通知验证成功后,再将 paymentStatus 更新为 paid。支付失败或取消时,则根据业务需要更新为相应状态。

避免重复提交

用户刷新页面时,可能再次写入数据库并创建新订单。可以为 orderRef 建立唯一索引,并在前端禁止重复点击。支付回调也必须支持幂等处理。

问题不在于 CURLOPT_POST 没有执行,而在于页面流程设计不正确。数据库保存后的重定向把浏览器带回了 index.php,服务器端 cURL 又不能自动将浏览器送入托管支付页面。保留一个表单入口,在保存订单后输出自动提交的支付表单,才能把这两个步骤正确衔接起来。

备注:内容仅供参考。