Ver Fonte

Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into modules-zjy

zhaojinyu há 16 horas atrás
pai
commit
b235a606e7
100 ficheiros alterados com 8968 adições e 161 exclusões
  1. 4 0
      .vscode/settings.json
  2. 71 75
      pom.xml
  3. 33 0
      service-agbox/README.md
  4. 1 0
      service-agbox/service-agbox-biz/pom.xml
  5. 7 1
      service-agbox/service-agbox-biz/src/main/java/com/usky/agbox/service/mqtt/event/event.java
  6. 1 1
      service-agbox/service-agbox-biz/src/main/resources/logback.xml
  7. 35 0
      service-ai/README.md
  8. 35 0
      service-alarm/README.md
  9. 1 0
      service-alarm/service-alarm-biz/pom.xml
  10. 9 4
      service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmNoticeResultServiceImpl.java
  11. 1 0
      service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmServiceImpl.java
  12. 7 2
      service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/mqtt/alarm/Alarm.java
  13. 33 0
      service-backend/README.md
  14. 34 0
      service-cdi/README.md
  15. 3 3
      service-cdi/pom.xml
  16. 31 0
      service-cdi/service-cdi-api/pom.xml
  17. 31 0
      service-cdi/service-cdi-api/src/main/java/com/usky/cdi/AlarmDataSyncTaskService.java
  18. 20 0
      service-cdi/service-cdi-api/src/main/java/com/usky/cdi/RemotecdiTaskService.java
  19. 29 0
      service-cdi/service-cdi-api/src/main/java/com/usky/cdi/factory/AlarmDataSyncTaskFactory.java
  20. 30 0
      service-cdi/service-cdi-api/src/main/java/com/usky/cdi/factory/RemotecdiTaskFactory.java
  21. 42 41
      service-cdi/service-cdi-biz/pom.xml
  22. 108 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/MybatisGeneratorUtils.java
  23. 11 27
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/cdiApplication.java
  24. 39 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/AlarmDataController.java
  25. 89 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/BaseDataController.java
  26. 68 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/IotDataController.java
  27. 34 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/api/AlarmDataSyncTaskApi.java
  28. 30 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/api/RemotecdiTaskApi.java
  29. 9 7
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseAlarmController.java
  30. 21 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseBuildController.java
  31. 21 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseBuildFacilityController.java
  32. 21 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseBuildPlaneController.java
  33. 21 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseBuildUnitController.java
  34. 158 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/CdiDefenseProjectController.java
  35. 92 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/CdiDeliveryLogController.java
  36. 33 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/DmpDeviceInfoController.java
  37. 22 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/DmpProductController.java
  38. 21 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/SysUserController.java
  39. 127 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseAlarm.java
  40. 212 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseBuild.java
  41. 162 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseBuildFacility.java
  42. 56 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseBuildPlane.java
  43. 101 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseBuildUnit.java
  44. 135 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/CdiDefenseProject.java
  45. 90 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/CdiDeliveryLog.java
  46. 132 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/DmpDevice.java
  47. 141 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/DmpProduct.java
  48. 136 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/SysUser.java
  49. 18 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseAlarmMapper.java
  50. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseBuildFacilityMapper.java
  51. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseBuildMapper.java
  52. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseBuildPlaneMapper.java
  53. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseBuildUnitMapper.java
  54. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/CdiDefenseProjectMapper.java
  55. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/CdiDeliveryLogMapper.java
  56. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/DmpDeviceMapper.java
  57. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/DmpProductMapper.java
  58. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/SysUserMapper.java
  59. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseAlarmService.java
  60. 18 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseBuildFacilityService.java
  61. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseBuildPlaneService.java
  62. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseBuildService.java
  63. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseBuildUnitService.java
  64. 20 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/CdiDefenseProjectService.java
  65. 31 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/CdiDeliveryLogService.java
  66. 25 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/DmpDeviceInfoService.java
  67. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/DmpProductService.java
  68. 16 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/SysUserService.java
  69. 67 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/DeviceFieldConfig.java
  70. 44 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttBaseConfig.java
  71. 47 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttInConfig.java
  72. 123 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttOutConfig.java
  73. 193 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/AlarmType.java
  74. 103 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/DeliveryDataType.java
  75. 67 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/EnvMonitorMqttTopic.java
  76. 157 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/MqttTopics.java
  77. 569 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataSyncService.java
  78. 75 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataTransferService.java
  79. 35 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseAlarmServiceImpl.java
  80. 32 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseBuildFacilityServiceImpl.java
  81. 20 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseBuildPlaneServiceImpl.java
  82. 20 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseBuildServiceImpl.java
  83. 20 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseBuildUnitServiceImpl.java
  84. 375 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java
  85. 63 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDefenseProjectServiceImpl.java
  86. 805 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java
  87. 35 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/DmpDeviceInfoServiceImpl.java
  88. 20 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/DmpProductServiceImpl.java
  89. 1499 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java
  90. 20 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/SysUserServiceImpl.java
  91. 61 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/listener/MqttListener.java
  92. 227 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/MqttConnectionTool.java
  93. 13 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/MqttStrategy.java
  94. 35 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/SimpleContext.java
  95. 29 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/event/event.java
  96. 31 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/info/Info.java
  97. 381 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/AirDefenseSimulator.java
  98. 177 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/BasementClimateUtil.java
  99. 42 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceAlarmDataSyncService.java
  100. 683 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java

+ 4 - 0
.vscode/settings.json

@@ -0,0 +1,4 @@
+{
+    "java.compile.nullAnalysis.mode": "automatic",
+    "java.configuration.updateBuildConfiguration": "automatic"
+}

+ 71 - 75
pom.xml

@@ -1,101 +1,97 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-          
-  
-  
-  <parent>
-                    
-    
-    
-    <groupId>com.usky</groupId>
-                    
-    
-    
-    <artifactId>uskycloud</artifactId>
-                    
-    
-    
-    <version>0.0.1</version>
-                
-  
-  
-  </parent>
-          
-  
-  
-  <modelVersion>4.0.0</modelVersion>
-          
-  
-  
-  <artifactId>usky-modules</artifactId>
-          
-  
-  
-  <packaging>pom</packaging>
-          
-  
-  
-  <description>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+
+    <parent>
+
+
+        <groupId>com.usky</groupId>
+
+
+        <artifactId>uskycloud</artifactId>
+
+
+        <version>0.0.1</version>
+
+
+    </parent>
+
+
+    <modelVersion>4.0.0</modelVersion>
+
+
+    <artifactId>usky-modules</artifactId>
+
+
+    <packaging>pom</packaging>
+
+
+    <description>
         usky-modules
     </description>
-          
-  
-  
-  <modules>
-                
-    
-    
-    <module>usky-module-demo</module>
-                
-    
-    
-    <module>service-backend</module>
-                
-    
-    
-    <module>service-website</module>
-                
-    
-    
-    <module>service-iot</module>
 
 
+    <modules>
 
-    <module>service-alarm</module>
-                
-    
-    
-    <module>service-fire</module>
-                
 
-    
-    <module>service-park</module>
+        <module>usky-module-demo</module>
+
+
+        <module>service-backend</module>
+
+
+        <module>service-website</module>
+
+
+        <module>service-iot</module>
+
+
+        <module>service-alarm</module>
+
 
+        <module>service-fire</module>
 
 
-    <module>service-issue</module>
+        <module>service-park</module>
 
 
-    <module>service-meeting</module>
+        <module>service-issue</module>
 
 
-    <module>service-agbox</module>
+        <module>service-meeting</module>
 
 
-    <module>service-job</module>
+        <module>service-agbox</module>
 
+        <module>service-job</module>
 
-    <module>service-eg</module>
 
+        <module>service-eg</module>
 
-    <module>service-oa</module>
 
+        <module>service-oa</module>
 
-    <module>service-ai</module>
 
-    <module>service-pm</module>
+        <module>service-ai</module>
 
-    <module>service-ids</module>
+        <module>service-pm</module>
+
+        <module>service-ids</module>
+
+        <module>service-cdi</module>
+
+        <module>service-ems</module>
+
+        <module>service-transfer</module>
+
+        <module>service-tsdb</module>
+
+        <module>service-rule</module>
+
+        <!--    <module>service-data</module>-->
+
+        <module>service-sas</module>
 
   </modules>
           
@@ -122,7 +118,7 @@
   
   
   </dependencies>
-      
+    
 
 
 </project>

+ 33 - 0
service-agbox/README.md

@@ -0,0 +1,33 @@
+# 巡更管理系统
+
+## 模块信息
+
+| 项目 | 内容 |
+|------|------|
+| 所属系统(模块) | 巡更管理系统 |
+| 分区 | patrol |
+| 英文全称 | Patrol management |
+| 描述 | 提供智能巡更系统 |
+| 模块分类 | 应用模块 |
+| 工程模块 | service-agbox |
+
+## 功能说明
+
+- 提供智能巡更系统
+- 支持巡更路线管理
+- 提供巡更记录和状态监控
+
+## 技术栈
+
+- Java
+- Spring Boot
+- MyBatis
+
+## 模块结构
+
+```
+service-agbox/
+├── service-agbox-api/         # API 接口模块
+├── service-agbox-biz/         # 业务逻辑模块
+└── pom.xml                  # 模块依赖配置
+```

+ 1 - 0
service-agbox/service-agbox-biz/pom.xml

@@ -74,6 +74,7 @@
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.2.6.RELEASE</version>
                 <executions>
                     <execution>
                         <goals>

+ 7 - 1
service-agbox/service-agbox-biz/src/main/java/com/usky/agbox/service/mqtt/event/event.java

@@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.Map;
 
 @Service("event")
@@ -40,9 +41,14 @@ public class event implements MqttStrategy {
                 }else {
                     eventVO.put("eventCode",16);
                 }
+                // 定义原始格式的解析器(包含'T'和毫秒)
+                DateTimeFormatter originalFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
+                DateTimeFormatter targetFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                 String timeWithT = eventVO1.get("createTime").toString();
+                LocalDateTime dateTime = LocalDateTime.parse(timeWithT, originalFormatter);
+                String targetTime = dateTime.format(targetFormatter);
                 eventVO.put("deviceId",eventVO1.get("deviceId"));
-                eventVO.put("triggerTime",timeWithT.replace("T", " "));
+                eventVO.put("triggerTime",targetTime);
                 eventVO.put("name",eventVO1.get("createBy"));
                 eventVO.put("certifiedNo","");
                 PatrolAgbox.addEvent(eventVO.toJSONString());

+ 1 - 1
service-agbox/service-agbox-biz/src/main/resources/logback.xml

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration scan="true" scanPeriod="60 seconds" debug="false">
     <!-- 日志存放路径 -->
-    <property name="log.path" value="/var/log/uskycloud/service-alarm" />
+    <property name="log.path" value="/var/log/uskycloud/service-agbox" />
     <!-- 日志输出格式 -->
     <property name="log.pattern" value="%d{MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{26}:%line: %msg%n" />
     <!--    	<property name="log.pattern" value="%gray(%d{MM-dd HH:mm:ss.SSS}) %highlight(%-5level) &#45;&#45; [%gray(%thread)] %cyan(%logger{26}:%line): %msg%n" />-->

+ 35 - 0
service-ai/README.md

@@ -0,0 +1,35 @@
+# AI 智能体系统
+
+## 模块信息
+
+| 项目 | 内容 |
+|------|------|
+| 所属系统(模块) | AI 智能体系统 |
+| 分区 | ai |
+| 英文全称 | AI |
+| 描述 | 提供 AI 文本推理,视频算法管理 |
+| 模块分类 | 通用模块 |
+| 工程模块 | service-ai |
+
+## 功能说明
+
+- 提供 AI 文本推理能力
+- 管理视频算法
+- 支持 AI 模型的集成和调用
+- 提供智能分析和处理服务
+
+## 技术栈
+
+- Java
+- Spring Boot
+- AI 模型集成
+- MyBatis
+
+## 模块结构
+
+```
+service-ai/
+├── service-ai-api/         # API 接口模块
+├── service-ai-biz/         # 业务逻辑模块
+└── pom.xml                  # 模块依赖配置
+```

+ 35 - 0
service-alarm/README.md

@@ -0,0 +1,35 @@
+# 告警中心
+
+## 模块信息
+
+| 项目 | 内容 |
+|------|------|
+| 所属系统(模块) | 告警中心 |
+| 分区 | alarm |
+| 英文全称 | Alarm |
+| 描述 | 提供告警服务 |
+| 模块分类 | 通用模块 |
+| 工程模块 | service-alarm |
+
+## 功能说明
+
+- 提供告警信息的采集、处理和分发
+- 支持多种告警级别和类型的管理
+- 提供告警通知和处理流程
+- 支持告警历史查询和统计分析
+
+## 技术栈
+
+- Java
+- Spring Boot
+- MyBatis
+- 消息队列(可选)
+
+## 模块结构
+
+```
+service-alarm/
+├── service-alarm-api/         # API 接口模块
+├── service-alarm-biz/         # 业务逻辑模块
+└── pom.xml                  # 模块依赖配置
+```

+ 1 - 0
service-alarm/service-alarm-biz/pom.xml

@@ -74,6 +74,7 @@
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.2.6.RELEASE</version>
                 <executions>
                     <execution>
                         <goals>

+ 9 - 4
service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmNoticeResultServiceImpl.java

@@ -93,24 +93,26 @@ public class BaseAlarmNoticeResultServiceImpl extends AbstractCrudService<BaseAl
 
                 if (records3.get(0).getNoticeMethod().contains("电话")) {
                     handleVoiceNotice(sysUser, baseAlarmNoticeResult, records.get(0), baseAlarmType, baseAlarm);
+                    saveAlarmResult = baseMapper.insert(baseAlarmNoticeResult);
                 }
 
                 if (records3.get(0).getNoticeMethod().contains("短信")) {
                     handleSmsNotice(sysUser, baseAlarmNoticeResult, templateParam);
+                    saveAlarmResult = baseMapper.insert(baseAlarmNoticeResult);
                 }
 
                 if (records3.get(0).getNoticeMethod().contains("APP")) {
                     handleMceNotice(sysUser, baseAlarmNoticeResult, baseAlarm, baseAlarmType, records2.get(0), "1");
+                    saveAlarmResult = baseMapper.insert(baseAlarmNoticeResult);
                 }
 
                 if (records3.get(0).getNoticeMethod().contains("微信")) {
                     handleMceNotice(sysUser, baseAlarmNoticeResult, baseAlarm, baseAlarmType, records2.get(0), "2");
+                    saveAlarmResult = baseMapper.insert(baseAlarmNoticeResult);
                 }
-
-                saveAlarmResult = baseMapper.insert(baseAlarmNoticeResult);
             }
         } catch (Exception e) {
-            log.error("Error in sending alarm notice", e);
+            log.error("发送报警通知时异常", e);
         }
         return saveAlarmResult;
     }
@@ -163,7 +165,7 @@ public class BaseAlarmNoticeResultServiceImpl extends AbstractCrudService<BaseAl
             request.setTemplateCode(SMS_TEMPLATE_CODE);
             request.setTemplateParam(templateParam);
             SendSmsResponse response = client.getAcsResponse(request);
-            baseAlarmNoticeResult.setNoticeResult(response.getCode() != null && response.getCode().equals("OK") ? 1 : 2);
+            baseAlarmNoticeResult.setNoticeResult(response.getCode() != null && "OK".equals(response.getCode()) ? 1 : 2);
         } catch (Exception e) {
             log.error("用户:{},告警消息短信发送失败", sysUser.getUserId(), e);
             baseAlarmNoticeResult.setNoticeResult(2);
@@ -198,12 +200,15 @@ public class BaseAlarmNoticeResultServiceImpl extends AbstractCrudService<BaseAl
         mceRequestVO.setInfoType(INFO_TYPE);
         mceRequestVO.setInfoTitle(INFO_TITLE);
         mceRequestVO.setDeviceId(device.getDeviceId());
+        log.info("设备ID:{}", device.getDeviceId());
         mceRequestVO.setInfoContent(baseAlarmType.getTypeName());
         mceRequestVO.setAlarmTime(formatAlarmTime(baseAlarm.getAlarmTime()));
+        log.info("告警时间:{}", baseAlarm.getAlarmTime());
         mceRequestVO.setAlarmSendType(sendType);
         mceRequestVO.setRemark(String.format(REMARK_TEMPLATE, device.getDeviceName()));
         mceRequestVO.setUserIds(Collections.singletonList(sysUser.getUserId()));
         mceRequestVO.setUserName(sysUser.getUserName());
+        mceRequestVO.setId(baseAlarm.getId());
         return mceRequestVO;
     }
 

+ 1 - 0
service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/impl/BaseAlarmServiceImpl.java

@@ -115,6 +115,7 @@ public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, B
                     List<BaseAlarmNotice> records3 = baseAlarmNoticeService.list(lambdaQuery3);
                     if (records3.size()>0){
                         Integer saveAlarmResult = baseAlarmNoticeResultService.send(baseAlarm,records3,alarmId,records,baseAlarmType,records2);
+                        log.info("异步发送告警通知数据: {}", baseAlarm);
                     }
                     return saveResult;
                 }

+ 7 - 2
service-alarm/service-alarm-biz/src/main/java/com/usky/alarm/service/mqtt/alarm/Alarm.java

