跳转到内容

案例 9:IoT 设备告警分级

本案例对应 JQuick-Java 仓库 USE-CASES.md 的场景 9:IoT 设备告警分级,属于物联网/设备运维领域,解决对传感器、网关、摄像头、工业机器人等设备做「六维实时指标独立打分 → 告警等级/类型 → 派单/通知/自动处置/升级时限」的实时告警决策问题。

IoT 平台需要对海量在线设备做实时告警分级。设备运行快照包含:设备编号、名称、类型(SENSOR/GATEWAY/CAMERA/ROBOT)、设备重要性(HIGH/MEDIUM/LOW)、当前温度、CPU 使用率、内存使用率、窗口期错误次数、是否断连、最近心跳间隔(秒)、电池电量与振动强度。

评分从六个维度独立进行:温度按 90/80/70/60℃ 四档记 35/25/15/8 分;错误次数按 20/10/5/1 次四档记 25/18/10/4 分;CPU 与内存取峰值后按 95%/85%/75% 记 20/12/6 分;电量不高于 5% 记 15 分、不高于 20% 记 8 分;断连直接记 25 分,心跳间隔达到 300 秒记 15 分、达到 120 秒记 8 分;振动强度按 8/5/3 三档记 20/12/5 分。六维汇总后按 70/45/20 三条线映射 CRITICAL/MAJOR/MINOR/NORMAL。

告警类型按「网络 > 温度 > 性能 > 电量 > 振动」的业务优先级唯一归类:断连或心跳超过 300 秒归为 NETWORK,温度达到 70℃ 归为 TEMPERATURE,CPU 或内存达到 85% 归为 PERFORMANCE,电量不高于 20% 归为 POWER,振动达到 5 归为 VIBRATION。在等级与类型之上,再联动产出派单结论与 P0-P3 优先级(45 分以上必派单,20-44 分的关键 HIGH 设备也要派单,优先级 P2)、通知渠道(CRITICAL 电话、MAJOR 短信、MINOR IM、其余邮件)、自动处置动作(70 分以上或网络类隔离 ISOLATE,性能类限流 RATE_LIMIT,温度/振动类降频 THROTTLE)与升级时限(CRITICAL 5 分钟、MAJOR 15 分钟、MINOR 60 分钟、NORMAL 240 分钟)。

决策同时附带命中指标明细与处置动作清单,便于告警复盘与审计。场景 9 覆盖 6 组形态:炼钢炉温控传感器严重过热 80 分 P0、园区闸机网关断连 51 分 P1、图像识别节点性能告警 22 分 MINOR 不派单、关键设备低电量 25 分 P2 派单、健康设备 0 分 NORMAL、传送带电机振动异常 38 分降频。

  • 六维阈值(温度 90/80/70/60℃、错误 20/10/5/1 次、心跳 300/120 秒、电量 5%/20% 等)硬编码在网关脚本、流式作业与告警平台中,调一次阈值要重新上线整条链路。
  • 告警类型按优先级归类与六维评分是两套逻辑,散落在不同组件里,新增一个告警维度就要同时改多处。
  • 派单不仅看分数还要叠加设备重要性(20-44 分的关键设备 P2 派单),用普通 if-else 堆叠后迅速腐化、无人敢改。
  • 通知渠道、自动处置(隔离/限流/降频)、升级时限都与告警等级联动,改一个升级时限可能影响派单与通知,牵一发而动全身。
  • 告警风暴之下,运维需要每条告警附带命中了哪些指标、执行了哪些处置动作,黑盒告警无法复盘和定责。
  • 边缘网关与设备平台多为 JDK 8 与国产化环境,资源受限,无法部署重型规则中间件。

本案例采用标准「六件套」:入参模型承载设备实时快照,决策结果模型承载等级、类型、派单、通知、处置与升级时限,规则接口声明 13 个规则方法,XML 脚本外置全部打分口径与阈值,Service 负责六维汇总封顶与指标/动作组装,JUnit 以 6 组告警形态回归。

