diff --git a/src/main/java/com/zcloud/mapper/datasource/dust/DustSensorMapper.java b/src/main/java/com/zcloud/mapper/datasource/dust/DustSensorMapper.java new file mode 100644 index 0000000..2fa14d8 --- /dev/null +++ b/src/main/java/com/zcloud/mapper/datasource/dust/DustSensorMapper.java @@ -0,0 +1,39 @@ +package com.zcloud.mapper.datasource.dust; + + +import com.zcloud.entity.PageData; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface DustSensorMapper { + /** + * 查询所有启用的传感器 + * + * @return 传感器列表 + */ + List selectAllActiveSensors(); + /** + * 根据设备传感器ID查询规则 + * @param deviceSensorId + * @return + */ + PageData selectRuleByDeviceSensorId(String deviceSensorId); + /** + * 根据设备ID和传感器类型查询传感器 + * @param deviceId + * @param assocSensorType + * @return + */ + PageData findByDeviceIdAndSensorType(@Param("deviceId") String deviceId, @Param("assocSensorType") String assocSensorType); + /** + * 批量创建报警记录 + * @param alarmRecords + * @return + */ + int insertBatch(@Param("alarmRecords") List alarmRecords); + + List getLatestData(); + + PageData getLatestDataById(String deviceSensorId); +} diff --git a/src/main/java/com/zcloud/mapper/dsno2/sensor/LastSensorDataMapper.java b/src/main/java/com/zcloud/mapper/dsno2/sensor/LastSensorDataMapper.java new file mode 100644 index 0000000..5831bf1 --- /dev/null +++ b/src/main/java/com/zcloud/mapper/dsno2/sensor/LastSensorDataMapper.java @@ -0,0 +1,10 @@ +package com.zcloud.mapper.dsno2.sensor; + +import com.zcloud.entity.PageData; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface LastSensorDataMapper { + +} diff --git a/src/main/java/com/zcloud/scheduled/DustSensorAlarmScheduled.java b/src/main/java/com/zcloud/scheduled/DustSensorAlarmScheduled.java new file mode 100644 index 0000000..1ef18fa --- /dev/null +++ b/src/main/java/com/zcloud/scheduled/DustSensorAlarmScheduled.java @@ -0,0 +1,295 @@ +package com.zcloud.scheduled; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.IdUtil; +import com.xxl.job.core.handler.annotation.XxlJob; +import com.zcloud.entity.PageData; +import com.zcloud.mapper.dsno2.sensor.LastSensorDataMapper; +import com.zcloud.service.dust.DustSensorService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class DustSensorAlarmScheduled { + + private static final Logger log = LoggerFactory.getLogger(DustSensorAlarmScheduled.class); + + + @Resource + private DustSensorService sensorAlarmService; + @Resource + private LastSensorDataMapper lastSensorDataMapper; + + // 缓存规则模板:RULE_ID -> Rule + private final Map ruleCache = new ConcurrentHashMap<>(); + + // 报警状态缓存:DEVICE_SENSOR_ID -> AlarmState + private final Map alarmStateCache = new ConcurrentHashMap<>(); + + + @XxlJob(value = "sensorAlarmScheduled") + public void sensorAlarmScheduled() { + // 1. 获取所有启用的传感器(排除停用/删除) + List allSensors = sensorAlarmService.selectAllActiveSensors(); + // 2. 批量获取最新实时数据 + List latestDataMap = sensorAlarmService.getLatestData(); + // 3.批量错误 数据 + List alarmRecords = new ArrayList<>(); + allSensors.forEach(sensor -> { + PageData currentData = latestDataMap.stream() + .filter(item -> item.get("DEVICE_SENSOR_ID").equals(sensor.get("DEVICE_SENSOR_ID"))) + .findFirst() + .orElse(null); + if (currentData == null) { + log.warn("未获取到传感器实时数据, DEVICE_SENSOR_ID={}", sensor.get("DEVICE_SENSOR_ID")); + } + evaluateSingleSensorAlarm(alarmRecords, sensor, currentData); + }); + if (!alarmRecords.isEmpty()) { + // 4. 批量创建报警记录 + sensorAlarmService.createAlarmRecord(alarmRecords); + } + } + + /** + * 判断单个传感器是否触发/消警 + */ + private void evaluateSingleSensorAlarm(List alarmRecords, PageData sensor, PageData currentData) { + // 1. 获取规则(优先从缓存) + PageData rule = getRuleTemplate(sensor.getString("DEVICE_SENSOR_ID")); + if (rule == null || !"1".equals(rule.getString("IS_ACTIVE"))) { + return; + } + + // 2. 检查传感器是否离线超时(24小时) + if (isOfflineTimeout(currentData, Integer.parseInt(rule.getString("OFFLINE_TIMEOUT_HOUR")))) { + handleOfflineClear(sensor, rule); + return; + } + + // 3. 处理关联传感器(如启停信号) + if (!checkAssociatedCondition(sensor, rule, currentData)) { + // 不满足关联条件,不报警(但不清除已有报警!) + // 注意:业务规则中,关联不满足 ≠ 消警,只是不触发新报警 + return; + } + + // 4. 判断是否超限 + boolean isOverLimit = isValueOverLimit(currentData, rule); + + // 5. 获取当前报警状态 + PageData state = alarmStateCache.computeIfAbsent( + sensor.getString("DEVICE_SENSOR_ID"), + k -> { + PageData stateInit = new PageData(); + stateInit.put("IS_ALARMING", false); + stateInit.put("CONSECUTIVE_NORMAL_POINTS", 0); + return stateInit; + } + ); + + if (isOverLimit) { + // --- 触发逻辑 --- + handleTriggerLogic(alarmRecords, sensor, rule, currentData, state); + } else { + // --- 消警逻辑 --- + handleClearLogic(sensor, rule, currentData, state); + } + } + + /** + * 处理触发逻辑(考虑持续时间) + */ + private void handleTriggerLogic(List alarmRecords, PageData sensor, PageData rule, + PageData currentData, PageData state) { + LocalDateTime now = LocalDateTime.now(); + if (!Boolean.parseBoolean(state.get("IS_ALARMING").toString())) { + // 判断规则是否存在 持续报警时间 + if (rule.get("TRIGGER_DURATION_SEC") != null) { + // 首次超限 + if (state.get("FIRST_TRIGGER_TIME") == null) { + state.put("FIRST_TRIGGER_TIME", now); + } + // 检查是否满足持续时间 单位:秒 + // 暂时还没考虑数据点的情况,如后期有需求,在数据库的规则表中增加一个字段TRIGGER_DURATION_POINT + long durationSec = Duration.between(LocalDateTime.parse(state.get("FIRST_TRIGGER_TIME").toString()), now).getSeconds(); + if (durationSec >= Integer.parseInt(rule.getString("TRIGGER_DURATION_SEC"))) { + // 触发报警 + createAlarmRecord(alarmRecords, sensor, rule, currentData); + state.put("IS_ALARMING", true); + log.info("【满足持续时间报警触发】sensor={}, value={}", sensor.get("SENSOR_NAME"), currentData.getString("CURRENT_VALUE")); + } + }else { + // 触发报警 + createAlarmRecord(alarmRecords, sensor, rule, currentData); + state.put("IS_ALARMING", true); + log.info("【报警触发】sensor={}, value={}", sensor.get("SENSOR_NAME"), currentData.getString("CURRENT_VALUE")); + } + } + + } + + /** + * 创建报警记录 + */ + private void createAlarmRecord(List alarmRecords, PageData sensor, PageData rule, PageData currentData) { + PageData alarmRecord = new PageData(); + alarmRecord.put("ALARM_RECORD_ID", IdUtil.fastSimpleUUID()); + alarmRecord.put("RULE_ID", rule.get("RULE_ID")); + alarmRecord.put("DEVICE_SENSOR_ID", sensor.get("DEVICE_SENSOR_ID")); + alarmRecord.put("SENSOR_CODE", sensor.get("SENSOR_CODE")); + alarmRecord.put("DEVICE_ID", sensor.get("DEVICE_ID")); + alarmRecord.put("ALARM_TYPE", rule.get("ALARM_TYPE")); + alarmRecord.put("TRIGGER_REASON", currentData.get("TRIGGER_REASON")); + alarmRecord.put("ALARM_LEVEL", currentData.get("ALARM_LEVEL")); + alarmRecord.put("VALUE_AT_TRIGGER", currentData.getString("CURRENT_VALUE")); + alarmRecord.put("TRIGGERED_TIME", DateUtil.now()); + alarmRecord.put("CREATOR", "scheduled"); + alarmRecord.put("OPERATOR", "scheduled"); + alarmRecord.put("CREATTIME", DateUtil.now()); + alarmRecord.put("OPERATTIME", DateUtil.now()); + alarmRecords.add(alarmRecord); + + } + + /** + * 处理消警逻辑 + */ + private void handleClearLogic(PageData sensor, PageData rule, + PageData currentData, PageData state) { + if (!Boolean.parseBoolean(state.get("IS_ALARMING").toString())) { + // 未报警,重置状态 + state.put("FIRST_TRIGGER_TIME", null); + state.put("CONSECUTIVE_NORMAL_POINTS", 0); + return; + } + + // 值已恢复正常,开始计数 + int normalPoints = Integer.parseInt(state.get("CONSECUTIVE_NORMAL_POINTS").toString()) + 1; + state.put("CONSECUTIVE_NORMAL_POINTS", normalPoints); + + // 假设1个数据点 = 1秒(可根据实际调整) + if (normalPoints >= Integer.parseInt(rule.get("CLEAR_DURATION_SEC").toString())) { + // TODO 消警逻辑 + // sensorAlarmService.clearAlarmEvent(sensor.get("DEVICE_SENSOR_ID").toString(), "normal"); + state.put("IS_ALARMING", false); + state.put("CONSECUTIVE_NORMAL_POINTS", 0); + log.info("【报警消警】sensor={}", sensor.get("SENSOR_NAME")); + } + } + + /** + * 判断当前值是否超限 + */ + private boolean isValueOverLimit(PageData data, PageData rule) { + BigDecimal value = new BigDecimal(data.getString("CURRENT_VALUE")); + + if ("2".equals(rule.getString("SIGNAL_TYPE"))) { // 开关量 + return value.compareTo(new BigDecimal(rule.getString("SWITCH_ALARM_VALUE"))) == 0; + } else { // 模拟量 + if (rule.getString("HIGH_ALARM") != null && value.compareTo(new BigDecimal(rule.getString("HIGH_ALARM"))) > 0) { + data.put("TRIGGER_REASON", data.getString("TARGET_NAME") + "超过高报警"); + data.put("ALARM_LEVEL", "2"); + return true; + } + if (rule.getString("HIGH_HIGH_ALARM") != null && value.compareTo(new BigDecimal(rule.getString("HIGH_HIGH_ALARM"))) > 0) { + data.put("TRIGGER_REASON", data.getString("TARGET_NAME") + "超过高高报警"); + data.put("ALARM_LEVEL", "3"); + return true; + } + if (rule.getString("LOW_ALARM") != null && value.compareTo(new BigDecimal(rule.getString("LOW_ALARM"))) < 0) { + data.put("TRIGGER_REASON", data.getString("TARGET_NAME") + "超过低报警"); + data.put("ALARM_LEVEL", "2"); + return true; + } + if (rule.getString("LOW_LOW_ALARM") != null && value.compareTo(new BigDecimal(rule.getString("LOW_LOW_ALARM"))) < 0) { + data.put("TRIGGER_REASON", data.getString("TARGET_NAME") + "超过低低报警"); + data.put("ALARM_LEVEL", "3"); + return true; + } + } + return false; + } + + /** + * 检查关联条件(如同设备下的启停信号是否为“启动”) + */ + private boolean checkAssociatedCondition(PageData sensor, PageData rule, PageData currentData) { + String assocSensorType = rule.getString("ASSOCIATED_SENSOR_TYPE"); + if (assocSensorType == null) { + return true; // 无关联,直接通过 + } + + // 在同一设备下查找关联传感器 + PageData associatedSensor = sensorAlarmService.findByDeviceIdAndSensorType( + sensor.getString("DEVICE_ID"), assocSensorType + ); + + if (associatedSensor == null) { + log.warn("未找到关联传感器, deviceId={}, type={}", sensor.getString("DEVICE_ID"), assocSensorType); + return false; + } + + // 获取关联传感器的最新数据 + PageData assocData = sensorAlarmService.getLatestDataById(associatedSensor.getString("DEVICE_SENSOR_ID")); + if (assocData == null) { + return true; // 注意:这里返回 true 表示“不阻断”,但实际不触发报警(由上层控制) + } + + // 判断关联条件(must_be_on → 值=1) + if ("1".equals(rule.getString("ASSOCIATED_CONDITION"))) { + return BigDecimal.ONE.compareTo(new BigDecimal(assocData.getString("CURRENT_VALUE"))) == 0; + } + if ("0".equals(rule.getString("ASSOCIATED_CONDITION"))) { + return BigDecimal.ZERO.compareTo(new BigDecimal(assocData.getString("CURRENT_VALUE"))) == 0; + } + + return true; + } + + + /** + * 离线超时自动消警 + */ + private void handleOfflineClear(PageData sensor, PageData rule) { + PageData state = alarmStateCache.get(sensor.getString("DEVICE_SENSOR_ID")); + if (state != null && Boolean.parseBoolean(state.get("IS_ALARMING").toString())) { + // TODO 消警逻辑 + //alarmEventService.clearAlarmEvent(sensor.get("DEVICE_SENSOR_ID").toString(), "offline_timeout"); + state.put("IS_ALARMING", false); + log.info("【离线超时消警】sensor={}", sensor.get("SENSOR_NAME")); + } + } + + /** + * 检查是否离线超时(24小时) + */ + private boolean isOfflineTimeout(PageData data, int offlineTimeoutHour) { + LocalDateTime insertTime = DateUtil.parseLocalDateTime(data.getString("INSERT_TIME")); + long offlineHours = Duration.between(insertTime, LocalDateTime.now()).toHours(); + return offlineHours >= offlineTimeoutHour; + } + + /** + * 获取规则模板(带缓存) + */ + private PageData getRuleTemplate(String deviceSensorId) { + return ruleCache.computeIfAbsent(deviceSensorId, id -> { + PageData rule = sensorAlarmService.selectRuleByDeviceSensorId(deviceSensorId); + if (rule == null) { + log.error("规则模板不存在: deviceSensorId={}", deviceSensorId); + } + return rule; + }); + } +} diff --git a/src/main/java/com/zcloud/service/dust/DustSensorService.java b/src/main/java/com/zcloud/service/dust/DustSensorService.java new file mode 100644 index 0000000..b374040 --- /dev/null +++ b/src/main/java/com/zcloud/service/dust/DustSensorService.java @@ -0,0 +1,45 @@ +package com.zcloud.service.dust; + +import com.zcloud.entity.PageData; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +public interface DustSensorService { + /** + * 查询所有活跃的传感器 + * @return + */ + List selectAllActiveSensors(); + /** + * 根据设备传感器ID查询规则 + * @param deviceSensorId + * @return + */ + PageData selectRuleByDeviceSensorId(String deviceSensorId); + /** + * 根据设备ID和传感器类型查询传感器 + * @param deviceId + * @param assocSensorType + * @return + */ + PageData findByDeviceIdAndSensorType(String deviceId, String assocSensorType); + /** + * 创建报警记录 + * @param alarmRecords + */ + void createAlarmRecord(List alarmRecords); + + /** + * 批量获取最新实时数据 + * @return + */ + List getLatestData(); + /** + * 根据设备传感器ID获取最新数据 + * @param deviceSensorId + * @return + */ + PageData getLatestDataById(@Param("deviceSensorId") String deviceSensorId); +} diff --git a/src/main/java/com/zcloud/service/dust/impl/DustSensorServiceImpl.java b/src/main/java/com/zcloud/service/dust/impl/DustSensorServiceImpl.java new file mode 100644 index 0000000..40cbff4 --- /dev/null +++ b/src/main/java/com/zcloud/service/dust/impl/DustSensorServiceImpl.java @@ -0,0 +1,43 @@ +package com.zcloud.service.dust.impl; + +import com.zcloud.entity.PageData; +import com.zcloud.mapper.datasource.dust.DustSensorMapper; +import com.zcloud.service.dust.DustSensorService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +@Service +public class DustSensorServiceImpl implements DustSensorService { + + @Resource + private DustSensorMapper dustSensorMapper; + + @Override + public List selectAllActiveSensors() { + return dustSensorMapper.selectAllActiveSensors(); + } + + @Override + public PageData selectRuleByDeviceSensorId(String deviceSensorId) { + return dustSensorMapper.selectRuleByDeviceSensorId(deviceSensorId); + } + @Override + public PageData findByDeviceIdAndSensorType(String deviceId, String assocSensorType) { + return dustSensorMapper.findByDeviceIdAndSensorType(deviceId, assocSensorType); + } + + @Override + public void createAlarmRecord(List alarmRecords) { + dustSensorMapper.insertBatch(alarmRecords); + } + @Override + public List getLatestData() { + return dustSensorMapper.getLatestData(); + } + @Override + public PageData getLatestDataById(String deviceSensorId) { + return dustSensorMapper.getLatestDataById(deviceSensorId); + } +} diff --git a/src/main/resources/mybatis/datasource/dust/DustSensorMapper.xml b/src/main/resources/mybatis/datasource/dust/DustSensorMapper.xml new file mode 100644 index 0000000..1fe934d --- /dev/null +++ b/src/main/resources/mybatis/datasource/dust/DustSensorMapper.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + insert into dust_device_sensor_alarm_record + (ALARM_RECORD_ID, + DEVICE_ID, + RULE_ID, + DEVICE_SENSOR_ID, + SENSOR_CODE, + ALARM_TYPE, + TRIGGER_REASON, + ALARM_LEVEL, + VALUE_AT_TRIGGER, + TRIGGERED_TIME, + CREATOR, + CREATTIME, + OPERATTIME, + OPERATOR) + values + + (#{item.ALARM_RECORD_ID}, + #{item.DEVICE_ID}, + #{item.RULE_ID}, + #{item.DEVICE_SENSOR_ID}, + #{item.SENSOR_CODE}, + #{item.ALARM_TYPE}, + #{item.TRIGGER_REASON}, + #{item.ALARM_LEVEL}, + #{item.VALUE_AT_TRIGGER}, + #{item.TRIGGERED_TIME}, + #{item.CREATOR}, + #{item.CREATTIME}, + #{item.OPERATTIME}, + #{item.OPERATOR}) + + + + + + + diff --git a/src/main/resources/mybatis/dsno2/sensor/LastSensorDataMapper.xml b/src/main/resources/mybatis/dsno2/sensor/LastSensorDataMapper.xml new file mode 100644 index 0000000..6555be7 --- /dev/null +++ b/src/main/resources/mybatis/dsno2/sensor/LastSensorDataMapper.xml @@ -0,0 +1,6 @@ + + + + + +