PAYATHON 2026

Gateway response 在 WordPress 子目录安装中失效

支付老李

结论

这通常不是 certificate 的问题,也不是因为 WordPress 安装在子目录后无法接收 POST。

更可能的情况是,gateway 提交 callback URL 后,该 URL 又触发了 301、302 或 canonical redirect。比如从根目录跳到子目录、从 HTTP 跳到 HTTPS、自动补上尾斜杠,或者被 WordPress 重定向到规范 URL。许多客户端跟随这类跳转时,会把原来的 POST 改成 GET。等插件代码真正执行时,$_POST 已经是空的。

另一个常见原因是,gateway 的服务器通知地址和用户付款后的返回地址使用了同一个 URL。用户浏览器会通过 GET 访问 callback,插件返回的:

REDIRECT=...

就会直接显示在页面上。REDIRECT= 并不是 HTTP 重定向,而是部分 gateway 约定的响应格式。只有 gateway 的服务器收到并识别这段内容后,才会继续处理跳转。

为什么子目录安装更容易出现这个问题

假设 WordPress 的实际安装地址是:

https://example.com/wordpress/

但提交给 gateway 的 callback 是:

https://example.com/?constrivPayer-api=Gateway_ConsTriv

服务器可能会把它重定向到:

https://example.com/wordpress/?constrivPayer-api=Gateway_ConsTriv

整个请求过程可能变成:

POST /?constrivPayer-api=Gateway_ConsTriv
302 Location: /wordpress/?constrivPayer-api=Gateway_ConsTriv
GET /wordpress/?constrivPayer-api=Gateway_ConsTriv

插件最终收到的是第二个请求,因此:

$_SERVER['REQUEST_METHOD'] === 'GET'
$_POST === []

其他类似情况还有:

  • http:// 自动跳转到 https://
  • www.example.com 跳转到 example.com
  • URL 自动添加或移除尾斜杠
  • WordPress 的 redirect_canonical() 改写地址
  • 反向代理、CDN 或安全插件执行重定向
  • callback 被配置成页面 URL,而不是专用通知端点
  • gateway 的通知 URL 和用户返回 URL 配置颠倒

303 会明确要求客户端改用 GET。不少客户端处理 301 和 302 时,也会把 POST 改成 GET。callback 需要保留 POST 时,通常应彻底避免跳转,而不是依赖客户端正确处理 307 或 308。

先检查实际请求链

不要只看浏览器最终显示的 URL。gateway 的服务器通知和用户浏览器返回通常是两个相互独立的请求。

先从 Web 服务器访问日志中确认:

  • gateway 是否真的发出了 POST
  • POST 到达了哪个路径
  • 服务器是否返回 301、302、403 或 404
  • 跳转后的请求是否变成 GET
  • POST 和浏览器 GET 是否来自不同 IP、不同 User-Agent
  • gateway 收到的响应状态和响应正文是什么

也可以用 curl 模拟请求:

curl -i -X POST \
  'https://example.com/wordpress/wp-admin/admin-post.php?action=constrivpayer_callback' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'result=CAPTURED&udf1=test&udf2=test&udf4=test'

检查它是直接返回 200,还是先返回 301/302。

如需查看完整跳转链,可以使用:

curl -i -L -X POST \
  'https://example.com/wordpress/wp-admin/admin-post.php?action=constrivpayer_callback' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'result=CAPTURED&udf1=test&udf2=test&udf4=test'

不过,curl 的跳转行为未必与 gateway 完全一致。最终仍应以 gateway 日志和服务器访问日志为准。

使用 WordPress 生成 callback URL

不要手工拼接域名和子目录。可以把 WordPress 的 admin-post.php 用作独立的 callback 入口:

$callbackUrl = add_query_arg(
    'action',
    'constrivpayer_callback',
    admin_url('admin-post.php')
);

WordPress 安装在子目录时,它会生成类似下面的地址:

https://example.com/wordpress/wp-admin/admin-post.php?action=constrivpayer_callback

把这个完整地址提交给 gateway,并确保它不会再次触发协议、域名、路径或尾斜杠重定向。

用户付款后的浏览器返回地址需要单独生成:

$returnUrl = home_url('/payment-result/');

两者的用途不同:

  • callback/notify URL:gateway 服务器通过 POST 提交交易结果。
  • return URL:gateway 将用户浏览器带回网站,通常使用 GET。

不要依赖 return URL 中的数据确认付款成功。最终支付状态应以经过签名验证的服务器 callback,或主动查询 gateway API 得到的结果为准。

更稳妥的 callback 示例

add_action(
    'admin_post_nopriv_constrivpayer_callback',
    'constrivpayer_handle_callback'
);

add_action(
    'admin_post_constrivpayer_callback',
    'constrivpayer_handle_callback'
);

