feat(moneris): 添加唯一订单ID生成和自动重试逻辑

实现唯一订单ID生成函数以避免Moneris主机拒绝重复订单
在API请求失败时自动重试一次并生成新订单ID
增加请求/响应日志记录和敏感信息脱敏处理
This commit is contained in:
tikkhun 2025-11-05 15:53:20 +08:00
parent a9acdacf80
commit 73dc9ca3c3
2 changed files with 74 additions and 3 deletions

View File

@ -121,22 +121,51 @@ class Yoone_Moneris_API implements Yoone_Moneris_API_Interface {
if ( empty( $this->store_id ) || empty( $this->api_token ) ) { if ( empty( $this->store_id ) || empty( $this->api_token ) ) {
return array( 'success' => false, 'error' => 'Missing Moneris credentials' ); return array( 'success' => false, 'error' => 'Missing Moneris credentials' );
} }
// 为避免重复 orderId 导致主机拒绝,生成唯一订单 ID
if ( ! function_exists( 'yoone_moneris_unique_order_id' ) ) {
require_once __DIR__ . '/utils/orderid.php';
}
$unique_order_id = yoone_moneris_unique_order_id( $order_id );
$type = $capture ? 'res_purchase_cc' : 'res_preauth_cc'; $type = $capture ? 'res_purchase_cc' : 'res_preauth_cc';
$payload = array( $payload = array(
'dataKey' => (string) $token, 'dataKey' => (string) $token,
'orderId' => (string) $order_id, 'orderId' => (string) $unique_order_id,
'amount' => $this->format_amount( $amount ), 'amount' => $this->format_amount( $amount ),
'cryptType' => $this->crypt_type, 'cryptType' => $this->crypt_type,
); );
error_log('【Yoone Moneris API】charge_token 请求type=' . $type . ' token_len=' . strlen($token) . ' amount=' . $payload['amount'] . ' currency=' . $currency . ' orderId=' . $order_id); error_log('【Yoone Moneris API】charge_token 请求type=' . $type . ' token_len=' . strlen($token) . ' amount=' . $payload['amount'] . ' currency=' . $currency . ' orderId=' . $unique_order_id . ' (源Woo订单ID=' . $order_id . ')');
$res = $this->send_moneris_xml( $type, $payload ); $res = $this->send_moneris_xml( $type, $payload );
if ( $res['ok'] ) { if ( $res['ok'] ) {
return array( return array(
'success' => true, 'success' => true,
'transaction_id' => (string) ( $res['receipt']['txnNumber'] ?? '' ), 'transaction_id' => (string) ( $res['receipt']['txnNumber'] ?? '' ),
'order_id_used' => $unique_order_id,
'retry' => false,
); );
} }
return array( 'success' => false, 'error' => (string) $res['error'] ); // 如果出现系统错误,尝试一次自动重试(更换新的唯一 orderId
$err_msg = isset($res['error']) ? (string)$res['error'] : '';
$is_system_error = stripos($err_msg, 'SYSTEM ERROR') !== false;
if ( $is_system_error ) {
$retry_order_id = yoone_moneris_unique_order_id( $order_id );
$payload['orderId'] = (string) $retry_order_id;
error_log('【Yoone Moneris API】检测到 SYSTEM ERROR准备重试一次。新的 orderId=' . $retry_order_id);
// 短暂延时,避免立即重复错误
usleep(300 * 1000);
$res_retry = $this->send_moneris_xml( $type, $payload );
if ( $res_retry['ok'] ) {
return array(
'success' => true,
'transaction_id' => (string) ( $res_retry['receipt']['txnNumber'] ?? '' ),
'order_id_used' => $retry_order_id,
'retry' => true,
);
}
// 记录重试失败
error_log('【Yoone Moneris API】重试仍失败' . (string) ($res_retry['error'] ?? 'Unknown'));
return array( 'success' => false, 'error' => (string) ($res_retry['error'] ?? $err_msg) );
}
return array( 'success' => false, 'error' => (string) $err_msg );
} }
/** /**
@ -195,6 +224,13 @@ class Yoone_Moneris_API implements Yoone_Moneris_API_Interface {
$xml = $this->array_to_moneris_xml( array( 'request' => $body ) ); $xml = $this->array_to_moneris_xml( array( 'request' => $body ) );
// 日志:打印部分已脱敏的 XML 以便排查格式问题(掩码 pan、data_key、api_token
$xml_log = $xml;
$xml_log = preg_replace('/<pan>[^<]*<\/pan>/', '<pan>****</pan>', $xml_log);
$xml_log = preg_replace('/<data_key>[^<]*<\/data_key>/', '<data_key>****</data_key>', $xml_log);
$xml_log = preg_replace('/<api_token>[^<]*<\/api_token>/', '<api_token>****</api_token>', $xml_log);
error_log('【Yoone Moneris API】请求 XML脱敏前 400 字符:' . substr($xml_log, 0, 400));
$args = array( $args = array(
'method' => $this->http_method ?: 'POST', 'method' => $this->http_method ?: 'POST',
'timeout' => $this->timeout, 'timeout' => $this->timeout,
@ -221,6 +257,8 @@ class Yoone_Moneris_API implements Yoone_Moneris_API_Interface {
// 解析 XML // 解析 XML
$parsed = $this->parse_moneris_xml( $raw ); $parsed = $this->parse_moneris_xml( $raw );
// 打印部分响应原文,便于调试
error_log('【Yoone Moneris API】响应原文前 300 字符:' . substr($raw, 0, 300));
if ( ! $parsed['parsed'] ) { if ( ! $parsed['parsed'] ) {
error_log('【Yoone Moneris API】XML 解析失败:' . ($parsed['error'] ?? 'Unknown')); error_log('【Yoone Moneris API】XML 解析失败:' . ($parsed['error'] ?? 'Unknown'));
return array( 'ok' => false, 'error' => 'Parse error: ' . ( $parsed['error'] ?? 'Unknown' ) ); return array( 'ok' => false, 'error' => 'Parse error: ' . ( $parsed['error'] ?? 'Unknown' ) );

View File

@ -0,0 +1,33 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 生成用于 Moneris 的唯一订单 ID避免重复 orderId 导致主机拒绝)。
*
* 规则:<原始订单ID>-<毫秒时间戳>-<四位随机数>
* 示例1234-1730835123456-4821
*
* 注意Moneris orderId 的长度是有限制的(通常不超过 50 字符)。本实现控制在合理长度范围内。
* 如需自定义格式,可在此函数中调整。
*
* @param int|string $order_id WooCommerce 订单 ID
* @return string 唯一订单 ID 字符串
*/
function yoone_moneris_unique_order_id( $order_id ) {
// 毫秒级时间戳
$ms = (int) floor( microtime( true ) * 1000 );
// 四位随机数
$rand = mt_rand( 1000, 9999 );
// 仅保留数字和字母,避免特殊字符
$base = preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $order_id );
// 拼接唯一后缀
$unique = $base . '-' . $ms . '-' . $rand;
// 限长保护(最多 50 字符)
if ( strlen( $unique ) > 50 ) {
// 若过长,截断前半部分(保留后缀)
$unique = substr( $base, 0, max( 1, 50 - ( 1 + strlen( (string) $ms ) + 1 + 4 ) ) ) . '-' . $ms . '-' . $rand;
}
return $unique;
}