diff --git a/includes/class-yoone-moneris-api.php b/includes/class-yoone-moneris-api.php index 32ee23b..3722bab 100644 --- a/includes/class-yoone-moneris-api.php +++ b/includes/class-yoone-moneris-api.php @@ -121,22 +121,51 @@ class Yoone_Moneris_API implements Yoone_Moneris_API_Interface { if ( empty( $this->store_id ) || empty( $this->api_token ) ) { 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'; $payload = array( 'dataKey' => (string) $token, - 'orderId' => (string) $order_id, + 'orderId' => (string) $unique_order_id, 'amount' => $this->format_amount( $amount ), '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 ); if ( $res['ok'] ) { return array( 'success' => true, '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 以便排查格式问题(掩码 pan、data_key、api_token) + $xml_log = $xml; + $xml_log = preg_replace('/[^<]*<\/pan>/', '****', $xml_log); + $xml_log = preg_replace('/[^<]*<\/data_key>/', '****', $xml_log); + $xml_log = preg_replace('/[^<]*<\/api_token>/', '****', $xml_log); + error_log('【Yoone Moneris API】请求 XML(脱敏)前 400 字符:' . substr($xml_log, 0, 400)); + $args = array( 'method' => $this->http_method ?: 'POST', 'timeout' => $this->timeout, @@ -221,6 +257,8 @@ class Yoone_Moneris_API implements Yoone_Moneris_API_Interface { // 解析 XML $parsed = $this->parse_moneris_xml( $raw ); + // 打印部分响应原文,便于调试 + error_log('【Yoone Moneris API】响应原文前 300 字符:' . substr($raw, 0, 300)); if ( ! $parsed['parsed'] ) { error_log('【Yoone Moneris API】XML 解析失败:' . ($parsed['error'] ?? 'Unknown')); return array( 'ok' => false, 'error' => 'Parse error: ' . ( $parsed['error'] ?? 'Unknown' ) ); diff --git a/includes/utils/orderid.php b/includes/utils/orderid.php new file mode 100644 index 0000000..27d026c --- /dev/null +++ b/includes/utils/orderid.php @@ -0,0 +1,33 @@ +-<毫秒时间戳>-<四位随机数> + * 示例: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; +} \ No newline at end of file