Snowflake ID 遇上 intval():PHP + JS 双端静默截断的根因与正解
某次排查一个"有数据但查不出来"的 bug,最终发现是一个看起来人畜无害的 intval() 调用。后端控制器接收前端传来的记录 ID,用 intval() 做类型强转,然后拿去 WHERE 查库——结果完全查不到。数据库里明明有这条记录,日志里没有报错,SQL 也没有语法问题,就是返回空。
排查到最后才意识到:这是一个跨越 PHP 运行时、JavaScript 引擎、MySQL 协议三层的 静默整数截断 问题。
现象
- PHP 控制器
intval($this->request->post('id'))拿到的值和前端传来的不一致 WHERE id = ?查询返回空记录,或偶发查出错误记录$id <= 0校验对一个合法 ID 返回true(ID 被截断成了负数或PHP_INT_MAX)- 日志无异常,SQL 正常执行,只是 WHERE 条件用了一个被篡改的 ID
根因
系统使用了 Snowflake 算法生成的 64 位雪花 ID(19 位十进制整数)。这类 ID 在两个地方会被静默截断:
第一层:32 位 PHP 的 PHP_INT_MAX
32 位 PHP 的 PHP_INT_MAX = 2,147,483,647(10 位),而 Snowflake ID 通常是 18~19 位。intval() 在 32 位运行时会把超出范围的值截断为 PHP_INT_MAX 或负数:
// 32 位 PHP 上
$id = intval('1234567890123456789');
var_dump($id); // int(2147483647) ← 截断了!即便是 64 位 PHP(PHP_INT_MAX = 9,223,372,036,854,775,807),intval() 在技术上能容纳大多数雪花 ID,但这是实现细节,不是可靠约定。
第二层:JavaScript 的 Number.MAX_SAFE_INTEGER
JavaScript 的浮点数(IEEE 754 双精度)能精确表示的最大整数是 2^53 - 1 = 9,007,199,254,740,991(16 位),而 Snowflake ID 普遍超过这个范围:
// JS 精度丢失
console.log(9007199254740992 === 9007199254740993) // true ← 完全错误!前端如果把 API 返回的 ID 解析成 Number 再传给后端,传过去的值早已不对,后端用再正确的方式处理也救不回来。
intval() 对超出整型范围的 ID 截断是无声的——不抛异常、不打日志、不返回错误,只是悄悄用一个错误的值去查库。JavaScript parseInt() / Number() 对超出 Number.MAX_SAFE_INTEGER 的 ID 同样静默丢精度。两者叠加,ID 在链路上经历两次静默变形。
正解
PHP 侧:把 intval() 换成空值校验,保留字符串
// 错误写法 —— 截断风险
$id = intval($this->request->post('id'));
if ($id <= 0) {
$this->error('参数错误');
}
// 正确写法 —— 保持字符串,只做空值校验
$id = $this->request->post('id');
if (empty($id)) {
$this->error('参数错误');
}
// ThinkPHP query builder 正确处理字符串 ID,参数化查询不受影响
$record = Db::name('orders')->where('id', $id)->find();Service 层方法签名也不要强制 int 类型提示:
// 错误:类型提示为 int 会强制截断
public static function getDetail(int $id): array { ... }
// 正确:不限类型,接受 string 或 int 均可
public static function getDetail($id): array { ... }JavaScript 侧:ID 从一开始就当字符串用
// 错误 —— 精度丢失
const id = parseInt(response.data.id)
const id = Number(response.data.id)
// 正确 —— 保持字符串
const id = String(response.data.id)
// 或直接用 API 返回的原始值(后端应返回 string 而非 number)后端 API 在序列化 JSON 时,雪花 ID 字段应明确转为字符串再输出,彻底堵住前端无意中 JSON.parse 丢精度的可能:
// PHP 响应时显式转字符串
$this->success('ok', [
'id' => (string)$record['id'],
'name' => $record['name'],
]);快速验证当前环境
// 检查 PHP 位宽
echo PHP_INT_SIZE; // 8 = 64 位,4 = 32 位
echo PHP_INT_MAX; // 64 位:9223372036854775807
// 模拟截断
var_dump(intval('1234567890123456789'));
// 64 位正常;32 位输出 int(2147483647) 即已截断// 检查 JS 精度边界
console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991
console.log(9007199254740992 === 9007199254740993) // true = 已超出安全范围雪花 ID 全链路保持字符串语义:PHP 不转型、ThinkPHP query builder 接受字符串 WHERE 参数、JavaScript 不解析为 Number、JSON 响应显式转 string。零额外成本,无需改 DB schema,彻底规避两端截断。
一句话外卖
Snowflake ID 是字符串语义的整数——PHP 的
intval()和 JS 的Number()都会静默截断它,正解是全链路保持字符串,让数据库协议去做类型匹配。