PAYATHON 2026

OpenCart 支付后白屏并自动退出登录

支付小周

结论

报错的直接原因是支付回调执行时,$this->session->data['payment_method'] 已经不存在。代码没有先检查该键,直接读取后触发了 Undefined index。如果后续流程仍依赖这些 session 数据,就可能中断并出现白屏。

解决问题不能只靠隐藏 Notice,还要查清 session 丢失的原因,并改造支付回调。支付结果验证不应依赖客户浏览器的登录状态或 payment_method session。回调应根据订单号、支付流水号等参数找到订单,再通过支付网关的服务端接口验证结果。

为什么会同时白屏和退出登录

客户登录信息、购物车、当前订单和 payment_method 通常保存在同一个 OpenCart session 中。如果请求没有带回 session cookie,或者服务器找不到对应的 session 数据,就会发生以下情况:

  • 客户变为未登录状态;
  • payment_method、order_id 等数据消失;
  • 支付回调无法继续处理;
  • 代码读取不存在的数组键,触发 PHP Notice;
  • 错误显示配置或支付扩展之后抛出的异常造成白屏。

Undefined index 说明 session 数据已经缺失,但它不一定是故障的起点。网关验证超时也无法解释客户为什么同时退出登录,除非扩展在验证失败后主动清除了 session。

先修复回调中的未定义数组访问

至少要先加入防御性检查,避免 PHP Notice 破坏响应:

public function callback() {
    if (
        !isset($this->session->data['payment_method']['code']) ||
        $this->session->data['payment_method']['code'] !== 'saman'
    ) {
        $this->log->write(
            'Saman callback: payment_method is missing; session_id=' .
            session_id()
        );

        $this->response->addHeader('HTTP/1.1 400 Bad Request');
        $this->response->setOutput('Invalid payment session');
        return;
    }

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

    // 原有验证逻辑
}

Mellat 扩展也需要做同样的处理。不过,这项修改只能避免白屏并留下诊断信息,无法从根本上保证支付成功后订单能被正确更新。

不要简单地用 @ 抑制错误,也不要在 payment_method 缺失时直接将订单标记为已付款。

支付回调不应依赖 session

浏览器从银行或支付网关返回时,原 session 可能已经过期,cookie 可能没有发送,客户也可能已经关闭页面。网关发送的服务器通知更不会包含客户 session。可靠的支付流程应按以下方式处理:

  1. 创建支付请求时,保存 OpenCart 订单号和网关交易标识。
  2. 将网关允许的订单标识作为请求参数提交给网关。
  3. 收到回调后,从已经校验的返回参数中取得标识。
  4. 根据标识从数据库查询订单和支付记录。
  5. 调用网关的 verify API,在服务端核对金额、订单号和交易状态。
  6. 验证成功后,以幂等方式更新订单状态。
  7. 浏览器返回页只显示处理结果,不作为确认收款的唯一环节。

下面是结构示意。具体字段名、签名算法和 verify 方法必须按照相应网关的文档实现:

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

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

    if (!$order_id || $transaction_id === '') {
        $this->log->write('Payment callback: required parameters are missing');
        $this->response->addHeader('HTTP/1.1 400 Bad Request');
        $this->response->setOutput('Invalid callback');
        return;
    }

    $this->load->model('checkout/order');
    $order_info = $this->model_checkout_order->getOrder($order_id);

    if (!$order_info) {
        $this->log->write('Payment callback: order not found: ' . $order_id);
        $this->response->addHeader('HTTP/1.1 404 Not Found');
        $this->response->setOutput('Order not found');
        return;
    }

    // 必须调用网关服务端 verify API,并核对签名、订单号、金额和状态。
    $verified = $this->verifyWithGateway(
        $transaction_id,
        $order_info['total'],
        $order_id
    );

    if (!$verified) {
        $this->log->write(
            'Payment callback: verification failed for order ' . $order_id
        );
        $this->response->addHeader('HTTP/1.1 400 Bad Request');
        $this->response->setOutput('Verification failed');
        return;
    }

    // 支付成功状态 ID 应读取扩展配置,不应在示例中随意写死。
    $success_status_id = (int)$this->config->get(
        'payment_saman_order_status_id'
    );

    $this->model_checkout_order->addOrderHistory(
        $order_id,
        $success_status_id,
        'Gateway transaction: ' . $transaction_id,
        false
    );

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

