php小数精度丢失源于ieee 754浮点限制,须全链路用字符串处理:mysql用decimal,php接收不转float,api传字符串,运算用bcmath,输出前转字符串。

PHP保存小数时精度丢失的常见表现
后端用 float 或 double 类型接收前端传来的金额、坐标、比例等小数,存进 MySQL 后变成 19.999999999999996 或 0.1 + 0.2 = 0.30000000000000004,H5 页面展示异常,小程序调用 toFixed(2) 后仍对不上预期值。这不是 PHP 的 bug,而是 IEEE 754 浮点数表示法的固有局限。
关键问题不在“怎么四舍五入”,而在于“从哪一环开始就该放弃浮点数”。
MySQL 字段类型必须用 DECIMAL,且 PHP 不要用 float 接收
DECIMAL(10,2) 是唯一可靠选择,它按字符串方式存储精确小数,不受二进制浮点误差影响。但光改数据库不够:
- PHP 接收参数时,别用
(float) $_POST['price'] —— 这一步就把原始字符串转成了不精确的二进制浮点
- 应直接使用原始字符串:
$price = $_POST['price'] ?? '';,再校验格式(如正则 /^\d+(\.\d{1,2})?$/)
- 插入数据库前,确保不经过任何隐式转换:
$stmt->bindValue(':price', $price, PDO::PARAM_STR);
- 如果用 Laravel Eloquent,给对应字段加
$casts = ['price' => 'string'];,避免自动转 float
前后端交互必须约定“小数统一传字符串”
H5 和小程序都倾向把数字当 number 类型处理,但 JS 的 Number 本质也是 IEEE 754。一旦涉及计算或序列化(如 JSON.stringify({price: 19.99})),就可能引入误差。
(float) $_POST['price'] —— 这一步就把原始字符串转成了不精确的二进制浮点$price = $_POST['price'] ?? '';,再校验格式(如正则 /^\d+(\.\d{1,2})?$/)$stmt->bindValue(':price', $price, PDO::PARAM_STR);
$casts = ['price' => 'string'];,避免自动转 floatnumber 类型处理,但 JS 的 Number 本质也是 IEEE 754。一旦涉及计算或序列化(如 JSON.stringify({price: 19.99})),就可能引入误差。
解决方案不是让前端做更多处理,而是协议层降级:所有小数字段,在 API 请求/响应中强制为字符串。
- 后端输出 JSON 前,对小数字段显式转字符串:
'price' => (string)$model->price - 小程序 wx.request 成功回调里,别直接
res.data.price.toFixed(2),先parseFloat(res.data.price)再操作——但更稳妥的是服务端已返回"19.99",前端直接显示 - H5 若用 axios,可在
transformResponse里批量把 key 匹配/price|amount|rate/的字段转成字符串
PHP 处理小数运算必须用 BCMath 扩展
bcadd()、bcmul()、bcdiv() 是唯一能保证精度的方案。启用前确认扩展已开启:extension=php_bcmath.dll(Windows)或 extension=bcmath.so(Linux)。
- 不要用
round($a + $b, 2) —— 加法本身已经失真
- 正确写法:
bcadd('19.99', '0.01', 2),第三个参数是小数位数,不是四舍五入规则
- 注意:所有参数必须是字符串,
bcadd(19.99, 0.01, 2) 会先触发 PHP 浮点转换,前功尽弃
- BCMath 不支持科学计数法,输入如
"1e-2" 会失败,需提前标准化
小数精度问题从来不是单点修复,而是从接口定义、数据接收、存储、运算到输出的全链路格式约束。最容易被跳过的环节,是 PHP 接收参数那一刻没守住字符串边界——后面所有补救,都是在浮点误差的废墟上盖楼。
round($a + $b, 2) —— 加法本身已经失真bcadd('19.99', '0.01', 2),第三个参数是小数位数,不是四舍五入规则bcadd(19.99, 0.01, 2) 会先触发 PHP 浮点转换,前功尽弃"1e-2" 会失败,需提前标准化