层次 职责 对应文件
领域模型 设备运行快照、告警决策结果 DeviceAlarm.java、AlarmDecision.java
规则集与 XML 六维评分 + 类型归类 + 等级/派单/通知/处置/升级共 13 个方法 DeviceAlarmRules.java、device-alarm.xml
编排服务 六维汇总并 clamp 到 0-100、指标明细与处置动作清单组装 DeviceAlarmService.java
测试 6 组告警形态(过热/断连/性能/低电量/正常/振动) DeviceAlarmServiceTest.java
规则方法 口径 分值/输出
scoreTemperature ≥90℃ 35 / ≥80 25 / ≥70 15 / ≥60 8 / 否则 0 0-35
scoreError 错误次数 ≥20 记 25 / ≥10 记 18 / ≥5 记 10 / ≥1 记 4 / 否则 0 0-25
scoreResource CPU 与内存取峰值,≥95 记 20 / ≥85 记 12 / ≥75 记 6 / 否则 0 0-20
scoreBattery 电量 ≤5% 记 15 / ≤20% 记 8 / 否则 0 0-15
scoreConnection 断连记 25 / 心跳 ≥300s 记 15 / ≥120s 记 8 / 否则 0 0-25
scoreVibration 振动 ≥8 记 20 / ≥5 记 12 / ≥3 记 5 / 否则 0 0-20
alarmLevel ≥70 CRITICAL / ≥45 MAJOR / ≥20 MINOR / 否则 NORMAL CRITICAL/MAJOR/MINOR/NORMAL
alarmType 网络(断连或心跳≥300s)> 温度(≥70℃)> 性能(CPU/内存≥85)> 电量(≤20%)> 振动(≥5),否则 NONE NETWORK/TEMPERATURE/PERFORMANCE/POWER/VIBRATION/NONE
needDispatch ≥45 派单;20-44 且关键设备(HIGH)派单;否则不派 true / false
dispatchPriority ≥70 P0 / ≥45 P1 / 20-44 且 HIGH 为 P2 / 否则 P3 P0/P1/P2/P3
notifyChannel CRITICAL PHONE_CALL / MAJOR SMS / MINOR IM / 其余 EMAIL PHONE_CALL/SMS/IM/EMAIL
autoRemediation ≥70 或 NETWORK → ISOLATE;PERFORMANCE → RATE_LIMIT;TEMPERATURE/VIBRATION → THROTTLE;否则 NONE ISOLATE/RATE_LIMIT/THROTTLE/NONE
escalateMinutes CRITICAL 5 / MAJOR 15 / MINOR 60 / NORMAL 240 升级时限(分钟)

将六个文件按仓库原有包结构放入 Maven 工程:两个模型放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口与测试放入 com.github.paohaijiao.demo 包,编排服务放入 com.github.paohaijiao.demo.service 包,XML 脚本放入 src/test/resources/demo/device-alarm.xml,包名全部保持源码原样。工程根目录执行 mvn test -Dtest=DeviceAlarmServiceTest 即可运行。

