Bladeren bron

feate: 异常报警机制

jackzhou 6 maanden geleden
bovenliggende
commit
a83f0063a5
14 gewijzigde bestanden met toevoegingen van 443 en 169 verwijderingen
  1. 5 5
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/cache/monitordevice/MonitorDeviceCacheInitializer.java
  2. 19 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/config/JfcloudColdChainConstants.java
  3. 10 2
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorAlarmEvent.java
  4. 67 103
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorDataEventListener2.java
  5. 0 20
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/SensorThreshold.java
  6. 0 31
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/SensorThresholdService.java
  7. 64 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/bean/SensorThreshold.java
  8. 36 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/config/ColdChainAlarmMessageProperties.java
  9. 149 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/DefaultSensorAlarmChecker.java
  10. 19 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/SensorAlarmChecker.java
  11. 37 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/SensorThresholdService.java
  12. 4 2
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/ThresholdService.java
  13. 20 6
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevice/param/MonitorDeviceAddParam.java
  14. 13 0
      snowy-web-app/src/main/resources/application.properties

+ 5 - 5
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/cache/monitordevice/MonitorDeviceCacheInitializer.java

@@ -43,21 +43,21 @@ public class MonitorDeviceCacheInitializer implements CommandLineRunner {
             for (MonitorDevice monitorDevice : deviceList) {
                 String deviceCode = monitorDevice.getDeviceCode();
                 String deviceModel = monitorDevice.getModelName();
-                Integer parsedDeviceCode = Integer.parseInt(deviceCode);  // Avoid parsing multiple times
+                if (!deviceCode.matches("^[0-9]+$")) {
+                    log.error("Invalid device code: " + deviceCode);
+                    continue;
+                }
+                Integer parsedDeviceCode = Integer.parseInt(deviceCode);
                 // 调用设备编号配置上报参数接口
-//                List<Integer> paramIds = monitorDevice.getParamIds();
                 String paramIds = monitorDevice.getParamIds();
                 if (paramIds == null || StrUtil.isBlank(paramIds)) {  // Explicitly check for null and empty list
                     log.info("触发设备【编号】配置上报参数接口:采集设备编号 = {}, 采集设备型号 = {}", deviceCode, deviceModel);
                     renKeService.callParamIds(parsedDeviceCode);
                 }
                 // 调用设备参数配置上报参数接口
-//                List<ParamItem> parameters = monitorDevice.getParameters();
                 String parameters = monitorDevice.getParameters();
                 if (StrUtil.isNotBlank(paramIds) && StrUtil.isBlank(parameters)) {  // Check for null and empty list
                     log.info("触发设备【完整参数】配置上报参数接口:采集设备编号 = {}, 采集设备型号 = {}", deviceCode, deviceModel);
-//                    Type listType = new TypeToken<List<Integer>>(){}.getType();
-//                    List<Integer> paramIds = gson.fromJson(json, listType);
                     TypeReference<List<Integer>> typeReference = new TypeReference<>() {
                     };
                     List<Integer> paramId2s = JSONUtil.toBean(paramIds, typeReference, true);

+ 19 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/config/JfcloudColdChainConstants.java

@@ -34,4 +34,23 @@ public interface JfcloudColdChainConstants {
      * influxDB default Measurement
      */
     String INFLUXDB_DEFAULT_MEASUREMENT_NAME = "sensor_data";
+
+    /**
+     * 队列最大容量和写入数据库的时间间隔
+     */
+    int MAX_QUEUE_SIZE = 10;
+
+    /**
+     * 队列最大容量和写入数据库的时间间隔
+     */
+    long CHECK_INTERVAL_MS = 1 * 60 * 1000L;
+
+    /**
+     * 重试机制配置- 最大重试5次数
+     */
+    int MAX_RETRIES = 5;
+    /**
+     * 每次重试的间隔时间,单位:毫秒
+     */
+    int RETRY_DELAY_MS = 5000;
 }

+ 10 - 2
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorAlarmEvent.java

@@ -1,5 +1,7 @@
 package vip.xiaonuo.coldchain.core.event;
 
+import lombok.Getter;
+import lombok.Setter;
 import org.springframework.context.ApplicationEvent;
 
 import java.time.Clock;
@@ -11,12 +13,18 @@ import java.time.Clock;
  * @description
  * @date 2024/11/25 16:55:56
  */
+@Getter
+@Setter
 public class SensorAlarmEvent extends ApplicationEvent {
-    public SensorAlarmEvent(Object source) {
+    private String message;
+
+    public SensorAlarmEvent(Object source, String message) {
         super(source);
+        this.message = message;
     }
 
-    public SensorAlarmEvent(Object source, Clock clock) {
+    public SensorAlarmEvent(Object source, Clock clock, String message) {
         super(source, clock);
+        this.message = message;
     }
 }

+ 67 - 103
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorDataEventListener2.java

@@ -3,149 +3,113 @@ package vip.xiaonuo.coldchain.core.event;
 import com.github.jfcloud.influxdb.service.JfcloudInfluxDBService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
-import vip.xiaonuo.coldchain.core.event.alarm.SensorThreshold;
-import vip.xiaonuo.coldchain.core.event.alarm.ThresholdService;
+import vip.xiaonuo.coldchain.core.config.JfcloudColdChainConstants;
+import vip.xiaonuo.coldchain.core.event.alarm.service.SensorAlarmChecker;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.stream.Collectors;
 
-/**
- * 事件监听器:处理 SensorData 事件,缓存数据并进行定时写入数据库,同时检查是否需要报警
- */
 @Slf4j
 @RequiredArgsConstructor
 @Component
 public class SensorDataEventListener2 {
     private final JfcloudInfluxDBService jfcloudInfluxDBService;
-    private final ApplicationEventPublisher eventPublisher;
-    private final ThresholdService thresholdService;
-    // 使用线程安全的队列来缓存传感器数据
-    private final Queue<SensorData> sensorDataQueue = new ConcurrentLinkedQueue<>();
-    // 队列最大容量和写入数据库的时间间隔
-    private static final int MAX_QUEUE_SIZE = 10;
-    private static final long CHECK_INTERVAL_MS = 10000L;  // 每10秒检查一次
+    private final SensorAlarmChecker sensorAlarmChecker;
+    // 使用双缓冲机制:一个队列缓存数据,另一个队列进行批量写入
+    private final Queue<SensorData> bufferQueue = new ConcurrentLinkedQueue<>();
+    private final Queue<SensorData> writeQueue = new ConcurrentLinkedQueue<>();
 
-    /**
-     * 监听 SensorDataEvent 事件,收到传感器数据后进行缓存和处理
-     *
-     * @param event 传感器数据事件
-     */
+    // 事件监听,处理传感器数据
     @EventListener
     public void handleSensorDataEvent(SensorDataEvent event) {
         SensorData sensorData = event.getSensorData();
-        // 将数据放入队列并检查是否需要报警
-        addSensorData(sensorData);
-        // 获取传感器的阈值范围
-        SensorThreshold sensorThreshold = thresholdService.getThresholdBySensorId(sensorData.getDeviceId(), sensorData.getRoads());
-        // 新探头 不做校验
-        if (Objects.nonNull(sensorThreshold)) {
-            // 在数据放入队列后进行报警检查
-            checkForAlarm(sensorData, sensorThreshold);
+        if (Objects.nonNull(sensorData)) {
+            // 将数据添加到缓存队列
+            addSensorDataToBufferQueue(sensorData);
+            // 获取传感器的阈值范围并检查报警
+            sensorAlarmChecker.checkAlarm(sensorData);
         }
     }
 
-    /**
-     * 将新的 SensorData 加入队列,并根据队列大小判断是否需要写入数据库
-     *
-     * @param sensorData 传感器数据
-     */
-    public void addSensorData(SensorData sensorData) {
-        // 将数据放入队列
-        sensorDataQueue.offer(sensorData);
-        // 如果队列达到最大容量,批量写入数据库
-        if (sensorDataQueue.size() >= MAX_QUEUE_SIZE) {
-            writeDataToDatabase();
+    // 将数据添加到缓存队列中
+    public void addSensorDataToBufferQueue(SensorData sensorData) {
+        bufferQueue.offer(sensorData);
+        // 如果缓存队列的数据超过设定阈值,转移数据到写入队列
+        if (bufferQueue.size() >= JfcloudColdChainConstants.MAX_QUEUE_SIZE) {
+            transferDataToWriteQueue();
         }
     }
 
-    /**
-     * 定时检查队列并写入数据库
-     * 每隔 CHECK_INTERVAL_MS 时间检查一次
-     */
-    @Scheduled(fixedRate = CHECK_INTERVAL_MS)
+    // 定期检查并批量写入数据
+    @Scheduled(fixedRate = JfcloudColdChainConstants.CHECK_INTERVAL_MS)
     public void checkAndWriteData() {
-        // 只有队列达到一定大小才进行写入
-        if (sensorDataQueue.size() >= MAX_QUEUE_SIZE) {
-            writeDataToDatabase();
+        // 如果缓存队列达到阈值,则将数据转移到写入队列并异步写入
+        if (bufferQueue.size() >= JfcloudColdChainConstants.MAX_QUEUE_SIZE) {
+            transferDataToWriteQueue();
+            // 异步写入数据到数据库
+            writeDataToDatabaseAsync();
         }
     }
 
-    /**
-     * 将队列中的数据批量写入数据库,并清空队列
-     */
-    private void writeDataToDatabase() {
-        if (!sensorDataQueue.isEmpty()) {
-            try {
-                log.info("开始批量写入数据到 InfluxDB,队列大小: {}", sensorDataQueue.size());
-
-                // 将队列中的数据转为 List
-                List<SensorData> dataList = sensorDataQueue.stream()
-                        .collect(Collectors.toList());
+    // 将缓存队列的数据转移到写入队列
+    private void transferDataToWriteQueue() {
+        if (!bufferQueue.isEmpty()) {
+            writeQueue.addAll(bufferQueue);
+            bufferQueue.clear();  // 清空缓存队列
+            log.debug("数据已从缓存队列转移到写入队列,当前写入队列大小: {}", writeQueue.size());
+        }
+    }
 
-                // 批量写入数据库
+    // 异步写入数据到数据库
+    @Async("coldChainAsyncTask")
+    public void writeDataToDatabaseAsync() {
+        if (!writeQueue.isEmpty()) {
+            List<SensorData> dataList = new ArrayList<>(writeQueue);
+            try {
+                log.info("开始异步批量写入数据到 InfluxDB,数据量: {}", dataList.size());
                 jfcloudInfluxDBService.writePojo(dataList);
-
-                // 写入完成后清空队列
-                sensorDataQueue.clear();
-
-                log.info("成功批量写入数据到 InfluxDB,队列已清空");
-
+                writeQueue.clear();  // 写入完成后清空写入队列
+                log.info("数据成功写入 InfluxDB,队列已清空");
             } catch (Exception e) {
                 log.error("写入数据到 InfluxDB 时出错: {}", e.getMessage(), e);
+                // 如果写入失败,进行重试
+                retryWriteDataToDatabase(dataList);
             }
         }
     }
 
     /**
-     * 检查传感器数据是否满足报警条件
-     * 触发报警事件
-     *
-     * @param sensorData 传感器数据
-     * @param threshold  传感器的阈值范围
+     * 如果写入失败,进行重试
      */
-    private void checkForAlarm(SensorData sensorData, SensorThreshold threshold) {
-        // 检查温度是否超标
-        if (sensorData.getTemperature() != null) {
-            if (sensorData.getTemperature() < threshold.getTemperatureMin() || sensorData.getTemperature() > threshold.getTemperatureMax()) {
-                publishAlarm("温度超标", sensorData.getTemperature(), "°C");
-            }
-        }
-
-        // 检查湿度是否超标
-        if (sensorData.getHumidity() != null) {
-            if (sensorData.getHumidity() < threshold.getHumidityMin() || sensorData.getHumidity() > threshold.getHumidityMax()) {
-                publishAlarm("湿度超标", sensorData.getHumidity(), "%");
-            }
-        }
-
-        // 检查二氧化碳是否超标
-        if (sensorData.getCo2() != null) {
-            if (sensorData.getCo2() < threshold.getCo2Min() || sensorData.getCo2() > threshold.getCo2Max()) {
-                publishAlarm("二氧化碳超标", sensorData.getCo2(), "ppm");
+    private void retryWriteDataToDatabase(List<SensorData> dataList) {
+        int retries = JfcloudColdChainConstants.MAX_RETRIES;
+        int delay = JfcloudColdChainConstants.RETRY_DELAY_MS;
+        while (retries > 0) {
+            try {
+                log.info("正在进行重试,剩余次数: {}", retries);
+                jfcloudInfluxDBService.writePojo(dataList);
+                log.info("数据成功写入 InfluxDB,重试成功");
+                return;
+            } catch (Exception e) {
+                retries--;
+                log.error("重试失败,剩余重试次数: {}, 错误信息: {}", retries, e.getMessage());
+                if (retries == 0) {
+                    log.error("重试已达最大次数,放弃写入操作");
+                }
+                try {
+                    Thread.sleep(delay);  // 等待一段时间后重试
+                } catch (InterruptedException ie) {
+                    Thread.currentThread().interrupt();
+                }
             }
         }
     }
-
-    /**
-     * 发布报警事件
-     *
-     * @param alarmType 报警类型
-     * @param value     超标的数值
-     * @param unit      单位
-     */
-    private void publishAlarm(String alarmType, Float value, String unit) {
-        String alarmMessage = String.format("%s: %.2f %s", alarmType, value, unit);
-        log.warn("数据异常,发布报警: {}", alarmMessage);
-
-        // 发布报警事件(假设 AlarmEvent 已经定义)
-        eventPublisher.publishEvent(new SensorAlarmEvent(this/*, alarmMessage*/));
-    }
 }

+ 0 - 20
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/SensorThreshold.java

@@ -1,20 +0,0 @@
-package vip.xiaonuo.coldchain.core.event.alarm;
-
-import lombok.Data;
-
-/**
- * @author jackzhou
- * @version 1.0
- * @project jfcloud-coldchain
- * @description
- * @date 2024/11/25 17:08:04
- */
-@Data
-public class SensorThreshold {
-    private float temperatureMin;
-    private float temperatureMax;
-    private float humidityMin;
-    private float humidityMax;
-    private float co2Min;
-    private float co2Max;
-}

+ 0 - 31
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/SensorThresholdService.java

@@ -1,31 +0,0 @@
-package vip.xiaonuo.coldchain.core.event.alarm;
-
-/**
- * @author jackzhou
- * @version 1.0
- * @project jfcloud-coldchain
- * @description
- * @date 2024/11/25 17:13:50
- */
-import org.springframework.stereotype.Service;
-
-/**
- * 从数据库获取传感器阈值的实现类
- */
-@Service
-public class SensorThresholdService implements ThresholdService {
-//    @Override
-//    public SensorThreshold getThresholdBySensorId(Long sensorId) {
-////        // 查询数据库获取对应传感器的阈值
-////        return sensorThresholdRepository.findBySensorId(sensorId)
-////                .orElseThrow(() ->
-//        return null;
-//    }
-
-    @Override
-    public SensorThreshold getThresholdBySensorId(String sensorCode, Integer sensorNo) {
-        SensorThreshold sensorThreshold = new SensorThreshold();
-        return sensorThreshold;
-
-    }
-}

+ 64 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/bean/SensorThreshold.java

@@ -0,0 +1,64 @@
+package vip.xiaonuo.coldchain.core.event.alarm.bean;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import vip.xiaonuo.coldchain.modular.app.param.FloatNullToDashSerializer;
+
+/**
+ * @author jackzhou
+ * @version 1.0
+ * @project jfcloud-coldchain
+ * @description
+ * @date 2024/11/25 17:08:04
+ */
+@Data
+public class SensorThreshold {
+    /**
+     * 温度报警上限 temperature humidity co2
+     */
+    @Schema(description = "温度报警上限")
+    @JsonSerialize(using = FloatNullToDashSerializer.class)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
+    private Float temperatureUp;
+    /**
+     * 温度报警下限
+     */
+    @Schema(description = "温度报警下限")
+    @JsonSerialize(using = FloatNullToDashSerializer.class)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
+    private Float temperatureDown;
+
+    /**
+     * 湿度报警上限 temperature humidity co2
+     */
+
+    @Schema(description = "湿度报警上限")
+    @JsonSerialize(using = FloatNullToDashSerializer.class)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
+    private Float humidityUp;
+    /**
+     * 湿度报警下限
+     */
+    @Schema(description = "湿度报警下限")
+    @JsonSerialize(using = FloatNullToDashSerializer.class)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
+    private Float humidityDown;
+
+    /**
+     * 二氧化碳报警上限 temperature humidity co2
+     */
+    @Schema(description = "二氧化碳报警上限")
+    @JsonSerialize(using = FloatNullToDashSerializer.class)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
+    private Float co2Up;
+
+    /**
+     * 二氧化碳报警下限
+     */
+    @Schema(description = "二氧化碳报警下限")
+    @JsonSerialize(using = FloatNullToDashSerializer.class)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#.00")
+    private Float co2Down;
+}

+ 36 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/config/ColdChainAlarmMessageProperties.java

@@ -0,0 +1,36 @@
+package vip.xiaonuo.coldchain.core.event.alarm.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "coldchain.alarm.message")
+@Data
+public class ColdChainAlarmMessageProperties {
+
+    // Default message templates with placeholders
+    private String temperatureOverLimit = "温度报警:设备【{deviceName}】的温度超标!\n" +
+            "当前温度:{value} {unit},已超出上限(阈值:{thresholdUp})。\n" +
+            "报警时间:{time}";
+
+    private String temperatureBelowLimit = "温度报警:设备【{deviceName}】的温度过低!\n" +
+            "当前温度:{value} {unit},已低于下限(阈值:{thresholdDown})。\n" +
+            "报警时间:{time}";
+
+    private String humidityOverLimit = "湿度报警:设备【{deviceName}】的湿度超标!\n" +
+            "当前湿度:{value} {unit},已超出上限(阈值:{thresholdUp})。\n" +
+            "报警时间:{time}";
+
+    private String humidityBelowLimit = "湿度报警:设备【{deviceName}】的湿度过低!\n" +
+            "当前湿度:{value} {unit},已低于下限(阈值:{thresholdDown})。\n" +
+            "报警时间:{time}";
+
+    private String co2OverLimit = "二氧化碳报警:设备【{deviceName}】的二氧化碳浓度超标!\n" +
+            "当前浓度:{value} {unit},已超出上限(阈值:{thresholdUp})。\n" +
+            "报警时间:{time}";
+
+    private String co2BelowLimit = "二氧化碳报警:设备【{deviceName}】的二氧化碳浓度过低!\n" +
+            "当前浓度:{value} {unit},已低于下限(阈值:{thresholdDown})。\n" +
+            "报警时间:{time}";
+}

+ 149 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/DefaultSensorAlarmChecker.java

@@ -0,0 +1,149 @@
+package vip.xiaonuo.coldchain.core.event.alarm.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
+import vip.xiaonuo.coldchain.core.event.SensorAlarmEvent;
+import vip.xiaonuo.coldchain.core.event.alarm.bean.SensorThreshold;
+import vip.xiaonuo.coldchain.core.event.alarm.config.ColdChainAlarmMessageProperties;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class DefaultSensorAlarmChecker implements SensorAlarmChecker {
+    private final SensorThresholdService thresholdService;
+    private final ColdChainAlarmMessageProperties alarmMessageProperties;
+    private final ApplicationEventPublisher applicationEventPublisher;
+
+    // 定义一个日期格式化器,用于将时间格式化成可读的字符串
+    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+    @Override
+    public boolean checkAlarm(SensorData sensorData) {
+        if (sensorData == null) {
+            throw new IllegalArgumentException("传感器数据不能为空!");
+        }
+
+        // 获取传感器阈值配置
+        SensorThreshold threshold = thresholdService.getThresholdBySensorId(sensorData.getDeviceId(), sensorData.getRoads());
+        if (threshold == null) {
+            log.warn("没有找到设备 {} 的阈值配置", sensorData.getDeviceId());
+            return false;
+        }
+        // 检查传感器数据并触发报警
+        boolean alarmTriggered = false;
+        if (sensorData.getTemperature() != null) {
+            alarmTriggered = checkThreshold(sensorData.getTemperature(), threshold.getTemperatureUp(), threshold.getTemperatureDown(), "温度", sensorData);
+        }
+        if (sensorData.getHumidity() != null) {
+            alarmTriggered |= checkThreshold(sensorData.getHumidity(), threshold.getHumidityUp(), threshold.getHumidityDown(), "湿度", sensorData);
+        }
+        if (sensorData.getCo2() != null) {
+            alarmTriggered |= checkThreshold(sensorData.getCo2(), threshold.getCo2Up(), threshold.getCo2Down(), "二氧化碳", sensorData);
+        }
+
+        return alarmTriggered;
+    }
+
+    /**
+     * 检查阈值并触发报警
+     *
+     * @param value          传感器数据的值
+     * @param upperThreshold 阈值上限
+     * @param lowerThreshold 阈值下限
+     * @param type           数据类型(温度、湿度、二氧化碳)
+     * @param sensorData     传感器数据
+     * @return 是否触发报警
+     */
+    private boolean checkThreshold(Float value, Float upperThreshold, Float lowerThreshold, String type, SensorData sensorData) {
+        boolean alarmTriggered = false;
+        String deviceName = "设备名称";
+        String time = DATE_FORMAT.format(new Date()); // 获取当前时间
+        String unit = getUnit(type);
+        if (value > upperThreshold) {
+            // 超过上限,触发超标报警
+            publishAlarm(type + "超标", value, unit, deviceName, time);
+            alarmTriggered = true;
+        } else if (value < lowerThreshold) {
+            // 低于下限,触发低于阈值报警
+            publishAlarm(type + "过低", value, unit, deviceName, time);
+            alarmTriggered = true;
+        }
+        return alarmTriggered;
+    }
+
+    /**
+     * 发布报警事件
+     *
+     * @param alarmType  报警类型(例如 温度超标、湿度过低等)
+     * @param value      传感器数据值
+     * @param unit       传感器数据单位
+     * @param deviceName 设备名称
+     * @param time       时间戳
+     */
+    private void publishAlarm(String alarmType, Float value, String unit, String deviceName, String time) {
+        // 获取对应的报警消息模板
+        String alarmMessage = getAlarmMessage(alarmType, value, unit, deviceName, time);
+        // 发布报警事件(可以替换成实际的报警处理逻辑)
+        applicationEventPublisher.publishEvent(new SensorAlarmEvent(this, alarmMessage));
+    }
+
+    /**
+     * 获取报警消息模板并替换占位符
+     *
+     * @param alarmType  报警类型(例如 温度超标、湿度过低等)
+     * @param value      传感器数据值
+     * @param unit       传感器数据单位
+     * @param deviceName 设备名称
+     * @param time       时间戳
+     * @return 格式化后的报警消息
+     */
+    private String getAlarmMessage(String alarmType, Float value, String unit, String deviceName, String time) {
+        String messageTemplate = "";
+        // 根据报警类型获取对应的消息模板
+        switch (alarmType) {
+            case "温度超标":
+                messageTemplate = alarmMessageProperties.getTemperatureOverLimit();
+                break;
+            case "温度过低":
+                messageTemplate = alarmMessageProperties.getTemperatureBelowLimit();
+                break;
+            case "湿度超标":
+                messageTemplate = alarmMessageProperties.getHumidityOverLimit();
+                break;
+            case "湿度过低":
+                messageTemplate = alarmMessageProperties.getHumidityBelowLimit();
+                break;
+            case "二氧化碳超标":
+                messageTemplate = alarmMessageProperties.getCo2OverLimit();
+                break;
+            case "二氧化碳过低":
+                messageTemplate = alarmMessageProperties.getCo2BelowLimit();
+                break;
+            default:
+                messageTemplate = "{alarmType}: 当前值:{value} {unit}";
+                break;
+        }
+
+        // 替换模板中的占位符
+        return messageTemplate.replace("{value}", String.format("%.2f", value)).replace("{unit}", unit).replace("{alarmType}", alarmType).replace("{time}", time).replace("{deviceName}", deviceName);
+    }
+
+    private String getUnit(String sensorType) {
+        switch (sensorType) {
+            case "温度":
+                return "°C";
+            case "湿度":
+                return "%";
+            case "二氧化碳":
+                return "ppm";
+            default:
+                return "";
+        }
+    }
+}

+ 19 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/SensorAlarmChecker.java

@@ -0,0 +1,19 @@
+package vip.xiaonuo.coldchain.core.event.alarm.service;
+
+import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
+
+/**
+ * @author jackzhou
+ * @version 1.0
+ * @project jfcloud-coldchain
+ * @description
+ * @date 2024/11/25 18:44:48
+ */
+public interface SensorAlarmChecker {
+    /**
+     * 检查传感器数据是否超出阈值
+     *
+     * @param sensorData 传感器数据
+     */
+    boolean checkAlarm(SensorData sensorData);
+}

+ 37 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/SensorThresholdService.java

@@ -0,0 +1,37 @@
+package vip.xiaonuo.coldchain.core.event.alarm.service;
+
+/**
+ * @author jackzhou
+ * @version 1.0
+ * @project jfcloud-coldchain
+ * @description
+ * @date 2024/11/25 17:13:50
+ */
+
+import cn.hutool.core.bean.BeanUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import vip.xiaonuo.coldchain.core.event.alarm.bean.SensorThreshold;
+import vip.xiaonuo.coldchain.modular.monitortargetregion.entity.MonitorTargetRegion;
+import vip.xiaonuo.coldchain.modular.monitortargetregion.service.MonitorTargetRegionService;
+
+/**
+ * 从数据库获取传感器阈值的实现类
+ */
+@Service
+@RequiredArgsConstructor
+public class SensorThresholdService implements ThresholdService {
+    private final MonitorTargetRegionService monitorTargetRegionService;
+
+    @Override
+    public SensorThreshold getThresholdBySensorId(String sensorCode, Integer sensorNo) {
+        MonitorTargetRegion oneByDeviceCodeAndSensorNo = monitorTargetRegionService.findOneByDeviceCodeAndSensorNo(sensorCode, sensorNo);
+        if (oneByDeviceCodeAndSensorNo == null) {
+            return null;
+        }
+        SensorThreshold sensorThreshold = new SensorThreshold();
+        BeanUtil.copyProperties(oneByDeviceCodeAndSensorNo, sensorThreshold);
+        return sensorThreshold;
+
+    }
+}

+ 4 - 2
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/ThresholdService.java → snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/ThresholdService.java

@@ -1,4 +1,4 @@
-package vip.xiaonuo.coldchain.core.event.alarm;
+package vip.xiaonuo.coldchain.core.event.alarm.service;
 
 /**
  * @author jackzhou
@@ -8,6 +8,8 @@ package vip.xiaonuo.coldchain.core.event.alarm;
  * @date 2024/11/25 17:08:36
  */
 
+import vip.xiaonuo.coldchain.core.event.alarm.bean.SensorThreshold;
+
 /**
  * 获取传感器阈值范围的服务接口
  */
@@ -19,5 +21,5 @@ public interface ThresholdService {
      * @param sensorNo 传感器 路数
      * @return 传感器的阈值范围
      */
-    SensorThreshold getThresholdBySensorId(String sensorCode,Integer sensorNo);
+    SensorThreshold getThresholdBySensorId(String sensorCode, Integer sensorNo);
 }

+ 20 - 6
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevice/param/MonitorDeviceAddParam.java

@@ -17,6 +17,7 @@ import lombok.Getter;
 import lombok.Setter;
 
 import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
 import javax.validation.constraints.Size;
 
 /**
@@ -39,6 +40,7 @@ public class MonitorDeviceAddParam {
      */
     @Schema(description = "设备编码")
     @NotNull(message = "设备编码不能为空")
+    @Pattern(regexp = "^[0-9]+$", message = "设备编码必须是数字字符串")
     private String deviceCode;
 
     /**
@@ -68,27 +70,39 @@ public class MonitorDeviceAddParam {
     @Schema(description = "排序码")
     private Integer sortCode;
 
-    /** 报警上限 */
+    /**
+     * 报警上限
+     */
     @Schema(description = "报警上限")
     private Float temperatureUp;
 
-    /** 报警上限 */
+    /**
+     * 报警上限
+     */
     @Schema(description = "报警上限")
     private Float humidityUp;
 
-    /** 报警上限 */
+    /**
+     * 报警上限
+     */
     @Schema(description = "报警上限")
     private Float co2Up;
 
-    /** 报警下限 */
+    /**
+     * 报警下限
+     */
     @Schema(description = "报警下限")
     private Float temperatureDown;
 
-    /** 报警下限 */
+    /**
+     * 报警下限
+     */
     @Schema(description = "报警下限")
     private Float humidityDown;
 
-    /** 报警下限 */
+    /**
+     * 报警下限
+     */
     @Schema(description = "报警下限")
     private Float co2Down;
 }

+ 13 - 0
snowy-web-app/src/main/resources/application.properties

@@ -38,6 +38,19 @@ spring.data.influxdb.token=${INFLUXDB_TOKEN:1NdDyN3LCKIEBFkQ1AqAmRSitZGfdCs0nuF1
 spring.data.influxdb.org=${INFLUXDB_ORG:coldchain}
 spring.data.influxdb.bucket=${INFLUXDB_BUCKET:coldchain}
 
+# 温度超标报警模板
+coldchain.alarm.message.temperatureOverLimit=温度报警:设备【{deviceName}】的温度超标!\n当前温度:{value} {unit},已超出上限(阈值:{thresholdUp})。\n报警时间:{time}
+# 温度过低报警模板
+coldchain.alarm.message.temperatureBelowLimit=温度报警:设备【{deviceName}】的温度过低!\n当前温度:{value} {unit},已低于下限(阈值:{thresholdDown})。\n报警时间:{time}
+# 湿度超标报警模板
+coldchain.alarm.message.humidityOverLimit=湿度报警:设备【{deviceName}】的湿度超标!\n当前湿度:{value} {unit},已超出上限(阈值:{thresholdUp})。\n报警时间:{time}
+# 湿度过低报警模板
+coldchain.alarm.message.humidityBelowLimit=湿度报警:设备【{deviceName}】的湿度过低!\n当前湿度:{value} {unit},已低于下限(阈值:{thresholdDown})。\n报警时间:{time}
+# 二氧化碳超标报警模板
+coldchain.alarm.message.co2OverLimit=二氧化碳报警:设备【{deviceName}】的二氧化碳浓度超标!\n当前浓度:{value} {unit},已超出上限(阈值:{thresholdUp})。\n报警时间:{time}
+# 二氧化碳过低报警模板
+coldchain.alarm.message.co2BelowLimit=二氧化碳报警:设备【{deviceName}】的二氧化碳浓度过低!\n当前浓度:{value} {unit},已低于下限(阈值:{thresholdDown})。\n报警时间:{time}
+
 # postgres
 #spring.datasource.dynamic.datasource.master.driver-class-name=org.postgresql.Driver
 #spring.datasource.dynamic.datasource.master.url=jdbc:postgresql://localhost:5432/snowy