PAYATHON 2026

如何在 WordPress 中创建支付网关 return URL?

支付老李

结论

可以实现,但不建议在 init 中直接比较 $_SERVER['REQUEST_URI']。更合适的做法是在插件中注册 WordPress 重写规则,将这些虚拟 URL 映射到自定义 query var,再到 template_redirect 阶段处理请求并立即 exit。

这种方式不需要创建真实的 PHP 文件,也不会进入主题模板或最终返回 404。例如:

  • /get-return.php → 支付完成后的用户返回页
  • /get-error.php → 支付失败后的用户返回页
  • /get-receipt.php → 网关发送支付结果的回调接口

插件实现示例

创建一个插件,加入以下代码:

<?php
/**
 * Plugin Name: Payment Gateway Endpoints
 */

defined( 'ABSPATH' ) || exit;

/**
 * 注册虚拟 URL。
 */
function my_gateway_add_rewrite_rules() {
    add_rewrite_rule(
        '^get-return\.php/?$',
        'index.php?gateway_endpoint=return',
        'top'
    );

    add_rewrite_rule(
        '^get-error\.php/?$',
        'index.php?gateway_endpoint=error',
        'top'
    );

    add_rewrite_rule(
        '^get-receipt\.php/?$',
        'index.php?gateway_endpoint=receipt',
        'top'
    );
}
add_action( 'init', 'my_gateway_add_rewrite_rules' );

/**
 * 声明允许使用的 query var。
 */
function my_gateway_register_query_vars( $vars ) {
    $vars[] = 'gateway_endpoint';

    return $vars;
}
add_filter( 'query_vars', 'my_gateway_register_query_vars' );

/**
 * 处理虚拟端点。
 */
function my_gateway_handle_endpoint() {
    $endpoint = get_query_var( 'gateway_endpoint' );

    if ( ! $endpoint ) {
        return;
    }

    switch ( $endpoint ) {
        case 'return':
            my_gateway_handle_return();
            break;

        case 'error':
            my_gateway_handle_error();
            break;

        case 'receipt':
            my_gateway_handle_receipt();
            break;

        default:
            status_header( 404 );
            exit;
    }
}
add_action( 'template_redirect', 'my_gateway_handle_endpoint' );

接着实现各个端点的业务逻辑:

/**
 * 用户支付成功后由浏览器返回。
 */
function my_gateway_handle_return() {
    status_header( 200 );
    nocache_headers();

    // 根据订单号查询支付状态。
    // 不要仅凭 URL 参数将订单标记为已支付。

    wp_safe_redirect( home_url( '/payment-success/' ) );
    exit;
}

/**
 * 用户支付失败或取消后返回。
 */
function my_gateway_handle_error() {
    status_header( 200 );
    nocache_headers();

    wp_safe_redirect( home_url( '/payment-failed/' ) );
    exit;
}

/**
 * 接收支付网关的服务器回调。
 */
function my_gateway_handle_receipt() {
    nocache_headers();

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

    $raw_body = file_get_contents( 'php://input' );

    // 以下函数需要根据支付网关的签名规范自行实现。
    if ( ! my_gateway_verify_signature( $raw_body, $_SERVER ) ) {
        status_header( 401 );
        exit;
    }

    // 按网关实际格式解析数据,例如 JSON。
    $payload = json_decode( $raw_body, true );

    if ( ! is_array( $payload ) ) {
        status_header( 400 );
        exit;
    }

    // 校验订单号、支付金额、币种、商户号和交易状态。
    // 更新 orders 表时必须保证重复回调不会重复记账。
    my_gateway_process_payment_notification( $payload );

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

    // 返回内容必须符合支付网关规定,例如 OK、SUCCESS 或特定 JSON。
    echo 'OK';
    exit;
}

my_gateway_verify_signature() 和 my_gateway_process_payment_notification() 只是示意函数,具体参数和实现必须遵循实际支付网关的协议,不能直接照搬。

刷新重写规则

添加重写规则后,需要刷新 WordPress 的 rewrite rules。建议在插件启用和停用时执行:

function my_gateway_activate_plugin() {
    my_gateway_add_rewrite_rules();
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'my_gateway_activate_plugin' );

function my_gateway_deactivate_plugin() {
    flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'my_gateway_deactivate_plugin' );

不要在每次 init 时执行 flush_rewrite_rules(),这个操作开销较大。

如果插件已经启用,也可以进入 WordPress 后台的“设置 → 固定链接”,保存一次固定链接设置。

为什么不直接在 init 中判断 URL

从技术上说,也可以这样处理:

add_action( 'init', function () {
    $path = wp_parse_url( $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH );

    if ( '/get-return.php' === $path ) {
        // 处理请求。
        exit;
    }
} );

不过,这种写法可能受到以下情况影响:

  • WordPress 安装在子目录中;
  • URL 带有尾部斜杠或存在编码差异;
  • 站点使用了反向代理;
  • 多站点配置改变了请求路径;
  • 代码绕过了 WordPress 正常的请求解析流程;
  • 其他插件需要识别或过滤该请求。

使用重写规则后,WordPress 可以正常识别这些地址。template_redirect 又在主题输出之前执行,适合返回响应、执行跳转并阻止模板继续加载。

Web 服务器必须把请求交给 WordPress

这个方案有一个前提:Web 服务器必须把不存在的 /get-return.php 转发给 WordPress 的 index.php。

常见的 WordPress Apache 重写配置通常会转发不存在的文件,所以虚拟 .php 地址一般可以正常工作。如果服务器、CDN 或安全规则会直接拦截所有 .php 路径,可以改用不带扩展名的地址,例如:

https://www.mysite.com/payment/return
https://www.mysite.com/payment/error
https://www.mysite.com/payment/receipt

对应的规则可以改为:

add_rewrite_rule(
    '^payment/(return|error|receipt)/?$',
    'index.php?gateway_endpoint=$matches[1]',
    'top'
);

这类 URL 更符合 WordPress 的路由方式,通常也较少受到服务器安全策略的影响。

支付回调的注意事项

不能只根据用户浏览器访问的 return URL 判断支付成功。用户可能关闭页面、伪造参数,也可能在付款后没有成功跳转。订单状态应以网关的服务器回调或主动查询结果为准。

处理回调时至少需要做到:

  • 使用 HTTPS;
  • 按照网关规范验证签名,不能只检查订单号或 status code;
  • 核对商户号、订单号、金额和币种;
  • 使用网关交易号,避免同一笔交易重复入账;
  • 将处理逻辑设计为幂等操作,因为网关通常会重试回调;
  • 完成可靠的数据库更新后,再返回网关要求的成功响应;
  • 不要强制外部支付回调使用 WordPress nonce,外部网关通常无法生成 nonce;
  • 验证失败时不要返回成功,否则网关可能停止重试;
  • 使用 $wpdb->prepare() 处理数据库参数,并在更新前确认订单当前状态。

如果支付网关允许配置任意路径,也可以考虑通过 WordPress REST API 创建 /wp-json/... 回调端点。如果网关明确要求使用 get-return.php 这类固定路径,自定义 rewrite rule 更合适。

备注:内容仅供参考。