PAYATHON 2026

Opencart 2.0 升级后 Atom Payment gateway 无法使用

支付老李

结论

问题不一定出在 controller 或 model。OpenCart 2.0 的结账页会通过 AJAX 加载支付模块视图,最终的 “Confirm Order” 按钮及其点击事件需要由 catalog/view/theme/.../template/payment/atompay.tpl 正确输出。

如果使用的仍是 OpenCart 1.5 模板,按钮带有 disabled 属性,或者 JavaScript 绑定了旧的按钮 ID,按钮就无法启用。这类问题通常不会写入 PHP error log。

当前代码还混用了 OpenCart 1.5 的 API。即使按钮恢复正常,支付跳转和回调仍可能因为 $this->redirect()、confirm()、update() 等旧方法而失败。

先修复支付模板

OpenCart 2.0.1.0 使用 .tpl,不是后续版本使用的 Twig 模板。控制器可以把支付请求地址传给模板:

<?php
class ControllerPaymentAtompay extends Controller {
    public function index() {
        $this->load->language('payment/atompay');

        $data['button_confirm'] = $this->language->get('button_confirm');
        $data['continue'] = $this->url->link(
            'payment/atompay/dopayment',
            '',
            'SSL'
        );

        $template = $this->config->get('config_template')
            . '/template/payment/atompay.tpl';

        if (file_exists(DIR_TEMPLATE . $template)) {
            return $this->load->view($template, $data);
        }

        return $this->load->view(
            'default/template/payment/atompay.tpl',
            $data
        );
    }
}

这里应使用:

$this->load->language('payment/atompay');

不要使用旧式写法:

$this->language->load('payment/atompay');

接着检查:

catalog/view/theme/当前主题/template/payment/atompay.tpl

可以先用下面这个最小模板进行验证:

<div class="buttons">
    <div class="pull-right">
        <input
            type="button"
            value="<?php echo $button_confirm; ?>"
            id="button-confirm"
            class="btn btn-primary"
        />
    </div>
</div>

<script type="text/javascript">
$('#button-confirm').on('click', function () {
    window.location.href = '<?php echo $continue; ?>';
});
</script>

按钮中不能包含:

disabled="disabled"

事件选择器也必须与实际按钮一致。比如 JavaScript 绑定的是 #button-confirm,HTML 中就必须使用:

id="button-confirm"

不要继续沿用 OpenCart 1.5 模板中的旧 ID、旧 DOM 结构或已经不存在的变量。

先确认被禁用的是哪个按钮

OpenCart 2.0 的默认结账流程中,至少有两个相关按钮可能处于禁用状态。

如果被禁用的是:

id="button-payment-method"

先检查页面上是否有 “我已阅读并同意条款” 复选框。没有勾选条款时,结账流程不会进入支付确认区域,这与 Atom 模块控制器无关。

如果被禁用的是:

id="button-confirm"

则应重点检查 atompay.tpl:

  • 按钮是否写死了 disabled。
  • 系统是否加载了错误主题下的模板。
  • 页面是否出现 JavaScript 错误。
  • 点击事件是否绑定到正确的 ID。
  • 模板是否确实收到了 $continue 或 $url2。
  • AJAX 返回的模板 HTML 是否完整。

PHP 日志中没有报错并不奇怪,因为按钮状态和点击事件都属于浏览器端行为。打开浏览器开发者工具,检查 Console,并在 Network 中查看以下路由的请求:

index.php?route=payment/atompay
index.php?route=payment/atompay/dopayment

替换 OpenCart 1.5 的重定向方法

OpenCart 2.0 应使用:

$this->response->redirect($url);

不要继续使用:

$this->redirect($url);

例如:

$this->response->redirect(
    $this->url->link('checkout/atomfailure', 'msg=1', 'SSL')
);

原代码中的路由写法也有问题:

$this->url->link('checkout/atomfailure&msg=1')

msg=1 应作为 $this->url->link() 的第二个参数传入,不能拼接到 route 名称中。

支付网关返回跳转地址后,也建议通过 OpenCart 的响应对象完成重定向:

$this->response->redirect($url);

不要直接调用:

header('Location: ' . $url);

更新订单状态 API

OpenCart 2.0 的订单模型应使用 addOrderHistory()。原代码中的以下方法属于旧版本接口:

$this->model_checkout_order->confirm(...)
$this->model_checkout_order->update(...)

成功回调可以改成:

