代码看着没毛病,但这链路从没跑过——显示屏推送排查的四次打脸
用户抛了一个看起来简单的问题:显示屏推送代码是不是没问题?线上能顺利保存「推送显示屏」的告警配置,但不确定真实告警触发时推不推得出去。排查后结论有点意外:这条链路从没真正跑过。更值得记录的不是结论本身,而是排查过程中我做的四次错误推断——每一次都被事实打脸。这篇诚实复盘整个过程。
结论先行
这条推送链路从没真正跑过。唯一配屏的处理器(水泵房监控,2023-04 建立)从未被触发,既谈不上「有问题」也谈不上「没问题」。代码层面看链路是通的,但一旦有触发机会,排障会非常困难——因为推送失败全部静默,零日志。
完整推送链路
排查第一步是搞清楚「推屏」从代码入口到最终物理屏穿了几层。最终确认是三层跳转:
1 | flowchart LR |
关键代码节点:
generalAlarmProcessing(line 152-154)——告警主入口。先 constructionParameters 构建记录,再判断 processorDTO.getAlarmingPushScreenList() 非空调 pushScreenDevice(list)。
pushScreenDevice(line 451-470)——遍历屏列表,每块屏调一次 Feign:
1 | ResultDTO<DhPlayPlan> result = pushDeviceClient.pushDevice(dto); |
PushDeviceClient(Feign 接口)打到 smart-service 的 /pushDeviceInfo/pushDevice,smart-service 内部再 HTTP 调大华平台 /dh/...,返回类型 DhPlayPlan,成功则收集 playId 拼成 screenPlanId。
排查过程:四次打脸
这一节诚实记录被纠正的过程。复盘的价值不在「最终我对了」,而在记录「哪些思维定式容易把人带歪」。
弯路 1:误判 list 恒为 null
我的推断:findOneByCondition 是基类方法、AlarmingProcessorServiceImpl 没重写它 → 触发时 alarmingPushScreenList 恒为 null → 屏永远不推。
实际错了:
listByCondition被AlarmingProcessorServiceImpl重写(line 77-84),重写内调processingDetails(line 174-201),后者用processorId二次查alarming_push_screen表填充 list。- 基类
findOneByCondition内部invokevirtual listByCondition是虚方法调用,动态分派到重写版本 → 触发链路的findOneByCondition会走到重写的listByCondition→ list 能加载,并非恒 null。 - mapper XML 无
<collection>是事实,但 list 由 Service 层二次查询填充,不能据此认定 list 为空。
教训:Java 动态分派。看到「基类调一个方法」不要假设它走基类版本。
invokevirtual一律动态分派,只要子类重写了就走子类。应该先 grep 子类有没有@Override,而不是直接下结论。
弯路 2:误用 screen_plan_id IS NULL 作为证据
我的推断:alarming_record.screen_plan_id IS NULL 可证明没推屏。
实际错了:constructionParameters 里的执行顺序是:
1 | alarmingRecordService.addSelective(alarmingRecordDTO); // line 145, insert |
insert 在前,set 在后,set 之后没有 update → 库里 screen_plan_id 恒为 null,与推没推无关。这根本不是一个有效证据。
顺序陷阱:看到「字段在库里是 null」先看 insert/update 的先后,再下结论。DTO 被 set 了不等于库里被改了。
弯路 3:错误的「广播能推、屏不能推」对比
我的推断:广播功能正常推、屏功能不推,形成对比,证明是屏的代码有问题。
实际错了:pushBroadcastDevice(processorId)(line 478-485)查完广播列表、collect 完 broadcastIds 后,方法就结束了——broadcastIds 没传给任何下游、没调推送接口。广播也是半成品,没真推。不是对比关系,是难兄难弟。
对比前提要先坐实:用作「正常参照」的那条链路,得先确认它真的正常,否则对比结论是空中楼阁。语音播报那条链路的完整拆解另见这篇。
弯路 4:猜列名 / 猜常量被现实打脸
猜列名:我猜 alarming_processor 有 processor_name 列 → 实际没有,SQL 直接报 Unknown column。
猜常量:我按直觉假设 screen_push_flag '1'=是 → 查常量才发现这个项目所有布尔字段反直觉。WarningConstant.java 里:
1 | SCREEN_PUSH_FLAG_YES = "0" // 是 |
这个项目的硬规则:看到任何布尔标记,必须先 grep
WarningConstant确认值,不能按常理猜。'0'= 是、'1'= 否,全部反直觉。