function constrivpayer_handle_callback()
{
    global $constrivPayer_options;

    if (
        !isset($_SERVER['REQUEST_METHOD']) ||
        $_SERVER['REQUEST_METHOD'] !== 'POST'
    ) {
        status_header(405);
        header('Allow: POST');
        exit('METHOD_NOT_ALLOWED');
    }

    $result = isset($_POST['result'])
        ? sanitize_text_field(wp_unslash($_POST['result']))
        : '';

    $udf1 = isset($_POST['udf1'])
        ? sanitize_text_field(wp_unslash($_POST['udf1']))
        : '';

    $udf2 = isset($_POST['udf2'])
        ? sanitize_text_field(wp_unslash($_POST['udf2']))
        : '';

    $udf4 = isset($_POST['udf4'])
        ? wp_unslash($_POST['udf4'])
        : '';

    if ($result === 'CAPTURED' || $result === 'APPROVED') {
        $expectedSignature = md5(
            $udf2 . CONSTRIVPAYER_MD5 . get_option('siteurl')
        );

        if (
            $udf1 !== '' &&
            hash_equals($expectedSignature, $udf1)
        ) {
            $decoded = base64_decode($udf4, true);
            $paymentData = $decoded !== false
                ? json_decode($decoded, true)
                : null;

            if (
                is_array($paymentData) &&
                isset($paymentData['amount'], $paymentData['payment_id'])
            ) {
                $message = '1&amount=' .
                    rawurlencode((string) $paymentData['amount']) .
                    '&id=' .
                    rawurlencode((string) $paymentData['payment_id']);
            } else {
                $message = '4';
            }
        } else {
            $message = '2';
        }
    } elseif (isset($_POST['Error'])) {
        $message = '3';
    } else {
        $message = '4';
    }

    $pageUrl = $constrivPayer_options['page_url'];

    $redirect = 'REDIRECT=' .
        $pageUrl .
        '?transaction_result&transaction_message=' .
        $message;

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

    echo $redirect;
    exit;
}

这段代码会先检查请求方法以及字段是否存在,避免原代码在 POST 为空时直接访问:

$_POST["result"]

否则,PHP 会产生 undefined array key/undefined index 警告。如果错误信息被写入响应正文,还可能破坏 gateway 要求的精确响应格式。

检查签名中的 siteurl

原代码把下面的值加入了签名:

get_option('siteurl')

在子目录安装中,siteurl 很可能会从:

https://example.com

变成:

https://example.com/wordpress

这不会让 $_POST 变空,但可能导致签名校验失败。

需要确认生成付款请求时参与签名的 URL,与 callback 校验时使用的字符串完全一致,包括:

  • HTTP 或 HTTPS
  • 域名是否包含 www
  • 子目录
  • 端口
  • 尾斜杠
  • 大小写和编码方式

如果 gateway 协议没有要求把 WordPress 的 siteurl 加入签名,最好不要自行添加。如果协议确实有此要求,创建付款请求和验证 callback 时应使用同一个已经保存并规范化的值,不要在两个阶段分别临时推导。

还要注意下面两个值的区别:

get_option('siteurl')
get_option('home')

siteurl 表示 WordPress 核心文件所在的位置,home 表示网站对外访问地址。WordPress 位于子目录、首页位于根目录时,两者可以不同。

检查 POST 的 Content-Type

只有请求使用常见的表单编码时,PHP 才会自动填充 $_POST,例如:

application/x-www-form-urlencoded
multipart/form-data

如果 gateway 发送的是 JSON:

application/json

那么 $_POST 为空是正常的。此时需要读取原始请求体:

$rawBody = file_get_contents('php://input');
$payload = json_decode($rawBody, true);

不过,如果同一个 gateway 在 WordPress 安装于根目录时可以正常填充 $_POST,Content-Type 通常不是最先需要怀疑的地方。除非子目录地址使用了不同的代理、WAF 或 callback 配置。

REDIRECT= 与 HTTP 重定向不是一回事

如果 gateway 文档明确要求 callback 返回:

REDIRECT=https://example.com/payment-result/

那就应继续返回纯文本,并确保响应正文中没有其他内容。

如果要让 PHP 重定向当前浏览器,则需要使用真正的 HTTP 重定向:

wp_safe_redirect($pageUrl);
exit;

但在没有确认 gateway 协议前,不能直接替换。服务器之间的 callback 即使收到 Location,执行跳转的也只是 gateway 的 HTTP 客户端,未必会影响用户浏览器。

建议的排查顺序

  1. 分开设置 gateway callback URL 和用户 return URL。
  2. 使用 admin_url()、home_url() 等函数生成完整地址,不要手工拼接子目录。
  3. 确认 callback URL 从外部访问时不会触发任何 301/302/303。
  4. 检查访问日志,区分 gateway POST 和用户浏览器 GET。
  5. 查看 gateway 后台记录的通知状态、HTTP 状态码和响应正文。
  6. 检查请求的 Content-Type,确认数据是否应该出现在 $_POST 中。
  7. 检查安全插件、CDN、WAF、Basic Auth 和防火墙是否拦截服务器通知。
  8. 确认签名计算中的 siteurl 与发起交易时使用的值完全一致。
  9. 确保 callback 只输出协议要求的纯文本,不要输出 PHP warning、HTML、BOM 或调试信息。

只有日志表明 gateway 根本无法建立 HTTPS 连接,或者明确报告 TLS 握手、证书链、主机名验证错误时,才应优先检查 certificate。对于“POST 变成 GET、REDIRECT 被打印”这一现象,URL 重定向或 callback/return URL 混用更符合实际表现。

备注:内容仅供参考。