代码看着没毛病,但这链路从没跑过——显示屏推送排查的四次打脸

用户抛了一个看起来简单的问题:显示屏推送代码是不是没问题?线上能顺利保存「推送显示屏」的告警配置,但不确定真实告警触发时推不推得出去。排查后结论有点意外:这条链路从没真正跑过。更值得记录的不是结论本身,而是排查过程中我做的四次错误推断——每一次都被事实打脸。这篇诚实复盘整个过程。

结论先行

这条推送链路从没真正跑过。唯一配屏的处理器(水泵房监控,2023-04 建立)从未被触发,既谈不上「有问题」也谈不上「没问题」。代码层面看链路是通的,但一旦有触发机会,排障会非常困难——因为推送失败全部静默,零日志。

完整推送链路

排查第一步是搞清楚「推屏」从代码入口到最终物理屏穿了几层。最终确认是三层跳转

1
2
3
4
5
flowchart LR
A["warning-service<br/>AlarmingEvenSinkServiceImpl<br/>.generalAlarmProcessing<br/>line 152-154"] --> B["warning<br/>pushScreenDevice<br/>line 451-470"]
B -->|"Feign<br/>pushDeviceClient.pushDevice"| C["smart-service<br/>PushDeviceInfoController<br/>/pushDeviceInfo/pushDevice"]
C -->|"HTTP"| D["大华发布屏平台<br/>10.152.64.7:9120<br/>/dh/..."]
style D fill:#ffd6a0

关键代码节点:

generalAlarmProcessing(line 152-154)——告警主入口。先 constructionParameters 构建记录,再判断 processorDTO.getAlarmingPushScreenList() 非空调 pushScreenDevice(list)

pushScreenDevice(line 451-470)——遍历屏列表,每块屏调一次 Feign:

1
2
3
4
5
ResultDTO<DhPlayPlan> result = pushDeviceClient.pushDevice(dto);
if (result.isSuccess() && result.getData() != null
&& StringUtils.isNotBlank(result.getData().getPlayId())) {
playIds.add(result.getData().getPlayId());
}

PushDeviceClient(Feign 接口)打到 smart-service 的 /pushDeviceInfo/pushDevice,smart-service 内部再 HTTP 调大华平台 /dh/...,返回类型 DhPlayPlan,成功则收集 playId 拼成 screenPlanId

排查过程:四次打脸

这一节诚实记录被纠正的过程。复盘的价值不在「最终我对了」,而在记录「哪些思维定式容易把人带歪」。

弯路 1:误判 list 恒为 null

我的推断findOneByCondition 是基类方法、AlarmingProcessorServiceImpl 没重写它 → 触发时 alarmingPushScreenList 恒为 null → 屏永远不推。

实际错了

  • listByConditionAlarmingProcessorServiceImpl 重写(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
2
3
4
alarmingRecordService.addSelective(alarmingRecordDTO);   // line 145, insert
// ...
alarmingRecordDTO.setScreenPlanId(pushScreenDevice(...)); // line 153, set
// 注意:set 之后没有 updateSelective

insert 在前,set 在后,set 之后没有 update → 库里 screen_plan_id 恒为 null,与推没推无关。这根本不是一个有效证据。

顺序陷阱:看到「字段在库里是 null」先看 insert/update 的先后,再下结论。DTO 被 set 了不等于库里被改了。

弯路 3:错误的「广播能推、屏不能推」对比

我的推断:广播功能正常推、屏功能不推,形成对比,证明是屏的代码有问题。

实际错了pushBroadcastDevice(processorId)(line 478-485)查完广播列表、collect 完 broadcastIds 后,方法就结束了——broadcastIds 没传给任何下游、没调推送接口。广播也是半成品,没真推。不是对比关系,是难兄难弟。

对比前提要先坐实:用作「正常参照」的那条链路,得先确认它真的正常,否则对比结论是空中楼阁。语音播报那条链路的完整拆解另见这篇

弯路 4:猜列名 / 猜常量被现实打脸

猜列名:我猜 alarming_processorprocessor_name 列 → 实际没有,SQL 直接报 Unknown column

猜常量:我按直觉假设 screen_push_flag '1'=是 → 查常量才发现这个项目所有布尔字段反直觉WarningConstant.java 里:

1
2
3
4
5
6
7
SCREEN_PUSH_FLAG_YES     = "0"   //  是
SCREEN_PUSH_FLAG_NO = "1" // 否
BROADCAST_PUSH_FLAG_YES = "0"
CONFIG_STATUS_YES = "0"
THRESHOLD_STATUS_YES = "0"
ALARM_POPUP_WINDOW_YES = "0"
// 全部 YES='0',NO='1'

这个项目的硬规则:看到任何布尔标记,必须先 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
2
3
if (!result.isSuccess()) {
log.info("pushDevice failed, dto={}, msg={}", dto, result.getMessage());
}

下游线索

排查尾声,直接 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,是最小成本的最大改善。