49 Commity c2da1491e7 ... 06f6b60fef

Autor SHA1 Wiadomość Data
  fuyuchuan 06f6b60fef 优化人防单元等基础数据推送代码 1 tydzień temu
  hanzhengyi 8da7539662 Merge branch 'han' of uskycloud/usky-modules into master 3 tygodni temu
  james a60bc4aeb4 Merge branch 'han' of http://47.111.81.118:3000/uskycloud/usky-modules into han 1 miesiąc temu
  james bf9aa77aec 调整请求agbox回调数据请求逻辑 1 miesiąc temu
  hanzhengyi 42f051fa9d ipc解绑接口调整 1 miesiąc temu
  james 36a90ec9cf 1、优化十个类型事件对应的图片获取逻辑,增加访问AGox获取场景图片的逻辑; 1 miesiąc temu
  hanzhengyi 52c5692c8c 调整电子地图查询接口 1 miesiąc temu
  hanzhengyi 99ea39b852 优化ems 1 miesiąc temu
  hanzhengyi daf2ed0740 完善电子地图相关接口,新增ems 1 miesiąc temu
  hanzhengyi 19c89c8472 Merge branch 'han' of http://47.111.81.118:3000/uskycloud/usky-modules into han 1 miesiąc temu
  james eb834925d1 优化安防系统上报不同事件类型事件过程中遇到的逻辑或数据问题(存库的历史事件数据未在界面展示、出入门禁控制人员类型和进出事件类型对应不上) 1 miesiąc temu
  hanzhengyi 030fa74147 Merge branch 'han' of http://47.111.81.118:3000/uskycloud/usky-modules into han 1 miesiąc temu
  fuyuchuan 184c3c0f30 解决冲突 1 miesiąc temu
  hanzhengyi c55857ce98 Merge branch 'han' 1 miesiąc temu
  hanzhengyi 663d8e08d9 完善电子地图相关接口 1 miesiąc temu
  hanzhengyi 9842472de6 完善电子地图相关接口 1 miesiąc temu
  hanzhengyi 69019bc422 补充视频导出防护配置、实时电子巡检配置相关接口 1 miesiąc temu
  james 82c21133fc 优化mqtt服务实时巡检事件逻辑,增加channel赋值默认0查询 1 miesiąc temu
  hanzhengyi ed11c8c8f3 完善相关接口 1 miesiąc temu
  james 569ad51b3e 如果没有在sys_device表中,接收到的事件不存入库,同时不推websocket,同时在推送的websocket消息体中增加事件等级Id 1 miesiąc temu
  hanzhengyi b99f18630c 完善相关接口 1 miesiąc temu
  fuyuchuan a06486a352 系统信息代码提交 1 miesiąc temu
  zhaojinyu aaa5480f72 处理各事件类型图片存储 1 miesiąc temu
  hanzhengyi 4a74f6d9aa 完善相关接口 1 miesiąc temu
  hanzhengyi d203cd0688 新增线上环境海康大华配置文件 1 miesiąc temu
  hanzhengyi 01efeeccd1 新增相关类型查询接口,完善海康大华相关接口 1 miesiąc temu
  james ca2a6b644b 开发处理消费事件mqtt消息推送websocket服务逻辑,同时处理心跳mqtt消息发生时间同步到设备信息表对应字段 1 miesiąc temu
  hanzhengyi 948ad1bce5 删除相关无关文件 1 miesiąc temu
  hanzhengyi 4fbbf2c18e 海康、大华、onvif相关插件及接口 1 miesiąc temu
  hanzhengyi 80de41555f 海康、大华、onvif相关插件及接口 1 miesiąc temu
  james 7d563eb113 创建websocket服务监听 1 miesiąc temu
  james e6d6c0c9b0 生成onvif摄像头SDK配置 1 miesiąc temu
  james dfb82578c0 添加dahua依赖 1 miesiąc temu
  james 6f90f956e2 优化事件列表:各模块所有事件分页(中上-事件列表区域)和查询事件详情两个接口,增加deviceType设备类型查询 1 miesiąc temu
  hanzhengyi 5d75d07c4a 增加批量新增设备配置 1 miesiąc temu
  james 8ac594a9e2 优化首页接口逻辑,开发智能分析相关接口 1 miesiąc temu
  fuyuchuan e523d5b9e5 付宇川-智能安防集成系统代码提交:系统信息 1 miesiąc temu
  hanzhengyi 27596c54f7 电子地图、系统管理相关接口 1 miesiąc temu
  hanzhengyi a904a7716e 调整mqtt模块 1 miesiąc temu
  hanzhengyi 4b6d69e479 系统集成配置接口 1 miesiąc temu
  zhaojinyu 71317e348e 优化事件类及mqtt监听类解决接收事件报错问题——目前USB与巡检 1 miesiąc temu
  zhaojinyu 3aed249482 添加mqtt依赖,完成10个设备类型事件agbox对接 1 miesiąc temu
  hanzhengyi 544115171d 一标六实及人员管理接口完善、agbox对接 1 miesiąc temu
  hanzhengyi d93ea7eb1d 首页接口补充 1 miesiąc temu
  hanzhengyi f078c4aa77 智能安防集成应用系统 1 miesiąc temu
  fuyuchuan 61b9928e04 Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 miesięcy temu
  fuyuchuan 2c3c9df4f9 Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 miesięcy temu
  fuyuchuan c5cbd9bfcc Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 miesięcy temu
  fuyuchuan b03d835b0a Merge branch 'fu-dev' of uskycloud/usky-modules into master 2 miesięcy temu