@@ -56,13 +56,18 @@ public class Alarm implements MqttStrategy {
         List<DmpDevice> list = dmpDeviceService.list(queryWrapper1);
         baseAlarm.setDeviceId(alarams.get(0).get("devId").toString());
         baseAlarm.setAlarmTime(time2);
-        baseAlarm.setAlarmType(dp.get(0).get("serial").toString());
+        String alarmType = dp.get(0).get("serial").toString();
+        baseAlarm.setAlarmType(alarmType);
         baseAlarm.setAlarmObject(alarams.get(0).get("deviceName").toString());
         baseAlarm.setAlarmData(dp.get(0).get("status").toString());
         baseAlarm.setAlarmAttribute(dp.get(0).get("property").toString());
         if (alarams.get(0).get("deviceType").equals("1")){
             baseAlarm.setAlarmContent(dp.get(0).get("property").toString()+","+dp.get(0).get("value").toString());
-            baseAlarm.setAlarmGrade(1);
+            if(alarmType.equals("812") || alarmType.equals("815") || alarmType.equals("819")){
+                baseAlarm.setAlarmGrade(1);
+            }else{
+                baseAlarm.setAlarmGrade(2);
+            }
             baseAlarm.setAlarmAddress(dp.get(0).get("value").toString());
         }else if (alarams.get(0).get("deviceType").equals("2")){
             baseAlarm.setAlarmContent(dp.get(0).get("property").toString()+","+dp.get(0).get("value").toString());

+ 33 - 0
service-backend/README.md

@@ -0,0 +1,33 @@
+# 后端服务
+
+## 模块信息
+
+| 项目 | 内容 |
+|------|------|
+| 所属系统(模块) | 后端服务 |
+| 分区 | backend |
+| 英文全称 | Backend Service |
+| 描述 | 提供基础后端服务支持 |
+| 模块分类 | 通用模块 |
+| 工程模块 | service-backend |
+
+## 功能说明
+
+- 提供基础后端服务功能
+- 支持其他模块的后端需求
+- 提供通用的后端接口和服务
+
+## 技术栈
+
+- Java
+- Spring Boot
+- MyBatis
+
+## 模块结构
+
+```
+service-backend/
+├── service-backend-api/         # API 接口模块
+├── service-backend-biz/         # 业务逻辑模块
+└── pom.xml                  # 模块依赖配置
+```

+ 34 - 0
service-cdi/README.md

@@ -0,0 +1,34 @@
+# 人防物联系统
+
+## 模块信息
+
+| 项目 | 内容 |
+|------|------|
+| 所属系统(模块) | 人防物联系统 |
+| 分区 | cdi |
+| 英文全称 | Civil Defense IoT |
+| 描述 | 提供人防物联场景应用,对接国动办平台 |
+| 模块分类 | 应用模块 |
+| 工程模块 | service-cdi |
+
+## 功能说明
+
+- 提供人防物联场景应用
+- 对接国动办平台
+- 管理人防物联网设备和数据
+
+## 技术栈
+
+- Java
+- Spring Boot
+- MyBatis
+- IoT 协议支持
+
+## 模块结构
+
+```
+service-cdi/
+├── service-cdi-api/         # API 接口模块
+├── service-cdi-biz/         # 业务逻辑模块
+└── pom.xml                  # 模块依赖配置
+```

+ 3 - 3
service-cockpit/pom.xml → service-cdi/pom.xml

@@ -8,13 +8,13 @@
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>service-iot</artifactId>
+    <artifactId>service-cdi</artifactId>
 
     <packaging>pom</packaging>
     <version>0.0.1</version>
 
     <modules>
-        <module>service-iot-biz</module>
-        <module>service-iot-api</module>
+        <module>service-cdi-biz</module>
+        <module>service-cdi-api</module>
     </modules>
 </project>

+ 31 - 0
service-cdi/service-cdi-api/pom.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>service-cdi</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-cdi-api</artifactId>
+    <!-- SpringCloud Openfeign -->
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>usky-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-openfeign-core</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>

+ 31 - 0
service-cdi/service-cdi-api/src/main/java/com/usky/cdi/AlarmDataSyncTaskService.java

@@ -0,0 +1,31 @@
+package com.usky.cdi;
+
+import com.usky.cdi.factory.AlarmDataSyncTaskFactory;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2025/12/29
+ */
+@FeignClient(contextId = "AlarmDataSyncTaskService", value = "service-cdi", fallbackFactory = AlarmDataSyncTaskFactory.class)
+public interface AlarmDataSyncTaskService {
+    /**
+     * 同步告警数据
+     *
+     * @param tenantId      租户ID
+     * @param engineeringId 工程ID
+     * @param username      mqtt用户名
+     * @param password      mqtt密码
+     * @param status        peacetime:平时 wartime:战时
+     */
+    @GetMapping("/synchronizeAlarmData")
+    void synchronizeAlarmData(@RequestParam(value = "tenantId") Integer tenantId,
+                              @RequestParam(value = "engineeringId") Long engineeringId,
+                              @RequestParam(value = "username") String username,
+                              @RequestParam(value = "password") String password,
+                              @RequestParam(value = "status") String status);
+}

+ 20 - 0
service-cdi/service-cdi-api/src/main/java/com/usky/cdi/RemotecdiTaskService.java

@@ -0,0 +1,20 @@
+package com.usky.cdi;
+
+import com.usky.cdi.factory.RemotecdiTaskFactory;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+@FeignClient(contextId = "RemotecdiTaskService", value = "service-cdi", fallbackFactory = RemotecdiTaskFactory.class)
+public interface RemotecdiTaskService {
+    @GetMapping("/synchronizeDeviceData")
+    void synchronizeDeviceData(@RequestParam(value = "tenantId") Integer tenantId,
+                               @RequestParam(value = "engineeringId") Long engineeringId,
+                               @RequestParam(value = "username") String username,
+                               @RequestParam(value = "password") String password);
+
+    @GetMapping("/allData")
+    void allData(@RequestParam("engineeringId") Long engineeringId,
+                               @RequestParam("username") String username,
+                               @RequestParam("password") String password);
+}

+ 29 - 0
service-cdi/service-cdi-api/src/main/java/com/usky/cdi/factory/AlarmDataSyncTaskFactory.java

@@ -0,0 +1,29 @@
+package com.usky.cdi.factory;
+
+import com.usky.cdi.AlarmDataSyncTaskService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2025/12/29
+ */
+@Component
+public class AlarmDataSyncTaskFactory implements FallbackFactory<AlarmDataSyncTaskService> {
+    private static final Logger log = LoggerFactory.getLogger(AlarmDataSyncTaskFactory.class);
+
+    @Override
+    public AlarmDataSyncTaskService create(Throwable throwable) {
+        log.error("用户服务调用失败:{}", throwable.getMessage());
+        return new AlarmDataSyncTaskService() {
+            @Override
+            public void synchronizeAlarmData(Integer tenantId, Long engineeringId, String username, String password, String status) {
+                throw new RuntimeException(throwable);
+            }
+        };
+    }
+}

+ 30 - 0
service-cdi/service-cdi-api/src/main/java/com/usky/cdi/factory/RemotecdiTaskFactory.java

@@ -0,0 +1,30 @@
+package com.usky.cdi.factory;
+
+import com.usky.cdi.RemotecdiTaskService;
+import com.usky.common.core.exception.FeignBadRequestException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class RemotecdiTaskFactory implements FallbackFactory<RemotecdiTaskService> {
+    private static final Logger log = LoggerFactory.getLogger(RemotecdiTaskFactory.class);
+
+    @Override
+    public RemotecdiTaskService create(Throwable throwable) {
+        log.error("用户服务调用失败:{}", throwable.getMessage());
+        return new RemotecdiTaskService() {
+            @Override
+            public void synchronizeDeviceData(Integer tenantId, Long engineeringId, String username, String password) {
+                throw new FeignBadRequestException(500, "人防设备数据定时推送异常(带租户)" + throwable.getMessage());
+            }
+
+            @Override
+            public void allData(Long engineeringId, String username, String password) {
+                throw new FeignBadRequestException(500, "人防设备数据定时推送异常" + throwable.getMessage());
+            }
+
+        };
+    }
+}

+ 42 - 41
service-cockpit/service-cockpit-biz/pom.xml → service-cdi/service-cdi-biz/pom.xml

@@ -1,14 +1,32 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <parent>
-        <artifactId>service-iot</artifactId>
+        <artifactId>service-cdi</artifactId>
         <groupId>com.usky</groupId>
         <version>0.0.1</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>service-iot-biz</artifactId>
+    <artifactId>service-cdi-biz</artifactId>
+
     <dependencies>
+        <!--                <dependency>-->
+        <!--                    <groupId>com.usky</groupId>-->
+        <!--                    <artifactId>common-cloud-starter</artifactId>-->
+        <!--                    <exclusions>-->
+        <!--                        &lt;!&ndash; 排除Nacos相关依赖 &ndash;&gt;-->
+        <!--                        <exclusion>-->
+        <!--                            <groupId>com.alibaba.cloud</groupId>-->
+        <!--                            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>-->
+        <!--                        </exclusion>-->
+        <!--                        <exclusion>-->
+        <!--                            <groupId>com.alibaba.cloud</groupId>-->
+        <!--                            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>-->
+        <!--                        </exclusion>-->
+        <!--                    </exclusions>-->
+        <!--                </dependency>-->
+
         <dependency>
             <groupId>com.usky</groupId>
             <artifactId>common-cloud-starter</artifactId>
@@ -22,7 +40,7 @@
 
         <dependency>
             <groupId>com.usky</groupId>
-            <artifactId>service-iot-api</artifactId>
+            <artifactId>service-cdi-api</artifactId>
             <version>0.0.1</version>
         </dependency>
 
@@ -60,54 +78,37 @@
             <artifactId>spring-websocket</artifactId>
             <version>5.2.8.RELEASE</version>
         </dependency>
-        <dependency>
-            <groupId>com.usky</groupId>
-            <artifactId>service-agbox-api</artifactId>
-            <version>0.0.1</version>
-            <scope>compile</scope>
-        </dependency>
 
-        <dependency>
-            <groupId>com.usky</groupId>
-            <artifactId>service-system-api</artifactId>
-            <version>0.0.1</version>
-        </dependency>
-
-        <dependency>
-            <groupId>com.github.binarywang</groupId>
-            <artifactId>weixin-java-mp</artifactId>
-            <version>4.3.0</version>
-        </dependency>
+        <!--Redis依赖-->
         <dependency>
             <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-amqp</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>com.usky</groupId>
-            <artifactId>service-alarm-api</artifactId>
-            <version>0.0.1</version>
-            <scope>compile</scope>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
 
+        <!--MySQL依赖-->
         <dependency>
-            <groupId>com.usky</groupId>
-            <artifactId>ruoyi-common-core</artifactId>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>runtime</scope>
         </dependency>
 
-        <!--钉钉-->
-        <!--获取企业accessToken(企业内部应用) 新版SDK-->
         <dependency>
-            <groupId>com.aliyun</groupId>
-            <artifactId>dingtalk</artifactId>
-            <version>2.1.34</version>
-        </dependency>
-        <!--旧版SDK-->
-        <dependency>
-            <groupId>com.aliyun</groupId>
-            <artifactId>alibaba-dingtalk-service-sdk</artifactId>
-            <version>2.0.0</version>
+            <groupId>com.mchange</groupId>
+            <artifactId>mchange-commons-java</artifactId>
+            <version>0.2.15</version>
+            <scope>compile</scope>
         </dependency>
 
+        <!--        <dependency>-->
+        <!--            <groupId>com.alibaba</groupId>-->
+        <!--            <artifactId>druid-spring-boot-starter</artifactId>-->
+        <!--            <version>1.2.20</version>-->
+        <!--        </dependency>-->
+        <!--        <dependency>-->
+        <!--            <groupId>com.alibaba.nacos</groupId>-->
+        <!--            <artifactId>nacos-client</artifactId>-->
+        <!--        </dependency>-->
+
     </dependencies>
 
     <build>

+ 108 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/MybatisGeneratorUtils.java

@@ -0,0 +1,108 @@
+package com.usky.cdi;//package com.usky.iot.controller;//package com.usky.dm.controller.web.business;//package com.usky.dm.controller.web;
+
+
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.generator.AutoGenerator;
+import com.baomidou.mybatisplus.generator.InjectionConfig;
+import com.baomidou.mybatisplus.generator.config.*;
+import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author yq
+ * @date 2021/7/6 11:42
+ */
+public class MybatisGeneratorUtils {
+    public static void main(String[] args) {
+
+            shell("service-cdi","service-cdi-biz");
+    }
+
+    private static void shell(String parentName,String model) {
+
+        AutoGenerator mpg = new AutoGenerator();
+        //1、全局配置
+        GlobalConfig gc = new GlobalConfig();
+//        File file = new File(model);
+//        String path = file.getAbsolutePath();
+        String projectPath = System.getProperty("user.dir");
+        projectPath+="/"+parentName;
+        projectPath+="/"+model;
+        gc.setOutputDir(projectPath+ "/src/main/java");  //生成路径(一般都是生成在此项目的src/main/java下面)
+        //修改为自己的名字
+        gc.setAuthor("fu"); //设置作者
+        gc.setOpen(false);
+        gc.setFileOverride(true); //第二次生成会把第一次生成的覆盖掉
+        gc.setServiceName("%sService"); //生成的service接口名字首字母是否为I,这样设置就没有
+        gc.setBaseResultMap(true); //生成resultMap
+        mpg.setGlobalConfig(gc);
+
+        //2、数据源配置
+        //修改数据源
+        DataSourceConfig dsc = new DataSourceConfig();
+        dsc.setUrl("jdbc:mysql://192.168.10.165:3306/usky-cloud?useUnicode=true&serverTimezone=GMT&useSSL=false&characterEncoding=utf8");
+        dsc.setDriverName("com.mysql.jdbc.Driver");
+        dsc.setUsername("root");
+        dsc.setPassword("yt123456");
+        mpg.setDataSource(dsc);
+
+        // 3、包配置
+        PackageConfig pc = new PackageConfig();
+        pc.setParent("com.usky.cdi");
+        pc.setController("controller.web");
+        pc.setEntity("domain");
+        pc.setMapper("mapper");
+        pc.setService("service");
+        pc.setServiceImpl("service.impl");
+//        pc.setXml("mapper.demo");
+        //pc.setModuleName("test");
+        mpg.setPackageInfo(pc);
+
+        // 4、策略配置
+        StrategyConfig strategy = new StrategyConfig();
+        strategy.setNaming(NamingStrategy.underline_to_camel);
+        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
+        strategy.setSuperMapperClass("com.usky.common.mybatis.core.CrudMapper");
+        strategy.setSuperServiceClass("com.usky.common.mybatis.core.CrudService");
+        strategy.setSuperServiceImplClass("com.usky.common.mybatis.core.AbstractCrudService");
+        // strategy.setTablePrefix("t_"); // 表名前缀
+        strategy.setEntityLombokModel(true); //使用lombokbase_build_plane
+        //修改自己想要生成的表
+        strategy.setInclude("dmp_device");  // 逆向工程使用的表   如果要生成多个,这里可以传入String[]
+        mpg.setStrategy(strategy);
+
+        // 关闭默认 xml 生成,调整生成 至 根目录
+        //修改对应的模块名称
+        TemplateConfig tc = new TemplateConfig();
+        // 自定义配置
+        InjectionConfig cfg = new InjectionConfig() {
+            @Override
+            public void initMap() {
+                // to do nothing
+            }
+        };
+        //如果模板引擎是 velocity
+        String templatePath = "/templates/mapper.xml.vm";
+        // 自定义输出配置
+        List<FileOutConfig> focList = new ArrayList<>();
+        // 自定义配置会被优先输出
+        String finalProjectPath = projectPath;
+        focList.add(new FileOutConfig(templatePath) {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return finalProjectPath + "/src/main/resources/mapper/cdi" + "/"
+                        + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
+            }
+        });
+        cfg.setFileOutConfigList(focList);
+        mpg.setCfg(cfg);
+        tc.setXml(null);
+        mpg.setTemplate(tc);
+        //5、执行
+        mpg.execute();
+    }
+}

+ 11 - 27
service-cockpit/service-cockpit-biz/src/main/java/com/usky/cockpit/RuoYiSystemApplication.java → service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/cdiApplication.java

@@ -1,24 +1,17 @@
-package com.usky.iot;
+package com.usky.cdi;
 
-
-
-import com.usky.iot.constant.constant;
-import io.swagger.annotations.SwaggerDefinition;
-import lombok.val;
-import me.chanjar.weixin.mp.api.WxMpService;
-import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
-import me.chanjar.weixin.mp.config.impl.WxMpMapConfigImpl;
 import org.mybatis.spring.annotation.MapperScan;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.amqp.rabbit.annotation.EnableRabbit;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.openfeign.EnableFeignClients;
 import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.core.env.Environment;
+import org.springframework.integration.config.EnableIntegration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -30,28 +23,19 @@ import java.net.UnknownHostException;
  */
 
 //@EnableSwagger2
+@EnableScheduling
+@EnableAsync // 启用异步执行支持
 @EnableFeignClients(basePackages = "com.usky")
-@MapperScan(value = "com.usky.iot.mapper")
+@MapperScan(value = "com.usky.cdi.mapper")
 @ComponentScan("com.usky")
+@EnableIntegration // 启用Spring Integration
 @SpringBootApplication
-@EnableRabbit
-public class RuoYiSystemApplication
+public class cdiApplication
 {
-    private static final Logger LOGGER = LoggerFactory.getLogger(RuoYiSystemApplication.class);
-
-    @Bean
-    public WxMpService wxMpService(){
-        WxMpMapConfigImpl wxMpMapConfig = new WxMpMapConfigImpl();
-        wxMpMapConfig.setAppId(constant.WE_CHAT_APP_ID);
-        wxMpMapConfig.setSecret(constant.WE_CHAT_SECRET);
-        val wxMpService = new WxMpServiceImpl();
-        wxMpService.setWxMpConfigStorage(wxMpMapConfig);
-
-        return wxMpService;
-    }
+    private static final Logger LOGGER = LoggerFactory.getLogger(cdiApplication.class);
 
     public static void main(String[] args) throws UnknownHostException {
-        ConfigurableApplicationContext application = SpringApplication.run(RuoYiSystemApplication.class, args);
+        ConfigurableApplicationContext application = SpringApplication.run(cdiApplication.class, args);
         Environment env = application.getEnvironment();
         String ip = InetAddress.getLocalHost().getHostAddress();
         String port = env.getProperty("server.port");

+ 39 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/AlarmDataController.java

@@ -0,0 +1,39 @@
+package com.usky.cdi.controller;
+
+import com.usky.cdi.service.impl.AlarmDataTransferService;
+import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
+import com.usky.common.core.bean.ApiResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 告警数据 HTTP 入口:将告警上报至市适配平台(MQTT)。
+ *
+ * @author han
+ * @date 2025/12/08
+ */
+@RestController
+@RequestMapping("/api/alarm")
+@ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
+@RequiredArgsConstructor
+public class AlarmDataController {
+
+    private final AlarmDataTransferService alarmDataTransferService;
+
+    private static ApiResult<String> toSubmitResult(boolean success) {
+        return success ? ApiResult.success("上报成功") : ApiResult.error("上报失败");
+    }
+
+    /**
+     * 告警推送
+     */
+    @PostMapping("/alarmMessage")
+    public ApiResult<String> alarmMessage(@RequestBody AlarmMessageVO<?> vo) {
+        return toSubmitResult(alarmDataTransferService.publishAlarm(vo));
+    }
+
+}

+ 89 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/BaseDataController.java

@@ -0,0 +1,89 @@
+package com.usky.cdi.controller;
+
+import com.usky.cdi.service.impl.BaseDataTransferService;
+import com.usky.cdi.service.vo.info.EngineeringBaseVO;
+import com.usky.cdi.service.vo.info.FacilityDeviceVO;
+import com.usky.cdi.service.vo.info.FloorPlaneVO;
+import com.usky.cdi.service.vo.info.ProtectiveUnitVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 基础类数据传输控制器
+ * 提供基础类数据上报的接口
+ *
+ * @author han
+ * @date 2025/03/20
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/base")
+@ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
+public class BaseDataController {
+
+    @Autowired
+    private BaseDataTransferService baseDataTransferService;
+
+    /**
+     * 上报人防工程基础信息
+     */
+    @PostMapping("/engineering")
+    public String sendEngineeringBase(@RequestBody EngineeringBaseVO vo) {
+        boolean success = baseDataTransferService.sendEngineeringBase(vo);
+        return success ? "上报成功" : "上报失败";
+    }
+
+    /**
+     * 上报防护单元基础信息
+     */
+    @PostMapping("/protectiveUnit")
+    public String sendProtectiveUnit(@RequestBody ProtectiveUnitVO vo) {
+        boolean success = baseDataTransferService.sendProtectiveUnit(vo);
+        return success ? "上报成功" : "上报失败";
+    }
+
+    /**
+     * 批量上报防护单元基础信息
+     */
+    @PostMapping("/protectiveUnits")
+    public String batchSendProtectiveUnits(@RequestBody List<ProtectiveUnitVO> units) {
+        int successCount = baseDataTransferService.batchSendProtectiveUnits(units);
+        return String.format("上报成功 %d/%d", successCount, units.size());
+    }
+
+    /**
+     * 上报楼层平面图信息
+     */
+    @PostMapping("/floorPlane")
+    public String sendFloorPlane(@RequestBody FloorPlaneVO vo) {
+        boolean success = baseDataTransferService.sendFloorPlane(vo);
+        return success ? "上报成功" : "上报失败";
+    }
+
+    /**
+     * 上报智能监管物联设施信息
+     */
+    @PostMapping("/sensorInfo")
+    public String sendSensorInfo(@RequestBody FacilityDeviceVO vo) {
+        boolean success = baseDataTransferService.sendSensorInfo(vo);
+        return success ? "上报成功" : "上报失败";
+    }
+
+    /**
+     * 批量上报智能监管物联设施信息
+     */
+    @GetMapping("/sensorInfos")
+    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"));
+    }
+}
+

+ 68 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/IotDataController.java

@@ -0,0 +1,68 @@
+package com.usky.cdi.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.usky.cdi.service.impl.IotDataTransferService;
+import com.usky.cdi.service.vo.IotDataTransferVO;
+import com.usky.cdi.service.vo.base.*;
+import com.usky.common.core.bean.ApiResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2025/11/20
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/iotInfo")
+@ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
+public class IotDataController {
+
+    @Autowired
+    private IotDataTransferService iotDataTransferService;
+
+    /**
+     * 上报水浸状态
+     */
+    @PostMapping("/flooded")
+    public ApiResult<Void> sendWaterLeak(@RequestBody IotDataTransferVO jsonObject) {
+        // iotDataTransferService.createMqttConnection("3101130019", "ptrEQZK2");
+        iotDataTransferService.sendWaterLeak(jsonObject);
+        return ApiResult.success();
+    }
+
+    /**
+     * 上报温度、湿度、氧气、一氧化碳、二氧化碳
+     */
+    @PostMapping("/envData")
+    public ApiResult<Void> sendEnvData(@RequestBody IotDataTransferVO jsonObject) {
+        iotDataTransferService.sendEnvData(jsonObject);
+        return ApiResult.success();
+    }
+
+    /**
+     * 上报人员闯入
+     */
+    @PostMapping("/personPresence")
+    public ApiResult<Void> sendPerson(@RequestBody IotDataTransferVO jsonObject) {
+        iotDataTransferService.sendPersonPresence(jsonObject);
+        return ApiResult.success();
+    }
+
+    /**
+     * 上报用电负荷
+     */
+    @PostMapping("/electricityLoad")
+    public ApiResult<Void> sendElectricityLoad(@RequestBody IotDataTransferVO jsonObject) {
+        iotDataTransferService.sendElectricityLoad(jsonObject);
+        return ApiResult.success();
+    }
+
+}

+ 34 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/api/AlarmDataSyncTaskApi.java

@@ -0,0 +1,34 @@
+package com.usky.cdi.controller.api;
+
+import com.usky.cdi.AlarmDataSyncTaskService;
+import com.usky.cdi.service.impl.AlarmDataSyncService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 多租户定时任务API接口
+ *
+ * @author usky
+ * @since 2025-12-29
+ */
+@Slf4j
+@RestController
+public class AlarmDataSyncTaskApi implements AlarmDataSyncTaskService {
+
+    @Autowired
+    private AlarmDataSyncService alarmDataSyncService;
+
+    /**
+     * 同步告警数据
+     * tenantId: 租户ID
+     * engineeringId: 工程ID
+     * username: mqtt用户名
+     * password: mqtt密码
+     * status: peacetime:平时 wartime:战时
+     */
+    @Override
+    public void synchronizeAlarmData(Integer tenantId, Long engineeringId, String username, String password, String status) {
+        alarmDataSyncService.synchronizeAlarmData(tenantId, engineeringId, username, password, status);
+    }
+}

+ 30 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/api/RemotecdiTaskApi.java

@@ -0,0 +1,30 @@
+package com.usky.cdi.controller.api;
+
+import com.usky.cdi.RemotecdiTaskService;
+import com.usky.cdi.service.impl.IotDataTransferService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ *  数据统一查询 前端控制器
+ * </p>
+ *
+ * @author f
+ */
+@RestController
+public class RemotecdiTaskApi implements RemotecdiTaskService {
+
+    @Autowired
+    private IotDataTransferService iotDataTransferService;
+
+    @Override
+    public void synchronizeDeviceData(Integer tenantId, Long engineeringId, String username, String password) {
+        iotDataTransferService.synchronizeDeviceData(tenantId, engineeringId, username, password);
+    }
+
+    @Override
+    public void allData(Long engineeringId, String username, String password) {
+        iotDataTransferService.allData(engineeringId, username, password);
+    }
+}

+ 9 - 7
service-cockpit/service-cockpit-biz/src/main/java/com/usky/cockpit/controller/web/SysUserController.java → service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseAlarmController.java

@@ -1,20 +1,22 @@
-package cn.com.v2.controller;
+package com.usky.cdi.controller.web;
 
 
 import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+
 import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RestController;
 
 /**
  * <p>
- *  前端控制器
+ * 统一告警表 前端控制器
  * </p>
  *
- * @author fc
- * @since 2023-04-30
+ * @author fu
+ * @since 2025-12-29
  */
 @RestController
-@RequestMapping("/v2/sys-user")
-public class SysUserController {
+@RequestMapping("/baseAlarm")
+public class BaseAlarmController {
 
 }
+

+ 21 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseBuildController.java

@@ -0,0 +1,21 @@
+package com.usky.cdi.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 建筑信息 前端控制器
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+@Controller
+@RequestMapping("/baseBuild")
+public class BaseBuildController {
+
+}
+

+ 21 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseBuildFacilityController.java

@@ -0,0 +1,21 @@
+package com.usky.cdi.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 建筑设施 前端控制器
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-02
+ */
+@Controller
+@RequestMapping("/baseBuildFacility")
+public class BaseBuildFacilityController {
+
+}
+

+ 21 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseBuildPlaneController.java

@@ -0,0 +1,21 @@
+package com.usky.cdi.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 建筑楼层平面图 前端控制器
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+@Controller
+@RequestMapping("/baseBuildPlane")
+public class BaseBuildPlaneController {
+
+}
+

+ 21 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/BaseBuildUnitController.java

@@ -0,0 +1,21 @@
+package com.usky.cdi.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ *  前端控制器
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+@Controller
+@RequestMapping("/baseBuildUnit")
+public class BaseBuildUnitController {
+
+}
+

+ 158 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/CdiDefenseProjectController.java

@@ -0,0 +1,158 @@
+package com.usky.cdi.controller.web;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.cdi.domain.CdiDefenseProject;
+import com.usky.cdi.service.CdiDefenseProjectService;
+import com.usky.cdi.service.mqtt.MqttConnectionTool;
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.log.annotation.Log;
+import com.usky.common.log.enums.BusinessType;
+import com.usky.common.security.utils.SecurityUtils;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 人防_工程表(人防工程基础信息及MQTT连接配置) 前端控制器
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+@Api(tags = "人防_工程表")
+@Slf4j
+@RestController
+@RequestMapping("/cdiDefenseProject")
+@RequiredArgsConstructor
+public class CdiDefenseProjectController {
+
+    private final MqttConnectionTool mqttConnectionTool;
+
+    @Autowired
+    private CdiDefenseProjectService cdiDefenseProjectService;
+
+    @ApiOperation("查询人防工程配置")
+    @GetMapping("config")
+    public ApiResult<CdiDefenseProject> query() {
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (tenantId == null) {
+            return null;
+        }
+        return ApiResult.success(cdiDefenseProjectService.listByTenantId(tenantId));
+    }
+
+    /**
+     * 新增人防工程
+     */
+    @ApiOperation("新增人防工程配置")
+    @Log(title = "新增人防工程配置", businessType = BusinessType.INSERT)
+    @PostMapping("config")
+    public ApiResult<String> add(@RequestBody CdiDefenseProject cdiDefenseProject) {
+        cdiDefenseProject.setCreateBy(SecurityUtils.getUsername());
+        cdiDefenseProject.setCreateTime(LocalDateTime.now());
+        cdiDefenseProject.setTenantId(SecurityUtils.getTenantId());
+        cdiDefenseProject.setIsEnable(1);
+        boolean save = cdiDefenseProjectService.save(cdiDefenseProject);
+
+        if (save) {
+            mqttConnectionTool.connectOrRefresh(cdiDefenseProject.getMqttUserName(), cdiDefenseProject.getMqttPassword());
+            return ApiResult.success("新增成功!");
+        } else {
+            return ApiResult.error("新增失败!");
+        }
+    }
+
+    /**
+     * 修改人防工程
+     */
+    @ApiOperation("修改人防工程")
+    @Log(title = "修改人防工程", businessType = BusinessType.UPDATE)
+    @PutMapping("config")
+    public ApiResult<String> edit(@RequestBody CdiDefenseProject cdiDefenseProject) {
+        Long id = cdiDefenseProject.getId();
+        if (id == null || id <= 0) {
+            return ApiResult.error("参数错误,请检查ID");
+        }
+        CdiDefenseProject existCdiDefenseProject = cdiDefenseProjectService.getById(id);
+        if (existCdiDefenseProject == null) {
+            return ApiResult.error("工程配置不存在!");
+        }
+
+        cdiDefenseProject.setUpdateBy(SecurityUtils.getUsername());
+        cdiDefenseProject.setUpdateTime(LocalDateTime.now());
+        boolean b = cdiDefenseProjectService.updateById(cdiDefenseProject);
+        if (b) {
+            return ApiResult.success("修改成功!");
+        } else {
+            return ApiResult.error("修改失败!");
+        }
+    }
+
+    /**
+     * 更改人防工程状态
+     */
+    @ApiOperation("更改人防工程状态")
+    @Log(title = "更改人防工程状态", businessType = BusinessType.UPDATE)
+    @PutMapping("/configState")
+    public ApiResult<String> changeStatus(@RequestBody CdiDefenseProject cdiDefenseProject) {
+        Long id = cdiDefenseProject.getId();
+        if (id == null || id <= 0) {
+            return ApiResult.error("参数错误,请检查ID");
+        }
+        Integer isEnable = cdiDefenseProject.getIsEnable();
+        CdiDefenseProject cdiDefenseProject1 = cdiDefenseProjectService.getById(id);
+        if (cdiDefenseProject1 == null) {
+            return ApiResult.error("工程配置不存在!");
+        }
+        if (isEnable < 0 || isEnable > 1) {
+            return ApiResult.error("参数错误,请检查状态值!");
+        }
+
+        cdiDefenseProject1.setId(id);
+        cdiDefenseProject1.setIsEnable(isEnable);
+        cdiDefenseProject1.setUpdateBy(SecurityUtils.getUsername());
+        cdiDefenseProject1.setUpdateTime(LocalDateTime.now());
+        boolean b = cdiDefenseProjectService.updateById(cdiDefenseProject1);
+
+        if (b && isEnable == 1) {
+            return ApiResult.success("启用成功!");
+        } else if (b) {
+            return ApiResult.success("禁用成功!");
+        } else {
+            return ApiResult.error("操作失败!");
+        }
+
+    }
+
+    /**
+     * MQTT连通性测试
+     */
+    @ApiOperation("MQTT连通性测试")
+    @GetMapping("configVerify")
+    public ApiResult<String> testMqttConnection(@ApiParam("工程ID") @RequestParam Long id) {
+        CdiDefenseProject cdiDefenseProject = cdiDefenseProjectService.getById(id);
+        if (cdiDefenseProject == null) {
+            return ApiResult.error("工程配置不存在!");
+        }
+
+        String broker = cdiDefenseProject.getPushAddress();
+        String user = cdiDefenseProject.getMqttUserName();
+        String pass = cdiDefenseProject.getMqttPassword();
+        String clientId = cdiDefenseProject.getMqttClientId();
+        boolean b = cdiDefenseProjectService.tryConnectAndPublish(broker, user, pass, "PING", clientId);
+
+        if (b) {
+            return ApiResult.success("MQTT 连通性测试成功!");
+        } else {
+            return ApiResult.error("MQTT 连通性测试失败,请检查网络、账号和密码!");
+        }
+    }
+}
+

+ 92 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/CdiDeliveryLogController.java

@@ -0,0 +1,92 @@
+package com.usky.cdi.controller.web;
+
+import com.usky.cdi.domain.CdiDeliveryLog;
+import com.usky.cdi.mapper.CdiDefenseProjectMapper;
+import com.usky.cdi.service.CdiDeliveryLogService;
+import com.usky.cdi.service.vo.SyncTaskStatisticsVO;
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.security.utils.SecurityUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 人防_投递日志表(记录数据的MQTT投递情况) 前端控制器
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+@RestController
+@RequestMapping("/cdiDeliveryLog")
+public class CdiDeliveryLogController {
+
+    @Autowired
+    private CdiDeliveryLogService cdiDeliveryLogService;
+    @Autowired
+    private CdiDefenseProjectMapper cdpMapper;
+
+    /**
+     * 同步数据查询
+     * @param id 主键id
+     * @return 同步数据
+     */
+    @GetMapping("synchronizeData")
+    public List<SyncTaskStatisticsVO> synchronizeData(@RequestParam(value = "id", required = false) Long id) {
+        return cdiDeliveryLogService.selectById(id);
+    }
+
+    /**
+     * 同步数据
+     * @param vo 同步数据
+     * @return 同步结果
+     */
+    @PutMapping("synchronizeData")
+    public ApiResult<Void> synchronizeData(@RequestBody SyncTaskStatisticsVO vo) {
+        cdiDeliveryLogService.synchronizeData(vo);
+        return ApiResult.success();
+    }
+
+    /**
+     * 日志列表
+     * @param id 主键id
+     * @param pageNum     页码
+     * @param pageSize     每页数量
+     * @param dataType 数据类型
+     * @param logType 日志类型
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 日志列表
+     */
+    @GetMapping("logList")
+    public ApiResult<CommonPage<CdiDeliveryLog>> logList(@RequestParam(value = "id", required = false) Long id,
+                                                         @RequestParam(value = "pageNum", required = false, defaultValue = "1") Integer pageNum,
+                                                         @RequestParam(value = "pageSize", required = false, defaultValue = "20") Integer pageSize,
+                                                         @RequestParam(value = "dataType", required = false) Integer dataType,
+                                                         @RequestParam(value = "logType", required = false) Integer logType,
+                                                         @RequestParam(value = "startTime", required = false) String startTime,
+                                                         @RequestParam(value = "endTime", required = false) String endTime) {
+        return ApiResult.success(cdiDeliveryLogService.logList(id, pageNum, pageSize, dataType, logType, startTime, endTime));
+    }
+
+    /**
+     * 日志详情
+     * @param id 主键id
+     * @return 日志详情
+     */
+    @GetMapping("logId")
+    public ApiResult<String> logId(@RequestParam(value = "id") Long id) {
+        CdiDeliveryLog log = cdiDeliveryLogService.lambdaQuery()
+                .eq(CdiDeliveryLog::getTenantId, SecurityUtils.getTenantId())
+                .eq(CdiDeliveryLog::getId, id)
+                .one();
+        if (log == null) {
+            return ApiResult.error("日志不存在");
+        }
+        return ApiResult.success(log.getInfoContent());
+    }
+}
+

+ 33 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/DmpDeviceInfoController.java

@@ -0,0 +1,33 @@
+package com.usky.cdi.controller.web;
+
+import com.usky.backend.domain.*;
+import com.usky.cdi.domain.DmpDevice;
+import com.usky.cdi.service.DmpDeviceInfoService;
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.utils.poi.ExcelUtil;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备信息表
+ *
+ * @author ya
+ * @since 2022-10-08
+ */
+@RestController
+@RequestMapping("/cdiDmpDeviceInfo")
+public class DmpDeviceInfoController {
+
+    @Autowired
+    private DmpDeviceInfoService dmpDeviceInfoService;
+
+}
+

+ 22 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/DmpProductController.java

@@ -0,0 +1,22 @@
+package com.usky.cdi.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 产品信息表 前端控制器
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-05
+ */
+@RestController
+@RequestMapping("/dmpProduct")
+public class DmpProductController {
+
+}
+

+ 21 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/web/SysUserController.java

@@ -0,0 +1,21 @@
+package com.usky.cdi.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 用户信息表 前端控制器
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-04
+ */
+@Controller
+@RequestMapping("/sysUser")
+public class SysUserController {
+
+}
+

+ 127 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseAlarm.java

@@ -0,0 +1,127 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 统一告警表
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-29
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class BaseAlarm implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 设备ID
+     */
+    private String deviceId;
+
+    /**
+     * 告警时间
+     */
+    private LocalDateTime alarmTime;
+
+    /**
+     * 告警类型
+     */
+    private String alarmType;
+
+    /**
+     * 告警对象
+     */
+    private String alarmObject;
+
+    /**
+     * 告警值
+     */
+    private String alarmData;
+
+    /**
+     * 告警属性
+     */
+    private String alarmAttribute;
+
+    /**
+     * 告警内容
+     */
+    private String alarmContent;
+
+    /**
+     * 告警等级
+     * 1 一级,2 二级,3 三级,4 四级,5 五级
+     */
+    private Integer alarmGrade;
+
+    /**
+     * 告警位置
+     */
+    private String alarmAddress;
+
+    /**
+     * 处理人
+     */
+    private String handleBy;
+
+    /**
+     * 处理时间
+     */
+    private LocalDateTime handleTime;
+
+    /**
+     * 处理内容
+     */
+    private String handleContent;
+
+    /**
+     * 处理人电话
+     */
+    private String handlePhone;
+
+    /**
+     * 处理状态;0 未处理,1 已处理
+     */
+    private Integer handleStatus;
+
+    /**
+     * 是否误报;0 非误报,1 误报
+     */
+    private Integer alarmFalse;
+
+    /**
+     * 现场照片
+     */
+    private String sitePhoto;
+
+    /**
+     * 产品编码
+     */
+    private String productCode;
+
+    /**
+     * 组织机构ID
+     */
+    private Integer deptId;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+
+}

+ 212 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseBuild.java

@@ -0,0 +1,212 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import java.time.LocalDate;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 建筑信息
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class BaseBuild implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 建筑编号
+     */
+    private String buildNum;
+
+    /**
+     * 建筑名称
+     */
+    private String buildName;
+
+    /**
+     * 详细地址
+     */
+    private String address;
+
+    /**
+     * 模型地址
+     */
+    private String modelAddress;
+
+    /**
+     * 地上楼层
+     */
+    private Integer aboveFloor;
+
+    /**
+     * 地下楼层
+     */
+    private Integer underFloor;
+
+    /**
+     * 建筑面积
+     */
+    private Double buildArea;
+
+    /**
+     * 占地面积
+     */
+    private Double coverArea;
+
+    /**
+     * 耐火等级
+     */
+    private Integer fireRating;
+
+    /**
+     * 使用性质
+     */
+    private Integer useCharacter;
+
+    /**
+     * 建筑结构
+     */
+    private Integer buildStructure;
+
+    /**
+     * 建筑高度
+     */
+    private Double buildHigh;
+
+    /**
+     * 建筑高度分类
+     */
+    private Integer highType;
+
+    /**
+     * 竣工年份
+     */
+    private LocalDate completeYear;
+
+    /**
+     * 安全责任人
+     */
+    private String safePerson;
+
+    /**
+     * 安全管理人
+     */
+    private String managePerson;
+
+    /**
+     * 火灾危险性
+     */
+    private Integer fireRisk;
+
+    /**
+     * 消防控制室位置
+     */
+    private String fireControlRoom;
+
+    /**
+     * 建筑立面图
+     */
+    private String buildInside;
+
+    /**
+     * 建筑平面图
+     */
+    private String buildPlan;
+
+    /**
+     * 设施ID
+     */
+    private Integer facilityId;
+
+    /**
+     * BIM地址
+     */
+    private String bimUrl;
+
+    /**
+     * 联系人电话
+     */
+    private String contactPhone;
+
+    /**
+     * 建筑备注
+     */
+    private String buildDesc;
+
+    /**
+     * 单元数量
+     */
+    private Integer unitCount;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 更新人
+     */
+    private String updateBy;
+
+    /**
+     * 创建人
+     */
+    private String createBy;
+
+    /**
+     * 删除标识
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 地下空间
+     */
+    private Double underSpace;
+
+    /**
+     * 防火涂层(0、无 1、有)
+     */
+    private Integer fireproofCoat;
+
+    /**
+     * 组织机构ID
+     */
+    private Integer deptId;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+    /**
+     * 经度(当设施类型为点时使用该字段)
+     */
+    private String longitude;
+
+    /**
+     * 纬度(当设施类型为点时使用该字段)
+     */
+    private String latitude;
+
+
+}

+ 162 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseBuildFacility.java

@@ -0,0 +1,162 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 建筑设施
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-02
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class BaseBuildFacility implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 设施编号
+     */
+    private String facilityNum;
+
+    /**
+     * 设施名称
+     */
+    private String facilityName;
+
+    /**
+     * 设施类型
+     */
+    private String facilityType;
+
+    /**
+     * 所属楼层
+     */
+    private String floor;
+
+    /**
+     * 安装位置
+     */
+    private String address;
+
+    /**
+     * 图⽚地址URL
+     */
+    private String imagesUrl;
+
+    /**
+     * 设备ID
+     */
+    private String deviceId;
+
+    /**
+     * 联系人
+     */
+    private String contact;
+
+    /**
+     * 联系方式
+     */
+    private String contactPhone;
+
+    /**
+     * 平面X轴坐标
+     */
+    private String planeX;
+
+    /**
+     * 平面Y轴坐标
+     */
+    private String planeY;
+
+    private String coordinateX;
+
+    private String coordinateY;
+
+    private String coordinateZ;
+
+    /**
+     * 删除标识
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 更新人
+     */
+    private String updateBy;
+
+    /**
+     * 创建人
+     */
+    private String createBy;
+
+    /**
+     * 组织结构ID
+     */
+    private Integer deptId;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+    /**
+     * 建筑设施备注
+     */
+    private String facilityDesc;
+
+    /**
+     * 三维角度X
+     */
+    private Double anglesX;
+
+    /**
+     * 三维角度y
+     */
+    private Double anglesY;
+
+    /**
+     * 三维角度z
+     */
+    private Double anglesZ;
+
+    /**
+     * 三维图标大小
+     */
+    private Double scaleL;
+
+    /**
+     * 三维图标大小
+     */
+    private Double scaleW;
+
+    /**
+     * 三维图标大小
+     */
+    private Double scaleH;
+
+
+}

+ 56 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseBuildPlane.java

@@ -0,0 +1,56 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 建筑楼层平面图
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class BaseBuildPlane implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 建筑ID
+     */
+    private Integer buildId;
+
+    /**
+     * 所属楼层
+     */
+    private String floor;
+
+    /**
+     * 平面图路径
+     */
+    private String planeViewUrl;
+
+    /**
+     * 创建人
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+
+}

+ 101 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/BaseBuildUnit.java

@@ -0,0 +1,101 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class BaseBuildUnit implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 单元信息表
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 单元名称
+     */
+    private String unitName;
+
+    /**
+     * 单元主要出入口
+     */
+    private String unitMainExit;
+
+    /**
+     * 单元次要出入口
+     */
+    private String unitOtherExit;
+
+    /**
+     * 单元面积
+     */
+    private Double unitArea;
+
+    /**
+     * 楼层
+     */
+    private String floor;
+
+    /**
+     * 防护单元用途
+     */
+    private String unitUsage;
+
+    /**
+     * 掩蔽人数上限
+     */
+    private Integer peopleNumber;
+
+    /**
+     * 建筑ID
+     */
+    private Integer buildId;
+
+    /**
+     * 创建人
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新人
+     */
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 组织机构ID
+     */
+    private Integer deptId;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+
+
+}

+ 135 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/CdiDefenseProject.java

