在 Joomla 2.5 的 Akeeba Subscription 中集成 SecurePay
结论
在 Joomla 2.5 中集成 SecurePay,一般不需要改动 Akeeba Subscriptions 的核心代码。较稳妥的方案是开发独立的支付插件,参照当前安装版本自带的支付插件,实现以下流程:
- 创建 SecurePay 交易并跳转到付款页面,或从服务端调用 SecurePay API。
- 接收 SecurePay 的浏览器返回和服务器通知。
- 验证通知的真实性、支付金额等信息。
- 把 SecurePay 的交易状态转换为 Akeeba Subscriptions 可以识别的付款状态。
- 由组件原有流程决定启用、续订或保持订阅不变。
不同版本的 Akeeba Subscriptions 在支付插件事件、交易模型和辅助类方面可能并不相同,因此无法仅根据 Joomla 2.5 确定具体的方法名。开发时应以服务器实际安装版本中的支付插件为准,选择工作方式最接近 SecurePay 的插件作为模板。
先确定 SecurePay 的接入模式
SecurePay 可能提供托管支付页、重定向支付、Direct Post、服务端 API 等多种产品。使用的字段、接口地址和签名算法,取决于商户开通的产品及接口版本。
通常优先考虑托管支付页或重定向模式。这样,银行卡数据不会直接经过 Joomla 网站,PCI DSS 合规压力也会小一些。
两种常见模式的区别如下:
| 模式 | Joomla 端职责 | 主要注意事项 |
|---|---|---|
| 托管支付页 | 创建订单、生成请求、接收回调 | 验签、金额校验、重复通知 |
| 服务端 API | 收集支付信息并调用 API | PCI 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 状态码和响应内容。如果平台要求返回特定的确认文本,就必须严格按照对应接口文档处理,否则平台可能不断重试。
推荐实施顺序
- 确认 Akeeba Subscriptions 的准确版本。
- 确认 SecurePay 商户开通的产品和接口版本。
- 在测试环境中复制一个流程相近的 Akeeba 支付插件。
- 完成插件名称、配置项和安装清单。
- 实现支付请求及测试环境跳转。
- 实现独立的服务器通知入口。
- 完成签名验证或服务端交易查询。
- 校验订单号、金额、货币和商户身份。
- 实现交易状态映射和幂等处理。
- 调用组件原有的付款完成流程。
- 测试成功、拒付、取消、超时、重复通知、金额不一致等情况。
- 确认日志中没有银行卡信息或商户密钥,再切换到生产环境。
注意事项
Joomla 2.5 和早期版本的 Akeeba Subscriptions 都已是老旧软件。即使能够完成插件功能,也需要检查 PHP 版本、TLS 支持、加密套件和依赖库是否符合 SecurePay 当前接口的要求。
如果 SecurePay 已经不再支持旧版 TLS、旧版 PHP HTTP 客户端或相关接口版本,单独编写插件无法解决问题,还需要升级 Joomla、PHP 和订阅组件。支付系统涉及真实资金和敏感数据,不应通过关闭证书验证、降低 TLS 版本或跳过回调验签来兼容旧环境。
备注:内容仅供参考。