package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* IoT 设备运行快照(告警分级所需的实时指标)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeviceAlarm {
/**
* 设备编号
*/
private String deviceId;
/**
* 设备名称
*/
private String deviceName;
/**
* 设备类型:SENSOR / GATEWAY / CAMERA / ROBOT
*/
private String deviceType;
/**
* 设备重要性:HIGH / MEDIUM / LOW
*/
private String criticality;
/**
* 温度(摄氏度)
*/
private double temperature;
/**
* CPU 使用率(0-100)
*/
private int cpuUsage;
/**
* 内存使用率(0-100)
*/
private int memoryUsage;
/**
* 窗口期内错误次数
*/
private int errorCount;
/**
* 是否断连
*/
private boolean connectionLost;
/**
* 最近心跳间隔(秒)
*/
private int lastHeartbeatSeconds;
/**
* 电池电量(0-100)
*/
private int batteryLevel;
/**
* 振动强度
*/
private double vibrationLevel;
}
package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* IoT 设备告警决策结果
*/
@Data
public class AlarmDecision {
/**
* 设备编号
*/
private String deviceId;
/**
* 告警分(0-100)
*/
private int alarmScore;
/**
* 告警等级:CRITICAL / MAJOR / MINOR / NORMAL
*/
private String alarmLevel;
/**
* 告警类型:TEMPERATURE / PERFORMANCE / NETWORK / POWER / VIBRATION / NONE
*/
private String alarmType;
/**
* 是否派发工单
*/
private boolean needDispatch;
/**
* 派单优先级:P0 / P1 / P2 / P3
*/
private String dispatchPriority;
/**
* 通知渠道:PHONE_CALL / SMS / IM / EMAIL
*/
private String notifyChannel;
/**
* 自动处置动作:ISOLATE / RATE_LIMIT / THROTTLE / NONE
*/
private String autoRemediation;
/**
* 升级时限(分钟)
*/
private int escalateMinutes;
/**
* 命中指标明细
*/
private List<String> indicators = new ArrayList<String>();
/**
* 处置动作建议
*/
private List<String> actions = new ArrayList<String>();
public void addIndicator(String indicator) {
this.indicators.add(indicator);
}
public void addAction(String action) {
this.actions.add(action);
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/**
* 场景 9:IoT 设备告警分级
* <p>
* 各维度独立打分(温度/错误/资源/电量/连接/振动),再由综合分决定等级、派单、通知与自动处置。
*/
public interface DeviceAlarmRules {
int scoreTemperature(@Param("temperature") double temperature);
int scoreError(@Param("errorCount") int errorCount);
int scoreResource(@Param("cpuUsage") int cpuUsage,
@Param("memoryUsage") int memoryUsage);
int scoreBattery(@Param("batteryLevel") int batteryLevel);
int scoreConnection(@Param("connectionLost") boolean connectionLost,
@Param("lastHeartbeatSeconds") int lastHeartbeatSeconds);
int scoreVibration(@Param("vibrationLevel") double vibrationLevel);
String alarmLevel(@Param("alarmScore") int alarmScore);
String alarmType(@Param("connectionLost") boolean connectionLost,
@Param("lastHeartbeatSeconds") int lastHeartbeatSeconds,
@Param("temperature") double temperature,
@Param("cpuUsage") int cpuUsage,
@Param("memoryUsage") int memoryUsage,
@Param("batteryLevel") int batteryLevel,
@Param("vibrationLevel") double vibrationLevel);
boolean needDispatch(@Param("alarmScore") int alarmScore,
@Param("criticality") String criticality);
String dispatchPriority(@Param("alarmScore") int alarmScore,
@Param("criticality") String criticality);
String notifyChannel(@Param("alarmLevel") String alarmLevel,
@Param("criticality") String criticality);
String autoRemediation(@Param("alarmType") String alarmType,
@Param("alarmScore") int alarmScore);
int escalateMinutes(@Param("alarmLevel") String alarmLevel);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN"
"classpath:paohaijiao/dtd/Jquick-java.dtd">
<javas namespace="com.github.paohaijiao.demo.DeviceAlarmRules">
<!-- 温度评分:0~35 -->
<java name="scoreTemperature" returnClass="int">
<![CDATA[
int def scoreTemperature(double:temperature) {
if (temperature >= 90.0) {
return 35;
} else if (temperature >= 80.0) {
return 25;
} else if (temperature >= 70.0) {
return 15;
} else if (temperature >= 60.0) {
return 8;
} else {
return 0;
}
}
]]>
</java>
<!-- 错误次数评分:0~25 -->
<java name="scoreError" returnClass="int">
<![CDATA[
int def scoreError(int:errorCount) {
if (errorCount >= 20) {
return 25;
} else if (errorCount >= 10) {
return 18;
} else if (errorCount >= 5) {
return 10;
} else if (errorCount >= 1) {
return 4;
} else {
return 0;
}
}
]]>
</java>
<!-- 资源占用评分(取 CPU/内存峰值):0~20 -->
<java name="scoreResource" returnClass="int">
<![CDATA[
int def scoreResource(int:cpuUsage, int:memoryUsage) {
int peak = cpuUsage;
if (memoryUsage > peak) {
peak = memoryUsage;
}
if (peak >= 95) {
return 20;
} else if (peak >= 85) {
return 12;
} else if (peak >= 75) {
return 6;
} else {
return 0;
}
}
]]>
</java>
<!-- 电量评分:0~15 -->
<java name="scoreBattery" returnClass="int">
<![CDATA[
int def scoreBattery(int:batteryLevel) {
if (batteryLevel <= 5) {
return 15;
} else if (batteryLevel <= 20) {
return 8;
} else {
return 0;
}
}
]]>
</java>
<!-- 连接异常评分:0~25 -->
<java name="scoreConnection" returnClass="int">
<![CDATA[
int def scoreConnection(boolean:connectionLost, int:lastHeartbeatSeconds) {
if (connectionLost) {
return 25;
} else if (lastHeartbeatSeconds >= 300) {
return 15;
} else if (lastHeartbeatSeconds >= 120) {
return 8;
} else {
return 0;
}
}
]]>
</java>
<!-- 振动评分:0~20 -->
<java name="scoreVibration" returnClass="int">
<![CDATA[
int def scoreVibration(double:vibrationLevel) {
if (vibrationLevel >= 8.0) {
return 20;
} else if (vibrationLevel >= 5.0) {
return 12;
} else if (vibrationLevel >= 3.0) {
return 5;
} else {
return 0;
}
}
]]>
</java>
<!-- 告警等级 -->
<java name="alarmLevel" returnClass="java.lang.String">
<![CDATA[
str def alarmLevel(int:alarmScore) {
if (alarmScore >= 70) {
return "CRITICAL";
} else if (alarmScore >= 45) {
return "MAJOR";
} else if (alarmScore >= 20) {
return "MINOR";
} else {
return "NORMAL";
}
}
]]>
</java>
<!-- 告警类型(按业务优先级归类) -->
<java name="alarmType" returnClass="java.lang.String">
<![CDATA[
str def alarmType(boolean:connectionLost, int:lastHeartbeatSeconds, double:temperature,
int:cpuUsage, int:memoryUsage, int:batteryLevel, double:vibrationLevel) {
if (connectionLost || lastHeartbeatSeconds >= 300) {
return "NETWORK";
} else if (temperature >= 70.0) {
return "TEMPERATURE";
} else if (cpuUsage >= 85 || memoryUsage >= 85) {
return "PERFORMANCE";
} else if (batteryLevel <= 20) {
return "POWER";
} else if (vibrationLevel >= 5.0) {
return "VIBRATION";
} else {
return "NONE";
}
}
]]>
</java>
<!-- 是否派单 -->
<java name="needDispatch" returnClass="boolean">
<![CDATA[
boolean def needDispatch(int:alarmScore, str:criticality) {
if (alarmScore >= 45) {
return true;
} else if (alarmScore >= 20 && criticality == "HIGH") {
return true;
} else {
return false;
}
}
]]>
</java>
<!-- 派单优先级 -->
<java name="dispatchPriority" returnClass="java.lang.String">
<![CDATA[
str def dispatchPriority(int:alarmScore, str:criticality) {
if (alarmScore >= 70) {
return "P0";
} else if (alarmScore >= 45) {
return "P1";
} else if (alarmScore >= 20 && criticality == "HIGH") {
return "P2";
} else {
return "P3";
}
}
]]>
</java>
<!-- 通知渠道 -->
<java name="notifyChannel" returnClass="java.lang.String">
<![CDATA[
str def notifyChannel(str:alarmLevel, str:criticality) {
if (alarmLevel == "CRITICAL") {
return "PHONE_CALL";
} else if (alarmLevel == "MAJOR") {
return "SMS";
} else if (alarmLevel == "MINOR") {
return "IM";
} else {
return "EMAIL";
}
}
]]>
</java>
<!-- 自动处置动作 -->
<java name="autoRemediation" returnClass="java.lang.String">
<![CDATA[
str def autoRemediation(str:alarmType, int:alarmScore) {
if (alarmScore >= 70) {
return "ISOLATE";
} else if (alarmType == "NETWORK") {
return "ISOLATE";
} else if (alarmType == "PERFORMANCE") {
return "RATE_LIMIT";
} else if (alarmType == "TEMPERATURE") {
return "THROTTLE";
} else if (alarmType == "VIBRATION") {
return "THROTTLE";
} else {
return "NONE";
}
}
]]>
</java>
<!-- 升级时限(分钟) -->
<java name="escalateMinutes" returnClass="int">
<![CDATA[
int def escalateMinutes(str:alarmLevel) {
if (alarmLevel == "CRITICAL") {
return 5;
} else if (alarmLevel == "MAJOR") {
return 15;
} else if (alarmLevel == "MINOR") {
return 60;
} else {
return 240;
}
}
]]>
</java>
</javas>
package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.DeviceAlarmRules;
import com.github.paohaijiao.demo.model.AlarmDecision;
import com.github.paohaijiao.demo.model.DeviceAlarm;
/**
* 场景 9:IoT 设备告警分级服务
* <p>
* 职责划分:
* <ul>
* <li><b>Java 侧</b>:六维评分汇总与封顶、告警类型与等级装配、指标与动作组装</li>
* <li><b>规则侧</b>:各维度打分口径、类型归类优先级、等级/派单/通知阈值,全部外置在 XML 中</li>
* </ul>
*/
public class DeviceAlarmService {
private static final int SCORE_FLOOR = 0;
private static final int SCORE_CEIL = 100;
private final DeviceAlarmRules rules;
public DeviceAlarmService(DeviceAlarmRules rules) {
this.rules = rules;
}
/**
* 计算设备告警决策
*
* @param device 设备运行快照
* @return 告警决策
*/
public AlarmDecision evaluate(DeviceAlarm device) {
AlarmDecision decision = new AlarmDecision();
decision.setDeviceId(device.getDeviceId());
int rawScore = rules.scoreTemperature(device.getTemperature())
+ rules.scoreError(device.getErrorCount())
+ rules.scoreResource(device.getCpuUsage(), device.getMemoryUsage())
+ rules.scoreBattery(device.getBatteryLevel())
+ rules.scoreConnection(device.isConnectionLost(), device.getLastHeartbeatSeconds())
+ rules.scoreVibration(device.getVibrationLevel());
int alarmScore = clamp(rawScore);
String alarmLevel = rules.alarmLevel(alarmScore);
String alarmType = rules.alarmType(device.isConnectionLost(), device.getLastHeartbeatSeconds(),
device.getTemperature(), device.getCpuUsage(), device.getMemoryUsage(),
device.getBatteryLevel(), device.getVibrationLevel());
decision.setAlarmScore(alarmScore);
decision.setAlarmLevel(alarmLevel);
decision.setAlarmType(alarmType);
decision.setNeedDispatch(rules.needDispatch(alarmScore, device.getCriticality()));
decision.setDispatchPriority(rules.dispatchPriority(alarmScore, device.getCriticality()));
decision.setNotifyChannel(rules.notifyChannel(alarmLevel, device.getCriticality()));
decision.setAutoRemediation(rules.autoRemediation(alarmType, alarmScore));
decision.setEscalateMinutes(rules.escalateMinutes(alarmLevel));
buildIndicators(device, decision);
buildActions(device, decision);
return decision;
}
private int clamp(int score) {
if (score < SCORE_FLOOR) {
return SCORE_FLOOR;
}
if (score > SCORE_CEIL) {
return SCORE_CEIL;
}
return score;
}
private void buildIndicators(DeviceAlarm device, AlarmDecision decision) {
if (device.getTemperature() >= 60.0) {
decision.addIndicator("温度 " + device.getTemperature() + "℃");
}
if (device.getCpuUsage() >= 75) {
decision.addIndicator("CPU 使用率 " + device.getCpuUsage() + "%");
}
if (device.getMemoryUsage() >= 75) {
decision.addIndicator("内存使用率 " + device.getMemoryUsage() + "%");
}
if (device.getErrorCount() >= 1) {
decision.addIndicator("错误次数 " + device.getErrorCount());
}
if (device.isConnectionLost()) {
decision.addIndicator("设备断连");
} else if (device.getLastHeartbeatSeconds() >= 120) {
decision.addIndicator("心跳间隔 " + device.getLastHeartbeatSeconds() + " 秒");
}
if (device.getBatteryLevel() <= 20) {
decision.addIndicator("电量 " + device.getBatteryLevel() + "%");
}
if (device.getVibrationLevel() >= 3.0) {
decision.addIndicator("振动强度 " + device.getVibrationLevel());
}
}
private void buildActions(DeviceAlarm device, AlarmDecision decision) {
if (decision.isNeedDispatch()) {
decision.addAction("按 " + decision.getDispatchPriority() + " 优先级派发运维工单");
} else {
decision.addAction("暂不派单,持续观察指标趋势");
}
decision.addAction("通过 " + decision.getNotifyChannel() + " 通知责任人");
if (!"NONE".equals(decision.getAutoRemediation())) {
decision.addAction("执行自动处置:" + decision.getAutoRemediation());
}
if ("HIGH".equals(device.getCriticality()) && decision.getAlarmScore() >= 20) {
decision.addAction("关键设备告警,同步通知设备负责人");
}
decision.addAction(decision.getEscalateMinutes() + " 分钟内未恢复则升级");
}
}
package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.AlarmDecision;
import com.github.paohaijiao.demo.model.DeviceAlarm;
import com.github.paohaijiao.demo.service.DeviceAlarmService;
import com.github.paohaijiao.xml.JQuickJava;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* 场景 9 生产级样板:IoT 设备告警分级
* <p>
* 覆盖 6 组告警形态:严重过热、网络断连、性能资源耗尽、低电量、
* 设备正常、振动异常。
*/
public class DeviceAlarmServiceTest {
private DeviceAlarmService service;
@Before
public void setUp() {
DeviceAlarmRules rules = JQuickJava.create()
.createApi(DeviceAlarmRules.class, "demo/device-alarm.xml");
this.service = new DeviceAlarmService(rules);
}
/**
* 严重过热:温度 92℃ + 高频错误 22 次 + CPU 96% → 80 分 CRITICAL
*/
@Test
public void shouldEscalateOverheatDeviceToCritical() {
DeviceAlarm device = new DeviceAlarm();
device.setDeviceId("DEV-9001");
device.setDeviceName("炼钢炉温控传感器");
device.setDeviceType("SENSOR");
device.setCriticality("HIGH");
device.setTemperature(92.0);
device.setCpuUsage(96);
device.setMemoryUsage(70);
device.setErrorCount(22);
device.setConnectionLost(false);
device.setLastHeartbeatSeconds(60);
device.setBatteryLevel(80);
device.setVibrationLevel(1.0);
AlarmDecision decision = service.evaluate(device);
Assert.assertEquals(80, decision.getAlarmScore());
Assert.assertEquals("CRITICAL", decision.getAlarmLevel());
Assert.assertEquals("TEMPERATURE", decision.getAlarmType());
Assert.assertTrue(decision.isNeedDispatch());
Assert.assertEquals("P0", decision.getDispatchPriority());
Assert.assertEquals("PHONE_CALL", decision.getNotifyChannel());
Assert.assertEquals("ISOLATE", decision.getAutoRemediation());
Assert.assertEquals(5, decision.getEscalateMinutes());
Assert.assertEquals(3, decision.getIndicators().size());
Assert.assertTrue(decision.getIndicators().contains("温度 92.0℃"));
Assert.assertTrue(decision.getIndicators().contains("CPU 使用率 96%"));
Assert.assertTrue(decision.getIndicators().contains("错误次数 22"));
Assert.assertTrue(decision.getActions().contains("按 P0 优先级派发运维工单"));
Assert.assertTrue(decision.getActions().contains("执行自动处置:ISOLATE"));
}
/**
* 网络断连:断连 + 错误 10 次 + 低电量 15% → 51 分 MAJOR
*/
@Test
public void shouldClassifyNetworkOutage() {
DeviceAlarm device = new DeviceAlarm();
device.setDeviceId("DEV-9002");
device.setDeviceName("园区闸机网关");
device.setDeviceType("GATEWAY");
device.setCriticality("HIGH");
device.setTemperature(40.0);
device.setCpuUsage(50);
device.setMemoryUsage(50);
device.setErrorCount(10);
device.setConnectionLost(true);
device.setLastHeartbeatSeconds(600);
device.setBatteryLevel(15);
device.setVibrationLevel(0.5);
AlarmDecision decision = service.evaluate(device);
Assert.assertEquals(51, decision.getAlarmScore());
Assert.assertEquals("MAJOR", decision.getAlarmLevel());
Assert.assertEquals("NETWORK", decision.getAlarmType());
Assert.assertTrue(decision.isNeedDispatch());
Assert.assertEquals("P1", decision.getDispatchPriority());
Assert.assertEquals("SMS", decision.getNotifyChannel());
Assert.assertEquals("ISOLATE", decision.getAutoRemediation());
Assert.assertEquals(15, decision.getEscalateMinutes());
Assert.assertTrue(decision.getIndicators().contains("设备断连"));
Assert.assertTrue(decision.getIndicators().contains("电量 15%"));
}
/**
* 性能资源耗尽:内存 92% + 错误 6 次,但设备重要性 MEDIUM → 22 分 MINOR 且不派单
*/
@Test
public void shouldFlagPerformanceIssueWithoutDispatch() {
DeviceAlarm device = new DeviceAlarm();
device.setDeviceId("DEV-9003");
device.setDeviceName("图像识别节点");
device.setDeviceType("CAMERA");
device.setCriticality("MEDIUM");
device.setTemperature(45.0);
device.setCpuUsage(88);
device.setMemoryUsage(92);
device.setErrorCount(6);
device.setConnectionLost(false);
device.setLastHeartbeatSeconds(30);
device.setBatteryLevel(60);
device.setVibrationLevel(1.0);
AlarmDecision decision = service.evaluate(device);
Assert.assertEquals(22, decision.getAlarmScore());
Assert.assertEquals("MINOR", decision.getAlarmLevel());
Assert.assertEquals("PERFORMANCE", decision.getAlarmType());
Assert.assertFalse(decision.isNeedDispatch());
Assert.assertEquals("P3", decision.getDispatchPriority());
Assert.assertEquals("IM", decision.getNotifyChannel());
Assert.assertEquals("RATE_LIMIT", decision.getAutoRemediation());
Assert.assertEquals(60, decision.getEscalateMinutes());
Assert.assertTrue(decision.getIndicators().contains("内存使用率 92%"));
}
/**
* 低电量:电量 3% + 错误 6 次,关键设备 → 25 分 MINOR 且按 P2 派单
*/
@Test
public void shouldAlertLowBatteryOnCriticalDevice() {
DeviceAlarm device = new DeviceAlarm();
device.setDeviceId("DEV-9004");
device.setDeviceName("冷库温湿度记录仪");
device.setDeviceType("SENSOR");
device.setCriticality("HIGH");
device.setTemperature(50.0);
device.setCpuUsage(40);
device.setMemoryUsage(40);
device.setErrorCount(6);
device.setConnectionLost(false);
device.setLastHeartbeatSeconds(50);
device.setBatteryLevel(3);
device.setVibrationLevel(0.2);
AlarmDecision decision = service.evaluate(device);
Assert.assertEquals(25, decision.getAlarmScore());
Assert.assertEquals("MINOR", decision.getAlarmLevel());
Assert.assertEquals("POWER", decision.getAlarmType());
Assert.assertTrue(decision.isNeedDispatch());
Assert.assertEquals("P2", decision.getDispatchPriority());
Assert.assertEquals("IM", decision.getNotifyChannel());
Assert.assertEquals("NONE", decision.getAutoRemediation());
Assert.assertTrue(decision.getIndicators().contains("电量 3%"));
}
/**
* 设备正常:各项指标健康 → 0 分 NORMAL
*/
@Test
public void shouldKeepHealthyDeviceNormal() {
DeviceAlarm device = new DeviceAlarm();
device.setDeviceId("DEV-9005");
device.setDeviceName("办公区环境传感器");
device.setDeviceType("SENSOR");
device.setCriticality("MEDIUM");
device.setTemperature(42.0);
device.setCpuUsage(30);
device.setMemoryUsage(40);
device.setErrorCount(0);
device.setConnectionLost(false);
device.setLastHeartbeatSeconds(30);
device.setBatteryLevel(85);
device.setVibrationLevel(0.5);
AlarmDecision decision = service.evaluate(device);
Assert.assertEquals(0, decision.getAlarmScore());
Assert.assertEquals("NORMAL", decision.getAlarmLevel());
Assert.assertEquals("NONE", decision.getAlarmType());
Assert.assertFalse(decision.isNeedDispatch());
Assert.assertEquals("P3", decision.getDispatchPriority());
Assert.assertEquals("EMAIL", decision.getNotifyChannel());
Assert.assertEquals("NONE", decision.getAutoRemediation());
Assert.assertEquals(240, decision.getEscalateMinutes());
Assert.assertTrue(decision.getIndicators().isEmpty());
Assert.assertTrue(decision.getActions().contains("暂不派单,持续观察指标趋势"));
}
/**
* 振动异常:振动强度 9.5 + 错误 10 次 → 38 分 MINOR,自动限流
*/
@Test
public void shouldDetectAbnormalVibration() {
DeviceAlarm device = new DeviceAlarm();
device.setDeviceId("DEV-9006");
device.setDeviceName("传送带电机");
device.setDeviceType("ROBOT");
device.setCriticality("MEDIUM");
device.setTemperature(55.0);
device.setCpuUsage(70);
device.setMemoryUsage(65);
device.setErrorCount(10);
device.setConnectionLost(false);
device.setLastHeartbeatSeconds(40);
device.setBatteryLevel(70);
device.setVibrationLevel(9.5);
AlarmDecision decision = service.evaluate(device);
Assert.assertEquals(38, decision.getAlarmScore());
Assert.assertEquals("MINOR", decision.getAlarmLevel());
Assert.assertEquals("VIBRATION", decision.getAlarmType());
Assert.assertFalse(decision.isNeedDispatch());
Assert.assertEquals("P3", decision.getDispatchPriority());
Assert.assertEquals("IM", decision.getNotifyChannel());
Assert.assertEquals("THROTTLE", decision.getAutoRemediation());
Assert.assertTrue(decision.getIndicators().contains("振动强度 9.5"));
Assert.assertTrue(decision.getActions().contains("执行自动处置:THROTTLE"));
}
}

在工程根目录执行:

Terminal window
mvn test -Dtest=DeviceAlarmServiceTest
测试方法 构造的业务形态 关键断言/期望决策
shouldEscalateOverheatDeviceToCritical 炼钢炉温控传感器:92℃、错误 22 次、CPU 96%、HIGH 设备 80 分(35+25+20)、CRITICAL、TEMPERATURE、派单 P0、PHONE_CALL、ISOLATE、5 分钟升级、指标 3 条
shouldClassifyNetworkOutage 园区闸机网关:断连、心跳 600s、错误 10 次、电量 15% 51 分(25+18+8)、MAJOR、NETWORK、派单 P1、SMS、ISOLATE、15 分钟
shouldFlagPerformanceIssueWithoutDispatch 图像识别节点:CPU 88%、内存 92%、错误 6 次、MEDIUM 设备 22 分(12+10)、MINOR、PERFORMANCE、不派单、P3、IM、RATE_LIMIT、60 分钟
shouldAlertLowBatteryOnCriticalDevice 冷库记录仪:电量 3%、错误 6 次、HIGH 关键设备 25 分(15+10)、MINOR、POWER、关键设备 P2 派单、IM、自动处置 NONE
shouldKeepHealthyDeviceNormal 办公区环境传感器:全部指标健康 0 分、NORMAL、NONE、不派单、P3、EMAIL、NONE、240 分钟、指标为空
shouldDetectAbnormalVibration 传送带电机:振动 9.5、错误 10 次、MEDIUM 设备 38 分(20+18)、MINOR、VIBRATION、不派单、P3、IM、THROTTLE 降频

USE-CASES.md 场景 9 收录的两组实测输出:

炼钢炉温控传感器严重过热(92℃、22 次错误、CPU 96%):

告警分: 80 (CRITICAL) 类型: TEMPERATURE 派单: P0
通知: PHONE_CALL 自动处置: ISOLATE 升级时限: 5 分钟

园区闸机网关断连(断连 + 心跳 600s + 10 次错误 + 低电量 15%):

告警分: 51 (MAJOR) 类型: NETWORK 派单: P1
通知: SMS 自动处置: ISOLATE 升级时限: 15 分钟
  • 六个维度各为一个独立评分方法,13 个规则方法覆盖评分、归类、派单、通知、处置、升级时限,全部阈值(70/45/20、300/120 秒、5%/20% 等)外置 XML,调阈值不必重启业务系统。
  • Service 在 Java 侧把六维原始分统一 clamp 到 0-100,并按指标实际命中情况组装明细清单,每条告警都可复盘「为什么是这个等级、执行了什么动作」。
  • scoreResource 在脚本内先取 CPU 与内存的峰值再分档,避免两个资源维度重复计分;alarmType 以一条正向 if-else 链表达「网络 > 温度 > 性能 > 电量 > 振动」的归类优先级。
  • 派单规则把设备重要性作为第二条件:20-44 分区间只有 HIGH 关键设备才 P2 派单,普通设备只观察不派单,显著抑制告警风暴。
  • autoRemediation 前置「总分 ≥70 一律隔离」,再按 NETWORK/PERFORMANCE/TEMPERATURE/VIBRATION 分流隔离、限流、降频;@Param 按名绑定支持 alarmType 多达 7 个入参,代理实例可复用,底层为 ASM 调用链。
  • Java 8 即可运行、无重型规则平台依赖,适合部署在资源受限的边缘网关与信创环境。