@@ -0,0 +1,135 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+
+import java.time.LocalDateTime;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+
+import java.io.Serializable;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * <p>
+ * 人防_工程表(人防工程基础信息及MQTT连接配置)
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+//@TableName("cdi_defense_project")
+public class CdiDefenseProject implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 人防_工程表主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 工程名称
+     */
+    @NotBlank(message = "工程名称不能为空")
+    private String engineeringName;
+
+    /**
+     * 工程id
+     */
+    @NotNull(message = "工程id不能为空")
+    private Long engineeringId;
+
+    /**
+     * 密码
+     */
+    @NotBlank(message = "密码不能为空")
+    private String passwd;
+
+    /**
+     * 推送地址(带端口)
+     */
+    @NotBlank(message = "推送地址不能为空")
+    private String pushAddress;
+
+    /**
+     * mqtt用户名
+     */
+    @NotBlank(message = "mqtt用户名不能为空")
+    private String mqttUserName;
+
+    /**
+     * mqtt密码
+     */
+    @NotBlank(message = "mqtt密码不能为空")
+    private String mqttPassword;
+
+    /**
+     * mqtt客户端id
+     */
+    @NotBlank(message = "mqtt客户端id不能为空")
+    private String mqttClientId;
+
+    /**
+     * 基础数据MQTT消息主题
+     */
+    //@NotBlank(message = "基础数据MQTT消息主题不能为空")
+    private String baseTopic;
+
+    /**
+     * 监测数据MQTT消息主题
+     */
+    //@NotBlank(message = "监测数据MQTT消息主题不能为空")
+    //@TableField("iotInfo_topic")
+    private String iotinfoTopic;
+
+    /**
+     * 告警数据MQTT消息主题
+     */
+    //@NotBlank(message = "告警数据MQTT消息主题不能为空")
+    private String alarmTopic;
+
+    /**
+     * 创建者
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 是否启用(0:未启用,1:已启用)
+     */
+    @NotNull(message = "是否启用不能为空")
+    private Integer isEnable;
+
+    /**
+     * 租户ID
+     */
+    @NotNull(message = "租户ID不能为空")
+    private Integer tenantId;
+
+
+}

+ 90 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/CdiDeliveryLog.java

@@ -0,0 +1,90 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 人防_投递日志表(记录数据的MQTT投递情况)
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+// @DS("second")
+public class CdiDeliveryLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 人防_投递日志表主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 工程id
+     */
+    private Long engineeringId;
+
+    /**
+     * 数据类型:1-单元,2-平面图,3-设施,4-监测, 5-告警
+     */
+    private Integer dataType;
+
+    /**
+     * 数据类型名称
+     */
+    @TableField(exist = false)
+    private String dataTypeName;
+
+    /**
+     * MQTT消息主题(base/engineering、iotInfo/temp、alarm/message等)
+     */
+    private String topic;
+
+    /**
+     * 数据内容(JSON格式字符串,存储此次上报数据推送情况)
+     */
+    @TableField("data_content")
+    private String infoContent;
+
+    /**
+     * 推送结果(0:失败,1:成功)
+     */
+    private Integer pushFlag;
+
+    /**
+     * 创建者
+     */
+    @TableField("create_by")
+    private String userName;
+
+    /**
+     * 创建者昵称
+     */
+    @TableField(exist = false)
+    private String nickName;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+
+}

+ 132 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/DmpDevice.java

@@ -0,0 +1,132 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 设备信息表
+ * </p>
+ *
+ * @author han
+ * @since 2023-09-22
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DmpDevice implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 设备ID;设备注册时系统自动生成一个唯一编号
+     */
+    private String deviceId;
+
+    /**
+     * 设备名称
+     */
+    private String deviceName;
+
+    /**
+     * 设备类型(501、监控系统  502、门禁系统  503、梯控系统  504、机房系统  509、环境系统  510、照明系统)
+     */
+    private Integer deviceType;
+
+    /**
+     * 产品ID
+     */
+    private Integer productId;
+
+    /**
+     * 物联网卡号
+     */
+    private String simCode;
+
+    /**
+     * 国际移动用户识别码
+     */
+    private String imsiCode;
+
+    /**
+     * 自动订阅标识(0:否,1:是)
+     */
+    private Integer subscribeFlag;
+
+    /**
+     * 节点类型
+     */
+    private Integer nodeType;
+
+    /**
+     * 分组id
+     */
+    private Integer groupId;
+
+    /**
+     * 删除标识
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createdTime;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updatedTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+
+    /**
+     * 单位编号
+     */
+    private String companyCode;
+
+    /**
+     * 安装位置
+     */
+    private String installAddress;
+
+    /**
+     * 业务状态;1:未激活,2:已激活,3:禁用
+     */
+    private Integer serviceStatus;
+
+    /**
+     * 产品编码
+     */
+    private String productCode;
+
+    /**
+     * 设备UUID
+     */
+    private String deviceUuid;
+
+
+}

+ 141 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/DmpProduct.java

@@ -0,0 +1,141 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 产品信息表
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-05
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DmpProduct implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 产品名称
+     */
+    private String productName;
+
+    /**
+     * 接入方式
+     */
+    private Integer accessMode;
+
+    /**
+     * 网络类型
+     */
+    private Integer networkType;
+
+    /**
+     * 设备类型
+     */
+    private Integer deviceType;
+
+    /**
+     * 通信协议
+     */
+    private Integer comProtocol;
+
+    /**
+     * 认证方式
+     */
+    private String authMode;
+
+    /**
+     * 设备型号
+     */
+    private String deviceModel;
+
+    /**
+     * 产品描述
+     */
+    private String productDescribe;
+
+    /**
+     * 厂家名称
+     */
+    private String factoryName;
+
+    /**
+     * 厂家联系人
+     */
+    private String factoryPerson;
+
+    /**
+     * 厂家联系电话
+     */
+    private String factoryPhone;
+
+    /**
+     * 资质证书1
+     */
+    private String certificateUrl1;
+
+    /**
+     * 资质证书2
+     */
+    private String certificateUrl2;
+
+    /**
+     * 资质证书3
+     */
+    private String certificateUrl3;
+
+    /**
+     * 协议文档
+     */
+    private String agreementUrl;
+
+    /**
+     * 删除标识
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createdTime;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updatedTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+
+    /**
+     * 产品编码
+     */
+    private String productCode;
+
+
+}

+ 136 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/domain/SysUser.java

@@ -0,0 +1,136 @@
+package com.usky.cdi.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 用户信息表
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-04
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class SysUser implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    @TableId(value = "user_id", type = IdType.AUTO)
+    private Long userId;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 用户账号
+     */
+    private String userName;
+
+    /**
+     * 用户昵称
+     */
+    private String nickName;
+
+    /**
+     * 用户类型(00系统用户, 01 租户管理员)
+     */
+    private String userType;
+
+    /**
+     * 用户邮箱
+     */
+    private String email;
+
+    /**
+     * 手机号码
+     */
+    private String phonenumber;
+
+    /**
+     * 用户性别(0男 1女 2未知)
+     */
+    private String sex;
+
+    /**
+     * 姓名
+     */
+    private String fullName;
+
+    /**
+     * 头像地址
+     */
+    private String avatar;
+
+    /**
+     * 密码
+     */
+    private String password;
+
+    /**
+     * 帐号状态(0正常 1停用)
+     */
+    private String status;
+
+    /**
+     * 删除标志(0代表存在 2代表删除)
+     */
+    private String delFlag;
+
+    /**
+     * 最后登录IP
+     */
+    private String loginIp;
+
+    /**
+     * 最后登录时间
+     */
+    private LocalDateTime loginDate;
+
+    /**
+     * 创建者
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+    /**
+     * 地址
+     */
+    private String address;
+
+
+}

+ 18 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseAlarmMapper.java

@@ -0,0 +1,18 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.BaseAlarm;
+import com.usky.common.mybatis.core.CrudMapper;
+import org.springframework.stereotype.Repository;
+
+/**
+ * <p>
+ * 统一告警表 Mapper 接口
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-29
+ */
+@Repository
+public interface BaseAlarmMapper extends CrudMapper<BaseAlarm> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseBuildFacilityMapper.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.BaseBuildFacility;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 建筑设施 Mapper 接口
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-02
+ */
+public interface BaseBuildFacilityMapper extends CrudMapper<BaseBuildFacility> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseBuildMapper.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.BaseBuild;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 建筑信息 Mapper 接口
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+public interface BaseBuildMapper extends CrudMapper<BaseBuild> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseBuildPlaneMapper.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.BaseBuildPlane;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 建筑楼层平面图 Mapper 接口
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+public interface BaseBuildPlaneMapper extends CrudMapper<BaseBuildPlane> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/BaseBuildUnitMapper.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.BaseBuildUnit;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ *  Mapper 接口
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+public interface BaseBuildUnitMapper extends CrudMapper<BaseBuildUnit> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/CdiDefenseProjectMapper.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.CdiDefenseProject;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 人防_工程表(人防工程基础信息及MQTT连接配置) Mapper 接口
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+public interface CdiDefenseProjectMapper extends CrudMapper<CdiDefenseProject> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/CdiDeliveryLogMapper.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.CdiDeliveryLog;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 人防_投递日志表(记录数据的MQTT投递情况) Mapper 接口
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+public interface CdiDeliveryLogMapper extends CrudMapper<CdiDeliveryLog> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/DmpDeviceMapper.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.DmpDevice;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 设备信息表 Mapper 接口
+ * </p>
+ *
+ * @author han
+ * @since 2023-09-22
+ */
+public interface DmpDeviceMapper extends CrudMapper<DmpDevice> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/DmpProductMapper.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.DmpProduct;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 产品信息表 Mapper 接口
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-05
+ */
+public interface DmpProductMapper extends CrudMapper<DmpProduct> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/mapper/SysUserMapper.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.mapper;
+
+import com.usky.cdi.domain.SysUser;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 用户信息表 Mapper 接口
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-04
+ */
+public interface SysUserMapper extends CrudMapper<SysUser> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseAlarmService.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.service;
+
+import com.usky.cdi.domain.BaseAlarm;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 统一告警表 服务类
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-29
+ */
+public interface BaseAlarmService extends CrudService<BaseAlarm> {
+
+}

+ 18 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseBuildFacilityService.java

@@ -0,0 +1,18 @@
+package com.usky.cdi.service;
+
+import com.usky.cdi.domain.BaseBuildFacility;
+import com.usky.common.mybatis.core.CrudService;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 建筑设施 服务类
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-02
+ */
+public interface BaseBuildFacilityService extends CrudService<BaseBuildFacility> {
+    List<BaseBuildFacility> facilityInfo(Integer tenantId);
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseBuildPlaneService.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.service;
+
+import com.usky.cdi.domain.BaseBuildPlane;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 建筑楼层平面图 服务类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+public interface BaseBuildPlaneService extends CrudService<BaseBuildPlane> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseBuildService.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.service;
+
+import com.usky.cdi.domain.BaseBuild;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 建筑信息 服务类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+public interface BaseBuildService extends CrudService<BaseBuild> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/BaseBuildUnitService.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.service;
+
+import com.usky.cdi.domain.BaseBuildUnit;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ *  服务类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+public interface BaseBuildUnitService extends CrudService<BaseBuildUnit> {
+
+}

+ 20 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/CdiDefenseProjectService.java

@@ -0,0 +1,20 @@
+package com.usky.cdi.service;
+
+import com.usky.cdi.domain.CdiDefenseProject;
+import com.usky.common.mybatis.core.CrudService;
+
+
+/**
+ * <p>
+ * 人防_工程表(人防工程基础信息及MQTT连接配置) 服务类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+public interface CdiDefenseProjectService extends CrudService<CdiDefenseProject> {
+
+    boolean tryConnectAndPublish(String broker, String user, String pass, String ping, String clientId);
+
+    CdiDefenseProject listByTenantId(Integer tenantId);
+}

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

@@ -0,0 +1,31 @@
+package com.usky.cdi.service;
+
+import com.usky.cdi.domain.CdiDeliveryLog;
+import com.usky.cdi.service.vo.SyncTaskStatisticsVO;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.mybatis.core.CrudService;
+import org.springframework.scheduling.annotation.Async;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * <p>
+ * 人防_投递日志表(记录数据的MQTT投递情况) 服务类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+public interface CdiDeliveryLogService extends CrudService<CdiDeliveryLog> {
+
+    List<SyncTaskStatisticsVO> selectById(Long id);
+
+    CommonPage<CdiDeliveryLog> logList(Long id, Integer pageNum, Integer pageSize, Integer dataType, Integer logType, String startTime, String endTime);
+
+    void synchronizeData(SyncTaskStatisticsVO vo);
+
+    // 存储日志
+    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, String userName);
+}

+ 25 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/DmpDeviceInfoService.java

@@ -0,0 +1,25 @@
+package com.usky.cdi.service;
+
+import com.usky.backend.domain.*;
+import com.usky.cdi.domain.BaseBuildFacility;
+import com.usky.cdi.domain.DmpDevice;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.mybatis.core.CrudService;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * <p>
+ * 设备信息表
+ * </p>
+ *
+ * @author ya
+ * @since 2022-10-08
+ */
+public interface DmpDeviceInfoService extends CrudService<DmpDevice> {
+    List<DmpDevice> deviceInfo(Integer tenantId);
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/DmpProductService.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.service;
+
+import com.usky.cdi.domain.DmpProduct;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 产品信息表 服务类
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-05
+ */
+public interface DmpProductService extends CrudService<DmpProduct> {
+
+}

+ 16 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/SysUserService.java

@@ -0,0 +1,16 @@
+package com.usky.cdi.service;
+
+import com.usky.cdi.domain.SysUser;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 用户信息表 服务类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-04
+ */
+public interface SysUserService extends CrudService<SysUser> {
+
+}

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

@@ -0,0 +1,67 @@
+package com.usky.cdi.service.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 设备字段映射配置类
+ * 替代配置文件中的device.field-mapping配置
+ * @author fyc
+ * @date 2025/12/20
+ */
+@Configuration
+public class DeviceFieldConfig {
+
+    /**
+     * 设备类型与字段映射配置
+     * @return 设备字段映射Map
+     */
+    @Bean
+    public Map<String, String> deviceFieldMapping() {
+        Map<String, String> fieldMapping = new HashMap<>();
+
+        // 水浸传感器(702)
+        fieldMapping.put("702", "leach_status");
+
+        // 人员统计(703)
+        fieldMapping.put("703", "amount_into,amount_out,day_into,day_out");
+
+        // 电气火灾(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");
+
+        // 湿度传感器(708)
+        fieldMapping.put("708", "sd");
+
+        // 氧气传感器(709)
+        fieldMapping.put("709", "o2");
+
+        // 二氧化碳传感器(710)
+        fieldMapping.put("710", "co2");
+
+        // 一氧化碳传感器(711)
+        fieldMapping.put("711", "co");
+
+        // // 倾斜传感器
+        // fieldMapping.put("712", "qx");
+        //
+        // // 裂缝传感器
+        // fieldMapping.put("713", "cd");
+
+        // 位移传感器
+        fieldMapping.put("714", "wy");
+
+        // 液位
+        fieldMapping.put("716", "sensorValue");
+
+        return fieldMapping;
+    }
+}

+ 44 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttBaseConfig.java

@@ -0,0 +1,44 @@
+package com.usky.cdi.service.config.mqtt;
+
+import lombok.Data;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
+import org.springframework.stereotype.Component;
+
+@Data
+public class MqttBaseConfig {
+
+    private String username;
+
+    private String password;
+
+    private String hostUrl;
+
+    private String msgTopic;
+
+    //心跳间隔
+    private int keepAliveInterval;
+    
+    //完成超时
+    private int completionTimeout;
+
+
+    public MqttPahoClientFactory mqttClientFactory() {
+        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+        MqttConnectOptions options = new MqttConnectOptions();
+        options.setServerURIs(new String[]{this.getHostUrl()});
+        options.setUserName(this.getUsername());
+        if (this.getPassword() != null) {
+            options.setPassword(this.getPassword().toCharArray());
+        }
+        options.setKeepAliveInterval(this.getKeepAliveInterval());
+        factory.setConnectionOptions(options);
+        return factory;
+    }
+
+}

+ 47 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttInConfig.java

@@ -0,0 +1,47 @@
+package com.usky.cdi.service.config.mqtt;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.integration.channel.DirectChannel;
+import org.springframework.integration.core.MessageProducer;
+import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
+import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
+import org.springframework.messaging.MessageChannel;
+
+/**
+ * @author han
+ * @date 2025/03/20 14:30
+ */
+public class MqttInConfig {
+    public MqttBaseConfig mqttBaseConfig;
+
+    public static final String CHANNEL_NAME_INPUT = "mqttInputChannel";
+
+    public MessageChannel mqttInputChannel() {
+        return new DirectChannel();
+    }
+
+
+    /**
+     * 消息订阅绑定-消费者
+     *
+     * @return
+     */
+    public MessageProducer inbound() {
+        String msgTopic = mqttBaseConfig.getMsgTopic();
+        if (msgTopic == null || msgTopic.trim().isEmpty()) {
+            throw new IllegalStateException("MQTT订阅主题配置不能为空");
+        }
+        String[] tops = msgTopic.split(",");
+        String clientId = "h-agbox-mqtt-in-" + System.currentTimeMillis();
+        MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(clientId,
+                mqttBaseConfig.mqttClientFactory(), tops);
+        adapter.setCompletionTimeout(mqttBaseConfig.getCompletionTimeout());
+        adapter.setConverter(new DefaultPahoMessageConverter());
+        adapter.setQos(2);
+        adapter.setOutputChannel(mqttInputChannel());
+        return adapter;
+    }
+}

+ 123 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/config/mqtt/MqttOutConfig.java

@@ -0,0 +1,123 @@
+package com.usky.cdi.service.config.mqtt;
+
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.springframework.context.annotation.Bean;
+import org.springframework.integration.annotation.MessagingGateway;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.integration.channel.DirectChannel;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.MessageHandler;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * @author han
+ * @date 2025/03/20 14:31
+ */
+@Component
+public class MqttOutConfig {
+    public MqttBaseConfig mqttBaseConfig;
+
+    public static final String CHANNEL_NAME_OUT = "mqttOutboundChannel";
+
+    public static final String MESSAGE_NAME = "messageOut";
+
+    public static final String DEFAULT_TOPIC = "testTopic";
+
+    /**
+     * 连接通道
+     *
+     * @return
+     */
+    @Bean(name = CHANNEL_NAME_OUT)
+    public MessageChannel mqttOutboundChannel() {
+        return new DirectChannel();
+    }
+
+
+    /**
+     * MQTT消息发送处理器
+     * 注意:这个方法会被Spring自动创建,用于处理mqttOutboundChannel通道上的消息
+     *
+     * @param factory MQTT客户端工厂
+     * @return MessageHandler实例
+     */
+    @Bean(name = MESSAGE_NAME)
+    @ServiceActivator(inputChannel = CHANNEL_NAME_OUT)
+    public MessageHandler outbound(DefaultMqttPahoClientFactory factory) {
+        // 注意:这里的client-id暂时使用固定值,因为username在启动时还不可用
+        // 实际使用时,会在createMqttConnection方法中重新设置
+        String clientId = "mqttx-" + System.currentTimeMillis();
+        MqttPahoMessageHandler messageHandler =
+                new MqttPahoMessageHandler(clientId, factory);
+        // 如果设置成true,发送消息时将不会阻塞。
+        messageHandler.setAsync(true);
+        messageHandler.setDefaultTopic(DEFAULT_TOPIC);
+        return messageHandler;
+    }
+
+    /**
+     * MQTT客户端工厂
+     * 注意:这个方法会被Spring自动创建,用于创建MQTT客户端
+     *
+     * @return DefaultMqttPahoClientFactory实例
+     */
+    @Bean
+    public DefaultMqttPahoClientFactory mqttClientFactory() {
+        // 创建默认的MqttPahoClientFactory
+        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
+
+        // 设置默认的MqttConnectOptions,确保serverURIs不为null
+        // 使用时,会在createMqttConnection方法中重新配置
+        MqttConnectOptions options = new MqttConnectOptions();
+        // 设置默认的服务器地址
+        options.setServerURIs(new String[]{"ssl://114.80.201.143:8883"});
+        // 设置默认的心跳间隔
+        options.setKeepAliveInterval(60);
+        factory.setConnectionOptions(options);
+
+        return factory;
+    }
+
+    // 注意:这个接口需要被Spring扫描到,所以我们保留@MessagingGateway注解
+    // Spring会自动创建这个接口的实现类
+    @MessagingGateway(defaultRequestChannel = CHANNEL_NAME_OUT)
+    public interface MqttGateway {
+        /**
+         * 发送消息
+         *
+         * @param payload
+         */
+        void sendToMqtt(String payload);
+
+        /**
+         * 指定top发送消息
+         *
+         * @param topic
+         * @param payload
+         */
+        void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload);
+
+        /**
+         * 指定top发送消息
+         *
+         * @param topic
+         * @param payload
+         */
+        void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, Map<String, Object> payload);
+
+        /**
+         * 指定队列和qos
+         *
+         * @param topic
+         * @param qos
+         * @param payload
+         */
+        void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
+    }
+}

+ 193 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/AlarmType.java