图 2:开关的取值和直觉相反——'0' 是开、'1' 是关,所有布尔标记全部反着来。
最终事实:基于 DB 查询 + 常量确认
弯路走完,回到数据库查事实。
查询 1:最近告警记录——alarming_record ORDER BY create_time DESC LIMIT 10。最近告警全是 ACCESS_CONTROL_MONITORING / EQUIPMENT_FAILURE(门禁设备故障),screen_push_flag='1'。按反直觉常量解读:'1' = SCREEN_PUSH_FLAG_NO = 没配屏。门禁故障处理器没配屏,不推屏是正常的。
查询 2:谁配了屏——alarming_push_screen 表,配屏的 processor_id 结尾是 ...107。
注意结尾 07 ≠ 14。门禁故障处理器
...114和配屏处理器...107前缀相似但完全是两个不同的处理器。排查时差点被前缀迷惑跳过。
查询 3:处理器 107 配置——按 BaseResultMap 列序对齐:
| 字段 | 值 | 常量解读 |
|---|---|---|
| biz_type | WATER_HOUSE_MONITORING | 水泵房监控 |
| alarm_type | DEVICE_ABNORMAL | 设备异常 |
| config_status | ‘0’ | CONFIG_STATUS_YES → 已启用 |
| threshold_status | ‘1’ | THRESHOLD_STATUS_NO → 阈值关闭(直接放行) |
| screen_push_flag | ‘0’ | SCREEN_PUSH_FLAG_YES → 配了屏要推 |
| alarm_popup_window | ‘0’ | ALARM_POPUP_WINDOW_YES → 开了弹窗 |
| popup_recipient_user_ids | NULL | 弹窗全员 |
| delete_flag | ‘0’ | 未删除 |
| create_time | 2023-04-19 | |
| update_time | 2024-02-01 |
配置完整:启用 + 配屏 + 开弹窗 + 阈值放行 + 未删除。
查询 4:处理器 107 的告警记录——SELECT * FROM alarming_record WHERE processor_id='...107',结果:空。processor 107 一条告警记录都没有。
排除法收束
processor 107 配置完整,代码层面看不出任何阻断点。不是 config_status(已启用)、不是阈值(关闭即放行)、不是被删——唯一解释是上游从来没有 WATER_HOUSE_MONITORING / DEVICE_ABNORMAL 类型的事件推给 warning-service。
这处理器 2023-04 建立,从没被触发。很可能业务一直没用起来:水泵房设备没真报过异常,或上游采集根本没接这类数据。
所以「屏推送代码有没有问题」——还没机会跑。 触发链路按源码看是通的,但这条线上从未真正执行过。

图 1:链路代码完整,但从未真正跑过——一条落满灰的传送带。
真正的隐患:推送失败零日志
代码层面有一个实打实的隐患。pushScreenDevice(line 460-468)对 pushDeviceClient.pushDevice(dto) 的结果处理:只有 isSuccess() 且 data 非 null 且 playId 非空才收集 playId;失败(isSuccess=false)或抛异常全部静默,零日志。
一旦未来真触发却屏不亮,排障会非常困难——可能是 Feign 失败、smart-service 路由不通、大华平台拒收,代码层面都看不出来。
最小修复(不改逻辑,只让失败有迹可循):
1 | if (!result.isSuccess()) { |
下游线索
排查尾声,直接 curl 大华平台 http://10.152.64.7:9120/dh/queryPageDeviceList(body {}),返回 HTTP 500 Internal Server Error。
但这不能直接等同于推屏也会失败——queryPageDeviceList(查设备列表)≠ pushDevice(推屏),是同一大华平台的不同接口。500 可能是空 body 缺分页参数、需鉴权、或平台故障。但说明大华平台当前状态不对,是另一条独立线索,详见信息屏中间件那条线。
小结
这次排查最值得记的不是「链路从没跑过」这个结论,而是四次误判暴露的思维定式:
| 误判 | 根因 | 纠正方法 |
|---|---|---|
| list 恒为 null | 忽略 Java 动态分派 | 基类调方法先 grep 子类 @Override |
| screen_plan_id IS NULL 证没推 | insert/set 顺序陷阱 | 字段为 null 先看 insert/update 先后 |
| 广播能推 vs 屏不能推 | 对比前提没坐实 | 「正常参照」先确认它真正常 |
| 猜列名猜常量 | 按常理猜 | 列名查 schema,布尔标记查常量类 |
四个误判里三个源于「按常理推断代码行为」,一个是「按常理猜取值」。代码的行为最终要以源码分派、DB 顺序、常量定义为准,常理是最不靠谱的那一个。
链路从没跑过这件事本身不危险,危险的是「失败零日志」——等真触发时,没有任何现场可查。给那一段加一行失败 log.info,是最小成本的最大改善。