如何在 OpenCart 中集成 CitrusPay Payment Gateway
结论
CitrusPay 不能直接嵌入 OpenCart 的现有页面调用,需要封装成独立的支付扩展。扩展一般包括后台配置、前台支付逻辑、支付跳转页面和回调处理,并通过 OpenCart 的订单状态机制确认付款结果。
不要修改 OpenCart 核心文件。可以参考当前版本已有的跳转型支付网关扩展,例如 PayPal Standard,再把请求参数、签名算法和回调验证替换成 CitrusPay PHP integration kit 提供的实现。
OpenCart 各版本的扩展目录和接口差异较大。以下示例采用 OpenCart 3.x 的传统扩展结构。如果使用 OpenCart 1.5、2.x 或 4.x,需要参照对应版本已有的支付扩展,调整目录、命名空间和事件接口。
需要创建的文件
一个基本的 CitrusPay 支付扩展通常需要这些文件:
admin/
├── controller/extension/payment/citruspay.php
├── language/en-gb/extension/payment/citruspay.php
├── model/extension/payment/citruspay.php
└── view/template/extension/payment/citruspay.twig
catalog/
├── controller/extension/payment/citruspay.php
├── language/en-gb/extension/payment/citruspay.php
├── model/extension/payment/citruspay.php
└── view/theme/default/template/extension/payment/citruspay.twig
各文件的用途如下:
admin/controller:保存商户号、密钥、测试模式和订单状态等配置。admin/model:处理扩展安装或卸载时所需的数据表或事件。简单扩展可能不需要额外的数据表。admin/view:后台配置页面。catalog/model:判断结账时是否显示 CitrusPay。catalog/controller:准备支付请求、生成签名并接收回调。catalog/view:生成提交到支付网关的表单。language:保存后台和前台显示的文本。
如果店铺使用自定义主题,需要把前台模板放到对应的主题目录中,或者确认该主题可以回退到 default 模板。
后台注册支付方式
后台控制器用于显示和保存配置。下面是一个简化示例,其中的配置字段只是扩展内部命名,不代表 CitrusPay integration kit 的真实参数名:
<?php
class ControllerExtensionPaymentCitruspay extends Controller
{
private $error = [];
public function index()
{
$this->load->language('extension/payment/citruspay');
$this->document->setTitle($this->language->get('heading_title'));
$this->load->model('setting/setting');
if (
$this->request->server['REQUEST_METHOD'] === 'POST'
&& $this->validate()
) {
$this->model_setting_setting->editSetting(
'payment_citruspay',
$this->request->post
);
$this->session->data['success'] =
$this->language->get('text_success');
$this->response->redirect(
$this->url->link(
'marketplace/extension',
'user_token=' . $this->session->data['user_token']
. '&type=payment',
true
)
);
}
// 在这里加载配置值、面包屑、订单状态和模板变量。
$this->response->setOutput(
$this->load->view('extension/payment/citruspay', $data)
);
}
protected function validate()
{
if (!$this->user->hasPermission(
'modify',
'extension/payment/citruspay'
)) {
$this->error['warning'] =
$this->language->get('error_permission');
}
return !$this->error;
}
}
后台表单至少需要提供以下配置:
payment_citruspay_merchant_id
payment_citruspay_secret_key
payment_citruspay_test
payment_citruspay_order_status_id
payment_citruspay_total
payment_citruspay_geo_zone_id
payment_citruspay_status
payment_citruspay_sort_order
具体需要哪些凭据,应以收到的 CitrusPay integration kit 为准,不要根据其他支付网关的字段猜测 CitrusPay 参数。
让支付方式出现在结账页面
前台模型需要返回支付方式信息。OpenCart 3.x 通常可以这样写:
<?php
class ModelExtensionPaymentCitruspay extends Model
{
public function getMethod($address, $total)
{
if (!$this->config->get('payment_citruspay_status')) {
return [];
}
$minimum = (float)$this->config->get('payment_citruspay_total');
if ($minimum > 0 && $total < $minimum) {
return [];
}
$this->load->language('extension/payment/citruspay');
return [
'code' => 'citruspay',
'title' => $this->language->get('text_title'),
'terms' => '',
'sort_order' => $this->config->get(
'payment_citruspay_sort_order'
)
];
}
}
生产环境还需要处理 geo_zone_id。可以参考同一 OpenCart 版本中其他支付扩展的 getMethod(),通过 zone_to_geo_zone 表判断收货地址是否在允许区域内。
对应的前台语言文件可以写成:
<?php
$_['text_title'] = 'CitrusPay';
创建订单并跳转到 CitrusPay
客户确认支付后,控制器需要从 OpenCart 当前订单中读取金额、币种、账单信息和订单号,再按照 integration kit 的规则生成请求。
下面的代码只展示整体结构。gateway_url、请求字段和 generateSignature() 的实现,都必须替换成 CitrusPay 文档或 PHP integration kit 中的真实定义:
<?php
class ControllerExtensionPaymentCitruspay extends Controller
{
public function index()
{
$this->load->model('checkout/order');
$orderId = (int)$this->session->data['order_id'];
$orderInfo = $this->model_checkout_order->getOrder($orderId);
if (!$orderInfo) {
return;
}
$data['action'] = $this->getGatewayUrl();
$data['fields'] = [
'merchant_id' => $this->config->get(
'payment_citruspay_merchant_id'
),
'order_id' => (string)$orderId,
'amount' => $this->formatGatewayAmount(
$orderInfo['total'],
$orderInfo['currency_code']
),
'currency' => $orderInfo['currency_code'],
'customer_email' => $orderInfo['email'],
'return_url' => $this->url->link(
'extension/payment/citruspay/callback',
'',
true
)
];
$data['fields']['signature'] = $this->generateSignature(
$data['fields']
);
return $this->load->view(
'extension/payment/citruspay',
$data
);
}
private function getGatewayUrl()
{
if ($this->config->get('payment_citruspay_test')) {
return 'REPLACE_WITH_TEST_ENDPOINT';
}
return 'REPLACE_WITH_PRODUCTION_ENDPOINT';
}
private function formatGatewayAmount($amount, $currency)
{
// 按 integration kit 规定处理小数位和货币单位。
return number_format((float)$amount, 2, '.', '');
}
private function generateSignature(array $fields)
{
$secret = $this->config->get(
'payment_citruspay_secret_key'
);
/*
* 必须使用 integration kit 指定的:
* 1. 字段集合;
* 2. 字段顺序;
* 3. 拼接方式;
* 4. 哈希或 HMAC 算法;
* 5. 输出编码。
*/
throw new LogicException(
'Implement the CitrusPay signing specification'
);
}
}
模板负责提交表单:
<form action="{{ action }}" method="post" id="form-citruspay">
{% for name, value in fields %}
<input type="hidden"
name="{{ name|e }}"
value="{{ value|e }}">
{% endfor %}
<div class="buttons">
<div class="pull-right">
<button type="submit"
class="btn btn-primary">
Confirm Order
</button>
</div>
</div>
</form>
这里没有填写真实的 CitrusPay URL、字段名和签名公式,因为不同的 integration kit 或账户接入版本可能采用不同协议。直接使用未经确认的参数,可能导致支付失败,也可能造成签名验证漏洞。
正确处理支付回调
支付网关显示支付成功页面,并不能证明订单已经可靠付款。回调处理需要验证签名、订单号、金额、币种和交易状态。必要时,还要通过服务端查询接口再次确认交易。
典型流程如下:
public function callback()
{
$payload = $this->request->post;
if (!$this->verifyCallbackSignature($payload)) {
$this->log->write(
'CitrusPay callback signature verification failed'
);
$this->response->addHeader(
$this->request->server['SERVER_PROTOCOL']
. ' 400 Bad Request'
);
return;
}
$orderId = isset($payload['order_id'])
? (int)$payload['order_id']
: 0;
$this->load->model('checkout/order');
$orderInfo = $this->model_checkout_order->getOrder($orderId);
if (!$orderInfo) {
$this->response->addHeader(
$this->request->server['SERVER_PROTOCOL']
. ' 404 Not Found'
);
return;
}
if (!$this->callbackMatchesOrder($payload, $orderInfo)) {
$this->log->write(
'CitrusPay callback order data mismatch: ' . $orderId
);
$this->response->addHeader(
$this->request->server['SERVER_PROTOCOL']
. ' 400 Bad Request'
);
return;
}
if ($this->isSuccessfulPayment($payload)) {
if (!$orderInfo['order_status_id']) {
$this->model_checkout_order->addOrderHistory(
$orderId,
(int)$this->config->get(
'payment_citruspay_order_status_id'
),
'CitrusPay payment confirmed',
true
);
}
}
$this->response->redirect(
$this->url->link('checkout/success', '', true)
);
}
verifyCallbackSignature()、callbackMatchesOrder() 和 isSuccessfulPayment() 必须按照 integration kit 实现,不能只检查浏览器参数中是否存在类似 status=success 的值。
还需要区分两类地址:
return_url:客户完成付款后,浏览器返回的页面。- 服务端通知地址:CitrusPay 服务器主动发送交易结果的 webhook 或 callback。
如果 CitrusPay 接入协议同时支持这两种通知,应以通过签名验证的服务端通知或交易查询结果作为订单付款依据。客户可能在付款后关闭浏览器,浏览器返回请求也可能被伪造。
订单状态的处理建议
客户点击确认支付时,可以先将订单设为 “等待付款”。只有收到验证通过的成功通知后,才能将其改为 “处理中” 或店铺配置的已付款状态。
支付失败、客户取消或状态未知时,都不应把订单标记为已付款:
发起支付 -> Pending
验证成功 -> Processing 或店铺指定状态
明确支付失败 -> Failed
客户主动取消 -> Canceled
无法验证或未知 -> 保持 Pending,并记录日志
支付回调可能重复发送,因此处理逻辑必须具有幂等性。同一笔交易的通知多次到达时,不能重复扣减库存、发送邮件或累计积分。除了检查订单状态,最好保存 CitrusPay 的交易编号,并为该编号设置唯一约束或进行重复检查。
集成 PHP kit 时的注意事项
不要把整个 integration kit 直接复制到 catalog/controller。可以按以下方式处理:
- 将 SDK 放入扩展自己的库目录,或者通过 Composer 安装官方包。
- 在控制器或独立服务类中加载 SDK。
- 将商户密钥保存在 OpenCart 配置中,并限制后台配置权限。
- 前台模板只输出非敏感字段。
- 签名生成和验证都在服务器端完成。
如果 kit 中包含示例 index.php、成功页和失败页,需要把其中的逻辑拆分到 OpenCart 控制器和模板中,不能让示例程序绕过 OpenCart 的订单系统独立运行。
上线前必须验证
上线前至少需要测试以下场景:
- 正常支付成功。
- 客户取消支付。
- 支付失败或余额不足。
- 回调签名错误。
- 回调金额或币种与订单不一致。
- 同一回调重复发送。
- 客户付款后没有返回店铺。
- 回调早于浏览器返回。
- 测试环境与生产环境的凭据相互隔离。
- 使用 HTTPS 时,返回地址可以从公网访问。
- 日志不会记录密钥、完整卡号或其他敏感支付数据。
如果商店使用的是 OpenCart 4.x,不能直接照搬上述文件结构。应先找到该版本自带的跳转型支付扩展,以其命名空间、安装机制、路由和模板路径为基础,再接入 CitrusPay kit。无论 OpenCart 版本如何变化,处理流程都是注册支付方式、生成支付请求、验证服务端回调并更新订单。
备注:内容仅供参考。
