Sfoglia il codice sorgente

Merge branch 'master' of http://git.nzklabs.com:3000/huangyuanhao/jfcloud-coldchain

lh_hub 6 mesi fa
parent
commit
5ea2178ee9
22 ha cambiato i file con 712 aggiunte e 112 eliminazioni
  1. 1 21
      snowy-admin-web/src/views/basicset/monitor/form.vue
  2. 53 42
      snowy-admin-web/src/views/motoring/location/form.vue
  3. 5 5
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/cache/monitordevice/MonitorDeviceCacheInitializer.java
  4. 19 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/config/JfcloudColdChainConstants.java
  5. 30 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorAlarmEvent.java
  6. 26 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorAlarmEventListener.java
  7. 76 35
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorDataEventListener.java
  8. 115 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorDataEventListener2.java
  9. 64 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/bean/SensorThreshold.java
  10. 36 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/config/ColdChainAlarmMessageProperties.java
  11. 149 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/DefaultSensorAlarmChecker.java
  12. 19 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/SensorAlarmChecker.java
  13. 37 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/SensorThresholdService.java
  14. 25 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/alarm/service/ThresholdService.java
  15. 1 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevice/entity/MonitorDevice.java
  16. 20 6
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevice/param/MonitorDeviceAddParam.java
  17. 2 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevice/service/impl/MonitorDeviceServiceImpl.java
  18. 7 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/entity/MonitorDeviceType.java
  19. 5 1
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitortargetregion/service/MonitorTargetRegionService.java
  20. 7 0
      snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitortargetregion/service/impl/MonitorTargetRegionServiceImpl.java
  21. 2 0
      snowy-web-app/src/main/java/vip/xiaonuo/Application.java
  22. 13 0
      snowy-web-app/src/main/resources/application.properties

+ 1 - 21
snowy-admin-web/src/views/basicset/monitor/form.vue

@@ -25,24 +25,6 @@
 			<a-form-item label="监控位置区域">
 				<a-input v-model:value="formData.monitorPoint" placeholder="请输入监控位置区域" allow-clear />
 			</a-form-item>
-			<a-form-item label="报警上限" name="limitUp">
-				<a-input-number
-					id="inputNumber"
-					v-model:value="formData.limitUp"
-					placeholder="请输入报警上限"
-					allow-clear
-					style="width: 100%"
-				/>
-			</a-form-item>
-			<a-form-item label="报警下限" name="limitDown">
-				<a-input-number
-					id="inputNumber"
-					v-model:value="formData.limitDown"
-					placeholder="请输入报警下限"
-					allow-clear
-					style="width: 100%"
-				/>
-			</a-form-item>
 		</a-form>
 
 		<template #footer>
@@ -64,9 +46,7 @@
 	// 默认要校验的
 	const formRules = {
 		name: [required('请输入对象名称')],
-		status: [required('请选择对象状态')],
-		limitUp: [required('请输入报警上限')],
-		limitDown: [required('请输入报警下限')]
+		status: [required('请选择对象状态')]
 	}
 
 	// 表单数据

+ 53 - 42
snowy-admin-web/src/views/motoring/location/form.vue

@@ -27,7 +27,6 @@
 							v-model:value="formData.monitorTargetId"
 							:options="monitorTargetOptions"
 							placeholder="请选择监控对象"
-							@change="monitorTargetIdChange"
 						/>
 					</a-form-item>
 				</a-col>
@@ -81,7 +80,7 @@
 								v-model:value="formData.temperatureUp"
 								placeholder="请输入温度上限"
 								allow-clear
-								:min="1"
+								:min="temperatureMax.downMax"
 								:max="temperatureMax.upMax"
 								style="width: 100%"
 							/>
@@ -94,7 +93,8 @@
 								v-model:value="formData.temperatureDown"
 								placeholder="请输入温度下限"
 								allow-clear
-								:max="temperatureMax.downMax"
+								:min="temperatureMax.downMax"
+								:max="temperatureMax.upMax"
 								style="width: 100%"
 							/>
 						</a-form-item>
@@ -106,6 +106,7 @@
 								v-model:value="formData.humidityUp"
 								placeholder="请输入湿度上限"
 								allow-clear
+								:min="humidityMax.downMax"
 								:max="humidityMax.upMax"
 								style="width: 100%"
 							/>
@@ -118,7 +119,8 @@
 								v-model:value="formData.humidityDown"
 								placeholder="请输入湿度下限"
 								allow-clear
