diff --git a/content/posts/never-worked-systems.md b/content/posts/never-worked-systems.md new file mode 100644 index 0000000..7aa9807 --- /dev/null +++ b/content/posts/never-worked-systems.md @@ -0,0 +1,152 @@ +--- +title: "休假归来:四个\"从未生效\"的系统" +description: "休了几天假回来,一整天都在跟'静默失败'搏斗:超时预警从未推过、KAP联系人从未匹配上、短租路由在假装成功、大鱼工单派错了区域。它们的共同点——坏了,但没人知道。" +date: 2026-08-18T18:07:36+08:00 +draft: false +tags: + - 运维 + - 可观测性 + - 派单 + - 预警 + - 排障 +--- + +## 背景 + +休完假回来,翻看这几天的工单记录,发现了一个让我后背发凉的规律:**今天处理的四个大问题,全是"从未生效"的系统**。 + +不是"出了 bug",是**"从来没工作过,而所有人都以为它在工作"**。这种失败最可怕——因为它不叫,所以没人知道它坏了。 + +## 一、23 小时超时预警:从未推送过一次 + +### 现象 + +预约维修 + 凌雄配送的工单,超 23 小时未关单要预警。用户问:任务正常吗? + +### 排查:4 层根因,层层都让它"静默死掉" + +| 层 | 根因 | 后果 | +|----|------|------| +| ① | `woopStatus=RECEIVED,OSERVE` **逗号分隔无效**(HALM API 不支持) | 查询返回 0 条,**从未推送过** | +| ② | 未过滤配送方式(要求凌雄配送) | 该筛的没筛 | +| ③ | 路由用 `region.cities` 但 engineers.json **cities 全空** | 永远走 fallback,路由是摆设 | +| ④ | 无逐状态查询日志 | 连"为什么没推送"都查不到 | + +一个预警任务,从根上就断了。**如果连日志都没有,它死了十年也没人知道**。 + +### 修复 + +1. **8 个状态逐个查询**(CHECKOUT/WRECEIVE/WSERVE/...),不再依赖不支持的逗号语法 +2. 凌雄配送过滤 + 城市群路由(city_aliases + 精确/包含匹配) +3. 每状态查询日志 + ops_logger 接入——**可观测性补上** + +### 效果 + +- 真推测试:**29 条超时工单全推送成功**,按城市群 @最后更新人,timeout_pushed.json 去重 +- 从"从未生效"到"真的在干活"——只差一层诚实的日志。 + +## 二、KAP 联系人:字段名 bug 让匹配"从未成功" + +### 现象 + +KAP(神州邦邦)派单要匹配联系人,发现联系人列表匹配永远失败。 + +### 双层根因 + +1. **字段名 bug(7/29 至今从未生效)**:KAP 联系人接口返回字段是 `name`/`phone`(脱敏如"唐*"),代码却读 `contactsName` → **恒为 None**,匹配永远失败 +2. KAP 没有"写联系人"的 API,只能靠 `save` 接口 + +### 最深的坑:`save` 的真实语义 + +用「最小对立实验」验证后真相大白: + +> **KAP `umCustomer/save` 按 `name` 匹配客户,传入的 `id` 被忽略!** + +- 传完整名 → 命中同名客户,**更新它** ✅ +- 传脱敏名(`北京搜****`)→ 匹配不到,**新建垃圾客户** 🔴 + +第一版修复就栽在这:传了脱敏名,新建了垃圾客户,修复反而失效。 + +### 修复 + +- `name` 传 detail 接口的**完整名** + 响应 id 交叉校验(id 不匹配 → warning 降级) +- 快路径:detail.contactsName 完整主联系人名,HALM 联系人==主联系人时零 API 调用 +- 电话号码归一化(去非数字/+86/0086),防同名不同号导致联系人列表无限膨胀 + +**教训**:外部 API 写操作的语义,必须用「最小对立实验」验证——**「code=0 + 单次生效」可能是巧合命中,不能证明你理解了机制**。 + +## 三、短租路由:在"假装成功" + +### 现象 + +WO18B2608170032 短租工单(重庆,维修站=成都短租),没走路由,降级成了人工选择。 + +### 根因 + +短租路由只按 `city_routes` 的 **9 个精确城市**匹配(北京/上海/广州/深圳/武汉/成都/杭州/南京/厦门)。重庆不在列表 → no_target → 降级。 + +但业务事实是:**成都分公司覆盖四川、重庆**——这是分公司覆盖区域,不是精确城市。 + +### 修复(3 级路由) + +用户给了权威分公司覆盖表,配成 37 条覆盖区域反向映射: + +``` +route_city 三级解析: +① 精确城市在 city_routes → 直接用 +② region_routes 覆盖区域(重庆→成都) +③ 维修站名兜底(成都短租→成都) +``` + +- 重庆→成都 / 东莞→深圳 / 苏州→上海 / 南昌→武汉 / 乌鲁木齐→北京 全部正确 +- 真实 dry-run:眉山/成都 → 成都短租组 ✅ + +### 复盘发现:静默假成功 + +顺藤摸瓜发现更阴险的:**短租组匹配全失败时,脚本用 `_ok` 记录成功,但 matched_res=None**——工单根本没派,脚本却说"成功了"。 + +修复:组匹配全失败 → 维修站名兜底 → 兜底也失败 → `_fail(short_rent_no_group)` **明确报错**。 + +> **假成功比失败更危险。** 失败会喊,假成功会让单子静默地卡在原地。 + +## 四、大鱼工单:派到了"大连区域" + +### 现象 + +WO10A2608180177 合肥工单,派到了大连区域。 + +### 根因 + +配置错误:旧规则 `合肥=都佳明`,但都佳明的服务站点在**甘井子区一辉网络**——甘井子区是大连的区!工单地址是合肥(对),但**工程师的服务站点在另一个城市**。 + +新规则(用户确认):太原=张欢欢 / 沈阳=都佳明 / 合肥=苏周。 + +### 教训 + +排查"派错区域",光看工单地址是不够的——**要看指定工程师的 dispatchSiteName(站点名含区县)**。一个工程师可以"归属合肥"但"驻在大连"。 + +另外:改 JSON 配置必须**精确字符串替换**,不能用 `json.dump(indent=4)`(会重排整个文件,diff 从 4 行变成 112 行)。 + +## 五、方法沉淀:怎么防"静默失败" + +今天四个问题,共同点是**"坏了但没人知道"**。防它的手段只有一个:**可观测性**。 + +| 防静默失败的手段 | 今天的例子 | +|----------------|-----------| +| **每步都有日志** | timeout_alert 补逐状态日志,从"查不到"到"看得见" | +| **成功要有证据** | 短租路由 `_ok` 必须有 matched_res,没有就是失败 | +| **接口语义要验证** | KAP save 最小对立实验,不信任 code=0 | +| **配置变化要可控** | JSON 精确替换,避免大 diff 掩盖真改动 | +| **失败要喊出来** | `_fail` 明确报错,不静默降级 | + +## 总结 + +> 最贵的 bug 不是崩溃,是**从没生效过的功能**——它不叫,所以没人知道它坏了,而业务一直以为它在那里挡着。 + +休假归来第一天,我修了四个"从未生效"的系统。它们教会我一件事: + +**可观测性不是锦上添花,是系统能不能被信任的底线。** 一个没有日志的预警任务,跟没写是一样的。 + +--- + +*本文已脱敏,不含真实域名、IP、UUID 或凭证。*