PAYATHON 2026

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 金额固定保留两位小数。

备注:内容仅供参考。