如何在 Joomla 中创建处理支付网关结果的单页面
结论
无需在 Joomla 后台创建“单页面文章”,也不要单独放置 PHP 文件。VirtueMart 支付插件通常使用下面的支付通知入口:
https://example.com/index.php?option=com_virtuemart&view=pluginresponse&task=pluginnotification&tmpl=component
支付网关向这个 URL 发送结果后,VirtueMart 会将请求交给支付插件的 plgVmOnPaymentNotification() 方法。签名验证、订单查询、状态更新和响应输出都应在这个方法中完成。
不同版本的 VirtueMart 可能使用不同的插件事件名称或调用方式。以下内容适用于支持 plgVmOnPaymentNotification() 的常见版本。
设置通知地址
创建支付交易时,按照网关的要求,将通知地址作为 notify_url、callback_url 或类似参数提交:
use Joomla\CMS\Uri\Uri;
$notificationUrl = Uri::root()
. 'index.php?option=com_virtuemart'
. '&view=pluginresponse'
. '&task=pluginnotification'
. '&tmpl=component';
$gatewayData = [
'merchant_order_id' => (string) $order['details']['BT']->virtuemart_order_id,
'amount' => (string) $amount,
'currency' => $currencyCode,
'notify_url' => $notificationUrl,
];
建议使用完整的 HTTPS 地址。如果网站部署在反向代理或 CDN 后面,还需确认 Uri::root() 生成的协议和域名与公网访问地址一致。
通知地址不同于用户完成支付后的跳转地址:
notify_url:由支付平台服务器直接请求,用于可靠地更新订单。return_url:用户浏览器返回商店的地址,用于展示支付结果。cancel_url:用户取消支付后跳转的地址。
判断订单状态时,应以服务器通知或主动查询网关的结果为准,不能只依赖浏览器跳转。
在支付插件中处理通知
在 VirtueMart 支付插件类中实现通知方法:
use Joomla\CMS\Factory;
public function plgVmOnPaymentNotification()
{
$app = Factory::getApplication();
// 如果网关发送 JSON,可以读取原始请求体。
$rawBody = file_get_contents('php://input');
$data = json_decode($rawBody, true);
// 如果网关发送普通表单,则改用:
// $data = $app->input->post->getArray();
if (!is_array($data)) {
http_response_code(400);
echo 'invalid payload';
$app->close();
}
$orderId = isset($data['merchant_order_id'])
? (int) $data['merchant_order_id']
: 0;
$transactionId = isset($data['transaction_id'])
? (string) $data['transaction_id']
: '';
$gatewayStatus = isset($data['status'])
? (string) $data['status']
: '';
$receivedSign = isset($data['signature'])
? (string) $data['signature']
: '';
if ($orderId <= 0 || $transactionId === '' || $receivedSign === '') {
http_response_code(400);
echo 'missing parameters';
$app->close();
}
$expectedSign = $this->buildGatewaySignature($data);
if (!hash_equals($expectedSign, $receivedSign)) {
http_response_code(403);
echo 'invalid signature';
$app->close();
}
// 根据订单号读取订单,并进一步核对金额、币种和支付方式。
$orderModel = VmModel::getModel('orders');
$order = $orderModel->getOrder($orderId);
if (empty($order) || empty($order['details']['BT'])) {
http_response_code(404);
echo 'order not found';
$app->close();
}
// 此处应增加金额和币种校验,字段名取决于网关实际返回格式。
if ($gatewayStatus === 'paid') {
$orderData = [
'order_status' => 'C',
'customer_notified' => 1,
'comments' => 'Payment confirmed: ' . $transactionId,
];
$orderModel->updateStatusForOneOrder($orderId, $orderData, true);
} elseif ($gatewayStatus === 'failed') {
$orderData = [
'order_status' => 'X',
'customer_notified' => 0,
'comments' => 'Payment failed: ' . $transactionId,
];
$orderModel->updateStatusForOneOrder($orderId, $orderData, true);
}
http_response_code(200);
echo 'OK';
$app->close();
}
C 和 X 在这里只是示例状态码。请根据当前商店的配置,选择“已确认”“已取消”或自定义订单状态,不要假定这些代码在所有站点中都有相同含义。
签名验证示例
签名算法必须严格按照支付网关的文档实现。假设网关要求对订单号、交易号、金额和状态计算 HMAC-SHA256:
private function buildGatewaySignature(array $data): string
{
$payload = implode('|', [
(string) ($data['merchant_order_id'] ?? ''),
(string) ($data['transaction_id'] ?? ''),
(string) ($data['amount'] ?? ''),
(string) ($data['status'] ?? ''),
]);
return hash_hmac('sha256', $payload, $this->merchantSecret);
}
字段顺序、分隔符、编码方式和签名输出格式都必须遵循网关规范。如果网关提供公钥验签,应使用其指定的 RSA 或其他验签方式,不能自行替换为 HMAC。
必须处理的安全问题
通知入口通常不能要求用户登录,也不能使用 Joomla 表单 CSRF Token,因为支付平台无法提前获得站点会话。该入口应通过网关签名和业务数据校验来保证安全。
处理通知时,至少需要检查以下内容:
- 签名是否正确。
- 商户号是否属于当前站点。
- 商户订单号是否存在。
- 返回的金额和币种是否与原订单一致。
- 支付方式是否为当前插件。
- 交易状态是否明确表示支付成功。
- 同一个交易号是否已经处理。
支付平台可能因请求超时而重复发送通知,因此处理逻辑必须具备幂等性。如果订单已经处于已支付状态,就不应再次扣减库存、发送邮件或创建支付记录。
日志中可以记录通知时间、交易号、验证结果和必要的错误信息,但不要写入密钥、完整银行卡数据或其他敏感信息。
如果当前版本没有通知事件
如果当前使用的 VirtueMart 版本没有提供 pluginnotification 分发入口,可以创建一个最小的 Joomla 组件,并将组件控制器任务设为回调地址,例如:
https://example.com/index.php?option=com_paymentcallback&task=callback.handle&format=raw
随后在组件控制器的 handle() 方法中执行相同的验签和订单更新逻辑。不要在网站根目录直接创建 callback.php 并手动加载 Joomla。这种做法容易绕过 Joomla 的初始化、配置、日志和数据库管理机制,也会增加升级和安全维护的成本。
无论使用 VirtueMart 原生通知事件还是独立组件,都应按照支付平台的要求返回确认内容,例如 OK、success 或指定格式的 JSON。如果响应格式不正确,即使订单已经更新,网关仍可能反复重试通知。
备注:内容仅供参考。
