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 响应并未输出到浏览器,用户也就看不到支付页面。
正确的处理流程
可以按以下顺序处理:
- 捐款表单只提交到
create.php。 create.php校验表单数据。- 创建唯一的
orderRef。 - 以”待支付”状态将订单写入数据库。
- 生成 PayDollar 要求的
secureHash。 - 输出包含支付参数的隐藏表单。
- 通过 JavaScript 自动将表单提交到 PayDollar。
- 支付完成后,根据网关回调或返回地址更新订单状态。
原始表单只保留一个 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 又不能自动将浏览器送入托管支付页面。保留一个表单入口,在保存订单后输出自动提交的支付表单,才能把这两个步骤正确衔接起来。
备注:内容仅供参考。
