Przeglądaj źródła

Merge branch 'fu-dev' of uskycloud/usky-modules into master

hanzhengyi 16 godzin temu
rodzic
commit
09f181493a
21 zmienionych plików z 1110 dodań i 374 usunięć
  1. 5 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/BaseDataController.java
  2. 1 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/CdiDeliveryLogService.java
  3. 6 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/DeviceFieldConfig.java
  4. 4 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/AlarmType.java
  5. 15 8
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataSyncService.java
  6. 10 10
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataTransferService.java
  7. 62 35
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java
  8. 368 221
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java
  9. 134 11
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java
  10. 177 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/BasementClimateUtil.java
  11. 173 42
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java
  12. 5 5
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataSyncService.java
  13. 44 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/FixedWaterLevelGenerator.java
  14. 10 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/SnowflakeIdGenerator.java
  15. 4 4
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/alarm/AlarmMessageVO.java
  16. 42 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/base/BaseMqttInfo.java
  17. 2 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/EngineeringBaseVO.java
  18. 5 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/FacilityDeviceVO.java
  19. 2 15
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/FloorPlaneVO.java
  20. 2 15
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/ProtectiveUnitVO.java
  21. 39 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/WaterLevelVO.java

+ 5 - 2
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/BaseDataController.java

@@ -78,8 +78,11 @@ public class BaseDataController {
      * 批量上报智能监管物联设施信息
      */
     @GetMapping("/sensorInfos")
-    public String batchSendSensorInfos(@RequestParam(value = "tenantId",required = false) Integer tenantId) {
-        Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId);
+    public String batchSendSensorInfos(@RequestParam(value = "tenantId",required = false) Integer tenantId,
+                                       @RequestParam(value = "engineeringId") Long engineeringId,
+                                       @RequestParam(value = "username") String username,
+                                       @RequestParam(value = "password") String password) {
+        Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId, engineeringId, username, password);
         return String.format("上报成功 %d", map.get("success"));
     }
 }

+ 1 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/CdiDeliveryLogService.java

@@ -27,5 +27,5 @@ public interface CdiDeliveryLogService extends CrudService<CdiDeliveryLog> {
 
     // 存储日志
     void saveLog(String topic, String dataTypeName, Integer dataType, Integer tenantId, Long engineeringId, LocalDateTime now, long startTime, long endTime,
-                 int total, int success, int failure, int notSynced, int pushFlag);
+                 int total, int success, int failure, int notSynced, int pushFlag, String userName);
 }

+ 6 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/DeviceFieldConfig.java

@@ -32,6 +32,9 @@ public class DeviceFieldConfig {
         // 电气火灾(704)
         fieldMapping.put("704", "voltage_a,voltage_b,voltage_c,current_a,current_b,current_c,temperature_a,temperature_b,temperature_c,current_residual,active_power");
 
+        // 电能采集(705)
+        fieldMapping.put("705", "electrical_energy");
+
         // 温度传感器(707)
         fieldMapping.put("707", "wd");
 
@@ -56,6 +59,9 @@ public class DeviceFieldConfig {
         // 位移传感器
         fieldMapping.put("714", "wy");
 
+        // 液位
+        fieldMapping.put("716", "sensorValue");
+
         return fieldMapping;
     }
 }

+ 4 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/AlarmType.java

