PAYATHON 2026

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 可能已经不存在。

推荐的支付流程

可以按以下流程处理:

  1. OpenCart 创建订单并取得 order_id。
  2. 将 order_id、金额、币种和回调地址等参数发送给支付网关。
  3. 支付网关处理付款。
  4. 支付网关请求支付模块的 callback 地址。
  5. 回调接口验证网关签名和支付状态。
  6. 使用返回的 order_id 查询订单。
  7. 核对金额、币种、商户号和交易状态。
  8. 调用 OpenCart 订单模型更新订单状态。
  9. 将用户重定向到 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 只负责显示订单确认后的结果。

备注:内容仅供参考。