PAYATHON 2026

在 Joomla 2.5 的 Akeeba Subscription 中集成 SecurePay

支付小周

结论

在 Joomla 2.5 中集成 SecurePay,一般不需要改动 Akeeba Subscriptions 的核心代码。较稳妥的方案是开发独立的支付插件,参照当前安装版本自带的支付插件,实现以下流程:

  1. 创建 SecurePay 交易并跳转到付款页面,或从服务端调用 SecurePay API。
  2. 接收 SecurePay 的浏览器返回和服务器通知。
  3. 验证通知的真实性、支付金额等信息。
  4. 把 SecurePay 的交易状态转换为 Akeeba Subscriptions 可以识别的付款状态。
  5. 由组件原有流程决定启用、续订或保持订阅不变。

不同版本的 Akeeba Subscriptions 在支付插件事件、交易模型和辅助类方面可能并不相同,因此无法仅根据 Joomla 2.5 确定具体的方法名。开发时应以服务器实际安装版本中的支付插件为准,选择工作方式最接近 SecurePay 的插件作为模板。

先确定 SecurePay 的接入模式

SecurePay 可能提供托管支付页、重定向支付、Direct Post、服务端 API 等多种产品。使用的字段、接口地址和签名算法,取决于商户开通的产品及接口版本。

通常优先考虑托管支付页或重定向模式。这样,银行卡数据不会直接经过 Joomla 网站,PCI DSS 合规压力也会小一些。

两种常见模式的区别如下:

模式Joomla 端职责主要注意事项
托管支付页创建订单、生成请求、接收回调验签、金额校验、重复通知
服务端 API收集支付信息并调用 APIPCI DSS、敏感数据保护、TLS、日志脱敏

如果 SecurePay 要求网站直接接收完整卡号、CVV 等数据,不要只是把这些字段加入 Joomla 表单。除非已经完成相应的安全与合规评估,否则应使用 SecurePay 提供的托管页面或令牌化方案。

检查现有支付插件

先在测试环境中找到 Akeeba Subscriptions 已安装的支付插件。旧版扩展通常通过 Joomla 插件组接入支付网关,但具体的插件组名称、事件名称和目录结构,必须以实际安装包为准。

可以复制一个流程相近的插件作为开发模板,例如:

  • 跳转到第三方付款页面的插件;
  • 通过异步通知确认付款的插件;
  • 需要验证签名或摘要的插件。

复制后,需要完整替换以下内容:

  • PHP 类名;
  • 插件名称;
  • XML 清单中的文件名和语言文件;
  • 商户配置字段;
  • 网关标识;
  • 支付请求生成逻辑;
  • 回调验证逻辑。

不要直接编辑原插件,也不要修改 Akeeba Subscriptions 的核心文件,否则组件升级时很容易丢失改动。

建议的插件结构

实际目录和插件组应以现有插件为准。典型结构可能如下:

plugins/
└── akpayment/
    └── securepay/
        ├── securepay.php
        ├── securepay.xml
        ├── language/
        │   └── en-GB/
        │       ├── en-GB.plg_akpayment_securepay.ini
        │       └── en-GB.plg_akpayment_securepay.sys.ini
        └── tmpl/
            └── payment.php

如果现有插件不使用 akpayment 目录,或者没有模板目录,就应完全沿用现有插件的布局,不要自行假定。

XML 清单通常需要提供商户号、API 凭据、测试模式等参数。下面仅用于展示 Joomla 2.5 风格的结构,并不代表 SecurePay 的真实字段名称:

<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" version="2.5" group="akpayment" method="upgrade">
    <name>SecurePay payment integration</name>
    <files>
        <filename plugin="securepay">securepay.php</filename>
    </files>

    <config>
        <fields name="params">
            <fieldset name="basic">
                <field
                    name="merchant_id"
                    type="text"
                    label="Merchant ID"
                    required="true"
                />

                <field
                    name="api_credential"
                    type="password"
                    label="API credential"
                    required="true"
                />

                <field
                    name="test_mode"
                    type="radio"
                    default="1"
                    label="Test mode"
                >
                    <option value="1">JYES</option>
                    <option value="0">JNO</option>
                </field>
            </fieldset>
        </fields>
    </config>
</extension>

生产环境的凭据不应写死在 PHP 文件或 JavaScript 中,也不能提交到公开仓库。

发起支付

支付请求至少需要建立以下对应关系:

  • Akeeba 订阅订单号对应 SecurePay 商户参考号;
  • 应付总额对应 SecurePay 金额字段;
  • 货币代码对应 SecurePay 货币字段;
  • 当前用户或订阅记录关联本地订单;
  • 成功返回地址指向 Joomla 前台路由;
  • 服务器通知地址指向可公开访问的回调入口。

