PAYATHON 2026

WHMCS Callback File 中 json_decode() 返回 null

支付老李

结论

问题通常不在 json_decode(),而是 WHMCS 加载 init.php 时可能会过滤或转义请求变量,改变 $_GET['resp'] 中的双引号等字符,使其不再是合法的 JSON。

比较稳妥的做法是:先保存原始参数,再加载 init.php,最后解码 JSON。 PHP 已经自动对查询参数进行了 URL 解码,通常不需要再次调用 urldecode()。

推荐写法

<?php

// 必须在加载 WHMCS init.php 之前读取。
// 此时 $_GET['resp'] 已由 PHP 完成 URL 解码。
$rawResponse = isset($_GET['resp']) && is_string($_GET['resp'])
    ? $_GET['resp']
    : '';

require_once __DIR__ . '/../../../init.php';

if ($rawResponse === '') {
    exit('Missing payment gateway response');
}

$response = json_decode($rawResponse);

if (json_last_error() !== JSON_ERROR_NONE) {
    $error = function_exists('json_last_error_msg')
        ? json_last_error_msg()
        : 'JSON error code: ' . json_last_error();

    // 实际项目中应写入网关日志,不建议直接向请求方输出完整响应。
    logTransaction(
        'gateway',
        [
            'response' => $rawResponse,
            'json_error' => $error,
        ],
        'Invalid JSON response'
    );

    exit('Invalid payment gateway response');
}

if (
    !isset($response->tx) ||
    !is_object($response->tx) ||
    !isset($response->tx->amount)
) {
    exit('Payment response is missing transaction data');
}

$amount = $response->tx->amount;

如果需要关联数组,可以将 json_decode() 的第二个参数设为 true:

$response = json_decode($rawResponse, true);

if (json_last_error() !== JSON_ERROR_NONE) {
    exit('Invalid JSON response');
}

$amount = $response['tx']['amount'] ?? null;

为什么不应再次直接调用 urldecode()

访问 PHP 脚本时,查询字符串中的:

%7B%22name%22...

PHP 已经会将其解析为类似下面的内容:

{"name":"vbvcomplete", ...}

因此,正常情况下:

$_GET['resp']

已经是 URL 解码后的字符串。如果再次执行:

urldecode($_GET['resp']);

可能会破坏 JSON 中原本合法的 %xx、+ 或嵌套 URL。应先检查实际字符串,再判断是否需要额外处理。

如果只能在加载 init.php 后读取

在某些 WHMCS 环境中,引号可能已经被转换成 HTML 实体,例如:

{&quot;name&quot;:&quot;vbvcomplete&quot;}

确认收到的内容确实是这种形式后,可以先还原 HTML 实体:

require_once __DIR__ . '/../../../init.php';

$rawResponse = isset($_GET['resp']) && is_string($_GET['resp'])
    ? $_GET['resp']
    : '';

$json = html_entity_decode($rawResponse, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$response = json_decode($json);

if (json_last_error() !== JSON_ERROR_NONE) {
    exit('JSON decode failed: ' . json_last_error_msg());
}

不要在未检查输入的情况下同时使用 urldecode()、stripslashes() 和 html_entity_decode()。这些函数处理的问题不同,重复转换可能会损坏有效数据。可以先记录转换前的值和 json_last_error_msg(),查清数据究竟发生了什么变化。

更可靠地定位错误

不要只检查返回值是否为 null,因为 JSON 字面量 null 也会被合法解码成 PHP 的 null。应该检查 JSON 错误状态:

$response = json_decode($rawResponse);

if (json_last_error() !== JSON_ERROR_NONE) {
    $message = json_last_error_msg();
}

还可以临时记录字符串的可见形式:

logTransaction(
    'gateway',
    [
        'response' => $rawResponse,
        'response_debug' => var_export($rawResponse, true),
        'json_error' => json_last_error_msg(),
    ],
    'Callback debug'
);

生产环境的日志应避免记录银行卡信息、令牌、完整邮箱或其他敏感字段。

支付回调还需要注意

JSON 解码成功并不表示付款信息可信。处理回调时,至少还要完成以下检查:

  • 验证支付网关提供的签名、哈希或 Webhook 密钥。
  • 必要时通过网关的服务端 API 再次查询交易状态。
  • 校验 invoiceid、交易编号、金额和币种。
  • 不要仅凭客户端重定向参数中的 status 判断付款成功。
  • 使用 WHMCS 的 checkCbInvoiceID()、checkCbTransID() 和 addInvoicePayment() 等回调函数。
  • 对同一交易编号进行幂等处理,避免因刷新回调地址而重复入账。

处理这个问题的重点仍然是:在 init.php 改写请求数据前保存 $_GET['resp'],再对保存的字符串调用 json_decode()。只有确认内容已被转换成 HTML 实体时,才使用 html_entity_decode() 还原。

备注:内容仅供参考。