PAYATHON 2026

WordPress 中实现 Datatrans Ajax 支付时出现 1003 错误

支付阿杰

结论

问题不在于请求没有经过 WordPress 的 wp-admin/admin-ajax.php。

UPP.paymentRequest() 是 Datatrans SDK 发起的第三方请求,api.js 会直接与 Datatrans 通信。它不受 WordPress Ajax 路由机制约束,也不需要为了符合 WordPress 的使用方式而转发到 admin-ajax.php。

response.uppTransactionId 为 undefined 通常是请求失败后的结果,并不是失败原因。只有 Datatrans 成功创建交易,响应中才会包含 uppTransactionId。状态码 1003 说明之前的交易初始化没有成功,应先检查响应中的错误字段、商户配置和实际提交的参数。

为什么回调执行了,却没有 uppTransactionId

responseCallback 会在请求结束后执行,并不代表支付已经成功。无论成功还是失败,都可能进入这个函数。

当前代码直接读取:

response.uppTransactionId

如果响应内容类似:

{
  "status": "error",
  "errorCode": "1003",
  "errorMessage": "..."
}

其中自然不会有 uppTransactionId。正确的处理顺序是先判断 response.status,确认成功后再使用交易编号:

function responseCallback(response) {
    console.log('Datatrans response:', response);

    if (!response || response.status !== 'success') {
        console.error('Datatrans payment request failed', {
            status: response && response.status,
            errorCode: response && response.errorCode,
            errorMessage: response && response.errorMessage,
            response: response
        });
        return;
    }

    if (!response.uppTransactionId) {
        console.error(
            'Datatrans returned success without uppTransactionId',
            response
        );
        return;
    }

    console.log('Transaction ID:', response.uppTransactionId);
}

不要只显示 status 和 uppTransactionId。定位问题时,更需要查看完整响应中的 errorCode、errorMessage 或同类字段。具体字段名应以当前使用的 Datatrans API 版本为准。

优先检查这些问题

目前缺少浏览器实际返回的完整 JSON,因此无法仅凭 1003 判断具体是哪项配置有误。可以按下面的顺序排查。

1. 两处 merchantId 是否完全一致

商户号同时出现在 SDK 地址和请求参数中:

https://pilot.datatrans.biz/upp/ajax/api.js?merchantId=...
params.merchantId

两处必须完全一致,而且都应属于 pilot 测试环境。还要确认页面中的隐藏字段没有被模板、缓存插件或用户输入覆盖。

可以在调用前直接检查:

console.log({
    configuredMerchantId: DatatransConfig.merchantId,
    submittedMerchantId: params.merchantId
});

2. 是否混用了测试环境和生产环境

示例使用的是:

https://pilot.datatrans.biz/

所以商户号、测试卡、回调地址和后续服务端请求都必须属于同一个环境。测试商户配置不能直接用于生产端点,生产配置也不能用于测试端点。

3. WordPress 页面域名是否已被商户配置允许

如果本地 HTML 可以运行,而 WordPress 页面失败,应重点比较两个页面的运行来源:

  • 页面是否使用 HTTPS;
  • 域名是否与商户后台配置一致;
  • 页面是否从不同的子域名加载;
  • Datatrans 是否限制请求来源或返回地址;
  • returnUrl 是否属于允许的地址。

如果 Datatrans 商户配置限制了来源域名,本地示例和正式站点可能得到不同结果。是否需要登记域名,以及域名采用什么匹配规则,应以当前 Datatrans 商户配置和 API 文档为准。

4. 实际传入的金额和货币格式

不要只看 PHP 变量,还要检查浏览器最终提交的值:

console.log('Datatrans request:', params);

重点确认:

{
    merchantId: "...",
    amount: "...",
    currency: "...",
    cardNumber: "...",
    expm: "...",
    expy: "..."
}

金额通常需要使用最小货币单位,例如 1000 表示 10.00,但具体规则仍应以所用 API 版本为准。同时检查是否存在千位分隔符、小数点、空格或 HTML 转义引起的格式变化。

5. WordPress 中的 jQuery 兼容模式

WordPress 通常以 noConflict 模式加载 jQuery,因此全局 $ 不一定可用。代码应放在下面的结构中:

