ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

订单号查不到?长整型ID精度丢失的排查与修复全记录

订单号查不到?长整型ID精度丢失的排查与修复全记录 接手过线上工单的人都知道最让人头皮发麻的不是报错堆栈而是一串看起来人畜无害、却怎么都查不到的数字。上周我就碰上这么一单运营同学的工单里写着一行“订单号为 111111113用户反馈下单成功但订单查不到”。我第一反应是去订单表里搜这个单号结果连索引都没走到直接返回空。这时候我意识到问题可能不在数据层而在“这串数字是怎么到达数据库的”。订单业务我以前也排查过不少基础判断是先确认这单是不是真的存在。我用用户手机号把订单列表调出来发现用户的订单确实存在创建时间也对得上但真实订单号是一串19位的雪花ID前缀正好是111111113后面还跟着一大截。看到这里我基本已经确定方向了这不是数据丢了是精度丢了。这串“111111113”之所以值得写是因为它太典型了——一个长整型ID在某个环节被截断、重写或人工抄录时丢失了剩余位数最后剩下一个看似合法、实则残缺的数字。如果你也维护过订单、用户、支付流水这类强ID系统大概率遇到过类似的坑数据库里明明有值用完整ID查得到可用户复述的、Excel里看到的、接口返回的却对不上。这篇就把这条链路从头到尾捋一遍讲清楚精度是在哪一段丢的、怎么定位、怎么彻底修。1. 这串“111111113”是从哪条路径跑到工单里的1.1 第一轮排查直接查库查了个寂寞拿到工单里的数字我没有马上去翻前端代码而是先做了一件最基础的事拿这个数字去订单库验证它到底存不存在。SELECT * FROM t_order WHERE order_no 111111113 LIMIT 10;结果很干脆空。整个订单表里没有一条订单的订单号是纯“111111113”。这里有个细节值得说明订单表的主键和业务订单号是分开的主键是自增ID业务订单号是雪花ID生成的bigint。工单里填的是业务订单号所以第一步就用订单号精确匹配。精确匹配没有我换了个思路用户反馈的是“下单成功但查不到”那么订单状态可能不是成功或者被分表分到了别的地方又或者他提供的订单号本来就不完整。我决定用模糊查询再探一次。SELECT * FROM t_order WHERE order_no LIKE 111111113% ORDER BY create_time DESC LIMIT 10;这次有结果了。订单表里确实存在两个以111111113开头的订单号完整长度都是19位创建时间也和用户反馈的时间段对得上。到这里基本能确认订单没丢业务数据都正常问题出在“订单号的传递过程”——用户拿到的那个数字不是数据库里的完整数字。1.2 用用户维度反查真实订单线索一下就串起来了光靠模糊查询还不够我做了一次反向确认用用户ID和手机号把该用户最近的订单列表全部拉出来逐条对照。查询结果里有一条订单完整订单号是1111111137524485120前缀和工单里的数字完全一致后面那几位是真实存在的。也就是说用户手里那个“111111113”是从这个19位真实ID里“砍掉”了后面一截只留下了前缀。这种前缀一致、后续位数缺失的情况最常见的来源有三个用户在某处看到的数字被科学计数法显示成了“1.11111E18”于是凭记忆只记了前面几位页面或IM复制时长数字被客户端做了省略展示比如“1111111137…120”这种用户只复制到省略号之前客服在Excel里双击单元格看到的是尾数被填了0的假ID手工记录时又只取了前面一段。前两个是展示层的锅第三个是导出/表格工具的锅。我后来通过用户反馈的截图确认他是从一条推送消息里复制的订单号推送文案对ID做了截断展示复制时把截断后的内容带走了。这个“111111113”就是截断后的结果。1.3 一串数字暴露出来的问题类型看到“短一截”先怀疑精度我在排查过程中有个习惯拿到一个数字先看它的位数和尾数特征。如果它是一个超过15位的长整数而工单里的数字位数明显不足或者末尾出现了大段的0我基本会先往“精度丢失/截断展示”这个方向走。为什么是15位而不是16位、17位因为Excel单元格默认的数字精度就是15位有效数字超过15位的部分会被直接写为0。而JavaScript的Number类型精确整数范围是2^53-1也就是9007199254740991共16位。这两个关键阈值基本覆盖了绝大多数“长数字变短、变假”的现场。所以当看到“111111113”这种以真实ID前缀开头、但位数不够的数字与其在数据库里反复全量扫描不如先确认用户在哪个界面拿到这串数字顺着用户的视角走一遍数据链路。很多坑不是数据层的问题是“数字在不同载体间转移”时被悄悄改写了。2. 精度是在哪一段丢的JavaScript、JSON、Java Long 的三角关系2.1 JavaScript 的精度上限比想象中低先补一个最基础的概念很多人都知道JavaScript的Number是双精度浮点数但没意识到它对整数的限制有多强。双精度浮点用IEEE 754标准存储64位里只有52位用来存尾数再加上隐含的一位最多能精确表示2^53-1以内的整数也就是9007199254740991。超过这个值整数就开始“按位舍入”了。最经典的例子是// 在浏览器控制台里执行 console.log(9007199254740993); // 输出 9007199254740992你写了一个9007199254740993JavaScript打印出来变成了9007199254740992。没有报错没有异常就是一个很安静的精度偏移。对业务系统来说这比直接报错更可怕——用户看到的订单号、流水号和数据库里的真实ID从某一位开始就悄悄对不上了。2.2 Java Long 和 JSON 数字字面量之间的裂缝后端系统里订单号一般用Java的Long类型承载数据库里对应bigint都是64位有符号整数完全能存下雪花ID。问题出在把Long输出给前端的那一刻。大多数Spring Boot接口默认用Jackson做JSON序列化。一个Long类型字段默认会序列化成JSON里的数字字面量。而前端拿到JSON后如果是axios、fetch这类工具会直接对响应体做JSON.parse()JSON里的数字字面量最终会被解析成JavaScript的Number。这一下就出事了。Java Long能精确表示的整数JavaScript Number不一定能精确表示。尤其是雪花ID这种动辄19位的数字超过2^53-1是家常便饭。于是后端返回的是1111111137524485120前端JSON.parse()之后实际拿到的可能是1111111137524485100末位悄悄变了。我见过不止一次类似场景前端开发拿着接口文档说“后端返回的就是这个数”后端指着数据库说“我数据库里明明是另一个数”两边都觉得自己没错实际上是JSON数字字面量这个中间表达方式把精度弄丢了。2.3 一张表看清整条链路的精度能力差异为了排查方便我把这条链路上每个环节能精确表示的整数上限列了个表环节类型/载体能否完整表示19位雪花ID典型后果数据库BIGINT64位整数能存储没问题Java后端Long64位整数能内存和日志没问题JSON响应数字字面量无类型标记文本上能表示解析时看接收方文本没问题解析有风险前端解析NumberIEEE 754双精度不能完整表示尾数被舍入ID变假前端显示字符串渲染取决于底层值页面展示的是已经丢精度的值Excel/CSV15位有效数字不能完整表示科学计数法、末位补0人工抄录肉眼记忆可能截断得到“111111113”这种残缺ID从这个表能直观看到同一串数字在数据库、Java后端、JSON文本里都能原样保留但一旦落到JavaScript的Number或者Excel的单元格里超过各自精度上限的部分就可能被改写或吃掉。很多问题不是“某一段代码写错了”而是“数字在链路里的表示能力天然不一致”。3. 还有两个隐蔽的精度黑洞导出报表和人工复制3.1 CSV导出后Excel让订单号变成了科学计数法排查完接口层之后我顺手检查了后台报表导出功能果然也有类似问题。运营同学日常会从后台导出订单明细CSV在本地用Excel打开订单号那一列经常显示成1.11111E18这种科学计数法形式。为什么会这样CSV本质是纯文本文件里面存的是完整ID。但Excel打开CSV时会自动把看起来像数字的单元格按“数字”处理默认精度是15位有效数字。一位19位的雪花ID从第16位开始全部被当成无效精度抹掉变成0。你随手打开一个带长数字的CSV大概率会看到类似下表的情况原始IDExcel里显示实际存储值11111111375244851201.11111E18111111113752448000011111111375245230001.11111E181111111137524520000用户在Excel里看到的已经不是真实ID了复制出来的值末尾全是0。如果运营同学再把这个复制后的值填进工单或者发给用户核对那错误就继续往下传。这不是Excel单方面的问题而是长ID在“非技术载体”里的通用困境。就算你把它输出成PDF、打印成纸质单、发到IM里只要有人试图通过一个不支持19位数字精度的工具打开就有被改写的风险。3.2 从真实ID到“111111113”的最后一步人工截断在这次的工单里完整路径是这样的真实订单号1111111137524485120先在某个推送文案里被做了截断展示展示成类似“尾号7524订单号1111111137…”的效果用户复制时复制到了截断后的内容又以文字形式发给了客服。客服填工单时把那段截断文字里最像ID的部分填了进去就得到了111111113。这个过程里每一环都在“丢失信息”但每一环的人都没做错什么用户只是复制了看到的内容客服只是如实记录了用户提供的内容。真正的问题在于最初那一步就不该让长ID以“可被截断、可被误解”的形式出现在用户面前。所以排查这类工单时我不建议大家只盯代码。如果前端、后端都检查过数据都是完整的那就要去检查所有“ID离开系统后经过的路径”推送文案、短信模板、邮件通知、客服手工整理的话术、Excel导出的实体表格。只要某条路径存在截断或格式转化就可能在某个节点生产出一个像“111111113”这样的假ID。3.3 什么样症状出现时可以直接锁定这类问题我把这些经验默认成了几项“快速诊断”条件命中两条以上基本就是精度/截断问题在作祟ID位数明显少于系统定义值比如系统都是19位工单里只有9位ID末尾出现整段0复制出来和数据库里对不上长ID被显示成科学计数法或者带E字样能通过前缀模糊查到数据但用用户提供的完整数字精确查不到同一个ID在不同页面/不同导出文件里显示不一致。这次工单的“111111113”最初只命中第一、第四条但也足够让我快速切入正确方向。很多时候问题不是出在某个系统写错了而是ID在某一环被“人肉加工”过。4. 一次性修复不能只改一处全链路的处理方案4.1 后端优先所有ID类型字段统一序列化为字符串接口层最稳的做法不是靠前端去猜而是后端在序列化阶段就把Long类型的ID变成字符串。这样前端拿到的永远是一段文本不会触发JavaScript Number的精度转换。单个字段可以用Jackson注解public class OrderDTO { JsonSerialize(using ToStringSerializer.class) private Long orderNo; private Long userId; }但真实项目里ID字段散落在几十上百个DTO里一个个加注解太容易漏。我通常会在全局ObjectMapper里做配置把Long和long统一处理Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer longToStringCustomizer() { return builder - builder .serializerByType(Long.class, ToStringSerializer.instance) .serializerByType(Long.TYPE, ToStringSerializer.instance); } }这么配完所有接口返回的Long字段都会变成字符串。但这同时也意味着接口契约变了前端的取值逻辑全部要跟着变。之前是orderNo数字现在是字符串所有强转数字、加减乘除的地方都得排查一遍。上这个配置之前一定要让后端列出所有受影响接口和前端对齐否则会引发一大批类型错误。还有一个常见疑问金额字段是不是也会被转成字符串我的做法是金额字段不用Long用BigDecimal或Integer单位分这样就不会被全局规则误伤。如果你的系统里有人用Long存金额那全局配置就得改成只对特定包路径或自定义注解生效避免把金额也变成字符串。4.2 前端适配BigInt只能解决一部分问题如果后端短时间来不及改前端有没有临时方案有但有限。JavaScript里可以用BigInt表示任意精度整数但JSON.parse()不会自动把超大数字解析成BigInt默认还是Number该丢精度还是丢。要让JSON.parse保留精度得换解析库比如json-bigint或者在后端返回字符串的前提下直接BigInt(1111111137524485120)。// 后端返回字符串后前端可以直接安全使用 const orderNo BigInt(response.orderNo); console.log(orderNo.toString()); // 1111111137524485120但BigInt不是银弹。它不能直接和普通Number混用运算序列化回JSON时也要手动toString()如果项目里有大量历史代码改造成本并不低。所以从我实际经验看治本方案还是后端统一给字符串前端只做展示透传不在前端对ID做任何数值运算。ID本来就只是一个唯一标识谁也不该拿它做加减乘除。4.3 导出和Excel环节别再让长ID进默认数字格式导出CSV的场景需要在写文件时对ID列做特殊处理。CSV里给字段加上制表符前缀可以诱导Excel按文本识别。一些常见的做法如下order_no \t1111111137524485120注意这里是反斜杠转义的制表符实际写入文件时是一个Tab字符而不是字面的\t。用这种方式导出的CSVExcel打开后通常会把ID识别为文本不再触发科学计数法。用POI写xlsx的话更直接的办法是把单元格格式设置为文本CellStyle textStyle workbook.createCellStyle(); DataFormat format workbook.createDataFormat(); textStyle.setDataFormat(format.getFormat()); cell.setCellStyle(textStyle);这个格式就是Excel里的“文本”格式强制把内容按字符串展示。我见过一些项目只给ID字段加双引号就完事但CSV双引号只能保证内容里的逗号和换行没问题Excel还是可能把数字当数字处理。所以最佳实践是能导xlsx就导xlsx并预置文本列必须导CSV就用制表符前缀同时在文档里标注“订单号请以文本格式查看”。4.4 产品层面的兜底减少“人肉传ID”的场景技术修完还要堵住“用户和各角色手工传ID”的入口。我这次的处理方案里有三条产品侧的改动很有效推送文案和短信模板里不再展示完整ID改为“订单号尾号7524”并提供跳转链接用户点击直接进订单详情客服后台订单列表增加“按用户ID时间段金额”联合搜索不强制依赖完整订单号客服工单系统把订单号输入框做成“模糊查询自动补全”输入前缀后展示候选订单客服从候选里选而不是让用户抄一串长数字。这三条改完后用户和客服都很少再手工接触完整ID精度问题在源头就少了一大半。纯靠后端和前端修只能保证系统内部不出错产品层面把“ID暴露给人工”的路径收窄才能避免类似“111111113”再次出现在工单里。5. 以后再看这类“短了一截的ID”排查链路可以这样走5.1 三步快速定位问题层级拿到一个可疑ID我建议按这个顺序排查能省很多时间先用完整ID精确查数据库确认它是不是真实存在的值查不到再用前缀模糊查确认它是不是“真实ID的一部分”。到后端请求日志里搜用户的操作轨迹看接口接收到的ID是什么、返回出去的ID是什么对比数据库原始值。打开浏览器控制台把ID作为字符串传进Number()跑一遍看输出结果和原值是否一致。如果不一致说明精度在JS层已经丢了。const rawId 1111111137524485120; console.log(Number(rawId) BigInt(rawId) ? 一致 : 不一致);这里的Number(rawId)会把字符串按Number解析如果解析结果和BigInt结果不一致就可以明确告诉团队前端拿到的数和数据库里的数不一样了。这套三步法基本能覆盖绝大多数精度类工单。定位的时间通常不超过半小时比从代码里盲翻高效得多。5.2 长期监控给长整型ID加上“精度体检”修复过后我一般还会在测试环境加一条自动检查对全量接口做响应体扫描凡是字段名包含Id/No/Code且类型是数字的自动比对原始JSON文本和JSON.parse之后的值有差异就报错。这样能提前发现新接口是否有“漏配的Long”。线上监控也可以做一层轻量的在网关层对响应里的ID字段做一个正则校验如果字段值是超过16位的数字就告警提示“疑似精度风险”。这类告警不一定要自动阻断但至少能让开发知道“这里还有一个未处理的Long”。我是这么想的长ID精度问题的根源在于“系统里同时存在两种精度表达能力”。只要还有人给某个Long字段漏加字符串序列化或者某条导出链路没做文本格式处理类似问题就会换个数字再次出现。加一道自动化体检比靠人肉review可靠得多。5.3 最后沉淀下来的工作习惯经过这次“111111113”工单我给自己定了几条习惯现在也一直在用凡是接口里出现ID相关字段不管位数长短默认先按字符串定义除非有确凿理由用数字凡是写导出功能看到Long类型字段第一时间问“这个字段会不会超过15位”会就按文本格式处理凡是客服、运营要手动记录的编号类信息一律不让他们抄完整ID通过链接或下拉选择来替代凡是在代码里看到Long和Number互传的边界都默认假设这里“会丢精度”然后去验证。这些习惯看着简单真能坚持下来能少踩很多坑。长数字精度问题不像空指针和死循环那么显眼它就是安安静静地在某个转换节点把数字改掉一点点等到发现时往往已经变成了用户手里那串怎么都查不到的“111111113”。希望这篇复盘能帮你下次更快锁定同类问题。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进