@@ -46,7 +46,10 @@ public enum AlarmType {
     TILT("712", "34.1", Category.STRUCTURE_MONITORING, "有倾斜"),
 
     // 裂缝检测告警
-    CRACK("713", "36.1", Category.STRUCTURE_MONITORING, "有裂缝");
+    CRACK("713", "36.1", Category.STRUCTURE_MONITORING, "有裂缝"),
+
+    // 集水井水位偏高
+    SEWAGE_LEVEL("716", "11.1", Category.STRUCTURE_MONITORING, "集水井水位偏高");
 
     private final String originalCode;
     private final String mappedCode;

+ 15 - 8
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataSyncService.java

@@ -12,6 +12,7 @@ import com.usky.cdi.service.mqtt.MqttConnectionTool;
 import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
 import com.usky.cdi.service.enums.AlarmType;
+import com.usky.common.security.utils.SecurityUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -21,11 +22,9 @@ import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
+import java.text.SimpleDateFormat;
 import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -49,6 +48,7 @@ public class AlarmDataSyncService {
     private static final String WARTIME = "wartime";
     private static final String ALARM_DELIVERY_KEY_PREFIX = "alarm:delivery:";
     private final MqttConnectionTool mqttConnectionTool;
+    private final SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
 
     @Autowired
     private StringRedisTemplate stringRedisTemplate;
@@ -69,8 +69,8 @@ public class AlarmDataSyncService {
     /**
      * 获取当前时间
      */
-    private LocalDateTime getCurrentTime() {
-        return LocalDateTime.now();
+    private String getCurrentTime() {
+        return timeFormat.format(new Date());
     }
 
     /**
@@ -117,6 +117,13 @@ public class AlarmDataSyncService {
         String topic = MqttTopics.Alarm.MESSAGE.getTopic();
         String desc = MqttTopics.Alarm.MESSAGE.getDesc();
 
+        String userName = "自动同步";
+        try {
+            userName = SecurityUtils.getUsername();
+        } catch (Exception e) {
+            log.error("定时任务无法获取用户名,使用默认‘自动同步’", e);
+        }
+
         try {
             // 2.创建MQTT连接
             mqttConnectionTool.connectOrRefresh(username, password);
@@ -198,7 +205,7 @@ public class AlarmDataSyncService {
             endTime = System.currentTimeMillis();
 
             cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
-                    successCount, failureCount, size - successCount - failureCount, 1);
+                    successCount, failureCount, size - successCount - failureCount, 1, userName);
         } catch (Exception e) {
             log.error("租户{}的告警数据推送定时任务执行失败:{}", tenantId, e.getMessage(), e);
         } finally {
@@ -206,7 +213,7 @@ public class AlarmDataSyncService {
             log.info("结束时间:{}, 耗时:{}ms", getCurrentTime(), endTime - startTime);
 
             cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
-                    successCount, failureCount, size - successCount - failureCount, 0);
+                    successCount, failureCount, size - successCount - failureCount, 0, userName);
         }
     }
 

+ 10 - 10
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataTransferService.java

@@ -53,7 +53,7 @@ public class AlarmDataTransferService {
      * 生成数据包ID
      */
     private Long generateDataPacketID() {
-        return idGenerator.nextPacketId();
+        return idGenerator.nextPacketId10();
     }
 
     /**
@@ -69,7 +69,7 @@ public class AlarmDataTransferService {
                 vo.setDataPacketID(generateDataPacketID());
             }
             if (vo.getPublishTime() == null) {
-                vo.setPublishTime(LocalDateTime.now());
+                vo.setPublishTime(getCurrentTime());
             }
 
 //            HashMap<String, Object> map = new HashMap<>();
@@ -87,7 +87,7 @@ public class AlarmDataTransferService {
             JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
             String json = jsonObject.toJSONString();
             System.out.println(json);
-            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101070011", "5RqhJ7VG");
+            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101100017", "gjB4v1bh");
             String topic = "alarm/message";
             IQeIRyXG.sendToMqtt(topic, json);
 
@@ -106,17 +106,16 @@ public class AlarmDataTransferService {
      * @return 是否发送成功
      */
     public boolean sendAlarmMessage1(AlarmMessageVO vo) {
-        MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101070011", "5RqhJ7VG");
 
         try {
             if (vo.getDataPacketID() == null) {
                 vo.setDataPacketID(generateDataPacketID());
             }
             if (vo.getPublishTime() == null) {
-                vo.setPublishTime(LocalDateTime.now());
+                vo.setPublishTime(getCurrentTime());
             }
 
-            HashMap<String, Object> map = new HashMap<>();
+            // HashMap<String, Object> map = new HashMap<>();
 //            map.put("dataPacketID", vo.getDataPacketID());
 //            map.put("engineeringID", vo.getEngineeringID());
 //            map.put("floor", vo.getFloor());
@@ -132,6 +131,7 @@ public class AlarmDataTransferService {
             String json = jsonObject.toJSONString();
             System.out.println(json);
             String topic = "alarm/message";
+            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101100017", "gjB4v1bh");
             IQeIRyXG.sendToMqtt(topic, json);
 
             return true;
@@ -147,13 +147,13 @@ public class AlarmDataTransferService {
                 vo.setDataPacketID(generateDataPacketID());
             }
             if (vo.getPublishTime() == null) {
-                vo.setPublishTime(LocalDateTime.now());
+                vo.setPublishTime(getCurrentTime());
             }
 
             JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
             String json = jsonObject.toJSONString();
             System.out.println(json);
-            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101070011", "5RqhJ7VG");
+            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101100017", "gjB4v1bh");
             String topic = "alarm/message";
             IQeIRyXG.sendToMqtt(topic, json);
 
@@ -170,10 +170,10 @@ public class AlarmDataTransferService {
                 vo.setDataPacketID(generateDataPacketID());
             }
             if (vo.getPublishTime() == null) {
-                vo.setPublishTime(LocalDateTime.now());
+                vo.setPublishTime(getCurrentTime());
             }
 
-            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101070011", "5RqhJ7VG");
+            MqttConnectionTool.MqttGateway IQeIRyXG = mqttConnectionTool.connectOrRefresh("3101100017", "3101100017");
 
             JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
             String json = jsonObject.toJSONString();

+ 62 - 35
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java

@@ -3,13 +3,15 @@ package com.usky.cdi.service.impl;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.nacos.shaded.com.google.gson.Gson;
+import com.alibaba.nacos.shaded.com.google.gson.GsonBuilder;
+import com.alibaba.nacos.shaded.com.google.gson.LongSerializationPolicy;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.usky.cdi.domain.BaseBuildFacility;
 import com.usky.cdi.domain.DmpDevice;
 import com.usky.cdi.service.BaseBuildFacilityService;
-import com.usky.cdi.service.CdiDeliveryLogService;
 import com.usky.cdi.service.DmpDeviceInfoService;
 import com.usky.cdi.service.config.mqtt.MqttOutConfig;
+import com.usky.cdi.service.mqtt.MqttConnectionTool;
 import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.info.EngineeringBaseVO;
 import com.usky.cdi.service.vo.info.FacilityDeviceVO;
@@ -22,9 +24,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 
 /**
@@ -48,8 +55,11 @@ public class BaseDataTransferService {
     @Resource
     private MqttOutConfig.MqttGateway mqttGateway;
 
-    @Value("${config.engineeringID}")
-    private String engineeringID;
+    // @Value("${config.engineeringID}")
+    // private String engineeringID;
+
+    @Autowired
+    private MqttConnectionTool mqttConnectionTool;
 
     private final SnowflakeIdGenerator idGenerator;
     private final SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@@ -122,7 +132,8 @@ public class BaseDataTransferService {
             String topic = "base/protectiveUnit";
 
             log.info("发送防护单元基础信息,Topic: {}, Data: {}", topic, json);
-            mqttGateway.sendToMqtt(topic, json);
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, json);
 
             return true;
         } catch (Exception e) {
@@ -140,57 +151,67 @@ public class BaseDataTransferService {
      */
     public boolean sendFloorPlane(FloorPlaneVO vo) {
         try {
+            // ========== 1. 基础参数填充 ==========
             if (vo.getDataPacketID() == null) {
                 vo.setDataPacketID(generateDataPacketID());
             }
             if (vo.getPublishTime() == null) {
-                vo.setPublishTime(getCurrentTime());
+                vo.setPublishTime(timeFormat.format(new Date()));
             }
 
-            String imagePath = "D://games/3492.jpg";
-            // 将图片文件读取为字节数组
+            // ========== 2. 读取本地图片 ==========
+            String imagePath = "C:\\Users\\f\\Downloads\\45_平面图.jpg";
             byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
 
-            // 检查文件大小(不超过5MB)
-            if (vo.getFloorFile() != null && imageBytes.length > 5 * 1024 * 1024) {
-                log.error("楼层平面图文件大小超过5MB限制,FileID: {}", vo.getFloorFileID());
+            // 大小校验 ≤5MB
+            if (imageBytes.length > 5 * 1024 * 1024) {
+                System.err.println("文件超过5MB");
                 return false;
             }
 
-            HashMap<String, Object> map = new HashMap<>();
+            // 格式校验
+            if (!Arrays.asList("jpg", "jpeg", "png").contains(vo.getFloorFileSuffix().toLowerCase())) {
+                System.err.println("不支持的格式");
+                return false;
+            }
+
+            // 获取宽高
+            BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes));
+            int width = image.getWidth();
+            int height = image.getHeight();
+
+            // ========== 3. 时间格式化 ==========
+
+            // ========== 4. 构建标准JSON消息体 ==========
+            Map<String, Object> map = new HashMap<>();
             map.put("dataPacketID", vo.getDataPacketID());
             map.put("engineeringID", vo.getEngineeringID());
             map.put("floor", vo.getFloor());
             map.put("floorFileID", vo.getFloorFileID());
             map.put("floorFileName", vo.getFloorFileName());
             map.put("floorFileSuffix", vo.getFloorFileSuffix());
-            map.put("filePixWidth", vo.getFilePixWidth());
-            map.put("filePixHeight", vo.getFilePixHeight());
+            map.put("filePixWidth", width);
+            map.put("filePixHeight", height);
             map.put("floorFile", imageBytes);
             map.put("publishTime", vo.getPublishTime());
+
+            //使用Gson:
             Gson gson = new Gson();
-            // 将字节数组转换为Base64编码
-            JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
-            vo.setFloorFile(imageBytes);
-//            jsonObject.put("floorFile", imageBytes);
-            if (vo.getFloorFile() != null) {
-                // 使用Base64编码传输二进制数据
-                String base64File = java.util.Base64.getEncoder().encodeToString(vo.getFloorFile());
-                jsonObject.put("floorFile", imageBytes);
-            }
 
-            String json = jsonObject.toJSONString();
-            System.out.println(gson.toJson(map));
+            // ========== 5. MQTT发送(修复版) ==========
             String topic = "base/floorPlane";
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh("3101100021", "SIixzph1");
 
-            log.info("发送楼层平面图信息,Topic: {}, FileID: {}, FileSize: {} bytes",
-                    topic, vo.getFloorFileID(),
-                    vo.getFloorFile() != null ? vo.getFloorFile().length : 0);
-            mqttGateway.sendToMqtt(topic, gson.toJson(map));
+            // 发送JSON字符串
+            gateway.sendToMqtt(topic, gson.toJson(map));
 
+            System.out.println("✅ MQTT发送成功 TOPIC: " + topic);
             return true;
+
         } catch (Exception e) {
-            log.error("发送楼层平面图信息失败,FileID: {}", vo.getFloorFileID(), e);
+            // 打印完整异常
+            e.printStackTrace();
+            System.err.println("❌ 发送失败:" + e.getMessage());
             return false;
         }
     }
@@ -223,19 +244,21 @@ public class BaseDataTransferService {
             userIdToName.put(702, 31);
             userIdToName.put(703, 33);
             userIdToName.put(704, 11);
+            userIdToName.put(705, 11);
             userIdToName.put(707, 19);
             userIdToName.put(708, 19);
             userIdToName.put(709, 15);
             userIdToName.put(710, 16);
             userIdToName.put(711, 2);
-            //userIdToName.put(712, 34);
-            //userIdToName.put(713, 36);
+            // userIdToName.put(712, 34);
+            // userIdToName.put(713, 36);
             userIdToName.put(714, 37);
+            userIdToName.put(716, 26);
 
             HashMap<String, Object> map = new HashMap<>();
             map.put("dataPacketID", generateDataPacketID());
-            map.put("engineeringID", Long.parseLong(engineeringID));
-            map.put("floor", "B2");
+            map.put("engineeringID", vo.getEngineeringID());
+            map.put("floor", vo.getFloor());
             map.put("floorFileID", 1);
             map.put("sensorID", Integer.parseInt(vo.getDeviceId()));
             map.put("sensorNo", vo.getDeviceUuid());
@@ -253,7 +276,8 @@ public class BaseDataTransferService {
             String topic = "base/sensorInfo";
             System.out.println(gson.toJson(map));
 //            log.info("发送智能监管物联设施信息,Topic: {}, SensorID: {}", topic, vo.getSensorID());
-            mqttGateway.sendToMqtt(topic, gson.toJson(map));
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, gson.toJson(map));
 
             return true;
         } catch (Exception e) {
@@ -290,7 +314,7 @@ public class BaseDataTransferService {
      * @param tenantId 租户ID
      * @return 成功发送的数量
      */
-    public Map<String, Integer> batchSendSensorInfos(Integer tenantId) {
+    public Map<String, Integer> batchSendSensorInfos(Integer tenantId, Long engineeringId, String username, String password) {
         List<BaseBuildFacility> list = baseBuildFacilityService.facilityInfo(tenantId);
         List<DmpDevice> list1 = dmpDeviceInfoService.deviceInfo(tenantId);
         List<FacilityDeviceVO> list2 = new ArrayList<>();
@@ -309,6 +333,9 @@ public class BaseDataTransferService {
                         facilityDeviceVO.setDeviceUuid(list1.get(k).getDeviceUuid());
                         facilityDeviceVO.setFacilityDesc(list.get(j).getFacilityDesc());
                         facilityDeviceVO.setDeviceType(list1.get(k).getDeviceType());
+                        facilityDeviceVO.setEngineeringID(engineeringId);
+                        facilityDeviceVO.setUserName(username);
+                        facilityDeviceVO.setPassword(password);
                         list2.add(facilityDeviceVO);
                     }
                 }

+ 368 - 221
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java

@@ -3,12 +3,13 @@ package com.usky.cdi.service.impl;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
-import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
+import com.alibaba.nacos.shaded.com.google.gson.Gson;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -17,6 +18,7 @@ import com.usky.cdi.service.enums.MqttTopics;
 import com.usky.cdi.mapper.*;
 import com.usky.cdi.service.CdiDeliveryLogService;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
+import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.SyncTaskStatisticsVO;
 import com.usky.cdi.service.vo.info.FloorPlaneVO;
 import com.usky.cdi.service.vo.info.ProtectiveUnitVO;
@@ -29,15 +31,20 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Value;
 
+import javax.annotation.PostConstruct;
 import javax.imageio.ImageIO;
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
 import java.math.BigDecimal;
+import java.net.HttpURLConnection;
 import java.net.URL;
 import java.net.URLConnection;
+import java.net.URLEncoder;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
@@ -84,108 +91,60 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
     @Autowired
     private MqttConnectionTool mqttConnectionTool;
 
+    // 从配置文件读取Snowflake参数,默认值为1
+    @Value("${snowflake.worker-id:1}")
+    private long workerId;
+
+    @Value("${snowflake.data-center-id:1}")
+    private long dataCenterId;
+
+    private SnowflakeIdGenerator idGenerator;
+
+    @PostConstruct
+    public void init() {
+        this.idGenerator = new SnowflakeIdGenerator(workerId, dataCenterId);
+    }
+
+    /**
+     * 生成数据包ID
+     */
+    private Long generateDataPacketID() {
+        return idGenerator.nextPacketId();
+    }
+
     @Override
     public List<SyncTaskStatisticsVO> selectById(Long id) {
-        // 1. 租户ID校验(必须非空,无租户直接返回空列表)
         Integer tenantId = SecurityUtils.getTenantId();
-        if (tenantId == null) {
+        if (tenantId == null || tenantId <= 0) {
             log.warn("未获取到当前租户ID,无法查询人防投递日志");
             return Collections.emptyList();
         }
 
-        // 2. 动态构建查询条件:id为null时只查租户,id不为null时租户+id精准查
-        // 【小优化】按ID倒序,后续取最新数据更直观(ID自增则大ID是最新)
         List<CdiDeliveryLog> logList = lambdaQuery()
                 .eq(CdiDeliveryLog::getTenantId, tenantId)
                 .eq(id != null, CdiDeliveryLog::getId, id)
-                .orderByDesc(CdiDeliveryLog::getId) // 改为倒序,优先最新数据
+                .orderByDesc(CdiDeliveryLog::getId)
                 .list();
 
         LambdaQueryWrapper<CdiDefenseProject> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(CdiDefenseProject::getTenantId, tenantId);
-        // 【空指针防护】新增非空判断,避免selectOne返回null时报错
         CdiDefenseProject defenseProject = cdiDefenseProjectMapper.selectOne(queryWrapper);
         boolean isEnable = defenseProject != null && defenseProject.getIsEnable() == 0;
 
         List<SyncTaskStatisticsVO> finalResult = new ArrayList<>();
 
-        // 3. 日志集合判空:无数据查询设备表(原逻辑不变)
-        if (CollectionUtils.isEmpty(logList)) {
-
-            // 单元数据
-            List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
-            SyncTaskStatisticsVO vo1 = new SyncTaskStatisticsVO();
-            vo1.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
-            vo1.setTopic(MqttTopics.Base.PROTECTIVE_UNIT.getTopic());
-            vo1.setDataType(1);
-            vo1.setTotal(buildUnitList.size());
-            vo1.setSuccessNumber(0);
-            vo1.setFailNumber(0);
-            vo1.setNotSynced(0);
-            vo1.setState(isEnable ? 1 : 0);
-            finalResult.add(vo1);
-
-            // 平面图
-            List<Integer> buildIdList = getBuildList(tenantId).stream().map(BaseBuild::getId).collect(Collectors.toList());
-            List<BaseBuildPlane> buildPlaneList = getBuildPlaneList(buildIdList);
-            SyncTaskStatisticsVO vo4 = new SyncTaskStatisticsVO();
-            vo4.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
-            vo4.setTopic(MqttTopics.Base.FLOOR_PLANE.getTopic());
-            vo4.setDataType(2);
-            vo4.setTotal(buildPlaneList.size());
-            vo4.setSuccessNumber(0);
-            vo4.setFailNumber(0);
-            vo4.setNotSynced(0);
-            vo4.setState(isEnable ? 1 : 0);
-            finalResult.add(vo4);
-
-            // 设施数据
-            List<BaseBuildFacility> buildFacilityList = getBuildFacilityList(tenantId);
-            SyncTaskStatisticsVO vo2 = new SyncTaskStatisticsVO();
-            vo2.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
-            vo2.setTopic(MqttTopics.Base.SENSOR_INFO.getTopic());
-            vo2.setDataType(3);
-            vo2.setTotal(buildFacilityList.size());
-            vo2.setSuccessNumber(0);
-            vo2.setFailNumber(0);
-            vo2.setNotSynced(0);
-            vo2.setState(isEnable ? 1 : 0);
-            finalResult.add(vo2);
-
-            // 监测数据
-            SyncTaskStatisticsVO vo3 = new SyncTaskStatisticsVO();
-            vo3.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
-            vo3.setTopic(MqttTopics.IotInfo.MONITORING_DATA.getTopic());
-            vo3.setDataType(4);
-            vo3.setTotal(buildFacilityList.size());
-            vo3.setSuccessNumber(0);
-            vo3.setFailNumber(0);
-            vo3.setNotSynced(0);
-            vo3.setState(isEnable ? 1 : 0);
-            finalResult.add(vo3);
-
-            SyncTaskStatisticsVO vo5 = new SyncTaskStatisticsVO();
-            vo5.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
-            vo5.setTopic(MqttTopics.Alarm.MESSAGE.getTopic());
-            vo5.setDataType(5);
-            vo5.setTotal(0);
-            vo5.setSuccessNumber(0);
-            vo5.setFailNumber(0);
-            vo5.setNotSynced(0);
-            vo5.setState(isEnable ? 1 : 0);
-            finalResult.add(vo5);
+        // 预先查询各类型的基础数据量(无日志时使用)
+        List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
+        List<BaseBuild> buildList = getBuildList(tenantId);
+        List<Integer> buildIdList = buildList.stream().map(BaseBuild::getId).collect(Collectors.toList());
+        List<BaseBuildPlane> buildPlaneList = buildIdList.isEmpty() ? Collections.emptyList() : getBuildPlaneList(buildIdList);
+        List<BaseBuildFacility> buildFacilityList = getBuildFacilityList(tenantId);
 
+        if (CollectionUtils.isEmpty(logList)) {
+            fillEmptyStatistics(finalResult, tenantId, isEnable);
             return finalResult;
         }
 
-        // 优先自动解析:JSON数组直接转VO列表,简洁高效
-        //     JSONArray jsonArray = JSONUtil.parseArray(jsonContent);
-        //     List<SyncTaskStatisticsVO> autoParseList = JSONUtil.toList(jsonArray, SyncTaskStatisticsVO.class);
-        //     finalResult.addAll(autoParseList);
-        //     log.info("租户ID:{} 日志ID:{} 自动解析成功,解析出{}条同步统计数据", tenantId, logId, autoParseList.size());
-
-        // ########## 核心改造:按dataType分组,取每种类型最新的一条日志 ##########
-        // 步骤1:先过滤出infoContent非空的有效日志(提前过滤,减少分组计算量)
         List<CdiDeliveryLog> validLogList = logList.stream()
                 .filter(logEntity -> logEntity != null
                         && StrUtil.isNotBlank(logEntity.getInfoContent())
@@ -193,130 +152,185 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 .collect(Collectors.toList());
 
         if (CollectionUtils.isEmpty(validLogList)) {
-            log.warn("租户ID:{} 查询到{}条日志,但所有日志的JSON内容为空或dataType为空,查询ID:{}", tenantId, logList.size(), id);
-            return Collections.emptyList();
+            log.warn("租户ID:{} 查询到{}条日志,但所有日志的JSON内容为空或dataType为空", tenantId, logList.size());
+            fillEmptyStatistics(finalResult, tenantId, isEnable);
+            return finalResult;
         }
 
-        // 步骤2:按dataType分组,每组按ID倒序(最新)取第一条,保证1-4类型各一条
-        Map<Integer, CdiDeliveryLog> latestLogByType = validLogList.stream()
-                .collect(Collectors.groupingBy(
-                        CdiDeliveryLog::getDataType, // 分组键:dataType(1-4)
-                        Collectors.collectingAndThen(
-                                Collectors.maxBy(Comparator.comparingLong(CdiDeliveryLog::getId)), // 取组内ID最大的(最新)
-                                opt -> opt.orElse(null) // 空值处理
-                        )
-                ));
+        // ★ 改动1:按 dataType 分组,保留每种类型的全部日志
+        Map<Integer, List<CdiDeliveryLog>> allLogsByType = validLogList.stream()
+                .collect(Collectors.groupingBy(CdiDeliveryLog::getDataType));
 
-        // 步骤3:转换为列表,仅保留1-4类型的最新日志(过滤非目标类型)
-        List<CdiDeliveryLog> finalValidLogList = new ArrayList<>();
         for (int type = 1; type <= 5; type++) {
-            CdiDeliveryLog latestLog = latestLogByType.get(type);
-            if (latestLog != null) {
-                finalValidLogList.add(latestLog);
+            List<CdiDeliveryLog> logsForType = allLogsByType.get(type);
+
+            // 创建基础 VO,设置 dataType 和 topic
+            SyncTaskStatisticsVO vo = createEmptyVO(type, isEnable);
+
+            // ★ total 始终从数据库查询获取(单元表、平面图表等),与日志记录无关
+            switch (type) {
+                case 1:
+                    vo.setTotal(buildUnitList.size());
+                    break;
+                case 2:
+                    vo.setTotal(buildPlaneList.size());
+                    break;
+                case 3:
+                case 4:
+                    vo.setTotal(buildFacilityList.isEmpty() ? 0 : buildFacilityList.size());
+                    break;
+                case 5:
+                default:
+                    break;
             }
-            // 若某类型无日志,无需处理:后续解析后若该类型无数据,是否补空VO可按需求调整,原逻辑是返回解析到的内容
-        }
 
-        if (CollectionUtils.isEmpty(finalValidLogList)) {
-            log.warn("租户ID:{} 无1-4类型的有效日志,查询ID:{}", tenantId, id);
-            return Collections.emptyList();
-        }
-        // ########## 核心改造结束 ##########
-
-        // 5. 遍历【每种类型最新的日志】,解析JSON并合并结果(单日志解析失败不影响其他)
-        for (CdiDeliveryLog deliveryLog : finalValidLogList) {
-            Long logId = deliveryLog.getId();
-            String jsonContent = deliveryLog.getInfoContent();
-            try {
-                JSONArray array = JSONUtil.parseArray(jsonContent);
-                List<SyncTaskStatisticsVO> manualParseList = new ArrayList<>(array.size());
-                for (int i = 0; i < array.size(); i++) {
-                    JSONObject obj = array.getJSONObject(i);
-                    SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
-                    // 手动映射字段:按实际入库的JSON字段名匹配
-                    vo.setId(obj.getLong("id"));
-                    vo.setTopic(obj.getStr("topic"));
-                    switch (obj.getInt("dataType")) {
-                        case 1:
-                            vo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
-                            break;
-                        case 2:
-                            vo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
-                            break;
-                        case 3:
-                            vo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
-                            break;
-                        case 4:
-                            vo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
-                            break;
-                        case 5:
-                            vo.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
-                            break;
+            if (logsForType != null && !logsForType.isEmpty()) {
+                // ★ 有日志时:按类型累加 success / fail / notSynced
+                int sumSuccess = 0, sumFail = 0, sumNotSynced = 0;
+
+                for (CdiDeliveryLog logEntry : logsForType) {
+                    try {
+                        // ★ infoContent 是 JSON 对象,用 parseObj 解析
+                        JSONObject o = JSONUtil.parseObj(logEntry.getInfoContent());
+                        sumSuccess += o.getInt("successNumber");
+                        sumFail += o.getInt("failNumber");
+                        sumNotSynced += o.getInt("notSynced");
+                    } catch (Exception ignored) {
+                        // 单条解析失败不影响其他日志的累加
+                    }
+                }
+
+                vo.setSuccessNumber(sumSuccess);
+                vo.setFailNumber(sumFail);
+                vo.setNotSynced(sumNotSynced);
+
+                // 取最新一条日志作为元数据来源(createTime / costTime 等)
+                CdiDeliveryLog latestLog = logsForType.stream()
+                        .max(Comparator.comparingLong(CdiDeliveryLog::getId))
+                        .orElse(null);
+
+                if (latestLog != null) {
+                    try {
+                        // ★ infoContent 是 JSON 对象,用 parseObj 解析
+                        JSONObject obj = JSONUtil.parseObj(latestLog.getInfoContent());
+                        vo.setId(obj.getLong("id"));
+                        vo.setCreateTime(obj.getStr("createTime"));
+                        vo.setCostTime(obj.getFloat("costTime"));
+                        vo.setState(isEnable ? 1 : obj.getInt("state"));
+                    } catch (Exception ex) {
+                        log.error("租户ID:{} dataType:{} 解析最新日志元数据失败", tenantId, type, ex);
                     }
-                    vo.setDataType(obj.getInt("dataType"));
-                    vo.setTotal(obj.getInt("total"));
-                    vo.setSuccessNumber(obj.getInt("successNumber"));
-                    vo.setFailNumber(obj.getInt("failNumber"));
-                    vo.setNotSynced(obj.getInt("notSynced"));
-                    vo.setCreateTime(obj.getStr("createTime"));
-                    vo.setCostTime(obj.getFloat("costTime"));
-                    vo.setState(isEnable ? 1 : obj.getInt("state"));
-                    manualParseList.add(vo);
                 }
-                finalResult.addAll(manualParseList);
-                log.info("租户ID:{} 日志ID:{} 手动解析成功,解析出{}条同步统计数据", tenantId, logId, manualParseList.size());
-            } catch (Exception ex) {
-                // 单日志解析失败,仅打印日志,继续解析其他日志
-                log.error("租户ID:{} 日志ID:{} 解析失败,跳过该日志", tenantId, logId, ex);
+
+                log.info("租户ID:{} dataType:{} 共{}条日志, 聚合结果: total={}, success={}, fail={}, notSynced={}",
+                        tenantId, type, logsForType.size(), vo.getTotal(), sumSuccess, sumFail, sumNotSynced);
+
+            } else {
+                // ★ 无日志时:success/fail/notSynced 保持为0(createEmptyVO已设置)
             }
+
+            finalResult.add(vo);
         }
 
-        // // 6. 【可选优化】若解析后部分类型缺失,补全空VO(和无日志时格式完全一致)
-        // // 提取已解析的dataType
-        // Set<Integer> parsedTypes = finalResult.stream()
-        //         .map(SyncTaskStatisticsVO::getDataType)
-        //         .filter(Objects::nonNull)
-        //         .collect(Collectors.toSet());
-        // // 补全1-4中缺失的类型,设置默认值(和无日志时一致)
-        // for (int type = 1; type <= 3; type++) {
-        //     if (!parsedTypes.contains(type)) {
-        //         SyncTaskStatisticsVO emptyVo = new SyncTaskStatisticsVO();
-        //         // 按类型设置名称、主题,和情况一保持一致
-        //         switch (type) {
-        //             case 1:
-        //                 emptyVo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
-        //                 emptyVo.setTopic(MqttTopics.Base.PROTECTIVE_UNIT.getTopic());
-        //                 break;
-        //             case 2:
-        //                 emptyVo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
-        //                 emptyVo.setTopic(MqttTopics.Base.FLOOR_PLANE.getTopic());
-        //                 break;
-        //             case 3:
-        //                 emptyVo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
-        //                 emptyVo.setTopic(MqttTopics.Base.SENSOR_INFO.getTopic());
-        //                 break;
-        //             // case 4:
-        //             //     emptyVo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
-        //             //     emptyVo.setTopic(MqttTopics.IotInfo.MONITORING_DATA.getTopic());
-        //             //     break;
-        //         }
-        //         emptyVo.setDataType(type);
-        //         emptyVo.setTotal(0);
-        //         emptyVo.setSuccessNumber(0);
-        //         emptyVo.setFailNumber(0);
-        //         emptyVo.setNotSynced(0);
-        //         emptyVo.setState(isEnable ? 1 : 0);
-        //         finalResult.add(emptyVo);
-        //     }
-        // }
-
-        // 对结果按dataType排序(1-4),和情况一返回顺序一致
         finalResult.sort(Comparator.comparingInt(SyncTaskStatisticsVO::getDataType));
-
-        // 6. 返回结果
         return finalResult;
     }
 
+    private void fillEmptyStatistics(List<SyncTaskStatisticsVO> result, Integer tenantId, boolean isEnable) {
+        List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
+        SyncTaskStatisticsVO vo1 = createEmptyVO(1, isEnable);
+        vo1.setTotal(buildUnitList.size());
+        result.add(vo1);
+
+        List<Integer> buildIdList = getBuildList(tenantId).stream().map(BaseBuild::getId).collect(Collectors.toList());
+        List<BaseBuildPlane> buildPlaneList = buildIdList.isEmpty() ? Collections.emptyList() : getBuildPlaneList(buildIdList);
+        SyncTaskStatisticsVO vo2 = createEmptyVO(2, isEnable);
+        vo2.setTotal(buildPlaneList.size());
+        result.add(vo2);
+
+        List<BaseBuildFacility> buildFacilityList = getBuildFacilityList(tenantId);
+        SyncTaskStatisticsVO vo3 = createEmptyVO(3, isEnable);
+        vo3.setTotal(buildFacilityList.isEmpty() ? 0 : buildFacilityList.size());
+        result.add(vo3);
+
+        SyncTaskStatisticsVO vo4 = createEmptyVO(4, isEnable);
+        vo4.setTotal(buildFacilityList.isEmpty() ? 0 : buildFacilityList.size());
+        result.add(vo4);
+
+        SyncTaskStatisticsVO vo5 = createEmptyVO(5, isEnable);
+        result.add(vo5);
+    }
+
+    private SyncTaskStatisticsVO createEmptyVO(int dataType, boolean isEnable) {
+        SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
+        vo.setDataType(dataType);
+        vo.setSuccessNumber(0);
+        vo.setFailNumber(0);
+        vo.setNotSynced(0);
+        vo.setState(isEnable ? 1 : 0);
+
+        switch (dataType) {
+            case 1:
+                vo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
+                vo.setTopic(MqttTopics.Base.PROTECTIVE_UNIT.getTopic());
+                break;
+            case 2:
+                vo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
+                vo.setTopic(MqttTopics.Base.FLOOR_PLANE.getTopic());
+                break;
+            case 3:
+                vo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
+                vo.setTopic(MqttTopics.Base.SENSOR_INFO.getTopic());
+                break;
+            case 4:
+                vo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
+                vo.setTopic(MqttTopics.IotInfo.MONITORING_DATA.getTopic());
+                break;
+            case 5:
+                vo.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
+                vo.setTopic(MqttTopics.Alarm.MESSAGE.getTopic());
+                break;
+        }
+        return vo;
+    }
+
+    private SyncTaskStatisticsVO parseSyncStatisticsVO(JSONObject obj, boolean isEnable) {
+        SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
+        vo.setId(obj.getLong("id"));
+        vo.setTopic(obj.getStr("topic"));
+
+        int dataType = obj.getInt("dataType");
+        vo.setDataType(dataType);
+
+        switch (dataType) {
+            case 1:
+                vo.setDataTypeName(MqttTopics.Base.PROTECTIVE_UNIT.getDesc());
+                break;
+            case 2:
+                vo.setDataTypeName(MqttTopics.Base.FLOOR_PLANE.getDesc());
+                break;
+            case 3:
+                vo.setDataTypeName(MqttTopics.Base.SENSOR_INFO.getDesc());
+                break;
+            case 4:
+                vo.setDataTypeName(MqttTopics.IotInfo.MONITORING_DATA.getDesc());
+                break;
+            case 5:
+                vo.setDataTypeName(MqttTopics.Alarm.MESSAGE.getDesc());
+                break;
+        }
+
+        vo.setTotal(obj.getInt("total"));
+        vo.setSuccessNumber(obj.getInt("successNumber"));
+        vo.setFailNumber(obj.getInt("failNumber"));
+        vo.setNotSynced(obj.getInt("notSynced"));
+        vo.setCreateTime(obj.getStr("createTime"));
+        vo.setCostTime(obj.getFloat("costTime"));
+        vo.setState(isEnable ? 1 : obj.getInt("state"));
+
+        return vo;
+    }
+
     @Override
     public CommonPage<CdiDeliveryLog> logList(Long id, Integer pageNum, Integer pageSize, Integer dataType, Integer
             logType, String startTime, String endTime) {
@@ -432,6 +446,13 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         Integer tenantId = one.getTenantId();
         LocalDateTime now = LocalDateTime.now();
 
+        String userName = "自动同步";
+        try {
+            userName = SecurityUtils.getUsername();
+        } catch (Exception e) {
+            log.error("无法获取用户名或姓名,使用默认‘自动同步’", e);
+        }
+
         switch (vo.getDataType()) {
             // 单元信息
             case 1:
@@ -460,7 +481,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 endTime = System.currentTimeMillis();
 
                 notSynced = total - success - failure;
-                saveLog(topic, desc, 1, tenantId, engineeringId, now, startTime, endTime, total, success, failure, notSynced, failure > 0 ? 0 : 1);
+                saveLog(topic, desc, 1, tenantId, engineeringId, now, startTime, endTime, total, success, failure, notSynced, failure > 0 ? 0 : 1, userName);
 
                 break;
             // 平面图信息
@@ -472,10 +493,16 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 int total2 = 0, success2 = 0, failure2 = 0, notSynced2 = 0;
 
                 List<FloorPlaneVO> floorPlaneVOS = buildPlanes(tenantId, engineeringId);
+                if (CollUtil.isEmpty(floorPlaneVOS)) {
+                    log.error("未找到楼层平面图信息!");
+                    break;
+                }
                 total2 = floorPlaneVOS.size();
                 iotDataTransferService.createMqttConnection(username, password);
                 String topic1 = MqttTopics.Base.FLOOR_PLANE.getTopic();
                 String desc1 = MqttTopics.Base.FLOOR_PLANE.getDesc();
+
+
                 for (FloorPlaneVO floorPlaneVO : floorPlaneVOS) {
                     try {
                         iotDataTransferService.sendMqttMessage(topic1, floorPlaneVO, desc1, username);
@@ -488,7 +515,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 endTime2 = System.currentTimeMillis();
                 notSynced2 = total2 - success2 - failure2;
                 saveLog(topic1, desc1, 2, tenantId, engineeringId, now, startTime2, endTime2, total2, success2,
-                        failure2, notSynced2, failure2 > 0 ? 0 : 1);
+                        failure2, notSynced2, failure2 > 0 ? 0 : 1, userName);
 
                 break;
             // 推送设施信息
@@ -498,11 +525,11 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
                 long startTime3, endTime3;
                 startTime3 = System.currentTimeMillis();
 
-                Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId);
+                Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId, engineeringId, username, password);
 
                 endTime3 = System.currentTimeMillis();
                 saveLog(MqttTopics.Base.SENSOR_INFO.getTopic(), MqttTopics.Base.SENSOR_INFO.getDesc(), 3, tenantId, engineeringId,
-                        now, startTime3, endTime3, map.get("total"), map.get("success"), map.get("failure"), map.get("notSynced"), map.get("failure") > 0 ? 0 : 1);
+                        now, startTime3, endTime3, map.get("total"), map.get("success"), map.get("failure"), map.get("notSynced"), map.get("failure") > 0 ? 0 : 1, userName);
 
                 break;
             // 推送监测数据
@@ -518,7 +545,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
     @Override
     @Async("asyncServiceExecutor")
     public void saveLog(String topic, String dataTypeName, Integer dataType, Integer tenantId, Long engineeringId, LocalDateTime now, long startTime, long endTime,
-                        int total, int success, int failure, int notSynced, int pushFlag) {
+                        int total, int success, int failure, int notSynced, int pushFlag, String userName) {
 
         SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
         vo.setDataType(dataType);
@@ -537,8 +564,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         log.setDataType(dataType);
         log.setTopic(topic);
         log.setDataTypeName(vo.getDataTypeName());
-        log.setUserName(SecurityUtils.getUsername() == null ? "自动同步" : SecurityUtils.getUsername());
-        log.setNickName(SecurityUtils.getUsername() == null ? "自动同步" : SecurityUtils.getLoginUser().getSysUser().getNickName());
+        log.setUserName(userName);
         log.setCreateTime(now);
         log.setTenantId(tenantId);
         log.setPushFlag(pushFlag);
@@ -559,6 +585,7 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         List<ProtectiveUnitVO> result = new ArrayList<>(buildUnitList.size());
         for (BaseBuildUnit buildUnit : buildUnitList) {
             ProtectiveUnitVO vo = new ProtectiveUnitVO();
+            vo.setDataPacketID(generateDataPacketID());
             vo.setEngineeringID(engineeringId);
             vo.setUnitName(buildUnit.getUnitName());
             vo.setFloor(buildUnit.getFloor());
@@ -586,21 +613,42 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         List<Integer> buildIds = buildList.stream().map(BaseBuild::getId).collect(Collectors.toList());
         List<BaseBuildPlane> buildPlaneList = getBuildPlaneList(buildIds);
 
-        String time = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
+        if (CollUtil.isEmpty(buildPlaneList)) {
+            return Collections.emptyList();
+        }
+
+        Map<String, BaseBuildPlane> latestPlaneByFloor = buildPlaneList.stream()
+                .collect(Collectors.groupingBy(
+                        BaseBuildPlane::getFloor,
+                        Collectors.collectingAndThen(
+                                Collectors.maxBy(Comparator.comparingInt(BaseBuildPlane::getId)),
+                                opt -> opt.orElse(null)
+                        )
+                ));
 
-        List<FloorPlaneVO> result = new ArrayList<>(buildPlaneList.size());
-        for (BaseBuildPlane buildPlane : buildPlaneList) {
+        List<BaseBuildPlane> filteredPlaneList = latestPlaneByFloor.values().stream()
+                .filter(Objects::nonNull)
+                .sorted(Comparator.comparing(BaseBuildPlane::getFloor))
+                .collect(Collectors.toList());
 
-            String planeViewUrl = buildPlane.getPlaneViewUrl();
+        log.info("楼层平面图数据过滤:原始{}条,按楼层去重后{}条", buildPlaneList.size(), filteredPlaneList.size());
 
+        String time = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
 
+        List<FloorPlaneVO> result = new ArrayList<>(filteredPlaneList.size());
+        for (BaseBuildPlane buildPlane : filteredPlaneList) {
+            String planeViewUrl = buildPlane.getPlaneViewUrl();
             FloorPlaneVO vo = new FloorPlaneVO();
             checkFileSize(vo, planeViewUrl);
+            fillImageInfo(vo, planeViewUrl);
+
+            vo.setDataPacketID(generateDataPacketID());
             vo.setEngineeringID(engineeringId);
             vo.setFloor(buildPlane.getFloor());
             vo.setFloorFileID(Long.valueOf(buildPlane.getId()));
-            fillImageInfo(vo, planeViewUrl);
             vo.setPublishTime(time);
+
+            result.add(vo);
         }
 
         return result;
@@ -613,42 +661,141 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
     private void checkFileSize(FloorPlaneVO vo, String filePath) {
         Assert.notBlank(filePath, "文件路径不能为空");
 
-        long size = FileUtil.size(new File(filePath));
-        if (size > MAX_FILE_SIZE_BYTES) {
-            double sizeMB = size / 1024.0 / 1024.0;
-            throw new BusinessException(
-                    StrUtil.format("楼层平面图大小超过{}MB!当前:{:.2f}MB", MAX_FILE_SIZE_MB, sizeMB)
-            );
+        byte[] fileBytes;
+
+        if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
+            fileBytes = downloadFileFromUrl(filePath);
+        } else {
+            File localFile = new File(filePath);
+            if (!localFile.exists()) {
+                log.warn("本地文件不存在: {}", filePath);
+                throw new BusinessException("楼层平面图文件不存在:" + filePath);
+            }
+
+            long size = FileUtil.size(localFile);
+            if (size > MAX_FILE_SIZE_BYTES) {
+                double sizeMB = size / 1024.0 / 1024.0;
+                throw new BusinessException(
+                        StrUtil.format("楼层平面图大小超过{}MB!当前:{:.2f}MB", MAX_FILE_SIZE_MB, sizeMB)
+                );
+            }
+            fileBytes = FileUtil.readBytes(localFile);
+        }
+
+        vo.setFloorFile(fileBytes);
+    }
+
+    private byte[] downloadFileFromUrl(String fileUrl) {
+        try {
+            String encodedUrl = encodeUrl(fileUrl);
+            URL url = new URL(encodedUrl);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("GET");
+            conn.setConnectTimeout(5000);
+            conn.setReadTimeout(10000);
+            conn.setRequestProperty("User-Agent", "Mozilla/5.0");
+
+            int responseCode = conn.getResponseCode();
+            if (responseCode != HttpURLConnection.HTTP_OK) {
+                log.error("下载文件失败,HTTP状态码: {}, URL: {}", responseCode, fileUrl);
+                throw new BusinessException("下载楼层平面图失败,服务器返回状态码:" + responseCode);
+            }
+
+            int contentLength = conn.getContentLength();
+            if (contentLength > MAX_FILE_SIZE_BYTES) {
+                double sizeMB = contentLength / 1024.0 / 1024.0;
+                throw new BusinessException(
+                        StrUtil.format("楼层平面图大小超过{}MB!当前:{:.2f}MB", MAX_FILE_SIZE_MB, sizeMB)
+                );
+            }
+
+            try (InputStream inputStream = conn.getInputStream()) {
+                return IoUtil.readBytes(inputStream);
+            }
+        } catch (BusinessException e) {
+            throw e;
+        } catch (IOException e) {
+            log.error("从URL下载文件失败: {}", fileUrl, e);
+            throw new BusinessException("下载楼层平面图失败:" + e.getMessage());
         }
-        vo.setFloorFile(FileUtil.readBytes(filePath));
     }
 
-    // 获取图片信息
+    private String encodeUrl(String url) throws UnsupportedEncodingException {
+        if (StrUtil.isBlank(url)) {
+            return url;
+        }
+
+        if (!(url.startsWith("http://") || url.startsWith("https://"))) {
+            return url;
+        }
+
+        int protocolEnd = url.indexOf("://");
+        String protocol = url.substring(0, protocolEnd + 3);
+        String rest = url.substring(protocolEnd + 3);
+
+        int firstSlash = rest.indexOf("/");
+        if (firstSlash == -1) {
+            return url;
+        }
+
+        String hostAndPort = rest.substring(0, firstSlash);
+        String path = rest.substring(firstSlash);
+
+        String[] pathSegments = path.split("/");
+        StringBuilder encodedPath = new StringBuilder();
+        for (int i = 0; i < pathSegments.length; i++) {
+            if (i > 0) {
+                encodedPath.append("/");
+            }
+            encodedPath.append(URLEncoder.encode(pathSegments[i], "UTF-8"));
+        }
+
+        return protocol + hostAndPort + encodedPath.toString();
+    }
+
     private void fillImageInfo(FloorPlaneVO vo, String imageUrl) {
         if (StrUtil.isBlank(imageUrl)) {
             return;
         }
 
-        // 提取文件名信息
         String fileName = FileUtil.getName(imageUrl);
         vo.setFloorFileName(FileUtil.mainName(fileName));
         vo.setFloorFileSuffix(FileUtil.extName(fileName));
 
-        // 读取像素尺寸(带超时控制)
         try {
-            URLConnection conn = new URL(imageUrl).openConnection();
-            conn.setConnectTimeout(3000);
-            conn.setReadTimeout(5000);
-
-            try (InputStream in = conn.getInputStream()) {
-                BufferedImage image = ImageIO.read(in);
-                if (image != null) {
-                    vo.setFilePixWidth(image.getWidth());
-                    vo.setFilePixHeight(image.getHeight());
+            BufferedImage image;
+            if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
+                String encodedUrl = encodeUrl(imageUrl);
+                URL url = new URL(encodedUrl);
+                URLConnection conn = url.openConnection();
+                conn.setConnectTimeout(3000);
+                conn.setReadTimeout(5000);
+
+                try (InputStream in = conn.getInputStream()) {
+                    image = ImageIO.read(in);
                 }
+            } else {
+                File imageFile = new File(imageUrl);
+                if (imageFile.exists()) {
+                    image = ImageIO.read(imageFile);
+                } else {
+                    log.warn("图片文件不存在: {}", imageUrl);
+                    vo.setFilePixWidth(7016);
+                    vo.setFilePixHeight(9933);
+                    return;
+                }
+            }
+
+            if (image != null) {
+                vo.setFilePixWidth(image.getWidth());
+                vo.setFilePixHeight(image.getHeight());
+            } else {
+                log.warn("无法读取图片尺寸: {}", imageUrl);
+                vo.setFilePixWidth(7016);
+                vo.setFilePixHeight(9933);
             }
         } catch (IOException e) {
-            log.error("获取图片尺寸失败: {}", imageUrl);
+            log.error("获取图片尺寸失败: {}", imageUrl, e);
             vo.setFilePixWidth(7016);
             vo.setFilePixHeight(9933);
         }

+ 134 - 11
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java

@@ -12,8 +12,6 @@ import com.usky.cdi.mapper.CdiDefenseProjectMapper;
 import com.usky.cdi.mapper.CdiDeliveryLogMapper;
 import com.usky.cdi.mapper.DmpDeviceMapper;
 import com.usky.cdi.mapper.DmpProductMapper;
-import com.usky.cdi.service.CdiDeliveryLogService;
-import com.usky.cdi.service.config.mqtt.MqttOutConfig;
 import com.usky.cdi.service.enums.MqttTopics;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
 import com.usky.cdi.service.util.DeviceDataQuery;
@@ -50,8 +48,6 @@ import java.util.stream.Collectors;
 @Service
 public class IotDataTransferService {
 
-    private MqttOutConfig.MqttGateway mqttGateway;
-
     @Autowired
     private MqttConnectionTool mqttConnectionTool;
 
@@ -576,7 +572,7 @@ public class IotDataTransferService {
     }
 
     /**
-     * 推送温度信息(701
+     * 推送温度信息(707
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -597,11 +593,12 @@ public class IotDataTransferService {
         tempVO.setPublishTime(getCurrentTime());
         tempVO.setSensorValue(value);
         tempVO.setDataEndTime(dataEndTime);
+        System.out.println("监测时间:" + dataEndTime);
         sendMqttMessage(MqttTopics.IotInfo.TEMP.getTopic(), tempVO, MqttTopics.IotInfo.TEMP.getDesc(), username);
     }
 
     /**
-     * 推送湿度信息(702
+     * 推送湿度信息(708
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -651,7 +648,7 @@ public class IotDataTransferService {
     }
 
     /**
-     * 推送一氧化碳浓度信息(706
+     * 推送一氧化碳浓度信息(711
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -676,7 +673,7 @@ public class IotDataTransferService {
     }
 
     /**
-     * 推送二氧化碳浓度信息(707
+     * 推送二氧化碳浓度信息(710)
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -929,6 +926,91 @@ public class IotDataTransferService {
         }
     }
 
+    /**
+     * 发送液位数据(716)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendWaterLevel(IotDataTransferVO transferVO) {
+        Map<String, Integer> result = new HashMap<>();
+        result.put("successCount", 0);
+        result.put("failureCount", 0);
+
+        if (!validateMqttGateway(transferVO.getUsername())) {
+            return result;
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        long startTime = System.currentTimeMillis();
+        long endTime;
+
+        List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+        Integer deviceType = transferVO.getDeviceType();
+        Integer totalDevices = transferVO.getDevices().size();
+
+        try {
+
+            log.info("开始推送集水井水位数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+                    deviceType, totalDevices, deviceData.size());
+
+            if (deviceData.isEmpty()) {
+                log.warn("没有获取到位移数据!设备类型:{}", deviceType);
+                result.put("failureCount", totalDevices);
+                return result;
+            }
+
+            Long engineeringId = transferVO.getEngineeringId();
+            for (JSONObject deviceDataItem : deviceData) {
+                LocalDateTime dataEndTime = parseDataTime(deviceDataItem);
+                if (dataEndTime == null) {
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+                Double value = deviceDataItem.getDouble("sensorValue");
+                if (value == null) {
+                    log.warn("设备{}的水位数据为空", deviceId);
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+
+                WaterLevelVO vo = new WaterLevelVO();
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setSensorID(deviceId);
+                vo.setEngineeringID(engineeringId);
+                vo.setPublishTime(getCurrentTime());
+                vo.setDataEndTime(dataEndTime);
+                vo.setSensorValue(value);
+
+                try {
+                    sendMqttMessage(MqttTopics.IotInfo.SEWAGE_LEVEL.getTopic(), vo, MqttTopics.IotInfo.SEWAGE_LEVEL.getDesc(), transferVO.getUsername());
+                    result.put("successCount", result.get("successCount") + 1);
+                } catch (Exception e) {
+                    log.warn("设备{}的水位数据推送失败:{}", deviceId, e.getMessage());
+                    result.put("failureCount", result.get("failureCount") + 1);
+                }
+            }
+
+            log.info("水位数据推送完成,设备类型:{},成功:{},失败:{}",
+                    deviceType, result.get("successCount"), result.get("failureCount"));
+
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 1, SecurityUtils.getUsername());
+            return result;
+        } catch (Exception e) {
+            log.error("水位数据推送发生异常", e);
+            result.put("failureCount", transferVO.getDevices().size());
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 0, SecurityUtils.getUsername());
+            return result;
+        }
+    }
+
     /**
      * 同步设备数据
      * @param tenantId 租户ID
@@ -1035,7 +1117,9 @@ public class IotDataTransferService {
                 case 714:
                     result = sendDeviationData(transferVO);
                     break;
-
+                case 716:
+                    result = sendWaterLevel(transferVO);
+                    break;
                 default:
                     log.debug("不支持的设备类型:{}", deviceType);
                     continue;
@@ -1153,8 +1237,17 @@ public class IotDataTransferService {
      * @param username 用户名
      */
     void sendMqttMessage(String topic, Object vo, String messageType, String username) {
-        String json = JSON.toJSONString(vo);
-        // 不再记录每条数据的详情,只记录发送操作
+        String json;
+
+        // 针对楼层平面图特殊处理:将 byte[] 转为 Base64 字符串
+        if (vo instanceof com.usky.cdi.service.vo.info.FloorPlaneVO) {
+            json = serializeFloorPlaneVO((com.usky.cdi.service.vo.info.FloorPlaneVO) vo);
+        } else {
+            json = JSON.toJSONString(vo);
+        }
+
+        log.info("发送MQTT消息,Topic: {}, 消息类型: {}, JSON长度: {}", topic, messageType, json.length());
+
         MqttConnectionTool.MqttGateway gateway = mqttGatewayMap.get(username);
         if (gateway != null) {
             gateway.sendToMqtt(topic, json);
@@ -1163,6 +1256,36 @@ public class IotDataTransferService {
         }
     }
 
+    /**
+     * 序列化楼层平面图VO(将 floorFile byte[] 转为 Base64 字符串)
+     */
+    private String serializeFloorPlaneVO(com.usky.cdi.service.vo.info.FloorPlaneVO vo) {
+        com.alibaba.fastjson.JSONObject jsonObject = new com.alibaba.fastjson.JSONObject();
+
+        jsonObject.put("dataPacketID", vo.getDataPacketID());
+        jsonObject.put("engineeringID", vo.getEngineeringID());
+        jsonObject.put("floor", vo.getFloor());
+        jsonObject.put("floorFileID", vo.getFloorFileID());
+        jsonObject.put("floorFileName", vo.getFloorFileName());
+        jsonObject.put("floorFileSuffix", vo.getFloorFileSuffix());
+        jsonObject.put("filePixWidth", vo.getFilePixWidth());
+        jsonObject.put("filePixHeight", vo.getFilePixHeight());
+        jsonObject.put("publishTime", vo.getPublishTime());
+
+        // 关键:将 byte[] 转为 Base64 字符串
+        if (vo.getFloorFile() != null) {
+            String base64File = java.util.Base64.getEncoder().encodeToString(vo.getFloorFile());
+            jsonObject.put("floorFile", base64File);
+            log.info("平面图文件转换Base64成功,FileID: {}, 原始大小: {} bytes, Base64长度: {}",
+                    vo.getFloorFileID(), vo.getFloorFile().length, base64File.length());
+        } else {
+            jsonObject.put("floorFile", "");
+            log.warn("平面图文件为空,FileID: {}", vo.getFloorFileID());
+        }
+
+        return jsonObject.toJSONString();
+    }
+
     public void allData(Long engineeringId, String username, String password) {
         Integer tenantId = 0;
         synchronizeDeviceData(tenantId, engineeringId, username, password);

+ 177 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/BasementClimateUtil.java

@@ -0,0 +1,177 @@
+package com.usky.cdi.service.util;
+
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.Random;
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/4/23
+ */
+
+/**
+ * 上海地区 地下室温湿度常量 + 四季判断工具
+ */
+public class BasementClimateUtil {
+
+    private static final Random RANDOM = new Random();
+
+    // ====================== 春季(3-5月)======================
+    public static final double SPRING_TEMP_MIN = 16.0;
+    public static final double SPRING_TEMP_MAX = 20.0;
+    public static final double SPRING_HUMI_MIN = 70.0;
+    public static final double SPRING_HUMI_MAX = 74.0;
+
+    // ====================== 夏季(6-8月)======================
+    public static final double SUMMER_TEMP_MIN = 22.0;
+    public static final double SUMMER_TEMP_MAX = 26.0;
+    public static final double SUMMER_HUMI_MIN = 80.0;
+    public static final double SUMMER_HUMI_MAX = 84.0;
+
+    // ====================== 秋季(9-11月)=====================
+    public static final double AUTUMN_TEMP_MIN = 17.0;
+    public static final double AUTUMN_TEMP_MAX = 21.0;
+    public static final double AUTUMN_HUMI_MIN = 68.0;
+    public static final double AUTUMN_HUMI_MAX = 72.0;
+
+    // ====================== 冬季(12-1-2月)===================
+    public static final double WINTER_TEMP_MIN = 12.0;
+    public static final double WINTER_TEMP_MAX = 16.0;
+    public static final double WINTER_HUMI_MIN = 60.0;
+    public static final double WINTER_HUMI_MAX = 64.0;
+
+    /**
+     * 获取当前系统时间的季节
+     * 0=春 1=夏 2=秋 3=冬
+     */
+    public static int getSeason() {
+        return getSeason(LocalDate.now());
+    }
+
+    /**
+     * 根据日期判断季节(JDK8 标准写法)
+     */
+    public static int getSeason(LocalDate date) {
+        Month month = date.getMonth();
+        int monthValue = month.getValue();
+
+        if (monthValue >= 3 && monthValue <= 5) {
+            return 0; // 春
+        } else if (monthValue >= 6 && monthValue <= 8) {
+            return 1; // 夏
+        } else if (monthValue >= 9 && monthValue <= 11) {
+            return 2; // 秋
+        } else {
+            return 3; // 冬(12、1、2月)
+        }
+    }
+
+    /**
+     * 获取当前季节的温度范围 [最小值, 最大值]
+     */
+    public static double getCurrentTempRange() {
+        return randomBetween(getTempRange(getSeason())[0], getTempRange(getSeason())[1]);
+    }
+
+    /**
+     * 获取当前季节的湿度范围 [最小值, 最大值]
+     */
+    public static double getCurrentHumiRange() {
+        return randomBetween(getHumiRange(getSeason())[0], getHumiRange(getSeason())[1]);
+    }
+
+    /**
+     * 根据季节获取温度范围
+     */
+    public static double[] getTempRange(int season) {
+        if (season == 0) {
+            return new double[]{SPRING_TEMP_MIN, SPRING_TEMP_MAX};
+        } else if (season == 1) {
+            return new double[]{SUMMER_TEMP_MIN, SUMMER_TEMP_MAX};
+        } else if (season == 2) {
+            return new double[]{AUTUMN_TEMP_MIN, AUTUMN_TEMP_MAX};
+        } else if (season == 3) {
+            return new double[]{WINTER_TEMP_MIN, WINTER_TEMP_MAX};
+        } else {
+            throw new IllegalArgumentException("无效季节:" + season);
+        }
+    }
+
+    /**
+     * 根据季节获取湿度范围
+     */
+    public static double[] getHumiRange(int season) {
+        if (season == 0) {
+            return new double[]{SPRING_HUMI_MIN, SPRING_HUMI_MAX};
+        } else if (season == 1) {
+            return new double[]{SUMMER_HUMI_MIN, SUMMER_HUMI_MAX};
+        } else if (season == 2) {
+            return new double[]{AUTUMN_HUMI_MIN, AUTUMN_HUMI_MAX};
+        } else if (season == 3) {
+            return new double[]{WINTER_HUMI_MIN, WINTER_HUMI_MAX};
+        } else {
+            throw new IllegalArgumentException("无效季节:" + season);
+        }
+    }
+
+    /**
+     * 获取地下室相对室外的温度差值
+     * 负=地下室更冷,正=地下室更暖
+     */
+    public static double getTempDiffWithOutdoor() {
+        return getTempDiffWithOutdoor(getSeason());
+    }
+
+    /**
+     * 根据季节获取温度差值
+     */
+    public static double getTempDiffWithOutdoor(int season) {
+        // 所有区间范围都 < 2.0
+        switch (season) {
+            case 0: // 春:地下室略低
+                return randomBetween(-1.0, -0.5);
+            case 1: // 夏:地下室明显低
+                return randomBetween(-6.0, -5.5);
+            case 2: // 秋:低于室外
+                return randomBetween(-3.5, -3.0);
+            case 3: // 冬:地下室更高
+                return randomBetween(4.5, 5.0);
+            default:
+                return 0.0;
+        }
+    }
+
+    /**
+     * 获取地下室相对室外的湿度差值
+     */
+    public static double getHumiDiffWithOutdoor() {
+        return getHumiDiffWithOutdoor(getSeason());
+    }
+
+    /**
+     * 根据季节获取湿度差值
+     */
+    public static double getHumiDiffWithOutdoor(int season) {
+        switch (season) {
+            case 0:
+                return randomBetween(12.0, 13.0);
+            case 1:
+                return randomBetween(19.0, 20.0);
+            case 2:
+                return randomBetween(9.0, 10.0);
+            case 3:
+                return randomBetween(7.0, 8.0);
+            default:
+                return 0.0;
+        }
+    }
+
+    /**
+     * 生成 [min, max] 之间的随机小数,保留 2 位
+     */
+    private static double randomBetween(double min, double max) {
+        double val = min + (max - min) * RANDOM.nextDouble();
+        return Math.round(val * 100) / 100.0;
+    }
+}

+ 173 - 42
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java

@@ -3,6 +3,7 @@ package com.usky.cdi.service.util;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.usky.cdi.domain.DmpDevice;
 import com.usky.cdi.mapper.DmpDeviceMapper;
 import com.usky.cdi.service.config.DeviceFieldConfig;
@@ -40,9 +41,14 @@ public class DeviceDataQuery {
     @Autowired
     private DeviceFieldConfig deviceFieldConfig;
 
+    private static final Set<Integer> ALLOWED_DEVICE_TYPES = new HashSet<>(Arrays.asList(
+            711, 710, 709, 708, 707
+    ));
+
     // 定义各参数的格式化器(整数位,小数位)
     private static final DecimalFormat FORMAT_2_2 = new DecimalFormat("00.00"); // 2位整数+2位小数
     private static final DecimalFormat FORMAT_0_3 = new DecimalFormat("0.000"); // 0位整数+3位小数
+    private static final DecimalFormat FORMAT_1_2 = new DecimalFormat("0.00");  // 1位整数+2位小数
     private static final DecimalFormat FORMAT_3_2 = new DecimalFormat("000.00"); // 3位整数+2位小数
     private static final DecimalFormat FORMAT_4_2 = new DecimalFormat("0000.00"); // 4位整数+2位小数
 
@@ -79,6 +85,8 @@ public class DeviceDataQuery {
     private static final double FLOATING_RANGE_MAX = 0.5;
     private static final double FLOATING_RANGE_MIN1 = 0.0;
     private static final double FLOATING_RANGE_MAX1 = 1.0;
+    private static final double SEWAGE_LEVEL_MIN = 0.0;
+    private static final double SEWAGE_LEVEL_MAX = 0.1;
 
     private static int callCount = 0;
 
@@ -107,11 +115,52 @@ public class DeviceDataQuery {
             log.warn("接口返回数据:{}", response);
 
             List<JSONObject> resultList = parseResponseData(response, transferVO.getDeviceType(), transferVO.getDevices());
+            boolean useXiangyuMock = false;
+            List<JSONObject> xiangyuDataList = new ArrayList<>();
+
+            // 当租户数据为空时获取桃浦象屿相同设备数据进行模拟(修复版:不修改原始设备列表)
+            if (resultList.isEmpty() && ALLOWED_DEVICE_TYPES.contains(transferVO.getDeviceType())) {
+                LambdaQueryWrapper<DmpDevice> queryWrapper = new LambdaQueryWrapper<>();
+                queryWrapper.eq(DmpDevice::getTenantId, 1205)
+                        .eq(DmpDevice::getDeviceType, transferVO.getDeviceType())
+                        .orderByAsc(DmpDevice::getId)
+                        .last("LIMIT 1");
+                DmpDevice xiangyuDevice = dmpDeviceMapper.selectOne(queryWrapper);
+
+                if (xiangyuDevice != null) {
+                    JSONObject xiangyuReq = new JSONObject();
+                    xiangyuReq.put("deviceuuid", Collections.singletonList(xiangyuDevice.getDeviceUuid()));
+                    String xiangyuResp = HttpClientUtils.doPostJson(baseUrl, xiangyuReq.toJSONString());
+
+                    JSONObject responseObj = JSON.parseObject(xiangyuResp);
+                    if (responseObj.getJSONArray("data") != null && !responseObj.getJSONArray("data").isEmpty()) {
+                        JSONArray dataArray = responseObj.getJSONArray("data");
+                        JSONObject deviceData = dataArray.getJSONObject(0);
+                        JSONObject metrics = deviceData.getJSONObject("metrics");
+                        Object realtime = metrics.get("realtime");
+                        System.out.println("接口返回数据时间戳对象:" + realtime);
+                        Long timestamp = realtime instanceof Long ? (Long) realtime : null;
+                        System.out.println("接口返回数据时间戳:" + timestamp);
+
+                        if (timestamp != null && timestamp >= (System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000L)) {
+                            // 修复:不修改原始设备列表,直接生成模拟模板
+                            useXiangyuMock = true;
+                            xiangyuDataList = parseResponseData(xiangyuResp, transferVO.getDeviceType(),
+                                    Collections.singletonList(xiangyuDevice));
+                        }
+                    }
+                }
+            }
 
             // 若返回数据为空且开启模拟模式,则生成模拟数据
             if (resultList.isEmpty() && simulation) {
                 log.info("接口返回数据为空,生成模拟数据,设备类型:{}", transferVO.getDeviceType());
-                resultList = generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), null);
+                if (useXiangyuMock && !xiangyuDataList.isEmpty()) {
+                    // 使用象屿数据作为模板生成全量模拟
+                    resultList = generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), xiangyuDataList.get(0));
+                } else {
+                    resultList = generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), null);
+                }
             } else if (resultList.size() < transferVO.getDevices().size()) {
                 log.warn("接口返回数据数量与请求数量不一致,设备类型:{}", transferVO.getDeviceType());
 
@@ -136,22 +185,22 @@ public class DeviceDataQuery {
                 List<DmpDevice> missingDevices = missingDeviceIds.stream()
                         .map(requestDeviceMap::get)
                         .collect(Collectors.toList());
-                List<JSONObject> missingSimulationData = generateSimulationData(transferVO.getDeviceType(), missingDevices, resultList.get(0));
-                // 将模拟数据与返回数据结合
-                resultList.addAll(missingSimulationData);
 
-                // 校验结合后的数据是否与请求的device_id一一对应
-                Set<String> combinedDeviceIds = resultList.stream()
-                        .map(data -> data.getString("device_id"))
-                        .filter(Objects::nonNull)
-                        .collect(Collectors.toSet());
+                JSONObject templateData = resultList.isEmpty() ?
+                        (xiangyuDataList.isEmpty() ? null : xiangyuDataList.get(0))
+                        : resultList.get(0);
 
-                if (combinedDeviceIds.size() != transferVO.getDevices().size()) {
-                    log.warn("数据整合后仍存在缺失,请求设备数量:{},返回设备数量:{}",
-                            transferVO.getDevices().size(), combinedDeviceIds.size());
-                } else {
-                    log.debug("数据整合完成,设备数量与请求一致:{}", combinedDeviceIds.size());
-                }
+                List<JSONObject> missingSimulationData = generateSimulationData(transferVO.getDeviceType(), missingDevices, templateData);
+                // 修复:不再清空原有数据
+                resultList.addAll(missingSimulationData);
+            }
+
+            // 最终校验
+            if (resultList.size() != transferVO.getDevices().size()) {
+                log.warn("数据整合后仍存在缺失,请求设备数量:{},返回设备数量:{}",
+                        transferVO.getDevices().size(), resultList.size());
+            } else {
+                log.debug("数据整合完成,设备数量与请求一致:{}", resultList.size());
             }
 
             return resultList;
@@ -231,8 +280,15 @@ public class DeviceDataQuery {
                 }
 
                 // 添加设备标识信息
-                targetData.put("deviceuuid", deviceUuid);
                 String deviceId = deviceUuidToIdMap.get(deviceUuid);
+
+                // 如果找不到,直接丢弃这条数据,不放入结果
+                if (deviceId == null) {
+                    continue;
+                }
+
+                // 能走到这里,说明一定有 deviceId
+                targetData.put("deviceuuid", deviceUuid);
                 targetData.put("device_id", deviceId);
 
                 if (hasValidData) {
@@ -287,6 +343,7 @@ public class DeviceDataQuery {
     private List<JSONObject> generateSimulationData(Integer deviceType, List<DmpDevice> devices, JSONObject standard) {
         List<JSONObject> simulationList = new ArrayList<>();
         long currentTime = System.currentTimeMillis();
+        // System.out.println("深拷贝当前时间戳:" + currentTime);
 
         // 获取天气数据(仅在需要时)
         Map<String, Double> weatherData = null;
@@ -298,18 +355,25 @@ public class DeviceDataQuery {
             JSONObject simData;
 
             if (standard != null) {
-                // ✅ 使用标准模板进行模拟(深拷贝)
-                simData = JSONObject.parseObject(standard.toJSONString());
+                simData = new JSONObject();
+                simData.putAll(standard);
+
+                // 【关键】先移除,确保 FastJSON 内部缓存失效
+                simData.remove("realtime");
+                simData.remove("device_id");
+                simData.remove("device_uuid");
+
+                // 再重新 put,强制类型正确
                 simData.put("realtime", currentTime);
                 simData.put("device_id", device.getDeviceId());
-                // 可选:替换 device_uuid(如果模板中有)
-                if (device.getDeviceUuid() != null && simData.containsKey("device_uuid")) {
+                // System.out.println("深拷贝时间戳1:" + simData.getLong("realtime"));
+
+                if (device.getDeviceUuid() != null && standard.containsKey("device_uuid")) {
                     simData.put("device_uuid", device.getDeviceUuid());
                 }
 
-                // 对数值字段添加微小扰动(±2% 或固定范围)
                 addNoiseToNumericFields(simData, deviceType);
-
+                // System.out.println("深拷贝时间戳2:" + simData.getLong("realtime"));
             } else {
                 // ❌ 无标准模板,走原始随机逻辑
                 simData = new JSONObject();
@@ -319,17 +383,24 @@ public class DeviceDataQuery {
                 switch (deviceType) {
                     case 707:
                         double temp707 = 0.0;
-                        if (weatherData != null && !weatherData.isEmpty()) {
-                            double floating = ThreadLocalRandom.current().nextDouble(FLOATING_RANGE_MIN, FLOATING_RANGE_MAX);
-                            temp707 = weatherData.get("temperature") + 0.5 + floating;
+                        if (!weatherData.isEmpty()) {
+                            double floating = BasementClimateUtil.getTempDiffWithOutdoor();
+                            temp707 = weatherData.get("temperature") + floating;
                         } else {
-                            temp707 = ThreadLocalRandom.current().nextDouble(TEMP_RANGE_MIN, TEMP_RANGE_MAX);
+                            temp707 = BasementClimateUtil.getCurrentTempRange();
                         }
-                        simData.put("wd", formatNumber(temp707, FORMAT_2_2));
+                        simData.put("wd", temp707);
                         break;
 
                     case 708:
-                        simData.put("sd", formatNumber(ThreadLocalRandom.current().nextDouble(HUMIDITY_RANGE_MIN, HUMIDITY_RANGE_MAX), FORMAT_2_2));
+                        double humi708 = 0.0;
+                        if (!weatherData.isEmpty()) {
+                            double floating = BasementClimateUtil.getHumiDiffWithOutdoor();
+                            humi708 = weatherData.get("humidity") + floating;
+                        } else {
+                            humi708 = BasementClimateUtil.getCurrentHumiRange();
+                        }
+                        simData.put("sd", humi708);
                         break;
 
                     case 709:
@@ -355,6 +426,7 @@ public class DeviceDataQuery {
                         break;
 
                     case 704:
+                    case 705:
                         double aVoltage = ThreadLocalRandom.current().nextDouble(VOLTAGE_RANGE_MIN, VOLTAGE_RANGE_MAX);
                         simData.put("aVoltage", formatNumber(aVoltage, FORMAT_3_2));
                         double bVoltage = ThreadLocalRandom.current().nextDouble(VOLTAGE_RANGE_MIN, VOLTAGE_RANGE_MAX);
@@ -399,6 +471,12 @@ public class DeviceDataQuery {
                         simData.put("wy", 0);
                         break;
 
+                    case 716:
+                        String deviceId1 = device.getDeviceId();
+                        double value = FixedWaterLevelGenerator.getSensorValue(deviceId1);
+                        simData.put("sensorValue", formatNumber(value, FORMAT_1_2));
+                        break;
+
                     default:
                         log.warn("未知设备类型:{},无法生成模拟数据", deviceType);
                         continue;
@@ -413,33 +491,86 @@ public class DeviceDataQuery {
     }
 
     private void addNoiseToNumericFields(JSONObject data, Integer deviceType) {
-        // 定义各字段的噪声比例或范围(可根据设备类型定制)
         Map<String, Double> noiseConfig = getNoiseConfigByDeviceType(deviceType);
 
+        // 这些字段永远不加噪
+        Set<String> skipFields = new HashSet<>(Arrays.asList("realtime", "device_id", "device_uuid"));
+
         for (String key : data.keySet()) {
+            if (skipFields.contains(key)) continue;
+            if (isDiscreteField(key)) continue;
+
             Object value = data.get(key);
-            if (value instanceof Number) {
-                double original = ((Number) value).doubleValue();
-                // 默认 ±2%
-                double noiseFactor = noiseConfig.getOrDefault(key, 0.02);
+            Double original = null;
 
-                // 避免对状态类字段(如 0/1)加噪
-                if (isDiscreteField(key)) {
-                    continue;
+            // 【关键】支持 Number 和 String 两种类型
+            if (value instanceof Number) {
+                original = ((Number) value).doubleValue();
+            } else if (value instanceof String) {
+                try {
+                    original = Double.parseDouble((String) value);
+                } catch (NumberFormatException e) {
+                    System.out.println("跳过字段(非数字): " + key + " = " + value);
+                    continue; // 不是数字字符串,跳过
                 }
+            }
 
-                double noise = original * noiseFactor * (ThreadLocalRandom.current().nextDouble(-1, 1));
-                double newValue = original + noise;
+            if (original == null) continue;
 
-                // 根据原格式保留小数位(这里简化处理,也可记录原格式)
+            // 每个设备、每个字段、每次调用都是独立的随机噪声 ✅
+            double noiseFactor = noiseConfig.getOrDefault(key, 0.02);
+            double noise = original * noiseFactor * (ThreadLocalRandom.current().nextDouble(-1, 1));
+            double newValue = original + noise;
+
+            System.out.println("加噪: " + key + " | 原值=" + original + " | 噪声=" + noise + " | 新值=" + newValue);
+
+            // 保持原类型:原来是 String 就格式化回 String,原来是 Number 就放 Double
+            if (value instanceof String) {
+                data.put(key, formatNumber(newValue, getFormatByKey(key)));
+            } else {
                 data.put(key, newValue);
             }
         }
     }
 
+    /**
+     * 根据字段名获取对应的格式化器
+     */
+    private DecimalFormat getFormatByKey(String key) {
+        switch (key) {
+            case "aVoltage":
+            case "bVoltage":
+            case "cVoltage":
+            case "aElectricity":
+            case "bElectricity":
+            case "cElectricity":
+                return FORMAT_3_2;
+            case "totalPower":
+                return FORMAT_4_2;
+            case "line1TEMP":
+            case "Line2TEMP":
+            case "Line3TEMP":
+            case "Line4TEMP":
+            case "wd":
+            case "sd":
+            case "o2":
+                return FORMAT_2_2;
+            case "co2":
+                return FORMAT_0_3;
+            case "leakageCurrent":
+                return FORMAT_4_2;
+            case "sensorValue":
+                return FORMAT_1_2;
+            default:
+                return null; // 不格式化,保持原样
+        }
+    }
+
     private boolean isDiscreteField(String field) {
-        // 这些字段是状态码,不应加噪
-        return field.equalsIgnoreCase("leach_status")
+        return field.equalsIgnoreCase("realtime")
+                || field.equalsIgnoreCase("device_id")
+                || field.equalsIgnoreCase("device_uuid")
+                || field.equalsIgnoreCase("leach_status")
                 || field.equalsIgnoreCase("sensorValue")
                 || field.equalsIgnoreCase("qx")
                 || field.equalsIgnoreCase("cd")
@@ -452,7 +583,7 @@ public class DeviceDataQuery {
         Map<String, Double> config = new HashMap<>();
         switch (deviceType) {
             case 707: // 温度
-                config.put("wd", 0.03); // ±3%
+                config.put("wd", 0.03); //
                 break;
             case 708: // 湿度
                 config.put("sd", 0.05);

+ 5 - 5
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataSyncService.java

@@ -26,12 +26,12 @@ public class DeviceDataSyncService {
      * fixedDelay:任务执行完成后固定延迟29分钟执行下一次
      * initialDelay:初始化后立即执行第一次任务
      */
-    // @Scheduled(fixedDelay = 26 * 60 * 1000, initialDelay = 0)
+    // @Scheduled(fixedDelay = 2 * 60 * 1000, initialDelay = 0)
     // public void scheduledDeviceDataSync() {
-    //     Integer tenantId = 1205;
-    //     Long engineeringId = 3101070011L;
-    //     String username = "3101070011";
-    //     String password = "5RqhJ7VG";
+    //     Integer tenantId = 1222;
+    //     Long engineeringId = 3101100021L;
+    //     String username = "3101100021";
+    //     String password = "SIixzph1";
     //     log.info("开始执行桃浦象屿人防设备数据同步定时任务,租户ID:{},工程ID:{}", tenantId, engineeringId);
     //
     //     try {

+ 44 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/FixedWaterLevelGenerator.java

@@ -0,0 +1,44 @@
+package com.usky.cdi.service.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/4/28
+ */
+public class FixedWaterLevelGenerator {
+
+    // 关键:缓存每个设备的固定水位(只生成一次)
+    private static final Map<String, Double> deviceLevelMap = new HashMap<>();
+
+    /**
+     * 获取设备固定水位(永远不变)
+     */
+    public static double getSensorValue(String deviceId) {
+        // 如果这个设备还没生成过值 → 生成一个不重复的固定值
+        if (!deviceLevelMap.containsKey(deviceId)) {
+            double value = generateUniqueValue();
+            deviceLevelMap.put(deviceId, value);
+        }
+        // 直接返回固定值
+        return deviceLevelMap.get(deviceId);
+    }
+
+    /**
+     * 生成一个不重复、≤0.1 的水位
+     */
+    private static double generateUniqueValue() {
+        double val;
+        do {
+            // 生成 0.01 ~ 0.09 的随机数(保证不超0.1)
+            val = ThreadLocalRandom.current().nextDouble(0.02, 0.05);
+            // 保留2位小数
+            val = Math.round(val * 100) / 100.0;
+        } while (deviceLevelMap.containsValue(val)); // 确保不重复
+        return val;
+    }
+}

+ 10 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/SnowflakeIdGenerator.java

@@ -141,5 +141,15 @@ public class SnowflakeIdGenerator {
         // 取后15位,确保不超过15位
         return id % 1000000000000000L;
     }
+
+    /**
+     * 生成10位数据包ID(取后10位)
+     * */
+
+    public long nextPacketId10() {
+        long id = nextId();
+        // 取后10位,确保不超过10位
+        return id % 10000000000L;
+    }
 }
 

+ 4 - 4
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/alarm/AlarmMessageVO.java

@@ -1,6 +1,6 @@
 package com.usky.cdi.service.vo.alarm;
 
-import com.fasterxml.jackson.annotation.JsonFormat;
+import com.alibaba.fastjson.annotation.JSONField;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import lombok.Data;
 
@@ -93,7 +93,7 @@ public class AlarmMessageVO<T extends Number> implements Serializable {
      * 查询告警表 base_alarm 时新增告警取 alarm_type 字段,更新数据则取 handle_time 字段
      * 时间型(带毫秒),格式为 yyyy-MM-DD hh :mm :ss.SSS
      **/
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS", timezone = "GMT+8")
+    @JSONField(format = "yyyy-MM-dd HH:mm:ss.SSS")
     private String alarmUpdateTime;
 
     /**
@@ -113,8 +113,8 @@ public class AlarmMessageVO<T extends Number> implements Serializable {
      * 获取当前时间
      * 时间型(带毫秒),格式为 yyyy-MM-DD hh :mm :ss.SSS
      **/
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS", timezone = "GMT+8")
-    private LocalDateTime publishTime;
+    @JSONField(format = "yyyy-MM-dd HH:mm:ss.SSS")
+    private String publishTime;
 
     /** 告警数据字段 必填、通用
      * 水浸(Integer) 0:无水、1:有水

+ 42 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/base/BaseMqttInfo.java

@@ -0,0 +1,42 @@
+package com.usky.cdi.service.vo.base;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/4/10
+ */
+@Data
+public class BaseMqttInfo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 数据包ID
+     */
+    private Long dataPacketID;
+
+    /**
+     *  工程id
+     */
+    private Long engineeringID;
+
+    /**
+     * 上报时间
+     */
+    private String publishTime;
+
+    /**
+     *  MQTT用户名
+     */
+    private String userName;
+
+    /**
+     *  MQTT密码
+     */
+    private String password;
+}

+ 2 - 2
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/EngineeringBaseVO.java

@@ -1,7 +1,7 @@
 package com.usky.cdi.service.vo.info;
 
+import com.usky.cdi.service.vo.base.BaseMqttInfo;
 import lombok.Data;
-import java.io.Serializable;
 
 /**
  * 人防工程基础信息VO
@@ -11,7 +11,7 @@ import java.io.Serializable;
  * @date 2025/03/20
  */
 @Data
-public class EngineeringBaseVO implements Serializable {
+public class EngineeringBaseVO extends BaseMqttInfo {
     private static final long serialVersionUID = 1L;
 
     /**

+ 5 - 2
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/FacilityDeviceVO.java

@@ -1,18 +1,20 @@
 package com.usky.cdi.service.vo.info;
 
+import com.usky.cdi.service.vo.base.BaseMqttInfo;
 import lombok.Data;
+
 import java.io.Serializable;
 import java.time.LocalDateTime;
 
 /**
  * 楼层平面图信息VO
  * Topic: base/floorPlane
- * 
+ *
  * @author han
  * @date 2025/03/20
  */
 @Data
-public class FacilityDeviceVO implements Serializable {
+public class FacilityDeviceVO extends BaseMqttInfo {
     private static final long serialVersionUID = 1L;
 
     private Integer id;
@@ -157,5 +159,6 @@ public class FacilityDeviceVO implements Serializable {
      * 设备UUID
      */
     private String deviceUuid;
+
 }
 

+ 2 - 15
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/FloorPlaneVO.java

@@ -1,5 +1,6 @@
 package com.usky.cdi.service.vo.info;
 
+import com.usky.cdi.service.vo.base.BaseMqttInfo;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -12,19 +13,9 @@ import java.io.Serializable;
  * @date 2025/03/20
  */
 @Data
-public class FloorPlaneVO implements Serializable {
+public class FloorPlaneVO extends BaseMqttInfo {
     private static final long serialVersionUID = 1L;
 
-    /**
-     * 数据包ID
-     */
-    private Long dataPacketID;
-
-    /**
-     * 人防工程ID
-     */
-    private Long engineeringID;
-
     /**
      * 楼层
      */
@@ -60,9 +51,5 @@ public class FloorPlaneVO implements Serializable {
      */
     private byte[] floorFile;
 
-    /**
-     * 上报时间
-     */
-    private String publishTime;
 }
 

+ 2 - 15
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/ProtectiveUnitVO.java

@@ -1,5 +1,6 @@
 package com.usky.cdi.service.vo.info;
 
+import com.usky.cdi.service.vo.base.BaseMqttInfo;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -13,19 +14,9 @@ import java.math.BigDecimal;
  * @date 2025/03/20
  */
 @Data
-public class ProtectiveUnitVO implements Serializable {
+public class ProtectiveUnitVO extends BaseMqttInfo {
     private static final long serialVersionUID = 1L;
 
-    /**
-     * 数据包ID
-     */
-    private Long dataPacketID;
-
-    /**
-     * 人防工程ID
-     */
-    private Long engineeringID;
-
     /**
      * 楼层
      */
@@ -61,9 +52,5 @@ public class ProtectiveUnitVO implements Serializable {
      */
     private String unitotherexit;
 
-    /**
-     * 上报时间
-     */
-    private String publishTime;
 }
 

+ 39 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/WaterLevelVO.java

@@ -0,0 +1,39 @@
+package com.usky.cdi.service.vo.info;
+
+import com.usky.cdi.service.vo.base.BaseEnvMonitorPushVO;
+import lombok.Data;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/4/27
+ */
+@Data
+public class WaterLevelVO extends BaseEnvMonitorPushVO {
+
+    private static final long serialVersionUID = 1L;
+
+    private static final Double MIN_SENSOR_VALUE = 0.0;
+    private static final Double MAX_SENSOR_VALUE = 2.0;
+
+    /**
+     * 水位高度
+     * 类型:double
+     */
+    private Double sensorValue;
+
+    @Override
+    public Number getSensorValue() {
+        return sensorValue;
+    }
+
+    @Override
+    protected void validateSensorValue() {
+        // 位移值校验规则,根据实际业务需求调整
+        if (sensorValue == null || sensorValue < MIN_SENSOR_VALUE || sensorValue > MAX_SENSOR_VALUE) {
+            throw new IllegalArgumentException("水位高度(sensorValue)为必填项,且必须在" + MIN_SENSOR_VALUE + "到" + MAX_SENSOR_VALUE + "之间");
+        }
+    }
+
+}