返回博客

PHP 返回雪花 ID,前端点编辑却 404:JSON 大整数精度丢失全记录

某后台管理系统列表页一切正常,点某行「编辑」按钮,后端返回 404「记录不存在」。刷新列表,那条记录又好端端地显示在那里。开 Network 面板一对比,发送编辑请求时携带的 ID 和列表接口返回的 ID……看起来一模一样,却差了几个数字。

这就是 IEEE 754 双精度浮点数在 JSON 里埋下的无声地雷。

现象

  • 列表接口正常返回,每行都有 ID
  • 点击编辑,弹窗调用详情接口,后端返回 404
  • Network 对比:列表响应 "id": 2030718534015598593,详情请求 URL 里是 ...598600——末尾悄悄多了 7
  • Console 没有任何报错,JSON.parse() 没有抛出异常,一切看起来正常

根因

Java 后端使用雪花算法生成 ID,典型值为 19 位整数,远超 JavaScript 安全整数上限:

Number.MAX_SAFE_INTEGER = 9007199254740991  // 16 位
雪花 ID               = 2030718534015598593 // 19 位,超出 2^53-1

PHP json_encode() 会如实输出完整数字字面量,JSON 规范本身也没有精度限制。问题出在浏览器:JSON.parse() 把所有 JSON 数字都用 IEEE 754 double 表示,超出 2^53-1 的整数会被静默舍入。

完整链路:

DB bigint              PHP json_encode()             JS JSON.parse()
2030718534015598593 → {"id":2030718534015598593} → 2030718534015598592
                                                    ↑ 差 1,且无任何错误提示
坑:JSON_BIGINT_AS_STRING 救不了你

看到文档里有 JSON_BIGINT_AS_STRING 常量,以为加上就能解决:

json_encode($data, JSON_BIGINT_AS_STRING);

实际上这个 flag 只影响 json_decode(),对 json_encode() 毫无作用。PHP 的 json_encode() 本来就会输出完整整数——精度丢失发生在浏览器端,不在 PHP 端。加了这个常量等于什么都没做。

正解

方案 A:PHP 后端统一转字符串(推荐)

在响应序列化的递归转换函数里,检测超出 JS 安全范围的整数并强转字符串:

// 递归转换 key/value 时,在值处理分支加:
if (is_int($value) && ($value > 9007199254740991 || $value < -9007199254740991)) {
    $value = (string) $value;
}
$result[$newKey] = $value;

ThinkPHP 项目通常放在 app/common/controller/Api.phpconvertKeysToCamelRecursive() 方法里。改完后 JSON 输出变为:

{"id": "2030718534015598593"}

字符串类型,JavaScript 原样保留,无精度丢失。一处修改,全部接口生效。

方案 B:前端用 json-bigint 库

import JSONBig from 'json-bigint'
 
const instance = axios.create({
  transformResponse: [data => {
    try { return JSONBig.parse(data) } catch (e) { return data }
  }]
})

优点:保留数值类型,可做算术。缺点:每个 axios 实例都要配;BigInt 值回传时还需手动 .toString()

正解:后端把大整数序列化为字符串

字符串 ID 在前端的常见用途全部安全:=== 比较、POST body 参数、URL 拼接、v-for :key、SQL 绑定参数。唯一不兼容的是直接做算术——如果有此需求,用 BigInt()localeCompare 排序。

快速自查

在浏览器 Console 粘贴列表接口返回的一个 ID:

const id = 2030718534015598593
console.log(Number.isSafeInteger(id))  // false → 中招了
console.log(id === 2030718534015598593) // true → JS 自己都骗了自己

如果 isSafeInteger 返回 false,且列表 ID 和详情请求 ID 在 Network 里肉眼看上去相同但数值不一致,基本可以确诊。

一句话外卖

只要后端有 Java 雪花 ID 流经 JSON → 浏览器的链路,就必须在序列化层把大整数转字符串,JSON.parse() 的静默截断不会给你任何报错。