示意代码如下:

<?php

defined('_JEXEC') or die;

class SecurePayRequestBuilder
{
    public static function build(array $order, array $config)
    {
        $amount = number_format((float) $order['amount'], 2, '.', '');

        $payload = array(
            // 以下名称仅为内部示例,发送前必须转换成
            // SecurePay 当前接口文档规定的真实字段。
            'merchant_reference' => (string) $order['order_number'],
            'amount'             => $amount,
            'currency'           => (string) $order['currency'],
            'return_url'         => (string) $config['return_url'],
            'callback_url'       => (string) $config['callback_url'],
        );

        return $payload;
    }
}

如果接口使用最小货币单位传递金额,例如以分为单位,不要直接用浮点数相乘。可以先统一小数位,再转换成整数:

<?php

function amountToMinorUnits($amount)
{
    $normalized = number_format((float) $amount, 2, '.', '');

    return (int) str_replace('.', '', $normalized);
}

是否需要这样处理,应根据所用 SecurePay 接口对金额格式的要求来决定。

不要依赖浏览器返回结果确认付款

用户从 SecurePay 返回网站时,浏览器参数可以用来显示”付款处理中”或”付款成功”页面,但不能只凭这些参数启用订阅。

浏览器请求可能被修改,用户也可能在支付成功后直接关闭窗口。最终结果应以验证通过的服务器通知或主动查询结果为准。

建议把浏览器返回和服务器通知分成两个入口:

<?php

defined('_JEXEC') or die;

class SecurePayCallbackHandler
{
    public function handleBrowserReturn(array $input)
    {
        // 仅用于用户界面展示。
        // 不在这里直接认定付款成功。
        return array(
            'message' => '支付结果正在确认,请稍后查看订阅状态。'
        );
    }

    public function handleServerNotification(array $input)
    {
        if (!$this->verifyNotification($input)) {
            throw new RuntimeException('Invalid SecurePay notification');
        }

        return $this->normalizeStatus($input);
    }

    private function verifyNotification(array $input)
    {
        // 必须按照所用 SecurePay 产品的官方协议实现:
        // 1. 校验签名、摘要或消息认证码;
        // 2. 必要时调用 SecurePay 查询接口复核交易;
        // 3. 校验商户号、订单号、金额和货币;
        // 4. 防止通知重放。
        return false;
    }

    private function normalizeStatus(array $input)
    {
        // 状态值必须按 SecurePay 当前接口文档填写。
        return array(
            'reference'     => '',
            'gateway_id'    => '',
            'status'        => 'pending',
            'amount'        => '',
            'currency'      => '',
            'response_code' => '',
        );
    }
}

示例中的 verifyNotification() 特意默认返回 false,以免未经验证的通知被当作付款成功。正式接入时,必须按照实际使用的 SecurePay 协议补全验证逻辑。

将交易状态映射到订阅状态

SecurePay 返回的状态不能直接写入 Akeeba Subscriptions,需要先转换成组件当前版本支持的状态。

通常可以按以下方式处理:

SecurePay 结果本地处理
已支付、已结算或明确成功标记交易成功,并交给组件启用订阅
处理中、待确认保持 pending,不启用订阅
拒绝、失败标记失败
取消标记取消
退款记录退款;是否撤销订阅取决于业务规则
状态未知保持 pending,并记录供人工检查

状态映射可以单独实现:

<?php

function mapSecurePayStatus($gatewayStatus)
{
    switch (strtoupper((string) $gatewayStatus)) {
        // CASE 值必须替换成实际 SecurePay 接口返回的状态。
        case 'SUCCESS_PLACEHOLDER':
            return 'paid';

        case 'PENDING_PLACEHOLDER':
            return 'pending';

        case 'CANCELLED_PLACEHOLDER':
            return 'cancelled';

        case 'FAILED_PLACEHOLDER':
            return 'failed';

        default:
            return 'pending';
    }
}

这里的 paid、pending、cancelled 和 failed 只是内部统一使用的状态。最终写入 Akeeba Subscriptions 时,应调用模板插件使用的组件 API,不要自行猜测数据库字段。

回调处理的必要校验

即使 SecurePay 的签名已经验证通过,仍需至少核对以下内容:

<?php