-								:max="humidityMax.downMax"
+								:min="humidityMax.downMax"
+								:max="humidityMax.upMax"
 								style="width: 100%"
 							/>
 						</a-form-item>
@@ -151,29 +153,6 @@
 						</a-form-item>
 					</a-col>
 				</div>
-
-				<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
-					<a-form-item label="报警上限" name="limitUp">
-						<a-input-number
-							id="inputNumber"
-							v-model:value="formData.limitUp"
-							placeholder="请输入报警上限"
-							allow-clear
-							style="width: 100%"
-						/>
-					</a-form-item>
-				</a-col>
-				<a-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
-					<a-form-item label="报警下限" name="limitDown">
-						<a-input-number
-							id="inputNumber"
-							v-model:value="formData.limitDown"
-							placeholder="请输入报警下限"
-							allow-clear
-							style="width: 100%"
-						/>
-					</a-form-item>
-				</a-col>
 			</a-row>
 		</a-form>
 
@@ -211,8 +190,51 @@
 		monitorDeviceId: [required('请选择监控设备')],
 		sensorType: [required('请选择传感器类型')],
 		sensorRoute: [required('请输入传感器路数')],
-		limitUp: [required('请选择报警上限')],
-		limitDown: [required('请选择报警下限')]
+		temperatureUp: [{ validator: validateTemperature, trigger: ['change', 'blur'] }],
+		temperatureDown: [{ validator: validateTemperature, trigger: ['change', 'blur'] }],
+		humidityUp: [{ validator: validateHumidity, trigger: ['change', 'blur'] }],
+		humidityDown: [{ validator: validateHumidity, trigger: ['change', 'blur'] }],
+		co2Up: [{ validator: validateCo2, trigger: ['change', 'blur'] }],
+		co2Down: [{ validator: validateCo2, trigger: ['change', 'blur'] }]
+	}
+
+	// 温度上下限校验
+	function validateTemperature(rule, value, callback) {
+		if (formData.value.temperatureDown && formData.value.temperatureUp) {
+			if (formData.value.temperatureDown >= formData.value.temperatureUp) {
+				callback(new Error('温度下限不能大于或等于温度上限'))
+			} else {
+				callback()
+			}
+		} else {
+			callback()
+		}
+	}
+
+	// 湿度上下限校验
+	function validateHumidity(rule, value, callback) {
+		if (formData.value.humidityDown && formData.value.humidityUp) {
+			if (formData.value.humidityDown >= formData.value.humidityUp) {
+				callback(new Error('湿度下限不能大于或等于湿度上限'))
+			} else {
+				callback()
+			}
+		} else {
+			callback()
+		}
+	}
+
+	// CO2上下限校验
+	function validateCo2(rule, value, callback) {
+		if (formData.value.co2Down && formData.value.co2Up) {
+			if (formData.value.co2Down >= formData.value.co2Up) {
+				callback(new Error('CO2下限不能大于或等于CO2上限'))
+			} else {
+				callback()
+			}
+		} else {
+			callback()
+		}
 	}
 
 	// 打开抽屉
