如何为 opencart 配置 custom payment module
结论
paymentProcessor.php 不能直接设置 OpenCart callback 方法中的 PHP 变量,因为两者运行在不同的 HTTP 请求中。正确的处理流程是:
- OpenCart 提交订单时生成并保存
order_id。 - 把
order_id、金额和 callback URL 一起发送给支付处理程序。 paymentProcessor.php完成交易后,通过 HTTPPOST请求 callback URL。- callback 从
$_POST或 JSON 请求体中读取结果,验证签名,再更新订单状态。
不能只传 "YES" 或 "NO"。通知数据至少要包含订单号、交易号、金额、状态、时间戳和签名,否则 callback 无法安全判断需要更新哪一笔订单。
为什么不能直接传递 PHP 变量
假设用户浏览器把表单提交到:
https://payment.example.com/paymentProcessor.php
此时执行:
echo 'YES';
只会把 YES 返回给当前请求方,也就是用户浏览器,并不会自动调用 OpenCart 中的 callback 方法。
要通知 OpenCart,支付处理程序需要向 OpenCart 暴露的 callback URL 发起新的 HTTP 请求,例如:
https://shop.example.com/index.php?route=extension/payment/custom_payment/callback
实际路由由 OpenCart 版本、扩展目录结构和自定义模块名称决定,示例地址不能直接照搬。
OpenCart 发起支付时应提交的信息
除了支付所需字段,还应发送订单号和通知地址:
<form method="post" action="https://payment.example.com/paymentProcessor.php">
<input type="hidden" name="order_id" value="12345">
<input type="hidden" name="amount" value="99.00">
<input type="hidden" name="currency" value="CNY">
<input type="hidden" name="callback_url"
value="https://shop.example.com/index.php?route=extension/payment/custom_payment/callback">
<input type="hidden" name="return_url"
value="https://shop.example.com/index.php?route=checkout/success">
<!-- 其他支付字段 -->
<button type="submit">Pay</button>
</form>
生产环境中不能完全信任表单里的 callback_url,否则可能引发服务端请求伪造风险。更稳妥的做法是在支付处理程序中预先配置允许访问的 callback 地址,或者严格校验域名和 HTTPS 证书。
paymentProcessor.php 使用表单 POST 通知 callback
支付完成后,可以使用 cURL 向 OpenCart 发送服务端通知:
<?php
$orderId = (string)($_POST['order_id'] ?? '');
$amount = (string)($_POST['amount'] ?? '');
$currency = (string)($_POST['currency'] ?? 'CNY');
// 此处执行真实的支付验证。
// 示例变量不能替代支付机构的实际交易结果。
$paymentSuccessful = true;
$transactionId = 'TXN-' . bin2hex(random_bytes(8));
$status = $paymentSuccessful ? 'YES' : 'NO';
$timestamp = time();
$secret = 'replace-with-a-shared-secret';
$signatureData = implode('|', [
$orderId,
$transactionId,
$amount,
$currency,
$status,
(string)$timestamp
]);
$signature = hash_hmac('sha256', $signatureData, $secret);
$callbackData = [
'order_id' => $orderId,
'transaction_id' => $transactionId,
'amount' => $amount,
'currency' => $currency,
'status' => $status,
'timestamp' => $timestamp,
'signature' => $signature,
];
$callbackUrl = 'https://shop.example.com/index.php?route=extension/payment/custom_payment/callback';
$ch = curl_init($callbackUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($callbackData),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false || $httpCode < 200 || $httpCode >= 300) {
// 记录失败,并安排重试。日志中不要写入卡号、CVV 等敏感数据。
error_log('Payment callback failed: HTTP ' . $httpCode . '; ' . $error);
}
callback 返回 2xx,只代表通知已正常接收。支付处理程序还要保存通知状态,并在网络超时或收到非 2xx 响应时进行有限次数的重试。
callback 接收普通 POST 数据
如果支付处理程序使用 application/x-www-form-urlencoded,callback 可以通过 PHP 的 $_POST 读取字段:
public function callback(): void
{
$orderId = (int)($this->request->post['order_id'] ?? 0);
$transactionId = (string)($this->request->post['transaction_id'] ?? '');
$amount = (string)($this->request->post['amount'] ?? '');
$currency = (string)($this->request->post['currency'] ?? '');
$status = (string)($this->request->post['status'] ?? '');
$timestamp = (int)($this->request->post['timestamp'] ?? 0);
$receivedSignature = (string)($this->request->post['signature'] ?? '');
$secret = 'replace-with-a-shared-secret';
$signatureData = implode('|', [
(string)$orderId,
$transactionId,
$amount,
$currency,
$status,
(string)$timestamp
]);
$expectedSignature = hash_hmac('sha256', $signatureData, $secret);
if (
$orderId <= 0 ||
$transactionId === '' ||
!hash_equals($expectedSignature, $receivedSignature)
) {
$this->response->addHeader('HTTP/1.1 400 Bad Request');
$this->response->setOutput('INVALID');
return;
}
if (abs(time() - $timestamp) > 300) {
$this->response->addHeader('HTTP/1.1 400 Bad Request');
$this->response->setOutput('EXPIRED');
return;
}
$this->load->model('checkout/order');
$orderInfo = $this->model_checkout_order->getOrder($orderId);
if (!$orderInfo) {
$this->response->addHeader('HTTP/1.1 404 Not Found');
$this->response->setOutput('ORDER_NOT_FOUND');
return;
}
// 必须与 OpenCart 中保存的订单金额和币种核对。
if (
$currency !== $orderInfo['currency_code'] ||
bccomp($amount, (string)$orderInfo['total'], 2) !== 0
) {
$this->response->addHeader('HTTP/1.1 400 Bad Request');
$this->response->setOutput('AMOUNT_MISMATCH');
return;
}
if ($status === 'YES') {
$paidStatusId = (int)$this->config->get(
'payment_custom_payment_paid_status_id'
);
$comment = 'Payment confirmed. Transaction: ' . $transactionId;
$this->model_checkout_order->addOrderHistory(
$orderId,
$paidStatusId,
$comment,
false
);
$this->response->setOutput('OK');
return;
}
$failedStatusId = (int)$this->config->get(
'payment_custom_payment_failed_status_id'
);
$this->model_checkout_order->addOrderHistory(
$orderId,
$failedStatusId,
'Payment failed. Transaction: ' . $transactionId,
false
);
$this->response->setOutput('OK');
}
不同 OpenCart 版本中的 addOrderHistory() 参数和模型命名可能不一样,应以当前安装版本的核心代码和其他支付扩展为准。订单状态 ID 也要从模块配置中读取,不能随意写死。
如果服务器没有安装 BCMath 扩展,不要直接使用浮点数比较金额。可以先把金额转换成最小货币单位,例如将 99.00 转成整数 9900,然后再比较。
使用 JSON 发送结果
callback 也可以使用 JSON,但发送端与接收端的 Content-Type 必须一致。
paymentProcessor.php 可以这样发送:
$payload = json_encode($callbackData, JSON_UNESCAPED_SLASHES);
$ch = curl_init($callbackUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json',
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
使用 JSON 后,callback 不能再依赖 $_POST,而要读取原始请求体:
public function callback(): void
{
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);
if (!is_array($data) || json_last_error() !== JSON_ERROR_NONE) {
$this->response->addHeader('HTTP/1.1 400 Bad Request');
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode([
'received' => false,
'error' => 'Invalid JSON'
]));
return;
}
$orderId = (int)($data['order_id'] ?? 0);
$status = (string)($data['status'] ?? '');
$transactionId = (string)($data['transaction_id'] ?? '');
$signature = (string)($data['signature'] ?? '');
// 继续进行签名、订单、金额、币种和重复通知校验。
}
JSON 只是一种数据格式,不能保证数据的真实性和完整性。即使使用 HTTPS,也仍然需要验证签名。
callback 与浏览器跳转不是一回事
通常需要同时设置两个地址:
callback_url:由支付服务器请求,用于可靠地更新订单状态。return_url:支付完成后供用户浏览器返回商城,用于显示成功或失败页面。
不能只依靠浏览器跳转确认付款。用户可能关闭页面、断网,也可能伪造 URL 参数。订单状态应以经过签名验证的服务端 callback 为准,也可以主动查询支付服务并使用查询结果。
必须处理的安全问题
callback 至少要完成以下检查:
- 验证 HMAC 签名,并使用
hash_equals()比较。 - 根据
order_id查询本地订单,不信任 callback 提供的商品信息。 - 核对金额和币种。
- 核对交易号,确保同一个交易号不能绑定多个订单。
- 验证时间戳,必要时加入随机数,防止重放。
- 保证处理过程幂等,避免重复通知导致重复扣库存、发送邮件或执行履约。
- 只有收到明确的成功状态,才能将订单标记为已支付。
- callback 全程使用 HTTPS。
- 将密钥保存在环境变量或安全配置中,不要提交到代码仓库。
- 不要记录卡号、CVV、完整身份信息等敏感数据。
paymentProcessor.php 应通过 HTTP 请求把一组可以验证的交易数据发送给 callback,而不是尝试设置另一个 PHP 脚本中的变量。OpenCart callback 验证通知后再更新订单,浏览器返回页面只负责展示结果。
备注:内容仅供参考。