100 zmienionych plików z 4483 dodań i 304 usunięć
  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>-->
 
-    </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>

+ 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.setEntityLombokModel(true); //使用lombokbase_build_plane
         //修改自己想要生成的表
-        strategy.setInclude("base_build");  // 逆向工程使用的表   如果要生成多个,这里可以传入String[]
+        strategy.setInclude("dmp_device");  // 逆向工程使用的表   如果要生成多个,这里可以传入String[]
         mpg.setStrategy(strategy);
 
         // 关闭默认 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.vo.alarm.AlarmMessageVO;
+import com.usky.cdi.service.vo.alarm.AlarmMessage1VO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 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")
-    public String batchSendSensorInfos(@RequestParam(value = "tenantId",required = false) Integer tenantId) {
-        Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId);
+    public String batchSendSensorInfos(@RequestParam(value = "tenantId",required = false) Integer tenantId,
+                                       @RequestParam(value = "engineeringId") Long engineeringId,
+                                       @RequestParam(value = "username") String username,
+                                       @RequestParam(value = "password") String password) {
+        Map<String, Integer> map = baseDataTransferService.batchSendSensorInfos(tenantId, engineeringId, username, password);
         return String.format("上报成功 %d", map.get("success"));
     }
 }

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

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

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

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

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

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

+ 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.DmpDeviceMapper;
 import com.usky.cdi.mapper.DmpProductMapper;
-import com.usky.cdi.service.CdiDeliveryLogService;
 import com.usky.cdi.service.config.mqtt.MqttOutConfig;
 import com.usky.cdi.service.enums.MqttTopics;
 import com.usky.cdi.service.mqtt.MqttConnectionTool;
@@ -50,8 +49,6 @@ import java.util.stream.Collectors;
 @Service
 public class IotDataTransferService {
 
-    private MqttOutConfig.MqttGateway mqttGateway;
-
     @Autowired
     private MqttConnectionTool mqttConnectionTool;
 
@@ -1153,8 +1150,17 @@ public class IotDataTransferService {
      * @param username 用户名
      */
     void sendMqttMessage(String topic, Object vo, String messageType, String username) {
-        String json = JSON.toJSONString(vo);
-        // 不再记录每条数据的详情,只记录发送操作
+        String json;
+        
+        // 针对楼层平面图特殊处理:将 byte[] 转为 Base64 字符串
+        if (vo instanceof com.usky.cdi.service.vo.info.FloorPlaneVO) {
+            json = serializeFloorPlaneVO((com.usky.cdi.service.vo.info.FloorPlaneVO) vo);
+        } else {
+            json = JSON.toJSONString(vo);
+        }
+        
+        log.info("发送MQTT消息,Topic: {}, 消息类型: {}, JSON长度: {}", topic, messageType, json.length());
+        
         MqttConnectionTool.MqttGateway gateway = mqttGatewayMap.get(username);
         if (gateway != null) {
             gateway.sendToMqtt(topic, json);
@@ -1163,6 +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) {
         Integer tenantId = 0;
         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;
 
+import com.usky.cdi.service.vo.base.BaseMqttInfo;
 import lombok.Data;
-import java.io.Serializable;
 
 /**
  * 人防工程基础信息VO
@@ -11,7 +11,7 @@ import java.io.Serializable;
  * @date 2025/03/20
  */
 @Data
-public class EngineeringBaseVO implements Serializable {
+public class EngineeringBaseVO extends BaseMqttInfo {
     private static final long serialVersionUID = 1L;
 
     /**

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

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

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

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

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

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

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

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików