PAYATHON 2026

表单提交后如何将数据传递给 CGI script

支付小周

明确结论

可以。应由 WordPress 服务器向 CCBill 的 CGI 接口发送 HTTP 请求,不要让浏览器通过 window.open() 直接访问接口。

请求链路应改为:

用户提交捐款表单
    → WordPress 在服务器端验证请求
    → WordPress 服务器调用 billingApi.cgi
    → WordPress 根据接口响应显示结果

这种方式不会触发浏览器的弹窗拦截,也不需要让用户离开网站。CCBill 通常会将请求识别为来自网站服务器的公网出口 IP,因此可以把这个 IP 加入白名单。

API 的 username、password、subscriptionId 等敏感信息不能出现在 JavaScript 或页面 URL 中。原来的做法会将这些信息暴露给访问者,还可能被浏览器历史记录、代理服务器和访问日志保存。

为什么弹窗方式会使用用户的 IP

window.open(url) 在用户的浏览器中执行。即使 URL 是由 WordPress 页面生成的,真正连接 bill.ccbill.com 的仍然是用户的浏览器:

用户浏览器 → CCBill

所以,支付处理方看到的是用户的公网 IP,而不是 WordPress 服务器的 IP。

需要将其改成服务器间通信:

用户浏览器 → WordPress → CCBill

WordPress 收到表单后,通过 PHP 中的 WordPress HTTP API 发起请求。此时,CCBill 通常看到的是服务器或其网络出口的公网 IP。

如果网站位于代理、NAT、云平台或动态出口网络之后,实际出口 IP 可能不同于服务器网卡上的 IP。需要向主机商确认应该将哪个公网 IP 加入白名单。

推荐的实现方式

可以将表单提交到 WordPress 自带的 admin-post.php,再由处理函数调用 CCBill。这样无需单独创建 PHP 接口,还能直接使用 WordPress 的登录状态、nonce 和重定向功能。

1. 输出捐款表单

function donation_shortcode()
{
    if (!is_user_logged_in()) {
        return '<p>Please log in before making a donation.</p>';
    }

    ob_start();
    ?>
    <form method="post"
          action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
        <input type="hidden" name="action" value="process_ccbill_donation">

        <?php wp_nonce_field(
            'process_ccbill_donation',
            'ccbill_donation_nonce'
        ); ?>

        <label for="donation_amount">Amount ($)</label>
        <input
            type="number"
            id="donation_amount"
            name="donation_amount"
            min="1.00"
            step="0.01"
            required
        >

        <button type="submit">Make Donation</button>
    </form>
    <?php

    return ob_get_clean();
}

add_shortcode('ccbill_donation', 'donation_shortcode');

这里的 action 字段不是远程接口的操作名称,而是用于告诉 WordPress 应该调用哪个表单处理函数。

2. 在服务器端处理提交

add_action(
    'admin_post_process_ccbill_donation',
    'process_ccbill_donation'
);

function process_ccbill_donation()
{
    if (!is_user_logged_in()) {
        wp_die('You must be logged in.', '', ['response' => 403]);
    }

    check_admin_referer(
        'process_ccbill_donation',
        'ccbill_donation_nonce'
    );

    $user_id = get_current_user_id();

    $subscription_id = get_user_meta(
        $user_id,
        'get_donate_details_id',
        true
    );

    if (empty($subscription_id)) {
        wp_die('No previous transaction was found.');
    }

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

    if (!is_numeric($raw_amount)) {
        wp_die('Invalid donation amount.');
    }

    $amount = round((float) $raw_amount, 2);

    if ($amount <= 0) {
        wp_die('The donation amount must be greater than zero.');
    }

    /*
     * 示例上限。实际值应按业务规则和支付接口限制设置。
     */
    if ($amount > 10000) {
        wp_die('The donation amount is too large.');
    }

    $request_data = [
        'clientAccnum'         => '900000',
        'username'             => CCBILL_API_USERNAME,
        'password'             => CCBILL_API_PASSWORD,
        'action'               => 'chargeByPreviousTransactionId',
        'newClientAccnum'      => '900000',
        'newClientSubacc'      => '0000',
        'sharedAuthentication' => '1',
        'initialPrice'         => number_format($amount, 2, '.', ''),
        'initialPeriod'        => '365',
        'recurringPrice'       => '0',
        'recurringPeriod'      => '0',
        'rebills'              => '0',
        'subscriptionId'       => $subscription_id,
        'currencyCode'         => '840',
    ];

    $response = wp_remote_post(
        'https://bill.ccbill.com/jpost/billingApi.cgi',
        [
            'timeout' => 30,
            'body'    => $request_data,
        ]
    );

    if (is_wp_error($response)) {
        error_log(
            'CCBill request failed: ' .
            $response->get_error_message()
        );

        wp_die(
            'The payment service could not be reached. Please try again later.'
        );
    }

    $status_code = wp_remote_retrieve_response_code($response);
    $body        = wp_remote_retrieve_body($response);

    /*
     * 必须按照 CCBill 对该接口返回格式的定义判断是否成功。
     * HTTP 200 只说明收到了 HTTP 响应,不一定代表扣款成功。
     */
    $payment_succeeded = ccbill_response_indicates_success(
        $status_code,
        $body
    );

    if (!$payment_succeeded) {
        error_log(
            'CCBill rejected donation. HTTP status: ' .
            (int) $status_code
        );

        wp_die(
            'The donation could not be processed. No success was confirmed.'
        );
    }

    $redirect_url = add_query_arg(
        'donation',
        'success',
        home_url('/donation/')
    );

    wp_safe_redirect($redirect_url);
    exit;
}