@@ -0,0 +1,193 @@
+package com.usky.cdi.service.enums;
+
+/**
+ * 告警类型枚举类
+ * 统一管理告警类型定义、分类和转换逻辑
+ *
+ * @author fu
+ * @since 2025-12-29
+ */
+public enum AlarmType {
+
+    // 水浸告警
+    WATER("701", "10.1", null, "有水"),
+
+    // 空气温度偏高告警
+    AIR_TEMP("702", "12.1", Category.THRESHOLD, "空气温度偏高"),
+
+    // 空气湿度偏高告警
+    AIR_HUMIDITY("703", "13.1", Category.THRESHOLD, "空气湿度偏高"),
+
+    // 氧气浓度偏低告警
+    OXYGEN_LOW("704", "14.1", Category.THRESHOLD, "氧气浓度偏低"),
+
+    // 二氧化碳浓度偏高告警
+    CO2_HIGH("705", "15.1", Category.THRESHOLD, "二氧化碳浓度偏高"),
+
+    // 一氧化碳浓度偏高告警
+    CO_HIGH("706", "16.1", Category.THRESHOLD, "一氧化碳浓度偏高"),
+
+    // 剩余电流偏大告警
+    RESIDUAL_CURRENT("707", "29.1", Category.THRESHOLD_LINE, "剩余电流偏大"),
+
+    // 供电电缆温度偏高告警
+    CABLE_TEMP("708", "29.2", Category.THRESHOLD_LINE, "供电电缆温度偏高"),
+
+    // 电流偏大
+    CURRENT_HIGH("709", "29.3", Category.THRESHOLD_LINE, "电流偏大"),
+
+    // 人员闯入告警
+    PERSON_INTRUSION("710", "33.1", Category.OTHER, "人员闯入告警"),
+
+    // 监测位置有位移告警
+    DISPLACEMENT("711", "37.1", Category.STRUCTURE_MONITORING, "有位移"),
+
+    // 监测位置有倾斜告警
+    TILT("712", "34.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;
+    private final Category category;
+    private final String description;
+
+    AlarmType(String originalCode, String mappedCode, Category category, String description) {
+        this.originalCode = originalCode;
+        this.mappedCode = mappedCode;
+        this.category = category;
+        this.description = description;
+    }
+
+    /**
+     * 根据原始告警类型代码获取对应的枚举值
+     */
+    public static AlarmType fromOriginalCode(String originalCode) {
+        for (AlarmType alarmType : values()) {
+            if (alarmType.getOriginalCode().equals(originalCode)) {
+                return alarmType;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 判断是否为结构监测告警类型(需要unitName映射)
+     */
+    public boolean isStructureMonitoringType() {
+        return category == Category.STRUCTURE_MONITORING;
+    }
+
+    /**
+     * 判断是否为阈值相关告警类型
+     */
+    public boolean isThresholdAlarmType() {
+        return category == Category.THRESHOLD || category == Category.THRESHOLD_LINE;
+    }
+
+    /**
+     * 判断是否为线路编号相关告警类型
+     */
+    public boolean isLineNoAlarmType() {
+        return category == Category.THRESHOLD_LINE;
+    }
+
+    /**
+     * 获取默认unitName值
+     */
+    public String getDefaultUnitName() {
+        // switch (this) {
+        //     case DISPLACEMENT:
+        //         return "监测位置";
+        //     case TILT:
+        //         return "倾斜监测点";
+        //     case CRACK:
+        //         return "裂缝检测点";
+        //     default:
+        return "防化单元";
+        // }
+    }
+
+    /**
+     * 获取默认阈值
+     */
+    public Float getDefaultThreshold() {
+        switch (this) {
+            case AIR_TEMP:
+                return 40.0f; // 35°C
+            case AIR_HUMIDITY:
+                return 95.0f; // 80%
+            case OXYGEN_LOW:
+                return 19.5f; // 19.5%
+            case CO2_HIGH:
+                return 1000.0f; // 0.5%
+            case CO_HIGH:
+                return 5.0f; // 10ppm
+            case RESIDUAL_CURRENT:
+                return 10.0f; // 300mA
+            case CABLE_TEMP:
+                return 55.0f; // 70°C
+            case CURRENT_HIGH:
+                return 10000.0f; // 10000mA
+            default:
+                return 0.0f;
+        }
+    }
+
+    /**
+     * 验证阈值是否在合理范围内
+     */
+    public boolean isValidThreshold(float value) {
+        switch (this) {
+            case AIR_TEMP:
+                return value >= -20 && value <= 80; // 温度范围 -50°C 到 100°C
+            case AIR_HUMIDITY:
+                return value >= 0 && value <= 100; // 湿度范围 0% 到 100%
+            case OXYGEN_LOW:
+                return value >= 0 && value <= 25; // 氧气浓度范围 0% 到 25%
+            case CO2_HIGH:
+                return value >= 0 && value <= 5; // 二氧化碳浓度范围 0% 到 10%
+            case CO_HIGH:
+                return value >= 0 && value <= 2000; // 一氧化碳浓度范围 0 到 1000ppm
+            case RESIDUAL_CURRENT:
+                return value >= 0 && value <= 500000; // 剩余电流范围 0 到 5000mA
+            case CABLE_TEMP:
+                return value >= -50 && value <= 200; // 电缆温度范围 -50°C 到 200°C
+            case CURRENT_HIGH:
+                return value >= 0 && value <= 100000; // 电流范围 0 到 1000A
+            default:
+                return true; // 其他类型不进行验证
+        }
+    }
+
+    /**
+     * 告警分类枚举
+     */
+    public enum Category {
+        STRUCTURE_MONITORING,    // 结构监测类型(需要unitName映射)
+        THRESHOLD,              // 阈值告警类型(需要thresholding)
+        THRESHOLD_LINE,         // 阈值+线路编号类型(需要thresholding和lineNo)
+        OTHER                   // 其他类型
+    }
+
+    // Getters
+    public String getOriginalCode() {
+        return originalCode;
+    }
+
+    public String getMappedCode() {
+        return mappedCode;
+    }
+
+    public Category getCategory() {
+        return category;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

+ 103 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/DeliveryDataType.java

@@ -0,0 +1,103 @@
+package com.usky.cdi.service.enums;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/2/5
+ */
+
+/**
+ * 人防投递日志-数据类型枚举(对应cdi_delivery_log表的data_type字段)
+ */
+public enum DeliveryDataType {
+
+    /**
+     * 防护单元基础信息
+     */
+    BASE_DATA(1, "防护单元基础信息"),
+
+    /**
+     * 楼层平面图信息
+     */
+    IOT_MONITOR_DATA(2, "楼层平面图信息"),
+
+    /**
+     * 智能监管物联设施信息
+     */
+    HIDDEN_DANGER(3, "智能监管物联设施信息");
+
+
+
+
+    /**
+     * 数据库存储的编码(对应data_type字段值)
+     */
+    private final Integer code;
+
+    /**
+     * 编码对应的业务描述
+     */
+    private final String desc;
+
+    // 构造方法(枚举类构造方法默认私有,无需显式写private)
+    DeliveryDataType(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+    // ========== 基础getter方法 ==========
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public static Map<Integer, String> getAllDesc() {
+        return Arrays.stream(values())
+                .collect(java.util.stream.Collectors.toMap(DeliveryDataType::getCode, DeliveryDataType::getDesc));
+    }
+
+    // ========== 核心工具方法(日常开发高频使用) ==========
+
+    /**
+     * 根据编码获取对应的枚举对象(推荐使用,空值/无效编码返回Optional.empty(),避免空指针)
+     * @param code 数据类型编码(1/2/3)
+     * @return 对应枚举的Optional对象
+     */
+    public static Optional<DeliveryDataType> getByCode(Integer code) {
+        // 空值直接返回空,避免遍历判断
+        if (code == null) {
+            return Optional.empty();
+        }
+        // 遍历枚举数组匹配编码
+        return Arrays.stream(values())
+                .filter(enumObj -> enumObj.getCode().equals(code))
+                .findFirst();
+    }
+
+    /**
+     * 根据编码获取描述(简化版,无匹配返回空字符串,适合接口返回/日志打印)
+     * @param code 数据类型编码(1/2/3)
+     * @return 编码对应的描述,无匹配返回""
+     */
+    public static String getDescByCode(Integer code) {
+        return getByCode(code)
+                .map(DeliveryDataType::getDesc)
+                .orElse("");
+    }
+
+    /**
+     * 判断编码是否为有效的数据类型编码(适合入参校验/数据校验)
+     * @param code 数据类型编码
+     * @return true-有效,false-无效/空值
+     */
+    public static boolean isValidCode(Integer code) {
+        return getByCode(code).isPresent();
+    }
+}

+ 67 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/EnvMonitorMqttTopic.java

@@ -0,0 +1,67 @@
+package com.usky.cdi.service.enums;
+
+import lombok.Data;
+
+/**
+ * 环境监测MQTT Topic枚举(统一管理)
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2025/11/20
+ */
+public enum EnvMonitorMqttTopic {
+    // 水浸状态
+    WATER_LEAK("iotInfo/flooded"),
+
+    // 温度
+    TEMP("iotInfo/temp"),
+
+    // 湿度
+    HUMIDITY("iotInfo/rh"),
+
+    // 氧气
+    OXYGEN("iotInfo/o2"),
+
+    // 二氧化碳
+    CO2("iotInfo/co2"),
+
+    // 一氧化碳
+    CO("iotInfo/co"),
+
+    // 人员闯入情况
+    PERSON_PRESENCE("iotInfo/personPresence"),
+
+    // 用电负荷(后续用电负荷用)
+    ELECTRICITY_LOAD("iotInfo/electricityLoad"),
+
+    // 倾斜
+    TILT("iotInfo/tilt"),
+
+    // 裂缝
+    CRACK("iotInfo/crack"),
+
+    // 位移
+    DEVIATION("iotInfo/deviation"),
+
+    // 工程信息
+    ENGINEERING("base/engineering"),
+
+    // 防化单元
+    PROTECTIVE_UNIT("base/protectiveUnit"),
+
+    // 平面图
+    FLOOR_PLANE("base/floorPlane"),
+
+    // 物联设施
+    SENSOR_INFO("base/sensorInfo");
+
+
+    private final String topic;
+
+    EnvMonitorMqttTopic(String topic) {
+        this.topic = topic;
+    }
+
+    public String getTopic() {
+        return topic;
+    }
+}

+ 157 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/enums/MqttTopics.java

@@ -0,0 +1,157 @@
+package com.usky.cdi.service.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/2/3
+ */
+public final class MqttTopics {
+
+    /* ========================== 1. base ========================== */
+    @Getter
+    @RequiredArgsConstructor
+    public enum Base {
+        ENGINEERING("base/engineering", "人防工程基础信息"),
+        PROTECTIVE_UNIT("base/protectiveUnit", "防护单元基础信息"),
+        FLOOR_PLANE("base/floorPlane", "楼层平面图信息"),
+        SENSOR_INFO("base/sensorInfo", "智能监管物联设施数据");
+
+        private final String topic;
+        private final String desc;
+    }
+
+    /* ========================== 2. iotInfo ========================== */
+    @Getter
+    @RequiredArgsConstructor
+    public enum IotInfo {
+        MONITORING_DATA("iotInfo", "物联监测数据"),
+        CIVIL_DEFENSE_DOOR("iotInfo/civilDefenseDoor", "人防门姿态"),
+        WATER_TANK_CAPACITY("iotInfo/waterTankCapacity", "战时水箱水量"),
+        PIPELINE_VALVE("iotInfo/pipelineValve", "给排水管道阀门启闭状态"),
+        VENTILATION_PIPELINE_VALVE("iotInfo/ventilationPipelineValve", "战时通风管道密闭阀启闭状态"),
+        OVER_PRESSURE_VALVE("iotInfo/overPressureValve", "超压排气活门启闭状态"),
+        MICRO_PRESSURE_DIFFERENCE("iotInfo/microPressureDifference", "微压差"),
+        BLOWER_STARTUP_TIME("iotInfo/blowerStartupTime", "战时风机每日运行时长"),
+        VENTILATION_RUN_STATUS("iotInfo/ventilationRunStatus", "战时通风系统运行工况"),
+        ALTERNATOR_STARTUP_TIME("iotInfo/alternatorStartupTime", "战时柴油发电机每日启动时长"),
+        ALTERNATOR_STATUS("iotInfo/alternatorStatus", "战时柴油发电机实时工况"),
+        DIESEL_CAPACITY("iotInfo/dieselCapacity", "战时储油容量"),
+        PERSON("iotInfo/person", "工程内掩蔽人数"),
+        SIGNAL_STRENGTH("iotInfo/sig", "工程外移动通信信号强度"),
+        MATERIAL("war/material", "工程战备物资储备情况"),
+        SEWAGE_LEVEL("iotInfo/sewageLevel", "集水井水位"),
+        H2S("iotInfo/h2S", "硫化氢浓度"),
+        NH3("iotInfo/nh3", "氨浓度"),
+        HCHO("iotInfo/hcho", "甲醛浓度"),
+        C6H6("iotInfo/c6h6", "苯浓度"),
+        RADIATION("iotInfo/radiation", "空气放射性物质含量"),
+        RN("iotInfo/rn", "氡活度浓度"),
+        CCL2F2("iotInfo/ccl2f2", "二氯二氟甲烷浓度"),
+        TVOC("iotInfo/tvoc", "总挥发性有机化合物浓度"),
+        PM("iotInfo/pm", "可吸入颗粒物浓度"),
+        NO2("iotInfo/no2", "二氧化氮浓度"),
+        SO2("iotInfo/so2", "二氧化硫浓度"),
+        BACTERIAL("iotInfo/bacterial", "细菌总数"),
+        TILT("iotInfo/tilt", "倾斜"),
+        CRACK("iotInfo/crack", "裂缝"),
+        DEVIATION("iotInfo/deviation", "位移"),
+        // 水浸状态
+        WATER_LEAK("iotInfo/flooded", "水浸状态"),
+        // 温度
+        TEMP("iotInfo/temp", "空气温度"),
+        // 湿度
+        HUMIDITY("iotInfo/rh", "空气湿度"),
+        // 氧气
+        OXYGEN("iotInfo/o2", "氧气浓度"),
+        // 二氧化碳
+        CO2("iotInfo/co2", "二氧化碳浓度"),
+        // 一氧化碳
+        CO("iotInfo/co", "一氧化碳浓度"),
+        // 人员闯入情况
+        PERSON_PRESENCE("iotInfo/personPresence", "人员闯入情况"),
+        // 用电负荷
+        ELECTRICITY_LOAD("iotInfo/electricityLoad", "人防用电负荷");
+
+        private final String topic;
+        private final String desc;
+    }
+
+    /* ========================== 3. alarm ========================== */
+    @Getter
+    @RequiredArgsConstructor
+    public enum Alarm {
+        // 通用告警事件(人防门、超压、滤尘器、油库、水浸、环境气体、电气、结构、人员、火险等全部走此 Topic)
+        MESSAGE("alarm/message", "告警信息");
+
+        private final String topic;
+        private final String desc;
+    }
+
+
+    /* =============== 工具方法:根据字符串 topic 反查枚举 =============== */
+    public static Base baseOf(String topic) {
+        for (Base b : Base.values()) {
+            if (b.topic.equals(topic)) return b;
+        }
+        return null;
+    }
+
+    public static IotInfo iotInfoOf(String topic) {
+        for (IotInfo i : IotInfo.values()) {
+            if (i.topic.equals(topic)) return i;
+        }
+        return null;
+    }
+
+    public static Alarm alarmOf(String topic) {
+        for (Alarm a : Alarm.values()) {
+            if (a.topic.equals(topic)) return a;
+        }
+        return null;
+    }
+
+    public static String getDescByTopic(String topic) {
+        String desc = "";
+        Base base = baseOf(topic);
+        if (base != null) desc = base.getDesc();
+        IotInfo iotInfo = iotInfoOf(topic);
+        if (iotInfo != null) desc = iotInfo.getDesc();
+        Alarm alarm = alarmOf(topic);
+        if (alarm != null) desc = alarm.getDesc();
+        return desc;
+    }
+
+    private MqttTopics() {
+    }
+
+    public static IotInfo getIotInfoByTypeCode(Integer typeCode) {
+        switch (typeCode) {
+            case 707:
+                return IotInfo.TEMP;
+            case 708:
+                return IotInfo.HUMIDITY;
+            case 709:
+                return IotInfo.OXYGEN;
+            case 710:
+                return IotInfo.CO2;
+            case 711:
+                return IotInfo.CO;
+            case 703:
+                return IotInfo.PERSON_PRESENCE;
+            case 704:
+                return IotInfo.ELECTRICITY_LOAD;
+            case 702:
+                return IotInfo.WATER_LEAK;
+            case 714:
+                return IotInfo.DEVIATION;
+            case 719:
+                return IotInfo.PERSON;
+            default:
+                return IotInfo.MONITORING_DATA;
+        }
+    }
+}

+ 569 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataSyncService.java

@@ -0,0 +1,569 @@
+package com.usky.cdi.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.cdi.domain.BaseAlarm;
+import com.usky.cdi.domain.BaseBuildFacility;
+import com.usky.cdi.mapper.BaseAlarmMapper;
+import com.usky.cdi.mapper.BaseBuildFacilityMapper;
+import com.usky.cdi.service.CdiDeliveryLogService;
+import com.usky.cdi.service.enums.MqttTopics;
+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;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.StringRedisTemplate;
+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.*;
+import java.util.stream.Collectors;
+
+/**
+ * 多租户定时任务服务
+ *
+ * @author fu
+ * @since 2025-12-29
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AlarmDataSyncService {
+
+    @Value("${snowflake.worker-id:3}")
+    private long workerId;
+
+    @Value("${snowflake.data-center-id:3}")
+    private long dataCenterId;
+
+    private static final String PEACETIME = "peacetime";
+    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;
+    @Autowired
+    private BaseAlarmMapper baseAlarmMapper;
+    @Autowired
+    private BaseBuildFacilityMapper baseBuildFacilityMapper;
+    @Autowired
+    private CdiDeliveryLogService cdiDeliveryLogService;
+
+    private SnowflakeIdGenerator idGenerator;
+
+    @PostConstruct
+    public void init() {
+        this.idGenerator = new SnowflakeIdGenerator(workerId, dataCenterId);
+    }
+
+    /**
+     * 获取当前时间
+     */
+    private String getCurrentTime() {
+        return timeFormat.format(new Date());
+    }
+
+    /**
+     * 生成数据包ID
+     */
+    private Long generateDataPacketID() {
+        return idGenerator.nextPacketId();
+    }
+
+    @Async
+    public void synchronizeAlarmData(Integer tenantId, Long engineeringId, String username, String password, String status) {
+        log.info("租户:{}的人防告警数据推送定时任务开始执行,平战时状态:{}", tenantId, PEACETIME.equals(status) ? "平时" : "战时");
+
+        LocalDateTime now = LocalDateTime.now();
+        long startTime = System.currentTimeMillis();
+        long endTime;
+        log.info("开始时间:{}", getCurrentTime());
+
+        // 1.查询 base_alarm 表中的告警数据,筛选条件为 tenant_id = tenantId
+        LambdaQueryWrapper<BaseAlarm> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(BaseAlarm::getTenantId, tenantId)
+                .eq(BaseAlarm::getAlarmGrade, 1);
+        List<BaseAlarm> alarmList = baseAlarmMapper.selectList(queryWrapper);
+
+        if (alarmList.isEmpty()) {
+            log.warn("租户{}没有新的告警数据,任务结束", tenantId);
+            return;
+        }
+        int size = alarmList.size();
+        log.info("查询到租户{}的告警数据总数:{}", tenantId, size);
+
+        // 批量查询建筑设施数据,构建设备ID到设施的映射
+        Map<String, BaseBuildFacility> facilityMap = buildFacilityMap(alarmList, tenantId);
+
+        // 记录每一条告警数据
+        for (BaseAlarm alarm : alarmList) {
+            log.info("告警数据:ID={}, 设备ID={}, 告警类型={}, 告警时间={}, 处理状态={}, 是否误报={}",
+                    alarm.getId(), alarm.getDeviceId(), alarm.getAlarmType(),
+                    alarm.getAlarmTime(), alarm.getHandleStatus(), alarm.getAlarmFalse());
+        }
+
+        int successCount = 0;
+        int failureCount = 0;
+        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);
+
+            // 3.遍历告警数据,转换为AlarmMessageVO并发送
+            for (BaseAlarm alarm : alarmList) {
+                try {
+                    // 根据告警类型映射关系转换
+                    String alarmType = convertAlarmType(alarm.getAlarmType());
+                    if (alarmType == null) {
+                        log.warn("不支持的告警类型:{},跳过处理", alarm.getAlarmType());
+                        failureCount++;
+                        continue;
+                    }
+
+                    // 转换告警状态
+                    Integer alarmStatus = convertAlarmStatus(alarm.getHandleStatus(), alarm.getAlarmFalse(), status);
+
+                    // 构建AlarmMessageVO对象
+                    AlarmMessageVO<Integer> alarmMessageVO = new AlarmMessageVO<>();
+                    alarmMessageVO.setDataPacketID(generateDataPacketID());
+                    alarmMessageVO.setEngineeringID(engineeringId);
+                    alarmMessageVO.setAlarmID(alarm.getId());
+                    // 固定值1,人防工程级物联平台自动发现
+                    alarmMessageVO.setAlarmSource(1);
+                    alarmMessageVO.setSensorID(alarm.getDeviceId() != null ? Integer.valueOf(alarm.getDeviceId()) : null);
+                    alarmMessageVO.setAlarmType(alarmType);
+                    alarmMessageVO.setAlarmStatus(alarmStatus);
+
+                    // 设置告警时间:新增告警取 alarm_type 字段,更新数据则取 handle_time 字段
+                    String alarmTime = alarm.getHandleTime() != null ?
+                            alarm.getHandleTime().toString() : alarm.getAlarmTime().toString();
+                    alarmMessageVO.setAlarmUpdateTime(alarmTime);
+
+                    // 设置监测对象告警描述和编号
+                    alarmMessageVO.setAlarmDesc(alarm.getAlarmAttribute());
+                    alarmMessageVO.setMonitorObjNo(facilityMap.get(alarm.getDeviceId()).getFacilityNum());
+
+                    // 设置上报时间为当前时间
+                    alarmMessageVO.setPublishTime(getCurrentTime());
+
+                    // 设置告警数据字段
+                    alarmMessageVO.setSensorValue(alarm.getAlarmData());
+
+                    // 根据告警类型填充unitName、thresholding、lineNo字段
+                    populateAlarmFields(alarmMessageVO, alarm, tenantId, facilityMap);
+
+                    // 从Redis获取缓存的投递状态,检查是否有变更
+                    String deliveryKey = alarm.getDeviceId() + "_" + alarm.getId();
+                    String cachedRecord = (String) stringRedisTemplate.opsForHash().get(ALARM_DELIVERY_KEY_PREFIX + tenantId, deliveryKey);
+                    if (cachedRecord != null && !hasAlarmChanged(alarm, cachedRecord, status)) {
+                        log.info("告警数据无变化,跳过推送:设备ID={}, 告警ID={}", alarm.getDeviceId(), alarm.getId());
+                        continue;
+                    }
+
+                    // 4.将AlarmMessageVO转换为JSON字符串并发送MQTT消息
+                    String jsonMessage = JSON.toJSONString(alarmMessageVO);
+                    MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(username, password);
+                    gateway.sendToMqtt(topic, jsonMessage);
+
+                    successCount++;
+
+                    // 更新Redis中的投递记录
+                    saveDeliveryRecordToRedis(alarm, alarmStatus, status, tenantId);
+
+                    log.info("成功发送告警消息:设备ID={}, 告警类型={}, 消息内容={}",
+                            alarm.getDeviceId(), alarm.getAlarmType(), jsonMessage);
+
+                } catch (Exception e) {
+                    failureCount++;
+                    log.error("发送告警消息失败:设备ID={}, 告警类型={}, 错误信息={}",
+                            alarm.getDeviceId(), alarm.getAlarmType(), e.getMessage(), e);
+                }
+            }
+
+            // 6.打印统计信息
+            log.info("租户{}的告警数据推送任务完成", tenantId);
+            log.info("告警数据总数:{}, 成功推送:{}, 失败:{}", size, successCount, failureCount);
+            endTime = System.currentTimeMillis();
+
+            cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
+                    successCount, failureCount, size - successCount - failureCount, 1, userName);
+        } catch (Exception e) {
+            log.error("租户{}的告警数据推送定时任务执行失败:{}", tenantId, e.getMessage(), e);
+        } finally {
+            endTime = System.currentTimeMillis();
+            log.info("结束时间:{}, 耗时:{}ms", getCurrentTime(), endTime - startTime);
+
+            cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
+                    successCount, failureCount, size - successCount - failureCount, 0, userName);
+        }
+    }
+
+    /**
+     * 将base_alarm的alarm_type转换为AlarmMessageVO的alarmType
+     */
+    private String convertAlarmType(String alarmType) {
+        AlarmType type = AlarmType.fromOriginalCode(alarmType);
+        return type != null ? type.getMappedCode() : null;
+    }
+
+    /**
+     * 根据平时/战时状态转换告警状态
+     * 平时:handle_status=0则alarmStatus=1,handle_status=1则alarmStatus=4,alarm_false=1则alarmStatus=2
+     * 战时:handle_status=0则alarmStatus=1,handle_status=1则alarmStatus=3,alarm_false=1则alarmStatus=5
+     */
+    private Integer convertAlarmStatus(Integer handleStatus, Integer alarmFalse, String status) {
+        // 如果是误报,优先返回对应的误报状态
+        if (alarmFalse != null && alarmFalse == 1) {
+            // 平时误报=2,战时误报=5
+            return PEACETIME.equals(status) ? 2 : 5;
+        }
+
+        // 根据处理状态和平战时状态返回对应状态
+        if (handleStatus != null) {
+            if (handleStatus == 0) {
+                // 未处理/待处置
+                return 1;
+            } else if (handleStatus == 1) {
+                // 平时已办结=4,战时已处置=3
+                return PEACETIME.equals(status) ? 4 : 3;
+            }
+        }
+
+        // 默认返回未处理状态
+        return 1;
+    }
+
+    /**
+     * 批量查询建筑设施数据,构建设备ID到设施的映射
+     */
+    private Map<String, BaseBuildFacility> buildFacilityMap(List<BaseAlarm> alarmList, Integer tenantId) {
+        Map<String, BaseBuildFacility> facilityMap = new HashMap<>();
+
+        // 收集所有设备ID
+        List<String> deviceIds = alarmList.stream()
+                .map(BaseAlarm::getDeviceId)
+                .filter(id -> id != null && !id.trim().isEmpty())
+                .distinct()
+                .collect(Collectors.toList());
+
+        if (deviceIds.isEmpty()) {
+            return facilityMap;
+        }
+
+        try {
+            LambdaQueryWrapper<BaseBuildFacility> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(BaseBuildFacility::getDeviceId, deviceIds)
+                    .eq(BaseBuildFacility::getTenantId, tenantId)
+                    .eq(BaseBuildFacility::getDeleteFlag, 0);
+
+            List<BaseBuildFacility> facilities = baseBuildFacilityMapper.selectList(queryWrapper);
+
+            for (BaseBuildFacility facility : facilities) {
+                if (facility.getDeviceId() != null) {
+                    facilityMap.put(facility.getDeviceId(), facility);
+                }
+            }
+
+            log.info("批量查询建筑设施数据完成:设备ID数量={}, 匹配到设施数量={}", deviceIds.size(), facilityMap.size());
+        } catch (Exception e) {
+            log.warn("批量查询BaseBuildFacility失败:{}", e.getMessage());
+        }
+
+        return facilityMap;
+    }
+
+    /**
+     * 将投递记录保存到Redis Hash中
+     * 使用Hash结构:key=alarm:delivery:{tenantId}, field=deviceId_alarmId, value=JSON格式的投递状态
+     */
+    private void saveDeliveryRecordToRedis(BaseAlarm alarm, Integer currentAlarmStatus, String status, Integer tenantId) {
+        try {
+            String deliveryKey = ALARM_DELIVERY_KEY_PREFIX + tenantId;
+            String fieldKey = alarm.getDeviceId() + "_" + alarm.getId();
+
+            Map<String, String> recordMap = new HashMap<>();
+            recordMap.put("alarmStatus", String.valueOf(currentAlarmStatus));
+            recordMap.put("handleStatus", String.valueOf(alarm.getHandleStatus()));
+            recordMap.put("alarmFalse", String.valueOf(alarm.getAlarmFalse()));
+            recordMap.put("handleTime", alarm.getHandleTime() != null ? alarm.getHandleTime().toString() : "");
+            recordMap.put("handleBy", alarm.getHandleBy() != null ? alarm.getHandleBy() : "");
+            recordMap.put("deliveryTime", LocalDateTime.now().toString());
+
+            stringRedisTemplate.opsForHash().put(deliveryKey, fieldKey, JSON.toJSONString(recordMap));
+
+            log.debug("保存投递记录到Redis:key={}, field={}", deliveryKey, fieldKey);
+        } catch (Exception e) {
+            log.warn("保存投递记录到Redis失败:告警ID={}, 错误={}", alarm.getId(), e.getMessage());
+        }
+    }
+
+    /**
+     * 检查告警数据是否有变更
+     * 比较handleStatus、alarmFalse、handleTime、handleBy是否发生变化
+     */
+    private boolean hasAlarmChanged(BaseAlarm alarm, String cachedRecord, String status) {
+        try {
+            Map<String, String> cachedMap = JSON.parseObject(cachedRecord, Map.class);
+
+            Integer currentAlarmStatus = convertAlarmStatus(alarm.getHandleStatus(), alarm.getAlarmFalse(), status);
+            Integer cachedAlarmStatus = parseInteger(cachedMap.get("alarmStatus"));
+            Integer cachedHandleStatus = parseInteger(cachedMap.get("handleStatus"));
+            Integer cachedAlarmFalse = parseInteger(cachedMap.get("alarmFalse"));
+
+            if (!Objects.equals(currentAlarmStatus, cachedAlarmStatus)) {
+                return true;
+            }
+            if (!Objects.equals(alarm.getHandleStatus(), cachedHandleStatus)) {
+                return true;
+            }
+            if (!Objects.equals(alarm.getAlarmFalse(), cachedAlarmFalse)) {
+                return true;
+            }
+
+            String cachedHandleTime = cachedMap.get("handleTime");
+            String currentHandleTime = alarm.getHandleTime() != null ? alarm.getHandleTime().toString() : "";
+            if (!Objects.equals(currentHandleTime, cachedHandleTime)) {
+                return true;
+            }
+
+            String cachedHandleBy = cachedMap.get("handleBy");
+            String currentHandleBy = alarm.getHandleBy() != null ? alarm.getHandleBy() : "";
+            if (!Objects.equals(currentHandleBy, cachedHandleBy)) {
+                return true;
+            }
+
+            return false;
+        } catch (Exception e) {
+            log.warn("解析缓存的投递记录失败:告警ID={}, 错误={}", alarm.getId(), e.getMessage());
+            return true;
+        }
+    }
+
+    private Integer parseInteger(String value) {
+        if (value == null || value.trim().isEmpty()) {
+            return null;
+        }
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 根据告警类型填充unitName、thresholding、lineNo字段
+     */
+    private void populateAlarmFields(AlarmMessageVO<Integer> alarmMessageVO, BaseAlarm alarm, Integer tenantId,
+                                     Map<String, BaseBuildFacility> facilityMap) {
+        String originalAlarmType = alarm.getAlarmType();
+        AlarmType alarmTypeEnum = AlarmType.fromOriginalCode(originalAlarmType);
+
+        try {
+            // 根据AlarmMessageVO字段注释中的规则填充字段
+            if (alarmTypeEnum != null && alarmTypeEnum.isStructureMonitoringType()) {
+                // 倾斜/位移/裂缝告警类型 - 设置unitName
+                populateUnitName(alarmMessageVO, alarm, tenantId, facilityMap);
+            }
+            if (alarmTypeEnum != null && alarmTypeEnum.isThresholdAlarmType()) {
+                // 温湿度、气体浓度告警类型 - 设置thresholding
+                populateThresholding(alarmMessageVO, alarm);
+            }
+            if (alarmTypeEnum != null && alarmTypeEnum.isLineNoAlarmType()) {
+                // 电气相关告警类型 - 设置lineNo
+                populateLineNo(alarmMessageVO, alarm);
+            }
+        } catch (Exception e) {
+            log.warn("填充告警字段时发生异常:告警类型={}, 设备ID={}, 错误信息={}",
+                    originalAlarmType, alarm.getDeviceId(), e.getMessage());
+        }
+    }
+
+    /**
+     * 填充unitName字段(倾斜才用)
+     * 对于711、712、713类型,实现与BaseBuildFacility表中facilityDesc字段的映射
+     */
+    private void populateUnitName(AlarmMessageVO<Integer> alarmMessageVO, BaseAlarm alarm, Integer tenantId,
+                                  Map<String, BaseBuildFacility> facilityMap) {
+        String originalAlarmType = alarm.getAlarmType();
+        AlarmType alarmTypeEnum = AlarmType.fromOriginalCode(originalAlarmType);
+
+        if (alarmTypeEnum != null && alarmTypeEnum.isStructureMonitoringType()) {
+            // 从Map中获取建筑设施信息
+            BaseBuildFacility facility = facilityMap.get(alarm.getDeviceId());
+            if (facility != null && facility.getFacilityDesc() != null) {
+                alarmMessageVO.setUnitName(facility.getFacilityDesc());
+                log.debug("成功映射unitName:设备ID={}, facilityDesc={}", alarm.getDeviceId(), facility.getFacilityDesc());
+            } else {
+                // 如果没有找到对应的facilityDesc,设置默认值
+                String defaultUnitName = alarmTypeEnum.getDefaultUnitName();
+                alarmMessageVO.setUnitName(defaultUnitName);
+                log.debug("未找到facilityDesc,使用默认值:设备ID={}, 默认值={}", alarm.getDeviceId(), defaultUnitName);
+            }
+        } else {
+            // 其他情况,设置默认值
+            String defaultUnitName = alarmTypeEnum != null ? alarmTypeEnum.getDefaultUnitName() : "监测单元";
+            alarmMessageVO.setUnitName(defaultUnitName);
+        }
+    }
+
+    /**
+     * 填充thresholding字段(氧气、温湿度、一氧化碳、二氧化碳、剩余电流、供电电缆温度、电流偏大专用)
+     */
+    private void populateThresholding(AlarmMessageVO<Integer> alarmMessageVO, BaseAlarm alarm) {
+        // try {
+        //     // 从告警数据中解析阈值,如果解析失败则设置默认值
+        //     Float threshold = parseThresholdFromAlarmData(alarm.getAlarmData(), alarm.getAlarmType());
+        //     alarmMessageVO.setThresholding(threshold);
+        // } catch (Exception e) {
+        // 解析失败时设置合理的默认值
+        Float defaultThreshold = getDefaultThreshold(alarm.getAlarmType());
+        alarmMessageVO.setThresholding(defaultThreshold);
+        // log.warn("解析阈值失败,使用默认值:设备ID={}, 告警类型={}, 错误={}",
+        //         alarm.getDeviceId(), alarm.getAlarmType(), e.getMessage());
+        // }
+    }
+
+    /**
+     * 从告警数据中解析阈值
+     */
+    private Float parseThresholdFromAlarmData(String alarmData, String alarmType) {
+        if (alarmData == null || alarmData.trim().isEmpty()) {
+            return getDefaultThreshold(alarmType);
+        }
+
+        try {
+            // 尝试从告警数据中提取数值
+            String cleanedData = alarmData.replaceAll("[^\\d.]", "");
+            if (!cleanedData.isEmpty()) {
+                float parsedValue = Float.parseFloat(cleanedData);
+
+                // 添加边界检查,防止异常值
+                if (isValidThreshold(parsedValue, alarmType)) {
+                    return parsedValue;
+                } else {
+                    log.debug("阈值超出合理范围,使用默认值:alarmData={}, alarmType={}, value={}",
+                            alarmData, alarmType, parsedValue);
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.debug("无法从告警数据中解析数值:alarmData={}", alarmData);
+        }
+
+        return getDefaultThreshold(alarmType);
+    }
+
+    /**
+     * 验证阈值是否在合理范围内
+     */
+    private boolean isValidThreshold(float value, String alarmType) {
+        AlarmType type = AlarmType.fromOriginalCode(alarmType);
+        return type == null || type.isValidThreshold(value);
+    }
+
+    /**
+     * 获取thresholding的默认值
+     */
+    private Float getDefaultThreshold(String alarmType) {
+        AlarmType type = AlarmType.fromOriginalCode(alarmType);
+        return type != null ? type.getDefaultThreshold() : 0.0f;
+    }
+
+    /**
+     * 填充lineNo字段(剩余电流、供电电缆温度、电流偏大专用)
+     */
+    private void populateLineNo(AlarmMessageVO<Integer> alarmMessageVO, BaseAlarm alarm) {
+        // try {
+        //     // 从设备ID或告警数据中解析线路编号
+        //     Integer lineNo = parseLineNoFromDeviceOrData(alarm.getDeviceId(), alarm.getAlarmData());
+        //     alarmMessageVO.setLineNo(lineNo);
+        // } catch (Exception e) {
+        // 解析失败时设置默认值
+        alarmMessageVO.setLineNo(1); // 默认线路1
+        // log.warn("解析线路编号失败,使用默认值:设备ID={}, 错误={}", alarm.getDeviceId(), e.getMessage());
+        // }
+    }
+
+    /**
+     * 从设备ID或告警数据中解析线路编号
+     */
+    // private Integer parseLineNoFromDeviceOrData(String deviceId, String alarmData) {
+    //     // 首先尝试从设备ID中解析
+    //     if (deviceId != null && !deviceId.trim().isEmpty()) {
+    //         // 假设设备ID格式可能包含线路信息,例如:DEV001_L1, DEV002_L2等
+    //         if (deviceId.contains("_L")) {
+    //             try {
+    //                 String linePart = deviceId.substring(deviceId.lastIndexOf("_L") + 2);
+    //                 int lineNo = Integer.parseInt(linePart);
+    //                 if (isValidLineNo(lineNo)) {
+    //                     return lineNo;
+    //                 } else {
+    //                     log.debug("线路编号超出范围,使用默认值:deviceId={}, lineNo={}", deviceId, lineNo);
+    //                 }
+    //             } catch (NumberFormatException e) {
+    //                 log.debug("无法从设备ID中解析线路编号:deviceId={}", deviceId);
+    //             }
+    //         }
+    //
+    //         // 或者尝试从设备ID末尾提取数字作为线路编号
+    //         String numbers = deviceId.replaceAll("[^\\d]", "");
+    //         if (!numbers.isEmpty()) {
+    //             try {
+    //                 int lineNo = Integer.parseInt(numbers);
+    //                 if (isValidLineNo(lineNo)) {
+    //                     return lineNo;
+    //                 } else {
+    //                     log.debug("从设备ID提取的线路编号超出范围,使用默认值:deviceId={}, lineNo={}", deviceId, lineNo);
+    //                 }
+    //             } catch (NumberFormatException e) {
+    //                 log.debug("无法从设备ID中提取有效线路编号:deviceId={}", deviceId);
+    //             }
+    //         }
+    //     }
+    //
+    //     // 然后尝试从告警数据中解析
+    //     if (alarmData != null && !alarmData.trim().isEmpty()) {
+    //         String numbers = alarmData.replaceAll("[^\\d]", "");
+    //         if (!numbers.isEmpty()) {
+    //             try {
+    //                 int lineNo = Integer.parseInt(numbers);
+    //                 if (isValidLineNo(lineNo)) {
+    //                     return lineNo;
+    //                 } else {
+    //                     log.debug("从告警数据中解析的线路编号超出范围,使用默认值:alarmData={}, lineNo={}", alarmData, lineNo);
+    //                 }
+    //             } catch (NumberFormatException e) {
+    //                 log.debug("无法从告警数据中解析线路编号:alarmData={}", alarmData);
+    //             }
+    //         }
+    //     }
+    //
+    //     // 默认返回线路1
+    //     return 1;
+    // }
+
+    /**
+     * 验证线路编号是否在有效范围内
+     */
+    private boolean isValidLineNo(int lineNo) {
+        return lineNo >= 1 && lineNo <= 20; // 限制在1-20范围内,符合实际情况
+    }
+}

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

@@ -0,0 +1,75 @@
+package com.usky.cdi.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.usky.cdi.service.mqtt.MqttConnectionTool;
+import com.usky.cdi.service.util.SnowflakeIdGenerator;
+import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * 告警类数据传输服务:向市适配平台通过 MQTT 上报告警数据。
+ *
+ * @author han
+ * @date 2025/12/08
+ */
+@Slf4j
+@Service
+public class AlarmDataTransferService {
+
+    private static final String MQTT_USERNAME = "3101100021";
+    private static final String MQTT_PASSWORD = "SIixzph1";
+    private static final String ALARM_TOPIC = "alarm/message";
+    private static final DateTimeFormatter PUBLISH_TIME_FORMAT =
+            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
+
+    private final MqttConnectionTool mqttConnectionTool;
+    private final SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(3L, 3L);
+
+    public AlarmDataTransferService(MqttConnectionTool mqttConnectionTool) {
+        this.mqttConnectionTool = mqttConnectionTool;
+    }
+
+    private String getCurrentTime() {
+        return LocalDateTime.now().format(PUBLISH_TIME_FORMAT);
+    }
+
+    private long generateDataPacketId() {
+        return idGenerator.nextPacketId();
+    }
+
+    /**
+     * 上报告警消息至 MQTT(topic: alarm/message)。
+     *
+     * @return 是否已成功投递到底层网关(不代表敌方平台一定消费成功)
+     */
+    public boolean publishAlarm(AlarmMessageVO<?> vo) {
+        try {
+            fillDefaults(vo);
+            String json = JSON.toJSONString(vo);
+            log.info("告警 MQTT 载荷: {}", json);
+            MqttConnectionTool.MqttGateway gateway =
+                    mqttConnectionTool.connectOrRefresh(MQTT_USERNAME, MQTT_PASSWORD);
+            gateway.sendToMqtt(ALARM_TOPIC, json);
+            return true;
+        } catch (Exception e) {
+            log.error("发送告警信息失败, alarmID: {}, engineeringID: {}",
+                    vo != null ? vo.getAlarmID() : null,
+                    vo != null ? vo.getEngineeringID() : null,
+                    e);
+            return false;
+        }
+    }
+
+    private void fillDefaults(AlarmMessageVO<?> vo) {
+        if (vo.getDataPacketID() == null) {
+            vo.setDataPacketID(generateDataPacketId());
+        }
+        if (vo.getPublishTime() == null) {
+            vo.setPublishTime(getCurrentTime());
+        }
+    }
+}

+ 35 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseAlarmServiceImpl.java

@@ -0,0 +1,35 @@
+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.usky.cdi.domain.BaseAlarm;
+import com.usky.cdi.mapper.BaseAlarmMapper;
+import com.usky.cdi.service.BaseAlarmService;
+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.alarm.AlarmMessageVO;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.HashMap;
+
+/**
+ * <p>
+ * 统一告警表 服务实现类
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-29
+ */
+@Service
+public class BaseAlarmServiceImpl extends AbstractCrudService<BaseAlarmMapper, BaseAlarm> implements BaseAlarmService {
+
+}

+ 32 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseBuildFacilityServiceImpl.java

@@ -0,0 +1,32 @@
+package com.usky.cdi.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.usky.cdi.domain.BaseBuildFacility;
+import com.usky.cdi.mapper.BaseBuildFacilityMapper;
+import com.usky.cdi.service.BaseBuildFacilityService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.common.security.utils.SecurityUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 建筑设施 服务实现类
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-02
+ */
+@Service
+public class BaseBuildFacilityServiceImpl extends AbstractCrudService<BaseBuildFacilityMapper, BaseBuildFacility> implements BaseBuildFacilityService {
+    @Override
+    public List<BaseBuildFacility> facilityInfo(Integer tenantId){
+        LambdaQueryWrapper<BaseBuildFacility> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.eq(BaseBuildFacility::getDeleteFlag,0)
+                .eq(BaseBuildFacility::getTenantId, tenantId);
+        List<BaseBuildFacility> list = this.list(queryWrapper);
+        return list;
+    }
+}

+ 20 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseBuildPlaneServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.cdi.service.impl;
+
+import com.usky.cdi.domain.BaseBuildPlane;
+import com.usky.cdi.mapper.BaseBuildPlaneMapper;
+import com.usky.cdi.service.BaseBuildPlaneService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 建筑楼层平面图 服务实现类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+@Service
+public class BaseBuildPlaneServiceImpl extends AbstractCrudService<BaseBuildPlaneMapper, BaseBuildPlane> implements BaseBuildPlaneService {
+
+}

+ 20 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseBuildServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.cdi.service.impl;
+
+import com.usky.cdi.domain.BaseBuild;
+import com.usky.cdi.mapper.BaseBuildMapper;
+import com.usky.cdi.service.BaseBuildService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 建筑信息 服务实现类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+@Service
+public class BaseBuildServiceImpl extends AbstractCrudService<BaseBuildMapper, BaseBuild> implements BaseBuildService {
+
+}

+ 20 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseBuildUnitServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.cdi.service.impl;
+
+import com.usky.cdi.domain.BaseBuildUnit;
+import com.usky.cdi.mapper.BaseBuildUnitMapper;
+import com.usky.cdi.service.BaseBuildUnitService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ *  服务实现类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-05
+ */
+@Service
+public class BaseBuildUnitServiceImpl extends AbstractCrudService<BaseBuildUnitMapper, BaseBuildUnit> implements BaseBuildUnitService {
+
+}

+ 375 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java

@@ -0,0 +1,375 @@
+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.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;
+import com.usky.cdi.service.vo.info.FloorPlaneVO;
+import com.usky.cdi.service.vo.info.ProtectiveUnitVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+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.*;
+
+/**
+ * 基础类数据传输服务
+ * 负责向市适配平台发送基础类数据
+ *
+ * @author han
+ * @date 2025/03/20
+ */
+@Slf4j
+@Service
+@ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
+public class BaseDataTransferService {
+
+    @Autowired
+    private BaseBuildFacilityService baseBuildFacilityService;
+
+    @Autowired
+    private DmpDeviceInfoService dmpDeviceInfoService;
+
+    @Resource
+    private MqttOutConfig.MqttGateway mqttGateway;
+
+    // @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");
+
+    public BaseDataTransferService() {
+        // 使用默认的workerId和datacenterId,实际项目中可以从配置读取
+        this.idGenerator = new SnowflakeIdGenerator(1L, 1L);
+    }
+
+    private String convertFloor(String floor) {
+        if (floor == null || floor.trim().isEmpty()) {
+            return floor;
+        }
+        try {
+            int floorNum = Integer.parseInt(floor.trim());
+            if (floorNum < 0) {
+                return "B" + Math.abs(floorNum);
+            }
+        } catch (NumberFormatException e) {
+            log.warn("楼层格式转换失败,原始值: {}", floor);
+        }
+        return floor;
+    }
+
+    private String getCurrentTime() {
+        return timeFormat.format(new Date());
+    }
+
+    /**
+     * 生成数据包ID
+     */
+    private Long generateDataPacketID() {
+        return idGenerator.nextPacketId();
+    }
+
+    /**
+     * 发送人防工程基础信息
+     * Topic: base/engineering
+     *
+     * @param vo 人防工程基础信息
+     * @return 是否发送成功
+     */
+    public boolean sendEngineeringBase(EngineeringBaseVO vo) {
+        try {
+            if (vo.getDataPacketID() == null) {
+                vo.setDataPacketID(generateDataPacketID());
+            }
+            if (vo.getPublishTime() == null) {
+                vo.setPublishTime(getCurrentTime());
+            }
+
+            String json = JSON.toJSONString(vo);
+            String topic = "base/engineering";
+
+            log.info("发送人防工程基础信息,Topic: {}, Data: {}", topic, json);
+            mqttGateway.sendToMqtt(topic, json);
+
+            return true;
+        } catch (Exception e) {
+            log.error("发送人防工程基础信息失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 发送防护单元基础信息
+     * Topic: base/protectiveUnit
+     *
+     * @param vo 防护单元基础信息
+     * @return 是否发送成功
+     */
+    public boolean sendProtectiveUnit(ProtectiveUnitVO vo) {
+        try {
+            if (vo.getDataPacketID() == null) {
+                vo.setDataPacketID(generateDataPacketID());
+            }
+            if (vo.getPublishTime() == null) {
+                vo.setPublishTime(getCurrentTime());
+            }
+
+            String json = JSON.toJSONString(vo);
+            String topic = "base/protectiveUnit";
+
+            log.info("发送防护单元基础信息,Topic: {}, Data: {}", topic, json);
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, json);
+
+            return true;
+        } catch (Exception e) {
+            log.error("发送防护单元基础信息失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 发送楼层平面图信息
+     * Topic: base/floorPlane
+     *
+     * @param vo 楼层平面图信息
+     * @return 是否发送成功
+     */
+    public boolean sendFloorPlane(FloorPlaneVO vo) {
+        try {
+            // ========== 1. 基础参数填充 ==========
+            if (vo.getDataPacketID() == null) {
+                vo.setDataPacketID(generateDataPacketID());
+            }
+            if (vo.getPublishTime() == null) {
+                vo.setPublishTime(getCurrentTime());
+            }
+
+            String imagePath = "D://BSP0-0103.jpg";
+            // 将图片文件读取为字节数组
+            byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
+
+            // 检查文件大小(不超过5MB)
+            if (vo.getFloorFile() != null && imageBytes.length > 5 * 1024 * 1024) {
+                log.error("楼层平面图文件大小超过5MB限制,FileID: {}", vo.getFloorFileID());
+                return false;
+            }
+
+            HashMap<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", 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));
+            String topic = "base/floorPlane";
+
+            log.info("发送楼层平面图信息,Topic: {}, FileID: {}, FileSize: {} bytes",
+                    topic, vo.getFloorFileID(),
+                    vo.getFloorFile() != null ? vo.getFloorFile().length : 0);
+
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, gson.toJson(map));
+
+            return true;
+        } catch (Exception e) {
+            log.error("发送楼层平面图信息失败,FileID: {}", vo.getFloorFileID(), e);
+            return false;
+        }
+    }
+
+    /**
+     * 根据输入的键(key)从Map中匹配并返回对应的值
+     * @param key 要匹配的键(输入值)
+     * @param dataMap 存储键值对的Map
+     * @return 匹配到的值(若未匹配到返回null)
+     */
+    public static <K, V> V matchByKey(K key, Map<K, V> dataMap) {
+        // 判空处理(避免空指针)
+        if (key == null || dataMap == null) {
+            return null;
+        }
+        // 直接通过Map的get方法匹配
+        return dataMap.get(key);
+    }
+
+    /**
+     * 发送智能监管物联设施信息
+     * Topic: base/sensorInfo
+     *
+     * @param vo 智能监管物联设施信息
+     * @return 是否发送成功
+     */
+    public boolean sendSensorInfo(FacilityDeviceVO vo) {
+        try {
+            Map<Integer, Integer> userIdToName = new HashMap<>();
+            userIdToName.put(702, 31);
+            // 30:网络监控摄像机(可兼用平时摄像机)
+            // userIdToName.put(717, 30);
+            // 20:人员统计-掩蔽人数双目统计摄像机
+            // userIdToName.put(719, 20);
+            // 33:人员闯入-人员闯入监测传感器
+            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(714, 37);
+            userIdToName.put(716, 26);
+
+            HashMap<String, Object> map = new HashMap<>();
+            map.put("dataPacketID", generateDataPacketID());
+            map.put("engineeringID", vo.getEngineeringID());
+            map.put("floor", convertFloor(vo.getFloor()));
+            map.put("floorFileID", 1);
+            map.put("sensorID", Integer.parseInt(vo.getDeviceId()));
+            map.put("sensorNo", vo.getDeviceUuid());
+            map.put("sensorType", matchByKey(vo.getDeviceType(), userIdToName));
+            map.put("unitName", vo.getFacilityDesc());
+            map.put("monitorObjNo", vo.getFacilityNum());
+            map.put("monitorObj", vo.getFacilityName());
+            map.put("location", vo.getAddress());
+//            map.put("xCoordinate", (int) Math.floor(Double.parseDouble(vo.getPlaneX())*4.34));
+//            map.put("yCoordinate", (int) Math.floor(Double.parseDouble(vo.getPlaneY())*4.34));
+            map.put("xCoordinate", Integer.valueOf(vo.getPlaneX()));
+            map.put("yCoordinate", Integer.valueOf(vo.getPlaneY()));
+            map.put("publishTime", getCurrentTime());
+            Gson gson = new Gson();
+            String topic = "base/sensorInfo";
+            System.out.println(gson.toJson(map));
+            log.info("发送智能监管物联设施信息,Topic: {}, SensorID: {}", topic, vo.getDeviceId());
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, gson.toJson(map));
+
+            return true;
+        } catch (Exception e) {
+            log.error("发送智能监管物联设施信息失败", e);
+            return false;
+        }
+    }
+
+    /**
+     * 批量发送防护单元基础信息
+     *
+     * @param units 防护单元列表
+     * @return 成功发送的数量
+     */
+    public int batchSendProtectiveUnits(java.util.List<ProtectiveUnitVO> units) {
+        if (units == null || units.isEmpty()) {
+            return 0;
+        }
+
+        int successCount = 0;
+        for (ProtectiveUnitVO unit : units) {
+            if (sendProtectiveUnit(unit)) {
+                successCount++;
+            }
+        }
+
+        log.info("批量发送防护单元基础信息,总数: {}, 成功: {}", units.size(), successCount);
+        return successCount;
+    }
+
+    /**
+     * 批量发送智能监管物联设施信息
+     *
+     * @param tenantId 租户ID
+     * @return 成功发送的数量
+     */
+    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<>();
+        if (CollectionUtils.isNotEmpty(list) && CollectionUtils.isNotEmpty(list1)) {
+            for (int j = 0; j < list.size(); j++) {
+                for (int k = 0; k < list1.size(); k++) {
+                    if (list.get(j).getDeviceId().equals(list1.get(k).getDeviceId())) {
+                        FacilityDeviceVO facilityDeviceVO = new FacilityDeviceVO();
+                        facilityDeviceVO.setFloor(list.get(j).getFloor());
+                        facilityDeviceVO.setFacilityName(list.get(j).getFacilityName());
+                        facilityDeviceVO.setFacilityNum(list.get(j).getFacilityNum());
+                        facilityDeviceVO.setDeviceId(list.get(j).getDeviceId());
+                        facilityDeviceVO.setAddress(list.get(j).getAddress());
+                        facilityDeviceVO.setPlaneX(list.get(j).getPlaneX());
+                        facilityDeviceVO.setPlaneY(list.get(j).getPlaneY());
+                        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);
+                    }
+                }
+            }
+        }
+        int successCount = 0, failCount = 0, notSynced = 0;
+        if (CollectionUtils.isNotEmpty(list2)) {
+            for (int i = 0; i < list2.size(); i++) {
+                if (sendSensorInfo(list2.get(i))) {
+                    successCount++;
+                } else {
+                    failCount++;
+                }
+            }
+        }
+        notSynced = list2.size() - successCount - failCount;
+        log.info("批量发送智能监管物联设施信息,总数: {}, 成功: {}, 失败: {}, 未同步: {}", list2.size(), successCount, failCount, notSynced);
+
+        Map<String, Integer> map = new HashMap<>();
+        map.put("total", list2.size());
+        map.put("success", successCount);
+        map.put("failure", failCount);
+        map.put("notSynced", notSynced);
+        return map;
+    }
+}
+

+ 63 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDefenseProjectServiceImpl.java

@@ -0,0 +1,63 @@
+package com.usky.cdi.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.cdi.domain.CdiDefenseProject;
+import com.usky.cdi.mapper.CdiDefenseProjectMapper;
+import com.usky.cdi.service.CdiDefenseProjectService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 人防_工程表(人防工程基础信息及MQTT连接配置) 服务实现类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+@Service
+public class CdiDefenseProjectServiceImpl extends AbstractCrudService<CdiDefenseProjectMapper, CdiDefenseProject> implements CdiDefenseProjectService {
+
+    @Override
+    public boolean tryConnectAndPublish(String brokerUrl, String username, String password, String payload, String tmpClientId) {
+
+        MqttClient client = null;
+        try {
+            client = new MqttClient(brokerUrl, tmpClientId, new MemoryPersistence());
+            MqttConnectOptions options = new MqttConnectOptions();
+            options.setUserName(username);
+            options.setPassword(password.toCharArray());
+            options.setConnectionTimeout(3);
+            options.setAutomaticReconnect(false);
+            client.connect(options);
+
+            return true;
+        } catch (Exception e) {
+            log.error("MQTT连接测试失败", e);
+            return false;
+        } finally {
+            if (client != null) {
+                try {
+                    client.disconnect();
+                } catch (Exception ignore) {
+                }
+                try {
+                    client.close();
+                } catch (Exception ignore) {
+                }
+                // 再加一行:把实例从缓存里强行踢掉
+                // MqttClient.removeClient(client.getClientId());
+            }
+        }
+    }
+
+    @Override
+    public CdiDefenseProject listByTenantId(Integer tenantId) {
+        LambdaQueryWrapper<CdiDefenseProject> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(CdiDefenseProject::getTenantId, tenantId);
+        return baseMapper.selectOne(queryWrapper);
+    }
+}

+ 805 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java

@@ -0,0 +1,805 @@
+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.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;
+import com.usky.cdi.domain.*;
+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;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.common.security.utils.SecurityUtils;
+import lombok.extern.slf4j.Slf4j;
+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.*;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * 人防_投递日志表(记录数据的MQTT投递情况) 服务实现类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-02
+ */
+@Slf4j
+@Service
+public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLogMapper, CdiDeliveryLog> implements CdiDeliveryLogService {
+
+    private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    @Autowired
+    private SysUserMapper sysUserMapper;
+
+    @Autowired
+    private BaseBuildFacilityMapper baseBuildFacilityMapper;
+
+    @Autowired
+    private BaseBuildUnitMapper baseBuildUnitMapper;
+
+    @Autowired
+    private CdiDefenseProjectMapper cdiDefenseProjectMapper;
+
+    @Autowired
+    private BaseBuildPlaneMapper baseBuildPlaneMapper;
+
+    @Autowired
+    private BaseBuildMapper baseBuildMapper;
+
+    @Autowired
+    private BaseDataTransferService baseDataTransferService;
+
+    @Autowired
+    private IotDataTransferService iotDataTransferService;
+
+    @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) {
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (tenantId == null || tenantId <= 0) {
+            log.warn("未获取到当前租户ID,无法查询人防投递日志");
+            return Collections.emptyList();
+        }
+
+        List<CdiDeliveryLog> logList = lambdaQuery()
+                .eq(CdiDeliveryLog::getTenantId, tenantId)
+                .eq(id != null, CdiDeliveryLog::getId, id)
+                .orderByDesc(CdiDeliveryLog::getId)
+                .list();
+
+        LambdaQueryWrapper<CdiDefenseProject> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(CdiDefenseProject::getTenantId, tenantId);
+        CdiDefenseProject defenseProject = cdiDefenseProjectMapper.selectOne(queryWrapper);
+        boolean isEnable = defenseProject != null && defenseProject.getIsEnable() == 0;
+
+        List<SyncTaskStatisticsVO> finalResult = new ArrayList<>();
+
+        // 预先查询各类型的基础数据量(无日志时使用)
+        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;
+        }
+
+        List<CdiDeliveryLog> validLogList = logList.stream()
+                .filter(logEntity -> logEntity != null
+                        && StrUtil.isNotBlank(logEntity.getInfoContent())
+                        && logEntity.getDataType() != null)
+                .collect(Collectors.toList());
+
+        if (CollectionUtils.isEmpty(validLogList)) {
+            log.warn("租户ID:{} 查询到{}条日志,但所有日志的JSON内容为空或dataType为空", tenantId, logList.size());
+            fillEmptyStatistics(finalResult, tenantId, isEnable);
+            return finalResult;
+        }
+
+        // ★ 改动1:按 dataType 分组,保留每种类型的全部日志
+        Map<Integer, List<CdiDeliveryLog>> allLogsByType = validLogList.stream()
+                .collect(Collectors.groupingBy(CdiDeliveryLog::getDataType));
+
+        for (int type = 1; type <= 5; type++) {
+            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;
+            }
+
+            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);
+                    }
+                }
+
+                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);
+        }
+
+        finalResult.sort(Comparator.comparingInt(SyncTaskStatisticsVO::getDataType));
+        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) {
+        IPage<CdiDeliveryLog> page = new Page<>(pageNum, pageSize);
+
+        LambdaQueryWrapper<SysUser> queryUser = new LambdaQueryWrapper<>();
+        queryUser.eq(SysUser::getTenantId, SecurityUtils.getTenantId());
+        Map<String, String> userMap = sysUserMapper.selectList(queryUser).stream().collect(
+                Collectors.toMap(SysUser::getUserName, SysUser::getNickName)
+        );
+
+        LambdaQueryWrapper<CdiDeliveryLog> queryWrapper = new LambdaQueryWrapper<>();
+        if (id != null) {
+            queryWrapper.eq(CdiDeliveryLog::getId, id);
+            page = this.page(page, queryWrapper);
+        } else {
+            LocalDateTime start = StrUtil.isNotBlank(startTime) ? LocalDateTime.parse(startTime, DATETIME_FORMATTER) : null;
+            LocalDateTime end = StrUtil.isNotBlank(endTime) ? LocalDateTime.parse(endTime, DATETIME_FORMATTER) : null;
+
+            queryWrapper.eq(CdiDeliveryLog::getTenantId, SecurityUtils.getTenantId())
+                    .eq(dataType != null, CdiDeliveryLog::getDataType, dataType)
+                    .eq(logType != null, CdiDeliveryLog::getPushFlag, logType)
+                    .between(start != null && end != null && start.isBefore(end),
+                            CdiDeliveryLog::getCreateTime, start, end)
+                    .orderByDesc(CdiDeliveryLog::getCreateTime);
+            page = this.page(page, queryWrapper);
+        }
+        page.getRecords().forEach(log -> {
+            log.setNickName(userMap.getOrDefault(log.getUserName(), log.getUserName()));
+            log.setDataTypeName((MqttTopics.getDescByTopic(log.getTopic())));
+        });
+
+        return new CommonPage<>(page.getRecords(), page.getTotal(), pageSize, pageNum);
+    }
+
+    /**
+     * 获取建筑设施列表
+     *
+     * @param tenantId 租户ID
+     * @return 结果列表
+     */
+    private List<BaseBuildFacility> getBuildFacilityList(Integer tenantId) {
+        LambdaQueryWrapper<BaseBuildFacility> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(BaseBuildFacility::getTenantId, tenantId)
+                .eq(BaseBuildFacility::getDeleteFlag, 0);
+        return baseBuildFacilityMapper.selectList(queryWrapper);
+    }
+
+    /**
+     * 获取建筑单元列表
+     *
+     * @param tenantId 租户ID
+     * @return 结果列表
+     */
+    private List<BaseBuildUnit> getBuildUnitList(Integer tenantId) {
+        LambdaQueryWrapper<BaseBuildUnit> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(BaseBuildUnit::getTenantId, tenantId);
+        return baseBuildUnitMapper.selectList(queryWrapper);
+    }
+
+    /**
+     * 获取建筑列表
+     *
+     * @param tenantId 租户ID
+     * @return 列表
+     */
+    private List<BaseBuild> getBuildList(Integer tenantId) {
+        LambdaQueryWrapper<BaseBuild> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(BaseBuild::getTenantId, tenantId);
+        return baseBuildMapper.selectList(queryWrapper);
+    }
+
+    /**
+     * 获取建筑平面列表
+     *
+     * @param buildIds 建筑ID列表
+     * @return 结果列表
+     */
+    private List<BaseBuildPlane> getBuildPlaneList(List<Integer> buildIds) {
+        LambdaQueryWrapper<BaseBuildPlane> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(BaseBuildPlane::getBuildId, buildIds);
+        return baseBuildPlaneMapper.selectList(queryWrapper);
+    }
+
+    /**
+     * 同步数据
+     *
+     * @param vo 同步任务统计信息
+     * @return null
+     */
+    @Override
+    public void synchronizeData(SyncTaskStatisticsVO vo) {
+
+        if (vo.getDataType() == null) {
+            throw new BusinessException("数据类型不能为空!");
+        } else if (vo.getDataType() < 1 || vo.getDataType() > 4) {
+            throw new BusinessException("同步数据类型错误!");
+        }
+
+        LambdaQueryWrapper<CdiDefenseProject> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(CdiDefenseProject::getTenantId, SecurityUtils.getTenantId());
+        CdiDefenseProject one = cdiDefenseProjectMapper.selectOne(queryWrapper);
+
+        if (one == null) {
+            throw new BusinessException("未找到对应的工程配置!无法同步");
+        } else if (!one.getIsEnable().equals(1)) {
+            throw new BusinessException("工程配置未启用!无法同步");
+        }
+
+        Long engineeringId = one.getEngineeringId();
+        String username = one.getMqttUserName();
+        String password = one.getMqttPassword();
+        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:
+                log.info("开始同步单元信息");
+
+                long startTime, endTime;
+                startTime = System.currentTimeMillis();
+                int total = 0, success = 0, failure = 0, notSynced = 0;
+
+                List<ProtectiveUnitVO> unitVOS = synchronizeBuildUnit(tenantId, engineeringId);
+                total = unitVOS.size();
+                mqttConnectionTool.connectOrRefresh(username, password);
+                iotDataTransferService.createMqttConnection(username, password);
+                String topic = MqttTopics.Base.PROTECTIVE_UNIT.getTopic();
+                String desc = MqttTopics.Base.PROTECTIVE_UNIT.getDesc();
+
+                for (ProtectiveUnitVO unitVO : unitVOS) {
+                    try {
+                        iotDataTransferService.sendMqttMessage(topic, unitVO, desc, username);
+                        success++;
+                    } catch (Exception e) {
+                        log.error("同步单元信息失败!", e);
+                        failure++;
+                    }
+                }
+                endTime = System.currentTimeMillis();
+
+                notSynced = total - success - failure;
+                saveLog(topic, desc, 1, tenantId, engineeringId, now, startTime, endTime, total, success, failure, notSynced, failure > 0 ? 0 : 1, userName);
+
+                break;
+            // 平面图信息
+            case 2:
+                log.info("开始同步楼层平面图信息");
+
+                long startTime2, endTime2;
+                startTime2 = System.currentTimeMillis();
+                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);
+                        success2++;
+                    } catch (Exception e) {
+                        log.error("同步楼层平面图信息失败!", e);
+                        failure2++;
+                    }
+                }
+                endTime2 = System.currentTimeMillis();
+                notSynced2 = total2 - success2 - failure2;
+                saveLog(topic1, desc1, 2, tenantId, engineeringId, now, startTime2, endTime2, total2, success2,
+                        failure2, notSynced2, failure2 > 0 ? 0 : 1, userName);
+
+                break;
+            // 推送设施信息
+            case 3:
+                log.info("开始同步设施信息");
+
+                long startTime3, endTime3;
+                startTime3 = System.currentTimeMillis();
+
+                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, userName);
+
+                break;
+            // 推送监测数据
+            case 4:
+                log.info("开始同步监测数据");
+                iotDataTransferService.synchronizeDeviceData(tenantId, engineeringId, username, password);
+                // iotDataTransferService.synchronizeDeviceData(1205, 3101070011L, "3101070011", "5RqhJ7VG");
+                break;
+        }
+    }
+
+    // 存储日志
+    @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, String userName) {
+
+        SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
+        vo.setDataType(dataType);
+        vo.setTotal(total);
+        vo.setSuccessNumber(success);
+        vo.setFailNumber(failure);
+        vo.setNotSynced(notSynced);
+        vo.setCreateTime(now.format(DATETIME_FORMATTER));
+        vo.setCostTime((float) (endTime - startTime) / 1000.0f);
+        vo.setState(1);
+        vo.setTopic(topic);
+        vo.setDataTypeName(dataTypeName);
+
+        CdiDeliveryLog log = new CdiDeliveryLog();
+        log.setEngineeringId(engineeringId);
+        log.setDataType(dataType);
+        log.setTopic(topic);
+        log.setDataTypeName(vo.getDataTypeName());
+        log.setUserName(userName);
+        log.setCreateTime(now);
+        log.setTenantId(tenantId);
+        log.setPushFlag(pushFlag);
+        log.setInfoContent(JSON.toJSONString(vo));
+
+        this.save(log);
+    }
+
+    // 单元信息
+    private List<ProtectiveUnitVO> synchronizeBuildUnit(Integer tenantId, Long engineeringId) {
+        List<BaseBuildUnit> buildUnitList = getBuildUnitList(tenantId);
+        if (CollUtil.isEmpty(buildUnitList)) {
+            return Collections.emptyList();
+        }
+
+        String time = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
+
+        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());
+            vo.setUnitArea(buildUnit.getUnitArea() != null
+                    ? BigDecimal.valueOf(buildUnit.getUnitArea())
+                    : BigDecimal.ZERO);
+            vo.setUnitUsage(buildUnit.getUnitUsage());
+            vo.setPeopleNumber(buildUnit.getPeopleNumber());
+            vo.setUnitmainexit(buildUnit.getUnitMainExit());
+            vo.setUnitotherexit(buildUnit.getUnitOtherExit());
+            vo.setPublishTime(time);
+            result.add(vo);
+        }
+
+        return result;
+    }
+
+    // 平面图信息
+    private List<FloorPlaneVO> buildPlanes(Integer tenantId, Long engineeringId) {
+        List<BaseBuild> buildList = getBuildList(tenantId);
+        if (CollUtil.isEmpty(buildList)) {
+            return Collections.emptyList();
+        }
+
+        List<Integer> buildIds = buildList.stream().map(BaseBuild::getId).collect(Collectors.toList());
+        List<BaseBuildPlane> buildPlaneList = getBuildPlaneList(buildIds);
+
+        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<BaseBuildPlane> filteredPlaneList = latestPlaneByFloor.values().stream()
+                .filter(Objects::nonNull)
+                .sorted(Comparator.comparing(BaseBuildPlane::getFloor))
+                .collect(Collectors.toList());
+
+        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()));
+            vo.setPublishTime(time);
+
+            result.add(vo);
+        }
+
+        return result;
+    }
+
+    // 校验文件大小不超过5MB,然后存入vo中
+    private static final long MAX_FILE_SIZE_MB = 5;
+    private static final long MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
+
+    private void checkFileSize(FloorPlaneVO vo, String filePath) {
+        Assert.notBlank(filePath, "文件路径不能为空");
+
+        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());
+        }
+    }
+
+    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 {
+            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, e);
+            vo.setFilePixWidth(7016);
+            vo.setFilePixHeight(9933);
+        }
+    }
+
+
+}

