上周处理一件小事:把接口导出的订单数据交给运营做对账表。数据是 JSON 数组,直接贴进在线转换工具生成了 Excel,发出去半小时后运营找回来:前两行的订单号一模一样,财务系统按单号对账直接撞车。
我回头核对原始 JSON,两条订单的 order_no 分别是 202609031200301001 和 202609031200301002,明明是两个数。转出来的 Excel 里,这两行全变成了 202609031200301000。中间隔了半小时排查,最后发现这事和转换工具关系不大------数据在进入 Excel 之前就已经错了。把整个过程记下来,这个坑比想象中容易踩。

现象:错在解析那一步,不在 Excel
先把现象固定下来。用一份五行的订单数据做验证,每行的 order_no 都是 18 位整数:
json
[
{ "order_no": 202609031200301001, "sku_code": "SKU0007", "goods_name": "机械键盘 87键 蓝牙双模", "qty": 2, "unit_price": 199.0, "paid": true, "coupon": null, "created_at": "2026-09-03 12:00:31" },
{ "order_no": 202609031200301002, "sku_code": "SKU0007", "paid": true, "coupon": 20.0, "created_at": "2026-09-03 12:05:18" },
{ "order_no": 202609031422180003, "sku_code": "SKU0012", "paid": false, "created_at": "2026-09-03 14:22:18" },
{ "order_no": 202609031630510014, "sku_code": "SKU0033", "paid": true, "coupon": 50.0, "created_at": "2026-09-03 16:30:51" },
{ "order_no": 202609041001220025, "sku_code": "SKU0041", "qty": 3, "paid": true, "created_at": "2026-09-04 10:01:22" }
]
贴进转换工具后,预览表格里的订单号就已经全变了。用浏览器控制台可以直接复现:
javascript
JSON.parse('{"n": 202609031200301001}').n
// 202609031200301000
也就是说,JSON.parse 把这串文本读成 JavaScript 数值的那一刻,值就不是原来那个数了。工具前端用的同样是 JSON.parse,所以预览表格里的订单号已经不可信,后面导出成什么格式都救不回来。

为了排除"Excel 显示层"的嫌疑,我把导出的 xlsx 用 openpyxl 读回,直接看单元格里的存储值:

五条全部对不上,而且前两条在文件里就是同一个值。结论:不是 Excel 把数字显示错了,是写入的源头就是错的。...1001 和 ...1002 舍入后都是 ...1000,两个不同的数塌成了同一个数------这比"显示不全"严重得多,因为没有任何报错,下游根本不知道数据坏了。
为什么是 53 位:float64 的尾数边界
JavaScript 的 Number 按 IEEE 754 双精度(float64)存储,结构是 1 位符号、11 位指数、52 位尾数(加上隐含的首位 1,有效精度 53 位)。53 位二进制能精确表示的整数上限是:
2^53 = 9007199254740992
这是 16 位数。18 位的订单号远超这个边界,超出之后浮点数只能按"最近的可表示数"取整。相邻的两个订单号 ...1001 和 ...1002,在 2^53 附近的数轴上,可表示数的间隔已经是 16 甚至 32,两个数落在同一个间隔里,就舍入成同一个值。
这也是为什么各种"最佳实践"反复强调:超过 2^53 的整数不要用 Number 类型承载 。雪花算法 ID(64 位)、18 位统一社会信用代码里的数字段、时间戳乘上随机数生成的订单号,全都落在这个雷区里。Java 后端用 Long 表示雪花 ID 没问题,但序列化成 JSON 给前端时,负责任的团队会把它写成字符串------Jackson 的 WRITE_NUMBERS_AS_STRINGS、Fastjson 的 @JSONField(serializeUsing) 这类配置存在的原因就在这。我见过有人把这种配置当成"前端兼容性玄学"删掉,现在算是补上了它防的是什么的实证。
还有一个容易混淆的层:Excel 单元格的数字按规范存 double,界面上只保证 15 位有效数字的显示精度。所以就算精度没在解析时丢,一个 18 位数字进了 Excel 也会显示成科学计数法或者被截断显示。也就是说,长数字过 Excel 这条路有两道精度关:JSON 解析是硬丢失(值永久改变),Excel 显示是软丢失(值还在,界面上看不全)。这次两道关都撞上了。
顺带验证的类型映射
精度之外,这次转换还把几种 JSON 类型在 xlsx 里的落点暴露得很清楚(同样用 openpyxl 逐格读回确认):
| JSON 里的写法 | xlsx 单元格实际存储 | 观察 |
|---|---|---|
202609031200301001(18位整数) |
数值 202609031200301000 |
精度丢失,值已变 |
"SKU0007"(字符串) |
文本 SKU0007 |
原样保留 |
true / false |
文本 "true" / "false" |
布尔降级成文本 |
null |
空单元格 | 无内容 |
199.0 |
数值 199 |
.0 不存在了,float64 里 199.0 与 199 是同一个数 |
"2026-09-03 12:00:31" |
文本原样 | 没有被转成日期序列值 |
几个点值得展开一句:
199.0变199不算丢数据------float64 里这两个字面量本来就是同一个值,只是 JSON 文本层面的写法差异在表格里被抹掉了。但如果是金额场景,"两位小数"的展示语义就没了,做表的人需要自己把单元格格式设回两位小数。- 布尔值变成文本
true/false之后,在 Excel 里就不能再用=IF(F2, ...)这种逻辑判断了,得写成=IF(F2="true", ...)。用这种表做二次加工的人会莫名其妙踩一下。 - 时间戳字符串没有被自动识别成 Excel 日期(没有变成 5 位日期序列数),这反而省事------不会出现"转完日期全变成 45000 多"的经典问题。
规避办法
按数据流向,责任和手段都清楚:
- 源头(最优):后端序列化时就把长 ID 写成字符串。这一步做了,后面所有环节都安全。凡是"这串数字丢了会出事故"的值(订单号、支付流水号、雪花 ID、证件号码段),都应该按这个标准对待。
- 既有数据的补救 :拿到手的 JSON 已经是数字型长 ID 时,先在文本层面给它们加引号再解析。字段名固定的话,一条正则就够:
"order_no":\s*(\\d{16,})替换成"order_no": "$1"。改完再转,16 位以上的数字全程按文本走。 - 进 Excel 时:就算值是对的,长数字列也应该先设为文本格式再粘贴/导入,避开 15 位显示精度那一关。Excel 的"数据→分列"里选"文本"列格式,就是干这个的。
小结
这次排查的最终结论很简单:JSON.parse 在 2^53 边界外就有损,转换工具的预览和产物只是如实呈现了已经坏掉的数据;Excel 的 15 位显示精度是叠加的第二道关。判断"我的数据会不会踩",只需要看一眼最长的纯数字字段有没有超过 16 位。
验证过程我用的是工具派上的 JSON 转 Excel 工具,把上面那份订单数据完整跑了一遍,产物 xlsx 用 openpyxl 读回逐格核对,顺手记录一下。