(function ($) {
    'use strict';

    // Datatrans integration
})(jQuery);

如果 $ 不可用,浏览器一般会直接报出 ReferenceError: $ is not defined,而不是收到 Datatrans 的 1003。这未必是本次错误的直接原因,但当前实现仍需要修正。

6. 页面缓存和脚本优化

合并脚本、延迟加载或 CDN 优化插件可能会改变第三方 SDK 的加载顺序。请在浏览器的 Network 和 Console 中检查:

  • api.js 是否成功返回;
  • UPP 是否已定义;
  • api.js 是否被 CSP、广告拦截器或混合内容策略阻止;
  • 调用 UPP.paymentRequest() 时 SDK 是否已经加载;
  • 请求和响应的完整内容是什么。

推荐的 WordPress 组织方式

不建议在同一个 PHP 函数中混合脚本注册、大段 HTML 和内联 JavaScript。可以采用下面的结构:

  1. 使用 wp_enqueue_scripts 注册依赖;
  2. 使用短代码或模板输出表单;
  3. 将支付逻辑放入独立的 JavaScript 文件;
  4. 只在支付页面加载 Datatrans SDK;
  5. 需要服务器确认或记账时,再调用 WordPress 后端。

PHP:加载 SDK 和业务脚本

add_action('wp_enqueue_scripts', function () {
    if (!is_page('payment')) {
        return;
    }

    $merchant_id = '101...';

    wp_enqueue_script('jquery');

    wp_enqueue_script(
        'datatrans-upp',
        'https://pilot.datatrans.biz/upp/ajax/api.js?merchantId=' . rawurlencode($merchant_id),
        array(),
        null,
        false
    );

    wp_enqueue_script(
        'site-datatrans-payment',
        get_stylesheet_directory_uri() . '/js/datatrans-payment.js',
        array('jquery', 'datatrans-upp'),
        '1.0.0',
        true
    );

    wp_localize_script(
        'site-datatrans-payment',
        'DatatransConfig',
        array(
            'merchantId' => $merchant_id,
            'returnUrl'  => home_url('/datatrans/return/'),
        )
    );
});

wp_localize_script() 可以用来传递非敏感配置,但不能用于暴露密钥。商户号通常需要发送给前端,不能将它当作服务端签名密钥使用。

JavaScript:发起请求并区分成功与失败

(function ($) {
    'use strict';

    window.callPayment = function () {
        var mode = $('input[name="mode"]:checked').val();

        var params = {
            merchantId: DatatransConfig.merchantId,
            cardNumber: $('input[name="cardNumber"]').val(),
            expy: $('input[name="expy"]').val(),
            expm: $('input[name="expm"]').val(),
            cvv: $('input[name="cvv"]').val(),
            currency: $('input[name="currency"]').val(),
            amount: $('input[name="amount"]').val()
        };

        console.log('Datatrans request:', {
            merchantId: params.merchantId,
            currency: params.currency,
            amount: params.amount,
            expm: params.expm,
            expy: params.expy
        });

        // 不要在日志中输出卡号或 CVV。
        if (mode === 'auto') {
            params.returnUrl = DatatransConfig.returnUrl;
            UPP.paymentRequest(params, responseCallback, frameCallback);
        } else {
            UPP.paymentRequest(params, responseCallback);
        }
    };

    function frameCallback() {
        var iframe = $('<iframe>', {
            id: 'paymentIFrame',
            title: '3-D Secure authentication'
        }).width(390).height(400);

        $('#frameHolder').empty().append(iframe);
        $('#uppform').hide();

        return iframe[0];
    }

    function responseCallback(response) {
        var $response = $('#response').empty();

        if (!response) {
            $response.text('Datatrans 未返回有效响应。');
            return;
        }

        if (response.status !== 'success') {
            $response.append(
                $('<p>').text('支付初始化失败。'),
                $('<p>').text('状态:' + (response.status || 'unknown')),
                $('<p>').text(
                    '错误代码:' +
                    (response.errorCode || response.code || 'unknown')
                ),
                $('<p>').text(
                    '错误信息:' +
                    (response.errorMessage || response.message || '未提供')
                )
            );

            console.error('Datatrans error response:', response);
            return;
        }

        if (!response.uppTransactionId) {
            $response.text('交易创建成功,但响应中没有 uppTransactionId。');
            console.error('Missing uppTransactionId:', response);
            return;
        }

        $response.append(
            $('<p>').text('状态:success'),
            $('<p>').text(
                'uppTransactionId:' + response.uppTransactionId
            )
        );

        if (response.is3DEnrolled) {
            $('#paymentIFrame').remove();
        }

        // 后续应把交易编号交给服务器验证,而不是仅信任前端结果。
        verifyTransactionOnServer(response.uppTransactionId);
    }

    function verifyTransactionOnServer(uppTransactionId) {
        // 在这里调用站点自己的后端接口。
        // 后端再与 Datatrans 通信并完成交易验证或后续处理。
    }
})(jQuery);