+ 35 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/DmpDeviceInfoServiceImpl.java

@@ -0,0 +1,35 @@
+package com.usky.cdi.service.impl;
+
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.usky.cdi.domain.BaseBuildFacility;
+import com.usky.cdi.domain.DmpDevice;
+import com.usky.cdi.mapper.DmpDeviceMapper;
+import com.usky.cdi.service.DmpDeviceInfoService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 设备信息表 服务实现类
+ * </p>
+ *
+ * @author ya
+ * @since 2022-10-08
+ */
+@Slf4j
+@Service
+public class DmpDeviceInfoServiceImpl extends AbstractCrudService<DmpDeviceMapper, DmpDevice> implements DmpDeviceInfoService {
+    @Override
+    public List<DmpDevice> deviceInfo(Integer tenantId){
+        LambdaQueryWrapper<DmpDevice> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.eq(DmpDevice::getDeleteFlag,0)
+                .eq(DmpDevice::getTenantId, tenantId);
+        List<DmpDevice> list = this.list(queryWrapper);
+        return list;
+    }
+}

+ 20 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/DmpProductServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.cdi.service.impl;
+
+import com.usky.cdi.domain.DmpProduct;
+import com.usky.cdi.mapper.DmpProductMapper;
+import com.usky.cdi.service.DmpProductService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 产品信息表 服务实现类
+ * </p>
+ *
+ * @author fu
+ * @since 2025-12-05
+ */
+@Service
+public class DmpProductServiceImpl extends AbstractCrudService<DmpProductMapper, DmpProduct> implements DmpProductService {
+
+}

+ 1499 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java

