PAYATHON 2026

如何在 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。如果响应格式不正确,即使订单已经更新,网关仍可能反复重试通知。

备注:内容仅供参考。