OpenCart 支付成功后应重定向到哪个链接?
明确结论
支付成功后,不要让支付网关直接重定向到:
http://example.com/opencart/index.php?route=checkout/success
checkout/success 只是结果展示页面。它既不验证支付结果,也不会调用订单确认逻辑。
支付模块应提供独立的回调接口,例如:
http://example.com/opencart/index.php?route=payment/kzm/callback
支付网关先请求这个接口。回调接口根据返回参数找到订单,验证支付状态、金额、币种和签名。确认无误后,再调用订单模型更新状态。最后将用户重定向到:
index.php?route=checkout/success
为什么订单没有被确认
当前模块真正用于确认订单的是:
$this->model_checkout_order->confirm(
$this->session->data['order_id'],
$this->config->get('bank_transfer_order_status_id'),
$comment,
true
);
访问 checkout/success 不会执行这段代码。直接跳转到成功页,只会显示成功提示,不会更新订单状态。
支付网关返回时,也不能只依赖:
$this->session->data['order_id']
用户可能更换浏览器、丢失 Cookie 或等待时间过长,支付网关也可能通过服务器直接发送通知。遇到这些情况时,原来的 OpenCart Session 可能已经不存在。
推荐的支付流程
可以按以下流程处理:
- OpenCart 创建订单并取得
order_id。 - 将
order_id、金额、币种和回调地址等参数发送给支付网关。 - 支付网关处理付款。
- 支付网关请求支付模块的
callback地址。 - 回调接口验证网关签名和支付状态。
- 使用返回的
order_id查询订单。 - 核对金额、币种、商户号和交易状态。
- 调用 OpenCart 订单模型更新订单状态。
- 将用户重定向到
checkout/success。
如果支付网关同时支持“服务器通知地址”和“浏览器返回地址”,应分别设置:
notify_url -> payment/kzm/callback
success_url -> payment/kzm/success
error_url -> payment/kzm/cancel
服务器通知负责确认订单,浏览器返回地址只负责显示结果。
回调控制器示例
下面的示例采用题目中的旧版 OpenCart 控制器结构。网关参数名称和签名算法需要根据支付网关文档替换。
class ControllerPaymentKzm extends Controller {
public function callback() {
$this->load->model('checkout/order');
$order_id = isset($this->request->get['order_id'])
? (int)$this->request->get['order_id']
: 0;
$transaction_id = isset($this->request->get['transaction_id'])
? $this->request->get['transaction_id']
: '';
$payment_status = isset($this->request->get['status'])
? $this->request->get['status']
: '';
$signature = isset($this->request->get['signature'])
? $this->request->get['signature']
: '';
if (!$order_id) {
$this->response->setOutput('Invalid order');
return;
}
$order_info = $this->model_checkout_order->getOrder($order_id);
if (!$order_info) {
$this->response->setOutput('Order not found');
return;
}
if (!$this->verifyGatewaySignature($this->request->get, $signature)) {
$this->response->setOutput('Invalid signature');
return;
}
if ($payment_status !== 'SUCCESS') {
$this->response->setOutput('Payment not completed');
return;
}
/*
* 还应核对:
* - merchant_id
* - amount
* - currency
* - transaction_id
*
* 金额应使用支付网关要求的最小货币单位或定点格式,
* 不要直接进行不可靠的浮点数比较。
*/
$paid_order_status_id = $this->config->get(
'kzm_paid_order_status_id'
);
$comment = 'Payment confirmed. Transaction: ' . $transaction_id;
$this->model_checkout_order->confirm(
$order_id,
$paid_order_status_id,
$comment,
true
);
$this->response->setOutput('OK');
}
private function verifyGatewaySignature($parameters, $received_signature) {
unset($parameters['route']);
unset($parameters['signature']);
ksort($parameters);
$payload = http_build_query($parameters, '', '&');
$secret = $this->config->get('kzm_secret');
$expected_signature = hash_hmac('sha256', $payload, $secret);
return hash_equals($expected_signature, $received_signature);
}
}
这里的 hash_hmac('sha256', ...) 只用于展示安全处理的基本结构。实际开发时,必须严格使用支付网关规定的字段顺序、字符编码和签名算法,不能自行选择签名方式。
部分旧版 OpenCart 使用 confirm(),较新版本可能通过 addOrderHistory() 更新订单状态。例如:
$this->model_checkout_order->addOrderHistory(
$order_id,
$paid_order_status_id,
$comment,
true
);
具体使用哪个 API,取决于实际的 OpenCart 版本。
浏览器成功返回接口
如果支付网关要求提供浏览器成功返回地址,可以增加一个只负责跳转的控制器方法:
public function success() {
$this->response->redirect(
$this->url->link('checkout/success', '', 'SSL')
);
}
对应地址是:
http://example.com/opencart/index.php?route=payment/kzm/success
用户到达这个地址,并不能证明付款已经成功。订单应先通过可信的服务器通知完成确认。如果没有服务器通知,这个接口必须主动调用支付网关的交易查询 API,确认付款状态后才能更新订单。
是否需要在 successurl 中传递加密值
可以传递订单标识,但不能把“能够解密订单编号”作为支付成功的证明。
最简单的做法是传递:
order_id=123
并附加签名:
order_id=123&expires=1788200000&signature=...
签名可以这样生成:
$order_id = (int)$this->session->data['order_id'];
$expires = time() + 3600;
$payload = $order_id . '|' . $expires;
$signature = hash_hmac(
'sha256',
$payload,
$this->config->get('kzm_secret')
);
$success_url = $this->url->link(
'payment/kzm/success',
'order_id=' . $order_id .
'&expires=' . $expires .
'&signature=' . urlencode($signature),
'SSL'
);
回调时需要重新计算 HMAC,并通过 hash_equals() 比较签名。这样可以防止用户随意篡改 order_id。
不过,这个签名只能证明参数是由商店生成的,不能证明支付已经发生。最终仍需验证支付网关返回的签名,或者调用网关的交易查询 API 核实交易状态。
模板中的正确处理方式
确认按钮通常先请求支付模块的初始化接口。服务器生成支付参数和回调地址后,再让浏览器跳转到支付网关。例如:
$('#button-confirm').bind('click', function () {
$.ajax({
url: 'index.php?route=payment/kzm/create',
type: 'post',
dataType: 'json',
success: function (json) {
if (json.redirect) {
location = json.redirect;
}
}
});
});
商户密钥和签名逻辑不能放在 .tpl 文件或 JavaScript 中。敏感参数和签名都应由服务端生成。
必须注意的安全问题
- 不能因为用户访问了
successurl,就把订单标记为已支付。 - 不能只依赖 Session 中的
order_id。 - 必须验证支付网关的签名或消息认证码。
- 必须核对订单金额、币种、商户号和支付状态。
- 应记录支付网关的唯一交易编号,防止同一交易被重复使用。
- 回调处理必须具备幂等性。网关重复发送通知时,不能重复确认订单、扣减库存或发送邮件。
errorurl只表示用户返回或取消,不一定意味着支付失败。订单的最终状态应以网关通知或交易查询结果为准。- 回调地址应使用 HTTPS。
- 通过 GET 参数提交
successurl和errorurl时,应使用urlencode()或rawurlencode()编码。 - 不要把商户密钥、完整签名原文或敏感支付信息输出到
testkzm.php。
最终的处理结构是:支付网关返回支付模块自己的回调路由,由该路由验证支付结果并确认对应订单。checkout/success 只负责显示订单确认后的结果。
备注:内容仅供参考。
