49 Commits c2da1491e7 ... 06f6b60fef

Tác giả SHA1 Thông báo Ngày
  fuyuchuan 06f6b60fef 优化人防单元等基础数据推送代码 2 tuần trước cách đây
  hanzhengyi 8da7539662 Merge branch 'han' of uskycloud/usky-modules into master 3 tuần trước cách đây
  james a60bc4aeb4 Merge branch 'han' of http://47.111.81.118:3000/uskycloud/usky-modules into han 1 tháng trước cách đây
  james bf9aa77aec 调整请求agbox回调数据请求逻辑 1 tháng trước cách đây
  hanzhengyi 42f051fa9d ipc解绑接口调整 1 tháng trước cách đây
  james 36a90ec9cf 1、优化十个类型事件对应的图片获取逻辑,增加访问AGox获取场景图片的逻辑; 1 tháng trước cách đây
  hanzhengyi 52c5692c8c 调整电子地图查询接口 1 tháng trước cách đây
  hanzhengyi 99ea39b852 优化ems 1 tháng trước cách đây
  hanzhengyi daf2ed0740 完善电子地图相关接口,新增ems 1 tháng trước cách đây
  hanzhengyi 19c89c8472 Merge branch 'han' of http://47.111.81.118:3000/uskycloud/usky-modules into han 1 tháng trước cách đây
  james eb834925d1 优化安防系统上报不同事件类型事件过程中遇到的逻辑或数据问题(存库的历史事件数据未在界面展示、出入门禁控制人员类型和进出事件类型对应不上) 1 tháng trước cách đây
  hanzhengyi 030fa74147 Merge branch 'han' of http://47.111.81.118:3000/uskycloud/usky-modules into han 1 tháng trước cách đây
  fuyuchuan 184c3c0f30 解决冲突 1 tháng trước cách đây
  hanzhengyi c55857ce98 Merge branch 'han' 1 tháng trước cách đây
  hanzhengyi 663d8e08d9 完善电子地图相关接口 1 tháng trước cách đây
  hanzhengyi 9842472de6 完善电子地图相关接口 1 tháng trước cách đây
  hanzhengyi 69019bc422 补充视频导出防护配置、实时电子巡检配置相关接口 1 tháng trước cách đây
  james 82c21133fc 优化mqtt服务实时巡检事件逻辑,增加channel赋值默认0查询 1 tháng trước cách đây
  hanzhengyi ed11c8c8f3 完善相关接口 1 tháng trước cách đây
  james 569ad51b3e 如果没有在sys_device表中,接收到的事件不存入库,同时不推websocket,同时在推送的websocket消息体中增加事件等级Id 1 tháng trước cách đây
  hanzhengyi b99f18630c 完善相关接口 1 tháng trước cách đây
  fuyuchuan a06486a352 系统信息代码提交 1 tháng trước cách đây
  zhaojinyu aaa5480f72 处理各事件类型图片存储 1 tháng trước cách đây
  hanzhengyi 4a74f6d9aa 完善相关接口 1 tháng trước cách đây
  hanzhengyi d203cd0688 新增线上环境海康大华配置文件 1 tháng trước cách đây
  hanzhengyi 01efeeccd1 新增相关类型查询接口,完善海康大华相关接口 1 tháng trước cách đây
  james ca2a6b644b 开发处理消费事件mqtt消息推送websocket服务逻辑,同时处理心跳mqtt消息发生时间同步到设备信息表对应字段 1 tháng trước cách đây
  hanzhengyi 948ad1bce5 删除相关无关文件 1 tháng trước cách đây
  hanzhengyi 4fbbf2c18e 海康、大华、onvif相关插件及接口 1 tháng trước cách đây
  hanzhengyi 80de41555f 海康、大华、onvif相关插件及接口 1 tháng trước cách đây
  james 7d563eb113 创建websocket服务监听 1 tháng trước cách đây
  james e6d6c0c9b0 生成onvif摄像头SDK配置 1 tháng trước cách đây
  james dfb82578c0 添加dahua依赖 1 tháng trước cách đây
  james 6f90f956e2 优化事件列表:各模块所有事件分页(中上-事件列表区域)和查询事件详情两个接口,增加deviceType设备类型查询 1 tháng trước cách đây
  hanzhengyi 5d75d07c4a 增加批量新增设备配置 1 tháng trước cách đây
  james 8ac594a9e2 优化首页接口逻辑,开发智能分析相关接口 1 tháng trước cách đây
  fuyuchuan e523d5b9e5 付宇川-智能安防集成系统代码提交:系统信息 1 tháng trước cách đây
  hanzhengyi 27596c54f7 电子地图、系统管理相关接口 1 tháng trước cách đây
  hanzhengyi a904a7716e 调整mqtt模块 1 tháng trước cách đây
  hanzhengyi 4b6d69e479 系统集成配置接口 1 tháng trước cách đây
  zhaojinyu 71317e348e 优化事件类及mqtt监听类解决接收事件报错问题——目前USB与巡检 1 tháng trước cách đây
  zhaojinyu 3aed249482 添加mqtt依赖,完成10个设备类型事件agbox对接 1 tháng trước cách đây
  hanzhengyi 544115171d 一标六实及人员管理接口完善、agbox对接 1 tháng trước cách đây
  hanzhengyi d93ea7eb1d 首页接口补充 1 tháng trước cách đây
  hanzhengyi f078c4aa77 智能安防集成应用系统 1 tháng trước cách đây
  fuyuchuan 61b9928e04 Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 tháng trước cách đây
  fuyuchuan 2c3c9df4f9 Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 tháng trước cách đây
  fuyuchuan c5cbd9bfcc Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 tháng trước cách đây
  fuyuchuan b03d835b0a Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 tháng trước cách đây
100 tập tin đã thay đổi với 4483 bổ sung304 xóa
  1. 28 19
      pom.xml
  2. 1 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/MybatisGeneratorUtils.java
  3. 1 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/AlarmDataController.java
  4. 5 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/controller/BaseDataController.java
  5. 1 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/CdiDeliveryLogService.java
  6. 10 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/AlarmDataSyncService.java
  7. 31 19
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java
  8. 368 221
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java
  9. 41 5
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java
  10. 69 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/alarm/AlarmMessage1VO.java
  11. 42 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/base/BaseMqttInfo.java
  12. 2 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/EngineeringBaseVO.java
  13. 5 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/FacilityDeviceVO.java
  14. 2 15
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/FloorPlaneVO.java
  15. 2 15
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/ProtectiveUnitVO.java
  16. 61 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAnalysisController.java
  17. 233 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsApiV1Controller.java
  18. 37 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAuthController.java
  19. 106 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsReportController.java
  20. 39 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsChannel.java
  21. 69 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDevice.java
  22. 50 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDeviceFunction.java
  23. 44 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsEnergyItemCode.java
  24. 64 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsGateway.java
  25. 71 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProject.java
  26. 45 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpace.java
  27. 46 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpaceArea.java
  28. 67 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpaceBuilding.java
  29. 45 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpaceFloor.java
  30. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsChannelMapper.java
  31. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDeviceFunctionMapper.java
  32. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDeviceMapper.java
  33. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsEnergyItemCodeMapper.java
  34. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsGatewayMapper.java
  35. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectMapper.java
  36. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceAreaMapper.java
  37. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceBuildingMapper.java
  38. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceFloorMapper.java
  39. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceMapper.java
  40. 19 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsAnalysisService.java
  41. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsAuthService.java
  42. 22 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsGatewayQueryService.java
  43. 72 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsModelService.java
  44. 20 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsOverviewService.java
  45. 32 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsReportService.java
  46. 61 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAnalysisServiceImpl.java
  47. 30 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAuthServiceImpl.java
  48. 87 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsGatewayQueryServiceImpl.java
  49. 401 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsModelServiceImpl.java
  50. 53 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsOverviewServiceImpl.java
  51. 164 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsReportServiceImpl.java
  52. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCategoryRatioItemVO.java
  53. 17 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCollectionRealtimeItemVO.java
  54. 16 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCollectionRealtimeRequest.java
  55. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCollectionRealtimeResponse.java
  56. 15 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareRequest.java
  57. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareResponse.java
  58. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareSeriesItemVO.java
  59. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareValueVO.java
  60. 18 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyStatisticsItemVO.java
  61. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyStatisticsRequest.java
  62. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyStatisticsResponse.java
  63. 16 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyTypeVO.java
  64. 24 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsGatewayDetailResponse.java
  65. 22 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsGatewayListItem.java
  66. 21 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsGatewayPageRequest.java
  67. 16 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsIdResponse.java
  68. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsLoginRequest.java
  69. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsLoginResponse.java
  70. 35 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsModelSaveRequest.java
  71. 23 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsProjectResponse.java
  72. 18 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsRegionAnalysisItemVO.java
  73. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsRegionAnalysisResponse.java
  74. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsReportAttributeVO.java
  75. 17 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsReportDeviceItemVO.java
  76. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsReportDevicesResponse.java
  77. 18 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsStructureTreeNode.java
  78. 11 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsSummaryRequest.java
  79. 20 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsSummaryResponse.java
  80. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendCategoryResponse.java
  81. 18 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendIndicatorsResponse.java
  82. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendItemVO.java
  83. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendResponse.java
  84. 21 0
      service-sas/pom.xml
  85. 28 0
      service-sas/service-sas-api/pom.xml
  86. 139 0
      service-sas/service-sas-biz/pom.xml
  87. 108 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/MybatisGeneratorUtils.java
  88. 47 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/ServiceSasApplication.java
  89. 16 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/StandardOnvifService.java
  90. 126 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/UnityVideoInfo.java
  91. 18 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/config/WebSocketConfig.java
  92. 71 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/DahuaNvrInfo.java
  93. 126 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/DahuaVideoInfo.java
  94. 185 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/DahuaVideoStreamService.java
  95. 10 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/EM_SEND_SEARCH_TYPE.java
  96. 58 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/FPlayDataCallBackEx.java
  97. 62 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/FRealDataCallBackEx.java
  98. 254 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/InitNetSDKLib.java
  99. 13 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/NET_BRIDGE_NET_CARDS_MAC_LIST.java
  100. 24 0
      service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/NET_IN_STARTSERACH_DEVICE.java

+ 28 - 19
pom.xml

@@ -90,25 +90,34 @@
 
 
         <!--    <module>service-data</module>-->
         <!--    <module>service-data</module>-->
 
 
-    </modules>
-
-
-    <dependencies>
-
-
-        <dependency>
-
-
-            <groupId>org.projectlombok</groupId>
-
-
-            <artifactId>lombok</artifactId>
-
-
-        </dependency>
-
-
-    </dependencies>
+        <module>service-sas</module>
+
+  </modules>
+          
+  
+  
+  <dependencies>
+                    
+    
+    
+    <dependency>
+                              
+      
+      
+      <groupId>org.projectlombok</groupId>
+                              
+      
+      
+      <artifactId>lombok</artifactId>
+                          
+    
+    
+    </dependency>
+                
+  
+  
+  </dependencies>
+    
 
 
 
 
 </project>
 </project>

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

@@ -71,7 +71,7 @@ public class MybatisGeneratorUtils {
         // strategy.setTablePrefix("t_"); // 表名前缀
         // strategy.setTablePrefix("t_"); // 表名前缀
         strategy.setEntityLombokModel(true); //使用lombokbase_build_plane
         strategy.setEntityLombokModel(true); //使用lombokbase_build_plane
         //修改自己想要生成的表
         //修改自己想要生成的表
-        strategy.setInclude("base_build");  // 逆向工程使用的表   如果要生成多个,这里可以传入String[]
+        strategy.setInclude("dmp_device");  // 逆向工程使用的表   如果要生成多个,这里可以传入String[]
         mpg.setStrategy(strategy);
         mpg.setStrategy(strategy);
 
 
         // 关闭默认 xml 生成,调整生成 至 根目录
         // 关闭默认 xml 生成,调整生成 至 根目录

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

@@ -2,6 +2,7 @@ package com.usky.cdi.controller;
 
 
 import com.usky.cdi.service.impl.AlarmDataTransferService;
 import com.usky.cdi.service.impl.AlarmDataTransferService;
 import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
 import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
+import com.usky.cdi.service.vo.alarm.AlarmMessage1VO;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;

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

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

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

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

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

@@ -12,6 +12,7 @@ import com.usky.cdi.service.mqtt.MqttConnectionTool;
 import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
 import com.usky.cdi.service.vo.alarm.AlarmMessageVO;
 import com.usky.cdi.service.enums.AlarmType;
 import com.usky.cdi.service.enums.AlarmType;
+import com.usky.common.security.utils.SecurityUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -117,6 +118,13 @@ public class AlarmDataSyncService {
         String topic = MqttTopics.Alarm.MESSAGE.getTopic();
         String topic = MqttTopics.Alarm.MESSAGE.getTopic();
         String desc = MqttTopics.Alarm.MESSAGE.getDesc();
         String desc = MqttTopics.Alarm.MESSAGE.getDesc();
 
 
+        String userName = "自动同步";
+        try {
+            userName = SecurityUtils.getUsername();
+        } catch (Exception e) {
+            log.error("定时任务无法获取用户名,使用默认‘自动同步’", e);
+        }
+
         try {
         try {
             // 2.创建MQTT连接
             // 2.创建MQTT连接
             mqttConnectionTool.connectOrRefresh(username, password);
             mqttConnectionTool.connectOrRefresh(username, password);
@@ -198,7 +206,7 @@ public class AlarmDataSyncService {
             endTime = System.currentTimeMillis();
             endTime = System.currentTimeMillis();
 
 
             cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
             cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
-                    successCount, failureCount, size - successCount - failureCount, 1);
+                    successCount, failureCount, size - successCount - failureCount, 1, userName);
         } catch (Exception e) {
         } catch (Exception e) {
             log.error("租户{}的告警数据推送定时任务执行失败:{}", tenantId, e.getMessage(), e);
             log.error("租户{}的告警数据推送定时任务执行失败:{}", tenantId, e.getMessage(), e);
         } finally {
         } finally {
@@ -206,7 +214,7 @@ public class AlarmDataSyncService {
             log.info("结束时间:{}, 耗时:{}ms", getCurrentTime(), endTime - startTime);
             log.info("结束时间:{}, 耗时:{}ms", getCurrentTime(), endTime - startTime);
 
 
             cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
             cdiDeliveryLogService.saveLog(topic, desc, 5, tenantId, engineeringId, now, startTime, endTime, size,
-                    successCount, failureCount, size - successCount - failureCount, 0);
+                    successCount, failureCount, size - successCount - failureCount, 0, userName);
         }
         }
     }
     }
 
 

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

@@ -7,9 +7,9 @@ import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.usky.cdi.domain.BaseBuildFacility;
 import com.usky.cdi.domain.BaseBuildFacility;
 import com.usky.cdi.domain.DmpDevice;
 import com.usky.cdi.domain.DmpDevice;
 import com.usky.cdi.service.BaseBuildFacilityService;
 import com.usky.cdi.service.BaseBuildFacilityService;
-import com.usky.cdi.service.CdiDeliveryLogService;
 import com.usky.cdi.service.DmpDeviceInfoService;
 import com.usky.cdi.service.DmpDeviceInfoService;
 import com.usky.cdi.service.config.mqtt.MqttOutConfig;
 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.util.SnowflakeIdGenerator;
 import com.usky.cdi.service.vo.info.EngineeringBaseVO;
 import com.usky.cdi.service.vo.info.EngineeringBaseVO;
 import com.usky.cdi.service.vo.info.FacilityDeviceVO;
 import com.usky.cdi.service.vo.info.FacilityDeviceVO;
@@ -48,8 +48,11 @@ public class BaseDataTransferService {
     @Resource
     @Resource
     private MqttOutConfig.MqttGateway mqttGateway;
     private MqttOutConfig.MqttGateway mqttGateway;
 
 
-    @Value("${config.engineeringID}")
-    private String engineeringID;
+    // @Value("${config.engineeringID}")
+    // private String engineeringID;
+
+    @Autowired
+    private MqttConnectionTool mqttConnectionTool;
 
 
     private final SnowflakeIdGenerator idGenerator;
     private final SnowflakeIdGenerator idGenerator;
     private final SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
     private final SimpleDateFormat timeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@@ -122,7 +125,8 @@ public class BaseDataTransferService {
             String topic = "base/protectiveUnit";
             String topic = "base/protectiveUnit";
 
 
             log.info("发送防护单元基础信息,Topic: {}, Data: {}", topic, json);
             log.info("发送防护单元基础信息,Topic: {}, Data: {}", topic, json);
-            mqttGateway.sendToMqtt(topic, json);
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, json);
 
 
             return true;
             return true;
         } catch (Exception e) {
         } catch (Exception e) {
@@ -147,9 +151,10 @@ public class BaseDataTransferService {
                 vo.setPublishTime(getCurrentTime());
                 vo.setPublishTime(getCurrentTime());
             }
             }
 
 
-            String imagePath = "D://games/3492.jpg";
+            String imagePath = "D://02H1-01.jpg";
             // 将图片文件读取为字节数组
             // 将图片文件读取为字节数组
             byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
             byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
