JudoPay ApiException:Amount 不符合 GBP 小数位要求
结论
不要先把 amount 格式化为两位小数,再强制转换成 float。传给 JudoPay 的值应是以 . 为小数点、固定保留两位小数的字符串,例如 "7.98":
$fixed_amount = number_format((float) $total_amount, 2, '.', '');
不要在外层添加 (float):
// 不推荐
$fixed_amount = (float) number_format($total_amount, 2, '.', '');
生产环境报错而本地正常,通常不是 sandbox key 导致的。更可能的原因是金额在计算、类型转换、区域设置或请求序列化过程中发生了变化。PHP 的 float 无法精确表示十进制货币值,SDK 最终发送的内容也不一定与调试时看到的 7.98 完全一致。
为什么转换成 float 可能出问题
number_format() 返回字符串:
number_format(7.98, 2, '.', ''); // "7.98"
这个字符串已经符合 GBP 保留两位小数的要求。转换成 float 后,固定两位小数的格式就会丢失:
(float) "7.90"; // float 7.9
浮点数使用二进制表示,某些十进制金额在内部可能是近似值:
7.979999999999...
或者:
7.980000000001...
json_encode() 通常能生成可接受的值,但结果可能受 PHP 版本、serialize_precision、SDK 序列化方式和运行环境配置影响。如果 SDK 将浮点数展开为超过两位小数的内容,JudoPay 会认为该金额不符合 GBP 的小数位要求。
区域设置也可能造成环境差异。如果生产服务器的 locale 使用逗号作为小数分隔符,SDK 内部以某些方式格式化浮点数时,理论上可能得到 7,98。是否会出现这种情况取决于 SDK 版本及其序列化实现,不能只根据变量是 float 类型来判断。
推荐写法
先确认参与计算的是未格式化的数值,再将最终结果格式化为字符串:
$subtotal = (float) $_SESSION['checkout_subtotal'];
$deliveryAmount = (float) $delivery;
$totalAmount = $subtotal + $deliveryAmount;
$fixedAmount = number_format(
round($totalAmount, 2),
2,
'.',
''
);
$payment->setAttributeValues([
'judoId' => $judoId,
'yourConsumerReference' => $consumerReference,
'yourPaymentReference' => $paymentReference,
'amount' => $fixedAmount,
'currency' => 'GBP',
'cardNumber' => $cardNumber,
'expiryDate' => $expiryDate,
'cv2' => $cv2,
]);
此时传入的金额是字符串:
"7.98"
整额或只有一位小数的金额也会保留 GBP 所需的格式:
"7.00"
"7.90"
更稳妥的货币计算方式
如果金额需要经过多次加减、折扣或税费计算,尽量不要直接使用 float 累加。可以先把金额转换为便士,用整数完成计算,最后再转换回 GBP 格式:
function poundsToPence(string $amount): int
{
$normalized = trim($amount);
if (!preg_match('/^\d+(?:\.\d{1,2})?$/', $normalized)) {
throw new InvalidArgumentException('Invalid GBP amount');
}
[$pounds, $pence] = array_pad(explode('.', $normalized, 2), 2, '');
$pence = str_pad($pence, 2, '0');
return ((int) $pounds * 100) + (int) $pence;
}
function penceToPounds(int $pence): string
{
return sprintf('%d.%02d', intdiv($pence, 100), $pence % 100);
}
$subtotalPence = poundsToPence((string) $_SESSION['checkout_subtotal']);
$deliveryPence = poundsToPence((string) $delivery);
$fixedAmount = penceToPounds($subtotalPence + $deliveryPence);
传给 JudoPay 时仍然使用字符串:
'amount' => $fixedAmount,
需要注意,如果 JudoPay 的 amount 表示 GBP 主货币单位,就应传入 "7.98"。不能因为内部使用便士计算而直接传入 798,除非当前使用的 API 明确要求最小货币单位。
检查生产环境中的实际输入
还应检查 $_SESSION['checkout_subtotal'] 和 $delivery,确认其中没有货币符号、千位分隔符或本地化小数点。下面这些值不能直接用于金额转换:
£7.98
1,234.56
7,98
发送请求前,可以记录与金额有关的调试数据,但不要包含银行卡信息:
error_log(json_encode([
'subtotal' => $_SESSION['checkout_subtotal'],
'delivery' => $delivery,
'totalAmount' => $totalAmount,
'fixedAmount' => $fixedAmount,
'fixedAmountType' => gettype($fixedAmount),
], JSON_UNESCAPED_SLASHES));
预期结果类似:
{
"subtotal": "5.99",
"delivery": "1.99",
"totalAmount": 7.98,
"fixedAmount": "7.98",
"fixedAmountType": "string"
}
不要在日志中记录 cardNumber、cv2 或完整的支付请求。
如果改用字符串后仍然报错,需要检查 SDK 实际发送的 HTTP 请求体,而不能只看 var_dump($fixedAmount)。重点确认最终字段是否确实是:
{
"amount": "7.98",
"currency": "GBP"
}
还应比较本地与生产环境的 PHP 版本、JudoPay SDK 版本、serialize_precision 和 locale 配置。货币计算应尽量避开浮点精度问题,在 API 边界使用含义明确的十进制字符串,并确保 GBP 金额固定保留两位小数。
备注:内容仅供参考。
