Browse Source

人防代码优化

fuyuchuan 1 day ago
parent
commit
8e2a0d9a58

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

+ 49 - 31
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java

@@ -3,6 +3,8 @@ 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;
@@ -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.*;
 
 /**
@@ -144,61 +151,70 @@ 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://02H1-01.jpg";
-            // 将图片文件读取为字节数组
+            // ========== 2. 读取本地图片 ==========
+            String imagePath = "C:\\Users\\f\\Downloads\\45_平面图.jpg";
             byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
-            vo.setFloorFile(imageBytes);
 
-            // 检查文件大小(不超过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;
             }
 
-            String base64File = java.util.Base64.getEncoder().encodeToString(vo.getFloorFile());
+            // 格式校验
+            if (!Arrays.asList("jpg", "jpeg", "png").contains(vo.getFloorFileSuffix().toLowerCase())) {
+                System.err.println("不支持的格式");
+                return false;
+            }
 
-            HashMap<String, Object> map = new HashMap<>();
+            // 获取宽高
+            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("floorFile", base64File);
+            map.put("filePixWidth", width);
+            map.put("filePixHeight", height);
+            map.put("floorFile", Base64.getEncoder().encodeToString(imageBytes));
             map.put("publishTime", vo.getPublishTime());
-            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));
+
+            //使用Gson:
+            Gson gson = new GsonBuilder()
+                    .setLongSerializationPolicy(LongSerializationPolicy.STRING)
+                    .create();
+            String json = 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);
-            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh("3101100017", "gjB4v1bh");
-            gateway.sendToMqtt(topic, gson.toJson(map));
+            // 发送JSON字符串
+            gateway.sendToMqtt(topic, json);
 
+            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;
         }
     }
@@ -231,6 +247,7 @@ 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);
@@ -239,6 +256,7 @@ public class BaseDataTransferService {
             // 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());

+ 100 - 13
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java

@@ -12,7 +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.config.mqtt.MqttOutConfig;
 import com.usky.cdi.service.enums.MqttTopics;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
 import com.usky.cdi.service.util.DeviceDataQuery;
@@ -573,7 +572,7 @@ public class IotDataTransferService {
     }
 
     /**
-     * 推送温度信息(701
+     * 推送温度信息(707
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -594,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
@@ -648,7 +648,7 @@ public class IotDataTransferService {
     }
 
     /**
-     * 推送一氧化碳浓度信息(706
+     * 推送一氧化碳浓度信息(711
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -673,7 +673,7 @@ public class IotDataTransferService {
     }
 
     /**
-     * 推送二氧化碳浓度信息(707
+     * 推送二氧化碳浓度信息(710)
      *
      * @param deviceDataItem 设备数据
      * @param deviceId 设备ID
@@ -926,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.DEVIATION.getTopic(), vo, MqttTopics.IotInfo.DEVIATION.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
@@ -1032,7 +1117,9 @@ public class IotDataTransferService {
                 case 714:
                     result = sendDeviationData(transferVO);
                     break;
-
+                case 716:
+                    result = sendWaterLevel(transferVO);
+                    break;
                 default:
                     log.debug("不支持的设备类型:{}", deviceType);
                     continue;
@@ -1151,16 +1238,16 @@ public class IotDataTransferService {
      */
     void sendMqttMessage(String topic, Object vo, String messageType, String username) {
         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);
@@ -1174,7 +1261,7 @@ public class IotDataTransferService {
      */
     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());
@@ -1184,18 +1271,18 @@ public class IotDataTransferService {
         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长度: {}", 
+            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();
     }
 

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

+ 171 - 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.5;
 
     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())
+                        .orderByDesc(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,10 @@ public class DeviceDataQuery {
                         simData.put("wy", 0);
                         break;
 
+                    case 716:
+                        simData.put("sensorValue", formatNumber(ThreadLocalRandom.current().nextDouble(SEWAGE_LEVEL_MIN, SEWAGE_LEVEL_MAX), FORMAT_1_2));
+                        break;
+
                     default:
                         log.warn("未知设备类型:{},无法生成模拟数据", deviceType);
                         continue;
@@ -413,33 +489,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 +581,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);

+ 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 + "之间");
+        }
+    }
+
+}