+            vo.setFloorFile(imageBytes);
 
 
             // 检查文件大小(不超过5MB)
             // 检查文件大小(不超过5MB)
             if (vo.getFloorFile() != null && imageBytes.length > 5 * 1024 * 1024) {
             if (vo.getFloorFile() != null && imageBytes.length > 5 * 1024 * 1024) {
@@ -157,6 +162,8 @@ public class BaseDataTransferService {
                 return false;
                 return false;
             }
             }
 
 
+            String base64File = java.util.Base64.getEncoder().encodeToString(vo.getFloorFile());
+
             HashMap<String, Object> map = new HashMap<>();
             HashMap<String, Object> map = new HashMap<>();
             map.put("dataPacketID", vo.getDataPacketID());
             map.put("dataPacketID", vo.getDataPacketID());
             map.put("engineeringID", vo.getEngineeringID());
             map.put("engineeringID", vo.getEngineeringID());
@@ -166,27 +173,28 @@ public class BaseDataTransferService {
             map.put("floorFileSuffix", vo.getFloorFileSuffix());
             map.put("floorFileSuffix", vo.getFloorFileSuffix());
             map.put("filePixWidth", vo.getFilePixWidth());
             map.put("filePixWidth", vo.getFilePixWidth());
             map.put("filePixHeight", vo.getFilePixHeight());
             map.put("filePixHeight", vo.getFilePixHeight());
-            map.put("floorFile", imageBytes);
+            map.put("floorFile", base64File);
             map.put("publishTime", vo.getPublishTime());
             map.put("publishTime", vo.getPublishTime());
             Gson gson = new Gson();
             Gson gson = new Gson();
             // 将字节数组转换为Base64编码
             // 将字节数组转换为Base64编码
-            JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
-            vo.setFloorFile(imageBytes);
+            // JSONObject jsonObject = (JSONObject) JSON.toJSON(vo);
+            // vo.setFloorFile(imageBytes);
 //            jsonObject.put("floorFile", imageBytes);
 //            jsonObject.put("floorFile", imageBytes);
-            if (vo.getFloorFile() != null) {
-                // 使用Base64编码传输二进制数据
-                String base64File = java.util.Base64.getEncoder().encodeToString(vo.getFloorFile());
-                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();
+            // String json = jsonObject.toJSONString();
             System.out.println(gson.toJson(map));
             System.out.println(gson.toJson(map));
             String topic = "base/floorPlane";
             String topic = "base/floorPlane";
 
 
             log.info("发送楼层平面图信息,Topic: {}, FileID: {}, FileSize: {} bytes",
             log.info("发送楼层平面图信息,Topic: {}, FileID: {}, FileSize: {} bytes",
                     topic, vo.getFloorFileID(),
                     topic, vo.getFloorFileID(),
                     vo.getFloorFile() != null ? vo.getFloorFile().length : 0);
                     vo.getFloorFile() != null ? vo.getFloorFile().length : 0);
-            mqttGateway.sendToMqtt(topic, gson.toJson(map));
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh("3101100017", "gjB4v1bh");
+            gateway.sendToMqtt(topic, gson.toJson(map));
 
 
             return true;
             return true;
         } catch (Exception e) {
         } catch (Exception e) {
@@ -234,8 +242,8 @@ public class BaseDataTransferService {
 
 
             HashMap<String, Object> map = new HashMap<>();
             HashMap<String, Object> map = new HashMap<>();
             map.put("dataPacketID", generateDataPacketID());
             map.put("dataPacketID", generateDataPacketID());
-            map.put("engineeringID", Long.parseLong(engineeringID));
-            map.put("floor", "B2");
+            map.put("engineeringID", vo.getEngineeringID());
+            map.put("floor", vo.getFloor());
             map.put("floorFileID", 1);
             map.put("floorFileID", 1);
             map.put("sensorID", Integer.parseInt(vo.getDeviceId()));
             map.put("sensorID", Integer.parseInt(vo.getDeviceId()));
             map.put("sensorNo", vo.getDeviceUuid());
             map.put("sensorNo", vo.getDeviceUuid());
@@ -253,7 +261,8 @@ public class BaseDataTransferService {
             String topic = "base/sensorInfo";
             String topic = "base/sensorInfo";
             System.out.println(gson.toJson(map));
             System.out.println(gson.toJson(map));
 //            log.info("发送智能监管物联设施信息,Topic: {}, SensorID: {}", topic, vo.getSensorID());
 //            log.info("发送智能监管物联设施信息,Topic: {}, SensorID: {}", topic, vo.getSensorID());
-            mqttGateway.sendToMqtt(topic, gson.toJson(map));
+            MqttConnectionTool.MqttGateway gateway = mqttConnectionTool.connectOrRefresh(vo.getUserName(), vo.getPassword());
+            gateway.sendToMqtt(topic, gson.toJson(map));
 
 
             return true;
             return true;
         } catch (Exception e) {
         } catch (Exception e) {
@@ -290,7 +299,7 @@ public class BaseDataTransferService {
      * @param tenantId 租户ID
      * @param tenantId 租户ID
      * @return 成功发送的数量
      * @return 成功发送的数量
      */
      */
-    public Map<String, Integer> batchSendSensorInfos(Integer tenantId) {
+    public Map<String, Integer> batchSendSensorInfos(Integer tenantId, Long engineeringId, String username, String password) {
         List<BaseBuildFacility> list = baseBuildFacilityService.facilityInfo(tenantId);
         List<BaseBuildFacility> list = baseBuildFacilityService.facilityInfo(tenantId);
         List<DmpDevice> list1 = dmpDeviceInfoService.deviceInfo(tenantId);
         List<DmpDevice> list1 = dmpDeviceInfoService.deviceInfo(tenantId);
         List<FacilityDeviceVO> list2 = new ArrayList<>();
         List<FacilityDeviceVO> list2 = new ArrayList<>();
@@ -309,6 +318,9 @@ public class BaseDataTransferService {
                         facilityDeviceVO.setDeviceUuid(list1.get(k).getDeviceUuid());
                         facilityDeviceVO.setDeviceUuid(list1.get(k).getDeviceUuid());
                         facilityDeviceVO.setFacilityDesc(list.get(j).getFacilityDesc());
                         facilityDeviceVO.setFacilityDesc(list.get(j).getFacilityDesc());
                         facilityDeviceVO.setDeviceType(list1.get(k).getDeviceType());
                         facilityDeviceVO.setDeviceType(list1.get(k).getDeviceType());
+                        facilityDeviceVO.setEngineeringID(engineeringId);
+                        facilityDeviceVO.setUserName(username);
+                        facilityDeviceVO.setPassword(password);
                         list2.add(facilityDeviceVO);
                         list2.add(facilityDeviceVO);
                     }
                     }
                 }
                 }

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

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

+ 41 - 5
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java

@@ -12,7 +12,6 @@ import com.usky.cdi.mapper.CdiDefenseProjectMapper;
 import com.usky.cdi.mapper.CdiDeliveryLogMapper;
 import com.usky.cdi.mapper.CdiDeliveryLogMapper;
 import com.usky.cdi.mapper.DmpDeviceMapper;
 import com.usky.cdi.mapper.DmpDeviceMapper;
 import com.usky.cdi.mapper.DmpProductMapper;
 import com.usky.cdi.mapper.DmpProductMapper;
-import com.usky.cdi.service.CdiDeliveryLogService;
 import com.usky.cdi.service.config.mqtt.MqttOutConfig;
 import com.usky.cdi.service.config.mqtt.MqttOutConfig;
 import com.usky.cdi.service.enums.MqttTopics;
 import com.usky.cdi.service.enums.MqttTopics;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
@@ -50,8 +49,6 @@ import java.util.stream.Collectors;
 @Service
 @Service
 public class IotDataTransferService {
 public class IotDataTransferService {
 
 
-    private MqttOutConfig.MqttGateway mqttGateway;
-
     @Autowired
     @Autowired
     private MqttConnectionTool mqttConnectionTool;
     private MqttConnectionTool mqttConnectionTool;
 
 
@@ -1153,8 +1150,17 @@ public class IotDataTransferService {
      * @param username 用户名
      * @param username 用户名
      */
      */
     void sendMqttMessage(String topic, Object vo, String messageType, String username) {
     void sendMqttMessage(String topic, Object vo, String messageType, String username) {
-        String json = JSON.toJSONString(vo);
-        // 不再记录每条数据的详情,只记录发送操作
+        String json;
+        
+        // 针对楼层平面图特殊处理:将 byte[] 转为 Base64 字符串
+        if (vo instanceof com.usky.cdi.service.vo.info.FloorPlaneVO) {
+            json = serializeFloorPlaneVO((com.usky.cdi.service.vo.info.FloorPlaneVO) vo);
+        } else {
+            json = JSON.toJSONString(vo);
+        }
+        
+        log.info("发送MQTT消息,Topic: {}, 消息类型: {}, JSON长度: {}", topic, messageType, json.length());
+        
         MqttConnectionTool.MqttGateway gateway = mqttGatewayMap.get(username);
         MqttConnectionTool.MqttGateway gateway = mqttGatewayMap.get(username);
         if (gateway != null) {
         if (gateway != null) {
             gateway.sendToMqtt(topic, json);
             gateway.sendToMqtt(topic, json);
@@ -1163,6 +1169,36 @@ public class IotDataTransferService {
         }
         }
     }
     }
 
 
+    /**
+     * 序列化楼层平面图VO(将 floorFile byte[] 转为 Base64 字符串)
+     */
+    private String serializeFloorPlaneVO(com.usky.cdi.service.vo.info.FloorPlaneVO vo) {
+        com.alibaba.fastjson.JSONObject jsonObject = new com.alibaba.fastjson.JSONObject();
+        
+        jsonObject.put("dataPacketID", vo.getDataPacketID());
+        jsonObject.put("engineeringID", vo.getEngineeringID());
+        jsonObject.put("floor", vo.getFloor());
+        jsonObject.put("floorFileID", vo.getFloorFileID());
+        jsonObject.put("floorFileName", vo.getFloorFileName());
+        jsonObject.put("floorFileSuffix", vo.getFloorFileSuffix());
+        jsonObject.put("filePixWidth", vo.getFilePixWidth());
+        jsonObject.put("filePixHeight", vo.getFilePixHeight());
+        jsonObject.put("publishTime", vo.getPublishTime());
+        
+        // 关键:将 byte[] 转为 Base64 字符串
+        if (vo.getFloorFile() != null) {
+            String base64File = java.util.Base64.getEncoder().encodeToString(vo.getFloorFile());
+            jsonObject.put("floorFile", base64File);
+            log.info("平面图文件转换Base64成功,FileID: {}, 原始大小: {} bytes, Base64长度: {}", 
+                    vo.getFloorFileID(), vo.getFloorFile().length, base64File.length());
+        } else {
+            jsonObject.put("floorFile", "");
+            log.warn("平面图文件为空,FileID: {}", vo.getFloorFileID());
+        }
+        
+        return jsonObject.toJSONString();
+    }
+
     public void allData(Long engineeringId, String username, String password) {
     public void allData(Long engineeringId, String username, String password) {
         Integer tenantId = 0;
         Integer tenantId = 0;
         synchronizeDeviceData(tenantId, engineeringId, username, password);
         synchronizeDeviceData(tenantId, engineeringId, username, password);

+ 69 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/alarm/AlarmMessage1VO.java

@@ -0,0 +1,69 @@
+package com.usky.cdi.service.vo.alarm;
+
+import lombok.Data;
+
+@Data
+public class AlarmMessage1VO {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 数据包ID
+     */
+    private Long dataPacketID;
+
+    /**
+     * 人防工程ID
+     */
+    private Long engineeringID;
+
+    /**
+     * 事件ID
+     */
+    private Integer alarmID;
+
+    /**
+     * 事件来源
+     */
+    private Integer alarmSource;
+
+    /**
+     * 物联设施ID
+     */
+    private Integer sensorID;
+
+    /**
+     * 事件类型
+     */
+    private String alarmType;
+
+    /**
+     * 事件状态
+     */
+    private Integer alarmStatus;
+
+    /**
+     * 最新水浸状态
+     */
+    private Integer sensorValue;
+
+    /**
+     * 事件发生/更新时间
+     */
+    private String alarmUpdateTime;
+
+    /**
+     * 监测对象编号
+     */
+    private String monitorObjNo;
+
+    /**
+     * 事件描述
+     */
+    private String alarmDesc;
+
+    /**
+     * 上报时间
+     */
+    private String publishTime;
+
+}

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

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

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

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

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

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

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

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

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

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

+ 61 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAnalysisController.java

@@ -0,0 +1,61 @@
+package com.usky.ems.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.ems.service.EmsAnalysisService;
+import com.usky.ems.service.vo.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 能耗分析接口
+ * 趋势、指标、分类占比、区域分析、对比分析
+ */
+@RestController
+@RequestMapping("/prod-api/service-ems")
+public class EmsAnalysisController {
+
+    @Autowired
+    private EmsAnalysisService emsAnalysisService;
+
+    @GetMapping("/analysis/trend")
+    public ApiResult<EmsTrendResponse> getTrend(
+            @RequestParam(required = false) Long projectId,
+            @RequestParam String timeDimension,
+            @RequestParam String timeValue,
+            @RequestParam(required = false) Long energyTypeId) {
+        return ApiResult.success(emsAnalysisService.getTrend(projectId, timeDimension, timeValue, energyTypeId));
+    }
+
+    @GetMapping("/analysis/trend/indicators")
+    public ApiResult<EmsTrendIndicatorsResponse> getTrendIndicators(
+            @RequestParam(required = false) Long projectId,
+            @RequestParam String timeDimension,
+            @RequestParam String timeValue,
+            @RequestParam(required = false) Long energyTypeId) {
+        return ApiResult.success(emsAnalysisService.getTrendIndicators(projectId, timeDimension, timeValue, energyTypeId));
+    }
+
+    @GetMapping("/analysis/trend/category")
+    public ApiResult<EmsTrendCategoryResponse> getTrendCategory(
+            @RequestParam(required = false) Long projectId,
+            @RequestParam String timeDimension,
+            @RequestParam String timeValue,
+            @RequestParam(required = false) Long energyTypeId) {
+        return ApiResult.success(emsAnalysisService.getTrendCategory(projectId, timeDimension, timeValue, energyTypeId));
+    }
+
+    @GetMapping("/analysis/region")
+    public ApiResult<EmsRegionAnalysisResponse> getRegionAnalysis(
+            @RequestParam(required = false) Long projectId,
+            @RequestParam(required = false) String regionIds,
+            @RequestParam String timeDimension,
+            @RequestParam String timeValue,
+            @RequestParam(required = false) Long energyTypeId) {
+        return ApiResult.success(emsAnalysisService.getRegionAnalysis(projectId, regionIds, timeDimension, timeValue, energyTypeId));
+    }
+
+    @PostMapping("/analysis/compare")
+    public ApiResult<EmsCompareResponse> getCompare(@RequestBody EmsCompareRequest request) {
+        return ApiResult.success(emsAnalysisService.getCompare(request));
+    }
+}

+ 233 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsApiV1Controller.java

@@ -0,0 +1,233 @@
+package com.usky.ems.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.ems.service.EmsGatewayQueryService;
+import com.usky.ems.service.EmsModelService;
+import com.usky.ems.service.EmsOverviewService;
+import com.usky.ems.service.vo.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 能源能耗系统 API
+ *
+ * 对应 API 文档基础路径:/prod-api/service-ems
+ * GET  /prod-api/service-ems/overview/project           获取项目信息
+ * GET  /prod-api/service-ems/overview/summary            获取项目数据概括
+ * GET  /prod-api/service-ems/model/structure/tree        获取项目层级树
+ * GET  /prod-api/service-ems/model/energy-type/list      能源类型列表
+ * POST /prod-api/service-ems/model/building              新增建筑
+ * PUT  /prod-api/service-ems/model/building/{id}        编辑建筑
+ * DELETE /prod-api/service-ems/model/building/{id}      删除建筑
+ * POST /prod-api/service-ems/model/region                新增区域
+ * PUT  /prod-api/service-ems/model/region/{id}           编辑区域
+ * DELETE /prod-api/service-ems/model/region/{id}         删除区域
+ * POST /prod-api/service-ems/model/floor                 新增楼层
+ * PUT  /prod-api/service-ems/model/floor/{id}            编辑楼层
+ * DELETE /prod-api/service-ems/model/floor/{id}          删除楼层
+ * POST /prod-api/service-ems/model/gateway               新增网关
+ * PUT  /prod-api/service-ems/model/gateway/{id}          编辑网关
+ * DELETE /prod-api/service-ems/model/gateway/{id}        删除网关
+ * POST /prod-api/service-ems/model/channel               新增通道
+ * PUT  /prod-api/service-ems/model/channel/{id}          编辑通道
+ * DELETE /prod-api/service-ems/model/channel/{id}        删除通道
+ * POST /prod-api/service-ems/model/device                新增设备
+ * PUT  /prod-api/service-ems/model/device/{id}           编辑设备
+ * DELETE /prod-api/service-ems/model/device/{id}         删除设备
+ * POST /prod-api/service-ems/model/attribute-point      新增属性点位
+ * PUT  /prod-api/service-ems/model/attribute-point/{id}  编辑属性点位
+ * DELETE /prod-api/service-ems/model/attribute-point/{id} 删除属性点位
+ * GET  /prod-api/service-ems/device/gateway/list         网关列表(分页)
+ * GET  /prod-api/service-ems/device/gateway/{id}         网关详情
+ */
+@RestController
+@RequestMapping("/prod-api/service-ems")
+public class EmsApiV1Controller {
+
+    @Autowired
+    private EmsOverviewService emsOverviewService;
+    @Autowired
+    private EmsModelService emsModelService;
+    @Autowired
+    private EmsGatewayQueryService emsGatewayQueryService;
+
+    /**
+     * 获取项目信息
+     */
+    @GetMapping("/overview/project")
+    public ApiResult<EmsProjectResponse> getProject(@RequestParam(required = false) Long projectId) {
+        return ApiResult.success(emsOverviewService.getProject(projectId));
+    }
+
+    /**
+     * 获取项目数据概括(时间维度联动)
+     */
+    @GetMapping("/overview/summary")
+    public ApiResult<EmsSummaryResponse> getSummary(EmsSummaryRequest request) {
+        return ApiResult.success(emsOverviewService.getSummary(
+                request.getProjectId(), request.getTimeDimension(), request.getTimeValue()));
+    }
+
+    /**
+     * 获取项目层级树(建筑、区域、楼层、网关)
+     */
+    @GetMapping("/model/structure/tree")
+    public ApiResult<EmsStructureTreeNode> getStructureTree(
+            @RequestParam(required = false) Long projectId,
+            @RequestParam(required = false, defaultValue = "true") Boolean includeGateway) {
+        return ApiResult.success(emsModelService.getStructureTree(projectId, includeGateway));
+    }
+
+    /**
+     * 能源类型列表(电、水、气)
+     */
+    @GetMapping("/model/energy-type/list")
+    public ApiResult<List<EmsEnergyTypeVO>> getEnergyTypeList() {
+        return ApiResult.success(emsModelService.getEnergyTypeList());
+    }
+
+    @PostMapping("/model/building")
+    public ApiResult<EmsIdResponse> createBuilding(@RequestBody EmsModelSaveRequest request) {
+        Long id = emsModelService.createBuilding(request);
+        return ApiResult.success(new EmsIdResponse(id));
+    }
+
+    @PutMapping("/model/building/{id}")
+    public ApiResult<Void> updateBuilding(@PathVariable Long id, @RequestBody EmsModelSaveRequest request) {
+        emsModelService.updateBuilding(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping("/model/building/{id}")
+    public ApiResult<Void> deleteBuilding(@PathVariable Long id) {
+        emsModelService.deleteBuilding(id);
+        return ApiResult.success();
+    }
+
+    @PostMapping("/model/region")
+    public ApiResult<EmsIdResponse> createRegion(@RequestBody EmsModelSaveRequest request) {
+        Long id = emsModelService.createRegion(request);
+        return ApiResult.success(new EmsIdResponse(id));
+    }
+
+    @PutMapping("/model/region/{id}")
+    public ApiResult<Void> updateRegion(@PathVariable Long id, @RequestBody EmsModelSaveRequest request) {
+        emsModelService.updateRegion(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping("/model/region/{id}")
+    public ApiResult<Void> deleteRegion(@PathVariable Long id) {
+        emsModelService.deleteRegion(id);
+        return ApiResult.success();
+    }
+
+    @PostMapping("/model/floor")
+    public ApiResult<EmsIdResponse> createFloor(@RequestBody EmsModelSaveRequest request) {
+        Long id = emsModelService.createFloor(request);
+        return ApiResult.success(new EmsIdResponse(id));
+    }
+
+    @PutMapping("/model/floor/{id}")
+    public ApiResult<Void> updateFloor(@PathVariable Long id, @RequestBody EmsModelSaveRequest request) {
+        emsModelService.updateFloor(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping("/model/floor/{id}")
+    public ApiResult<Void> deleteFloor(@PathVariable Long id) {
+        emsModelService.deleteFloor(id);
+        return ApiResult.success();
+    }
+
+    @PostMapping("/model/gateway")
+    public ApiResult<EmsIdResponse> createGateway(@RequestBody EmsModelSaveRequest request) {
+        String id = emsModelService.createGateway(request);
+        return ApiResult.success(new EmsIdResponse(id));
+    }
+
+    @PutMapping("/model/gateway/{id}")
+    public ApiResult<Void> updateGateway(@PathVariable String id, @RequestBody EmsModelSaveRequest request) {
+        emsModelService.updateGateway(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping("/model/gateway/{id}")
+    public ApiResult<Void> deleteGateway(@PathVariable String id) {
+        emsModelService.deleteGateway(id);
+        return ApiResult.success();
+    }
+
+    @PostMapping("/model/channel")
+    public ApiResult<EmsIdResponse> createChannel(@RequestBody EmsModelSaveRequest request) {
+        Long id = emsModelService.createChannel(request);
+        return ApiResult.success(new EmsIdResponse(id));
+    }
+
+    @PutMapping("/model/channel/{id}")
+    public ApiResult<Void> updateChannel(@PathVariable Long id, @RequestBody EmsModelSaveRequest request) {
+        emsModelService.updateChannel(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping("/model/channel/{id}")
+    public ApiResult<Void> deleteChannel(@PathVariable Long id) {
+        emsModelService.deleteChannel(id);
+        return ApiResult.success();
+    }
+
+    @PostMapping("/model/device")
+    public ApiResult<EmsIdResponse> createDevice(@RequestBody EmsModelSaveRequest request) {
+        String id = emsModelService.createDevice(request);
+        return ApiResult.success(new EmsIdResponse(id));
+    }
+
+    @PutMapping("/model/device/{id}")
+    public ApiResult<Void> updateDevice(@PathVariable String id, @RequestBody EmsModelSaveRequest request) {
+        emsModelService.updateDevice(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping("/model/device/{id}")
+    public ApiResult<Void> deleteDevice(@PathVariable String id) {
+        emsModelService.deleteDevice(id);
+        return ApiResult.success();
+    }
+
+    @PostMapping("/model/attribute-point")
+    public ApiResult<EmsIdResponse> createAttributePoint(@RequestBody EmsModelSaveRequest request) {
+        Long id = emsModelService.createAttributePoint(request);
+        return ApiResult.success(new EmsIdResponse(id));
+    }
+
+    @PutMapping("/model/attribute-point/{id}")
+    public ApiResult<Void> updateAttributePoint(@PathVariable Long id, @RequestBody EmsModelSaveRequest request) {
+        emsModelService.updateAttributePoint(id, request);
+        return ApiResult.success();
+    }
+
+    @DeleteMapping("/model/attribute-point/{id}")
+    public ApiResult<Void> deleteAttributePoint(@PathVariable Long id) {
+        emsModelService.deleteAttributePoint(id);
+        return ApiResult.success();
+    }
+
+    /**
+     * 网关列表(分页)
+     */
+    @GetMapping("/device/gateway/list")
+    public ApiResult<CommonPage<EmsGatewayListItem>> gatewayList(EmsGatewayPageRequest request) {
+        return ApiResult.success(emsGatewayQueryService.listGateways(request));
+    }
+
+    /**
+     * 网关详情
+     */
+    @GetMapping("/device/gateway/{id}")
+    public ApiResult<EmsGatewayDetailResponse> getGateway(@PathVariable String id) {
+        return ApiResult.success(emsGatewayQueryService.getGatewayById(id));
+    }
+}

+ 37 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAuthController.java

@@ -0,0 +1,37 @@
+package com.usky.ems.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.ems.service.EmsAuthService;
+import com.usky.ems.service.vo.EmsLoginRequest;
+import com.usky.ems.service.vo.EmsLoginResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+/**
+ * 能源系统认证接口
+ *
+ * POST /prod-api/service-ems/auth/login  用户登录
+ * POST /prod-api/service-ems/auth/logout 退出登录
+ */
+@RestController
+@RequestMapping("/prod-api/service-ems")
+public class EmsAuthController {
+
+    @Autowired
+    private EmsAuthService emsAuthService;
+
+    @PostMapping("/auth/login")
+    public ApiResult<EmsLoginResponse> login(@RequestBody EmsLoginRequest request) {
+        EmsLoginResponse data = emsAuthService.login(request);
+        return ApiResult.success("登录成功", data);
+    }
+
+    @PostMapping("/auth/logout")
+    public ApiResult<Void> logout() {
+        emsAuthService.logout();
+        return ApiResult.success("退出成功");
+    }
+}

+ 106 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsReportController.java

@@ -0,0 +1,106 @@
+package com.usky.ems.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.ems.service.EmsReportService;
+import com.usky.ems.service.vo.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+
+/**
+ * 统计报表接口
+ * 能源报表、区域报表、采集报表
+ */
+@RestController
+@RequestMapping("/prod-api/service-ems")
+public class EmsReportController {
+
+    @Autowired
+    private EmsReportService emsReportService;
+
+    // ---------- 能源报表 ----------
+    @GetMapping("/report/energy/devices")
+    public ApiResult<EmsReportDevicesResponse> getEnergyDevices(
+            @RequestParam Long energyTypeId,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Long projectId) {
+        return ApiResult.success(emsReportService.getEnergyDevices(energyTypeId, keyword, projectId));
+    }
+
+    @PostMapping("/report/energy/statistics")
+    public ApiResult<EmsEnergyStatisticsResponse> getEnergyStatistics(@RequestBody EmsEnergyStatisticsRequest request) {
+        return ApiResult.success(emsReportService.getEnergyStatistics(request));
+    }
+
+    @GetMapping("/report/energy/export")
+    public void exportEnergy(
+            @RequestParam String deviceIds,
+            @RequestParam(required = false) String attributePointIds,
+            @RequestParam String timeDimension,
+            @RequestParam String timeValue,
+            @RequestParam(required = false, defaultValue = "excel") String format,
+            HttpServletResponse response) {
+        emsReportService.exportEnergy(deviceIds, attributePointIds, timeDimension, timeValue, format, response);
+    }
+
+    // ---------- 区域报表 ----------
+    @GetMapping("/report/region/devices")
+    public ApiResult<EmsReportDevicesResponse> getRegionDevices(
+            @RequestParam Long energyTypeId,
+            @RequestParam(required = false) Long regionId,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Long projectId) {
+        return ApiResult.success(emsReportService.getRegionDevices(energyTypeId, regionId, keyword, projectId));
+    }
+
+    @PostMapping("/report/region/statistics")
+    public ApiResult<EmsEnergyStatisticsResponse> getRegionStatistics(
+            @RequestBody EmsEnergyStatisticsRequest request,
+            @RequestParam(required = false) List<Long> regionIds) {
+        return ApiResult.success(emsReportService.getRegionStatistics(request, regionIds));
+    }
+
+    @GetMapping("/report/region/export")
+    public void exportRegion(
+            @RequestParam String deviceIds,
+            @RequestParam(required = false) String attributePointIds,
+            @RequestParam String timeDimension,
+            @RequestParam String timeValue,
+            @RequestParam(required = false) String regionIds,
+            @RequestParam(required = false, defaultValue = "excel") String format,
+            HttpServletResponse response) {
+        emsReportService.exportRegion(deviceIds, attributePointIds, timeDimension, timeValue, regionIds, format, response);
+    }
+
+    // ---------- 采集报表 ----------
+    @GetMapping("/report/collection/devices")
+    public ApiResult<EmsReportDevicesResponse> getCollectionDevices(
+            @RequestParam Long energyTypeId,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) Long projectId) {
+        return ApiResult.success(emsReportService.getCollectionDevices(energyTypeId, keyword, projectId));
+    }
+
+    @PostMapping("/report/collection/realtime")
+    public ApiResult<EmsCollectionRealtimeResponse> getCollectionRealtime(@RequestBody EmsCollectionRealtimeRequest request) {
+        return ApiResult.success(emsReportService.getCollectionRealtime(request));
+    }
+
+    @PostMapping("/report/collection/statistics")
+    public ApiResult<EmsEnergyStatisticsResponse> getCollectionStatistics(@RequestBody EmsEnergyStatisticsRequest request) {
+        return ApiResult.success(emsReportService.getCollectionStatistics(request));
+    }
+
+    @GetMapping("/report/collection/export")
+    public void exportCollection(
+            @RequestParam String deviceIds,
+            @RequestParam(required = false) String attributePointIds,
+            @RequestParam String timeDimension,
+            @RequestParam String timeValue,
+            @RequestParam(required = false, defaultValue = "excel") String format,
+            HttpServletResponse response) {
+        emsReportService.exportCollection(deviceIds, attributePointIds, timeDimension, timeValue, format, response);
+    }
+}

+ 39 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsChannel.java

@@ -0,0 +1,39 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 网关通道(leo.ems_channel)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_channel")
+public class EmsChannel implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("gateway_id")
+    private String gatewayId;
+    private String name;
+    @TableField("channel_type_id")
+    private Integer channelTypeId;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 69 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDevice.java

@@ -0,0 +1,69 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 设备(leo.ems_device)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_device")
+public class EmsDevice implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.INPUT)
+    private String id;
+
+    @TableField("project_id")
+    private Long projectId;
+    private String number;
+    private String name;
+    @TableField("product_id")
+    private Long productId;
+    @TableField("product_template_id")
+    private Long productTemplateId;
+    @TableField("installation_location")
+    private Long installationLocation;
+    @TableField("monitoring_location")
+    private Long monitoringLocation;
+    private String location;
+    @TableField("comm_address")
+    private String commAddress;
+    @TableField("channel_id")
+    private Long channelId;
+    @TableField("gateway_id")
+    private String gatewayId;
+    @TableField("virtual_device")
+    private Integer virtualDevice;
+    private Integer focus;
+    @TableField("device_system")
+    private Integer deviceSystem;
+    private Integer status;
+    @TableField("comm_status")
+    private Integer commStatus;
+    @TableField("comm_status_code")
+    private String commStatusCode;
+    @TableField("online_time")
+    private LocalDateTime onlineTime;
+    @TableField("offline_time")
+    private LocalDateTime offlineTime;
+    @TableField("external_id")
+    private String externalId;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 50 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDeviceFunction.java

@@ -0,0 +1,50 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 设备功能数据/属性点位(leo.ems_device_function)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_device_function")
+public class EmsDeviceFunction implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("product_id")
+    private Long productId;
+    @TableField("product_template_id")
+    private Long productTemplateId;
+    @TableField("device_id")
+    private String deviceId;
+    private String identifier;
+    private String name;
+    private String value;
+    @TableField("acq_time")
+    private LocalDateTime acqTime;
+    private BigDecimal ratio;
+    private Integer preservable;
+    @TableField("binding_acq")
+    private Integer bindingAcq;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 44 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsEnergyItemCode.java

@@ -0,0 +1,44 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 能源分项编码(leo.ems_energy_item_code),用于能源类型列表
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_energy_item_code")
+public class EmsEnergyItemCode implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private String code;
+    @TableField("parent_code")
+    private String parentCode;
+    private String unit;
+    @TableField("unit_name")
+    private String unitName;
+    private String name;
+    private String identifier;
+    @TableField("energy_type")
+    private Integer energyType;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 64 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsGateway.java

@@ -0,0 +1,64 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 网关(leo.ems_gateway)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_gateway")
+public class EmsGateway implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.INPUT)
+    private String id;
+
+    @TableField("project_id")
+    private Long projectId;
+    private String name;
+    @TableField("space_id")
+    private Long spaceId;
+    private String version;
+    private String type;
+    private String ip;
+    private Integer port;
+    @TableField("comm_status")
+    private Integer commStatus;
+    @TableField("online_time")
+    private LocalDateTime onlineTime;
+    @TableField("offline_time")
+    private LocalDateTime offlineTime;
+    @TableField("update_config_time")
+    private LocalDateTime updateConfigTime;
+    @TableField("update_protocol_time")
+    private LocalDateTime updateProtocolTime;
+    @TableField("upgrade_time")
+    private LocalDateTime upgradeTime;
+    @TableField("data_center_id")
+    private Long dataCenterId;
+    private String iccid;
+    private Integer rssi;
+    @TableField("secret_key")
+    private String secretKey;
+    @TableField("virtual_device")
+    private Integer virtualDevice;
+    private String remark;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 71 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProject.java

@@ -0,0 +1,71 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 项目(leo.ems_project)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_project")
+public class EmsProject implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("space_id")
+    private Long spaceId;
+    private String name;
+    @TableField("platform_name")
+    private String platformName;
+    private String abbreviation;
+    private BigDecimal area;
+    @TableField("common_area")
+    private BigDecimal commonArea;
+    @TableField("air_conditioned_area")
+    private BigDecimal airConditionedArea;
+    @TableField("resident_population")
+    private Integer residentPopulation;
+    @TableField("province_code")
+    private String provinceCode;
+    @TableField("province_name")
+    private String provinceName;
+    @TableField("city_code")
+    private String cityCode;
+    @TableField("city_name")
+    private String cityName;
+    @TableField("district_code")
+    private String districtCode;
+    @TableField("district_name")
+    private String districtName;
+    private String location;
+    private String address;
+    @TableField("type_id")
+    private Integer typeId;
+    @TableField("type_name")
+    private String typeName;
+    private String image;
+    private String introduction;
+    private String logo;
+    @TableField("logo_min")
+    private String logoMin;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 45 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpace.java

@@ -0,0 +1,45 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 空间(leo.ems_space)
+ * 空间类型:1项目 2区域 3建筑 4楼层 5房间
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_space")
+public class EmsSpace implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private String name;
+    @TableField("parent_id")
+    private Long parentId;
+    private Integer type;
+    @TableField("root_id")
+    private Long rootId;
+    private String path;
+    @TableField("path_name")
+    private String pathName;
+    private Integer deep;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 46 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpaceArea.java

@@ -0,0 +1,46 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 区域(leo.ems_space_area)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_space_area")
+public class EmsSpaceArea implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("space_id")
+    private Long spaceId;
+    private String name;
+    private Integer type;
+    private BigDecimal area;
+    @TableField("common_area")
+    private BigDecimal commonArea;
+    @TableField("air_conditioned_area")
+    private BigDecimal airConditionedArea;
+    @TableField("resident_population")
+    private Integer residentPopulation;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 67 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpaceBuilding.java

@@ -0,0 +1,67 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 建筑(leo.ems_space_building)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_space_building")
+public class EmsSpaceBuilding implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("space_id")
+    private Long spaceId;
+    private String name;
+    private BigDecimal area;
+    @TableField("common_area")
+    private BigDecimal commonArea;
+    @TableField("air_conditioned_area")
+    private BigDecimal airConditionedArea;
+    @TableField("resident_population")
+    private Integer residentPopulation;
+    @TableField("province_code")
+    private String provinceCode;
+    @TableField("province_name")
+    private String provinceName;
+    @TableField("city_code")
+    private String cityCode;
+    @TableField("city_name")
+    private String cityName;
+    @TableField("district_code")
+    private String districtCode;
+    @TableField("district_name")
+    private String districtName;
+    private String location;
+    private String address;
+    private Integer floor;
+    private BigDecimal height;
+    @TableField("type_id")
+    private Integer typeId;
+    @TableField("type_name")
+    private String typeName;
+    private String image;
+    private String introduction;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 45 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpaceFloor.java

@@ -0,0 +1,45 @@
+package com.usky.ems.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 楼层(leo.ems_space_floor)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_space_floor")
+public class EmsSpaceFloor implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("space_id")
+    private Long spaceId;
+    private String name;
+    private BigDecimal area;
+    @TableField("common_area")
+    private BigDecimal commonArea;
+    @TableField("air_conditioned_area")
+    private BigDecimal airConditionedArea;
+    @TableField("resident_population")
+    private Integer residentPopulation;
+    @TableField("updated_by")
+    private Long updatedBy;
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+    @TableField("created_by")
+    private Long createdBy;
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsChannelMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsChannel;
+
+/**
+ * 通道 Mapper(leo.ems_channel)
+ */
+public interface EmsChannelMapper extends CrudMapper<EmsChannel> {
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDeviceFunctionMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsDeviceFunction;
+
+/**
+ * 设备功能/属性点位 Mapper(leo.ems_device_function)
+ */
+public interface EmsDeviceFunctionMapper extends CrudMapper<EmsDeviceFunction> {
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDeviceMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsDevice;
+
+/**
+ * 设备 Mapper(leo.ems_device)
+ */
+public interface EmsDeviceMapper extends CrudMapper<EmsDevice> {
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsEnergyItemCodeMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsEnergyItemCode;
+
+/**
+ * 能源分项编码 Mapper(leo.ems_energy_item_code)
+ */
+public interface EmsEnergyItemCodeMapper extends CrudMapper<EmsEnergyItemCode> {
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsGatewayMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsGateway;
+
+/**
+ * 网关 Mapper(leo.ems_gateway)
+ */
+public interface EmsGatewayMapper extends CrudMapper<EmsGateway> {
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsProject;
+
+/**
+ * 项目 Mapper(leo.ems_project)
+ */
+public interface EmsProjectMapper extends CrudMapper<EmsProject> {
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceAreaMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsSpaceArea;
+
+/**
+ * 区域 Mapper(leo.ems_space_area)
+ */
+public interface EmsSpaceAreaMapper extends CrudMapper<EmsSpaceArea> {
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceBuildingMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsSpaceBuilding;
+
+/**
+ * 建筑 Mapper(leo.ems_space_building)
+ */
+public interface EmsSpaceBuildingMapper extends CrudMapper<EmsSpaceBuilding> {
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceFloorMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsSpaceFloor;
+
+/**
+ * 楼层 Mapper(leo.ems_space_floor)
+ */
+public interface EmsSpaceFloorMapper extends CrudMapper<EmsSpaceFloor> {
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceMapper.java

@@ -0,0 +1,10 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsSpace;
+
+/**
+ * 空间 Mapper(leo.ems_space)
+ */
+public interface EmsSpaceMapper extends CrudMapper<EmsSpace> {
+}

+ 19 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsAnalysisService.java

@@ -0,0 +1,19 @@
+package com.usky.ems.service;
+
+import com.usky.ems.service.vo.*;
+
+/**
+ * 能耗分析服务:趋势、指标、分类占比、区域分析、对比分析
+ */
+public interface EmsAnalysisService {
+
+    EmsTrendResponse getTrend(Long projectId, String timeDimension, String timeValue, Long energyTypeId);
+
+    EmsTrendIndicatorsResponse getTrendIndicators(Long projectId, String timeDimension, String timeValue, Long energyTypeId);
+
+    EmsTrendCategoryResponse getTrendCategory(Long projectId, String timeDimension, String timeValue, Long energyTypeId);
+
+    EmsRegionAnalysisResponse getRegionAnalysis(Long projectId, String regionIds, String timeDimension, String timeValue, Long energyTypeId);
+
+    EmsCompareResponse getCompare(EmsCompareRequest request);
+}

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsAuthService.java

@@ -0,0 +1,14 @@
+package com.usky.ems.service;
+
+import com.usky.ems.service.vo.EmsLoginRequest;
+import com.usky.ems.service.vo.EmsLoginResponse;
+
+/**
+ * 能源系统认证服务(登录/登出)
+ */
+public interface EmsAuthService {
+
+    EmsLoginResponse login(EmsLoginRequest request);
+
+    void logout();
+}

+ 22 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsGatewayQueryService.java

@@ -0,0 +1,22 @@
+package com.usky.ems.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.ems.service.vo.EmsGatewayDetailResponse;
+import com.usky.ems.service.vo.EmsGatewayListItem;
+import com.usky.ems.service.vo.EmsGatewayPageRequest;
+
+/**
+ * 网关列表与详情查询服务
+ */
+public interface EmsGatewayQueryService {
+
+    /**
+     * 分页查询网关列表
+     */
+    CommonPage<EmsGatewayListItem> listGateways(EmsGatewayPageRequest request);
+
+    /**
+     * 网关详情
+     */
+    EmsGatewayDetailResponse getGatewayById(String id);
+}

+ 72 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsModelService.java

@@ -0,0 +1,72 @@
+package com.usky.ems.service;
+
+import com.usky.ems.service.vo.EmsEnergyTypeVO;
+import com.usky.ems.service.vo.EmsModelSaveRequest;
+import com.usky.ems.service.vo.EmsStructureTreeNode;
+
+import java.util.List;
+
+/**
+ * 基础建模服务(model:结构树、能源类型、建筑/区域/楼层/网关/通道/设备/属性点位)
+ */
+public interface EmsModelService {
+
+    /**
+     * 获取项目层级树(建筑、区域、楼层、网关)
+     */
+    EmsStructureTreeNode getStructureTree(Long projectId, Boolean includeGateway);
+
+    /**
+     * 能源类型列表(电、水、气)
+     */
+    List<EmsEnergyTypeVO> getEnergyTypeList();
+
+    /** 建筑:新增 */
+    Long createBuilding(EmsModelSaveRequest request);
+    /** 建筑:编辑 */
+    void updateBuilding(Long id, EmsModelSaveRequest request);
+    /** 建筑:删除 */
+    void deleteBuilding(Long id);
+
+    /** 区域:新增 */
+    Long createRegion(EmsModelSaveRequest request);
+    /** 区域:编辑 */
+    void updateRegion(Long id, EmsModelSaveRequest request);
+    /** 区域:删除 */
+    void deleteRegion(Long id);
+
+    /** 楼层:新增 */
+    Long createFloor(EmsModelSaveRequest request);
+    /** 楼层:编辑 */
+    void updateFloor(Long id, EmsModelSaveRequest request);
+    /** 楼层:删除 */
+    void deleteFloor(Long id);
+
+    /** 网关:新增 */
+    String createGateway(EmsModelSaveRequest request);
+    /** 网关:编辑 */
+    void updateGateway(String id, EmsModelSaveRequest request);
+    /** 网关:删除 */
+    void deleteGateway(String id);
+
+    /** 通道:新增 */
+    Long createChannel(EmsModelSaveRequest request);
+    /** 通道:编辑 */
+    void updateChannel(Long id, EmsModelSaveRequest request);
+    /** 通道:删除 */
+    void deleteChannel(Long id);
+
+    /** 设备:新增 */
+    String createDevice(EmsModelSaveRequest request);
+    /** 设备:编辑 */
+    void updateDevice(String id, EmsModelSaveRequest request);
+    /** 设备:删除 */
+    void deleteDevice(String id);
+
+    /** 属性点位:新增 */
+    Long createAttributePoint(EmsModelSaveRequest request);
+    /** 属性点位:编辑 */
+    void updateAttributePoint(Long id, EmsModelSaveRequest request);
+    /** 属性点位:删除 */
+    void deleteAttributePoint(Long id);
+}

+ 20 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsOverviewService.java

@@ -0,0 +1,20 @@
+package com.usky.ems.service;
+
+import com.usky.ems.service.vo.EmsProjectResponse;
+import com.usky.ems.service.vo.EmsSummaryResponse;
+
+/**
+ * 能源总览服务(overview)
+ */
+public interface EmsOverviewService {
+
+    /**
+     * 获取项目信息(当前项目或指定 projectId)
+     */
+    EmsProjectResponse getProject(Long projectId);
+
+    /**
+     * 获取项目数据概括(时间维度联动)
+     */
+    EmsSummaryResponse getSummary(Long projectId, String timeDimension, String timeValue);
+}

+ 32 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsReportService.java

@@ -0,0 +1,32 @@
+package com.usky.ems.service;
+
+import com.usky.ems.service.vo.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+
+/**
+ * 统计报表服务:能源报表、区域报表、采集报表
+ */
+public interface EmsReportService {
+
+    EmsReportDevicesResponse getEnergyDevices(Long energyTypeId, String keyword, Long projectId);
+
+    EmsEnergyStatisticsResponse getEnergyStatistics(EmsEnergyStatisticsRequest request);
+
+    void exportEnergy(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String format, HttpServletResponse response);
+
+    EmsReportDevicesResponse getRegionDevices(Long energyTypeId, Long regionId, String keyword, Long projectId);
+
+    EmsEnergyStatisticsResponse getRegionStatistics(EmsEnergyStatisticsRequest request, List<Long> regionIds);
+
+    void exportRegion(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String regionIds, String format, HttpServletResponse response);
+
+    EmsReportDevicesResponse getCollectionDevices(Long energyTypeId, String keyword, Long projectId);
+
+    EmsCollectionRealtimeResponse getCollectionRealtime(EmsCollectionRealtimeRequest request);
+
+    EmsEnergyStatisticsResponse getCollectionStatistics(EmsEnergyStatisticsRequest request);
+
+    void exportCollection(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String format, HttpServletResponse response);
+}

+ 61 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAnalysisServiceImpl.java

@@ -0,0 +1,61 @@
+package com.usky.ems.service.impl;
+
+import com.usky.ems.service.EmsAnalysisService;
+import com.usky.ems.service.vo.*;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+
+/**
+ * 能耗分析服务实现(占位数据,后续对接时序/聚合数据)
+ */
+@Service
+public class EmsAnalysisServiceImpl implements EmsAnalysisService {
+
+    private static final String[] ENERGY_TYPE_NAMES = {"", "电", "水", "气"};
+
+    @Override
+    public EmsTrendResponse getTrend(Long projectId, String timeDimension, String timeValue, Long energyTypeId) {
+        EmsTrendResponse resp = new EmsTrendResponse();
+        resp.setTimeDimension(timeDimension);
+        resp.setTimeValue(timeValue);
+        return resp;
+    }
+
+    @Override
+    public EmsTrendIndicatorsResponse getTrendIndicators(Long projectId, String timeDimension, String timeValue, Long energyTypeId) {
+        EmsTrendIndicatorsResponse resp = new EmsTrendIndicatorsResponse();
+        resp.setTimeDimension(timeDimension);
+        resp.setTimeValue(timeValue);
+        resp.setTotalUsage(BigDecimal.ZERO);
+        resp.setStandardCoal(BigDecimal.ZERO);
+        resp.setCarbonEmission(BigDecimal.ZERO);
+        resp.setAreaUsage(BigDecimal.ZERO);
+        resp.setPerCapitaUsage(BigDecimal.ZERO);
+        return resp;
+    }
+
+    @Override
+    public EmsTrendCategoryResponse getTrendCategory(Long projectId, String timeDimension, String timeValue, Long energyTypeId) {
+        EmsTrendCategoryResponse resp = new EmsTrendCategoryResponse();
+        resp.setTimeDimension(timeDimension);
+        resp.setTimeValue(timeValue);
+        return resp;
+    }
+
+    @Override
+    public EmsRegionAnalysisResponse getRegionAnalysis(Long projectId, String regionIds, String timeDimension, String timeValue, Long energyTypeId) {
+        return new EmsRegionAnalysisResponse();
+    }
+
+    @Override
+    public EmsCompareResponse getCompare(EmsCompareRequest request) {
+        EmsCompareResponse resp = new EmsCompareResponse();
+        if (request != null && request.getEnergyTypeId() != null) {
+            long id = request.getEnergyTypeId();
+            resp.setEnergyTypeName(id >= 1 && id <= 3 ? ENERGY_TYPE_NAMES[(int) id] : "");
+        }
+        if (request != null) resp.setDimension(request.getTimeDimension());
+        return resp;
+    }
+}

+ 30 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAuthServiceImpl.java

@@ -0,0 +1,30 @@
+package com.usky.ems.service.impl;
+
+import com.usky.ems.service.EmsAuthService;
+import com.usky.ems.service.vo.EmsLoginRequest;
+import com.usky.ems.service.vo.EmsLoginResponse;
+import org.springframework.stereotype.Service;
+
+import java.util.UUID;
+
+/**
+ * 能源系统认证服务实现(预留与统一认证对接,当前返回占位 token)
+ */
+@Service
+public class EmsAuthServiceImpl implements EmsAuthService {
+
+    private static final long EXPIRES_IN_SECONDS = 7200L;
+
+    @Override
+    public EmsLoginResponse login(EmsLoginRequest request) {
+        EmsLoginResponse resp = new EmsLoginResponse();
+        resp.setAccessToken("Bearer " + UUID.randomUUID().toString().replace("-", ""));
+        resp.setExpiresIn(EXPIRES_IN_SECONDS);
+        return resp;
+    }
+
+    @Override
+    public void logout() {
+        // 预留:使当前 token 失效
+    }
+}

+ 87 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsGatewayQueryServiceImpl.java

@@ -0,0 +1,87 @@
+package com.usky.ems.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.ems.domain.EmsGateway;
+import com.usky.ems.mapper.EmsGatewayMapper;
+import com.usky.ems.mapper.EmsSpaceMapper;
+import com.usky.ems.service.EmsGatewayQueryService;
+import com.usky.ems.service.vo.EmsGatewayDetailResponse;
+import com.usky.ems.service.vo.EmsGatewayListItem;
+import com.usky.ems.service.vo.EmsGatewayPageRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 网关列表与详情查询服务实现
+ */
+@Service
+public class EmsGatewayQueryServiceImpl implements EmsGatewayQueryService {
+
+    @Autowired
+    private EmsGatewayMapper emsGatewayMapper;
+    @Autowired
+    private EmsSpaceMapper emsSpaceMapper;
+
+    @Override
+    public CommonPage<EmsGatewayListItem> listGateways(EmsGatewayPageRequest request) {
+        int current = request.getCurrent() == null ? 1 : request.getCurrent();
+        int size = request.getSize() == null ? 10 : request.getSize();
+        Page<EmsGateway> p = new Page<>(current, size);
+        LambdaQueryWrapper<EmsGateway> q = new LambdaQueryWrapper<>();
+        if (request.getCommunicationStatus() != null) {
+            q.eq(EmsGateway::getCommStatus, request.getCommunicationStatus());
+        }
+        if (StringUtils.hasText(request.getName())) {
+            q.like(EmsGateway::getName, request.getName());
+        }
+        if (StringUtils.hasText(request.getCode())) {
+            q.eq(EmsGateway::getId, request.getCode());
+        }
+        Page<EmsGateway> page = emsGatewayMapper.selectPage(p, q);
+        List<EmsGatewayListItem> list = page.getRecords().stream().map(this::toListItem).collect(Collectors.toList());
+        return new CommonPage<>(list, page.getTotal(), size, current);
+    }
+
+    @Override
+    public EmsGatewayDetailResponse getGatewayById(String id) {
+        EmsGateway g = emsGatewayMapper.selectById(id);
+        if (g == null) return null;
+        EmsGatewayDetailResponse resp = new EmsGatewayDetailResponse();
+        resp.setId(g.getId());
+        resp.setName(g.getName());
+        resp.setSpaceId(g.getSpaceId());
+        resp.setCommunicationStatus(g.getCommStatus());
+        resp.setOnlineTime(g.getOnlineTime());
+        resp.setCreateTime(g.getCreateTime());
+        resp.setUpdateTime(g.getUpdateTime());
+        if (g.getSpaceId() != null) {
+            com.usky.ems.domain.EmsSpace space = emsSpaceMapper.selectById(g.getSpaceId());
+            if (space != null) {
+                resp.setFloorName(space.getName());
+            }
+        }
+        return resp;
+    }
+
+    private EmsGatewayListItem toListItem(EmsGateway g) {
+        EmsGatewayListItem item = new EmsGatewayListItem();
+        item.setId(g.getId());
+        item.setName(g.getName());
+        item.setSpaceId(g.getSpaceId());
+        item.setCommunicationStatus(g.getCommStatus());
+        item.setOnlineTime(g.getOnlineTime());
+        item.setCreateTime(g.getCreateTime());
+        item.setUpdateTime(g.getUpdateTime());
+        if (g.getSpaceId() != null) {
+            com.usky.ems.domain.EmsSpace space = emsSpaceMapper.selectById(g.getSpaceId());
+            if (space != null) item.setFloorName(space.getName());
+        }
+        return item;
+    }
+}

+ 401 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsModelServiceImpl.java

@@ -0,0 +1,401 @@
+package com.usky.ems.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.ems.domain.*;
+import com.usky.ems.mapper.*;
+import com.usky.ems.service.EmsModelService;
+import com.usky.ems.service.vo.EmsEnergyTypeVO;
+import com.usky.ems.service.vo.EmsModelSaveRequest;
+import com.usky.ems.service.vo.EmsStructureTreeNode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+
+/**
+ * 基础建模服务实现(结构树、能源类型、建筑/区域/楼层/网关/通道/设备/属性点位 CRUD)
+ */
+@Service
+public class EmsModelServiceImpl implements EmsModelService {
+
+    @Autowired
+    private EmsProjectMapper emsProjectMapper;
+    @Autowired
+    private EmsSpaceMapper emsSpaceMapper;
+    @Autowired
+    private EmsSpaceBuildingMapper emsSpaceBuildingMapper;
+    @Autowired
+    private EmsSpaceAreaMapper emsSpaceAreaMapper;
+    @Autowired
+    private EmsSpaceFloorMapper emsSpaceFloorMapper;
+    @Autowired
+    private EmsGatewayMapper emsGatewayMapper;
+    @Autowired
+    private EmsChannelMapper emsChannelMapper;
+    @Autowired
+    private EmsDeviceMapper emsDeviceMapper;
+    @Autowired
+    private EmsDeviceFunctionMapper emsDeviceFunctionMapper;
+    @Autowired
+    private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
+
+    private static final int SPACE_TYPE_PROJECT = 1;
+    private static final int SPACE_TYPE_REGION = 2;
+    private static final int SPACE_TYPE_BUILDING = 3;
+    private static final int SPACE_TYPE_FLOOR = 4;
+    private static final int SPACE_TYPE_ROOM = 5;
+
+    private EmsProject firstProject() {
+        List<EmsProject> list = emsProjectMapper.selectList(null);
+        return list.isEmpty() ? null : list.get(0);
+    }
+
+    @Override
+    public EmsStructureTreeNode getStructureTree(Long projectId, Boolean includeGateway) {
+        EmsProject project = projectId != null ? emsProjectMapper.selectById(projectId) : firstProject();
+        if (project == null) {
+            return null;
+        }
+        EmsStructureTreeNode root = new EmsStructureTreeNode();
+        root.setId(project.getId());
+        root.setName(project.getName());
+        root.setType("project");
+        root.setChildren(buildSpaceChildren(project.getSpaceId(), includeGateway == null || includeGateway));
+        return root;
+    }
+
+    private List<EmsStructureTreeNode> buildSpaceChildren(Long parentId, boolean includeGateway) {
+        if (parentId == null) return Collections.emptyList();
+        List<EmsSpace> list = emsSpaceMapper.selectList(new LambdaQueryWrapper<EmsSpace>().eq(EmsSpace::getParentId, parentId));
+        List<EmsStructureTreeNode> children = new ArrayList<>();
+        for (EmsSpace s : list) {
+            EmsStructureTreeNode node = new EmsStructureTreeNode();
+            node.setId(s.getId());
+            node.setName(s.getName());
+            if (s.getType() == SPACE_TYPE_BUILDING) {
+                node.setType("building");
+                EmsSpaceBuilding b = emsSpaceBuildingMapper.selectOne(new LambdaQueryWrapper<EmsSpaceBuilding>().eq(EmsSpaceBuilding::getSpaceId, s.getId()));
+                if (b != null) node.setName(b.getName());
+            } else if (s.getType() == SPACE_TYPE_REGION) {
+                node.setType("region");
+                EmsSpaceArea a = emsSpaceAreaMapper.selectOne(new LambdaQueryWrapper<EmsSpaceArea>().eq(EmsSpaceArea::getSpaceId, s.getId()));
+                if (a != null) node.setName(a.getName());
+            } else if (s.getType() == SPACE_TYPE_FLOOR) {
+                node.setType("floor");
+                EmsSpaceFloor f = emsSpaceFloorMapper.selectOne(new LambdaQueryWrapper<EmsSpaceFloor>().eq(EmsSpaceFloor::getSpaceId, s.getId()));
+                if (f != null) node.setName(f.getName());
+            } else {
+                node.setType("space");
+            }
+            List<EmsStructureTreeNode> sub = buildSpaceChildren(s.getId(), includeGateway);
+            if (includeGateway && (s.getType() == SPACE_TYPE_FLOOR || s.getType() == SPACE_TYPE_ROOM)) {
+                List<EmsGateway> gateways = emsGatewayMapper.selectList(new LambdaQueryWrapper<EmsGateway>().eq(EmsGateway::getSpaceId, s.getId()));
+                for (EmsGateway g : gateways) {
+                    EmsStructureTreeNode gw = new EmsStructureTreeNode();
+                    gw.setId(g.getId());
+                    gw.setName(g.getName());
+                    gw.setType("gateway");
+                    sub.add(gw);
+                }
+            }
+            node.setChildren(sub);
+            children.add(node);
+        }
+        return children;
+    }
+
+    @Override
+    public List<EmsEnergyTypeVO> getEnergyTypeList() {
+        String[] names = {"", "电", "水", "气"};
+        String[] codes = {"", "electric", "water", "gas"};
+        String[] units = {"", "kWh", "m³", "m³"};
+        List<EmsEnergyTypeVO> result = new ArrayList<>();
+        for (int i = 1; i <= 3; i++) {
+            EmsEnergyTypeVO vo = new EmsEnergyTypeVO();
+            vo.setId((long) i);
+            vo.setName(names[i]);
+            vo.setCode(codes[i]);
+            vo.setUnit(units[i]);
+            vo.setSortOrder(i - 1);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createBuilding(EmsModelSaveRequest request) {
+        Long projectId = request.getProjectId();
+        String name = request.getName();
+        EmsProject project = projectId != null ? emsProjectMapper.selectById(projectId) : firstProject();
+        if (project == null) return null;
+        EmsSpace space = new EmsSpace();
+        space.setName(name);
+        space.setParentId(project.getSpaceId());
+        space.setType(SPACE_TYPE_BUILDING);
+        space.setRootId(project.getSpaceId());
+        emsSpaceMapper.insert(space);
+        EmsSpaceBuilding building = new EmsSpaceBuilding();
+        building.setSpaceId(space.getId());
+        building.setName(name);
+        emsSpaceBuildingMapper.insert(building);
+        return space.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateBuilding(Long id, EmsModelSaveRequest request) {
+        String name = request.getName();
+        EmsSpace space = emsSpaceMapper.selectById(id);
+        if (space != null && name != null) {
+            space.setName(name);
+            emsSpaceMapper.updateById(space);
+        }
+        EmsSpaceBuilding b = emsSpaceBuildingMapper.selectOne(new LambdaQueryWrapper<EmsSpaceBuilding>().eq(EmsSpaceBuilding::getSpaceId, id));
+        if (b != null && name != null) {
+            b.setName(name);
+            emsSpaceBuildingMapper.updateById(b);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteBuilding(Long id) {
+        emsSpaceBuildingMapper.delete(new LambdaQueryWrapper<EmsSpaceBuilding>().eq(EmsSpaceBuilding::getSpaceId, id));
+        emsSpaceMapper.deleteById(id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createRegion(EmsModelSaveRequest request) {
+        Long buildingId = request.getBuildingId();
+        String name = request.getName();
+        EmsSpace space = new EmsSpace();
+        space.setName(name);
+        space.setParentId(buildingId);
+        EmsSpace parent = emsSpaceMapper.selectById(buildingId);
+        space.setRootId(parent != null ? parent.getRootId() : buildingId);
+        space.setType(SPACE_TYPE_REGION);
+        emsSpaceMapper.insert(space);
+        EmsSpaceArea area = new EmsSpaceArea();
+        area.setSpaceId(space.getId());
+        area.setName(name);
+        if (request.getArea() != null) area.setArea(request.getArea());
+        return space.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateRegion(Long id, EmsModelSaveRequest request) {
+        String name = request.getName();
+        EmsSpace space = emsSpaceMapper.selectById(id);
+        if (space != null && name != null) {
+            space.setName(name);
+            emsSpaceMapper.updateById(space);
+        }
+        EmsSpaceArea a = emsSpaceAreaMapper.selectOne(new LambdaQueryWrapper<EmsSpaceArea>().eq(EmsSpaceArea::getSpaceId, id));
+        if (a != null) {
+            if (name != null) a.setName(name);
+            if (request.getArea() != null) a.setArea(request.getArea());
+            emsSpaceAreaMapper.updateById(a);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteRegion(Long id) {
+        emsSpaceAreaMapper.delete(new LambdaQueryWrapper<EmsSpaceArea>().eq(EmsSpaceArea::getSpaceId, id));
+        emsSpaceMapper.deleteById(id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createFloor(EmsModelSaveRequest request) {
+        Long regionId = request.getRegionId();
+        String name = request.getName();
+        EmsSpace space = new EmsSpace();
+        space.setName(name);
+        space.setParentId(regionId);
+        EmsSpace parent = emsSpaceMapper.selectById(regionId);
+        space.setRootId(parent != null ? parent.getRootId() : regionId);
+        space.setType(SPACE_TYPE_FLOOR);
+        emsSpaceMapper.insert(space);
+        EmsSpaceFloor floor = new EmsSpaceFloor();
+        floor.setSpaceId(space.getId());
+        floor.setName(name);
+        emsSpaceFloorMapper.insert(floor);
+        return space.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateFloor(Long id, EmsModelSaveRequest request) {
+        String name = request.getName();
+        EmsSpace space = emsSpaceMapper.selectById(id);
+        if (space != null && name != null) {
+            space.setName(name);
+            emsSpaceMapper.updateById(space);
+        }
+        EmsSpaceFloor f = emsSpaceFloorMapper.selectOne(new LambdaQueryWrapper<EmsSpaceFloor>().eq(EmsSpaceFloor::getSpaceId, id));
+        if (f != null && name != null) {
+            f.setName(name);
+            emsSpaceFloorMapper.updateById(f);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteFloor(Long id) {
+        emsSpaceFloorMapper.delete(new LambdaQueryWrapper<EmsSpaceFloor>().eq(EmsSpaceFloor::getSpaceId, id));
+        emsSpaceMapper.deleteById(id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String createGateway(EmsModelSaveRequest request) {
+        Long floorId = request.getFloorId();
+        String name = request.getName();
+        String gwId = "GW" + System.currentTimeMillis();
+        if (gwId.length() > 13) gwId = gwId.substring(0, 13);
+        EmsGateway g = new EmsGateway();
+        g.setId(gwId);
+        g.setName(name);
+        g.setSpaceId(floorId);
+        g.setProjectId(0L);
+        EmsSpace space = floorId != null ? emsSpaceMapper.selectById(floorId) : null;
+        if (space != null) {
+            EmsProject p = firstProject();
+            if (p != null) g.setProjectId(p.getId());
+        }
+        g.setCommStatus(0);
+        g.setDataCenterId(0L);
+        g.setSecretKey("");
+        g.setVirtualDevice(0);
+        emsGatewayMapper.insert(g);
+        return g.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateGateway(String id, EmsModelSaveRequest request) {
+        EmsGateway g = emsGatewayMapper.selectById(id);
+        if (g == null) return;
+        if (request.getName() != null) g.setName(request.getName());
+        if (request.getFloorId() != null) g.setSpaceId(request.getFloorId());
+        emsGatewayMapper.updateById(g);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteGateway(String id) {
+        emsGatewayMapper.deleteById(id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createChannel(EmsModelSaveRequest request) {
+        String gatewayId = request.getGatewayId();
+        String name = request.getName();
+        EmsChannel c = new EmsChannel();
+        c.setGatewayId(gatewayId);
+        c.setName(name);
+        c.setChannelTypeId(1);
+        emsChannelMapper.insert(c);
+        return c.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateChannel(Long id, EmsModelSaveRequest request) {
+        EmsChannel c = emsChannelMapper.selectById(id);
+        if (c == null) return;
+        if (request.getName() != null) c.setName(request.getName());
+        if (request.getGatewayId() != null) c.setGatewayId(request.getGatewayId());
+        emsChannelMapper.updateById(c);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteChannel(Long id) {
+        emsChannelMapper.deleteById(id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String createDevice(EmsModelSaveRequest request) {
+        Long channelId = request.getChannelId();
+        String name = request.getName();
+        String devId = "DV" + System.currentTimeMillis();
+        if (devId.length() > 13) devId = devId.substring(0, 13);
+        EmsDevice d = new EmsDevice();
+        d.setId(devId);
+        d.setName(name);
+        d.setProjectId(0L);
+        d.setChannelId(channelId != null ? channelId : 0L);
+        d.setGatewayId("");
+        EmsChannel ch = channelId != null ? emsChannelMapper.selectById(channelId) : null;
+        if (ch != null) d.setGatewayId(ch.getGatewayId());
+        d.setProductId(0L);
+        d.setInstallationLocation(0L);
+        d.setMonitoringLocation(0L);
+        d.setCommAddress("");
+        d.setVirtualDevice(0);
+        d.setFocus(0);
+        d.setDeviceSystem(0);
+        d.setStatus(1);
+        d.setCommStatus(0);
+        emsDeviceMapper.insert(d);
+        return d.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateDevice(String id, EmsModelSaveRequest request) {
+        EmsDevice d = emsDeviceMapper.selectById(id);
+        if (d == null) return;
+        if (request.getName() != null) d.setName(request.getName());
+        if (request.getChannelId() != null) d.setChannelId(request.getChannelId());
+        emsDeviceMapper.updateById(d);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteDevice(String id) {
+        emsDeviceMapper.deleteById(id);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createAttributePoint(EmsModelSaveRequest request) {
+        String deviceId = request.getDeviceId();
+        String name = request.getName();
+        String code = request.getCode();
+        EmsDeviceFunction f = new EmsDeviceFunction();
+        f.setDeviceId(deviceId);
+        f.setName(name);
+        f.setIdentifier(code != null ? code : name);
+        f.setProductId(0L);
+        f.setProductTemplateId(0L);
+        f.setPreservable(1);
+        f.setBindingAcq(0);
+        emsDeviceFunctionMapper.insert(f);
+        return f.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateAttributePoint(Long id, EmsModelSaveRequest request) {
+        EmsDeviceFunction f = emsDeviceFunctionMapper.selectById(id);
+        if (f == null) return;
+        if (request.getName() != null) f.setName(request.getName());
+        if (request.getCode() != null) f.setIdentifier(request.getCode());
+        emsDeviceFunctionMapper.updateById(f);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteAttributePoint(Long id) {
+        emsDeviceFunctionMapper.deleteById(id);
+    }
+}

+ 53 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsOverviewServiceImpl.java

@@ -0,0 +1,53 @@
+package com.usky.ems.service.impl;
+
+import com.usky.ems.domain.EmsProject;
+import com.usky.ems.mapper.EmsProjectMapper;
+import com.usky.ems.service.EmsOverviewService;
+import com.usky.ems.service.vo.EmsProjectResponse;
+import com.usky.ems.service.vo.EmsSummaryResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 能源总览服务实现
+ */
+@Service
+public class EmsOverviewServiceImpl implements EmsOverviewService {
+
+    @Autowired
+    private EmsProjectMapper emsProjectMapper;
+
+    @Override
+    public EmsProjectResponse getProject(Long projectId) {
+        EmsProject project;
+        if (projectId != null) {
+            project = emsProjectMapper.selectById(projectId);
+        } else {
+            List<EmsProject> list = emsProjectMapper.selectList(null);
+            project = list.isEmpty() ? null : list.get(0);
+        }
+        if (project == null) {
+            return null;
+        }
+        EmsProjectResponse resp = new EmsProjectResponse();
+        resp.setId(project.getId());
+        resp.setName(project.getName());
+        resp.setCode(project.getAbbreviation());
+        resp.setDescription(project.getIntroduction());
+        resp.setArea(project.getArea());
+        resp.setResidentPopulation(project.getResidentPopulation());
+        resp.setCreateTime(project.getCreateTime());
+        resp.setUpdateTime(project.getUpdateTime());
+        return resp;
+    }
+
+    @Override
+    public EmsSummaryResponse getSummary(Long projectId, String timeDimension, String timeValue) {
+        EmsSummaryResponse resp = new EmsSummaryResponse();
+        resp.setTimeDimension(timeDimension);
+        resp.setTimeValue(timeValue);
+        return resp;
+    }
+}

+ 164 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsReportServiceImpl.java

@@ -0,0 +1,164 @@
+package com.usky.ems.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.ems.domain.EmsDevice;
+import com.usky.ems.domain.EmsDeviceFunction;
+import com.usky.ems.domain.EmsProject;
+import com.usky.ems.mapper.EmsDeviceFunctionMapper;
+import com.usky.ems.mapper.EmsDeviceMapper;
+import com.usky.ems.mapper.EmsProjectMapper;
+import com.usky.ems.service.EmsReportService;
+import com.usky.ems.service.vo.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 统计报表服务实现(设备列表基于 leo 设备与属性点位,统计与导出为占位/预留)
+ */
+@Service
+public class EmsReportServiceImpl implements EmsReportService {
+
+    private static final String[] ENERGY_TYPE_NAMES = {"", "电", "水", "气"};
+
+    @Autowired
+    private EmsProjectMapper emsProjectMapper;
+    @Autowired
+    private EmsDeviceMapper emsDeviceMapper;
+    @Autowired
+    private EmsDeviceFunctionMapper emsDeviceFunctionMapper;
+
+    private Long resolveProjectId(Long projectId) {
+        if (projectId != null) return projectId;
+        List<EmsProject> list = emsProjectMapper.selectList(null);
+        return list.isEmpty() ? null : list.get(0).getId();
+    }
+
+    private String energyTypeName(Long energyTypeId) {
+        if (energyTypeId == null || energyTypeId < 1 || energyTypeId > 3) return "";
+        return ENERGY_TYPE_NAMES[energyTypeId.intValue()];
+    }
+
+    @Override
+    public EmsReportDevicesResponse getEnergyDevices(Long energyTypeId, String keyword, Long projectId) {
+        Long pid = resolveProjectId(projectId);
+        if (pid == null) return new EmsReportDevicesResponse();
+        LambdaQueryWrapper<EmsDevice> q = new LambdaQueryWrapper<EmsDevice>().eq(EmsDevice::getProjectId, pid);
+        if (StringUtils.hasText(keyword)) {
+            q.and(w -> w.like(EmsDevice::getName, keyword).or().like(EmsDevice::getNumber, keyword));
+        }
+        List<EmsDevice> devices = emsDeviceMapper.selectList(q);
+        return buildReportDevicesResponse(devices, energyTypeId, null);
+    }
+
+    @Override
+    public EmsEnergyStatisticsResponse getEnergyStatistics(EmsEnergyStatisticsRequest request) {
+        EmsEnergyStatisticsResponse resp = new EmsEnergyStatisticsResponse();
+        if (request != null) {
+            resp.setTimeDimension(request.getTimeDimension());
+            resp.setTimeValue(request.getTimeValue());
+        }
+        return resp;
+    }
+
+    @Override
+    public void exportEnergy(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String format, HttpServletResponse response) {
+        doExport(response, format, "energy_report");
+    }
+
+    @Override
+    public EmsReportDevicesResponse getRegionDevices(Long energyTypeId, Long regionId, String keyword, Long projectId) {
+        Long pid = resolveProjectId(projectId);
+        if (pid == null) return new EmsReportDevicesResponse();
+        LambdaQueryWrapper<EmsDevice> q = new LambdaQueryWrapper<EmsDevice>().eq(EmsDevice::getProjectId, pid);
+        if (regionId != null) q.eq(EmsDevice::getInstallationLocation, regionId);
+        if (StringUtils.hasText(keyword)) {
+            q.and(w -> w.like(EmsDevice::getName, keyword).or().like(EmsDevice::getNumber, keyword));
+        }
+        List<EmsDevice> devices = emsDeviceMapper.selectList(q);
+        return buildReportDevicesResponse(devices, energyTypeId, regionId);
+    }
+
+    @Override
+    public EmsEnergyStatisticsResponse getRegionStatistics(EmsEnergyStatisticsRequest request, List<Long> regionIds) {
+        return getEnergyStatistics(request);
+    }
+
+    @Override
+    public void exportRegion(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String regionIds, String format, HttpServletResponse response) {
+        doExport(response, format, "region_report");
+    }
+
+    @Override
+    public EmsReportDevicesResponse getCollectionDevices(Long energyTypeId, String keyword, Long projectId) {
+        return getEnergyDevices(energyTypeId, keyword, projectId);
+    }
+
+    @Override
+    public EmsCollectionRealtimeResponse getCollectionRealtime(EmsCollectionRealtimeRequest request) {
+        EmsCollectionRealtimeResponse resp = new EmsCollectionRealtimeResponse();
+        return resp;
+    }
+
+    @Override
+    public EmsEnergyStatisticsResponse getCollectionStatistics(EmsEnergyStatisticsRequest request) {
+        return getEnergyStatistics(request);
+    }
+
+    @Override
+    public void exportCollection(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String format, HttpServletResponse response) {
+        doExport(response, format, "collection_report");
+    }
+
+    private EmsReportDevicesResponse buildReportDevicesResponse(List<EmsDevice> devices, Long energyTypeId, Long regionId) {
+        EmsReportDevicesResponse resp = new EmsReportDevicesResponse();
+        String typeName = energyTypeName(energyTypeId);
+        for (EmsDevice d : devices) {
+            EmsReportDeviceItemVO item = new EmsReportDeviceItemVO();
+            item.setId(d.getId());
+            item.setName(d.getName());
+            item.setCode(d.getNumber());
+            item.setEnergyTypeName(typeName);
+            List<EmsDeviceFunction> funcs = emsDeviceFunctionMapper.selectList(
+                    new LambdaQueryWrapper<EmsDeviceFunction>().eq(EmsDeviceFunction::getDeviceId, d.getId()));
+            List<EmsReportAttributeVO> attrs = funcs.stream().map(f -> {
+                EmsReportAttributeVO a = new EmsReportAttributeVO();
+                a.setId(f.getId());
+                a.setName(f.getName());
+                a.setCode(f.getIdentifier());
+                a.setUnit("");
+                return a;
+            }).collect(Collectors.toList());
+            item.setAttributes(attrs);
+            resp.getDevices().add(item);
+        }
+        return resp;
+    }
+
+    private void doExport(HttpServletResponse response, String format, String fileName) {
+        try {
+            if ("csv".equalsIgnoreCase(format != null ? format : "excel")) {
+                response.setContentType("text/csv;charset=UTF-8");
+                response.setHeader("Content-Disposition", "attachment; filename=" + fileName + ".csv");
+                try (OutputStream os = response.getOutputStream()) {
+                    os.write("\uFEFF".getBytes(StandardCharsets.UTF_8));
+                    os.write("时间维度,时间值\n".getBytes(StandardCharsets.UTF_8));
+                }
+            } else {
+                response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+                response.setHeader("Content-Disposition", "attachment; filename=" + fileName + ".xlsx");
+                response.getOutputStream().flush();
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("导出失败", e);
+        }
+    }
+}

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCategoryRatioItemVO.java

@@ -0,0 +1,14 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class EmsCategoryRatioItemVO {
+
+    private String energyTypeName;
+    private BigDecimal ratio;
+    private BigDecimal usage;
+    private String unit;
+}

+ 17 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCollectionRealtimeItemVO.java

@@ -0,0 +1,17 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class EmsCollectionRealtimeItemVO {
+
+    private String deviceId;
+    private Long pointId;
+    private String pointName;
+    private BigDecimal value;
+    private String unit;
+    private LocalDateTime collectedAt;
+}

+ 16 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCollectionRealtimeRequest.java

@@ -0,0 +1,16 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class EmsCollectionRealtimeRequest {
+
+    private List<String> deviceIds;
+    private List<Long> attributePointIds;
+    private String timeDimension;
+    private String timeValue;
+    private String startTime;
+    private String endTime;
+}

+ 12 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCollectionRealtimeResponse.java

@@ -0,0 +1,12 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class EmsCollectionRealtimeResponse {
+
+    private List<EmsCollectionRealtimeItemVO> items = new ArrayList<>();
+}

+ 15 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareRequest.java

@@ -0,0 +1,15 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class EmsCompareRequest {
+
+    private Long energyTypeId;
+    private List<String> deviceIds;
+    private String timeDimension;
+    private List<String> timeValues;
+}

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareResponse.java

@@ -0,0 +1,14 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class EmsCompareResponse {
+
+    private String energyTypeName;
+    private String dimension;
+    private List<EmsCompareSeriesItemVO> series = new ArrayList<>();
+}

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareSeriesItemVO.java

@@ -0,0 +1,14 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class EmsCompareSeriesItemVO {
+
+    private String deviceId;
+    private String deviceName;
+    private List<EmsCompareValueVO> values = new ArrayList<>();
+}

+ 12 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareValueVO.java

@@ -0,0 +1,12 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class EmsCompareValueVO {
+
+    private String timeValue;
+    private BigDecimal usage;
+}

+ 18 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyStatisticsItemVO.java

@@ -0,0 +1,18 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+public class EmsEnergyStatisticsItemVO {
+
+    private String deviceId;
+    private String deviceName;
+    private Long pointId;
+    private String pointName;
+    private String unit;
+    private BigDecimal value;
+    private List<Object> statistics;
+}

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyStatisticsRequest.java

@@ -0,0 +1,14 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class EmsEnergyStatisticsRequest {
+
+    private List<String> deviceIds;
+    private List<Long> attributePointIds;
+    private String timeDimension;
+    private String timeValue;
+}

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyStatisticsResponse.java

@@ -0,0 +1,14 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class EmsEnergyStatisticsResponse {
+
+    private String timeDimension;
+    private String timeValue;
+    private List<EmsEnergyStatisticsItemVO> items = new ArrayList<>();
+}

+ 16 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyTypeVO.java

@@ -0,0 +1,16 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+/**
+ * 能源类型(电、水、气)
+ */
+@Data
+public class EmsEnergyTypeVO {
+
+    private Long id;
+    private String name;
+    private String code;
+    private String unit;
+    private Integer sortOrder;
+}

+ 24 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsGatewayDetailResponse.java

@@ -0,0 +1,24 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 网关详情响应
+ */
+@Data
+public class EmsGatewayDetailResponse {
+
+    private String id;
+    private String name;
+    private Long spaceId;
+    private String floorName;
+    private String regionName;
+    private String buildingName;
+    private String installLocation;
+    private Integer communicationStatus;
+    private LocalDateTime onlineTime;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 22 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsGatewayListItem.java

@@ -0,0 +1,22 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 网关列表项(字段与 leo.ems_gateway 对应)
+ */
+@Data
+public class EmsGatewayListItem {
+
+    private String id;
+    private String name;
+    private Long spaceId;
+    private String floorName;
+    private String installLocation;
+    private Integer communicationStatus;
+    private LocalDateTime onlineTime;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 21 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsGatewayPageRequest.java

@@ -0,0 +1,21 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+/**
+ * 网关列表分页请求
+ */
+@Data
+public class EmsGatewayPageRequest {
+
+    private Integer current = 1;
+    private Integer size = 10;
+    /** 安装位置(模糊) */
+    private String installLocation;
+    /** 网关名称(模糊) */
+    private String name;
+    /** 网关编码(模糊) */
+    private String code;
+    /** 通讯状态:0-离线 1-在线,不传为全部 */
+    private Integer communicationStatus;
+}

+ 16 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsIdResponse.java

@@ -0,0 +1,16 @@
+package com.usky.ems.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 新增接口返回 id(Long 或 String 转 Object)
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class EmsIdResponse {
+
+    private Object id;
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsLoginRequest.java

@@ -0,0 +1,10 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+@Data
+public class EmsLoginRequest {
+
+    private String username;
+    private String password;
+}

+ 10 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsLoginResponse.java

@@ -0,0 +1,10 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+@Data
+public class EmsLoginResponse {
+
+    private String accessToken;
+    private Long expiresIn;
+}

+ 35 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsModelSaveRequest.java

@@ -0,0 +1,35 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 基础建模 - 建筑/区域/楼层/网关/通道/设备/属性点位 新增与编辑请求(通用字段)
+ * 各接口按需使用部分字段
+ */
+@Data
+public class EmsModelSaveRequest {
+
+    private Long projectId;
+    private Long buildingId;
+    private Long regionId;
+    private Long floorId;
+    private String gatewayId;
+    private Long channelId;
+    private String deviceId;
+    private Long energyTypeId;
+
+    private String name;
+    private String code;
+    private BigDecimal area;
+    private Integer floorNumber;
+    private Integer sortOrder;
+    private String installLocation;
+    private String configJson;
+    private Integer channelNo;
+    private String model;
+    private String dataType;
+    private String unit;
+    private String pointAddress;
+}

+ 23 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsProjectResponse.java

@@ -0,0 +1,23 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 项目信息响应(数据总览 - 获取项目信息)
+ * 字段名与 leo.ems_project 一致
+ */
+@Data
+public class EmsProjectResponse {
+
+    private Long id;
+    private String name;
+    private String code;
+    private String description;
+    private BigDecimal area;
+    private Integer residentPopulation;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 18 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsRegionAnalysisItemVO.java

@@ -0,0 +1,18 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class EmsRegionAnalysisItemVO {
+
+    private Long regionId;
+    private String regionName;
+    private BigDecimal area;
+    private BigDecimal totalUsage;
+    private BigDecimal areaUsage;
+    private String unit;
+    private String timeDimension;
+    private String timeValue;
+}

+ 12 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsRegionAnalysisResponse.java

@@ -0,0 +1,12 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class EmsRegionAnalysisResponse {
+
+    private List<EmsRegionAnalysisItemVO> items = new ArrayList<>();
+}

+ 12 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsReportAttributeVO.java

@@ -0,0 +1,12 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+@Data
+public class EmsReportAttributeVO {
+
+    private Long id;
+    private String name;
+    private String code;
+    private String unit;
+}

+ 17 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsReportDeviceItemVO.java

@@ -0,0 +1,17 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class EmsReportDeviceItemVO {
+
+    private String id;
+    private String name;
+    private String code;
+    private String energyTypeName;
+    private String regionName;
+    private List<EmsReportAttributeVO> attributes = new ArrayList<>();
+}

+ 12 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsReportDevicesResponse.java

@@ -0,0 +1,12 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class EmsReportDevicesResponse {
+
+    private List<EmsReportDeviceItemVO> devices = new ArrayList<>();
+}

+ 18 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsStructureTreeNode.java

@@ -0,0 +1,18 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 项目结构树节点(建筑、区域、楼层、网关)
+ */
+@Data
+public class EmsStructureTreeNode {
+
+    private Object id;
+    private String name;
+    private String type;
+    private List<EmsStructureTreeNode> children = new ArrayList<>();
+}

+ 11 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsSummaryRequest.java

@@ -0,0 +1,11 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+@Data
+public class EmsSummaryRequest {
+
+    private Long projectId;
+    private String timeDimension;
+    private String timeValue;
+}

+ 20 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsSummaryResponse.java

@@ -0,0 +1,20 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 项目数据概括响应
+ */
+@Data
+public class EmsSummaryResponse {
+
+    private String timeDimension;
+    private String timeValue;
+    private List<Object> categoryRatio = new ArrayList<>();
+    private List<Object> buildingRanking = new ArrayList<>();
+    private List<Object> usageTrend = new ArrayList<>();
+    private List<Object> subItemRatio = new ArrayList<>();
+}

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendCategoryResponse.java

@@ -0,0 +1,14 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class EmsTrendCategoryResponse {
+
+    private String timeDimension;
+    private String timeValue;
+    private List<EmsCategoryRatioItemVO> categoryRatio = new ArrayList<>();
+}

+ 18 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendIndicatorsResponse.java

@@ -0,0 +1,18 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class EmsTrendIndicatorsResponse {
+
+    private String timeDimension;
+    private String timeValue;
+    private BigDecimal totalUsage;
+    private BigDecimal standardCoal;
+    private BigDecimal carbonEmission;
+    private BigDecimal areaUsage;
+    private BigDecimal perCapitaUsage;
+    private Object rating;
+}

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendItemVO.java

@@ -0,0 +1,14 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class EmsTrendItemVO {
+
+    private String timeLabel;
+    private BigDecimal usage;
+    private BigDecimal standardCoal;
+    private BigDecimal carbonEmission;
+}

+ 14 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendResponse.java

@@ -0,0 +1,14 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class EmsTrendResponse {
+
+    private String timeDimension;
+    private String timeValue;
+    private List<EmsTrendItemVO> trend = new ArrayList<>();
+}

+ 21 - 0
service-sas/pom.xml

@@ -0,0 +1,21 @@
+<?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>
+        <artifactId>usky-modules</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-sas</artifactId>
+
+    <packaging>pom</packaging>
+    <version>0.0.1</version>
+
+    <modules>
+        <module>service-sas-biz</module>
+        <module>service-sas-api</module>
+    </modules>
+</project>
+

+ 28 - 0
service-sas/service-sas-api/pom.xml

@@ -0,0 +1,28 @@
+<?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-sas</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-sas-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>
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>
+

+ 139 - 0
service-sas/service-sas-biz/pom.xml

@@ -0,0 +1,139 @@
+<?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-sas</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-sas-biz</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>common-cloud-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-sas-api</artifactId>
+            <version>0.0.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- Pagehelper -->
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>ruoyi-common-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-system-api</artifactId>
+            <version>0.0.1</version>
+        </dependency>
+
+        <!-- Excel & Word 工具 -->
+        <dependency>
+            <groupId>cn.afterturn</groupId>
+            <artifactId>easypoi-spring-boot-starter</artifactId>
+            <version>4.1.0</version>
+        </dependency>
+
+        <!-- MQTT -->
+        <dependency>
+            <groupId>org.eclipse.paho</groupId>
+            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+            <version>1.2.5</version>
+        </dependency>
+
+        <!-- 工具类 -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.20</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>ruoyi-common-swagger</artifactId>
+        </dependency>
+
+        <!-- Hutool 用于 AG 接口 HTTP 请求 -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
+        <!-- JNA -->
+        <dependency>
+            <groupId>net.java.dev.jna</groupId>
+            <artifactId>jna</artifactId>
+            <version>5.13.0</version>
+        </dependency>
+        <dependency>
+            <groupId>net.java.dev.jna</groupId>
+            <artifactId>jna-platform</artifactId>
+            <version>5.13.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <!-- 建议使用最新稳定版,可到Maven中央仓库确认最新版本号 -->
+            <version>2.0.54</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-websocket</artifactId>
+        </dependency>
+
+        <!-- ZIP 压缩工具 -->
+        <dependency>
+            <groupId>net.lingala.zip4j</groupId>
+            <artifactId>zip4j</artifactId>
+            <version>2.11.5</version>
+        </dependency>
+
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.2.6.RELEASE</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.github.shalousun</groupId>
+                <artifactId>smart-doc-maven-plugin</artifactId>
+                <version>2.1.1</version>
+                <configuration>
+                    <!--指定生成文档的使用的配置文件,配置文件放在自己的项目中-->
+                    <configFile>./src/main/resources/smart-doc.json</configFile>
+                    <!--指定项目名称-->
+                    <projectName>sas</projectName>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
+

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

@@ -0,0 +1,108 @@
+package com.usky.sas;
+
+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;
+
+/**
+ * @Description:
+ * @Author: fu
+ * @Date: 2026/03/01 14:44
+ */
+public class MybatisGeneratorUtils {
+    public static void main(String[] args) {
+
+        shell("service-sas", "service-sas-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.sas");
+        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); // 使用lombok
+        // 修改自己想要生成的表
+        strategy.setInclude("sas_system_type_code");  // 逆向工程使用的表   如果要生成多个,这里可以传入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/pm" + "/"
+                        + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
+            }
+        });
+        cfg.setFileOutConfigList(focList);
+        mpg.setCfg(cfg);
+        tc.setXml(null);
+        mpg.setTemplate(tc);
+        // 5、执行
+        mpg.execute();
+    }
+}

+ 47 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/ServiceSasApplication.java

@@ -0,0 +1,47 @@
+package com.usky.sas;
+
+import com.ruoyi.common.swagger.annotation.EnableCustomSwagger2;
+import org.mybatis.spring.annotation.MapperScan;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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.ComponentScan;
+import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * SAS 模块启动类
+ *
+ * @author han
+ */
+@EnableCustomSwagger2
+@EnableFeignClients(basePackages = "com.usky")
+@MapperScan(value = "com.usky.sas.mapper")
+@ComponentScan("com.usky")
+@SpringBootApplication
+@EnableAsync
+public class ServiceSasApplication {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ServiceSasApplication.class);
+
+    public static void main(String[] args) throws UnknownHostException {
+        ConfigurableApplicationContext application = SpringApplication.run(ServiceSasApplication.class, args);
+        Environment env = application.getEnvironment();
+        String ip = InetAddress.getLocalHost().getHostAddress();
+        String port = env.getProperty("server.port");
+        String path = env.getProperty("server.servlet.context-path");
+        LOGGER.info("\n----------------------------------------------------------\n\t" +
+                "Application is running! Access URLs:\n\t" +
+                "Local: \t\thttp://localhost:" + port + (null == path ? "" : path) + "/\n\t" +
+                "External: \thttp://" + ip + ":" + port + (null == path ? "" : path) + "/\n\t" +
+                "Api: \t\thttp://" + ip + ":" + port + (null == path ? "" : path) + "/swagger-ui/index.html\n\t" +
+                "----------------------------------------------------------");
+    }
+}
+

+ 16 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/StandardOnvifService.java

@@ -0,0 +1,16 @@
+package com.usky.sas.common;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class StandardOnvifService {
+    public String getNvrRealTimeStreamUrl(String ip, String user, String password, String token) {
+        // Placeholder implementation
+        return String.format("rtsp://%s:%s@%s:554/stream/%s", user, password, ip, token);
+    }
+
+    public String getRealTimeStreamUrl(String ip, String user, String password) {
+        // Placeholder implementation
+        return String.format("rtsp://%s:%s@%s:554/stream", user, password, ip);
+    }
+}

+ 126 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/UnityVideoInfo.java

@@ -0,0 +1,126 @@
+package com.usky.sas.common;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class UnityVideoInfo {
+    @ApiModelProperty("设备mac地址")
+    private String macAddr;
+    @ApiModelProperty("ZLM主动拉流唯一标识")
+    private String key;
+    @ApiModelProperty("视频流唯一标识")
+    private String ssrc;
+    @ApiModelProperty("视频流播放地址")
+    private String url;
+
+    public UnityVideoInfo() {
+    }
+
+    public String getMacAddr() {
+        return this.macAddr;
+    }
+
+    public String getKey() {
+        return this.key;
+    }
+
+    public String getSsrc() {
+        return this.ssrc;
+    }
+
+    public String getUrl() {
+        return this.url;
+    }
+
+    public void setMacAddr(String macAddr) {
+        this.macAddr = macAddr;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public void setSsrc(String ssrc) {
+        this.ssrc = ssrc;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        } else if (!(o instanceof UnityVideoInfo)) {
+            return false;
+        } else {
+            UnityVideoInfo other = (UnityVideoInfo)o;
+            if (!other.canEqual(this)) {
+                return false;
+            } else {
+                Object this$macAddr = this.getMacAddr();
+                Object other$macAddr = other.getMacAddr();
+                if (this$macAddr == null) {
+                    if (other$macAddr != null) {
+                        return false;
+                    }
+                } else if (!this$macAddr.equals(other$macAddr)) {
+                    return false;
+                }
+
+                Object this$key = this.getKey();
+                Object other$key = other.getKey();
+                if (this$key == null) {
+                    if (other$key != null) {
+                        return false;
+                    }
+                } else if (!this$key.equals(other$key)) {
+                    return false;
+                }
+
+                Object this$ssrc = this.getSsrc();
+                Object other$ssrc = other.getSsrc();
+                if (this$ssrc == null) {
+                    if (other$ssrc != null) {
+                        return false;
+                    }
+                } else if (!this$ssrc.equals(other$ssrc)) {
+                    return false;
+                }
+
+                Object this$url = this.getUrl();
+                Object other$url = other.getUrl();
+                if (this$url == null) {
+                    if (other$url != null) {
+                        return false;
+                    }
+                } else if (!this$url.equals(other$url)) {
+                    return false;
+                }
+
+                return true;
+            }
+        }
+    }
+
+    protected boolean canEqual(Object other) {
+        return other instanceof UnityVideoInfo;
+    }
+
+    public int hashCode() {
+        int PRIME = 59;
+        int result = 1;
+        Object $macAddr = this.getMacAddr();
+        result = result * 59 + ($macAddr == null ? 43 : $macAddr.hashCode());
+        Object $key = this.getKey();
+        result = result * 59 + ($key == null ? 43 : $key.hashCode());
+        Object $ssrc = this.getSsrc();
+        result = result * 59 + ($ssrc == null ? 43 : $ssrc.hashCode());
+        Object $url = this.getUrl();
+        result = result * 59 + ($url == null ? 43 : $url.hashCode());
+        return result;
+    }
+
+    public String toString() {
+        return "UnityVideoInfo(macAddr=" + this.getMacAddr() + ", key=" + this.getKey() + ", ssrc=" + this.getSsrc() + ", url=" + this.getUrl() + ")";
+    }
+}

+ 18 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/config/WebSocketConfig.java

@@ -0,0 +1,18 @@
+package com.usky.sas.common.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+/**
+ * WebSocket 配置类
+ * 开启 WebSocket 支持
+ */
+@Configuration
+public class WebSocketConfig {
+
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+}

+ 71 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/DahuaNvrInfo.java

@@ -0,0 +1,71 @@
+package com.usky.sas.common.dahua;
+
+public class DahuaNvrInfo {
+    private Long loginHandle;
+    private int maxChannelNum;
+
+    public DahuaNvrInfo() {
+    }
+
+    public Long getLoginHandle() {
+        return this.loginHandle;
+    }
+
+    public int getMaxChannelNum() {
+        return this.maxChannelNum;
+    }
+
+    public void setLoginHandle(Long loginHandle) {
+        this.loginHandle = loginHandle;
+    }
+
+    public void setMaxChannelNum(int maxChannelNum) {
+        this.maxChannelNum = maxChannelNum;
+    }
+
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        } else if (!(o instanceof DahuaNvrInfo)) {
+            return false;
+        } else {
+            DahuaNvrInfo other = (DahuaNvrInfo)o;
+            if (!other.canEqual(this)) {
+                return false;
+            } else {
+                Object this$loginHandle = this.getLoginHandle();
+                Object other$loginHandle = other.getLoginHandle();
+                if (this$loginHandle == null) {
+                    if (other$loginHandle != null) {
+                        return false;
+                    }
+                } else if (!this$loginHandle.equals(other$loginHandle)) {
+                    return false;
+                }
+
+                if (this.getMaxChannelNum() != other.getMaxChannelNum()) {
+                    return false;
+                } else {
+                    return true;
+                }
+            }
+        }
+    }
+
+    protected boolean canEqual(Object other) {
+        return other instanceof DahuaNvrInfo;
+    }
+
+    public int hashCode() {
+        int PRIME = 59;
+        int result = 1;
+        Object $loginHandle = this.getLoginHandle();
+        result = result * 59 + ($loginHandle == null ? 43 : $loginHandle.hashCode());
+        result = result * 59 + this.getMaxChannelNum();
+        return result;
+    }
+
+    public String toString() {
+        return "DahuaNvrInfo(loginHandle=" + this.getLoginHandle() + ", maxChannelNum=" + this.getMaxChannelNum() + ")";
+    }
+}

+ 126 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/DahuaVideoInfo.java

@@ -0,0 +1,126 @@
+package com.usky.sas.common.dahua;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class DahuaVideoInfo {
+    @ApiModelProperty("设备mac地址")
+    private String macAddr;
+    @ApiModelProperty("视频流句柄")
+    private Long handle;
+    @ApiModelProperty("视频流唯一标识")
+    private String ssrc;
+    @ApiModelProperty("视频流播放地址")
+    private String url;
+
+    public DahuaVideoInfo() {
+    }
+
+    public String getMacAddr() {
+        return this.macAddr;
+    }
+
+    public Long getHandle() {
+        return this.handle;
+    }
+
+    public String getSsrc() {
+        return this.ssrc;
+    }
+
+    public String getUrl() {
+        return this.url;
+    }
+
+    public void setMacAddr(String macAddr) {
+        this.macAddr = macAddr;
+    }
+
+    public void setHandle(Long handle) {
+        this.handle = handle;
+    }
+
+    public void setSsrc(String ssrc) {
+        this.ssrc = ssrc;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        } else if (!(o instanceof DahuaVideoInfo)) {
+            return false;
+        } else {
+            DahuaVideoInfo other = (DahuaVideoInfo)o;
+            if (!other.canEqual(this)) {
+                return false;
+            } else {
+                Object this$macAddr = this.getMacAddr();
+                Object other$macAddr = other.getMacAddr();
+                if (this$macAddr == null) {
+                    if (other$macAddr != null) {
+                        return false;
+                    }
+                } else if (!this$macAddr.equals(other$macAddr)) {
+                    return false;
+                }
+
+                Object this$handle = this.getHandle();
+                Object other$handle = other.getHandle();
+                if (this$handle == null) {
+                    if (other$handle != null) {
+                        return false;
+                    }
+                } else if (!this$handle.equals(other$handle)) {
+                    return false;
+                }
+
+                Object this$ssrc = this.getSsrc();
+                Object other$ssrc = other.getSsrc();
+                if (this$ssrc == null) {
+                    if (other$ssrc != null) {
+                        return false;
+                    }
+                } else if (!this$ssrc.equals(other$ssrc)) {
+                    return false;
+                }
+
+                Object this$url = this.getUrl();
+                Object other$url = other.getUrl();
+                if (this$url == null) {
+                    if (other$url != null) {
+                        return false;
+                    }
+                } else if (!this$url.equals(other$url)) {
+                    return false;
+                }
+
+                return true;
+            }
+        }
+    }
+
+    protected boolean canEqual(Object other) {
+        return other instanceof DahuaVideoInfo;
+    }
+
+    public int hashCode() {
+        int PRIME = 59;
+        int result = 1;
+        Object $macAddr = this.getMacAddr();
+        result = result * 59 + ($macAddr == null ? 43 : $macAddr.hashCode());
+        Object $handle = this.getHandle();
+        result = result * 59 + ($handle == null ? 43 : $handle.hashCode());
+        Object $ssrc = this.getSsrc();
+        result = result * 59 + ($ssrc == null ? 43 : $ssrc.hashCode());
+        Object $url = this.getUrl();
+        result = result * 59 + ($url == null ? 43 : $url.hashCode());
+        return result;
+    }
+
+    public String toString() {
+        return "DahuaVideoInfo(macAddr=" + this.getMacAddr() + ", handle=" + this.getHandle() + ", ssrc=" + this.getSsrc() + ", url=" + this.getUrl() + ")";
+    }
+}

+ 185 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/DahuaVideoStreamService.java

@@ -0,0 +1,185 @@
+package com.usky.sas.common.dahua;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.StrUtil;
+import com.usky.sas.common.global.Constant;
+import com.usky.sas.common.global.StreamContext;
+import com.usky.sas.common.util.GetIpUtils;
+import com.usky.sas.service.vo.VideoStreamVo;
+import com.usky.sas.common.exception.BusinessException;
+import com.usky.sas.common.global.GlobalMemoryMap;
+import com.usky.sas.common.entity.DahuaNvrInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static java.lang.Long.toHexString;
+
+@Component
+public class DahuaVideoStreamService {
+    private static final Logger log = LoggerFactory.getLogger(DahuaVideoStreamService.class);
+    static int iPlayBack;
+    static final int RTP_HEADER_SIZE = 12;
+    static final int RTP_VERSION = 2;
+    static final int RTP_PAYLOAD_TYPE = 96;
+    public static final int MAX_RTP_PACKET_SIZE = 1400;
+    static Map<String, NetSDKLib.fDataCallBack> dahuaPlayDataCallBackMap;
+    static Map<String, StreamContext> streamContextMap;
+    static Map<String, DahuaVideoInfo> dahuaVideoInfo;
+
+    public DahuaVideoStreamService() {
+    }
+
+    public static byte[] createRtpPacket(byte[] data, int offset, int size, boolean marker, long timestamp, int sequenceNumber, long ssrc) {
+        byte[] rtpPacket = new byte[12 + size];
+        rtpPacket[0] = -128;
+        rtpPacket[1] = 96;
+        if (marker) {
+            rtpPacket[1] |= -128;
+        }
+
+        rtpPacket[2] = (byte)(sequenceNumber >> 8);
+        rtpPacket[3] = (byte)(sequenceNumber & 255);
+        rtpPacket[4] = (byte)((int)(timestamp >> 24));
+        rtpPacket[5] = (byte)((int)(timestamp >> 16));
+        rtpPacket[6] = (byte)((int)(timestamp >> 8));
+        rtpPacket[7] = (byte)((int)(timestamp & 255L));
+        rtpPacket[8] = (byte)((int)(ssrc >> 24));
+        rtpPacket[9] = (byte)((int)(ssrc >> 16));
+        rtpPacket[10] = (byte)((int)(ssrc >> 8));
+        rtpPacket[11] = (byte)((int)(ssrc & 255L));
+        System.arraycopy(data, offset, rtpPacket, 12, size);
+        return rtpPacket;
+    }
+
+    public static byte[] createTcpPacket(byte[] rtpPacket) {
+        byte[] tcpPacket = new byte[rtpPacket.length + 2];
+        tcpPacket[0] = (byte)(rtpPacket.length >> 8 & 255);
+        tcpPacket[1] = (byte)(rtpPacket.length & 255);
+        System.arraycopy(rtpPacket, 0, tcpPacket, 2, rtpPacket.length);
+        return tcpPacket;
+    }
+
+    public static VideoStreamVo playBackByTime(String oldMac, String macAddr, Long userID, int lChannel, int mediaRtpPort, Integer mediaHttpPort, Date beginTime, Date endTime) {
+        if (StrUtil.isNotBlank(oldMac) && streamContextMap.containsKey(oldMac)) {
+            ((StreamContext)streamContextMap.get(oldMac)).close();
+            streamContextMap.remove(oldMac);
+            dahuaPlayDataCallBackMap.remove(oldMac);
+        }
+
+        StreamContext context = null;
+
+        try {
+            context = new StreamContext(GetIpUtils.getServerIP(), mediaRtpPort);
+            log.info("设备 {} RTP连接已建立: {}:{}", new Object[]{macAddr, GetIpUtils.getServerIP(), mediaRtpPort});
+        } catch (IOException e) {
+            log.error("设备 {} 连接失败: {}", macAddr, e.getMessage());
+            throw new BusinessException("设备 " + macAddr + " 连接失败", -1);
+        }
+
+        String newMac = macAddr + System.currentTimeMillis();
+        Constant.globalSsrc = Constant.globalSsrc >= 4294967295L ? 65535L : Constant.globalSsrc + 1L;
+        context.ssrc = Constant.globalSsrc;
+        streamContextMap.put(newMac, context);
+        FPlayDataCallBackEx playDataCallBack = new FPlayDataCallBackEx(context, newMac);
+        dahuaPlayDataCallBackMap.put(newMac, playDataCallBack);
+        int beginYear = DateUtil.year(beginTime);
+        int beginMonth = DateUtil.month(beginTime) + 1;
+        int beginDay = DateUtil.dayOfMonth(beginTime);
+        int beginHour = DateUtil.hour(beginTime, true);
+        int beginMinute = DateUtil.minute(beginTime);
+        int beginSecond = DateUtil.second(beginTime);
+        NetSDKLib.NET_TIME begin = new NetSDKLib.NET_TIME();
+        begin.setTime(beginYear, beginMonth, beginDay, beginHour, beginMinute, beginSecond);
+        int endYear = DateUtil.year(endTime);
+        int endMonth = DateUtil.month(endTime) + 1;
+        int endDay = DateUtil.dayOfMonth(endTime);
+        int endHour = DateUtil.hour(endTime, true);
+        int endMinute = DateUtil.minute(endTime);
+        int endSecond = DateUtil.second(endTime);
+        NetSDKLib.NET_TIME end = new NetSDKLib.NET_TIME();
+        end.setTime(endYear, endMonth, endDay, endHour, endMinute, endSecond);
+        NetSDKLib.NET_IN_PLAYBACK_BY_DATA_TYPE stIn = new NetSDKLib.NET_IN_PLAYBACK_BY_DATA_TYPE();
+        stIn.emDataType = 1;
+        stIn.nChannelID = lChannel;
+        stIn.stStartTime = begin;
+        stIn.stStopTime = end;
+        stIn.nPlayDirection = 0;
+        stIn.dwPosUser = null;
+        stIn.hWnd = null;
+        stIn.cbDownLoadPos = null;
+        stIn.fDownLoadDataCallBack = playDataCallBack;
+        stIn.fDownLoadDataCallBackEx = null;
+        stIn.dwDataUser = null;
+        NetSDKLib.NET_OUT_PLAYBACK_BY_DATA_TYPE stOut = new NetSDKLib.NET_OUT_PLAYBACK_BY_DATA_TYPE();
+        Long lPlayHandle = InitNetSDKLib.dhNetSDK.CLIENT_PlayBackByDataType(userID, stIn, stOut, 5000);
+        if (lPlayHandle != 0L) {
+            log.info("回放取流成功,回放句柄:{}", lPlayHandle);
+            log.info("取流成功,播放地址:" + GetIpUtils.getServerIP() + ":" + mediaHttpPort + "/index/api/webrtc?app=rtp&stream=" + context.ssrc + "&type=play");
+            VideoStreamVo streamVo = new VideoStreamVo();
+            streamVo.setHandle(lPlayHandle);
+            streamVo.setMac(newMac);
+            streamVo.setUrl("http://" + GetIpUtils.getServerIP() + ":" + mediaHttpPort + "/index/api/webrtc?app=rtp&stream=" + toHexString(context.ssrc) + "&type=play");
+            DahuaVideoInfo videoInfo = new DahuaVideoInfo();
+            videoInfo.setSsrc(toHexString(context.ssrc));
+            videoInfo.setUrl("http://" + GetIpUtils.getServerIP() + ":" + mediaHttpPort + "/index/api/webrtc?app=rtp&stream=" + toHexString(context.ssrc) + "&type=play");
+            videoInfo.setHandle(lPlayHandle);
+            dahuaVideoInfo.put(newMac, videoInfo);
+            return streamVo;
+        } else {
+            log.info("回放取流失败,原因码:{}", InitNetSDKLib.dhNetSDK.CLIENT_GetLastError());
+            throw new BusinessException("回放失败", -1);
+        }
+    }
+
+    public static void stopPlayStreamData(Long PlayHandle, String macAddr) {
+        if (PlayHandle == 0L) {
+            log.info("回放取流未开启,请先开启回放取流");
+        } else if (!InitNetSDKLib.dhNetSDK.CLIENT_StopPlayBack(PlayHandle)) {
+            log.error("停止取流失败,err:" + InitNetSDKLib.dhNetSDK.CLIENT_GetLastError());
+        } else {
+            log.info("停止取流成功");
+            StreamContext context = (StreamContext)streamContextMap.get(macAddr);
+            if (context != null) {
+                context.close();
+            }
+
+            dahuaPlayDataCallBackMap.remove(macAddr);
+        }
+    }
+
+    public static String toHexString(long number) {
+        if (number == 0L) {
+            return "0";
+        } else {
+            StringBuilder hex = new StringBuilder();
+            char[] hexChars = "0123456789ABCDEF".toCharArray();
+
+            for(long current = number; current != 0L; current >>>= 4) {
+                int digit = (int)(current & 15L);
+                hex.insert(0, hexChars[digit]);
+            }
+
+            if (hex.length() < 8) {
+                int zerosToAdd = 8 - hex.length();
+
+                for(int i = 0; i < zerosToAdd; ++i) {
+                    hex.insert(0, '0');
+                }
+            }
+
+            return hex.toString();
+        }
+    }
+
+    static {
+        dahuaPlayDataCallBackMap = GlobalMemoryMap.dahuaPlayDataCallBackMap;
+        streamContextMap = GlobalMemoryMap.streamContextMap;
+        dahuaVideoInfo = GlobalMemoryMap.dahuaVideoInfo;
+    }
+}

+ 10 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/EM_SEND_SEARCH_TYPE.java

@@ -0,0 +1,10 @@
+package com.usky.sas.common.dahua;
+
+public enum EM_SEND_SEARCH_TYPE {
+    EM_SEND_SEARCH_TYPE_MULTICAST_AND_BROADCAST,
+    EM_SEND_SEARCH_TYPE_MULTICAST,
+    EM_SEND_SEARCH_TYPE_BROADCAST;
+
+    private EM_SEND_SEARCH_TYPE() {
+    }
+}

+ 58 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/FPlayDataCallBackEx.java

@@ -0,0 +1,58 @@
+package com.usky.sas.common.dahua;
+
+import com.usky.sas.common.global.StreamContext;
+import com.sun.jna.Pointer;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FPlayDataCallBackEx implements NetSDKLib.fDataCallBack {
+    private static final Logger log = LoggerFactory.getLogger(FPlayDataCallBackEx.class);
+    StreamContext context;
+    String macAddr;
+
+    public FPlayDataCallBackEx(StreamContext context, String macAddr) {
+        this.context = context;
+        this.macAddr = macAddr;
+    }
+
+    public void invoke(Long lRealHandle, int dwDataType, Pointer pBuffer, int dwBufSize, Pointer dwUser) {
+        try {
+            if (dwBufSize > 2097152) {
+                log.warn("Frame size too large ({}) - truncating to {}", dwBufSize, 2097152);
+                dwBufSize = 2097152;
+            }
+
+            ByteBuffer buffer = pBuffer.getByteBuffer(0L, (long)dwBufSize);
+            buffer.get(this.context.frameBuffer, 0, dwBufSize);
+            long currentTimestamp = (long)this.context.baseTimestamp;
+            StreamContext var10000 = this.context;
+            var10000.baseTimestamp += 3600;
+            int offset = 0;
+
+            int packetSize;
+            for(int remaining = dwBufSize; remaining > 0; remaining -= packetSize) {
+                packetSize = Math.min(remaining, 1400);
+                boolean marker = remaining == packetSize;
+                byte[] rtpPacket = DahuaVideoStreamService.createRtpPacket(this.context.frameBuffer, offset, packetSize, marker, currentTimestamp, this.context.sequenceNumber, this.context.ssrc);
+                byte[] tcpPacket = DahuaVideoStreamService.createTcpPacket(rtpPacket);
+                this.context.sendExecutor.submit(() -> {
+                    try {
+                        this.context.rtpOutputStream.write(tcpPacket);
+                        this.context.rtpOutputStream.flush();
+                    } catch (IOException e) {
+                        log.error("RTP数据发送失败: {}", e.getMessage(), e);
+                    }
+
+                });
+                this.context.sequenceNumber = this.context.sequenceNumber + 1 & '\uffff';
+                offset += packetSize;
+            }
+        } catch (Exception var15) {
+            DahuaVideoStreamService.stopPlayStreamData(lRealHandle, this.macAddr);
+            this.context.close();
+        }
+
+    }
+}

+ 62 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/FRealDataCallBackEx.java

@@ -0,0 +1,62 @@
+package com.usky.sas.common.dahua;
+
+import com.usky.sas.common.global.StreamContext;
+import com.sun.jna.Pointer;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FRealDataCallBackEx implements NetSDKLib.fRealDataCallBackEx {
+    private static final Logger log = LoggerFactory.getLogger(FRealDataCallBackEx.class);
+    StreamContext context;
+    String macAddr;
+
+    public FRealDataCallBackEx(StreamContext context, String macAddr) {
+        this.context = context;
+        this.macAddr = macAddr;
+    }
+
+    public void invoke(Long lRealHandle, int dwDataType, Pointer pBuffer, int dwBufSize, int param, Pointer dwUser) {
+        try {
+            log.info("实时预览回调");
+            if (dwBufSize > 2097152) {
+                log.warn("Frame size too large ({}) - truncating to {}", dwBufSize, 2097152);
+                dwBufSize = 2097152;
+            }
+
+            ByteBuffer buffer = pBuffer.getByteBuffer(0L, (long)dwBufSize);
+            buffer.get(this.context.frameBuffer, 0, dwBufSize);
+            if (dwDataType == 0) {
+                log.info("原始数据");
+                long currentTimestamp = (long)this.context.baseTimestamp;
+                StreamContext var10000 = this.context;
+                var10000.baseTimestamp += 3600;
+                int offset = 0;
+
+                int packetSize;
+                for(int remaining = dwBufSize; remaining > 0; remaining -= packetSize) {
+                    packetSize = Math.min(remaining, 1400);
+                    boolean marker = remaining == packetSize;
+                    byte[] rtpPacket = DahuaVideoStreamService.createRtpPacket(this.context.frameBuffer, offset, packetSize, marker, currentTimestamp, this.context.sequenceNumber, this.context.ssrc);
+                    byte[] tcpPacket = DahuaVideoStreamService.createTcpPacket(rtpPacket);
+                    this.context.sendExecutor.submit(() -> {
+                        try {
+                            this.context.rtpOutputStream.write(tcpPacket);
+                            this.context.rtpOutputStream.flush();
+                        } catch (IOException e) {
+                            log.error("RTP数据发送失败: {}", e.getMessage(), e);
+                        }
+
+                    });
+                    this.context.sequenceNumber = this.context.sequenceNumber + 1 & '\uffff';
+                    offset += packetSize;
+                }
+            }
+        } catch (Exception e) {
+            log.error("设备 {} 数据发送失败: {}", this.macAddr, e.getMessage());
+            this.context.close();
+        }
+
+    }
+}

+ 254 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/InitNetSDKLib.java

@@ -0,0 +1,254 @@
+package com.usky.sas.common.dahua;
+
+import cn.hutool.core.util.StrUtil;
+import com.usky.sas.common.exception.BusinessException;
+import com.usky.sas.common.global.GlobalMemoryMap;
+import com.usky.sas.common.hik.OsSelect;
+import com.usky.sas.service.impl.SasDeviceServiceImpl;
+import com.sun.jna.Callback;
+import com.sun.jna.Memory;
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+import com.sun.jna.Structure;
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class InitNetSDKLib {
+    private static final Logger log = LoggerFactory.getLogger(InitNetSDKLib.class);
+    static Map<String, DahuaNvrInfo> dhNvrUser;
+    public static Map<String, NetSDKLib> dhSDKHashMap;
+    public static NetSDKLib dhNetSDK;
+
+    public InitNetSDKLib() {
+    }
+
+    private static boolean createSDKInstance() {
+        if (dhNetSDK == null) {
+            synchronized(NetSDKLib.class) {
+                String strDllPath = "";
+
+                try {
+                    if (OsSelect.isWindows()) {
+                        strDllPath = "D:\\文档和图片\\工作文档\\厂家协议\\海康\\设备网络SDK_JAVA_Win64_IS_V3.060.0000003.0.R.251127\\General_NetSDK_ChnEng_JAVA_Win64_IS_V3.060.0000003.0.R.251127\\libs\\win64\\dhnetsdk.dll";
+                    } else if (OsSelect.isLinux()) {
+                        strDllPath = "/home/uskycloud_c01/conf/libdhnetsdk.so";
+                    }
+
+                    dhNetSDK = (NetSDKLib)Native.loadLibrary(strDllPath, NetSDKLib.class);
+                    log.info("初始化NetSDKLib成功============");
+                    dhNetSDK.CLIENT_Init((Callback)null, (Pointer)null);
+                    dhNetSDK.CLIENT_SetAutoReconnect((Callback)null, (Pointer)null);
+                    dhNetSDK.CLIENT_SetConnectTime(10000, 1);
+                    dhSDKHashMap.put("dhNetSDK", dhNetSDK);
+                    NetSDKLib.LOG_SET_PRINT_INFO setLog = new NetSDKLib.LOG_SET_PRINT_INFO();
+                    File path = new File("./dhsdklog/");
+                    if (!path.exists()) {
+                        path.mkdir();
+                    }
+
+                    String logPath = path.getAbsoluteFile().getParent() + "\\dhsdklog\\" + getDate() + ".log";
+                    setLog.nPrintStrategy = 0;
+                    setLog.bSetFilePath = 1;
+                    System.arraycopy(logPath.getBytes(), 0, setLog.szLogFilePath, 0, logPath.getBytes().length);
+                    setLog.bSetPrintStrategy = 1;
+                    boolean bLogopen = dhNetSDK.CLIENT_LogOpen(setLog);
+                    if (!bLogopen) {
+                        log.error("打开日志失败");
+                    }
+                } catch (Exception ex) {
+                    log.error("初始化失败,loadLibrary: " + strDllPath + " Error: " + ex.getMessage());
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    public static String getDate() {
+        SimpleDateFormat simpleDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        String date = simpleDate.format(new Date()).replace(" ", "_").replace(":", "-");
+        return date;
+    }
+
+    public static Long login(String m_strIp, int m_nPort, String m_strUser, String m_strPassword) {
+        NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam = new NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY();
+        pstInParam.nPort = m_nPort;
+        pstInParam.szIP = m_strIp.getBytes();
+        pstInParam.szPassword = m_strPassword.getBytes();
+        pstInParam.szUserName = m_strUser.getBytes();
+        NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam = new NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();
+        Long m_hLoginHandle = dhNetSDK.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);
+        if (m_hLoginHandle == 0L) {
+            log.error("设备:{}:{}登录失败,原因:{}", new Object[]{m_strIp, m_nPort, dhNetSDK.CLIENT_GetLastError()});
+            throw new BusinessException("请检查设备IP、端口、用户名、密码等配置是否正确!", -1);
+        } else {
+            log.info("设备:{}:{}登陆成功,句柄:{}", new Object[]{m_strIp, m_nPort, m_hLoginHandle});
+            return m_hLoginHandle;
+        }
+    }
+
+    public static DahuaNvrInfo nvrLogin(String m_strIp, int m_nPort, String m_strUser, String m_strPassword, String mac) {
+        NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam = new NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY();
+        pstInParam.nPort = m_nPort;
+        pstInParam.szIP = m_strIp.getBytes();
+        pstInParam.szPassword = m_strPassword.getBytes();
+        pstInParam.szUserName = m_strUser.getBytes();
+        NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam = new NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();
+        Long m_hLoginHandle = dhNetSDK.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);
+        if (m_hLoginHandle == 0L) {
+            log.error("设备:{}:{}登录失败,原因:{}", new Object[]{m_strIp, m_nPort, dhNetSDK.CLIENT_GetLastError()});
+            throw new BusinessException("请检查设备IP、端口、用户名、密码等配置是否正确!", -1);
+        } else {
+            log.info("设备:{}:{}登陆成功,句柄:{}", new Object[]{m_strIp, m_nPort, m_hLoginHandle});
+            DahuaNvrInfo dahuaNvrInfo = new DahuaNvrInfo();
+            dahuaNvrInfo.setLoginHandle(m_hLoginHandle);
+            dahuaNvrInfo.setMaxChannelNum(pstOutParam.stuDeviceInfo.byChanNum);
+            dhNvrUser.put(mac, dahuaNvrInfo);
+            return dahuaNvrInfo;
+        }
+    }
+
+    public static List<RemoteDeviceInfo> getNvrChannel(String m_strIp, int m_nPort, String m_strUser, String m_strPassword) {
+        NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY pstInParam = new NetSDKLib.NET_IN_LOGIN_WITH_HIGHLEVEL_SECURITY();
+        pstInParam.nPort = m_nPort;
+        pstInParam.szIP = m_strIp.getBytes();
+        pstInParam.szPassword = m_strPassword.getBytes();
+        pstInParam.szUserName = m_strUser.getBytes();
+        NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY pstOutParam = new NetSDKLib.NET_OUT_LOGIN_WITH_HIGHLEVEL_SECURITY();
+        Long m_hLoginHandle = dhNetSDK.CLIENT_LoginWithHighLevelSecurity(pstInParam, pstOutParam);
+        if (m_hLoginHandle == 0L) {
+            log.error("设备:{}:{}登录失败,原因:{}", new Object[]{m_strIp, m_nPort, dhNetSDK.CLIENT_GetLastError()});
+            throw new BusinessException("请检查NVR设备IP、端口、用户名、密码等配置是否正确!", -1);
+        } else {
+            log.info("设备:{}:{}登陆成功,句柄:{}", new Object[]{m_strIp, m_nPort, m_hLoginHandle});
+            DahuaNvrInfo dahuaNvrInfo = new DahuaNvrInfo();
+            dahuaNvrInfo.setLoginHandle(m_hLoginHandle);
+            dahuaNvrInfo.setMaxChannelNum(pstOutParam.stuDeviceInfo.byChanNum);
+            dhNvrUser.put(m_strIp, dahuaNvrInfo);
+            List<RemoteDeviceInfo> list = new ArrayList();
+            int byChanNum = pstOutParam.stuDeviceInfo.byChanNum;
+            if (byChanNum != 0) {
+                NetSDKLib.NET_MATRIX_CAMERA_INFO[] cameras = new NetSDKLib.NET_MATRIX_CAMERA_INFO[byChanNum];
+
+                for(int i = 0; i < byChanNum; ++i) {
+                    NetSDKLib.NET_MATRIX_CAMERA_INFO camera = new NetSDKLib.NET_MATRIX_CAMERA_INFO();
+                    NetSDKLib.NET_REMOTE_DEVICE device = new NetSDKLib.NET_REMOTE_DEVICE();
+                    camera.stuRemoteDevice = device;
+                    cameras[i] = camera;
+                }
+
+                NetSDKLib.NET_IN_MATRIX_GET_CAMERAS inMatrixGetCameras = new NetSDKLib.NET_IN_MATRIX_GET_CAMERAS();
+                inMatrixGetCameras.dwSize = inMatrixGetCameras.size();
+                NetSDKLib.NET_OUT_MATRIX_GET_CAMERAS outMatrixGetCameras = new NetSDKLib.NET_OUT_MATRIX_GET_CAMERAS();
+                outMatrixGetCameras.dwSize = outMatrixGetCameras.size();
+                outMatrixGetCameras.nMaxCameraCount = byChanNum;
+                outMatrixGetCameras.pstuCameras = new Memory((long)(cameras[0].size() * byChanNum));
+                outMatrixGetCameras.pstuCameras.clear((long)(cameras[0].size() * byChanNum));
+                SetStructArrToPointerData(cameras, outMatrixGetCameras.pstuCameras);
+                boolean getCameras = dhNetSDK.CLIENT_MatrixGetCameras(m_hLoginHandle, inMatrixGetCameras, outMatrixGetCameras, 5000);
+                if (getCameras) {
+                    GetPointerDataToStructArr(outMatrixGetCameras.pstuCameras, cameras);
+
+                    for(int i = 0; i < outMatrixGetCameras.nRetCameraCount; ++i) {
+                        int isRemoteDevice = cameras[i].bRemoteDevice;
+                        if (isRemoteDevice == 1) {
+                            NetSDKLib.NET_REMOTE_DEVICE remoteDevice = cameras[i].stuRemoteDevice;
+                            String deviceName = (new String(remoteDevice.szDevName)).trim();
+                            String ipAddr = (new String(remoteDevice.szIp)).trim();
+                            if (StrUtil.isNotBlank(ipAddr) && !ipAddr.equals("0.0.0.0") && !ipAddr.equals("192.168.0.0") && StrUtil.isNotBlank(deviceName)) {
+                                RemoteDeviceInfo info = new RemoteDeviceInfo();
+                                info.setChannel(cameras[i].nUniqueChannel + 1);
+                                info.setIpAddr(ipAddr);
+                                info.setNote(deviceName);
+                                list.add(info);
+                            }
+                        }
+                    }
+                }
+            }
+
+            return list;
+        }
+    }
+
+    public static Integer getIpcChannel(Long loginHandle, String ipAddr, int byChanNum) {
+        if (byChanNum != 0) {
+            NetSDKLib.NET_MATRIX_CAMERA_INFO[] cameras = new NetSDKLib.NET_MATRIX_CAMERA_INFO[byChanNum];
+
+            for(int i = 0; i < byChanNum; ++i) {
+                NetSDKLib.NET_MATRIX_CAMERA_INFO camera = new NetSDKLib.NET_MATRIX_CAMERA_INFO();
+                NetSDKLib.NET_REMOTE_DEVICE device = new NetSDKLib.NET_REMOTE_DEVICE();
+                camera.stuRemoteDevice = device;
+                cameras[i] = camera;
+            }
+
+            NetSDKLib.NET_IN_MATRIX_GET_CAMERAS inMatrixGetCameras = new NetSDKLib.NET_IN_MATRIX_GET_CAMERAS();
+            inMatrixGetCameras.dwSize = inMatrixGetCameras.size();
+            NetSDKLib.NET_OUT_MATRIX_GET_CAMERAS outMatrixGetCameras = new NetSDKLib.NET_OUT_MATRIX_GET_CAMERAS();
+            outMatrixGetCameras.dwSize = outMatrixGetCameras.size();
+            outMatrixGetCameras.nMaxCameraCount = byChanNum;
+            outMatrixGetCameras.pstuCameras = new Memory((long)(cameras[0].size() * byChanNum));
+            outMatrixGetCameras.pstuCameras.clear((long)(cameras[0].size() * byChanNum));
+            SetStructArrToPointerData(cameras, outMatrixGetCameras.pstuCameras);
+            boolean getCameras = dhNetSDK.CLIENT_MatrixGetCameras(loginHandle, inMatrixGetCameras, outMatrixGetCameras, 5000);
+            if (getCameras) {
+                GetPointerDataToStructArr(outMatrixGetCameras.pstuCameras, cameras);
+
+                for(int i = 0; i < outMatrixGetCameras.nRetCameraCount; ++i) {
+                    int isRemoteDevice = cameras[i].bRemoteDevice;
+                    if (isRemoteDevice == 1) {
+                        NetSDKLib.NET_REMOTE_DEVICE remoteDevice = cameras[i].stuRemoteDevice;
+                        String ip = (new String(remoteDevice.szIp)).trim();
+                        if (ip.equals(ipAddr)) {
+                            return cameras[i].nUniqueChannel;
+                        }
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public static void SetStructArrToPointerData(Structure[] pJavaStuArr, Pointer pNativeData) {
+        long offset = 0L;
+
+        for(int i = 0; i < pJavaStuArr.length; ++i) {
+            SasDeviceServiceImpl.SetStructDataToPointer(pJavaStuArr[i], pNativeData, offset);
+            offset += (long)pJavaStuArr[i].size();
+        }
+
+    }
+
+    public static void GetPointerDataToStruct(Pointer pNativeData, long OffsetOfpNativeData, Structure pJavaStu) {
+        pJavaStu.write();
+        Pointer pJavaMem = pJavaStu.getPointer();
+        pJavaMem.write(0L, pNativeData.getByteArray(OffsetOfpNativeData, pJavaStu.size()), 0, pJavaStu.size());
+        pJavaStu.read();
+    }
+
+    public static void GetPointerDataToStructArr(Pointer pNativeData, Structure[] pJavaStuArr) {
+        long offset = 0L;
+
+        for(int i = 0; i < pJavaStuArr.length; ++i) {
+            GetPointerDataToStruct(pNativeData, offset, pJavaStuArr[i]);
+            offset += (long)pJavaStuArr[i].size();
+        }
+
+    }
+
+    static {
+        dhNvrUser = GlobalMemoryMap.dhNvrUser;
+        dhSDKHashMap = new ConcurrentHashMap();
+        dhNetSDK = (NetSDKLib)dhSDKHashMap.get("dhNetSDK");
+        createSDKInstance();
+    }
+}

+ 13 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/NET_BRIDGE_NET_CARDS_MAC_LIST.java

@@ -0,0 +1,13 @@
+package com.usky.sas.common.dahua;
+
+import com.sun.jna.Structure;
+
+public class NET_BRIDGE_NET_CARDS_MAC_LIST extends Structure {
+    public byte[] szNetCardName = new byte[32];
+    public byte[] szNetCardMac = new byte[18];
+    public byte[] szReserved = new byte[14];
+
+    public NET_BRIDGE_NET_CARDS_MAC_LIST() {
+    }
+}
+

+ 24 - 0
service-sas/service-sas-biz/src/main/java/com/usky/sas/common/dahua/NET_IN_STARTSERACH_DEVICE.java

@@ -0,0 +1,24 @@
+package com.usky.sas.common.dahua;
+
+import com.sun.jna.Pointer;
+import com.sun.jna.Structure;
+import java.util.Arrays;
+import java.util.List;
+
+public class NET_IN_STARTSERACH_DEVICE extends Structure {
+    public int dwSize;
+    public byte[] szLocalIp = new byte[64];
+    public NetSDKLib.fSearchDevicesCBEx cbSearchDevices;
+    public Pointer pUserData;
+    public int emSendType;
+
+    public NET_IN_STARTSERACH_DEVICE() {
+        // 必须在数组字段初始化之后再设置 size
+        this.dwSize = this.size();
+    }
+
+    @Override
+    protected List<String> getFieldOrder() {
+        return Arrays.asList("dwSize", "szLocalIp", "cbSearchDevices", "pUserData", "emSendType");
+    }
+}

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác