PAYATHON 2026

WHMCS 回调文件中的 Third-party payment Processor API 调用失败

支付老李

结论

WHMCS 通常不会主动禁止 simplexml_load_string(),也不会因为文件位于 WHMCS 根目录就把合法 XML 变成不可解析的内容。

simplexml_load_string() 返回 false,说明传入的数据不是当前环境能够解析的有效 XML。最常见的原因包括:

  • 实际传入了错误变量。示例中读取响应使用 $xmlString,描述中却调用了 simplexml_load_string($jString)。
  • API 返回的不是 XML,而是 HTML 错误页、网关拦截页、空字符串或重定向响应。
  • WHMCS 目录存在单独的 .htaccess、.user.ini 或 PHP-FPM 配置,导致 PHP 版本、扩展、TLS/CA 配置或 allow_url_fopen 设置不同。
  • 回调请求中的 amount、tranxid、hash 与实际交易不完全一致,网关因而返回错误内容。
  • XML 本身格式不合法,例如编码声明错误、包含未转义的 &、控制字符或截断内容。

因此,应先检查 API 的原始响应和 XML 解析错误,而不是直接把问题归因于 WHMCS。

先修正示例中的明显问题

确保读取和解析的是同一个变量:

$xmlString = file_get_contents($url);

if ($xmlString === false) {
    echo 'Response Description: GTPAY Verification service failed to open.';
    exit;
}

$xml = simplexml_load_string($xmlString);

如果实际代码使用的是:

$xml = simplexml_load_string($jString);

而 $jString 没有被正确赋值,那么解析自然会失败。

原示例中的错误提示字符串还缺少结束引号。如果这不是粘贴时遗漏,会直接造成 PHP 语法错误。

输出真实响应和 XML 错误

不要只判断 simplexml_load_string() 是否返回 false。应记录响应长度、响应开头以及 libxml 给出的具体错误:

libxml_use_internal_errors(true);

$xmlString = file_get_contents($url);

if ($xmlString === false) {
    $lastError = error_get_last();

    error_log(
        'GTPay request failed: ' .
        ($lastError['message'] ?? 'unknown error')
    );

    exit;
}

error_log('GTPay response length: ' . strlen($xmlString));
error_log('GTPay response preview: ' . substr($xmlString, 0, 500));

$xml = simplexml_load_string($xmlString);

if ($xml === false) {
    foreach (libxml_get_errors() as $error) {
        error_log(sprintf(
            'XML error: %s at line %d, column %d',
            trim($error->message),
            $error->line,
            $error->column
        ));
    }

    libxml_clear_errors();
    exit;
}

如果响应开头是以下内容之一,就不是 SimpleXML 本身的问题:

<!DOCTYPE html>
<html>
Access Denied
Forbidden
Internal Server Error

这通常表示请求被支付网关、WAF、代理服务器或远程主机的 IP 白名单策略拦截。

调试时不要把完整响应、商户密钥或交易信息直接输出给浏览器。WHMCS 回调地址通常可以被公网访问,敏感内容应写入受保护的日志,并在排查完成后删除。

建议改用 cURL 检查 HTTP 状态

file_get_contents() 不便于查看 HTTP 状态码、重定向和 TLS 错误。支付回调验证更适合使用 cURL:

<?php

$mertid = 'secretmerchantID';
$amt = '10000';
$tranxid = 'gtPay130958397220820';
$hashkey = 'secretclientkey';

$hash = hash('sha512', $mertid . $tranxid . $hashkey);

$query = http_build_query(
    [
        'mertid' => $mertid,
        'amount' => $amt,
        'tranxid' => $tranxid,
        'hash' => $hash,
    ],
    '',
    '&',
    PHP_QUERY_RFC3986
);

$url = 'https://ibank.gtbank.com/GTPayService/gettransactionstatus.xml?' . $query;

$ch = curl_init($url);

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => false,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => [
        'Accept: application/xml, text/xml',
    ],
]);

$xmlString = curl_exec($ch);
$curlError = curl_error($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);

curl_close($ch);

if ($xmlString === false) {
    error_log('GTPay cURL error: ' . $curlError);
    exit;
}

if ($httpStatus < 200 || $httpStatus >= 300) {
    error_log('GTPay HTTP status: ' . $httpStatus);
    error_log('GTPay response preview: ' . substr($xmlString, 0, 500));
    exit;
}

libxml_use_internal_errors(true);

$xml = simplexml_load_string($xmlString);

if ($xml === false) {
    error_log('GTPay content type: ' . ($contentType ?? 'unknown'));
    error_log('GTPay response preview: ' . substr($xmlString, 0, 500));

    foreach (libxml_get_errors() as $error) {
        error_log(
            'GTPay XML parse error: ' . trim($error->message)
        );
    }

    libxml_clear_errors();
    exit;
}

// 继续验证响应中的交易编号、金额、币种和支付状态。

不要通过设置以下选项绕过证书验证:

CURLOPT_SSL_VERIFYPEER => false

如果出现证书错误,应为服务器安装或配置正确的 CA 证书包。关闭验证会使支付状态查询面临中间人攻击风险。

比较 WHMCS 目录和其他目录的 PHP 环境

“同一台服务器、不同目录结果不同”通常意味着两个目录没有使用完全相同的 PHP 配置。需要比较:

  • PHP_VERSION
  • PHP_SAPI
  • allow_url_fopen
  • openssl 扩展
  • curl 扩展
  • SimpleXML 扩展
  • libxml 版本
  • openssl.cafile
  • curl.cainfo
  • disable_functions
  • user_ini.filename
  • 实际加载的 php.ini
  • WHMCS 目录中的 .htaccess 和 .user.ini

可以暂时创建一个仅限管理员访问的诊断页面:

<?php

header('Content-Type: text/plain; charset=UTF-8');

echo 'PHP version: ' . PHP_VERSION . PHP_EOL;
echo 'PHP SAPI: ' . PHP_SAPI . PHP_EOL;
echo 'Loaded php.ini: ' . (php_ini_loaded_file() ?: 'none') . PHP_EOL;
echo 'allow_url_fopen: ' . ini_get('allow_url_fopen') . PHP_EOL;
echo 'openssl.cafile: ' . ini_get('openssl.cafile') . PHP_EOL;
echo 'curl.cainfo: ' . ini_get('curl.cainfo') . PHP_EOL;
echo 'SimpleXML: ' . (extension_loaded('SimpleXML') ? 'yes' : 'no') . PHP_EOL;
echo 'cURL: ' . (extension_loaded('curl') ? 'yes' : 'no') . PHP_EOL;
echo 'OpenSSL: ' . (extension_loaded('openssl') ? 'yes' : 'no') . PHP_EOL;

分别在正常目录和 WHMCS 目录执行并比较结果。诊断完成后应立即删除该页面,因为 PHP 配置信息可能暴露服务器路径和模块信息。

如果 file_get_contents($url) 已经取得非空响应,那么 allow_url_fopen 通常不是当前解析失败的直接原因。此时重点应放在 HTTP 状态、响应正文和 libxml 错误上。

检查回调上下文中的参数

浏览器手工测试成功,并不表示支付网关实际发起回调时使用了相同数据。回调处理程序应从收到的请求中提取交易号和金额,并严格按照网关规范生成签名。

需要确认:

  • amount 的单位是主货币单位还是最小货币单位。
  • 金额是否包含小数点或前导零。
  • tranxid 是否与网关记录完全一致。
  • 签名字段的拼接顺序和大小写是否正确。
  • 是否误用了测试环境的密钥访问生产接口,或反之。
  • 回调服务器公网出口 IP 是否在网关白名单中。
  • 网关是否要求特定 TLS 版本、请求头或客户端证书。

不要只凭回调携带的“支付成功”字段更新 WHMCS 发票。成功查询 API 后,还应核对交易编号、订单或发票编号、金额、币种、商户号和最终支付状态,然后再调用 WHMCS 的网关回调处理函数。

最可能的排查顺序

  1. 统一 $xmlString 和 $jString,排除变量名错误。
  2. 记录 HTTP 状态码、Content-Type、响应长度和响应开头。
  3. 使用 libxml_get_errors() 获取真实解析错误。
  4. 比较 WHMCS 目录与正常目录加载的 PHP 配置。
  5. 检查 .htaccess、.user.ini、WAF 和出口 IP 白名单。
  6. 核对实际回调中的交易号、金额及签名生成规则。
  7. 确认响应确实是完整 XML 后,再处理 WHMCS 发票状态。

只有在原始响应已经确认是完整、合法的 XML,而 simplexml_load_string() 仍失败时,才应进一步调查 PHP、SimpleXML 或 libxml 的版本与编码差异。

备注:内容仅供参考。