@@ -256,18 +278,16 @@
 	const monitorDeviceIdChange = (value) => {
 		if (value) {
 			const data = memListOptions.value.find((item) => item.value === value)
-			formData.value.sensorCode = data.deviceCode //设备编号
+			formData.value.sensorCode = data.deviceCode //传感器编号
 			formData.value.modelName = data.modelName //监控设备型号
 			formData.value.deviceCode = data.deviceCode //冷链编号
-			sensorRouteMax.value = data.sensorCount //传感器路数的最大值
-
 			formData.value.temperatureUp = data.temperatureUp //温度上限
 			formData.value.temperatureDown = data.temperatureDown //温度下限
-
 			formData.value.humidityUp = data.humidityUp //湿度上限
 			formData.value.humidityDown = data.humidityDown //湿度下限
 			formData.value.co2Up = data.co2Up //CO2上限
 			formData.value.co2Up = data.co2Up //CO2下限
+			sensorRouteMax.value = data.sensorCount //传感器路数的最大值
 
 			temperatureMax.value = {
 				upMax: data.temperatureUp,
@@ -284,15 +304,6 @@
 		}
 	}
 
-	// 选中监控对象
-	const monitorTargetIdChange = (value) => {
-		if (value) {
-			const item = monitorTargetOptions.value.find((item) => item.value === value)
-			formData.value.limitUp = item.limitUp //监控设备型号
-			formData.value.limitDown = item.limitDown //冷链编号
-		}
-	}
-
 	// 关闭抽屉
 	const onClose = () => {
 		formRef.value.resetFields()

+ 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;
 }

+ 30 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorAlarmEvent.java

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

+ 26 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorAlarmEventListener.java

@@ -0,0 +1,26 @@
+package vip.xiaonuo.coldchain.core.event;
+
+/**
+ * @author jackzhou
+ * @version 1.0
+ * @project jfcloud-coldchain
+ * @description
+ * @date 2024/11/12 15:31:43
+ */
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class SensorAlarmEventListener {
+
+    @Async
+    @EventListener
+    public void handleSensorAlarmEvent(SensorAlarmEvent event) {
+    }
+}

+ 76 - 35
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorDataEventListener.java

@@ -1,36 +1,77 @@
-package vip.xiaonuo.coldchain.core.event;
-
-/**
- * @author jackzhou
- * @version 1.0
- * @project jfcloud-coldchain
- * @description
- * @date 2024/11/12 15:31:43
- */
-
-import com.github.jfcloud.influxdb.service.JfcloudInfluxDBService;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.event.EventListener;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.stereotype.Component;
-import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
-
-@Slf4j
-@RequiredArgsConstructor
-@Component
-public class SensorDataEventListener {
-    private final JfcloudInfluxDBService jfcloudInfluxDBService;
-
-    @Async
-    @EventListener
-    public void handleSensorDataEvent(SensorDataEvent event) {
-        SensorData sensorData = event.getSensorData();
-        try {
-            jfcloudInfluxDBService.writePojo(sensorData);
+//package vip.xiaonuo.coldchain.core.event;
+//
+///**
+// * @author jackzhou
+// * @version 1.0
+// * @project jfcloud-coldchain
+// * @description 监听传感器数据事件并异步写入 InfluxDB
+// * @date 2024/11/12 15:31:43
+// */
+//
+//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.stereotype.Component;
+//import vip.xiaonuo.coldchain.core.bean.influxdb.SensorData;
+//
+//@Slf4j
+//@RequiredArgsConstructor
+//@Component
+//public class SensorDataEventListener {
+//    private final JfcloudInfluxDBService jfcloudInfluxDBService;
+//    private final ApplicationEventPublisher eventPublisher;
+//
+//    @Async
+//    @EventListener
+//    public void handleSensorDataEvent(SensorDataEvent event) {
+//        SensorData sensorData = event.getSensorData();
+//        try {
+//            log.info("开始写入数据到 InfluxDB: {}", sensorData);
+//            jfcloudInfluxDBService.writePojo(sensorData);
 //            log.info("成功写入数据到 InfluxDB: {}", sensorData);
-        } catch (Exception e) {
-            log.error("写入数据到 InfluxDB 时出错: {}", e.getMessage());
-        }
-    }
-}
+//
+//            // 数据写入成功后检查是否需要报警
+//            checkForAlarm(sensorData);
+//
+//        } catch (Exception e) {
+//            log.error("写入数据到 InfluxDB 时出错: {}", e.getMessage(), e);
+//            // 可以根据需要选择是否抛出新的异常或进行其他处理
+//        }
+//    }
+//
+//
+//    // 检查是否需要触发报警
+//    private void checkForAlarm(SensorData sensorData) {
+//        // 设置阈值(可以根据实际情况调整这些阈值)
+//        float temperatureThreshold = 50.0f;  // 温度阈值
+//        float humidityThreshold = 90.0f;     // 湿度阈值
+//        float co2Threshold = 1000.0f;        // 二氧化碳阈值
+//        StringBuilder alarmMessage = new StringBuilder();
+//
+//        // 检查温度是否超标
+//        if (sensorData.getTemperature() != null && sensorData.getTemperature() > temperatureThreshold) {
+//            alarmMessage.append("温度超标: ").append(sensorData.getTemperature()).append("°C; ");
+//        }
+//
+//        // 检查湿度是否超标
+//        if (sensorData.getHumidity() != null && sensorData.getHumidity() > humidityThreshold) {
+//            alarmMessage.append("湿度超标: ").append(sensorData.getHumidity()).append("%; ");
+//        }
+//
+//        // 检查二氧化碳是否超标
+//        if (sensorData.getCo2() != null && sensorData.getCo2() > co2Threshold) {
+//            alarmMessage.append("二氧化碳超标: ").append(sensorData.getCo2()).append("ppm; ");
+//        }
+//        // 如果发现超标数据,发布报警事件
+//        if (!alarmMessage.isEmpty()) {
+//            // 构造报警事件
+//            SensorAlarmEvent alarmEvent = new SensorAlarmEvent(this);
+//            log.warn("数据异常,发布报警: {}", alarmMessage.toString());
+//            // 发布报警事件
+//            eventPublisher.publishEvent(alarmEvent);
+//        }
+//    }
+//}

+ 115 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/core/event/SensorDataEventListener2.java

@@ -0,0 +1,115 @@
+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.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.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;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class SensorDataEventListener2 {
+    private final JfcloudInfluxDBService jfcloudInfluxDBService;
+    private final SensorAlarmChecker sensorAlarmChecker;
+    // 使用双缓冲机制:一个队列缓存数据,另一个队列进行批量写入
+    private final Queue<SensorData> bufferQueue = new ConcurrentLinkedQueue<>();
+    private final Queue<SensorData> writeQueue = new ConcurrentLinkedQueue<>();
+
+    // 事件监听,处理传感器数据
+    @EventListener
+    public void handleSensorDataEvent(SensorDataEvent event) {
+        SensorData sensorData = event.getSensorData();
+        if (Objects.nonNull(sensorData)) {
+            // 将数据添加到缓存队列
+            addSensorDataToBufferQueue(sensorData);
+            // 获取传感器的阈值范围并检查报警
+            sensorAlarmChecker.checkAlarm(sensorData);
+        }
+    }
+
+    // 将数据添加到缓存队列中
+    public void addSensorDataToBufferQueue(SensorData sensorData) {
+        bufferQueue.offer(sensorData);
+        // 如果缓存队列的数据超过设定阈值,转移数据到写入队列
+        if (bufferQueue.size() >= JfcloudColdChainConstants.MAX_QUEUE_SIZE) {
+            transferDataToWriteQueue();
+        }
+    }
+
+    // 定期检查并批量写入数据
+    @Scheduled(fixedRate = JfcloudColdChainConstants.CHECK_INTERVAL_MS)
+    public void checkAndWriteData() {
+        // 如果缓存队列达到阈值,则将数据转移到写入队列并异步写入
+        if (bufferQueue.size() >= JfcloudColdChainConstants.MAX_QUEUE_SIZE) {
+            transferDataToWriteQueue();
+            // 异步写入数据到数据库
+            writeDataToDatabaseAsync();
+        }
+    }
+
+    // 将缓存队列的数据转移到写入队列
+    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);
+                writeQueue.clear();  // 写入完成后清空写入队列
+                log.info("数据成功写入 InfluxDB,队列已清空");
+            } catch (Exception e) {
+                log.error("写入数据到 InfluxDB 时出错: {}", e.getMessage(), e);
+                // 如果写入失败,进行重试
+                retryWriteDataToDatabase(dataList);
+            }
+        }
+    }
+
+    /**
+     * 如果写入失败,进行重试
+     */
+    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();
+                }
+            }
+        }
+    }
+}

