如何通过 ePay/Bambora PHP API 完成单笔支付?
结论
capture() 不能创建新支付,只能对已经完成授权并取得真实 transactionid 的交易执行扣款。传入 -1 指向的是一笔不存在的交易,因此会返回 -1008。
标准流程如下:
- 服务端创建本地订单。
- 用户通过 ePay/Bambora 托管支付窗口输入银行卡信息并完成验证。
- 授权成功后,ePay/Bambora 创建交易并返回
transactionid。 - 服务端核验交易信息。
- 使用真实的
transactionid调用capture(),或者在发起支付时启用即时扣款。
这个流程与系统是否属于 webshop 无关。只要需要持卡人付款,就必须先授权,再扣款。
为什么不能直接调用 capture()
银行卡支付通常分为两个阶段:
- Authorize(授权):验证银行卡,完成必要的身份验证,并冻结或保留相应金额。授权成功后会生成
transactionid。 - Capture(扣款):使用已有的
transactionid,正式扣取已授权的金额。
当前代码跳过了授权,直接尝试捕获交易:
$result = $client->capture($epay_params);
transactionid = -1 并不表示“创建新交易”,它只是一个不存在的交易编号,所以接口返回:
{
"captureResult": false,
"pbsResponse": -1,
"epayresponse": -1008
}
这说明 SOAP 请求已经到达接口,但接口找不到要捕获的交易。
MerchantID 不需要保密
merchantnumber 或 MerchantID 通常只是商户的公开标识,不是密码。使用托管支付窗口时,这项信息不可避免地会出现在浏览器请求或前端代码中。
需要保密的是:
- API 密钥或 SOAP 访问凭据;
- MD5/签名密钥;
- 后台管理密码;
- 其他能够授权敏感操作的凭据。
这些信息只能保存在服务端,不能写进 JavaScript、HTML 或公开仓库。
推荐的支付流程
1. 在服务端创建订单
先在自己的数据库中创建待支付订单,并生成一个不可重复的 orderid。金额必须由服务端确定,不能直接采用浏览器提交的值。
例如:
<?php
$orderId = 'CAL-' . bin2hex(random_bytes(8));
$amount = 9995; // 最小货币单位,例如 99.95 DKK
$currency = 'DKK';
// 将订单写入数据库:
// order_id = $orderId
// amount = $amount
// currency = $currency
// status = pending
2. 打开托管支付窗口
如果使用的是问题中 SOAP 地址对应的旧版 ePay Payment Window,前端调用通常类似下面这样:
<script src="https://ssl.ditonlinebetalingssystem.dk/integration/ewindow/paymentwindow.js"></script>
<button type="button" id="pay-button">支付</button>
<script>
const paymentWindow = new PaymentWindow({
merchantnumber: "1234567",
amount: "9995",
currency: "DKK",
orderid: "CAL-EXAMPLE-001",
accepturl: "https://example.com/payment/accept.php",
cancelurl: "https://example.com/payment/cancel.php",
windowstate: "3"
});
document.getElementById("pay-button").addEventListener("click", function () {
paymentWindow.open();
});
</script>
具体参数名称可能因 ePay/Bambora 接入版本而异,应以测试账户对应的 Payment Window 文档为准。不要将签名密钥放进这段 JavaScript。
用户在支付窗口中输入银行卡信息并完成授权后,支付平台才会生成真正的交易编号。
接收并核验交易编号
授权成功后,平台通常会将用户跳转到 accepturl,同时附带交易编号、订单号等参数。在旧版集成中,交易编号可能通过 txnid 传递,实际名称应以对应版本的回调参数说明为准。
<?php
$transactionId = filter_input(
INPUT_GET,
'txnid',
FILTER_VALIDATE_INT
);
if (!$transactionId) {
http_response_code(400);
exit('Missing or invalid transaction ID');
}
$orderId = $_GET['orderid'] ?? '';
if ($orderId === '') {
http_response_code(400);
exit('Missing order ID');
}
浏览器访问了 accepturl,不代表订单已经支付。查询字符串可以被修改,用户也可能重复访问该地址。
服务端至少需要检查:
- 回调签名是否正确;
transactionid是否真实存在;- 交易是否属于当前商户;
orderid是否与本地订单一致;- 金额和币种是否一致;
- 交易是否已经授权;
- 订单是否已经处理,以免重复扣款。
不同历史版本使用的签名字段和算法可能不同,不要凭猜测自行拼接。应按照账户对应版本的签名说明实现,或通过官方提供的交易查询接口再次确认。
使用真实 transactionid 执行 capture
确认授权成功后,才能调用 capture():
<?php
$transactionId = 123456789; // 授权成功后由 ePay/Bambora 返回
$orderId = 'CAL-EXAMPLE-001';
$amount = 9995;
$params = [
'merchantnumber' => '1234567',
'transactionid' => $transactionId,
'amount' => $amount,
'group' => '',
'paymentcollection' => 1,
'orderid' => $orderId,
'pbsResponse' => -1,
'epayresponse' => -1,
];
try {
$client = new SoapClient(
'https://ssl.ditonlinebetalingssystem.dk/remote/payment.asmx?WSDL',
[
'exceptions' => true,
'trace' => false,
]
);
$result = $client->capture($params);
if (!empty($result->captureResult)) {
// 使用原子更新,避免同一订单被重复处理。
// updateOrderStatus($orderId, 'paid', $transactionId);
echo 'Payment captured';
} else {
$pbsResponse = $result->pbsResponse ?? null;
$epayResponse = $result->epayresponse ?? null;
error_log(sprintf(
'Capture failed: order=%s transaction=%d pbs=%s epay=%s',
$orderId,
$transactionId,
var_export($pbsResponse, true),
var_export($epayResponse, true)
));
http_response_code(502);
echo 'Payment could not be captured';
}
} catch (SoapFault $exception) {
error_log('ePay SOAP error: ' . $exception->getMessage());
http_response_code(502);
echo 'Payment service unavailable';
}
传给 capture() 的金额不能超过授权金额。账户配置、收单协议和接口版本会决定是否允许部分扣款或多次扣款。
即时扣款
如果业务要求付款完成后立即扣款,可以在创建支付窗口时启用类似 instantcapture 的选项:
const paymentWindow = new PaymentWindow({
merchantnumber: "1234567",
amount: "9995",
currency: "DKK",
orderid: "CAL-EXAMPLE-001",
instantcapture: "1",
accepturl: "https://example.com/payment/accept.php",
cancelurl: "https://example.com/payment/cancel.php"
});
授权成功后,平台会尝试立即完成 capture。PHP 后端通常只需核验结果并更新订单状态。
即时扣款是否可用、参数值采用什么格式,以及允许在什么时间扣款,仍取决于商户协议和账户设置。某些业务只能在服务已经提供后扣款,不能在创建预约时立即扣款。
能否完全通过 PHP 收集银行卡并创建交易
只有 ePay/Bambora 为账户开通了专门的服务器端授权接口,并且系统满足相应的 PCI DSS、安全认证和收单要求,自己的 PHP 服务才可能直接接收银行卡信息并发起授权。
普通的 remote/payment.asmx 管理接口通常用于处理已有交易,例如:
- 查询交易;
- 捕获交易;
- 退款;
- 删除或取消授权。
它不能把虚构的 transactionid 转换成一笔新支付。
对于一般的 calendar webapp,托管支付窗口更合适。银行卡信息会直接提交给支付平台,服务器只保存订单号、金额、交易编号和支付状态,承担的 PCI 风险也小得多。
所以,需要解决的不是寻找另一个 capture() 参数,而是先通过支付窗口完成授权,取得真实的 transactionid,再由 PHP 核验并捕获这笔交易。
备注:内容仅供参考。