admin-ajax.php 应该用在哪里

admin-ajax.php 适合处理站点自己的后端流程,例如:

  • 接收 uppTransactionId;
  • 在服务器端查询或确认交易状态;
  • 执行 Datatrans 要求的 server-to-server 请求;
  • 创建订单或更新订单状态;
  • 验证 nonce 和当前订单;
  • 防止客户端篡改金额、货币和订单编号。

没有必要让请求绕到 admin-ajax.php,再由它调用 UPP.paymentRequest()。浏览器端的 SDK 初始化和 WordPress 后端业务处理属于两个不同的阶段。

一个简化的 WordPress 处理器可以写成:

add_action('wp_ajax_verify_datatrans_transaction', 'verify_datatrans_transaction');
add_action('wp_ajax_nopriv_verify_datatrans_transaction', 'verify_datatrans_transaction');

function verify_datatrans_transaction() {
    check_ajax_referer('datatrans_payment', 'nonce');

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

    if ($transaction_id === '') {
        wp_send_json_error(
            array('message' => 'Missing uppTransactionId.'),
            400
        );
    }

    /*
     * 在服务器端调用 Datatrans 当前版本提供的查询、授权或结算接口。
     * 使用服务器保存的订单金额和货币,不要信任浏览器提交的数据。
     */

    wp_send_json_success(array(
        'uppTransactionId' => $transaction_id,
    ));
}

前端调用:

$.post(DatatransConfig.ajaxUrl, {
    action: 'verify_datatrans_transaction',
    nonce: DatatransConfig.nonce,
    uppTransactionId: uppTransactionId
}).done(function (result) {
    console.log('Server verification result:', result);
});

对应的配置需要增加:

wp_localize_script(
    'site-datatrans-payment',
    'DatatransConfig',
    array(
        'merchantId' => $merchant_id,
        'returnUrl'  => home_url('/datatrans/return/'),
        'ajaxUrl'    => admin_url('admin-ajax.php'),
        'nonce'      => wp_create_nonce('datatrans_payment'),
    )
);

也可以使用 WordPress REST API 代替 admin-ajax.php。无论选择哪一种,都不会解决 Datatrans 返回的 1003。它们只是站点后端接口的两种实现方式。

安全注意事项

卡号和 CVV 是高度敏感的数据。不要将它们发送到 admin-ajax.php,也不要写入 WordPress 数据库、PHP 日志或浏览器日志。如果这些数据经过自己的服务器,PCI DSS 的合规范围会显著扩大。

还需要注意:

  • 金额、货币和订单状态必须由服务器重新核对;
  • 不能只根据浏览器返回的 status === 'success' 将订单标记为已支付;
  • 后续授权、确认或结算应采用 Datatrans 当前推荐的服务器端流程;
  • 测试环境示例代码可能对应旧版 API,正式接入前应核对当前 Datatrans 文档和 PCI 要求;
  • 如果 Datatrans 已为当前账户提供托管支付页、Secure Fields 或更新的 SDK,应优先使用其当前推荐的方案。

下一步不是为了适配 admin-ajax.php 而拆分代码。应先记录完整的错误响应,再比较本地页面与 WordPress 页面中的 merchantId、环境、来源域名、returnUrl、金额和货币。确认 Datatrans 成功返回 uppTransactionId 后,再将该编号交给 WordPress 后端完成可信的交易验证。

备注:内容仅供参考。