PAYATHON 2026

OpenCart Payment Plugin 订单状态更新失败

支付老李

结论

问题出在两个分支调用 addOrderHistory() 时传入了同一个订单状态 ID:

$this->config->get('payx_order_status_id')

这意味着无论交易状态是 approved、failed 还是 declined,代码都会把订单更新为同一种状态。插件需要分别配置支付成功和支付失败对应的订单状态,再根据支付平台返回的结果选择正确的 order_status_id。

如果失败交易根本没有触发回调,还要确认支付平台是否会在交易未获批准时请求该 callback() 地址。没有回调到达服务器,OpenCart 自然无法处理。

原因分析

目前 if 和 else 中执行的代码完全相同:

if (trim(strtolower($status)) == 'approved') {
    $this->model_checkout_order->addOrderHistory(
        $order_id,
        $this->config->get('payx_order_status_id'),
        $message,
        false
    );
} else {
    $this->model_checkout_order->addOrderHistory(
        $order_id,
        $this->config->get('payx_order_status_id'),
        $message,
        false
    );
}

addOrderHistory() 的第二个参数才是订单要更新到的状态 ID。修改 $message 只会改变订单历史中的备注,不会让 OpenCart自行判断交易成功还是失败。

还需要检查以下几项:

  • 后台是否保存了失败状态对应的配置项。
  • 失败状态配置是否返回有效且非零的订单状态 ID。
  • 支付平台是否会在拒付、取消或失败时继续发送服务器回调。
  • 回调中的 merchant_ref 是否确实对应 OpenCart 的 order_id。
  • 支付平台返回的 $status 是否符合预期,例如 declined、failed 或 cancelled。
  • XML 节点属于 SimpleXMLElement 对象,比较或拼接之前最好显式转换为字符串。

解决方法

在插件后台至少增加两个订单状态配置项:

  • payx_order_status_id:支付成功时使用。
  • payx_failed_status_id:支付失败或被拒绝时使用。

如果支付平台能区分取消、失败和拒绝等状态,也可以分别增加:

  • payx_cancelled_status_id
  • payx_failed_status_id
  • payx_declined_status_id

保存设置时,配置项的键名必须与代码读取时使用的键名完全一致。

修改后的回调示例

下面的示例沿用原有结构,但为成功和失败状态选择不同的订单状态 ID:

public function callback() {
    if (empty($this->request->request['transaction_id'])) {
        return;
    }

    $requested_transaction_id = (string)$this->request->request['transaction_id'];

    $url = 'https://payx.com/?v_transaction_id=' .
        rawurlencode($requested_transaction_id);

    $xml = file_get_contents($url);

    if ($xml === false) {
        $this->log->write('PayX callback: unable to retrieve transaction');
        return;
    }

    try {
        $xml_elements = new SimpleXMLElement($xml);
    } catch (Exception $e) {
        $this->log->write(
            'PayX callback: invalid XML response - ' . $e->getMessage()
        );
        return;
    }

    $order_id = isset($xml_elements->merchant_ref)
        ? (int)$xml_elements->merchant_ref
        : 0;

    $status = isset($xml_elements->status)
        ? trim(strtolower((string)$xml_elements->status))
        : '';

    $transaction_id = isset($xml_elements->transaction_id)
        ? (string)$xml_elements->transaction_id
        : '';

    if (!$order_id || $status === '') {
        $this->log->write('PayX callback: missing order ID or status');
        return;
    }

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

    $order_info = $this->model_checkout_order->getOrder($order_id);

    if (!$order_info) {
        $this->log->write(
            'PayX callback: order not found - ' . $order_id
        );
        return;
    }

    $message = 'Payment Status: ' . $status .
        ' - Transaction Id: ' . $transaction_id;

    if ($status === 'approved') {
        $order_status_id = (int)$this->config->get(
            'payx_order_status_id'
        );
    } else {
        $order_status_id = (int)$this->config->get(
            'payx_failed_status_id'
        );
    }

    if (!$order_status_id) {
        $this->log->write(
            'PayX callback: no order status configured for payment status ' .
            $status
        );
        return;
    }

    $this->model_checkout_order->addOrderHistory(
        $order_id,
        $order_status_id,
        $message,
        false
    );
}

如果要精确映射多个支付状态,可以改用 switch:

switch ($status) {
    case 'approved':
        $order_status_id = (int)$this->config->get(
            'payx_order_status_id'
        );
        break;

    case 'cancelled':
        $order_status_id = (int)$this->config->get(
            'payx_cancelled_status_id'
        );
        break;

    case 'declined':
    case 'failed':
        $order_status_id = (int)$this->config->get(
            'payx_failed_status_id'
        );
        break;

    default:
        $order_status_id = (int)$this->config->get(
            'payx_pending_status_id'
        );
        break;
}

状态字符串必须以支付平台实际返回的值为准,不能假定所有平台都会使用 failed 或 declined。

如何确认问题发生在哪一层

可以暂时把回调中的几个关键字段写入日志:

$this->log->write(
    'PayX callback: order_id=' . $order_id .
    ', status=' . $status .
    ', transaction_id=' . $transaction_id
);

然后分别测试成功和失败交易:

  1. 如果两种交易都有日志,但订单最终状态相同,通常是状态 ID 的配置或映射有误。
  2. 如果能看到失败日志,但状态更新没有成功,检查 payx_failed_status_id 是否为空或等于 0。
  3. 如果失败交易没有留下任何回调日志,检查支付平台的回调规则、回调地址和服务器访问日志。
  4. 如果日志中的 $order_id 为空,或者对应订单不存在,检查 merchant_ref 的生成和传递过程。

安全与稳定性注意事项

不要只根据回调 URL 中的 transaction_id 和远程 XML 更新订单。至少还要验证:

  • 回调或查询结果中的签名、密钥或其他认证信息。
  • 返回的商户账号是否属于当前站点。
  • 返回金额是否与订单金额一致。
  • 币种是否一致。
  • merchant_ref 是否对应当前订单。
  • 查询结果中的 transaction_id 是否与请求值一致。
  • 同一笔交易重复回调时,是否会重复追加订单历史。

另外,file_get_contents() 能否访问 HTTPS URL 取决于 PHP 配置和运行环境。生产环境通常更适合使用 OpenCart 提供的请求方式或 cURL,同时明确处理超时、HTTP 状态码和 TLS 错误。

核心修正是为未批准的交易配置独立的订单状态,并将相应的状态 ID 传给 addOrderHistory()。

备注:内容仅供参考。