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_idpayx_failed_status_idpayx_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
);
然后分别测试成功和失败交易:
- 如果两种交易都有日志,但订单最终状态相同,通常是状态 ID 的配置或映射有误。
- 如果能看到失败日志,但状态更新没有成功,检查
payx_failed_status_id是否为空或等于0。 - 如果失败交易没有留下任何回调日志,检查支付平台的回调规则、回调地址和服务器访问日志。
- 如果日志中的
$order_id为空,或者对应订单不存在,检查merchant_ref的生成和传递过程。
安全与稳定性注意事项
不要只根据回调 URL 中的 transaction_id 和远程 XML 更新订单。至少还要验证:
- 回调或查询结果中的签名、密钥或其他认证信息。
- 返回的商户账号是否属于当前站点。
- 返回金额是否与订单金额一致。
- 币种是否一致。
merchant_ref是否对应当前订单。- 查询结果中的
transaction_id是否与请求值一致。 - 同一笔交易重复回调时,是否会重复追加订单历史。
另外,file_get_contents() 能否访问 HTTPS URL 取决于 PHP 配置和运行环境。生产环境通常更适合使用 OpenCart 提供的请求方式或 cURL,同时明确处理超时、HTTP 状态码和 TLS 错误。
核心修正是为未批准的交易配置独立的订单状态,并将相应的状态 ID 传给 addOrderHistory()。
备注:内容仅供参考。
