报警规则管理
想要获取报警数据,我们首先必须先制定报警规则,会根据不同的设备,不同的物模型来定义报警规则
需求分析
我们先来分析需求,打开原型图
数据来源:
逻辑规则:
1)若多条报警规则是包含/互斥关系时,只要符合报警规则时,就会产生一条报警数据;
例如:规则1: 手表电量 >=10 ,规则2:手表电量< 50, 此时是包含关系,当手机电量=40时,符合两条报警规则,则产生两条报警数据;
2)报警数据见下方示例,1分钟(数据聚合周期)检查一次智能手表(所属产品)中的全部设备(关联设备)的血氧(功能名称),
监控值(统计字段)是否 < 90(运算符+阈值),当持续3个周期(持续周期)都满足这个规则时,触发报警;
通知方式:
1)报警生效时间:报警规则的生效时间,报警规则只在生效时间内才会检查监控数据是否需要报警;
2)报警沉默周期:指报警发生后如果未恢复正常,重复发送报警通知的时间间隔;
报警方式:
1)当触发报警规则时,则发送消息通知,
通知对象:设备数据类型=老人异常数据时,通知老人对应的护理员;设备数据类型=设备异常数据时,通知后勤部维修工;
渠道:站内信(见消息通知模块原型图)、短信;
基于前面的报警规则,一旦有不符合预期的数据就会触发报警,产生报警数据,通知相关的负责人解决。
这里的概念大家要搞清楚,才能梳理清楚报警数据的过滤流程。下面举个例子来说明一下
案例一
详细报警规则,如下图:
监测的产品为睡眠监测带,物模型为心率,过滤的是该产品下的所有设备
报警类型为老人异常数据(设备报警通知老人绑定的护理员和超级管理员)
持续周期:
- 持续1个周期(1周期=1分钟):表示触发报警之后,马上会保存报警数据
- 持续3个周期(1周期=1分钟):表示触发报警之后,连续三次都是异常数据才会保存报警数据
阈值为65,运算符为**<:表示采集的心率数据如果小于65**就触发报警
沉默周期为5分钟,已经保存报警数据之后,如果后面有连续报警,5分钟之后再触发报警规则
报警生效时段为00:00:00~23:59:59:表示任意时段都会采集数据
案例二
报警规则如下图:
监测的产品为烟雾报警器,物模型为**温度,**过滤的是该产品下的全部设备
报警类型为设备异常数据(设备报警通知行政和超级管理员)
持续周期为持续1个周期(1后期=1分钟):表示触发报警之后,马上会保存报警数据
阈值为55,运算符为**>=:表示采集的室内温度数据大于等于55**就触发报警
沉默周期为5分钟,已经保存报警数据之后,如果后面有连续报警,5分钟之后再触发报警规则
报警生效时段为00:00:00~23:59:59:表示任意时段都会采集数据
添加规则
AlertRuleController
package com.zzyl.controller.web;
import com.zzyl.base.ResponseResult;
import com.zzyl.controller.BaseController;
import com.zzyl.entity.AlertRule;
import com.zzyl.service.AlertRuleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AlertRuleController extends BaseController {
@Autowired
private AlertRuleService alertRuleService;
// 创建告警规则
@PostMapping("/alert-rule/create")
public ResponseResult createAlertRule(@RequestBody AlertRule alertRule) {
alertRuleService.save(alertRule);
return success();
}
}
查询规则
AlertRuleController
//查询报警规则
@GetMapping("/alert-rule/get-page")
public ResponseResult getAlertRulePage(Integer pageNum, Integer pageSize, String alertRuleName, String productKey, String functionName) {
PageResponse<AlertRuleVo> pageResponse = alertRuleService.getAlertRulePage(pageNum, pageSize, alertRuleName, productKey, functionName);
return success(pageResponse);
}
AlertRuleService
PageResponse<AlertRuleVo> getAlertRulePage(Integer pageNum, Integer pageSize, String alertRuleName, String productKey, String functionName);
AlertRuleServiceImpl
@Override
public PageResponse<AlertRuleVo> getAlertRulePage(Integer pageNum, Integer pageSize, String alertRuleName, String productKey, String functionName) {
Page<AlertRule> page = new Page(pageNum, pageSize);
LambdaQueryWrapper<AlertRule> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StrUtil.isNotEmpty(alertRuleName), AlertRule::getAlertRuleName, alertRuleName)
.eq(StrUtil.isNotEmpty(productKey), AlertRule::getProductKey, productKey)
.eq(StrUtil.isNotEmpty(functionName), AlertRule::getFunctionName, functionName);
page = getBaseMapper().selectPage(page, wrapper);
List<AlertRuleVo> list = page.getRecords().stream().map(v -> {
AlertRuleVo alertRuleVo = BeanUtil.copyProperties(v, AlertRuleVo.class);
alertRuleVo.setRules(new StringBuilder(v.getFunctionName()).append(v.getOperator()).append(v.getValue()).append("持续触发").append(v.getDuration()).append("个周期时发生报警").toString());
return alertRuleVo;
}).collect(Collectors.toList());
return new PageResponse(page, list);
}
报警功能实现
报警数据采集
思路分析
基于我们刚才创建的规则,当设备上报数据的时候,我们就需要进行过滤,详细流程如下:
https://heuqqdmbyk.feishu.cn/wiki/NplEwj6NiiOntwkBo1KcGomKnnh
AlertTask
在zzyl-service中新增定时任务类,启动数据过滤
package com.zzyl.task;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONUtil;
import com.zzyl.entity.AlertRule;
import com.zzyl.service.AlertRuleService;
import com.zzyl.vo.DeviceDataVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
//设备上报数据报警规则过滤
@Component
public class AlertTask {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private AlertRuleService alertRuleService;
/**
* 设备上报数据报警规则过滤<br>
* 周期:每分钟执行一次
*/
@Scheduled(cron = "0 * * * * ? ")
public void deviceDataAlertFilter() {
//1.查询所有报警规则,如果为空,程序结束
List<AlertRule> allAlertRules = alertRuleService.list();
if (CollUtil.isEmpty(allAlertRules)) {
return;
}
//2.查询所有设备最新上报数据,如果为空,程序结束
List<Object> jsonStrList = redisTemplate.opsForHash().values("DEVICE_LAST_DATA");
if (CollUtil.isEmpty(jsonStrList)) {
return;
}
//3.设备上报数据不为空,提取设备上报数据为list
List<DeviceDataVo> deviceDataVoList = new ArrayList<>();
jsonStrList.forEach(json -> deviceDataVoList.addAll(JSONUtil.toList(json.toString(), DeviceDataVo.class)));
//4.对每一条设备上报数据进行报警规则校验
deviceDataVoList.forEach(d -> alertRuleService.alertFilter(d));
}
}
AlertRuleService
//校验设备上报数据,进行报警规则过滤处理
void alertFilter(DeviceDataVo deviceDataVo);
AlertRuleServiceImpl
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private DeviceMapper deviceMapper;
@Autowired
private AlertDataService alertsDataService;
@Autowired
private UserMapper userMapper;
//设备维护人员的角色名称
@Value("${zzyl.alert.deviceMaintainerRole}")
private String deviceMaintainerRole;
//超级管理员的角色名称
@Value("${zzyl.alert.managerRole}")
private String managerRole;
/**
* 校验设备上报数据,进行报警规则过滤处理
*
* @param deviceDataVo 设备上报数据
*/
@Override
public void alertFilter(DeviceDataVo deviceDataVo) {
//1. 获取设备上报数据时间,如果上报发生在1分钟前(1分钟前的数据应该已经被前一次定时任务处理过),不再处理
LocalDateTime alarmTime = deviceDataVo.getAlarmTime();
long between = LocalDateTimeUtil.between(alarmTime, LocalDateTime.now(), ChronoUnit.SECONDS);
if (between > 60) {
return;
}
//2. 查询应用在当前设备(有专门针对于当前设备的,还有全部设备的)当前功能上的所有规则
LambdaQueryWrapper<AlertRule> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AlertRule::getFunctionId, deviceDataVo.getFunctionId())
.eq(AlertRule::getProductKey, deviceDataVo.getProductKey())
.in(AlertRule::getIotId, deviceDataVo.getIotId(), "-1");
List<AlertRule> alertRules = getBaseMapper().selectList(wrapper);
//如果报警规则为空,程序结束
if (CollectionUtil.isEmpty(alertRules)) {
return;
}
//如果报警规则不为空,遍历报警规则,进行校验
alertRules.forEach(alertRule -> deviceDateAlarmHandler(deviceDataVo, alertRule));
}
/**
* 设备数据报警处理器
*
* @param deviceDataVo 设备上报数据
* @param alertRule 报警规则
*/
private void deviceDateAlarmHandler(DeviceDataVo deviceDataVo, AlertRule alertRule) {
//第一层校验:判断是否在生效时段内
String[] aepArr = alertRule.getAlertEffectivePeriod().split("~");
LocalTime startTime = LocalTime.parse(aepArr[0]);
LocalTime endTime = LocalTime.parse(aepArr[1]);
LocalTime time = LocalDateTimeUtil.of(deviceDataVo.getAlarmTime()).toLocalTime();//上报时间
//如果上报时间不在生效时间段内,校验结束
if (startTime.isAfter(time) || endTime.isBefore(time)) {
return;
}
//设备id
String iotId = deviceDataVo.getIotId();
//从redis中获取数据,报警规则连续触发次数rediskey
String aggCountKey = "alert_trigger_count:" + iotId + ":" + deviceDataVo.getFunctionId() + ":" + alertRule.getId();
//第二层校验:将设备上报值和阈值比较
//x – 第一个值,y – 第二个值。x==y返回0,x<y返回小于0的数,x>y返回大于0的数
int compareResult = NumberUtil.compare(Double.parseDouble(deviceDataVo.getDataValue()), alertRule.getValue());
//或 逻辑运算,2个条件满足其一即为true
//1.运算符为>=,且设备上报值 >= 阈值
//2.运算符为<,且设备上报值 < 阈值
if ((ObjectUtil.equals(alertRule.getOperator(), ">=") && compareResult >= 0)
|| (ObjectUtil.equals(alertRule.getOperator(), "<") && compareResult < 0)) {
log.debug("此时设备上报数据达到阈值");
} else {
//该情况不符合报警规则,所以设备上报数据为正常数据。需要删除redis聚合的异常数据,程序结束
redisTemplate.delete(aggCountKey);
return;
}
//第三层校验:校验当前设备+功能的报警规则是否处于沉默周期
String silentCacheKey = "alert_silent:" + iotId + ":" + deviceDataVo.getFunctionId() + ":" + alertRule.getId();
String silentData = redisTemplate.opsForValue().get(silentCacheKey);
//如果redis中存在对应沉默周期数据,则当前设备+功能正处于沉默周期,无需报警
if (StrUtil.isNotEmpty(silentData)) {
return;
}
//第四层校验:校验持续周期
//4.1获取连续触发报警规则的次数
//aggCountData是上次触发报警累计到的次数,这一次又触发了报警,所以累积到这次就+1
String aggCountData = redisTemplate.opsForValue().get(aggCountKey);
int triggerCount = ObjectUtil.isEmpty(aggCountData) ? 1 : Integer.parseInt(aggCountData) + 1;
//4.2判断次数是否与持续周期相等
//如果触发报警规则次数不等于持续周期,则无需报警
if (ObjectUtil.notEqual(alertRule.getDuration(), triggerCount)) {
redisTemplate.opsForValue().set(aggCountKey, triggerCount + "");
return;
}
//如果触发报警规则次数等于持续周期,则需要报警,完成以下操作:
//1)删除报警规则连续触发次数的缓存
//2)添加对应的沉默周期,设置过期时间添加对应的沉默周期,设置过期时间
//3)报警数据存储到数据库
redisTemplate.delete(aggCountKey);
redisTemplate.opsForValue().set(silentCacheKey, deviceDataVo.getDataValue(), alertRule.getAlertSilentPeriod(), TimeUnit.MINUTES);
//获取消息的消费者
List<Long> consumerIds = null;
if (ObjectUtil.equals(0, alertRule.getAlertDataType())) {
//如果是老人报警数据,需要通知绑定老人的护理员。根据iotId查询老人绑定的护理员
if (deviceDataVo.getLocationType() == 0) {
consumerIds = deviceMapper.selectNursingIdsByIotIdWithElder(iotId);
} else if (deviceDataVo.getLocationType() == 1 && deviceDataVo.getPhysicalLocationType() == 2) {
consumerIds = deviceMapper.selectNursingIdsByIotIdWithBed(iotId);
}
} else {
//如果是设备报警数据,需要通知设备维护人员。根据指定角色名称查询相关用户
consumerIds = userMapper.selectUserIdsByRoleName(deviceMaintainerRole);
}
//查询超级管理员,超级管理员无论什么消息都会接收
List<Long> managerIds = userMapper.selectUserIdsByRoleName(managerRole);
List<Long> allConsumerIds = CollUtil.addAllIfNotContains(consumerIds, managerIds);
allConsumerIds = CollUtil.distinct(allConsumerIds);
//新增报警数据
insertAlertData(deviceDataVo, alertRule, allConsumerIds);
}
//新增报警数据
private void insertAlertData(DeviceDataVo deviceDataVo, AlertRule alertRule, List<Long> consumerIds) {
String alertReason = CharSequenceUtil.format("{}{}{},持续{}个周期就报警", alertRule.getFunctionName(), alertRule.getOperator(), alertRule.getValue(), alertRule.getDuration());
AlertData alertData = BeanUtil.toBean(deviceDataVo, AlertData.class);
alertData.setAlertRuleId(alertRule.getId());
alertData.setAlertReason(alertReason);
alertData.setType(alertRule.getAlertDataType());
alertData.setStatus(0);
List<AlertData> list = consumerIds.stream().map(id -> {
AlertData dbAlertData = BeanUtil.toBean(alertData, AlertData.class);
dbAlertData.setUserId(id);
return dbAlertData;
}).collect(Collectors.toList());
alertsDataService.saveBatch(list);
}
上述代码中,对于超级管理员和维修工是通过配置的方式读取的名字,需要在application.yml文件中添加数据
zzyl:
alert:
deviceMaintainerRole: 维修工
managerRole: 超级管理员
DeviceMapper
List<Long> selectNursingIdsByIotIdWithElder(String iotId);
List<Long> selectNursingIdsByIotIdWithBed(String iotId);
DeviceMapper.xml
<select id="selectNursingIdsByIotIdWithElder" resultType="java.lang.Long">
SELECT ne.nursing_id
FROM monitor_device AS d
LEFT JOIN elder AS e ON e.id = d.binding_location
LEFT JOIN nursing_elder AS ne ON ne.elder_id = e.id
WHERE d.location_type = 0
AND d.iot_id = #{iotId}
</select>
<select id="selectNursingIdsByIotIdWithBed" resultType="java.lang.Long">
SELECT ne.nursing_id
FROM monitor_device AS d
left join base_bed b on d.binding_location = b.id
LEFT JOIN elder AS e ON e.bed_id = b.id
LEFT JOIN nursing_elder AS ne ON ne.elder_id = e.id
where d.location_type = 1
and d.physical_location_type = 2
and d.iot_id = #{iotId}
</select>
UserMapper
@Select("select sur.user_id from sys_user_role sur left join sys_role sr on sur.role_id = sr.id where sr.role_name = #{roleName}")
List<Long> selectUserIdsByRoleName(String roleName);
测试
创建一条规则,如下:
启动该设备的数据上报,手动改一下心率物模型的参数,需要保证上报的心率值地域65才行
启动后端项目
- 查看redis中key:alert_trigger_count,查看统计数据的变化
- 查看redis中key:alert_silent,沉默周期数据的变化
- 如果已经触发了报警规则三次后,是否能保存数据到数据库中,查看通知人的个人,保存的数据的条数是否相同
数据报警提醒
智能床位
我们打开智能床位的原型图,如下:
- 当床位的设备数据出现了报警数据之后,会有标红通知
- 有报警的楼层,在tab选项卡中会出现红点,来提醒工作人员查看并处理
家属端异常数据展示
在家属端会展示,最新采集的异常数据,如下图展示
站内信通知
其他通知
- 企业微信
- 钉钉
- 短信
- 语音播报
- 电话
- 硬件
t,查看统计数据的变化
- 查看redis中key:alert_silent,沉默周期数据的变化
- 如果已经触发了报警规则三次后,是否能保存数据到数据库中,查看通知人的个人,保存的数据的条数是否相同
数据报警提醒
智能床位
我们打开智能床位的原型图,如下:
[外链图片转存中…(img-0z1WCNp9-1725175520072)]
- 当床位的设备数据出现了报警数据之后,会有标红通知
- 有报警的楼层,在tab选项卡中会出现红点,来提醒工作人员查看并处理
家属端异常数据展示
在家属端会展示,最新采集的异常数据,如下图展示
[外链图片转存中…(img-ghCcAmU4-1725175520072)]
站内信通知
[外链图片转存中…(img-w95nWWP7-1725175520072)]
其他通知
- 企业微信
- 钉钉
- 短信
- 语音播报
- 电话
- 硬件