@@ -0,0 +1,1499 @@
+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.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.usky.cdi.domain.*;
+import com.usky.cdi.mapper.*;
+import com.usky.cdi.service.enums.MqttTopics;
+import com.usky.cdi.service.mqtt.MqttConnectionTool;
+import com.usky.cdi.service.util.AirDefenseSimulator;
+import com.usky.cdi.service.util.DeviceDataQuery;
+import com.usky.cdi.service.util.SnowflakeIdGenerator;
+import com.usky.cdi.service.vo.IotDataTransferVO;
+import com.usky.cdi.service.vo.SyncTaskStatisticsVO;
+import com.usky.cdi.service.vo.info.*;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationContext;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+/**
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2025/11/20
+ */
+@Slf4j
+@Service
+public class IotDataTransferService {
+
+    @Autowired
+    private MqttConnectionTool mqttConnectionTool;
+
+    // 注入ApplicationContext,确保总是能获取到
+    @Autowired
+    private ApplicationContext context;
+
+    // MQTT连接相关配置
+    private static final String MQTT_URL = "ssl://114.80.201.143:8883";
+    private static final String MQTT_TOPIC = "iotInfo/+";
+    private static final int KEEP_ALIVE_INTERVAL = 60;
+    private static final int COMPLETION_TIMEOUT = 5000;
+
+    // 存储每个任务的MQTT客户端工厂和网关
+    private final Map<String, MqttConnectionTool.MqttGateway> mqttGatewayMap = new ConcurrentHashMap<>();
+    private final Map<String, DefaultMqttPahoClientFactory> mqttClientFactoryMap = new ConcurrentHashMap<>();
+
+    private SnowflakeIdGenerator idGenerator;
+
+    @Autowired
+    private DeviceDataQuery deviceDataQuery;
+
+    @Autowired
+    private DmpDeviceMapper dmpDeviceMapper;
+
+    @Value("${device.data.simulation}")
+    private boolean simulation;
+
+    @Autowired
+    private DmpProductMapper dmpProductMapper;
+
+    // 从配置文件读取Snowflake参数,默认值为2
+    @Value("${snowflake.worker-id:2}")
+    private long workerId;
+
+    @Value("${snowflake.data-center-id:2}")
+    private long dataCenterId;
+
+    @Autowired
+    private CdiDefenseProjectMapper cdiDefenseProjectMapper;
+
+    @Autowired
+    private CdiDeliveryLogMapper cdiDeliveryLogMapper;
+
+    @Autowired
+    private BaseBuildUnitMapper baseBuildUnitMapper;
+
+    @PostConstruct
+    public void init() {
+        this.idGenerator = new SnowflakeIdGenerator(workerId, dataCenterId);
+    }
+
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    /**
+     * 获取当前时间
+     */
+    private LocalDateTime getCurrentTime() {
+        return LocalDateTime.now();
+    }
+
+    /**
+     * 生成数据包ID
+     */
+    private Long generateDataPacketID() {
+        return idGenerator.nextPacketId();
+    }
+
+    /**
+     * 发送水浸状态(deviceType:702)
+     * Topic: iotInfo/flooded
+     *
+     * @return 推送结果,包含成功数和失败数
+     */
+    public Map<String, Integer> sendWaterLeak(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();
+
+        try {
+            List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+            log.warn("获取到的数据:{}", deviceData);
+            Integer deviceType = transferVO.getDeviceType();
+            Integer totalDevices = transferVO.getDevices().size();
+
+            log.info("开始推送水浸状态数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+                    deviceType, totalDevices, deviceData.size());
+
+            if (deviceData.isEmpty()) {
+                log.warn("没有获取到水浸数据!设备类型:{}", deviceType);
+                result.put("failureCount", totalDevices);
+
+                // ✅ 在空数据时也记录一条日志(可选)
+                long endTime = System.currentTimeMillis();
+                saveLog(transferVO, now, startTime, endTime, totalDevices, 0, totalDevices, 0, 0, SecurityUtils.getUsername());
+                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;
+                }
+
+                // 为每条数据的监测时间添加毫秒级微差
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+                Integer value = deviceDataItem.getInteger("leach_status");
+                if (value == null) {
+                    log.warn("设备{}的水浸状态数据为空", deviceId);
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+
+                WaterLeakVO vo = new WaterLeakVO();
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setSensorID(deviceId);
+                vo.setEngineeringID(engineeringId);
+                vo.setPublishTime(getCurrentTime());
+                vo.setSensorValue(value);
+                vo.setDataEndTime(dataEndTime);
+
+                try {
+                    log.info("【水浸数据】开始推送,设备:{},数据:{}", deviceId, JSON.toJSONString(vo));
+                    sendMqttMessage(MqttTopics.IotInfo.WATER_LEAK.getTopic(), vo, MqttTopics.IotInfo.WATER_LEAK.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);
+                }
+            }
+
+            // ✅ 所有设备处理完毕后,统一记录一条日志
+            long endTime = System.currentTimeMillis();
+            int success = result.get("successCount");
+            int failure = result.get("failureCount");
+            int pending = totalDevices - success - failure;
+
+            saveLog(transferVO, now, startTime, endTime, totalDevices, success, failure, pending, success > 0 ? 1 : 0, SecurityUtils.getUsername());
+
+            log.info("水浸状态数据推送完成,设备类型:{},成功:{},失败:{}",
+                    deviceType, success, failure);
+
+            return result;
+        } catch (Exception e) {
+            log.error("水浸状态数据推送发生异常", e);
+            result.put("failureCount", transferVO.getDevices().size());
+
+            // ✅ 异常时也记录一条日志
+            long endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, transferVO.getDevices().size(), 0, transferVO.getDevices().size(), 0, 0, SecurityUtils.getUsername());
+            return result;
+        }
+    }
+
+    /**
+     * 发送温湿度及气体浓度数据(设备类型:701,707-711)
+     * 包含: wd(温度), sd(湿度), o2(氧气), co(一氧化碳), co2(二氧化碳)
+     *
+     * @return 推送结果,包含成功数和失败数
+     */
+    public Map<String, Integer> sendEnvData(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;
+
+        Integer deviceType = transferVO.getDeviceType();
+        Integer totalDevices = transferVO.getDevices().size();
+
+        try {
+            List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+
+            log.info("开始推送环境数据,设备类型:{},总设备数:{},获取到数据条数:{}",
+                    deviceType, totalDevices, deviceData.size());
+
+            Long engineeringId = transferVO.getEngineeringId();
+
+            int skippedNullField = 0;
+            for (JSONObject deviceDataItem : deviceData) {
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+                LocalDateTime dataEndTime = parseDataTime(deviceDataItem);
+
+                if (dataEndTime == null) {
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+
+                // 为每条数据的监测时间添加毫秒级微差
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                // 检查业务字段是否为空,为空则计入失败(修复:原逻辑静默跳过并误计为成功)
+                boolean fieldMissing = false;
+                switch (deviceType) {
+                    case 707:
+                        if (deviceDataItem.getFloat("wd") == null) fieldMissing = true;
+                        break;
+                    case 708:
+                        if (deviceDataItem.getFloat("sd") == null) fieldMissing = true;
+                        break;
+                    case 709:
+                        if (deviceDataItem.getFloat("o2") == null) fieldMissing = true;
+                        break;
+                    case 710:
+                        if (deviceDataItem.getFloat("co2") == null) fieldMissing = true;
+                        break;
+                    case 711:
+                        if (deviceDataItem.getFloat("co") == null) fieldMissing = true;
+                        break;
+                }
+                if (fieldMissing) {
+                    log.warn("设备{}[类型{}]的业务字段数据为空,跳过推送,计入失败", deviceId, deviceType);
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    skippedNullField++;
+                    continue;
+                }
+
+                boolean deviceSuccess = true;
+                try {
+                    switch (deviceType) {
+                        case 707:
+                            sendTempData(deviceId, dataEndTime, deviceDataItem, engineeringId, transferVO.getUsername());
+                            break;
+                        case 708:
+                            sendHumidityData(deviceId, dataEndTime, deviceDataItem, engineeringId, transferVO.getUsername());
+                            break;
+                        case 709:
+                            sendOxygenData(deviceId, dataEndTime, deviceDataItem, engineeringId, transferVO.getUsername());
+                            break;
+                        case 710:
+                            sendCo2Data(deviceId, dataEndTime, deviceDataItem, engineeringId, transferVO.getUsername());
+                            break;
+                        case 711:
+                            sendCoData(deviceId, dataEndTime, deviceDataItem, engineeringId, transferVO.getUsername());
+                            break;
+                    }
+                } catch (Exception e) {
+                    log.warn("设备{}[类型{}]推送异常:{}", deviceId, deviceType, e.getMessage(), e);
+                    deviceSuccess = false;
+                }
+
+                if (deviceSuccess) {
+                    result.put("successCount", result.get("successCount") + 1);
+                } else {
+                    result.put("failureCount", result.get("failureCount") + 1);
+                }
+            }
+
+            if (skippedNullField > 0) {
+                log.warn("[类型{}] 因业务字段为空而跳过的设备数量:{}", deviceType, skippedNullField);
+            }
+
+            int success = result.get("successCount");
+            int failure = result.get("failureCount");
+            int notSynced = totalDevices - success - failure;
+
+            endTime = System.currentTimeMillis();
+
+            // ✅ 确保这里会被执行,且deviceType被正确记录
+            saveLog(transferVO, now, startTime, endTime, totalDevices, success, failure, notSynced, 1, SecurityUtils.getUsername());
+
+            log.info("空气质量推送完成,设备类型:{},成功:{},失败:{},未同步:{}",
+                    deviceType, success, failure, notSynced);
+
+            return result;
+
+        } catch (Exception e) {
+            log.error("空气质量推送异常", e);
+            result.put("failureCount", totalDevices - result.get("successCount"));
+
+            endTime = System.currentTimeMillis();
+            int success = result.get("successCount");
+            int failure = result.get("failureCount");
+
+            // ✅ 异常情况下也要记录日志
+            saveLog(transferVO, now, startTime, endTime, totalDevices, success, failure,
+                    totalDevices - success - failure, 0, SecurityUtils.getUsername());
+
+            return result;
+        }
+    }
+
+    @Async("asyncServiceExecutor")
+    public void saveLog(IotDataTransferVO transferVO, LocalDateTime now, long startTime, long endTime,
+                        int total, int success, int failure, int notSynced, int pushFlag, String userName) {
+
+        Integer deviceType = transferVO.getDeviceType();
+
+        if (StringUtils.isBlank(userName)) {
+            userName = "自动同步";
+        }
+
+        SyncTaskStatisticsVO vo = new SyncTaskStatisticsVO();
+        vo.setDataType(4);
+        vo.setTotal(total);
+        vo.setSuccessNumber(success);
+        vo.setFailNumber(failure);
+        vo.setNotSynced(notSynced);
+        vo.setCreateTime(now.format(DATE_TIME_FORMATTER));
+        vo.setCostTime((float) (endTime - startTime) / 1000.0f);
+        vo.setState(1);
+        vo.setTopic(MqttTopics.getIotInfoByTypeCode(deviceType).getTopic());
+        vo.setDataTypeName(MqttTopics.getIotInfoByTypeCode(deviceType).getDesc());
+
+        CdiDeliveryLog log = new CdiDeliveryLog();
+        log.setEngineeringId(transferVO.getEngineeringId());
+        log.setDataType(4);
+        log.setTopic(vo.getTopic());
+        log.setDataTypeName(vo.getDataTypeName());
+        log.setUserName(userName);
+        log.setNickName(userName);
+        log.setCreateTime(now);
+        log.setTenantId(getTenantId(transferVO.getEngineeringId()));
+        log.setPushFlag(pushFlag);
+        log.setInfoContent(JSON.toJSONString(vo));
+
+        cdiDeliveryLogMapper.insert(log);
+    }
+
+    private Integer getTenantId(Long engineeringId) {
+        LambdaQueryWrapper<CdiDefenseProject> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(CdiDefenseProject::getEngineeringId, engineeringId)
+                .eq(CdiDefenseProject::getIsEnable, 1);
+        CdiDefenseProject cdiDefenseProject = cdiDefenseProjectMapper.selectOne(queryWrapper);
+        if (cdiDefenseProject == null) {
+            throw new BusinessException("未找到工程信息!无法同步");
+        }
+        return cdiDefenseProject.getTenantId();
+    }
+
+    /**
+     * 发送人员统计情况(703)-掩蔽人数
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendPersonCount(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();
+
+        // 模拟人员统计
+        log.info("开始执行【人员统计】数据推送,工程ID:{}", transferVO.getEngineeringId());
+        try {
+            LambdaQueryWrapper<BaseBuildUnit> buildUnitQuery = new LambdaQueryWrapper<>();
+            buildUnitQuery.eq(BaseBuildUnit::getTenantId, transferVO.getTenantId());
+            List<BaseBuildUnit> buildUnitList = baseBuildUnitMapper.selectList(buildUnitQuery);
+
+            // 修复3:单元为空,打印日志+标记失败,不静默返回
+            if (buildUnitList.isEmpty()) {
+                log.warn("【人员统计】未查询到建筑单元,租户ID:{},推送终止", transferVO.getTenantId());
+                result.put("failureCount", 1);
+                return result;
+            }
+
+            log.info("【人员统计】获取到建筑单元数量:{}", buildUnitList.size());
+            for (BaseBuildUnit buildUnit : buildUnitList) {
+                int peopleCount = AirDefenseSimulator.calculatePeopleCount(now);
+                HeadcountVO headcountVO = new HeadcountVO();
+                headcountVO.setDataPacketID(generateDataPacketID());
+                headcountVO.setEngineeringID(transferVO.getEngineeringId());
+                headcountVO.setSensorValue(peopleCount);
+                // headcountVO.setSensorID(buildUnit.getId());
+
+                // 为每条数据的监测时间添加毫秒级微差
+                LocalDateTime dataEndTime = addTimeOffset(now);
+
+                headcountVO.setDataEndTime(dataEndTime);
+                headcountVO.setPublishTime(getCurrentTime());
+                headcountVO.setUnitName(buildUnit.getUnitName());
+                headcountVO.setFloor(buildUnit.getFloor());
+
+                try {
+                    log.info("【人员统计】开始推送,单元:{},数据:{}", buildUnit.getUnitName(), JSON.toJSONString(headcountVO));
+                    sendMqttMessage(
+                            MqttTopics.IotInfo.PERSON.getTopic(),
+                            headcountVO,
+                            MqttTopics.IotInfo.PERSON.getDesc(),
+                            transferVO.getUsername()
+                    );
+                    // 添加2S延时
+                    Thread.sleep(2000);
+                    result.put("successCount", result.get("successCount") + 1);
+                    log.info("【人员统计】推送成功,单元:{},人数:{}", buildUnit.getUnitName(), peopleCount);
+                } catch (Exception e) {
+                    log.warn("【人员统计】数据推送失败,单元:{},异常:{}", buildUnit.getUnitName(), e.getMessage());
+                    result.put("failureCount", result.get("failureCount") + 1);
+                }
+            }
+
+            // 修复4:统计推送完成,打印汇总日志
+            log.info("【人员统计】推送完成,成功:{},失败:{}", result.get("successCount"), result.get("failureCount"));
+            // 可选:和闯入一样保存汇总日志(推荐加上)
+            long endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, buildUnitList.size(),
+                    result.get("successCount"), result.get("failureCount"), 0,
+                    result.get("successCount") > 0 ? 1 : 0, SecurityUtils.getUsername());
+
+        } catch (Exception e) {
+            // 修复5:全局异常兜底,绝不丢失
+            log.error("【人员统计】推送发生全局异常", e);
+            result.put("failureCount", result.getOrDefault("failureCount", 0) + 1);
+        }
+        return result;
+    }
+
+    /**
+     * 发送人员闯入情况(703)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendPersonPresence(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();
+        try {
+            List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+            Integer deviceType = transferVO.getDeviceType();
+            Integer totalDevices = transferVO.getDevices().size();
+
+            log.info("开始推送人员闯入情况数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+                    deviceType, totalDevices, deviceData.size());
+
+            // 处理无数据情况
+            if (deviceData.isEmpty()) {
+                log.warn("没有获取到人员闯入情况数据!设备类型:{}", deviceType);
+                result.put("failureCount", totalDevices);
+
+                // 记录一条汇总日志
+                long endTime = System.currentTimeMillis();
+                saveLog(transferVO, now, startTime, endTime, totalDevices,
+                        0, totalDevices, 0, 0, SecurityUtils.getUsername());
+                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;
+                }
+
+                // 处理数据时间
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+
+                PersonPresenceVO vo = new PersonPresenceVO();
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setSensorID(deviceId);
+                vo.setDataEndTime(dataEndTime);
+                vo.setPublishTime(getCurrentTime());
+                vo.setEngineeringID(engineeringId);
+                vo.setSensorValue(0); // 固定值(根据业务需求)
+
+                try {
+                    log.info("【人员闯入】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
+                    sendMqttMessage(
+                            MqttTopics.IotInfo.PERSON_PRESENCE.getTopic(),
+                            vo,
+                            MqttTopics.IotInfo.PERSON_PRESENCE.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);
+                }
+            }
+
+            // 所有设备处理完毕,统一记录一条汇总日志
+            long endTime = System.currentTimeMillis();
+            int success = result.get("successCount");
+            int failure = result.get("failureCount");
+            int pending = totalDevices - success - failure;
+            int status = (success > 0) ? 1 : 0; // 1=部分或全部成功,0=全失败
+
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    success, failure, pending, status, SecurityUtils.getUsername());
+
+            log.info("人员闯入情况数据推送完成,设备类型:{},成功:{},失败:{}",
+                    deviceType, success, failure);
+
+            return result;
+
+        } catch (Exception e) {
+            log.error("人员闯入情况数据推送发生异常", e);
+            int totalDevices = transferVO.getDevices() != null ? transferVO.getDevices().size() : 0;
+            result.put("failureCount", totalDevices);
+
+            // 异常时也记录一条日志
+            long endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    0, totalDevices, 0, 0, SecurityUtils.getUsername());
+
+            return result;
+        }
+    }
+
+    /**
+     * 发送人防用电负荷情况(704)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendElectricityLoad(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();
+
+        try {
+            List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+            Integer deviceType = transferVO.getDeviceType();
+            Integer totalDevices = (transferVO.getDevices() != null) ? transferVO.getDevices().size() : 0;
+
+            log.info("开始推送人防用电负荷情况数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+                    deviceType, totalDevices, deviceData.size());
+
+            // 处理无数据情况
+            if (deviceData.isEmpty()) {
+                log.warn("没有获取到人防用电负荷情况数据!设备类型:{}", deviceType);
+                result.put("failureCount", totalDevices);
+
+                // 记录一条汇总日志
+                long endTime = System.currentTimeMillis();
+                saveLog(transferVO, now, startTime, endTime, totalDevices,
+                        0, totalDevices, 0, 0, SecurityUtils.getUsername());
+                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;
+                }
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+
+                ElectricityLoadVO vo = new ElectricityLoadVO();
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setSensorID(deviceId);
+                vo.setEngineeringID(engineeringId);
+                vo.setPublishTime(getCurrentTime());
+                vo.setDataEndTime(dataEndTime);
+                vo.setAVoltage(deviceDataItem.getFloat("aVoltage"));
+                vo.setBVoltage(deviceDataItem.getFloat("bVoltage"));
+                vo.setCVoltage(deviceDataItem.getFloat("cVoltage"));
+                vo.setAElectricity(deviceDataItem.getFloat("aElectricity"));
+                vo.setBElectricity(deviceDataItem.getFloat("bElectricity"));
+                vo.setCElectricity(deviceDataItem.getFloat("cElectricity"));
+                vo.setLine1TEMP(deviceDataItem.getFloat("line1TEMP"));
+                vo.setLine2TEMP(deviceDataItem.getFloat("Line2TEMP"));
+                vo.setLine3TEMP(deviceDataItem.getFloat("Line3TEMP"));
+                vo.setLeakageCurrent(deviceDataItem.getFloat("leakageCurrent"));
+
+                // 根据模拟模式选择不同的功率字段
+                Float totalPower = simulation
+                        ? deviceDataItem.getFloat("totalPower")
+                        : deviceDataItem.getFloat("active_power");
+                vo.setTotalPower(totalPower);
+
+                try {
+                    log.info("【人防用电负荷情况】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
+                    sendMqttMessage(
+                            MqttTopics.IotInfo.ELECTRICITY_LOAD.getTopic(),
+                            vo,
+                            MqttTopics.IotInfo.ELECTRICITY_LOAD.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);
+                }
+            }
+
+            // 所有设备处理完毕,统一记录一条汇总日志
+            long endTime = System.currentTimeMillis();
+            int success = result.get("successCount");
+            int failure = result.get("failureCount");
+            int pending = totalDevices - success - failure;
+            int status = (success > 0) ? 1 : 0; // 1=有成功,0=全失败
+
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    success, failure, pending, status, SecurityUtils.getUsername());
+
+            log.info("人防用电负荷情况数据推送完成,设备类型:{},成功:{},失败:{}",
+                    deviceType, success, failure);
+
+            return result;
+
+        } catch (Exception e) {
+            log.error("人防用电负荷情况数据推送发生异常", e);
+            int totalDevices = (transferVO.getDevices() != null) ? transferVO.getDevices().size() : 0;
+            result.put("failureCount", totalDevices);
+
+            // 异常时也记录一条日志
+            long endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    0, totalDevices, 0, 0, SecurityUtils.getUsername());
+
+            return result;
+        }
+    }
+
+    /**
+     * 推送温度信息(707)
+     *
+     * @param deviceDataItem 设备数据
+     * @param deviceId 设备ID
+     * @param dataEndTime 数据结束时间
+     * @param engineeringID 工程ID
+     * @param username 用户名
+     **/
+    private void sendTempData(Integer deviceId, LocalDateTime dataEndTime, JSONObject deviceDataItem, Long engineeringID, String username) {
+        Float value = deviceDataItem.getFloat("wd");
+        if (value == null) {
+            log.warn("设备{}的温度数据为空", deviceId);
+            return;
+        }
+        TempVO tempVO = new TempVO();
+        tempVO.setDataPacketID(generateDataPacketID());
+        tempVO.setSensorID(deviceId);
+        tempVO.setEngineeringID(engineeringID);
+        tempVO.setPublishTime(getCurrentTime());
+        tempVO.setSensorValue(value);
+        tempVO.setDataEndTime(dataEndTime);
+        System.out.println("监测时间:" + dataEndTime);
+
+        log.info("【温度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(tempVO));
+        sendMqttMessage(MqttTopics.IotInfo.TEMP.getTopic(), tempVO, MqttTopics.IotInfo.TEMP.getDesc(), username);
+    }
+
+    /**
+     * 推送湿度信息(708)
+     *
+     * @param deviceDataItem 设备数据
+     * @param deviceId 设备ID
+     * @param dataEndTime 数据结束时间
+     * @param engineeringID 工程ID
+     * @param username 用户名
+     **/
+    private void sendHumidityData(Integer deviceId, LocalDateTime dataEndTime, JSONObject deviceDataItem, Long engineeringID, String username) {
+        Float value = deviceDataItem.getFloat("sd");
+        if (value == null) {
+            log.warn("设备{}的湿度数据为空", deviceId);
+            return;
+        }
+        HumidityVO humidityVO = new HumidityVO();
+        humidityVO.setDataPacketID(generateDataPacketID());
+        humidityVO.setSensorID(deviceId);
+        humidityVO.setEngineeringID(engineeringID);
+        humidityVO.setPublishTime(getCurrentTime());
+        humidityVO.setSensorValue(value);
+        humidityVO.setDataEndTime(dataEndTime);
+
+        log.info("【湿度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(humidityVO));
+        sendMqttMessage(MqttTopics.IotInfo.HUMIDITY.getTopic(), humidityVO, MqttTopics.IotInfo.HUMIDITY.getDesc(), username);
+    }
+
+    /**
+     * 推送氧气浓度信息(709)
+     *
+     * @param deviceDataItem 设备数据
+     * @param deviceId 设备ID
+     * @param dataEndTime 数据结束时间
+     * @param engineeringID 工程ID
+     * @param username 用户名
+     **/
+    private void sendOxygenData(Integer deviceId, LocalDateTime dataEndTime, JSONObject deviceDataItem, Long engineeringID, String username) {
+        Float value = deviceDataItem.getFloat("o2");
+        if (value == null) {
+            log.warn("设备{}的氧气浓度数据为空", deviceId);
+            return;
+        }
+        OxygenVO oxygenVO = new OxygenVO();
+        oxygenVO.setDataPacketID(generateDataPacketID());
+        oxygenVO.setSensorID(deviceId);
+        oxygenVO.setEngineeringID(engineeringID);
+        oxygenVO.setPublishTime(getCurrentTime());
+        oxygenVO.setSensorValue(value);
+        oxygenVO.setDataEndTime(dataEndTime);
+
+        log.info("【氧气浓度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(oxygenVO));
+        sendMqttMessage(MqttTopics.IotInfo.OXYGEN.getTopic(), oxygenVO, MqttTopics.IotInfo.OXYGEN.getDesc(), username);
+    }
+
+    /**
+     * 推送一氧化碳浓度信息(711)
+     *
+     * @param deviceDataItem 设备数据
+     * @param deviceId 设备ID
+     * @param dataEndTime 数据结束时间
+     * @param engineeringID 工程ID
+     * @param username 用户名
+     **/
+    private void sendCoData(Integer deviceId, LocalDateTime dataEndTime, JSONObject deviceDataItem, Long engineeringID, String username) {
+        Float value = deviceDataItem.getFloat("co");
+        if (value == null) {
+            log.warn("设备{}的一氧化碳浓度数据为空", deviceId);
+            return;
+        }
+        CoVO coVO = new CoVO();
+        coVO.setDataPacketID(generateDataPacketID());
+        coVO.setSensorID(deviceId);
+        coVO.setEngineeringID(engineeringID);
+        coVO.setPublishTime(getCurrentTime());
+        coVO.setSensorValue(value);
+        coVO.setDataEndTime(dataEndTime);
+
+        log.info("【一氧化碳浓度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(coVO));
+        sendMqttMessage(MqttTopics.IotInfo.CO.getTopic(), coVO, MqttTopics.IotInfo.CO.getDesc(), username);
+    }
+
+    /**
+     * 推送二氧化碳浓度信息(710)
+     *
+     * @param deviceDataItem 设备数据
+     * @param deviceId 设备ID
+     * @param dataEndTime 数据结束时间
+     * @param engineeringID 工程ID
+     * @param username 用户名
+     **/
+    private void sendCo2Data(Integer deviceId, LocalDateTime dataEndTime, JSONObject deviceDataItem, Long engineeringID, String username) {
+        Float value = deviceDataItem.getFloat("co2");
+        if (value == null) {
+            log.warn("设备{}的二氧化碳浓度数据为空", deviceId);
+            return;
+        }
+        Co2VO co2VO = new Co2VO();
+        co2VO.setDataPacketID(generateDataPacketID());
+        co2VO.setSensorID(deviceId);
+        co2VO.setEngineeringID(engineeringID);
+        co2VO.setPublishTime(getCurrentTime());
+        // 将value除以10000并保留三位小数
+        Float processedValue = new java.math.BigDecimal(value / 10000f)
+                .setScale(3, java.math.RoundingMode.HALF_UP)
+                .floatValue();
+        co2VO.setSensorValue(processedValue);
+        co2VO.setDataEndTime(dataEndTime);
+
+        log.info("【二氧化碳浓度】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(co2VO));
+        sendMqttMessage(MqttTopics.IotInfo.CO2.getTopic(), co2VO, MqttTopics.IotInfo.CO2.getDesc(), username);
+    }
+
+    /**
+     * 发送倾斜数据(712)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    // public Map<String, Integer> sendTiltData(IotDataTransferVO transferVO) {
+    //     Map<String, Integer> result = new HashMap<>();
+    //     result.put("successCount", 0);
+    //     result.put("failureCount", 0);
+    //
+    //     if (!validateMqttGateway()) {
+    //         return result;
+    //     }
+    //
+    //     try {
+    //         List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+    //         Integer deviceType = transferVO.getDeviceType();
+    //         Integer totalDevices = transferVO.getDevices().size();
+    //
+    //         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");
+    //             Integer value = deviceDataItem.getInteger("qx");
+    //             if (value == null) {
+    //                 log.warn("设备{}的倾斜数据为空", deviceId);
+    //                 result.put("failureCount", result.get("failureCount") + 1);
+    //                 continue;
+    //             }
+    //
+    //             TiltVO vo = new TiltVO();
+    //             vo.setDataPacketID(generateDataPacketID());
+    //             vo.setSensorID(deviceId);
+    //             vo.setEngineeringID(engineeringId);
+    //             vo.setPublishTime(getCurrentTime());
+    //             vo.setDataEndTime(dataEndTime);
+    //             vo.setSensorValue(value == 0 ? 0 : 1);
+    //
+    //             try {
+    //                 sendMqttMessage(EnvMonitorMqttTopic.TILT, vo, "倾斜信息");
+    //                 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"));
+    //
+    //         return result;
+    //     } catch (Exception e) {
+    //         log.error("倾斜数据推送发生异常", e);
+    //         result.put("failureCount", transferVO.getDevices().size());
+    //         return result;
+    //     }
+    // }
+
+    /**
+     * 发送裂缝数据(713)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    // public Map<String, Integer> sendCrackData(IotDataTransferVO transferVO) {
+    //     Map<String, Integer> result = new HashMap<>();
+    //     result.put("successCount", 0);
+    //     result.put("failureCount", 0);
+    //
+    //     if (!validateMqttGateway()) {
+    //         return result;
+    //     }
+    //
+    //     try {
+    //         List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+    //         Integer deviceType = transferVO.getDeviceType();
+    //         Integer totalDevices = transferVO.getDevices().size();
+    //
+    //         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");
+    //             Integer value = deviceDataItem.getInteger("cd");
+    //             if (value == null) {
+    //                 log.warn("设备{}的裂缝数据为空", deviceId);
+    //                 result.put("failureCount", result.get("failureCount") + 1);
+    //                 continue;
+    //             }
+    //
+    //             CrackVO vo = new CrackVO();
+    //             vo.setDataPacketID(generateDataPacketID());
+    //             vo.setSensorID(deviceId);
+    //             vo.setEngineeringID(engineeringId);
+    //             vo.setPublishTime(getCurrentTime());
+    //             vo.setDataEndTime(dataEndTime);
+    //             vo.setSensorValue(value == 0 ? 0 : 1);
+    //
+    //             try {
+    //                 sendMqttMessage(EnvMonitorMqttTopic.CRACK, vo, "裂缝信息");
+    //                 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"));
+    //
+    //         return result;
+    //     } catch (Exception e) {
+    //         log.error("裂缝数据推送发生异常", e);
+    //         result.put("failureCount", transferVO.getDevices().size());
+    //         return result;
+    //     }
+    // }
+
+    /**
+     * 发送位移数据(714)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendDeviationData(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;
+                }
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+                Integer value = deviceDataItem.getInteger("wy");
+                if (value == null) {
+                    log.warn("设备{}的位移数据为空", deviceId);
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+
+                DeviationVO vo = new DeviationVO();
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setSensorID(deviceId);
+                vo.setEngineeringID(engineeringId);
+                vo.setPublishTime(getCurrentTime());
+                vo.setDataEndTime(dataEndTime);
+                vo.setSensorValue(value == 0 ? 0 : 1);
+
+                try {
+                    log.info("【位移数据】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
+                    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;
+        }
+    }
+
+    /**
+     * 发送液位数据(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;
+                }
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                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 {
+                    log.info("【水位数据】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
+                    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
+     * @param engineeringId 工程ID
+     * @param username MQTT用户名
+     * @param password MQTT密码
+     */
+    public void synchronizeDeviceData(Integer tenantId, Long engineeringId, String username, String password) {
+        log.info("用户名:{},密码:{}", username, password);
+        // 参数校验
+        if (engineeringId == null || username == null || password == null) {
+            log.error("工程ID、MQTT用户名或密码不能为空");
+            return;
+        }
+
+        // 查询租户下的所有产品类型
+        List<String> deviceTypeList = getDeviceTypeListByTenant(tenantId);
+        if (deviceTypeList.isEmpty()) {
+            log.warn("租户{}不存在任何产品", tenantId);
+            return;
+        }
+
+        // 查询设备列表
+        List<DmpDevice> deviceList = getDeviceListByType(deviceTypeList);
+        if (deviceList.isEmpty()) {
+            log.warn("租户{}不存在任何设备", tenantId);
+            return;
+        }
+
+        // 按设备类型分组
+        Map<Integer, List<DmpDevice>> deviceTypeMap = deviceList.stream()
+                .collect(Collectors.groupingBy(DmpDevice::getDeviceType));
+
+        // 构建数据传输对象列表
+        List<IotDataTransferVO> transferList = new ArrayList<>();
+        deviceTypeMap.forEach((deviceType, devices) -> {
+            IotDataTransferVO transferVO = new IotDataTransferVO();
+            transferVO.setDeviceType(deviceType);
+            transferVO.setDevices(devices);
+            transferVO.setEngineeringId(engineeringId);
+            transferVO.setUsername(username); // 保存当前任务的用户名
+            transferVO.setTenantId(tenantId);
+            transferList.add(transferVO);
+        });
+
+        // 按产品代码分组,构建ProductCode到uuid列表的映射,过滤掉产品代码为null的设备
+        Map<String, List<String>> codeDeviceUuidsMap = deviceList.stream()
+                .filter(device -> device.getProductCode() != null)
+                .collect(Collectors.groupingBy(DmpDevice::getProductCode,
+                        Collectors.mapping(DmpDevice::getDeviceUuid, Collectors.toList())));
+
+        // 创建MQTT连接
+        createMqttConnection(username, password);
+
+        // 任务开始日志
+        Integer totalDevices = deviceList.size();
+        Integer totalProductTypes = codeDeviceUuidsMap.size();
+        log.info("设备数据同步任务开始,租户ID:{},工程ID:{}", tenantId, engineeringId);
+        log.info("总共涉及产品类型数:{}个,产品代码为:{}", totalProductTypes, codeDeviceUuidsMap.keySet());
+        log.info("总共需要推送设备数量:{}个,涉及设备类型数:{}个,设备类型为:{}",
+                totalDevices, deviceTypeMap.size(), deviceTypeMap.keySet());
+
+        // 记录每种设备类型的设备数量
+        deviceTypeMap.forEach((deviceType, devices) -> {
+            log.info("设备类型:{},设备数量:{}", deviceType, devices.size());
+        });
+
+        // 按设备类型处理数据同步
+        int totalSuccessCount = 0;
+        int totalFailureCount = 0;
+
+        for (IotDataTransferVO transferVO : transferList) {
+            Integer deviceType = transferVO.getDeviceType();
+            Map<String, Integer> result = new HashMap<>();
+
+            switch (deviceType) {
+                case 707:
+                case 708:
+                case 709:
+                case 710:
+                case 711:
+                    result = sendEnvData(transferVO);
+                    break;
+                case 702:
+                    result = sendWaterLeak(transferVO);
+                    break;
+                case 703:
+                    if (transferVO.getEngineeringId() == 3101100024L) {
+                        result = sendPersonCount(transferVO);
+                        break;
+                    }
+                    result = sendPersonPresence(transferVO);
+                    break;
+                case 704:
+                    if (tenantId == 1205) {
+                        // 设置默认值,避免空指针
+                        result.put("successCount", 0);
+                        result.put("failureCount", 0);
+                        break;
+                    }
+                    result = sendElectricityLoad(transferVO);
+                    break;
+                // case 712:
+                //     result = sendTiltData(transferVO);
+                //     break;
+                // case 713:
+                //     result = sendCrackData(transferVO);
+                //     break;
+                case 714:
+                    result = sendDeviationData(transferVO);
+                    break;
+                case 716:
+                    result = sendWaterLevel(transferVO);
+                    break;
+                case 719:
+                    result = sendPersonCount(transferVO);
+                    break;
+                default:
+                    log.debug("不支持的设备类型:{}", deviceType);
+                    continue;
+            }
+
+            // 累加成功数和失败数
+            int typeSuccess = result.getOrDefault("successCount", 0);
+            int typeFailure = result.getOrDefault("failureCount", 0);
+            totalSuccessCount += typeSuccess;
+            totalFailureCount += typeFailure;
+
+            // 分类型诊断日志:精确定位每种类型的成功/失败/未同步
+            int typeTotal = transferVO.getDevices() != null ? transferVO.getDevices().size() : 0;
+            int typeNotSynced = typeTotal - typeSuccess - typeFailure;
+            log.info("[类型分诊] deviceType={} | 总设备={} | 成功={} | 失败={} | 未同步={}",
+                    deviceType, typeTotal, typeSuccess, typeFailure, typeNotSynced);
+        }
+
+        // 任务完成总结
+        log.info("设备数据同步任务完成,租户ID:{},工程ID:{}", tenantId, engineeringId);
+        log.info("总共涉及产品类型数:{}个,产品代码为:{}", totalProductTypes, codeDeviceUuidsMap.keySet());
+        log.info("总共推送设备数量:{}个,成功:{}个,失败:{}个",
+                totalDevices, totalSuccessCount, totalFailureCount);
+
+    }
+
+    /**
+     * 查询租户下的设备类型列表
+     * @param tenantId 租户ID
+     * @return 设备类型列表
+     */
+    private List<String> getDeviceTypeListByTenant(Integer tenantId) {
+        LambdaQueryWrapper<DmpProduct> productQueryWrapper = new LambdaQueryWrapper<>();
+        productQueryWrapper.select(DmpProduct::getProductCode)
+                .eq(tenantId != null && tenantId > 0, DmpProduct::getTenantId, tenantId)
+                .eq(DmpProduct::getDeleteFlag, 0);
+        List<DmpProduct> productList = dmpProductMapper.selectList(productQueryWrapper);
+        return productList.stream()
+                .map(DmpProduct::getProductCode)
+                .distinct() // 去重
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 根据设备类型列表查询设备
+     * @param productCodeList 设备类型列表
+     * @return 设备列表
+     */
+    private List<DmpDevice> getDeviceListByType(List<String> productCodeList) {
+        LambdaQueryWrapper<DmpDevice> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.select(DmpDevice::getDeviceUuid, DmpDevice::getDeviceType, DmpDevice::getDeviceId, DmpDevice::getProductCode)
+                .in(DmpDevice::getProductCode, productCodeList)
+                .eq(DmpDevice::getDeleteFlag, 0)
+                .notIn(DmpDevice::getServiceStatus, 3)
+                .orderByAsc(DmpDevice::getProductCode);
+        return dmpDeviceMapper.selectList(queryWrapper);
+    }
+
+    /**
+     * 手动创建/刷新 MQTT 连接(含动态 clientId)
+     * @param username MQTT用户名
+     * @param password MQTT密码
+     */
+    public synchronized void createMqttConnection(String username, String password) {
+        log.info("手动创建/刷新 MQTT 连接(含动态 clientId),用户名:{},密码:{}", username, password);
+        try {
+            // 检查MqttConnectionTool是否已注入
+            if (this.mqttConnectionTool == null) {
+                throw new IllegalStateException("MqttConnectionTool未注入,无法获取MQTT Gateway");
+            }
+
+            // 使用MqttConnectionTool创建或刷新MQTT连接
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(username, password);
+
+            // 存储到映射中
+            mqttGatewayMap.put(username, gateway);
+            log.info("MQTT连接创建/刷新成功,用户名:{}", username);
+        } catch (Exception e) {
+            log.error("初始化MQTT连接失败: {}", e.getMessage(), e);
+            throw new RuntimeException("初始化MQTT连接失败", e);
+        }
+    }
+
+    /**
+     * 验证MQTT网关是否初始化
+     * @param username 用户名
+     * @return 是否初始化
+     */
+    private boolean validateMqttGateway(String username) {
+        if (username == null) {
+            log.warn("MQTT Gateway未初始化,无法发送消息,用户名:null");
+            return false;
+        }
+        // 一次性获取网关实例,避免竞态条件
+        MqttConnectionTool.MqttGateway gateway = mqttGatewayMap.get(username);
+        if (gateway == null) {
+            log.warn("MQTT Gateway未初始化,无法发送消息,用户名:{}", username);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 解析数据时间
+     * @param deviceDataItem 设备数据
+     * @return 解析后的时间,如果解析失败返回null
+     */
+    private LocalDateTime parseDataTime(JSONObject deviceDataItem) {
+        Object raw = deviceDataItem.get("realtime");
+        Long dataTime = null;
+        if (raw instanceof Long) {
+            dataTime = (Long) raw;
+        } else if (raw instanceof Integer) {
+            dataTime = ((Integer) raw).longValue();
+        } else if (raw instanceof Number) {
+            dataTime = ((Number) raw).longValue();
+        } else if (raw instanceof String) {
+            try {
+                dataTime = Long.parseLong((String) raw);
+            } catch (Exception ignored) {
+            }
+        }
+        if (dataTime == null) return null;
+        return LocalDateTime.ofInstant(Instant.ofEpochMilli(dataTime), ZoneId.systemDefault());
+    }
+
+    /**
+     * 发送MQTT消息
+     * @param topic MQTT topic
+     * @param vo 消息对象
+     * @param messageType 消息类型描述
+     * @param username 用户名
+     */
+    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);
+        } else {
+            log.warn("MQTT Gateway未找到,无法发送消息,用户名:{}", username);
+        }
+    }
+
+    /**
+     * 序列化楼层平面图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());
+        }
+
+        Gson gson = new Gson();
+        return gson.toJson(jsonObject);
+    }
+
+    public void allData(Long engineeringId, String username, String password) {
+        Integer tenantId = 0;
+        synchronizeDeviceData(tenantId, engineeringId, username, password);
+    }
+
+    /**
+     * 为监测时间添加随机偏移(用于模拟真实采集场景)
+     * @param originalTime 原始监测时间
+     * @return 偏移后的时间
+     */
+    private LocalDateTime addTimeOffset(LocalDateTime originalTime) {
+        if (originalTime == null) {
+            return null;
+        }
+
+        // 获取当前时间作为上限
+        LocalDateTime now = LocalDateTime.now();
+
+        // 如果原始时间已经是未来时间,先修正为当前时间
+        if (originalTime.isAfter(now)) {
+            log.warn("检测到未来时间的监测数据,已修正为当前时间:{}", originalTime);
+            originalTime = now;
+        }
+
+        // 随机秒偏移:0、1、2 秒(向前偏移,使用减法)
+        int secondsOffset = ThreadLocalRandom.current().nextInt(0, 3);
+        // 随机毫秒偏移:0 ~ 999 毫秒(向前偏移,使用减法)
+        int millisOffset = ThreadLocalRandom.current().nextInt(0, 1000);
+
+        LocalDateTime offsetTime = originalTime.minusSeconds(secondsOffset)
+                .minusNanos(millisOffset * 1_000_000L);
+
+        // 最终校验:确保不会晚于当前时间
+        if (offsetTime.isAfter(now)) {
+            log.warn("偏移后时间仍然晚于当前时间,已修正:原始={}, 偏移后={}, 当前={}",
+                    originalTime, offsetTime, now);
+            return now;
+        }
+
+        return offsetTime;
+    }
+}

+ 20 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/SysUserServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.cdi.service.impl;
+
+import com.usky.cdi.domain.SysUser;
+import com.usky.cdi.mapper.SysUserMapper;
+import com.usky.cdi.service.SysUserService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 用户信息表 服务实现类
+ * </p>
+ *
+ * @author fu
+ * @since 2026-02-04
+ */
+@Service
+public class SysUserServiceImpl extends AbstractCrudService<SysUserMapper, SysUser> implements SysUserService {
+
+}

+ 61 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/listener/MqttListener.java

@@ -0,0 +1,61 @@
+package com.usky.cdi.service.listener;
+
+import com.usky.cdi.service.config.mqtt.MqttInConfig;
+import com.usky.cdi.service.mqtt.SimpleContext;
+import com.usky.cdi.service.vo.MqttBaseVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.integration.annotation.ServiceActivator;
+import org.springframework.messaging.MessageHandler;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author han
+ * @date 2025/03/20 14:41
+ */
+@ConditionalOnProperty(prefix = "mqtt", value = {"enabled"}, havingValue = "true")
+@Slf4j
+@Component
+public class MqttListener {
+    public static final String MESSAGE_NAME = "messageInput";
+
+    @Autowired
+    private SimpleContext simpleContext;
+
+    /**
+     * 处理消息-消费者
+     *
+     * @return
+     */
+    @Bean(MESSAGE_NAME)
+    @ServiceActivator(inputChannel = MqttInConfig.CHANNEL_NAME_INPUT)
+    public MessageHandler handler() {
+        return message -> {
+            try {
+                String payload = message.getPayload().toString();
+                //进行接口推送
+                Object mqttReceivedTopic = message.getHeaders().get("mqtt_receivedTopic");
+                if (null != mqttReceivedTopic) {
+                    String topic = mqttReceivedTopic.toString();
+                    if (topic != null) {
+                        MqttBaseVO mqttBaseVO = new MqttBaseVO();
+                        mqttBaseVO.setTopic(topic);
+                        if (topic.indexOf("info") != -1) {
+                            mqttBaseVO.setDescribe("info");
+                            mqttBaseVO.setData(payload);
+                        } else if (topic.indexOf("event") != -1) {
+                            mqttBaseVO.setDescribe("event");
+                            mqttBaseVO.setData(payload);
+                        }
+                        //统一处理数据
+                        simpleContext.getResource(mqttBaseVO);
+                    }
+                }
+            } catch (Exception e) {
+                log.error("处理MQTT消息时发生错误", e);
+            }
+        };
+    }
+}

+ 227 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/MqttConnectionTool.java

@@ -0,0 +1,227 @@
+package com.usky.cdi.service.mqtt;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
+import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
+import org.springframework.integration.mqtt.support.MqttHeaders;
+import org.springframework.integration.annotation.MessagingGateway;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2025/12/22
+ * 动态 MQTT 连接工具类
+ * 用法:注入后调用 connectOrRefresh(...) 即可
+ *
+ */
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class MqttConnectionTool {
+
+    private final GenericApplicationContext context;
+
+    /* 默认 topic,可外部再 set */
+    private String defaultTopic = "testTopic";
+
+    /* 默认 keep-alive,可外部再 set */
+    private int keepAlive = 60;
+
+    private static final String MQTT_URL = "ssl://114.80.201.143:8883";
+
+    /**
+     * 存储每个用户名对应的连接信息
+     */
+    private final Map<String, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
+
+    /**
+     * 连接信息内部类
+     */
+    private static class ConnectionInfo {
+        private final String handlerBeanName;
+        private final String consumerBeanName;
+        private final String factoryBeanName;
+        private final String gatewayBeanName;
+
+        public ConnectionInfo(String username) {
+            this.handlerBeanName = "mqttHandler_" + username;
+            this.consumerBeanName = "mqttConsumer_" + username;
+            this.factoryBeanName = "mqttFactory_" + username;
+            this.gatewayBeanName = "mqttGateway_" + username;
+        }
+    }
+
+    /**
+     * 一键创建/刷新连接
+     *
+     * @param username  用户名
+     * @param password  密码
+     * @return 可直接发消息的 MqttGateway
+     */
+    public synchronized MqttGateway connectOrRefresh(String username, String password) {
+        Assert.notNull(username, "username cannot be null");
+        Assert.notNull(password, "password cannot be null");
+
+        String clientId = "mqttx-" + username;
+        try {
+            /* 1. 获取或创建连接信息 */
+            ConnectionInfo connectionInfo = connectionMap.computeIfAbsent(username, ConnectionInfo::new);
+
+            /* 2. 创建或更新专属工厂 */
+            DefaultMqttPahoClientFactory factory;
+            if (context.containsBean(connectionInfo.factoryBeanName)) {
+                factory = context.getBean(connectionInfo.factoryBeanName, DefaultMqttPahoClientFactory.class);
+                factory.setConnectionOptions(buildOptions(username, password, MQTT_URL));
+                log.info("已更新 MQTT 客户端工厂 -> {}", connectionInfo.factoryBeanName);
+            } else {
+                factory = new DefaultMqttPahoClientFactory();
+                factory.setConnectionOptions(buildOptions(username, password, MQTT_URL));
+                context.registerBean(connectionInfo.factoryBeanName, DefaultMqttPahoClientFactory.class, () -> factory);
+                log.info("已创建 MQTT 客户端工厂 -> {}", connectionInfo.factoryBeanName);
+            }
+
+            /* 3. 移除旧的 Handler 和 Consumer */
+            removeOldConnection(connectionInfo);
+
+            /* 4. 创建新的 Handler */
+            MqttPahoMessageHandler handler = new MqttPahoMessageHandler(clientId, factory);
+            handler.setAsync(true);
+            handler.setDefaultTopic(defaultTopic);
+            handler.afterPropertiesSet();
+
+            /* 5. 注册新的 Handler */
+            context.registerBean(connectionInfo.handlerBeanName, MqttPahoMessageHandler.class, () -> handler);
+
+            /* 6. 创建并注册新的专属网关 */
+            // 创建一个简单的Gateway实现,直接使用Handler发送消息
+            MqttGateway gateway = new MqttGateway() {
+                @Override
+                public void sendToMqtt(String payload) {
+                    try {
+                        handler.handleMessage(org.springframework.messaging.support.MessageBuilder
+                                .withPayload(payload)
+                                .setHeader(MqttHeaders.TOPIC, defaultTopic)
+                                .build());
+                    } catch (Exception e) {
+                        log.error("发送MQTT消息失败: {}", e.getMessage(), e);
+                        throw new RuntimeException("发送MQTT消息失败", e);
+                    }
+                }
+
+                @Override
+                public void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload) {
+                    try {
+                        handler.handleMessage(org.springframework.messaging.support.MessageBuilder
+                                .withPayload(payload)
+                                .setHeader(MqttHeaders.TOPIC, topic)
+                                .build());
+                    } catch (Exception e) {
+                        log.error("发送MQTT消息失败: {}", e.getMessage(), e);
+                        throw new RuntimeException("发送MQTT消息失败", e);
+                    }
+                }
+
+                @Override
+                public void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
+                                       @Header(MqttHeaders.QOS) int qos, String payload) {
+                    try {
+                        handler.handleMessage(org.springframework.messaging.support.MessageBuilder
+                                .withPayload(payload)
+                                .setHeader(MqttHeaders.TOPIC, topic)
+                                .setHeader(MqttHeaders.QOS, qos)
+                                .build());
+                    } catch (Exception e) {
+                        log.error("发送MQTT消息失败: {}", e.getMessage(), e);
+                        throw new RuntimeException("发送MQTT消息失败", e);
+                    }
+                }
+            };
+
+            log.info("MQTT 连接刷新完成 -> {} / {}", username, clientId);
+            return gateway;
+        } catch (Exception e) {
+            log.error("MQTT 连接失败 -> {}", clientId, e);
+            throw new RuntimeException("MQTT 连接失败", e);
+        }
+    }
+
+    /* ---------- 私有辅助 ---------- */
+
+    private org.eclipse.paho.client.mqttv3.MqttConnectOptions
+    buildOptions(String u, String p, String url) {
+        org.eclipse.paho.client.mqttv3.MqttConnectOptions opt =
+                new org.eclipse.paho.client.mqttv3.MqttConnectOptions();
+        opt.setServerURIs(new String[]{url});
+        opt.setUserName(u);
+        if (p != null) opt.setPassword(p.toCharArray());
+        opt.setKeepAliveInterval(keepAlive);
+        return opt;
+    }
+
+    /**
+     * 移除旧的连接实例
+     * @param connectionInfo 连接信息
+     */
+    private void removeOldConnection(ConnectionInfo connectionInfo) {
+        // 移除旧的 Handler
+        if (context.containsBeanDefinition(connectionInfo.handlerBeanName)) {
+            try {
+                MqttPahoMessageHandler oldHandler = context.getBean(connectionInfo.handlerBeanName, MqttPahoMessageHandler.class);
+                oldHandler.stop();
+            } catch (Exception e) {
+                log.warn("停止旧的MQTT处理器时出错: {}", e.getMessage(), e);
+            }
+            context.removeBeanDefinition(connectionInfo.handlerBeanName);
+            log.info("已移除旧的 MQTT 处理器 -> {}", connectionInfo.handlerBeanName);
+        }
+
+        // 从单例缓存中移除旧的 Handler
+        if (context.getDefaultListableBeanFactory().containsSingleton(connectionInfo.handlerBeanName)) {
+            context.getDefaultListableBeanFactory().destroySingleton(connectionInfo.handlerBeanName);
+        }
+
+        // 移除旧的 Factory
+        if (context.containsBeanDefinition(connectionInfo.factoryBeanName)) {
+            context.removeBeanDefinition(connectionInfo.factoryBeanName);
+            log.info("已移除旧的 MQTT 工厂 -> {}", connectionInfo.factoryBeanName);
+        }
+
+        // 从单例缓存中移除旧的 Factory
+        if (context.getDefaultListableBeanFactory().containsSingleton(connectionInfo.factoryBeanName)) {
+            context.getDefaultListableBeanFactory().destroySingleton(connectionInfo.factoryBeanName);
+        }
+    }
+
+    /* ---------- 对外可调用的 setter ---------- */
+
+    public MqttConnectionTool defaultTopic(String topic) {
+        this.defaultTopic = topic;
+        return this;
+    }
+
+    public MqttConnectionTool keepAlive(int keepAlive) {
+        this.keepAlive = keepAlive;
+        return this;
+    }
+
+    /* ---------- 复用原来的 Gateway 接口 ---------- */
+    @MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
+    public interface MqttGateway {
+        void sendToMqtt(String payload);
+
+        void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload);
+
+        void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic,
+                        @Header(MqttHeaders.QOS) int qos, String payload);
+    }
+}

+ 13 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/MqttStrategy.java

@@ -0,0 +1,13 @@
+package com.usky.cdi.service.mqtt;
+
+import com.usky.cdi.service.vo.MqttBaseVO;
+
+public interface MqttStrategy {
+    /**
+     * 处理消息(策略模式由子类实现)
+     *
+     * @param mqttBaseVO
+     * @return
+     */
+    String disposeMessage(MqttBaseVO mqttBaseVO);
+}

+ 35 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/SimpleContext.java

@@ -0,0 +1,35 @@
+package com.usky.cdi.service.mqtt;
+
+import com.usky.cdi.service.vo.MqttBaseVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 中间处理消息转发
+ */
+@Service
+public class SimpleContext {
+    private final Map<String, MqttStrategy> strategyMap;
+
+    @Autowired
+    public SimpleContext(Map<String, MqttStrategy> strategyMap) {
+        this.strategyMap = new ConcurrentHashMap<>();
+        if (strategyMap != null) {
+            strategyMap.forEach(this.strategyMap::put);
+        }
+    }
+
+    public String getResource(MqttBaseVO mqttBaseVO) {
+        if (mqttBaseVO == null || mqttBaseVO.getDescribe() == null) {
+            return null;
+        }
+        MqttStrategy strategy = strategyMap.get(mqttBaseVO.getDescribe());
+        if (strategy == null) {
+            return null;
+        }
+        return strategy.disposeMessage(mqttBaseVO);
+    }
+}

+ 29 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/event/event.java

@@ -0,0 +1,29 @@
+package com.usky.cdi.service.mqtt.event;
+
+import com.alibaba.fastjson.JSONObject;
+import com.usky.cdi.service.mqtt.MqttStrategy;
+import com.usky.cdi.service.vo.MqttBaseVO;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author han
+ * @date 2025/03/20
+ */
+@Service("event")
+public class event implements MqttStrategy {
+
+    //处理下发命令响应消息
+    public String disposeMessage(MqttBaseVO mqttBaseVO) {
+
+        try {
+            JSONObject eventVO1 = JSONObject.parseObject(mqttBaseVO.getData().toString());
+            JSONObject eventVO = new JSONObject();
+            JSONObject eventVO2 = new JSONObject();
+            System.out.println("FEventReceiver消费者收到消息: " + mqttBaseVO.getData().toString());
+        } catch (Exception e){
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+}

+ 31 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/mqtt/info/Info.java

@@ -0,0 +1,31 @@
+package com.usky.cdi.service.mqtt.info;
+
+import com.alibaba.fastjson.JSONObject;
+import com.usky.cdi.service.mqtt.MqttStrategy;
+import com.usky.cdi.service.vo.MqttBaseVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author han
+ * @date 2025/03/20 17:56
+ */
+@Slf4j
+@Service("info")
+public class Info implements MqttStrategy {
+
+    public String disposeMessage(MqttBaseVO mqttBaseVO) {
+        if (mqttBaseVO == null || mqttBaseVO.getData() == null) {
+            log.warn("MqttBaseVO或数据为空");
+            return null;
+        }
+        try {
+            JSONObject mapData = JSONObject.parseObject(mqttBaseVO.getData().toString());
+            log.info("FInfoReceiver消费者收到消息: {}", mqttBaseVO.getData().toString());
+        } catch (Exception e) {
+            log.error("处理Info消息时发生错误", e);
+        }
+
+        return null;
+    }
+}

+ 381 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/AirDefenseSimulator.java

@@ -0,0 +1,381 @@
+package com.usky.cdi.service.util;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * 人防掩蔽单元人数模拟工具类(动态自适应版)
+ * <p>
+ * 核心特性:
+ * 1. 即时计算:根据当前时间动态返回合理总人数,无预生成/硬编码条数限制
+ * 2. 自适应调用管理器:跟踪调用频率,动态评估并自动扩容阈值
+ * 3. 分时段基准曲线:每时段独立配置基准人数与波动范围,支持平滑插值
+ * 4. 边界安全:防负数、防无限循环、防溢出
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/5/30
+ */
+public class AirDefenseSimulator {
+
+    // ===================== 可配置常量 =====================
+    private static final int MIN_PEOPLE = 0;                    // 最小人数(禁止负数)
+    private static final int DEFAULT_MAX_CALLS = 48;            // 默认最大调用次数(向下兼容)
+    private static final int MAX_EXPANSION_LIMIT = 288;         // 扩容上限(=24h*12次/5min,即5分钟粒度全天)
+    private static final double EXPANSION_FACTOR = 2.0;         // 每次扩容倍数
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");
+
+    // ===================== 自适应调用管理器(单例状态) =====================
+    /** 已调用次数计数器 */
+    private static final AtomicInteger callCount = new AtomicInteger(0);
+    /** 当前动态阈值(初始为默认值,可自动扩容) */
+    private static final AtomicInteger dynamicThreshold = new AtomicInteger(DEFAULT_MAX_CALLS);
+    /** 上次调用时间戳(用于检测异常高频调用) */
+    private static final AtomicLong lastCallTimestamp = new AtomicLong(0);
+    /** 是否已触发过扩容标志 */
+    private static volatile boolean expanded = false;
+
+    // ===================== 时段规则定义(基准人数 + 波动范围)=====================
+    /**
+     * 时段规则:[起始小时, 结束小时) → {基准人数, 最小波动, 最大波动}
+     * 基准人数代表该时段中心时刻的典型值,波动范围为±随机偏移量
+     */
+    private static final int[][] TIME_SLOT_RULES = {
+        //  起始H   结束H   基准   波动Min  波动Max   规则说明
+            {0,      5,      0,     0,       0},      // 规则1: 00:00-05:00  人数为0
+            {5,      6,      3,     2,       3},      // 规则2: 05:00-06:00  缓慢上升
+            {6,      8,      12,    4,       6},      // 规则3: 06:00-08:00  攀升至第一高点
+            {8,      9,      25,    2,       5},      // 规则3延:08:00-09:00 第一高点后短暂调整
+            {9,      11,     31,    2,       5},      // 规则4: 09:00-11:00  趋于稳定
+            {11,     12,     19,    1,       3},      // 规则5: 11:00-12:00  大量流出(就餐)
+            {12,     13,     12,    1,       3},      // 规则5延:12:00-13:00 午间低谷
+            {13,     14,     21,    3,       6},      // 规则6: 13:00-14:00  开始回流
+            {14,     15,     30,    4,       7},      // 规则7: 14:00-15:00  达到第二高点
+            {15,     19,     31,    2,       5},      // 规则8: 15:00-19:00  保持稳定
+            {19,     21,     18,    3,       6},      // 规则9: 19:00-21:00  持续流出
+            {21,     22,     12,    2,       5},      // 规则9延:21:00-22:00  继续清场
+            {22,     24,     6,     1,       3}       // 规则10:22:00-24:00  逐渐降至0
+    };
+
+    // ===================== 公开API:获取当前时刻掩蔽单元内的总人数 =====================
+
+    /**
+     * 【核心方法】根据当前系统时间,动态计算并返回掩蔽单元内应有的模拟总人数。
+     * <p>
+     * 无需预生成数据,即时根据24小时时间规则曲线计算出合理人数,
+     * 支持任意时间粒度的连续调用,通过自适应管理器自动扩容保护。
+     *
+     * @return 当前时刻的模拟人数(int, >= 0)
+     */
+    public static int getCurrentPeopleCount() {
+        return calculatePeopleCount(LocalDateTime.now());
+    }
+
+    /**
+     * 【核心方法-指定时间版本】根据指定时间,动态计算掩蔽单元内应有的模拟总人数。
+     *
+     * @param targetTime 目标时间
+     * @return 该时刻的模拟人数(int, >= 0)
+     */
+    public static int calculatePeopleCount(LocalDateTime targetTime) {
+        // 1. 自适应检查:是否需要扩容
+        adaptiveCheck();
+
+        int hour = targetTime.getHour();
+        int minute = targetTime.getMinute();
+
+        // 2. 匹配对应时段规则
+        int[] rule = matchTimeRule(hour);
+        int baseCount = rule[2];
+        int fluctuationMin = rule[3];
+        int fluctuationMax = rule[4];
+
+        // 3. 时段内线性插值:使同一小时内不同分钟的人数更平滑自然
+        // 特殊处理:基准为0的时段(如00:00-05:00无人期)不参与插值,强制保持0
+        double minuteRatio = minute / 60.0;
+        int interpolatedBase = (baseCount == 0) ? 0 : interpolateWithinHour(hour, baseCount, minuteRatio);
+
+        // 4. 叠加随机波动
+        int fluctuation = ThreadLocalRandom.current().nextInt(fluctuationMin, fluctuationMax + 1);
+        boolean positiveTrend = isInRisingPhase(hour);
+        int peopleCount = interpolatedBase + (positiveTrend ? fluctuation : -fluctuation);
+
+        // 5. 边界约束
+        peopleCount = Math.max(peopleCount, MIN_PEOPLE);
+
+        // 6. 记录调用
+        recordCall();
+
+        return peopleCount / 4;
+    }
+
+    // ===================== 兼容接口:生成全天模拟数据列表(动态条数)=====================
+
+    /**
+     * 生成当天全天的模拟数据列表。
+     * <p>
+     * 条数由自适应管理器的当前阈值决定(默认48条/半小时粒度),
+     * 当业务需求更高频时可自动扩容至更细粒度(如288条/5分钟粒度)。
+     *
+     * @return 模拟数据列表
+     */
+    public static List<PeopleFlowData> generateAllDayData() {
+        return generateAllDayData(dynamicThreshold.get());
+    }
+
+    /**
+     * 生成全天模拟数据列表,自定义数据条数。
+     * <p>
+     * 动态扩容策略:
+     * - requestedCount <= DEFAULT_MAX_CALLS(48): 正常生成,半小时粒度
+     * - 48 < requestedCount <= MAX_EXPANSION_LIMIT(288): 自动扩容,最小粒度5分钟
+     * - requestedCount > 288: 截断到上限,防止内存溢出
+     *
+     * @param requestedCount 请求数据条数
+     * @return 模拟数据列表(实际条数可能因边界截断而小于请求值)
+     */
+    public static List<PeopleFlowData> generateAllDayData(int requestedCount) {
+        // 边界约束:防止非法参数导致问题
+        int actualCount = normalizeRequestedCount(requestedCount);
+
+        List<PeopleFlowData> dataList = new ArrayList<>(actualCount);
+        long totalMinutes = 24 * 60;
+        long intervalMinutes = totalMinutes / actualCount;
+
+        LocalDateTime currentTime = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0).withNano(0);
+        int lastPeople = 0;
+
+        for (int i = 0; i < actualCount; i++) {
+            int currentPeople = calculatePeopleCount(currentTime);
+
+            // 反推流入流出(保持数据完整性)
+            int delta = currentPeople - lastPeople;
+            int inFlow, outFlow;
+            if (delta >= 0) {
+                inFlow = delta + ThreadLocalRandom.current().nextInt(0, 2);
+                outFlow = ThreadLocalRandom.current().nextInt(0, 2);
+            } else {
+                inFlow = ThreadLocalRandom.current().nextInt(0, 2);
+                outFlow = -delta + ThreadLocalRandom.current().nextInt(0, 2);
+            }
+
+            String timeStr = currentTime.format(TIME_FORMATTER);
+            dataList.add(new PeopleFlowData(timeStr, lastPeople, inFlow, outFlow, currentPeople));
+
+            lastPeople = currentPeople;
+            currentTime = currentTime.plusMinutes(intervalMinutes);
+        }
+        return dataList;
+    }
+
+    // ===================== 自适应调用管理器 =====================
+
+    /**
+     * 自适应检查:在每次调用前评估是否需要触发扩容
+     */
+    private static void adaptiveCheck() {
+        int calls = callCount.incrementAndGet();
+        int threshold = dynamicThreshold.get();
+
+        if (calls > threshold && !expanded) {
+            synchronized (AirDefenseSimulator.class) {
+                // 双检锁,防止并发重复扩容
+                if (calls > dynamicThreshold.get() && !expanded) {
+                    int newThreshold = expandThreshold(threshold);
+                    dynamicThreshold.set(newThreshold);
+                    expanded = true;
+                }
+            }
+        }
+    }
+
+    /**
+     * 动态扩容策略:按指数因子扩展阈值,但不超过硬性上限
+     *
+     * @param currentThreshold 当前阈值
+     * @return 扩容后的新阈值
+     */
+    private static int expandThreshold(int currentThreshold) {
+        int newThreshold = (int) Math.min(currentThreshold * EXPANSION_FACTOR, MAX_EXPANSION_LIMIT);
+        return Math.max(newThreshold, currentThreshold + 1); // 至少+1保证递增
+    }
+
+    /**
+     * 记录一次有效调用
+     */
+    private static void recordCall() {
+        lastCallTimestamp.set(System.currentTimeMillis());
+    }
+
+    /**
+     * 归置自适应管理器状态(测试/重启场景使用)
+     */
+    public static void resetAdaptiveManager() {
+        callCount.set(0);
+        dynamicThreshold.set(DEFAULT_MAX_CALLS);
+        lastCallTimestamp.set(0);
+        expanded = false;
+    }
+
+    // ===================== 管理器状态查询API =====================
+
+    /** 获取已调用次数 */
+    public static int getCallCount() { return callCount.get(); }
+
+    /** 获取当前动态阈值 */
+    public static int getDynamicThreshold() { return dynamicThreshold.get(); }
+
+    /** 获取扩容上限 */
+    public static int getMaxExpansionLimit() { return MAX_EXPANSION_LIMIT; }
+
+    /** 是否已触发过扩容 */
+    public static boolean isExpanded() { return expanded; }
+
+    // ===================== 私有工具方法 =====================
+
+    /**
+     * 根据小时数匹配对应的时段规则
+     */
+    private static int[] matchTimeRule(int hour) {
+        for (int[] rule : TIME_SLOT_RULES) {
+            if (hour >= rule[0] && hour < rule[1]) {
+                return rule;
+            }
+        }
+        // 兜底:返回午夜规则
+        return TIME_SLOT_RULES[0];
+    }
+
+    /**
+     * 判断当前时段是否处于人数上升期(用于决定波动的正负方向)
+     */
+    private static boolean isInRisingPhase(int hour) {
+        return (hour >= 5 && hour < 9) || (hour >= 13 && hour < 15);
+    }
+
+    /**
+     * 时段内线性插值:根据分钟位置微调基准值,实现同小时内平滑过渡
+     * <p>
+     * 例如:6:00基准15人→8:00基准30人,6:30应约为22人而非突变为30人
+     */
+    private static int interpolateWithinHour(int hour, int baseCount, double minuteRatio) {
+        int[] currentRule = matchTimeRule(hour);
+        int nextHour = (hour + 1) % 24;
+        int[] nextRule = matchTimeRule(nextHour);
+        int nextBase = nextRule[2];
+
+        // 线性插值:当前基准 + (下一基准-当前基准) * 分钟占比
+        int interpolated = (int) Math.round(baseCount + (nextBase - baseCount) * minuteRatio * 0.3);
+        return Math.max(interpolated, MIN_PEOPLE);
+    }
+
+    /**
+     * 规范化请求数据条数:边界条件处理,防止越界
+     */
+    private static int normalizeRequestedCount(int requestedCount) {
+        if (requestedCount <= 0) {
+            return DEFAULT_MAX_CALLS; // 非法值回退默认
+        }
+        if (requestedCount > MAX_EXPANSION_LIMIT) {
+            return MAX_EXPANSION_LIMIT; // 截断到上限
+        }
+        return requestedCount;
+    }
+
+    // ===================== 测试主方法 =====================
+    public static void main(String[] args) {
+        System.out.println("========== 自适应人防掩蔽单元人数模拟器 ==========");
+        System.out.println("初始阈值: " + DEFAULT_MAX_CALLS + ", 上限: " + MAX_EXPANSION_LIMIT);
+
+        // ========== 核心:60次模拟调用测试 ==========
+        test60Calls();
+    }
+
+    /**
+     * 模拟60次调用 calculatePeopleCount,每次传入不同的时间点,
+     * 验证自适应扩容机制和时段规则的正确性
+     */
+    private static void test60Calls() {
+        final int TOTAL_CALLS = 60;
+
+        // 重置管理器,确保从干净状态开始
+        resetAdaptiveManager();
+
+        System.out.println("\n========== 【60次模拟调用测试】开始 ==========");
+        System.out.printf("初始状态 → 阈值:%d, 已调用:%d, 已扩容:%s%n",
+                getDynamicThreshold(), getCallCount(), isExpanded());
+        printSeparator(70);
+        System.out.printf("%-6s %-8s %-10s %-15s %-10s%n", "序号", "时间点", "返回人数", "时段规则描述", "当前阈值");
+        printSeparator(70);
+
+        LocalDateTime baseDate = LocalDateTime.now().toLocalDate().atStartOfDay();
+
+        for (int i = 0; i < TOTAL_CALLS; i++) {
+            // 每24分钟推进一次(60次 × 24min = 1440min = 24小时,刚好覆盖全天)
+            int totalMinutesElapsed = i * 24;
+            int hour = (totalMinutesElapsed / 60) % 24;
+            int minute = totalMinutesElapsed % 60;
+
+            LocalDateTime targetTime = baseDate.plusMinutes(totalMinutesElapsed);
+            int peopleCount = calculatePeopleCount(targetTime);
+
+            // 获取时段描述
+            String timeSlotDesc = getTimeSlotDescription(hour);
+            String timeStr = String.format("%02d:%02d", hour, minute);
+
+            System.out.printf("%-6d %-8s %-10d %-15s %-10d%n",
+                    i + 1, timeStr, peopleCount, timeSlotDesc, getDynamicThreshold());
+        }
+
+        printSeparator(70);
+        System.out.printf("最终状态 → 总调用:%d, 阈值:%d, 已扩容:%b%n",
+                getCallCount(), getDynamicThreshold(), isExpanded());
+
+        // 验证结论
+        if (getCallCount() >= TOTAL_CALLS) {
+            System.out.println("[PASS] 60次调用全部完成,自适应机制正常工作");
+        } else {
+            System.out.println("[WARN] 调用次数异常: 期望" + TOTAL_CALLS + ", 实际" + getCallCount());
+        }
+        if (getDynamicThreshold() > DEFAULT_MAX_CALLS) {
+            System.out.println("[PASS] 阈值已自动扩容: " + DEFAULT_MAX_CALLS + " → " + getDynamicThreshold()
+                    + "(因调用次数超过原阈值触发)");
+        }
+        System.out.println("========== 【60次模拟调用测试】结束 ==========");
+    }
+
+    /**
+     * Java8兼容:打印指定长度的分隔线(替代 String.repeat())
+     */
+    private static void printSeparator(int length) {
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            sb.append('─');
+        }
+        System.out.println(sb.toString());
+    }
+
+    /**
+     * 根据小时数返回时段规则描述(用于测试输出)
+     */
+    private static String getTimeSlotDescription(int hour) {
+        if (hour < 5) return "00-05无人";
+        if (hour < 6) return "05-06缓升";
+        if (hour < 8) return "06-08第一高点";
+        if (hour < 9) return "08-09调整";
+        if (hour < 11) return "09-11稳定";
+        if (hour < 12) return "11-12就餐流出";
+        if (hour < 13) return "12-13午间低谷";
+        if (hour < 14) return "13-14回流";
+        if (hour < 15) return "14-15第二高点";
+        if (hour < 19) return "15-19稳定";
+        if (hour < 21) return "19-21持续流出";
+        if (hour < 22) return "21-22清场";
+        return "22-00降至0";
+    }
+}

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

+ 42 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceAlarmDataSyncService.java

@@ -0,0 +1,42 @@
+package com.usky.cdi.service.util;
+
+import com.usky.cdi.service.impl.AlarmDataSyncService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 设备数据同步组件(包含定时任务)
+ */
+@Slf4j
+@Component
+public class DeviceAlarmDataSyncService {
+
+    private final AlarmDataSyncService alarmDataSyncService;
+
+    @Autowired
+    public DeviceAlarmDataSyncService(AlarmDataSyncService alarmDataSyncService) {
+        this.alarmDataSyncService = alarmDataSyncService;
+    }
+
+    /**
+     * 定时任务:定期执行设备数据同步上报
+     * fixedDelay:任务执行完成后固定延迟29分钟执行下一次
+     * initialDelay:初始化后立即执行第一次任务
+     */
+    // @Scheduled(fixedDelay = 1 * 60 * 1000, initialDelay = 0)
+    // public void scheduledDeviceDataSync() {
+    //     Integer tenantId = 1205;
+    //     Long engineeringId = 3101070011L;
+    //     String username = "3101070011";
+    //     String password = "5RqhJ7VG";
+    //     log.info("开始执行桃浦象屿人防设备数据同步定时任务,租户ID:{},工程ID:{}", tenantId, engineeringId);
+    //
+    //     try {
+    //         alarmDataSyncService.synchronizeAlarmData(tenantId, engineeringId, username, password, "peacetime");
+    //     } catch (Exception e) {
+    //         log.error("定时任务执行设备数据同步失败:{}", e.getMessage(), e);
+    //     }
+    // }
+}

+ 683 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java

@@ -0,0 +1,683 @@
+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;
+import com.usky.cdi.service.vo.IotDataTransferVO;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.text.DecimalFormat;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2025/11/27
+ */
+@Data
+@Slf4j
+@Component
+public class DeviceDataQuery {
+    @Autowired
+    private DmpDeviceMapper dmpDeviceMapper;
+    @Value("${device.data.base-url}")
+    private String baseUrl;
+    @Value("${device.data.simulation}")
+    private boolean simulation;
+
+    @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位小数
+
+    // ========== 新增缓存相关成员变量 ==========
+    // 缓存A/B/C相电流值
+    private static final Map<String, Double> currentValueCache = new ConcurrentHashMap<>();
+    // 缓存各相复用次数
+    private static final Map<String, Integer> currentReuseCountCache = new ConcurrentHashMap<>();
+    // 最大复用次数(可调整)
+    private static final int MAX_REUSE_COUNT = 3;
+    // 复用概率(0.5=50%概率复用,可调整)
+    private static final double REUSE_PROBABILITY = 0.5;
+
+    // 定义模拟数据范围常量
+    private static final double TEMP_RANGE_MIN = 10.0;
+    private static final double TEMP_RANGE_MAX = 20.0;
+    private static final double HUMIDITY_RANGE_MIN = 40.0;
+    private static final double HUMIDITY_RANGE_MAX = 41.0;
+    private static final double OXYGEN_RANGE_MIN = 20.0;
+    private static final double OXYGEN_RANGE_MAX = 21.0;
+    private static final double CO2_RANGE_MIN = 480.0;
+    private static final double CO2_RANGE_MAX = 490.0;
+    private static final double VOLTAGE_RANGE_MIN = 220.0;
+    private static final double VOLTAGE_RANGE_MAX = 230.0;
+    private static final double CURRENT_RANGE_MIN = 0.0;
+    private static final double CURRENT_RANGE_MAX = 1.5;
+    private static final double POWER_RANGE_MIN = 1.0;
+    private static final double POWER_RANGE_MAX = 20.0;
+    private static final double TEMP_LINE_RANGE_MIN = 20.0;
+    private static final double TEMP_LINE_RANGE_MAX = 50.0;
+    private static final double LEAKAGE_CURRENT_RANGE_MIN = 50.0;
+    private static final double LEAKAGE_CURRENT_RANGE_MAX = 60.0;
+    private static final double FLOATING_RANGE_MIN = 0.0;
+    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;
+
+    /**
+     * 获取指定设备类型的设备数据
+     * @param transferVO 设备数据传输请求参数
+     * @return 设备数据列表
+     */
+    public List<JSONObject> getDeviceData(IotDataTransferVO transferVO) {
+        // 参数验证
+        if (transferVO == null || transferVO.getDevices() == null || transferVO.getDevices().isEmpty()) {
+            log.warn("获取设备数据失败:参数无效");
+            return Collections.emptyList();
+        }
+
+        try {
+            // 构建请求参数
+            JSONObject requestBody = new JSONObject();
+            List<String> deviceUuids = transferVO.getDevices().stream()
+                    .map(DmpDevice::getDeviceUuid)
+                    .collect(Collectors.toList());
+            requestBody.put("deviceuuid", deviceUuids);
+
+            log.debug("请求设备数据接口,设备数量:{}", deviceUuids.size());
+            String response = HttpClientUtils.doPostJson(baseUrl, requestBody.toJSONString());
+            log.info("接口返回数据:{}", 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);
+                    // 修复:象屿接口可能返回null/空串/非JSON,导致responseObj为null引发NPE
+                    if (responseObj != null && 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());
+                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());
+
+                // 获取返回数据中的device_id集合
+                Set<String> returnedDeviceIds = resultList.stream()
+                        .map(data -> data.getString("device_id"))
+                        .filter(Objects::nonNull)
+                        .collect(Collectors.toSet());
+
+                // 获取请求中devices的device_id集合
+                Map<String, DmpDevice> requestDeviceMap = transferVO.getDevices().stream()
+                        .filter(device -> device.getDeviceId() != null)
+                        .collect(Collectors.toMap(DmpDevice::getDeviceId, device -> device));
+
+                // 找出返回数据中缺少的device_id
+                List<String> missingDeviceIds = requestDeviceMap.keySet().stream()
+                        .filter(deviceId -> !returnedDeviceIds.contains(deviceId))
+                        .collect(Collectors.toList());
+
+                log.info("发现缺少的设备数据,生成模拟数据,设备数量:{}", missingDeviceIds.size());
+                // 为缺少的设备生成模拟数据
+                List<DmpDevice> missingDevices = missingDeviceIds.stream()
+                        .map(requestDeviceMap::get)
+                        .collect(Collectors.toList());
+
+                JSONObject templateData = resultList.isEmpty() ?
+                        (xiangyuDataList.isEmpty() ? null : xiangyuDataList.get(0))
+                        : resultList.get(0);
+
+                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;
+        } catch (Exception e) {
+            log.error("获取设备数据失败:{}", e.getMessage(), e);
+            // 异常情况下若开启模拟模式,则生成模拟数据
+            if (simulation) {
+                log.info("接口调用异常,生成模拟数据,设备类型:{}", transferVO.getDeviceType());
+                return generateSimulationData(transferVO.getDeviceType(), transferVO.getDevices(), null);
+            }
+            return Collections.emptyList();
+        }
+    }
+
+    /**
+     * 解析接口响应数据,提取指定字段
+     * @param responseJson 接口响应JSON字符串
+     * @param deviceType 设备类型
+     * @param devices 设备列表
+     * @return 解析后的设备数据列表
+     */
+    private List<JSONObject> parseResponseData(String responseJson, Integer deviceType, List<DmpDevice> devices) {
+        List<JSONObject> resultList = new ArrayList<>();
+        if (responseJson == null || responseJson.trim().isEmpty()) {
+            log.warn("接口响应数据为空");
+            return resultList;
+        }
+
+        try {
+            // 构建设备UUID到ID的映射,过滤掉deviceUuid为null的设备
+            Map<String, String> deviceUuidToIdMap = devices.stream()
+                    .filter(device -> device.getDeviceUuid() != null)
+                    .collect(Collectors.toMap(DmpDevice::getDeviceUuid, DmpDevice::getDeviceId));
+
+            JSONObject responseObj = JSON.parseObject(responseJson);
+
+            // 检查响应状态
+            String status = responseObj.getString("status");
+            String code = responseObj.getString("code");
+
+            if (!"SUCCESS".equals(status) || !"0".equals(code)) {
+                log.warn("接口返回失败:状态={}, 错误码={}, 错误信息={}", status, code, responseObj.getString("msg"));
+                return resultList;
+            }
+
+            JSONArray dataArray = responseObj.getJSONArray("data");
+            if (dataArray == null || dataArray.isEmpty()) {
+                log.debug("接口响应数据为空数组");
+                return resultList;
+            }
+
+            List<String> targetFields = getTargetFieldsByDeviceType(deviceType);
+
+            for (int i = 0; i < dataArray.size(); i++) {
+                JSONObject deviceData = dataArray.getJSONObject(i);
+                if (deviceData == null) {
+                    continue;
+                }
+
+                JSONObject metrics = deviceData.getJSONObject("metrics");
+                String deviceUuid = deviceData.getString("deviceuuid");
+
+                if (metrics == null || deviceUuid == null) {
+                    continue;
+                }
+
+                JSONObject targetData = new JSONObject();
+                boolean hasValidData = false;
+
+                // 提取目标字段数据
+                for (String field : targetFields) {
+                    Object value = metrics.get(field);
+                    if (value != null) {
+                        targetData.put(field, value);
+                        hasValidData = true;
+                    }
+                }
+
+                // 添加设备标识信息
+                String deviceId = deviceUuidToIdMap.get(deviceUuid);
+
+                // 如果找不到,直接丢弃这条数据,不放入结果
+                if (deviceId == null) {
+                    continue;
+                }
+
+                // 能走到这里,说明一定有 deviceId
+                targetData.put("deviceuuid", deviceUuid);
+                targetData.put("device_id", deviceId);
+
+                if (hasValidData) {
+                    resultList.add(targetData);
+                }
+            }
+
+            log.debug("解析接口响应数据完成,有效设备数据数量:{}", resultList.size());
+            return resultList;
+        } catch (Exception e) {
+            log.error("解析接口响应数据失败:{}", e.getMessage(), e);
+            return Collections.emptyList();
+        }
+    }
+
+    /**
+     * 根据设备类型获取目标字段(自动包含time)
+     * @param deviceType 设备类型
+     * @return 目标字段列表
+     */
+    private List<String> getTargetFieldsByDeviceType(Integer deviceType) {
+        if (deviceType == null) {
+            log.warn("获取目标字段失败:设备类型为空");
+            return Collections.singletonList("realtime");
+        }
+
+        String fieldsStr = deviceFieldConfig.deviceFieldMapping().get(deviceType.toString());
+        if (fieldsStr == null || fieldsStr.trim().isEmpty()) {
+            log.warn("获取目标字段失败:设备类型{}对应的字段映射不存在", deviceType);
+            return Collections.singletonList("realtime");
+        }
+
+        List<String> fields = Arrays.stream(fieldsStr.split(","))
+                .map(String::trim)
+                .filter(field -> !field.isEmpty())
+                .collect(Collectors.toList());
+
+        // 确保包含时间字段
+        if (!fields.contains("realtime")) {
+            fields.add("realtime");
+        }
+
+        return fields;
+    }
+
+    /**
+     * 生成模拟数据(按指定精度格式化)
+     * @param deviceType 设备类型
+     * @param devices 设备列表
+     * @return 模拟数据列表
+     */
+    private List<JSONObject> generateSimulationData(Integer deviceType, List<DmpDevice> devices, JSONObject standard) {
+        List<JSONObject> simulationList = new ArrayList<>();
+        long currentTime = System.currentTimeMillis();
+        // System.out.println("深拷贝当前时间戳:" + currentTime);
+
+        // 获取天气数据(仅在需要时)
+        // 期望:只有“象屿模板为空/缺字段”时,才用天气数据对温湿度做修正;有象屿模板就不要再相加
+        boolean needWeatherForTemp = (deviceType != null && deviceType == 707) && (standard == null || standard.get("wd") == null);
+        boolean needWeatherForHumi = (deviceType != null && deviceType == 708) && (standard == null || standard.get("sd") == null);
+        boolean needWeatherForElectric = (deviceType != null && deviceType == 704) && (standard == null);
+        Map<String, Double> weatherData = null;
+        if (needWeatherForTemp || needWeatherForHumi || needWeatherForElectric) {
+            weatherData = WeatherFetcher.fetchWeather();
+        }
+
+        for (DmpDevice device : devices) {
+            JSONObject simData;
+
+            if (standard != null) {
+                simData = new JSONObject();
+                simData.putAll(standard);
+
+                // 【关键】先移除,确保 FastJSON 内部缓存失效
+                simData.remove("realtime");
+                simData.remove("device_id");
+                simData.remove("deviceuuid");
+
+                // 再重新 put,强制类型正确
+                simData.put("realtime", currentTime);
+                simData.put("device_id", device.getDeviceId());
+                // System.out.println("深拷贝时间戳1:" + simData.getLong("realtime"));
+
+                if (device.getDeviceUuid() != null && standard.containsKey("deviceuuid")) {
+                    simData.put("deviceuuid", device.getDeviceUuid());
+                }
+
+                // 若象屿模板里缺少温湿度字段,则再回退到天气+差值逻辑补齐(避免“所有情况都相加”)
+                if (deviceType != null && deviceType == 707 && simData.get("wd") == null) {
+                    double temp707;
+                    if (weatherData != null && !weatherData.isEmpty()) {
+                        double floating = BasementClimateUtil.getTempDiffWithOutdoor();
+                        temp707 = weatherData.get("temperature") + floating;
+                    } else {
+                        temp707 = BasementClimateUtil.getCurrentTempRange();
+                    }
+                    simData.put("wd", temp707);
+                } else if (deviceType != null && deviceType == 708 && simData.get("sd") == null) {
+                    double humi708;
+                    if (weatherData != null && !weatherData.isEmpty()) {
+                        double floating = BasementClimateUtil.getHumiDiffWithOutdoor();
+                        humi708 = weatherData.get("humidity") + floating;
+                    } else {
+                        humi708 = BasementClimateUtil.getCurrentHumiRange();
+                    }
+                    simData.put("sd", humi708);
+                }
+
+                addNoiseToNumericFields(simData, deviceType);
+                // System.out.println("深拷贝时间戳2:" + simData.getLong("realtime"));
+            } else {
+                // ❌ 无标准模板,走原始随机逻辑
+                simData = new JSONObject();
+                simData.put("realtime", currentTime);
+                simData.put("device_id", device.getDeviceId());
+
+                switch (deviceType) {
+                    case 707:
+                        double temp707 = 0.0;
+                        if (weatherData != null && !weatherData.isEmpty()) {
+                            double floating = BasementClimateUtil.getTempDiffWithOutdoor();
+                            temp707 = weatherData.get("temperature") + floating;
+                        } else {
+                            temp707 = BasementClimateUtil.getCurrentTempRange();
+                        }
+                        simData.put("wd", temp707);
+                        break;
+
+                    case 708:
+                        double humi708 = 0.0;
+                        if (weatherData != null && !weatherData.isEmpty()) {
+                            double floating = BasementClimateUtil.getHumiDiffWithOutdoor();
+                            humi708 = weatherData.get("humidity") + floating;
+                        } else {
+                            humi708 = BasementClimateUtil.getCurrentHumiRange();
+                        }
+                        simData.put("sd", humi708);
+                        break;
+
+                    case 709:
+                        double o2709 = ThreadLocalRandom.current().nextDouble(OXYGEN_RANGE_MIN, OXYGEN_RANGE_MAX);
+                        simData.put("o2", formatNumber(o2709, FORMAT_2_2));
+                        break;
+
+                    case 710:
+                        double co2710 = ThreadLocalRandom.current().nextDouble(CO2_RANGE_MIN, CO2_RANGE_MAX);
+                        simData.put("co2", formatNumber(co2710, FORMAT_0_3));
+                        break;
+
+                    case 711:
+                        simData.put("co", 0);
+                        break;
+
+                    case 702:
+                        simData.put("leach_status", 0);
+                        break;
+
+                    case 703:
+                        simData.put("sensorValue", 0);
+                        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);
+                        simData.put("bVoltage", formatNumber(bVoltage, FORMAT_3_2));
+                        double cVoltage = ThreadLocalRandom.current().nextDouble(VOLTAGE_RANGE_MIN, VOLTAGE_RANGE_MAX);
+                        simData.put("cVoltage", formatNumber(cVoltage, FORMAT_3_2));
+
+                        String deviceId = device.getDeviceId();
+                        double aElectricity = getCurrentValue(deviceId, "a");
+                        simData.put("aElectricity", formatNumber(aElectricity, FORMAT_3_2));
+                        double bElectricity = getCurrentValue(deviceId, "b");
+                        simData.put("bElectricity", formatNumber(bElectricity, FORMAT_3_2));
+                        double cElectricity = getCurrentValue(deviceId, "c");
+                        simData.put("cElectricity", formatNumber(cElectricity, FORMAT_3_2));
+
+                        double totalPower = (aVoltage * aElectricity + bVoltage * bElectricity + cVoltage * cElectricity) / 1000;
+                        simData.put("totalPower", formatNumber(totalPower, FORMAT_4_2));
+
+                        double lineTemp = 0.0;
+                        if (weatherData != null && !weatherData.isEmpty()) {
+                            lineTemp = weatherData.get("temperature") + 1.0;
+                        } else {
+                            lineTemp = ThreadLocalRandom.current().nextDouble(TEMP_LINE_RANGE_MIN, TEMP_LINE_RANGE_MAX);
+                        }
+                        simData.put("line1TEMP", formatNumber(lineTemp + ThreadLocalRandom.current().nextDouble(FLOATING_RANGE_MIN1, FLOATING_RANGE_MAX1), FORMAT_2_2));
+                        simData.put("Line2TEMP", formatNumber(lineTemp + ThreadLocalRandom.current().nextDouble(FLOATING_RANGE_MIN1, FLOATING_RANGE_MAX1), FORMAT_2_2));
+                        simData.put("Line3TEMP", formatNumber(lineTemp + ThreadLocalRandom.current().nextDouble(FLOATING_RANGE_MIN1, FLOATING_RANGE_MAX1), FORMAT_2_2));
+                        simData.put("Line4TEMP", formatNumber(lineTemp + ThreadLocalRandom.current().nextDouble(FLOATING_RANGE_MIN1, FLOATING_RANGE_MAX1), FORMAT_2_2));
+
+                        simData.put("leakageCurrent", formatNumber(ThreadLocalRandom.current().nextDouble(LEAKAGE_CURRENT_RANGE_MIN, LEAKAGE_CURRENT_RANGE_MAX), FORMAT_4_2));
+                        break;
+
+                    case 712:
+                        simData.put("qx", 0);
+                        break;
+
+                    case 713:
+                        simData.put("cd", 0);
+                        break;
+
+                    case 714:
+                        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;
+                }
+            }
+
+            simulationList.add(simData);
+        }
+
+        log.info("生成模拟数据完成,设备类型:{},数量:{}", deviceType, simulationList.size());
+        return simulationList;
+    }
+
+    private void addNoiseToNumericFields(JSONObject data, Integer deviceType) {
+        Map<String, Double> noiseConfig = getNoiseConfigByDeviceType(deviceType);
+
+        // 这些字段永远不加噪
+        Set<String> skipFields = new HashSet<>(Arrays.asList("realtime", "device_id", "deviceuuid"));
+
+        for (String key : data.keySet()) {
+            if (skipFields.contains(key)) continue;
+            if (isDiscreteField(key)) continue;
+
+            Object value = data.get(key);
+            Double original = null;
+
+            // 【关键】支持 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; // 不是数字字符串,跳过
+                }
+            }
+
+            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);
+            log.info("加噪: {} | 原值={} | 噪声={} | 新值={}", 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("realtime")
+                || field.equalsIgnoreCase("device_id")
+                || field.equalsIgnoreCase("deviceuuid")
+                || field.equalsIgnoreCase("leach_status")
+                || field.equalsIgnoreCase("sensorValue")
+                || field.equalsIgnoreCase("qx")
+                || field.equalsIgnoreCase("cd")
+                || field.equalsIgnoreCase("wy")
+                || field.equalsIgnoreCase("co");
+    }
+
+    // 获取各字段的浮动模拟配置
+    private Map<String, Double> getNoiseConfigByDeviceType(Integer deviceType) {
+        Map<String, Double> config = new HashMap<>();
+        switch (deviceType) {
+            case 707: // 温度
+                config.put("wd", 0.03); //
+                break;
+            case 708: // 湿度
+                config.put("sd", 0.05);
+                break;
+            case 709: // 氧气
+                config.put("o2", 0.02);
+                break;
+            case 710: // CO2
+                config.put("co2", 0.02);
+                break;
+            case 704: // 电气火灾
+                config.put("aVoltage", 0.01);
+                config.put("bVoltage", 0.01);
+                config.put("cVoltage", 0.01);
+                config.put("aElectricity", 0.05);
+                config.put("bElectricity", 0.05);
+                config.put("cElectricity", 0.05);
+                config.put("totalPower", 0.05);
+                config.put("line1TEMP", 0.03);
+                config.put("Line2TEMP", 0.03);
+                config.put("Line3TEMP", 0.03);
+                config.put("Line4TEMP", 0.03);
+                config.put("leakageCurrent", 0.1); // 电流波动大些
+                break;
+            default:
+                // 默认所有数值字段 ±2%
+                break;
+        }
+        return config;
+    }
+
+    /**
+     * 通用数值格式化方法
+     * @param value 原始数值
+     * @param format 格式化器
+     * @return 格式化后的字符串
+     */
+    private String formatNumber(double value, DecimalFormat format) {
+        if (format == null) {
+            return String.valueOf(value);
+        }
+        return format.format(value);
+    }
+
+    // ========== 获取带缓存的电流值 ==========
+    private double getCurrentValue(String deviceId, String phase) {
+        String cacheKey = (deviceId == null ? "dummy" : deviceId) + "_" + phase;
+        int count = currentReuseCountCache.getOrDefault(cacheKey, 0);
+
+        if (!currentValueCache.containsKey(cacheKey) || count >= MAX_REUSE_COUNT) {
+            return generateNewCurrent(cacheKey);
+        }
+
+        boolean reuse = ThreadLocalRandom.current().nextDouble() < REUSE_PROBABILITY;
+        if (reuse) {
+            currentReuseCountCache.put(cacheKey, count + 1);
+            return currentValueCache.get(cacheKey);
+        } else {
+            return generateNewCurrent(cacheKey);
+        }
+    }
+
+    // 同步修改generateNewCurrent方法(参数改为cacheKey)
+    private double generateNewCurrent(String cacheKey) {
+        double newValue = ThreadLocalRandom.current().nextDouble(CURRENT_RANGE_MIN, CURRENT_RANGE_MAX);
+        newValue = Math.round(newValue * 100.0) / 100.0;
+        currentValueCache.put(cacheKey, newValue);
+        currentReuseCountCache.put(cacheKey, 0);
+        return newValue;
+    }
+}

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff