OpenCart payment extension callback 无法工作
结论
index.php?route=payment/hyperion/callback 要正常执行,控制器文件、控制器类名和路由名称必须完全匹配。当前代码还依赖 $this->session->data['order_id'],但支付网关回调通常不会带上用户原有的 OpenCart Session Cookie。即使路由配置正确,回调也可能找不到订单。
需要一并处理以下问题:
- 确保
payment/hyperion/callback正确映射到catalog/controller/payment/hyperion.php。 - 回调不能只通过 Session 获取
order_id。提交支付请求时,应把订单号放入商户参数,并让支付平台在回调中原样返回。 - 创建支付请求前调用
confirm()。否则订单可能仍处于order_status_id = 0,默认不会显示在后台订单列表中。 - 回调收到请求后,先验证支付平台签名,再用合法的数字状态 ID 更新订单。
- 分开处理浏览器返回地址和支付平台的服务器通知地址。
检查 callback 路由
在较老版本的 OpenCart 中,下面的 URL:
index.php?route=payment/hyperion/callback
通常对应这个文件:
catalog/controller/payment/hyperion.php
文件中的类名必须是:
class ControllerPaymentHyperion extends Controller
{
public function callback()
{
// ...
}
}
以下三处名称必须一致:
| 项目 | 正确值 |
|---|---|
| 路由 | payment/hyperion/callback |
| 文件 | catalog/controller/payment/hyperion.php |
| 类名 | ControllerPaymentHyperion |
如果实际控制器文件名是 hyper.php,类名是 ControllerPaymentHyper,回调路由就应该写成:
index.php?route=payment/hyper/callback
代码使用 payment/hyper 语言文件和 hyper_success.tpl 模板本身没有问题,但这可能意味着模块曾在 hyper 和 hyperion 之间改过名称。需要检查控制器文件名、类名以及支付平台中配置的 URL,确认它们仍然使用同一个名称。
还要确认文件放在前台目录:
catalog/controller/payment/hyperion.php
而不是后台目录:
admin/controller/payment/hyperion.php
如果服务器区分大小写,文件名和类名的大小写也必须一致。
直接访问 callback 为什么会出现空白页
直接在浏览器中打开:
index.php?route=payment/hyperion/callback
请求中不会包含支付平台发送的 POST 数据,当前浏览器 Session 里也未必有:
$this->session->data['order_id']
原代码在确认订单是否存在前,就执行了:
$order_id = $order_info['order_id'];
如果 $order_info 是 false,这里可能触发 PHP 警告。后面的所有输出又都放在以下条件中:
if ($order_info) {
// 所有输出都在这里
}
条件不成立时,控制器不会设置任何响应内容,浏览器看到的就是空白页。
至少应该先检查订单号和查询结果:
if (empty($this->session->data['order_id'])) {
$this->response->setOutput('Missing order ID');
return;
}
$order_id = (int)$this->session->data['order_id'];
$order_info = $this->model_checkout_order->getOrder($order_id);
if (!$order_info) {
$this->response->setOutput('Order not found');
return;
}
这只能让错误信息更清楚,无法解决支付平台回调丢失 Session 的根本问题。
回调不能依赖 Session
用户跳转到第三方支付网站后,再由支付网站发起跨站 POST 返回时,浏览器不一定会发送原来的 Session Cookie。服务器之间的异步通知更不会带有用户的浏览器 Session。
因此,下面这种写法并不可靠:
$order_id = $this->session->data['order_id'];
创建支付请求时,应该把订单号写入支付平台提供的商户参考字段,例如 merchant_reference、order_id 或 custom。实际使用哪个字段,必须以支付平台的接口文档为准:
$order_id = (int)$this->session->data['order_id'];
$payment_data = array(
'merchant_reference' => $order_id,
'callback_url' => $this->url->link(
'payment/hyperion/callback',
'',
'SSL'
),
'return_url' => $this->url->link(
'payment/hyperion/returnPage',
'',
'SSL'
)
);
收到回调后,再从支付平台返回的数据中读取订单号:
$order_id = isset($this->request->post['merchant_reference'])
? (int)$this->request->post['merchant_reference']
: 0;
不能在收到未经验证的 order_id 后立即更新订单。代码还必须验证支付平台签名、交易号、金额和币种,以免攻击者伪造支付成功请求。
跳转支付平台前确认订单
旧版 OpenCart 可能会先以 order_status_id = 0 保存新订单。这类订单通常不会出现在后台的常规订单列表中。
生成支付表单并准备跳转到支付平台时,应先确认订单:
$order_id = (int)$this->session->data['order_id'];
$this->load->model('checkout/order');
$this->model_checkout_order->confirm(
$order_id,
(int)$this->config->get('config_order_status_id')
);
更合适的方式是配置一个“等待付款”状态,并在跳转前使用它:
$this->model_checkout_order->confirm(
$order_id,
(int)$this->config->get('hyper_pending_status_id')
);
这样,即使用户在支付平台关闭页面,订单也已经出现在后台,并保持“等待付款”状态。
不同 OpenCart 版本提供的订单接口并不完全相同。较新版本可能使用:
$this->model_checkout_order->addOrderHistory(
$order_id,
$order_status_id,
$comment,
false
);
题目中的 confirm() 和 update() 是较老版本的调用方式。具体代码需要根据当前项目中 catalog/model/checkout/order.php 实际提供的方法调整。
一个更可靠的 callback 示例
下面的示例沿用旧版 OpenCart 的 confirm() 和 update() 接口。签名验证部分仍需按照支付平台文档实现:
public function callback()
{
$this->load->model('checkout/order');
$order_id = isset($this->request->post['merchant_reference'])
? (int)$this->request->post['merchant_reference']
: 0;
if (!$order_id) {
$this->response->addHeader(
$this->request->server['SERVER_PROTOCOL'] . ' 400 Bad Request'
);
$this->response->setOutput('Missing order reference');
return;
}
$order_info = $this->model_checkout_order->getOrder($order_id);
if (!$order_info) {
$this->response->addHeader(
$this->request->server['SERVER_PROTOCOL'] . ' 404 Not Found'
);
$this->response->setOutput('Order not found');
return;
}
if (!$this->verifyHyperionCallback($this->request->post, $order_info)) {
$this->response->addHeader(
$this->request->server['SERVER_PROTOCOL'] . ' 403 Forbidden'
);
$this->response->setOutput('Invalid signature');
return;
}
$payment_status = isset($this->request->post['flag_msg'])
? $this->request->post['flag_msg']
: '';
$message = 'Payment Status = ' . $payment_status . "\n";
if (isset($this->request->post['Response'])) {
$message .= 'Response = ' . $this->request->post['Response'] . "\n";
}
if (isset($this->request->post['Result'])) {
$message .= 'Result = ' . $this->request->post['Result'] . "\n";
}
if ($payment_status === 'Status_Success') {
$status_id = (int)$this->config->get('hyper_order_status_id');
$this->model_checkout_order->update(
$order_id,
$status_id,
$message,
false
);
$this->response->setOutput('OK');
return;
}
if ($payment_status === 'Status_Fail') {
$status_id = (int)$this->config->get('hyper_failed_status_id');
if (isset($this->request->post['message'])) {
$message .= 'Reason = ' . $this->request->post['message'] . "\n";
}
$this->model_checkout_order->update(
$order_id,
$status_id,
$message,
false
);
$this->response->setOutput('OK');
return;
}
$this->response->addHeader(
$this->request->server['SERVER_PROTOCOL'] . ' 400 Bad Request'
);
$this->response->setOutput('Unknown payment status');
}
签名验证方法可以采用以下结构:
private function verifyHyperionCallback(array $post, array $order_info)
{
// 按支付平台文档:
// 1. 读取签名字段;
// 2. 使用共享密钥重新计算签名;
// 3. 使用 hash_equals() 比较;
// 4. 核对金额、币种、商户号和订单号;
// 5. 必要时主动查询支付平台交易状态。
return false;
}
在没有明确签名规则的情况下,不能为了让流程通过而让这个方法始终返回 true。
失败状态必须使用数字 ID
原代码中的这一行有误:
$this->model_checkout_order->update($order_id, 'Failed', $message, false);
订单模型需要的是订单状态 ID,不是状态名称。应该在模块设置中保存失败状态的 ID,并以整数形式传入:
$failed_status_id = (int)$this->config->get('hyper_failed_status_id');
$this->model_checkout_order->update(
$order_id,
$failed_status_id,
$message,
false
);
成功状态的配置也需要确认存在,且值不能为空:
$success_status_id = (int)$this->config->get('hyper_order_status_id');
分开处理浏览器返回页面和服务器通知
支付平台通常会发起两类请求:
callback或notify:由支付平台服务器通知 OpenCart,负责更新订单。return_url:由用户浏览器返回商店,负责显示支付结果。
服务器通知通常不需要渲染完整的主题模板。完成请求验证和订单更新后,返回支付平台要求的响应即可,例如 OK。
浏览器返回可以由单独的方法处理:
public function returnPage()
{
$this->redirect($this->url->link('checkout/success', '', 'SSL'));
}
如果需要显示自定义的成功或失败模板,也应该读取数据库中的真实订单状态,不能只相信浏览器提交的 flag_msg。
还需要检查的配置
修复代码后,逐项确认以下内容:
- 支付平台配置的回调 URL 是完整的公网 HTTPS 地址。
- 路由名称、控制器文件名和类名保持一致。
- 回调 URL 没有被
.htaccess、防火墙、维护模式或安全插件拦截。 - 支付平台确实使用
POST发送请求,字段名与代码完全一致。 hyper_order_status_id和hyper_failed_status_id保存的是有效数字 ID。- 跳转支付前已经对订单调用
confirm()。 - 回调通过平台返回的商户参考号定位订单,不依赖 Session。
- 只有在签名、金额、币种和商户号均通过核验后,才能把订单更新为成功状态。
- 重复通知需要进行幂等处理,避免同一订单被多次更新或重复发送通知。
- 检查 OpenCart 日志和 PHP 错误日志。调试时不能只根据空白页判断错误。
这里要解决的不只是模板问题。路由名称必须保持一致,订单识别方式也要从浏览器 Session 改为经过签名验证的支付平台订单引用。完成这些修改后,返回页面才能正常访问,订单会在跳转支付前进入后台,并在支付结果到达后更新为正确状态。
备注:内容仅供参考。