function validatePaymentAgainstOrder(array $payment, array $order)
{
    if ((string) $payment['merchant_reference'] !== (string) $order['order_number']) {
        throw new RuntimeException('Order reference mismatch');
    }

    if ((string) $payment['currency'] !== (string) $order['currency']) {
        throw new RuntimeException('Currency mismatch');
    }

    $paid = number_format((float) $payment['amount'], 2, '.', '');
    $due  = number_format((float) $order['amount'], 2, '.', '');

    if ($paid !== $due) {
        throw new RuntimeException('Payment amount mismatch');
    }

    return true;
}

还应检查:

  • 通知中的商户号是否属于当前网站;
  • 交易编号是否已经处理;
  • 订单是否存在;
  • 订单是否已经付款;
  • 请求时间是否超出允许范围;
  • 货币和金额是否与服务器保存的数据一致;
  • 支付结果是否确实属于当前订单。

金额和商品信息必须从服务器端的订单记录中读取,不能信任浏览器提交的隐藏字段。

幂等处理

支付平台可能重复发送同一条通知。如果每次通知都重新启用或延长订阅,用户可能得到重复的订阅期限。

可以为 SecurePay 交易号建立唯一约束,也可以在处理前检查该交易是否已成功处理:

<?php

function processPaymentOnce($gatewayTransactionId, callable $processor)
{
    if (transactionAlreadyProcessed($gatewayTransactionId)) {
        return true;
    }

    $processor();

    markTransactionProcessed($gatewayTransactionId);

    return true;
}

transactionAlreadyProcessed() 和 markTransactionProcessed() 应通过当前版本 Akeeba Subscriptions 提供的交易模型实现。如果组件没有合适的字段,再考虑建立单独的映射表,不要先修改组件的核心表结构。

与 Akeeba Subscriptions 对接

接入过程中最重要的是把 SecurePay 的统一处理结果交给 Akeeba Subscriptions 原有的付款完成流程。不要直接执行下面这类 SQL:

UPDATE #__akeebasubs_subscriptions
SET enabled = 1
WHERE id = ...

直接更新数据库可能绕过以下流程:

  • 订阅开始和结束日期计算;
  • 续订逻辑;
  • 用户组分配;
  • 优惠券和税费处理;
  • 邮件通知;
  • 组件插件事件;
  • 交易记录和审计信息。

应在现有支付插件中找到付款确认后的调用链,然后复用同样的模型、事件或辅助方法。以下内容都需要保持一致:

  • 支付插件事件名称;
  • 传入参数的顺序和类型;
  • 获取订单或订阅对象的方式;
  • 成功、失败和待处理状态值;
  • 交易编号的保存位置;
  • 组件完成付款时调用的方法。

这些接口取决于 Akeeba Subscriptions 的具体版本,无法只根据 Joomla 2.5 给出一个保证可以运行的方法名。

日志与错误响应

建议记录以下信息:

  • 本地订单号;
  • SecurePay 交易号;
  • 回调时间;
  • 处理后的状态;
  • 响应码;
  • 验签结果;
  • 金额或货币不一致的原因。

以下信息不能写入日志:

  • 完整卡号;
  • CVV;
  • API 密钥;
  • 完整授权头;
  • 可被重放的签名原文;
  • 含有敏感信息的完整请求体。

回调入口应尽快返回 SecurePay 要求的 HTTP 状态码和响应内容。如果平台要求返回特定的确认文本,就必须严格按照对应接口文档处理,否则平台可能不断重试。

推荐实施顺序

  1. 确认 Akeeba Subscriptions 的准确版本。
  2. 确认 SecurePay 商户开通的产品和接口版本。
  3. 在测试环境中复制一个流程相近的 Akeeba 支付插件。
  4. 完成插件名称、配置项和安装清单。
  5. 实现支付请求及测试环境跳转。
  6. 实现独立的服务器通知入口。
  7. 完成签名验证或服务端交易查询。
  8. 校验订单号、金额、货币和商户身份。
  9. 实现交易状态映射和幂等处理。
  10. 调用组件原有的付款完成流程。
  11. 测试成功、拒付、取消、超时、重复通知、金额不一致等情况。
  12. 确认日志中没有银行卡信息或商户密钥,再切换到生产环境。

注意事项

Joomla 2.5 和早期版本的 Akeeba Subscriptions 都已是老旧软件。即使能够完成插件功能,也需要检查 PHP 版本、TLS 支持、加密套件和依赖库是否符合 SecurePay 当前接口的要求。

如果 SecurePay 已经不再支持旧版 TLS、旧版 PHP HTTP 客户端或相关接口版本,单独编写插件无法解决问题,还需要升级 Joomla、PHP 和订阅组件。支付系统涉及真实资金和敏感数据,不应通过关闭证书验证、降低 TLS 版本或跳过回调验签来兼容旧环境。

备注:内容仅供参考。