经验分享

ID 明明存在,为什么 404?你的雪花 ID 正被浏览器静默改写

19 位雪花 ID 经 JSON.parse 会被静默改写末位,新建记录的编辑/删除接口全部 404,而老数据毫无异样。本文记录"全局一律转字符串 → 逐字段注解 → 全局按值域序列化"三种方案的完整演进、注解方案的否决依据与回归测试设计。

—— Long 序列化三种治理方案的演进与取舍

Long 序列化三种治理方案对比

背景:一个 Java 21 + Spring Boot 4(Jackson 3)+ MyBatis-Plus 雪花主键的前后端分离项目, 客户端形态包括 PC 管理端、门户 Web、小程序/UniApp 移动端。本文记录该问题在一个真实 项目里的三次方案演进:全局一律转字符串 → 逐字段注解(被否)→ 全局按值域序列化(最终), 并给出决策依据与回归测试设计。代码示例均可直接复用。

TL;DR

  1. 超过 2^53-1 的整数经 JSON.parse 会被静默改写末几位,雪花 ID 必须以字符串形式下发;
  2. "所有 Long 一律转字符串"修复了 ID,却把计数/分页/耗时拖下水——前端被迫维护字段名 白名单逐个还原,漏一个就是静默算错;
  3. 逐字段注解(@JsonSerialize)显式性好,但它是"默认不安全、靠自觉",且 Map 直返等 场景根本挂不上注解;
  4. 最终解:全局按值域序列化——|v| ≤ 2^53-1 输出数字,超出输出字符串。一处配置, ID 与计数同时正确,前端不再需要任何补丁;
  5. 注解不必扔掉:Jackson 注解优先级高于全局模块注册,留作特例覆盖手段。

一、问题:19 位 ID 被浏览器悄悄改写

MyBatis-Plus 雪花算法生成的主键是 19 位 Long(形如 2099404618435072002),而 ECMAScript 的 Number 只能精确表示到 2^53-1(9007199254740991,16 位)。后端若把 ID 按 JSON 数字 下发,浏览器 JSON.parse 时会不报错地改写末几位:

JSON.parse('{"id": 2099404618435072002}')
// { id: 2099404618435072000 }   ← 末尾 002 变 000,无任何异常

前端再拿这个 ID 去调编辑/删除接口,必然 404。更阴险的是暴露条件:存量数据的主键往往是 自增小数字(344、1530),一切正常;缺陷只在新插入的记录上出现。这类"测试环境 数据旧所以测不出来"的问题,通常要等 E2E 全链路自测才会现形——本项目正是如此。

二、为什么前端补丁治不了根

受影响的是所有消费 JSON 的客户端:PC 管理端、门户 Web、小程序、App、未来的第三方 脚本。在每个客户端里各写一层"ID 字段转字符串"的拦截器毫无意义——消息在 JSON.parse 那一刻已经丢精度了,事后再 toString 补救的字符串是错的。

所以修只能修在后端序列化这一处。问题只剩下:怎么修。

三、方案 A:全局一律转字符串(第一版)

最常见的做法,注册全局序列化器把 Long 一律输出为字符串:

SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);

ID 问题彻底解决,且是默认安全:新增任何 VO/字段都不需要记得任何事。

但副作用很快出现——计数、分页 total、文件字节数、耗时这些小值 Long也成了字符串。 前端一旦做算术就是字符串拼接,而且极具迷惑性:

"46" + 0    === "460"    // 静默算错
"46" - 0    === 46       // 恰好对,所以很难发现
"150" >= 46 === false    // 按字典序比较

我们的管理端为此维护了一个"字段名白名单"还

原创不易,完成人机校验,阅读全文

相关推荐