Hyperf v2.0 默认将 MySQL DECIMAL 映射为 float 导致精度丢失,v2.1 修复后支持 'decimal' cast;设为 'string' 可规避失真但禁用运算,手动拦截需重写 ModelUpdateVisitor。

Hyperf 默认把 MySQL 的 DECIMAL 字段映射为 float,尤其在 v2.0 中未做类型隔离,直接走 PHP 原生浮点解析,导致 123.45 存进数据库再查出来变成 123.44999999999999 —— 这不是数据库问题,是 Hyperf 模型层「主动丢精度」。
为什么 $casts 设为 'amount' => 'string' 能绕过精度丢失
PHP 的 float 类型无法精确表示大多数十进制小数,而字符串不会触发任何数值解析。只要你不调用 floatval() 或参与算术运算,string cast 就能原样保留数据库返回的字符形式(如 "123.45")。适用于:只读展示、JSON 输出、下游系统校验等场景。
注意事项:
-
stringcast 后字段不可直接用于数学运算,$model->amount + 1会隐式转成 float,再次失真 - 若字段需参与验证(如
decimal:2),必须配合预处理(trim()、str_replace()等),否则空格或换行会导致验证失败 - MySQL 返回的 DECIMAL 值默认不带尾随零(
123.4而非123.40),前端对齐显示需自行补零
为什么 $casts 设为 'amount' => 'decimal' 在 v2.1+ 才真正生效
v2.0 的 ModelUpdateVisitor 硬编码将所有 DECIMAL 列识别为 float,$casts 里写 'decimal' 会被忽略。v2.1 通过 PR#2979 修复了该逻辑,现在能正确识别 MySQL 的列类型并匹配 cast 规则。
升级后实操要点:
- 确保模型类中已声明
protected $casts = ['amount' => 'decimal']; - 确认数据库列定义为显式精度,例如
DECIMAL(18,2);若只写DECIMAL(无括号),MySQL 解析为DECIMAL(10,0),小数部分被截断 -
decimalcast 本质是调用new \Decimal($value)(需安装ext-bcmath),不依赖浮点,但要求值为合法数字字符串或整数
不改框架版本时,如何手动拦截 DECIMAL 字段解析
v2.0 用户无法升级时,最轻量的解法是重写 ModelUpdateVisitor,替换其对 DECIMAL 的类型判定逻辑。无需动核心包,只需在项目中新增一个 visitor 类,并在 config/autoload/annotations.php 中覆盖默认配置:
// app/Annotation/Visitor/DecimalModelUpdateVisitor.php
<?php
declare(strict_types=1);
namespace App\Annotation\Visitor;
use Hyperf\Database\Model\Commands\ModelUpdateVisitor;
use Hyperf\Utils\Str;
class DecimalModelUpdateVisitor extends ModelUpdateVisitor
{
protected function getCastType(string $type): ?string
{
if (Str::startsWith($type, 'decimal')) {
return 'string'; // 强制转 string,避免 float 解析
}
return parent::getCastType($type);
}
}
然后在 config/autoload/annotations.php 中指定:
'visitors' => [
'model' => \App\Annotation\Visitor\DecimalModelUpdateVisitor::class,
],
这样下次执行 php bin/hyperf.php gen:model 时,所有 DECIMAL 字段都会生成 'field' => 'string' 到 $casts 中。
cast 为 string 后,JSON 序列化仍出现浮点失真?
这是最容易被忽略的一环:Hyperf 的 Jsonable 默认调用 json_encode(),而 PHP 对 string 类型不做任何干预 —— 但如果该字符串变量曾被隐式转换为 float(比如中间赋值过 $x = (float)$s),再塞回模型属性,就已污染。更隐蔽的是 Laravel/Hyperf 的 toArray() 方法可能触发自动类型推导。
安全做法:
- 始终用
__toString()或显式(string)强转后再赋值给模型属性 - 避免在模型访问器(
getAmountAttribute)中做数值运算,改用外部 service 处理 - 对关键字段加断言:
assert(is_string($this->attributes['amount'])),防止上游代码误改类型


















