返回博客

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 上静默截断,查询无异常却查不到数据

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 = 已超出安全范围
正解:PHP 用 empty() 替换 intval(),JS 保持字符串,后端 JSON 显式 string 化

雪花 ID 全链路保持字符串语义:PHP 不转型、ThinkPHP query builder 接受字符串 WHERE 参数、JavaScript 不解析为 Number、JSON 响应显式转 string。零额外成本,无需改 DB schema,彻底规避两端截断。

一句话外卖

Snowflake ID 是字符串语义的整数——PHP 的 intval() 和 JS 的 Number() 都会静默截断它,正解是全链路保持字符串,让数据库协议去做类型匹配。