+ 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;
+
+    }
+}

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

@@ -0,0 +1,25 @@
+package vip.xiaonuo.coldchain.core.event.alarm.service;
+
+/**
+ * @author jackzhou
+ * @version 1.0
+ * @project jfcloud-coldchain
+ * @description
+ * @date 2024/11/25 17:08:36
+ */
+
+import vip.xiaonuo.coldchain.core.event.alarm.bean.SensorThreshold;
+
+/**
+ * 获取传感器阈值范围的服务接口
+ */
+public interface ThresholdService {
+
+    /**
+     * 根据传感器 ID 获取对应的阈值范围
+     * @param sensorCode 传感器 ID
+     * @param sensorNo 传感器 路数
+     * @return 传感器的阈值范围
+     */
+    SensorThreshold getThresholdBySensorId(String sensorCode, Integer sensorNo);
+}

+ 1 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevice/entity/MonitorDevice.java

@@ -78,7 +78,7 @@ public class MonitorDevice  extends CommonEntity {
      */
     @Schema(description = "传感器路数")
     @NotNull(message = "传感器路数不能为空")
-    @Size(min = 1, max = 4, message = "传感器路数的大小必须在1到10之间")
+    @Size(min = 1, max = 4, message = "传感器路数的大小必须在1到4之间")
     private Integer sensorCount;
 
     /**

+ 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;
 }

+ 2 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevice/service/impl/MonitorDeviceServiceImpl.java

@@ -44,6 +44,7 @@ import vip.xiaonuo.coldchain.modular.monitordevice.service.MonitorDeviceService;
 import vip.xiaonuo.coldchain.modular.monitordevicetype.entity.MonitorDeviceType;
 import vip.xiaonuo.coldchain.modular.monitordevicetype.service.MonitorDeviceTypeService;
 import vip.xiaonuo.coldchain.modular.monitortargetregion.service.MonitorTargetRegionService;
+import vip.xiaonuo.common.enums.CommonDeleteFlagEnum;
 import vip.xiaonuo.common.enums.CommonSortOrderEnum;
 import vip.xiaonuo.common.exception.CommonException;
 import vip.xiaonuo.common.page.CommonPageRequest;
@@ -209,6 +210,7 @@ public class MonitorDeviceServiceImpl extends ServiceImpl<MonitorDeviceMapper, M
             queryWrapper.eq(MonitorDevice::getStatus, MonitorDeviceStatusEnum.NORMAL.getValue())
                     .orderByAsc(MonitorDevice::getSortCode);
         }
+        queryWrapper.eq(MonitorDevice::getDeleteFlag, CommonDeleteFlagEnum.NOT_DELETE);
         return list(queryWrapper);
     }
 

+ 7 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitordevicetype/entity/MonitorDeviceType.java

@@ -18,6 +18,8 @@ import lombok.Getter;
 import lombok.Setter;
 import vip.xiaonuo.common.pojo.CommonEntity;
 
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
 import java.math.BigDecimal;
 import java.util.Date;
 
@@ -45,8 +47,12 @@ public class MonitorDeviceType extends CommonEntity {
     @Schema(description = "型号编码")
     private String code;
 
-    /** 传感器路数 */
+    /**
+     * 传感器路数
+     */
     @Schema(description = "传感器路数")
+    @NotNull(message = "传感器路数不能为空")
+    @Size(min = 1, max = 4, message = "传感器路数的大小必须在1到4之间")
     private Integer sensorCount;
 
     /** 排序码 */

+ 5 - 1
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitortargetregion/service/MonitorTargetRegionService.java

@@ -92,10 +92,14 @@ public interface MonitorTargetRegionService extends IService<MonitorTargetRegion
     /**
      * 根据deviceId获取targetId列表
      */
-    List<String> getTargetIdListByDeviceId(String deviceId);
+    List<String> getTargetIdListByDeviceId(String deviceCode);
 
     /**
      * 根据监控对象id获取监控点位列表
      */
     List<MonitorTargetRegion> listByTargetId(String targetId);
+
+
+    MonitorTargetRegion findOneByDeviceCodeAndSensorNo(String deviceCode,Integer sensorNo);
+
 }