verifyWithGateway() 只是用于说明流程,并非 OpenCart 内置 API。还需要防止同一笔交易被重复回调,以免重复记账或发货。

重点检查 session 丢失的原因

故障集中在特定时间段,因此应优先排查服务器和 session 存储,而不是继续更换 CDN。

1. 检查 session 存储是否被定时清理

确认以下 PHP 配置:

session.save_handler
session.save_path
session.gc_maxlifetime
session.gc_probability
session.gc_divisor

如果使用文件 session,需要检查:

  • session.save_path 是否存在,以及 PHP 用户是否有读写权限;
  • 磁盘空间或 inode 是否耗尽;
  • 临时目录是否被系统任务、控制面板或 cron 提前清理;
  • session 文件的实际存活时间是否短于支付过程;
  • 故障时间是否与清理、备份或部署任务重合。

除了 PHP error log,还应检查 PHP-FPM、Web 服务器、系统日志和定时任务日志。

2. 检查多节点或容器部署

如果源站有多个 PHP 节点,而 session 保存在各节点的本地文件中,请求切换节点后就可能随机退出登录。通常可以采用以下处理方式:

  • 使用数据库、Redis 等共享 session 存储;
  • 启用并验证负载均衡的会话保持;
  • 确保所有节点使用相同的 session 配置。

如果 Redis 可能淘汰数据,还要检查内存上限和 eviction 记录。

确认支付前后的 URL 完全一致,重点检查:

  • www.example.com 与 example.com;
  • HTTP 与 HTTPS;
  • OpenCart 商店 URL 和 SSL URL;
  • 反向代理传递的 HTTPS 状态;
  • cookie 的 Domain、Path、Secure 和 SameSite 属性。

如果网关通过跨站 POST 返回,浏览器的 SameSite 策略可能阻止请求携带 session cookie。这类问题一般与浏览器版本、返回方式和 cookie 配置有关,未必只在特定时间段出现,但仍然需要排查。

不要为了兼容而随意关闭 cookie 的安全属性。如果跨站 POST 确实需要携带 cookie,通常要设置 SameSite=None; Secure,并确保全站使用 HTTPS。具体配置方式取决于当前 PHP、Web 服务器和 OpenCart 的 session 实现。

4. 检查 session 名称和 PHP-FPM 配置是否变化

如果不同站点、PHP-FPM 池或节点使用不同的 session.name、session.save_path 或 PHP 版本,请求切换后也会创建新 session。还要确认服务是否在故障时段发生过重启、自动扩容或配置重载。

增加可关联的诊断日志

可以在发起支付和进入回调时分别记录以下信息:

$this->log->write(
    'Payment trace: session_id=' . session_id() .
    '; order_id=' .
    (isset($this->session->data['order_id'])
        ? (int)$this->session->data['order_id']
        : 0) .
    '; host=' . ($this->request->server['HTTP_HOST'] ?? '') .
    '; https=' . (!empty($this->request->server['HTTPS']) ? '1' : '0')
);

日志还应包含请求时间、服务器节点标识、回调方式、订单号和网关交易号。不要记录完整 cookie、银行卡资料、密钥、验证码或未经脱敏的支付报文。

如果支付前后的 session_id() 不同,应继续排查 cookie、域名和代理配置。如果 ID 相同但数据为空,则应重点检查 session 存储、清理机制以及多节点之间的一致性。

注意事项

生产环境应关闭面向客户的 PHP 错误显示,同时保留错误日志。关闭 display_errors 只能防止错误信息破坏页面,不能代替实际修复。

还需要确认 Saman 和 Mellat 扩展是否明确兼容 OpenCart 3.0.2.0 以及当前 PHP 版本。两个不同的支付扩展在相近时间段同时缺少 payment_method,更可能是公共 session 层、域名 cookie 或基础设施出现问题,而不是两个网关同时发生了相同的超时。最终需要同时解决两层问题:恢复 session 的稳定性,并让支付确认流程不再依赖浏览器 session。

备注:内容仅供参考。