ThinkPHP 5 的 max() 为什么把日期截成 4 位数字
某天接手一个后台管理系统的 bug:「最近发送时间」一栏显示「1970/1/1 上午 8:00:02」。数据库里记录是好的,直接跑 SELECT MAX(created_at) 也返回正确的 2026-05-07 17:43:52。怀疑过时区、怀疑过 SQL 字段名写错、怀疑过前端 dayjs 版本——最后在 PHP 层 var_dump 才发现,接口吐出来的值就是 float(2026),不是字符串,是个浮点数。
现象
PHP 接口逻辑大概长这样:
$latest = Db::name('some_log')
->where('store_id', $storeId)
->max('created_at'); // 没有第二参数
return $this->success('ok', ['latest_time' => $latest]);接口响应:
{ "latest_time": 2026 }前端拿到 number 类型的 2026,new Date(2026) 解释为毫秒时间戳,结果落在 1970 年 1 月 1 日 00:00:02(UTC)。
根因
ThinkPHP 5.0.x 的 max() / min() 签名:
public function max($field, $force = true)
{
return $this->aggregate('MAX', $field, $force);
}$force 默认 true。往下追到 value() 方法,force 分支最终执行了一次 PHP 弱类型强转:
// force=true 路径(部分 5.0.x patch 版本)
$result = (float) $result;PDO 把 MySQL 返回的 '2026-05-07 17:43:52' 以字符串形式交给 PHP,此时:
php > var_dump((float) '2026-05-07 17:43:52');
float(2026)PHP 弱类型转换只读前导数字,小数点后的 -05-07 17:43:52 全部丢弃。年份 2026 作为浮点数被 JSON 序列化成 number,发给前端。
->max('created_at') 不传第二参数时 $force 默认 true,触发 (float) 强转。DATETIME、DATE、VARCHAR 等非数值字段只保留前导数字,其余全部丢失——且不报任何错误、不抛任何异常,属于静默数据损坏。
正解
最小改动:显式传 false 作为第二参数,跳过强转,直接返回 PDO 原始字符串:
$latest = Db::name('some_log')
->where('store_id', $storeId)
->max('created_at', false); // ← 关键:force=false
// $latest = '2026-05-07 17:43:52'(string,完整)同理适用于 min():
$earliest = Db::name('some_log')->min('created_at', false);注意:avg() / sum() 作用于真正的数值字段时 force=true 是合理的,不要无差别改成 false,否则下游做数值运算会出问题。只有聚合字段不是 INT / DECIMAL / FLOAT 时才需要 force=false。
->max('created_at', false) 跳过 float 强转,返回完整的原始字符串。适用于所有 DATETIME、DATE、VARCHAR 类型字段的 max() / min() 调用。avg() / sum() 作用于数值字段时保持默认。
全项目审计
修完当前问题,顺手扫一遍存量调用,防止同款坑埋在统计模块里:
grep -rn "->max(\|->min(\|->sum(\|->avg(" application/ --include="*.php" \
| grep -v "false)" \
| grep -v "// safe"逐条确认字段类型:非数值型一律加 , false。
误判信号对照表
| 现象 | 容易误判为 | 实际根因 |
|------|-----------|---------|
| 接口返回年份数字(如 2026) | 前端格式化错误 | 后端 aggregate 已在 PHP 层截断 |
| new Date(2026) = 1970 年 | 时区问题 | 值本身已坏,不是时区 |
| 直接跑 SQL 结果正确 | TP 缓存问题 | PHP 层 (float) 强转,与 SQL 无关 |
| count() 正常,max() 出问题 | 逻辑偶发 bug | count() 永远返回 int,cast 无害;max() 才暴露问题 |
在 MySQL 里跑 SELECT MAX(created_at) + 0 FROM t,结果就是 PHP (float) 会拿到的值——'2026-05-07...' + 0 = 2026。拿到这个中间值,你就能在数据库侧复现 PHP 的截断逻辑,不需要加 var_dump。
一句话外卖
ThinkPHP 5 的
max()/min()第二参数$force默认true,对非数值字段会静默执行(float)强转——凡是聚合 DATETIME / VARCHAR 字段,务必显式传false。