public function success() {
    $order_id = isset($this->request->post['mer_txn'])
        ? (int)$this->request->post['mer_txn']
        : 0;

    $f_code = isset($this->request->post['f_code'])
        ? $this->request->post['f_code']
        : '';

    if (!$order_id) {
        $this->response->redirect(
            $this->url->link('checkout/atomfailure', 'msg=invalid_order', 'SSL')
        );
        return;
    }

    $this->load->model('checkout/order');

    if ($f_code === 'Ok') {
        $this->model_checkout_order->addOrderHistory(
            $order_id,
            (int)$this->config->get('atompay_order_status_id'),
            'Payment Received',
            false
        );

        $this->response->redirect(
            $this->url->link('checkout/success', '', 'SSL')
        );
        return;
    }

    $this->model_checkout_order->addOrderHistory(
        $order_id,
        (int)$this->config->get('config_order_status_id'),
        'Payment failed',
        false
    );

    $this->response->redirect(
        $this->url->link('checkout/atomfailure', '', 'SSL')
    );
}

生产环境不能只凭客户端提交的 f_code=Ok 判断付款成功。还需要按照 Atom 的接口规范校验签名、交易金额、商户编号和交易状态。如果 Atom 提供服务器端交易查询接口,最好再查询一次以确认结果。

修正 XML 返回值判断

下面的代码混淆了 “元素是否存在” 和 “元素值是否为空”:

if (
    isset($xml_values[3]['value']) == '' ||
    isset($xml_values[4]['value']) == '' ||
    isset($xml_values[5]['value']) == ''
)

应改为明确检查值是否为空:

if (
    empty($xml_values[3]['value']) ||
    empty($xml_values[4]['value']) ||
    empty($xml_values[5]['value']) ||
    empty($xml_values[6]['value'])
) {
    $this->response->redirect(
        $this->url->link('checkout/atomfailure', 'msg=1', 'SSL')
    );
    return;
}

不过,依赖固定数组下标读取 XML 很不可靠。网关只要增加节点、加入空白节点或调整元素顺序,下标就可能发生变化。条件允许时,应使用 SimpleXML 按元素名称读取返回值,同时检查 XML 是否解析成功。

修正 cURL 配置

下面这些设置不会让初始按钮一直处于 disabled 状态,但可能导致用户点击按钮后,支付请求失败:

curl_setopt($ch, CURLOPT_PORT, 80);
curl_setopt($ch, CURLOPT_SSLVERSION, 3);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

这些设置存在以下问题:

  • 如果 Atom 地址使用 HTTPS,强制指定端口 80 会造成协议或连接错误。
  • SSLv3 已经过时,现代支付服务器通常已经将其禁用。
  • 关闭证书校验会带来中间人攻击风险。
  • 代码没有检查 curl_exec()、HTTP 状态码和 XML 解析错误。

建议删除固定端口和 SSLv3 设置,并保留证书校验:

$ch = curl_init();

curl_setopt_array($ch, array(
    CURLOPT_URL            => $data['action'],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $postFields,
    CURLOPT_CONNECTTIMEOUT => 15,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2
));

$returnData = curl_exec($ch);
$curlError = curl_error($ch);
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

if ($returnData === false || $httpCode < 200 || $httpCode >= 300) {
    $this->log->write(
        'Atom payment request failed. HTTP ' . $httpCode
        . '; cURL error: ' . $curlError
    );

    $this->response->redirect(
        $this->url->link('checkout/atomfailure', 'msg=gateway_error', 'SSL')
    );
    return;
}

请求参数也应通过 http_build_query() 生成,避免姓名、地址或邮箱中的 &、+、空格等字符破坏请求:

$postFields = http_build_query(array(
    'login'      => $data['login'],
    'pass'       => $data['pass'],
    'ttype'      => $data['ttype'],
    'prodid'     => $data['prodid'],
    'amt'        => $data['amt'],
    'txncurr'    => 'INR',
    'txnscamt'   => '0',
    'clientcode' => base64_encode('123'),
    'txnid'      => $data['txnid'],
    'date'       => $datenow,
    'custacc'    => '123456789012',
    'udf1'       => $data['CustomerName'],
    'udf2'       => $data['CustomerEMail'],
    'udf3'       => $data['BillingPhone'],
    'udf4'       => $data['BillingAddress1'],
    'ru'         => $data['ru']
));

建议的排查顺序

先在浏览器中确认按钮的实际 ID。如果是 button-payment-method,检查服务条款复选框。如果是 Atom 的 button-confirm,检查实际加载的 atompay.tpl 和浏览器 Console。

确认模板能输出一个不带 disabled 属性的按钮,并且点击后能跳转到:

payment/atompay/dopayment

然后依次替换这些旧版 API:

$this->redirect()
confirm()
update()

分别改为:

$this->response->redirect()
addOrderHistory()

最后处理 cURL、XML 解析和回调验证。当前按钮无法使用,最常见的直接原因是支付模板没有按照 OpenCart 2.0 的结账结构更新。按钮恢复后,控制器中的旧 API 很可能会成为下一处故障。

备注:内容仅供参考。