+ 7 - 0
snowy-plugin/snowy-plugin-coldchain/src/main/java/vip/xiaonuo/coldchain/modular/monitortargetregion/service/impl/MonitorTargetRegionServiceImpl.java

@@ -204,6 +204,13 @@ public class MonitorTargetRegionServiceImpl extends ServiceImpl<MonitorTargetReg
         return list(queryWrapper);
     }
 
+    @Override
+    public MonitorTargetRegion findOneByDeviceCodeAndSensorNo(String deviceCode, Integer sensorNo) {
+        LambdaQueryWrapper<MonitorTargetRegion> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(MonitorTargetRegion::getDeviceCode, deviceCode).eq(MonitorTargetRegion::getSensorRoute,sensorNo);
+        return getOne(queryWrapper);
+    }
+
     private void fillParentLocationInfo(List<MonitorTargetRegion> resourceList) {
         if (CollUtil.isNotEmpty(resourceList)) {
             List<MonitorTargetRegion> locationTypes = resourceList.stream().filter(distinctByKey(MonitorTargetRegion::getParentId)).collect(Collectors.toList());

+ 2 - 0
snowy-web-app/src/main/java/vip/xiaonuo/Application.java

@@ -20,6 +20,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.core.env.Environment;
 import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -33,6 +34,7 @@ import org.springframework.web.bind.annotation.RestController;
 @RestController
 @SpringBootApplication(scanBasePackages = {"vip.xiaonuo","com.github.jfcloud"})
 @EnableAsync
+@EnableScheduling
 public class Application {
 
     /* 解决druid 日志报错:discard long time none received connection:xxx */

+ 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