ccbill_response_indicates_success() 应根据 CCBill 当前接口文档规定的响应格式来实现。接口可能返回键值对、特定文本或其他结构,不能只凭 HTTP 状态码 200 判断扣款成功。

可以先保守地定义这个函数,但之后必须加入真实的成功判断条件:

function ccbill_response_indicates_success($status_code, $body)
{
    if ((int) $status_code !== 200) {
        return false;
    }

    /*
     * 不要在不知道正式响应格式的情况下直接返回 true。
     * 请在这里解析 CCBill 文档规定的成功字段。
     */

    return false;
}

如果接口只接受 URL 查询参数

首先需要确认 billingApi.cgi 是否接受针对该操作的 HTTP POST 请求。如果接口明确要求使用查询字符串,可以改用 wp_remote_get()。由于请求仍由服务器发出,来源 IP 依然是服务器的出口 IP:

$url = add_query_arg(
    $request_data,
    'https://bill.ccbill.com/jpost/billingApi.cgi'
);

$response = wp_remote_get(
    $url,
    [
        'timeout' => 30,
    ]
);

不过,GET 请求会把密码、订阅编号和金额放进完整 URL。这些信息可能被 Web 服务器、代理或监控系统记录。只要接口支持 POST,就应优先使用 wp_remote_post()。

不要凭猜测修改请求方法或参数名称,应以当前使用的 CCBill API 文档和账户配置为准。

API 凭据应放在哪里

不要将 API 用户名和密码直接写入短代码输出,也不能放进 JavaScript。至少应把它们放在 wp-config.php 中:

define('CCBILL_API_USERNAME', 'your-api-username');
define('CCBILL_API_PASSWORD', 'your-api-password');

然后在插件中引用这些常量:

'username' => CCBILL_API_USERNAME,
'password' => CCBILL_API_PASSWORD,

如果示例中的凭据曾出现在真实网站的前端,即使后来已经删除,也应立即更换。它们可能已经被浏览器历史记录、缓存、日志或第三方分析系统保存。

需要特别注意的安全问题

不要相信浏览器提交的交易编号

subscriptionId、客户账号和子账号应根据当前登录用户,从服务器端数据库中读取。不要通过隐藏表单字段接收后直接使用,否则用户可以修改这些字段,尝试对其他交易发起操作。

金额必须在服务器端验证

HTML 中的 min、step 和 required 只能改善用户体验,不能提供安全保障。攻击者可以绕过表单直接提交请求,所以 PHP 端仍需检查:

  • 输入是否为合法数字;
  • 金额是否大于零;
  • 小数位和货币格式是否正确;
  • 是否超过业务允许的上限;
  • CCBill 是否另有最低或最高金额限制。

金额最好使用十进制字符串或最小货币单位处理,避免在复杂的财务计算中直接依赖浮点数。

防止重复扣款

用户可能刷新页面、双击按钮,或在请求超时后再次提交。因此,支付操作需要加入幂等或重复请求保护,例如:

  • 提交前生成唯一的本地请求编号;
  • 将请求状态记录为 pending;
  • 成功后记录远程交易编号;
  • 不得使用相同请求编号再次扣款;
  • 请求超时时先查询原交易状态,不要立即重试。

如果 CCBill 提供幂等键、交易查询或回调机制,应优先使用官方机制。

不要把完整接口响应写入公开日志

支付响应可能包含订阅编号、交易编号或其他敏感信息。日志只应保留排错所需的信息,并遮盖敏感字段。任何情况下都不能把 API 密码写入日志。

超时不等于失败

服务器请求超时只表示 WordPress 没有及时收到结果,不能证明 CCBill 没有完成扣款。遇到超时时,不要立即重新发送同一笔扣款请求,否则可能造成重复收费。

使用回调确认最终状态

如果 CCBill 为该流程提供 webhook、postback 或交易状态查询接口,应通过这些机制确认最终支付状态。用户浏览器显示的页面只能用于提示,不能作为财务记账的唯一依据。

AJAX 是否必要

不使用弹窗并不等于必须使用 AJAX。让普通表单提交到 WordPress,再由服务器请求 CCBill,就可以解决 IP 白名单和弹窗问题。

如果希望提交后页面完全不刷新,可以在此基础上使用 WordPress AJAX 或 REST API。无论前端采用普通表单、fetch() 还是其他方式,原则都一样:浏览器只能把捐款请求发送到本站,包含 CCBill 凭据的请求必须由 WordPress 服务器发出。

备注:内容仅供参考。