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。可以采用下面的结构:
- 使用
wp_enqueue_scripts注册依赖; - 使用短代码或模板输出表单;
- 将支付逻辑放入独立的 JavaScript 文件;
- 只在支付页面加载 Datatrans SDK;
- 需要服务器确认或记账时,再调用 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 后端完成可信的交易验证。
备注:内容仅供参考。
