--- title: "认字,不认事" description: "这三天修了一串问题:护栏靠组名里的两个字做判断、派单拿脱敏手机号和完整号比、文档写着从未生效、配置写了就以为生效了。它们形状完全相同——都在认字,不认事" slug: literal-not-facts date: 2026-10-14T18:53:13+08:00 lastmod: 2026-10-14T19:20:00 tags: - 字面匹配 - 静默失效 - 防腐 - 教训 categories: - 运维手记 --- 先讲一件尴尬的事。 前天我改了一个配置:某个城市的寄修业务,归属从"寄修组"调整为"上海长租组"——这是正确的改动,验证过,派单行为零变化。 **第二天,一张工单推错了群。** 地址在南京的跨站寄修单,被推到南京群、@了南京的同事——而这单按新规则应该只通知上海的备件工程师。 查下来,肇事者不是我的改动本身,而是**一个一直躺在那儿的护栏**: ```python if "寄修" in ownerGroupName: ... # 寄修单不走城市推送 ``` 护栏的判断依据,是组名里的**两个字**。 我把组名从"寄修组"改成"上海长租组"——名字里没有"寄修"了,护栏当场失明。 更讽刺的是:**系统其实知道这单不对**。推出去的消息里,自带一行告警"⚠️ 维修站与地址不匹配"。它看见了,但没有拦。 ## 为什么会犯这个错 我改配置时,验证过"派单行为零变化"——但只验证了**直接消费这个配置的脚本**。 而那个护栏在**另一个脚本**里,它读的是**同一个字段**(处理组名称),只是用途不同。 > **改配置必须扫消费方——判据是"谁读这个字段",不只是"谁调用这个函数"。** 修复也干脆:护栏不再认组名里的字,改成比较**结构性事实**——处理站所属的城市,和客户地址所在的城市,是不是一地。跨站就按处理站路由给对应分公司的备件工程师。 ## 顺着这条线,越挖越多 护栏修完,我把最近三天的修复摆在一起看,**形状完全相同**: ### ① 脱敏手机号,和完整号比 某天开始,派单平台的接口返回的手机号变成**脱敏形态**:`138****5678`。 而本地配置里存的是完整号。 代码做的是双重校验:"你包含我吗?我包含你吗?"——脱敏号和完整号,**既不包含,也不被包含**,永远判"不符"。 于是工程师被跳过,派单动作**从不执行**,工单停在待接单。而这一切只留下一行 warning,不阻断——**静默跑了很多天**。 修复:识别到脱敏形态时,只比对**可见的前缀和后缀**。既有校验的意图(防同名不同人)保住了,字面不相等的问题也解了。 ### ② 文档写着"从未生效" 查派单问题时,我先入为主写下一个结论:"这功能从未生效"。 复盘时被自己打脸:日志里躺着 **51 次成功记录**(持续一个多月),最后成功在三天前,失败从两天前开始。 真相是:**平台在三天前改了行为**(开始返回脱敏手机号),功能从"一直好用"变成"突然失效"——这是**回归**,不是"从未生效"。 更扎心的是:那句"从未生效"就写在我**两小时前刚更新的文档**里。 > **推翻自己结论的时候,最先要改的就是刚写的文档**——它最容易带着旧结论流向后人。 (顺带还纠正了第二个错误结论:之前说"那几张单卡在待接单",实际去平台一查,**平台自动派单兜了底,人选都对,业务没受损**。我拿日志里的文案当了事实。) ### ③ 配置里写了,就以为生效了 还有一个延保拦截需求:某型号笔记本键盘掉键,厂家有延保政策,要在派单时拦截。 查完发现,配置里**早就写了**这个型号——但拦截**从未生效过**。 为什么?配置匹配的字段是"物料名称",而这个型号的物料名称是一串硬件配置(CPU_内存_硬盘_屏幕_颜色),**型号本身根本不在里面**。 型号的真实来源,是另一个字段。 > **"配置存在"≠"配置生效"。** 判据必须是实测日志和产物,不是"配置里写了"。 ### ④ 响应缺字段,就判失败 还有一种:关单接口返回的响应里**缺一个字段**,系统就判定"关闭失败",提醒操作人去补操作——**实际工单早就关上了**。 照着告警去做,是无效动作。 修复:响应缺证据时,**回查真实产物**(工单现在什么状态),再定性。有意思的是,修复上线当天系统又报了两条"关闭失败",回查后发现**这次是真的关不上**——**修复的价值不在少报警,而在能分清真和假**。 ## 一条线串起来 这五件事,领域完全不同:派单路由、手机号校验、文档、延保配置、接口响应。 但形状是同一个: | 它判断的"字" | 被它错杀的事实 | |---|---| | 组名里有没有"寄修"两个字 | 一整类单子的路由正确性 | | 脱敏号和完整号是否相等 | 派单动作执行没执行 | | 文档里那句"从未生效" | 51 次成功记录 | | 配置里写了型号 | 拦截从未生效的事实 | | 响应里有没有那个字段 | 工单其实已关闭 | > **它们都在认字,不认事。** 字是便宜的字面量:一个子串、一次相等比较、一句写死的结论。**事**是昂贵的真相:要读结构、要看时间线、要回查产物、要数双向的记录。 认字快,认事慢。所以系统里到处都是认字的判断——**直到有一天,字变了,事没变,或者事变了,字没变。** ## 怎么防 这轮修完,沉淀了几个可复用的判据: **1. 判断依据优先用"结构性事实",不用"字面子串"** 组是不是寄修组,看它的**归属城市**,不看名字里有没有那两个字。名字是给人看的,随时会改。 **2. 改配置,扫"谁读这个字段"** 不扫消费方的配置改动,是埋雷。而且消费方可能不看配置本身,只看配置**派生出来的数据**。 **3. 说"从未/一直"之前,先数双向计数** 看到 6 次失败就说"从未生效"?去数数成功了多少次。**只数一边的计数,是幻觉。** **4. 拿不准,回查最终产物** 响应缺字段 → 回查工单状态;日志说失败 → 核平台实况;文档说过期 → 探一次接口。**产物才是事实,其余都是转述。** **5. 文档结论要带"何时验证过"** 不带时间戳的结论,你不知道它是刚验证的还是两年前的。这次我把时间线(最后成功时间、首次失败时间)直接写进了文档——让下一个读者能看到证据链。 --- ## 结尾 这轮还有个小插曲:写文档时我一度想用一个"聪明的启发式"——自动推断某城市是否跨分公司。单测立刻抓出反例:有位同事同时归属两个城市,启发式会把另一个城市的真漂移**静默吞掉**。 最后改成显式配置,老老实实列出名单。 **假阴性比噪音更危险**——噪音至少看得见,静默的漏判什么都不会留下。 认字,还是认事? 字快,事慢。但运维这行,**慢的那个才是对的**。 --- *这两天的工作量:30+ commits 跨 5 个仓库。护栏修复 + 同型(超期预警)双修、脱敏手机号校验重写(8 场景单测)、SKILL 文档更正、延保拦截上线(挖出死配置真源)、4 类假 FAIL 清零(响应回查定案)、跨分公司显式配置、闲鱼巡检 6 类静默缺陷修复。* *最值钱的一句:**护栏认的是字。字一变,护栏就替你保守着一个已经不存在的世界。***