Files
blog/content/posts/never-worked-systems.md
T

153 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 或凭证。*