Browse Source

Merge branch 'master' into usky-zyj

james 12 hours ago
parent
commit
77f58a6f10
100 changed files with 3803 additions and 197 deletions
  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. 2 2
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java
  5. 69 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/alarm/AlarmMessage1VO.java
  6. 2 0
      service-eg/service-eg-api/src/main/java/com/usky/eg/RemoteEgService.java
  7. 5 1
      service-eg/service-eg-api/src/main/java/com/usky/eg/factory/RemoteEgFallbackFactory.java
  8. 25 3
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceController.java
  9. 41 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceHeartbeatController.java
  10. 11 13
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDevice.java
  11. 84 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDeviceHeartbeat.java
  12. 34 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDevicePersonBind.java
  13. 1 1
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgRecord.java
  14. 98 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/SysPerson.java
  15. 18 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDeviceHeartbeatMapper.java
  16. 44 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDevicePersonBindMapper.java
  17. 22 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/SysPersonMapper.java
  18. 19 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceHeartbeatService.java
  19. 15 2
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceService.java
  20. 91 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceHeartbeatServiceImpl.java
  21. 425 133
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceServiceImpl.java
  22. 17 10
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgRecordServiceImpl.java
  23. 21 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonQueryVO.java
  24. 21 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonVO.java
  25. 9 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceRequestVO.java
  26. 20 0
      service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceHeartbeatMapper.xml
  27. 1 2
      service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceMapper.xml
  28. 62 0
      service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDevicePersonBindMapper.xml
  29. 32 0
      service-eg/service-eg-biz/src/main/resources/mapper/eg/SysPersonMapper.xml
  30. 61 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAnalysisController.java
  31. 233 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsApiV1Controller.java
  32. 37 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAuthController.java
  33. 106 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsReportController.java
  34. 39 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsChannel.java
  35. 69 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDevice.java
  36. 50 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDeviceFunction.java
  37. 44 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsEnergyItemCode.java
  38. 64 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsGateway.java
  39. 71 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProject.java
  40. 45 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpace.java
  41. 46 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpaceArea.java
  42. 67 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpaceBuilding.java
  43. 45 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsSpaceFloor.java
  44. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsChannelMapper.java
  45. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDeviceFunctionMapper.java
  46. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDeviceMapper.java
  47. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsEnergyItemCodeMapper.java
  48. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsGatewayMapper.java
  49. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectMapper.java
  50. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceAreaMapper.java
  51. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceBuildingMapper.java
  52. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceFloorMapper.java
  53. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsSpaceMapper.java
  54. 19 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsAnalysisService.java
  55. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsAuthService.java
  56. 22 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsGatewayQueryService.java
  57. 72 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsModelService.java
  58. 20 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsOverviewService.java
  59. 32 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsReportService.java
  60. 61 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAnalysisServiceImpl.java
  61. 30 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAuthServiceImpl.java
  62. 87 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsGatewayQueryServiceImpl.java
  63. 401 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsModelServiceImpl.java
  64. 53 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsOverviewServiceImpl.java
  65. 164 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsReportServiceImpl.java
  66. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCategoryRatioItemVO.java
  67. 17 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCollectionRealtimeItemVO.java
  68. 16 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCollectionRealtimeRequest.java
  69. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCollectionRealtimeResponse.java
  70. 15 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareRequest.java
  71. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareResponse.java
  72. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareSeriesItemVO.java
  73. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareValueVO.java
  74. 18 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyStatisticsItemVO.java
  75. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyStatisticsRequest.java
  76. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyStatisticsResponse.java
  77. 16 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsEnergyTypeVO.java
  78. 24 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsGatewayDetailResponse.java
  79. 22 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsGatewayListItem.java
  80. 21 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsGatewayPageRequest.java
  81. 16 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsIdResponse.java
  82. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsLoginRequest.java
  83. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsLoginResponse.java
  84. 35 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsModelSaveRequest.java
  85. 23 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsProjectResponse.java
  86. 18 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsRegionAnalysisItemVO.java
  87. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsRegionAnalysisResponse.java
  88. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsReportAttributeVO.java
  89. 17 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsReportDeviceItemVO.java
  90. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsReportDevicesResponse.java
  91. 18 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsStructureTreeNode.java
  92. 11 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsSummaryRequest.java
  93. 20 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsSummaryResponse.java
  94. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendCategoryResponse.java
  95. 18 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendIndicatorsResponse.java
  96. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendItemVO.java
  97. 14 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsTrendResponse.java
  98. 6 0
      service-job/pom.xml
  99. 9 0
      service-job/src/main/java/com/ruoyi/job/task/RyTask.java
  100. 0 10
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/EgDevice.java

+ 28 - 19
pom.xml

@@ -91,25 +91,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;

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

@@ -228,8 +228,8 @@ public class BaseDataTransferService {
             userIdToName.put(709, 15);
             userIdToName.put(710, 16);
             userIdToName.put(711, 2);
-            // userIdToName.put(712, 34);
-            // userIdToName.put(713, 36);
+            //userIdToName.put(712, 34);
+            //userIdToName.put(713, 36);
             userIdToName.put(714, 37);
 
             HashMap<String, Object> map = new HashMap<>();

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

+ 2 - 0
service-eg/service-eg-api/src/main/java/com/usky/eg/RemoteEgService.java

@@ -13,4 +13,6 @@ import java.util.Map;
 @FeignClient(contextId = "remoteEgService", value = "service-eg", fallbackFactory = RemoteEgFallbackFactory.class)
 public interface RemoteEgService {
 
+    @GetMapping("/egDeviceStatus")
+    void egDeviceStatus();
 }

+ 5 - 1
service-eg/service-eg-api/src/main/java/com/usky/eg/factory/RemoteEgFallbackFactory.java

@@ -1,5 +1,6 @@
 package com.usky.eg.factory;
 
+import com.usky.common.core.exception.FeignBadRequestException;
 import com.usky.eg.RemoteEgService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -23,7 +24,10 @@ public class RemoteEgFallbackFactory implements FallbackFactory<RemoteEgService>
     {
         log.error("用户服务调用失败:{}", throwable.getMessage());
         return new RemoteEgService() {
-
+            @Override
+            public void egDeviceStatus() {
+                throw new FeignBadRequestException(500,"同步门禁设备状态异常"+throwable.getMessage());
+            }
         };
     }
 }

+ 25 - 3
service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceController.java

@@ -7,6 +7,8 @@ import com.usky.common.log.annotation.Log;
 import com.usky.common.log.enums.BusinessType;
 import com.usky.eg.domain.EgDevice;
 import com.usky.eg.service.EgDeviceService;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonQueryVO;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonVO;
 import com.usky.eg.service.vo.EgDeviceRequestVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -32,6 +34,11 @@ public class EgDeviceController {
         return ApiResult.success(egDeviceService.page(requestVO));
     }
 
+    @PostMapping("/bindFacePerson/page")
+    public ApiResult<CommonPage<EgDeviceBindFacePersonVO>> pageBindFacePersons(@RequestBody EgDeviceBindFacePersonQueryVO queryVO) {
+        return ApiResult.success(egDeviceService.pageBindFacePersons(queryVO));
+    }
+
     @PostMapping("wePage")
     public ApiResult<CommonPage<EgDevice>> wePage(@RequestBody EgDeviceRequestVO requestVO){
         return ApiResult.success(egDeviceService.wePage(requestVO));
@@ -81,6 +88,20 @@ public class EgDeviceController {
         return ApiResult.success();
     }
 
+    /**
+     * 设备绑定人员
+     * @param params 包含 deviceId, personIds, isLoginNotify 的参数对象
+     * @return
+     */
+    @PostMapping("/bindPerson")
+    public ApiResult<Void> bindPerson(@RequestBody Map<String, Object> params){
+        Integer deviceId = (Integer) params.get("deviceId");
+        String personIds = (String) params.get("personIds");
+        Integer isLoginNotify = params.get("isLoginNotify") != null ? (Integer) params.get("isLoginNotify") : 0;
+        egDeviceService.bindPerson(deviceId, personIds, isLoginNotify);
+        return ApiResult.success();
+    }
+
     /**
      * 下发设备控制命令
      */
@@ -90,12 +111,13 @@ public class EgDeviceController {
                                                  @RequestParam("deviceUuid") String deviceUuid,
                                                  @RequestParam("commandCode") String commandCode,
                                                  @RequestParam("commandValue") String commandValue,
-                                                 @RequestParam(value = "domain",required = false) String domain,
                                                  @RequestParam(value = "userId",required = false) Long userId,
                                                  @RequestParam(value = "userName",required = false) String userName,
                                                  @RequestParam(value = "categoryType",required = false , defaultValue = "1") Integer categoryType,
-                                                 @RequestParam(value = "gatewayUuid",required = false) String gatewayUuid){
-        return ApiResult.success(egDeviceService.control(productCode,deviceUuid,commandCode,commandValue,domain,userId,userName,categoryType,gatewayUuid));
+                                                 @RequestParam(value = "gatewayUuid",required = false) String gatewayUuid,
+                                                 @RequestParam(value = "skipCheck",required = false, defaultValue = "false") Boolean skipCheck,
+                                                 @RequestParam(value = "passType",required = false) Integer passType){
+        return ApiResult.success(egDeviceService.control(productCode,deviceUuid,commandCode,commandValue,userId,userName,categoryType,gatewayUuid,skipCheck,passType));
     }
 
 }

+ 41 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceHeartbeatController.java

@@ -0,0 +1,41 @@
+package com.usky.eg.controller.web;
+
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.eg.service.EgDeviceHeartbeatService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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;
+
+/**
+ * <p>
+ * 门禁设备心跳表 前端控制器
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+@RestController
+@RequestMapping("/egDeviceHeartbeat")
+public class EgDeviceHeartbeatController {
+    private static final Logger log = LoggerFactory.getLogger(EgDeviceHeartbeatController.class);
+    @Autowired
+    private EgDeviceHeartbeatService heartbeatService;
+
+    @PostMapping("/escalation")
+    public ApiResult<Void> heartbeatEscalation(@RequestBody EgDeviceHeartbeat heartbeat) {
+        try {
+            heartbeatService.heartbeatEscalation(heartbeat);
+        } catch (Exception e) {
+            log.error("设备心跳异常", e);
+            return ApiResult.error("设备心跳入库异常!");
+        }
+        return ApiResult.success();
+    }
+}

+ 11 - 13
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDevice.java

@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import java.time.LocalDateTime;
 import java.io.Serializable;
-import java.util.List;
 
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -62,16 +61,6 @@ public class EgDevice implements Serializable {
      */
     private String deviceIp;
 
-    /**
-     * 端口
-     */
-    private Integer devicePort;
-
-    /**
-     * 门禁号
-     */
-    private String egNumber;
-
     /**
      * 绑定人脸信息
      */
@@ -108,6 +97,12 @@ public class EgDevice implements Serializable {
      */
     private Integer tenantId;
 
+    /**
+     * 绑定人员信息(从 eg_device_person_bind 表汇总的 person_id 列表,以逗号分隔)
+     */
+    @TableField("bind_person")
+    private String bindPerson;
+
     /**
      * 屏保
      */
@@ -134,10 +129,13 @@ public class EgDevice implements Serializable {
     private String workStatus;
 
     /**
+     * 设备code
+     */
+    private String deviceCode;
 
     /**
-     * 用户人脸信息记录
+     * 设备状态;1:在线,0:离线(从心跳表获取,不映射到数据库)
      */
     @TableField(exist = false)
-    private List<MeetingFace> meetingFaceList;
+    private Integer deviceStatus;
 }

+ 84 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDeviceHeartbeat.java

@@ -0,0 +1,84 @@
+package com.usky.eg.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 门禁设备心跳表
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class EgDeviceHeartbeat implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 门禁设备心跳表主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 设备code
+     */
+    private String deviceCode;
+
+    /**
+     * 设备ip地址
+     */
+    private String ipAddr;
+
+    /**
+     * 设备mac地址
+     */
+    private String macAddr;
+
+    /**
+     * 设备类型(1.会议屏 2.信息发布屏 3.综合屏)
+     */
+    private Boolean deviceType;
+
+    /**
+     * 创建日期
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 设备型号
+     */
+    private String model;
+
+    /**
+     *  设备厂商
+     */
+    private String manuFacturer;
+
+    /**
+     * 设备版本号
+     */
+    private String version;
+
+    /**
+     * 设备sdk
+     */
+    private String sdk;
+
+    /**
+     * 设备状态;1:在线,0:离线
+     */
+    private Integer deviceStatus;
+
+}

+ 34 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDevicePersonBind.java

@@ -0,0 +1,34 @@
+package com.usky.eg.domain;
+
+import java.io.Serializable;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 门禁设备人员绑定表 eg_device_person_bind
+ * </p>
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class EgDevicePersonBind implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 设备主键ID
+     */
+    private Integer deviceId;
+
+    /**
+     * 人员ID
+     */
+    private Integer personId;
+
+    /**
+     * 是否打开登录通知(1 表示是,0 表示否,默认0)
+     */
+    private Integer isLoginNotify;
+}
+

+ 1 - 1
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgRecord.java

@@ -46,7 +46,7 @@ public class EgRecord implements Serializable {
     private Integer egDeviceId;
 
     /**
-     * 通行方式(1、人脸 2、刷卡 3、手机)
+     * 通行方式(0、其它 1、人脸 2、刷卡 3、手机 4、电脑)
      */
     private Integer passType;
 

+ 98 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/SysPerson.java

@@ -0,0 +1,98 @@
+package com.usky.eg.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+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;
+
+/**
+ * <p>
+ * 人员信息表
+ * </p>
+ *
+ * @author zyj
+ * @since 2024-11-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sys_person")
+public class SysPerson implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /** 姓名 */
+    private String fullName;
+
+    /** 年龄 */
+    private Integer age;
+
+    /** 性别;1男、2女 */
+    private Integer gender;
+
+    /** 家庭住址 */
+    private String address;
+
+    /** 文化程度 */
+    private Integer educationDegree;
+
+    /** 身份证号 */
+    private String idNumber;
+
+    /** 联系方式 */
+    private String linkPhone;
+
+    /** 岗位ID */
+    private Long postId;
+
+    /** 部门ID */
+    private Long deptId;
+
+    /** 入职时间 */
+    private LocalDateTime entryTime;
+
+    /** 证书1 */
+    private String certificateUrl1;
+
+    /** 证书2 */
+    private String certificateUrl2;
+
+    /** 证书3 */
+    private String certificateUrl3;
+
+    /** 创建人 */
+    private String creator;
+
+    /** 创建时间 */
+    private LocalDateTime createTime;
+
+    /** 更新人 */
+    private String updatePerson;
+
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+
+    /** 图片数据(base64编码) */
+    private String faceBase;
+
+    /** 验证次数(默认0) */
+    private Integer vefNum;
+
+    /** 人脸名称 */
+    private String faceName;
+
+    /** 人脸备注 */
+    private String remark;
+
+    /** 人脸状态(0=可用,1=不可用) */
+    private Byte faceStatus;
+
+    /** 卡号 */
+    private String cardNum;
+}

+ 18 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDeviceHeartbeatMapper.java

@@ -0,0 +1,18 @@
+package com.usky.eg.mapper;
+
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.common.mybatis.core.CrudMapper;
+import org.springframework.stereotype.Repository;
+
+/**
+ * <p>
+ * 门禁设备心跳表 Mapper 接口
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+@Repository
+public interface EgDeviceHeartbeatMapper extends CrudMapper<EgDeviceHeartbeat> {
+
+}

+ 44 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDevicePersonBindMapper.java

@@ -0,0 +1,44 @@
+package com.usky.eg.mapper;
+
+import com.usky.eg.domain.EgDevicePersonBind;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface EgDevicePersonBindMapper {
+
+    /**
+     * 根据多个设备ID查询绑定关系
+     */
+    List<EgDevicePersonBind> selectByDeviceIds(@Param("deviceIds") List<Integer> deviceIds);
+
+    /**
+     * 统计某个设备下的绑定人数
+     */
+    Integer countByDeviceId(@Param("deviceId") Integer deviceId);
+
+    /**
+     * 根据用户ID和设备ID统计绑定关系
+     * 通过 sys_user_person -> sys_person -> eg_device_person_bind 进行关联
+     */
+    Integer countByUserIdAndDeviceId(@Param("userId") Long userId, @Param("deviceId") Integer deviceId);
+
+    /**
+     * 根据用户ID查询其绑定的设备ID列表
+     * 通过 sys_user_person -> sys_person -> eg_device_person_bind 进行关联
+     */
+    List<Integer> selectDeviceIdsByUserId(@Param("userId") Long userId);
+
+    /**
+     * 根据设备ID删除绑定关系
+     */
+    void deleteByDeviceId(@Param("deviceId") Integer deviceId);
+
+    /**
+     * 新增绑定关系
+     */
+    void insert(EgDevicePersonBind bind);
+}
+

+ 22 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/SysPersonMapper.java

@@ -0,0 +1,22 @@
+package com.usky.eg.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.usky.eg.domain.SysPerson;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 人员信息表 Mapper
+ */
+@Mapper
+public interface SysPersonMapper extends BaseMapper<SysPerson> {
+
+    /**
+     * 查询所有有效的人脸数据(face_base不为空且face_status=0)
+     */
+    @Select("SELECT id, full_name, face_base, face_name, card_num FROM sys_person WHERE face_base IS NOT NULL AND face_base != '' AND face_status = 0")
+    List<SysPerson> selectAllValidFace();
+}

+ 19 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceHeartbeatService.java

@@ -0,0 +1,19 @@
+package com.usky.eg.service;
+
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 门禁设备心跳表 服务类
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+public interface EgDeviceHeartbeatService extends CrudService<EgDeviceHeartbeat> {
+
+    void heartbeatEscalation(EgDeviceHeartbeat heartbeat);
+
+    void updateEgDeviceStatus();
+}

+ 15 - 2
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceService.java

@@ -3,8 +3,9 @@ package com.usky.eg.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.eg.domain.EgDevice;
 import com.usky.common.mybatis.core.CrudService;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonQueryVO;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonVO;
 import com.usky.eg.service.vo.EgDeviceRequestVO;
-import org.springframework.web.bind.annotation.RequestBody;
 
 import java.util.Map;
 
@@ -33,5 +34,17 @@ public interface EgDeviceService extends CrudService<EgDevice> {
 
     boolean checkDeviceNameUnique(EgDevice egDevice);
 
-    Map<String,Object> control(String productCode, String deviceUuid, String commandCode, String commandValue, String domain, Long userId, String userName, Integer categoryType, String gatewayUuid);
+    Map<String,Object> control(String productCode, String deviceUuid, String commandCode, String commandValue, Long userId, String userName, Integer categoryType, String gatewayUuid, Boolean skipCheck, Integer passType);
+
+    /**
+     * 设备与人员绑定
+     *
+     * @param deviceId      设备主键ID
+     * @param personIds     人员ID,多个以逗号分隔
+     * @param isLoginNotify 是否打开登录通知(1 表示是,0 表示否)
+     */
+    void bindPerson(Integer deviceId, String personIds, Integer isLoginNotify);
+
+    /** 绑定表人员分页(可选姓名、人脸状态) */
+    CommonPage<EgDeviceBindFacePersonVO> pageBindFacePersons(EgDeviceBindFacePersonQueryVO queryVO);
 }

+ 91 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceHeartbeatServiceImpl.java

@@ -0,0 +1,91 @@
+package com.usky.eg.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.eg.mapper.EgDeviceHeartbeatMapper;
+import com.usky.eg.service.EgDeviceHeartbeatService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * <p>
+ * 门禁设备心跳表 服务实现类
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+@Service
+public class EgDeviceHeartbeatServiceImpl extends AbstractCrudService<EgDeviceHeartbeatMapper, EgDeviceHeartbeat> implements EgDeviceHeartbeatService {
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void heartbeatEscalation(EgDeviceHeartbeat heartbeat) {
+        if (StringUtils.isBlank(heartbeat.getDeviceCode())) {
+            throw new BusinessException("设备编码不能为空!");
+        }
+        if (StringUtils.isBlank(heartbeat.getIpAddr())) {
+            throw new BusinessException("设备IP不能为空!");
+        }
+        if (heartbeat.getCreateTime() == null) {
+            heartbeat.setCreateTime(LocalDateTime.now());
+        }
+        if (heartbeat.getDeviceType() == null) {
+            throw new BusinessException("设备类型不能为空!");
+        }
+
+        // 查询是否存在该设备的心跳记录
+        LambdaQueryWrapper<EgDeviceHeartbeat> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.eq(EgDeviceHeartbeat::getDeviceCode, heartbeat.getDeviceCode());
+        EgDeviceHeartbeat existingHeartbeat = baseMapper.selectOne(queryWrapper);
+        
+        if (existingHeartbeat != null) {
+            // 如果存在,更新记录
+            heartbeat.setId(existingHeartbeat.getId());
+            baseMapper.updateById(heartbeat);
+        } else {
+            // 如果不存在,插入新记录
+            baseMapper.insert(heartbeat);
+        }
+    }
+
+    /**
+     * 更新设备状态定时任务服务接口
+     * 从心跳表中获取设备状态并同步更新
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateEgDeviceStatus() {
+        try {
+            LocalDateTime now = LocalDateTime.now();
+            
+            // 1. 查询所有当前状态为在线的心跳记录
+            LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatQueryWrapper = Wrappers.lambdaQuery();
+            heartbeatQueryWrapper.eq(EgDeviceHeartbeat::getDeviceStatus, 1);
+            List<EgDeviceHeartbeat> heartbeatList = baseMapper.selectList(heartbeatQueryWrapper);
+
+            // 2. 遍历在线记录,若心跳时间超过5分钟则更新为离线
+            for (EgDeviceHeartbeat heartbeat : heartbeatList) {
+                LocalDateTime heartbeatTime = heartbeat.getCreateTime();
+                if (heartbeatTime != null && !heartbeatTime.isAfter(now.minusMinutes(5))) {
+                    // 心跳超时,更新心跳表中的状态为离线
+                    LambdaUpdateWrapper<EgDeviceHeartbeat> updateWrapper = Wrappers.lambdaUpdate();
+                    updateWrapper.set(EgDeviceHeartbeat::getDeviceStatus, 0)
+                            .eq(EgDeviceHeartbeat::getId, heartbeat.getId());
+                    baseMapper.update(null, updateWrapper);
+                }
+            }
+        } catch (Exception e) {
+            // 定时任务失败不影响主流程,记录日志即可
+            throw new BusinessException("更新设备状态失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误"));
+        }
+    }
+}

+ 425 - 133
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceServiceImpl.java

@@ -13,22 +13,28 @@ import com.usky.common.core.exception.BusinessException;
 import com.usky.common.core.util.UUIDUtils;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.eg.domain.EgDevice;
-import com.usky.eg.domain.MeetingFace;
-import com.usky.eg.domain.MeetingFaceDevice;
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.eg.domain.EgDevicePersonBind;
+import com.usky.eg.domain.EgRecord;
+import com.usky.eg.domain.SysPerson;
 import com.usky.eg.mapper.EgDeviceMapper;
-import com.usky.eg.mapper.MeetingFaceDeviceMapper;
-import com.usky.eg.mapper.MeetingFaceMapper;
+import com.usky.eg.mapper.EgDeviceHeartbeatMapper;
+import com.usky.eg.mapper.EgDevicePersonBindMapper;
+import com.usky.eg.mapper.EgRecordMapper;
+import com.usky.eg.mapper.SysPersonMapper;
 import com.usky.eg.service.EgDeviceService;
 import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonQueryVO;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonVO;
 import com.usky.eg.service.vo.EgDeviceRequestVO;
 import com.usky.iot.RemoteIotTaskService;
 import com.usky.transfer.RemoteTransferService;
-import io.swagger.models.auth.In;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.concurrent.CompletableFuture;
 
 /**
  * <p>
@@ -40,25 +46,35 @@ import java.util.*;
  */
 @Service
 public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgDevice> implements EgDeviceService {
-    @Autowired
-    private MeetingFaceDeviceMapper meetingFaceDeviceMapper;
-    @Autowired
-    private MeetingFaceMapper meetingFaceMapper;
     @Autowired
     private EgDeviceMapper egDeviceMapper;
     @Autowired
+    private EgDeviceHeartbeatMapper egDeviceHeartbeatMapper;
+    @Autowired
     private RemoteIotTaskService remoteIotTaskService;
-
     @Autowired
     private RemoteTransferService remoteTransferService;
+    @Autowired
+    private EgDevicePersonBindMapper egDevicePersonBindMapper;
+    @Autowired
+    private EgRecordMapper egRecordMapper;
+    @Autowired
+    private SysPersonMapper sysPersonMapper;
 
     @Override
     public CommonPage<EgDevice> page(EgDeviceRequestVO requestVO){
         IPage<EgDevice> page = new Page<>(requestVO.getCurrent(),requestVO.getSize());
-        Integer tenantId ;
+        Integer tenantId;
 
-        if(StringUtils.isNotBlank(requestVO.getDomain())){
-            tenantId = egDeviceMapper.sysTenantId(requestVO.getDomain());
+        if(StringUtils.isNotBlank(requestVO.getDeviceCode())){
+            LambdaQueryWrapper<EgDevice> tempQueryWrapper = new LambdaQueryWrapper<>();
+            tempQueryWrapper.eq(EgDevice::getDeviceCode, requestVO.getDeviceCode());
+            EgDevice tempDevice = egDeviceMapper.selectOne(tempQueryWrapper);
+            if(tempDevice == null){
+                // 未查询到数据,返回空数组
+                return new CommonPage<>(new ArrayList<>(), 0L, requestVO.getSize(), requestVO.getCurrent());
+            }
+            tenantId = tempDevice.getTenantId();
         }else{
             tenantId = SecurityUtils.getTenantId();
         }
@@ -69,34 +85,70 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
                 .eq(null != requestVO.getServiceStatus(),EgDevice::getServiceStatus,requestVO.getServiceStatus())
                 .eq(null != requestVO.getId(),EgDevice::getId,requestVO.getId())
                 .eq(null != requestVO.getDeviceUuid(),EgDevice::getDeviceUuid, requestVO.getDeviceUuid())
+                .eq(StringUtils.isNotBlank(requestVO.getDeviceCode()),EgDevice::getDeviceCode, requestVO.getDeviceCode())
                 .eq(EgDevice::getTenantId,tenantId)
                 .orderByDesc(EgDevice::getId);
         page = this.page(page,queryWrapper);
+
         if(!page.getRecords().isEmpty()){
+            // 检查设备心跳
+            List<String> validDeviceCodes = new ArrayList<>();
+            Map<String, EgDevice> deviceCodeMap = new HashMap<>();
+            for (EgDevice device : page.getRecords()) {
+                String deviceCode = device.getDeviceCode();
+                if (StringUtils.isNotBlank(deviceCode)) {
+                    validDeviceCodes.add(deviceCode);
+                    deviceCodeMap.put(deviceCode, device);
+                } else {
+                    // deviceCode为空,直接设置为离线
+                    device.setDeviceStatus(0);
+                }
+            }
 
-            LambdaQueryWrapper<MeetingFace> meetingFaceQuery = Wrappers.lambdaQuery();
-            meetingFaceQuery.select(MeetingFace::getFid,MeetingFace::getCreateTime,MeetingFace::getVefNum,MeetingFace::getFaceName,MeetingFace::getRemark,MeetingFace::getFaceStatus,MeetingFace::getCardNum,MeetingFace::getBindDevice,MeetingFace::getDeptId,MeetingFace::getTenantId,MeetingFace::getUserId)
-                    .eq(MeetingFace::getTenantId,tenantId);
-            List<MeetingFace> meetingAllFaceList = meetingFaceMapper.selectList(meetingFaceQuery);
-
-            for (int i = 0; i < page.getRecords().size(); i++) {
-                if(Objects.nonNull(page.getRecords().get(i).getBindFace()) ||StringUtils.isNotBlank(page.getRecords().get(i).getBindFace())){
-                    String[] fidListStr = page.getRecords().get(i).getBindFace().split(",");
-                    Integer[] fidList = Arrays.stream(fidListStr).map(Integer::parseInt).toArray(Integer[]::new);
-
-                    List<MeetingFace> meetingFaceList = new ArrayList<>();
-                    for (int j = 0; j < fidList.length; j++) {
-                        for (int k = 0; k < meetingAllFaceList.size(); k++) {
-                            if(fidList[j] == meetingAllFaceList.get(k).getFid()){
-                                meetingFaceList.add(meetingAllFaceList.get(k));
-                                break;
+            // 批量查询心跳数据
+            if (!validDeviceCodes.isEmpty()) {
+                LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatQueryWrapper = Wrappers.lambdaQuery();
+                heartbeatQueryWrapper.in(EgDeviceHeartbeat::getDeviceCode, validDeviceCodes)
+                        .orderByDesc(EgDeviceHeartbeat::getCreateTime);
+                List<EgDeviceHeartbeat> heartbeatList = egDeviceHeartbeatMapper.selectList(heartbeatQueryWrapper);
+                
+                // 构建deviceCode到最新心跳记录的映射,并同时设置deviceStatus
+                Map<String, EgDeviceHeartbeat> deviceHeartbeatMap = new HashMap<>();
+                for (EgDeviceHeartbeat heartbeat : heartbeatList) {
+                    String deviceCode = heartbeat.getDeviceCode();
+                    if (StringUtils.isNotBlank(deviceCode)) {
+                        // 如果已存在,保留最新的(因为已按createTime降序排列)
+                        if (deviceHeartbeatMap.putIfAbsent(deviceCode, heartbeat) == null) {
+                            // 首次遇到该deviceCode,从心跳表直接获取deviceStatus并设置到EgDevice
+                            EgDevice device = deviceCodeMap.get(deviceCode);
+                            if (device != null && heartbeat.getDeviceStatus() != null) {
+                                device.setDeviceStatus(heartbeat.getDeviceStatus());
                             }
                         }
                     }
-                    page.getRecords().get(i).setMeetingFaceList(meetingFaceList);
                 }
-
+                
+                // 如果未找到对应的设备心跳记录,则设置DeviceStatus为0(离线)
+                for (String deviceCode : validDeviceCodes) {
+                    if (!deviceHeartbeatMap.containsKey(deviceCode)) {
+                        EgDevice device = deviceCodeMap.get(deviceCode);
+                        if (device != null) {
+                            device.setDeviceStatus(0);
+                        }
+                    }
+                }
+            } else {
+                // 如果没有有效的deviceCode,将所有设备的deviceStatus设置为0
+                for (EgDevice device : page.getRecords()) {
+                    device.setDeviceStatus(0);
+                }
             }
+
+        }
+
+        // 查询并填充绑定人员信息(来自 eg_device_person_bind,person_id 逗号分隔后写入 bindPerson 字段)
+        if (!page.getRecords().isEmpty()) {
+            fillBindPerson(page.getRecords());
         }
 
         return new CommonPage<>(page.getRecords(),page.getTotal(),requestVO.getSize(),requestVO.getCurrent());
@@ -105,86 +157,150 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
     @Override
     public CommonPage<EgDevice> wePage(EgDeviceRequestVO requestVO){
         long userId = SecurityUtils.getUserId();
-        //人员设备权限校验,校验通过,可以下发命令控制设备
-        Integer fid = baseMapper.getMeetingFaceData(userId);
-//        if(fid == null){
-//            throw new BusinessException("人脸卡号信息未注册");
-//        }
-        Integer[] deviceFid = baseMapper.getMeetingFaceDeviceList(fid);
-//        if(deviceFid.length == 0){
-//            throw new BusinessException("人员未绑定设备,请检查");
-//        }
+        // 通过 sys_user_person -> sys_person -> eg_device_person_bind -> eg_device 查询当前用户绑定的设备ID列表
+        List<Integer> deviceIds = egDevicePersonBindMapper.selectDeviceIdsByUserId(userId);
+        if (CollectionUtils.isEmpty(deviceIds)) {
+            // 未绑定任何设备,返回空分页
+            return new CommonPage<>(new ArrayList<>(), 0L, requestVO.getSize(), requestVO.getCurrent());
+        }
 
         IPage<EgDevice> page = new Page<>(requestVO.getCurrent(),requestVO.getSize());
-        if(deviceFid.length > 0){
-            LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
-            queryWrapper.like(StringUtils.isNotBlank(requestVO.getDeviceName()),EgDevice::getDeviceName,requestVO.getDeviceName())
-                    .like(StringUtils.isNotBlank(requestVO.getInstallAddress()),EgDevice::getInstallAddress,requestVO.getInstallAddress())
-                    .eq(null != requestVO.getServiceStatus(),EgDevice::getServiceStatus,requestVO.getServiceStatus())
-                    .eq(null != requestVO.getId(),EgDevice::getId,requestVO.getId())
-                    .in(EgDevice::getId,deviceFid)
-                    .eq(EgDevice::getTenantId,SecurityUtils.getTenantId())
-                    .orderByDesc(EgDevice::getId);
-            page = this.page(page,queryWrapper);
-        }
+        LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.like(StringUtils.isNotBlank(requestVO.getDeviceName()),EgDevice::getDeviceName,requestVO.getDeviceName())
+                .like(StringUtils.isNotBlank(requestVO.getInstallAddress()),EgDevice::getInstallAddress,requestVO.getInstallAddress())
+                .eq(null != requestVO.getServiceStatus(),EgDevice::getServiceStatus,requestVO.getServiceStatus())
+                .eq(null != requestVO.getId(),EgDevice::getId,requestVO.getId())
+                .in(EgDevice::getId, deviceIds)
+                .eq(EgDevice::getTenantId,SecurityUtils.getTenantId())
+                .orderByDesc(EgDevice::getId);
+        page = this.page(page,queryWrapper);
 
+        // 查询并填充绑定人员信息
+        if (!page.getRecords().isEmpty()) {
+            fillBindPerson(page.getRecords());
+        }
 
         return new CommonPage<>(page.getRecords(),page.getTotal(),requestVO.getSize(),requestVO.getCurrent());
     }
 
     @Override
-    public void add(EgDevice egDevice){
-        if(checkNameUnique(egDevice)){
-            throw new BusinessException("新增门禁门号设备'"+egDevice.getDeviceId()+","+egDevice.getEgNumber()+"'失败,设备已存在");
+    public void add(EgDevice egDevice) {
+        LocalDateTime now = LocalDateTime.now();
+        
+        // 1. 校验设备名称唯一性
+        if (checkDeviceNameUnique(egDevice)) {
+            throw new BusinessException("新增门禁设备'" + egDevice.getDeviceName() + "'失败,设备已存在");
         }
-        if(checkDeviceNameUnique(egDevice)){
-            throw new BusinessException("新增门禁设备'"+egDevice.getDeviceName()+"'失败,设备已存在");
+
+        // 2. 校验设备IP是否已被绑定
+        if (StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+            LambdaQueryWrapper<EgDevice> ipCheckWrapper = Wrappers.lambdaQuery();
+            ipCheckWrapper.eq(EgDevice::getDeviceIp, egDevice.getDeviceIp())
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId());
+            EgDevice existingDeviceByIp = this.getOne(ipCheckWrapper);
+            if (existingDeviceByIp != null) {
+                throw new BusinessException("设备IP'" + egDevice.getDeviceIp() + "'已被设备'" 
+                        + existingDeviceByIp.getDeviceName() + "'绑定,请更换IP或检查设备配置!");
+            }
+        }
+
+        // 3. 若未传deviceCode但传了deviceIp,尝试从心跳表中获取deviceCode
+        if (StringUtils.isBlank(egDevice.getDeviceCode()) && StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+            LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatCheckWrapper = Wrappers.lambdaQuery();
+            heartbeatCheckWrapper.eq(EgDeviceHeartbeat::getIpAddr, egDevice.getDeviceIp())
+                    .eq(EgDeviceHeartbeat::getDeviceStatus, 1);
+            EgDeviceHeartbeat heartbeat = egDeviceHeartbeatMapper.selectOne(heartbeatCheckWrapper);
+            if (heartbeat != null && StringUtils.isNotBlank(heartbeat.getDeviceCode())) {
+                egDevice.setDeviceCode(heartbeat.getDeviceCode());
+            }
         }
 
+        // 4. 校验设备编码是否已被绑定
+        if (StringUtils.isNotBlank(egDevice.getDeviceCode())) {
+            LambdaQueryWrapper<EgDevice> codeCheckWrapper = Wrappers.lambdaQuery();
+            codeCheckWrapper.eq(EgDevice::getDeviceCode, egDevice.getDeviceCode())
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId());
+            EgDevice existingDeviceByCode = this.getOne(codeCheckWrapper);
+            if (existingDeviceByCode != null) {
+                throw new BusinessException("设备编码'" + egDevice.getDeviceCode() + "'已被设备'" 
+                        + existingDeviceByCode.getDeviceName() + "'绑定,请更换设备编码或检查设备配置!");
+            }
+        }
+
+        // 5. 设置设备基本信息
         egDevice.setDeviceUuid(UUIDUtils.uuid());
         egDevice.setCreateBy(SecurityUtils.getUsername());
-        egDevice.setCreateTime(LocalDateTime.now());
+        egDevice.setCreateTime(now);
         egDevice.setTenantId(SecurityUtils.getTenantId());
+        egDevice.setOpenMode("人脸");
+        egDevice.setWorkStatus("4");
 
+        // 6. 保存设备
         this.save(egDevice);
 
-        String[] fids = new String[0];
-        if(Objects.nonNull(egDevice.getBindFace()) || StringUtils.isNotBlank(egDevice.getBindFace())){
-            fids = egDevice.getBindFace().split(",");
-        }
-        if(fids.length > 0){
-            for (int i = 0; i < fids.length; i++) {
-                egDeviceMapper.insertMeetingFaceDevice(Integer.parseInt(fids[i]),egDevice.getId());
-            }
+        // 7. 异步调用远程服务添加设备信息,静默处理异常,不影响设备保存成功
+        String deviceUuid = egDevice.getDeviceUuid();
+        String deviceName = egDevice.getDeviceName();
+        if (StringUtils.isNotBlank(deviceUuid) && StringUtils.isNotBlank(deviceName)) {
+            String installAddress = StringUtils.isNotBlank(egDevice.getInstallAddress()) 
+                    ? egDevice.getInstallAddress() 
+                    : "";
+            Integer serviceStatus = egDevice.getServiceStatus() != null 
+                    ? egDevice.getServiceStatus() 
+                    : 1;
+            
+            CompletableFuture.runAsync(() -> {
+                try {
+                    remoteIotTaskService.addDeviceInfo("502_USKY", deviceUuid, deviceUuid, 
+                            deviceName, installAddress, serviceStatus);
+                } catch (Exception e) {
+                    // 远程服务调用失败,静默处理,不影响设备保存成功
+                }
+            });
         }
-
-        remoteIotTaskService.addDeviceInfo("502_USKY", egDevice.getDeviceUuid(),egDevice.getDeviceId()+egDevice.getEgNumber(),egDevice.getDeviceName(),egDevice.getInstallAddress(),egDevice.getServiceStatus());
     }
 
     @Override
     public void update(EgDevice egDevice) {
-
-        String[] fids = new String[0];
-        if(Objects.nonNull(egDevice.getBindFace()) || StringUtils.isNotBlank(egDevice.getBindFace())){
-            fids = egDevice.getBindFace().split(",");
-
-            egDeviceMapper.deleteMeetingFaceDevice(egDevice.getId());
-        }else{
-            EgDevice one = this.getById(egDevice.getId());
-            egDevice.setBindFace(one.getBindFace());
+        if(checkDeviceNameUnique(egDevice)){
+            throw new BusinessException("修改门禁设备'"+egDevice.getDeviceName()+"'失败,设备名称已存在");
         }
-        if(fids.length > 0){
-            for (int i = 0; i < fids.length; i++) {
-                egDeviceMapper.insertMeetingFaceDevice(Integer.parseInt(fids[i]),egDevice.getId());
+
+        // 校验设备IP是否已被其他设备绑定(排除当前设备)
+        if (StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+            LambdaQueryWrapper<EgDevice> ipCheckWrapper = Wrappers.lambdaQuery();
+            ipCheckWrapper.eq(EgDevice::getDeviceIp, egDevice.getDeviceIp())
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId())
+                    .ne(EgDevice::getId, egDevice.getId());
+            EgDevice existingDeviceByIp = this.getOne(ipCheckWrapper);
+            if (existingDeviceByIp != null) {
+                throw new BusinessException("设备IP'" + egDevice.getDeviceIp() + "'已被设备'" 
+                        + existingDeviceByIp.getDeviceName() + "'绑定,请更换IP或检查设备配置!");
             }
         }
 
-
-        if(checkNameUnique(egDevice)){
-            throw new BusinessException("修改门禁门号设备'"+egDevice.getDeviceId()+","+egDevice.getEgNumber()+"'失败,设备已存在");
+        // 若未传deviceCode但传了deviceIp,尝试从心跳表中获取deviceCode
+        if (StringUtils.isBlank(egDevice.getDeviceCode()) && StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+            LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatCheckWrapper = Wrappers.lambdaQuery();
+            heartbeatCheckWrapper.eq(EgDeviceHeartbeat::getIpAddr, egDevice.getDeviceIp())
+                    .eq(EgDeviceHeartbeat::getDeviceStatus, 1);
+            EgDeviceHeartbeat heartbeat = egDeviceHeartbeatMapper.selectOne(heartbeatCheckWrapper);
+            if (heartbeat != null && StringUtils.isNotBlank(heartbeat.getDeviceCode())) {
+                egDevice.setDeviceCode(heartbeat.getDeviceCode());
+            }
         }
-        if(checkDeviceNameUnique(egDevice)){
-            throw new BusinessException("新增门禁设备'"+egDevice.getDeviceName()+"'失败,设备已存在");
+
+        // 校验设备编码是否已被其他设备绑定(排除当前设备)
+        if (StringUtils.isNotBlank(egDevice.getDeviceCode())) {
+            LambdaQueryWrapper<EgDevice> codeCheckWrapper = Wrappers.lambdaQuery();
+            codeCheckWrapper.eq(EgDevice::getDeviceCode, egDevice.getDeviceCode())
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId())
+                    .ne(EgDevice::getId, egDevice.getId());
+            EgDevice existingDeviceByCode = this.getOne(codeCheckWrapper);
+            if (existingDeviceByCode != null) {
+                throw new BusinessException("设备编码'" + egDevice.getDeviceCode() + "'已被设备'" 
+                        + existingDeviceByCode.getDeviceName() + "'绑定,请更换设备编码或检查设备配置!");
+            }
         }
 
         egDevice.setUpdateBy(SecurityUtils.getUsername());
@@ -199,9 +315,7 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         EgDevice one = this.getById(egDevice.getId());
         egDevice.setBindFace(one.getBindFace());
 
-        if(checkNameUnique(egDevice)){
-            throw new BusinessException("更新门禁设备附加功能'"+egDevice.getDeviceId()+"'失败,设备已存在");
-        }
+
         if(checkDeviceNameUnique(egDevice)){
             throw new BusinessException("新增门禁设备'"+egDevice.getDeviceName()+"'失败,设备已存在");
         }
@@ -217,16 +331,72 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         EgDevice egDevice = this.getById(id);
         Optional.ofNullable(egDevice).orElseThrow(() -> new BusinessException("门禁设备信息不存在"));
 
-        LambdaQueryWrapper<MeetingFaceDevice> queryWrapper = Wrappers.lambdaQuery();
-        queryWrapper.eq(MeetingFaceDevice::getDeviceId,id);
-        Integer count = meetingFaceDeviceMapper.selectCount(queryWrapper);
-        if(count > 0){
+        // 校验 eg_device_person_bind 是否存在绑定人员
+        Integer personBindCount = egDevicePersonBindMapper.countByDeviceId(id);
+        if (personBindCount != null && personBindCount > 0) {
             throw new BusinessException("已绑定人员不能删除");
         }
 
         this.removeById(id);
 
-        remoteIotTaskService.deleteDeviceInfo(egDevice.getDeviceUuid());
+        // 异步调用远程服务删除设备信息,静默处理异常,不影响设备删除
+        String deviceUuid = egDevice.getDeviceUuid();
+        if (StringUtils.isNotBlank(deviceUuid)) {
+            CompletableFuture.runAsync(() -> {
+                try {
+                    remoteIotTaskService.deleteDeviceInfo(deviceUuid);
+                } catch (Exception e) {
+                    // 远程服务调用失败,静默处理,不影响设备删除
+                }
+            });
+        }
+    }
+
+    /**
+     * 根据设备ID集合查询 eg_device_person_bind,将 person_id 按逗号拼接后写入设备的 bindPerson 字段
+     */
+    private void fillBindPerson(List<EgDevice> devices) {
+        if (CollectionUtils.isEmpty(devices)) {
+            return;
+        }
+
+        List<Integer> deviceIds = new ArrayList<>();
+        for (EgDevice device : devices) {
+            if (device != null && device.getId() != null) {
+                deviceIds.add(device.getId());
+            }
+        }
+
+        if (deviceIds.isEmpty()) {
+            return;
+        }
+
+        List<EgDevicePersonBind> bindList = egDevicePersonBindMapper.selectByDeviceIds(deviceIds);
+        if (CollectionUtils.isEmpty(bindList)) {
+            return;
+        }
+
+        Map<Integer, List<String>> devicePersonMap = new HashMap<>();
+        for (EgDevicePersonBind bind : bindList) {
+            if (bind.getDeviceId() == null || bind.getPersonId() == null) {
+                continue;
+            }
+            devicePersonMap
+                    .computeIfAbsent(bind.getDeviceId(), k -> new ArrayList<>())
+                    .add(String.valueOf(bind.getPersonId()));
+        }
+
+        for (EgDevice device : devices) {
+            if (device == null || device.getId() == null) {
+                continue;
+            }
+            List<String> personIds = devicePersonMap.get(device.getId());
+            if (personIds != null && !personIds.isEmpty()) {
+                device.setBindPerson(String.join(",", personIds));
+            } else {
+                device.setBindPerson(null);
+            }
+        }
     }
 
     @Override
@@ -234,7 +404,6 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         Integer id = null == egDevice.getId() ? -1 : egDevice.getId();
         LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
         queryWrapper.eq(EgDevice::getDeviceId,egDevice.getDeviceId())
-                .eq(EgDevice::getEgNumber,egDevice.getEgNumber())
                 .eq(EgDevice::getTenantId, SecurityUtils.getTenantId());
         EgDevice one = this.getOne(queryWrapper);
         return null != one && !Objects.equals(one.getId(),id);
@@ -251,56 +420,179 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
     }
 
     @Override
-    public Map<String,Object> control(String productCode, String deviceUuid, String commandCode, String commandValue, String domain, Long userId, String userName, Integer categoryType, String gatewayUuid){
-        Integer tenantId;
-        long commandUserId;
-        String commandUserName;
-        if(StringUtils.isNotBlank(domain)){
-            tenantId = baseMapper.sysTenantId(domain);
-            commandUserId = userId;
-            commandUserName = userName;
-        }else{
-            tenantId = SecurityUtils.getTenantId();
-            commandUserId = SecurityUtils.getUserId();
-            commandUserName = SecurityUtils.getUsername();
+    public Map<String,Object> control(String productCode, String deviceUuid, String commandCode, String commandValue, Long userId, String userName, Integer categoryType, String gatewayUuid, Boolean skipCheck, Integer passType){
+        // 1. 查询设备
+        EgDevice device = this.getOne(Wrappers.<EgDevice>lambdaQuery()
+                .select(EgDevice::getId, EgDevice::getTenantId)
+                .eq(EgDevice::getDeviceUuid, deviceUuid));
+        if (device == null) {
+            throw new BusinessException("设备未注册,请先注册");
         }
 
-        //人员设备权限校验,校验通过,可以下发命令控制设备
-        Integer fid = baseMapper.getMeetingFaceData(commandUserId);
-        if(fid == null){
-            throw new BusinessException("人脸卡号信息未注册");
-        }
-        Integer[] deviceFid = baseMapper.getMeetingFaceDeviceList(fid);
-        if(deviceFid.length == 0){
-            throw new BusinessException("人员未绑定设备,请检查");
-        }
+        Integer tenantId = device.getTenantId();
+        long commandUserId = userId != null ? userId : SecurityUtils.getUserId();
+        String commandUserName = userName != null ? userName : SecurityUtils.getUsername();
 
-        LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
-        queryWrapper.select(EgDevice::getId)
-                .eq(EgDevice::getDeviceUuid,deviceUuid);
-        EgDevice one = this.getOne(queryWrapper);
-        if(one != null){
-            boolean exist = Arrays.asList(deviceFid).contains(one.getId());
-            if(!exist){
+        // 2. 权限校验
+        if (!Boolean.TRUE.equals(skipCheck)) {
+            Integer bindCount = egDevicePersonBindMapper.countByUserIdAndDeviceId(commandUserId, device.getId());
+            if (bindCount == null || bindCount == 0) {
+                saveRecord(device.getId(), tenantId, commandUserName, passType, "失败:暂无权限");
                 throw new BusinessException("暂无权限");
             }
-        }else{
-            throw new BusinessException("设备未注册,请先注册");
         }
 
-
+        // 3. 构建下发命令参数
+        Map<String,Object> params = new HashMap<>();
+        params.put("commandCode", commandCode);
+        params.put("commandValue", commandValue);
         Map<String,Object> map = new HashMap<>();
-        map.put("method","control");
+        map.put("method", "control");
         map.put("deviceUuid", deviceUuid);
-        Map<String,Object> map1 = new HashMap<>();
-        map1.put("commandCode",commandCode);
-        map1.put("commandValue",commandValue);
-        map.put("params",map1);
+        map.put("params", params);
+
+        // 4. 下发命令并记录结果
+        String targetUuid = categoryType == 3 ? gatewayUuid : deviceUuid;
+        Map<String,Object> result = null;
+        String passResult = "下发命令失败";
+        try {
+            result = remoteTransferService.deviceControl(productCode, targetUuid, JSON.toJSONString(map), tenantId, commandUserId, commandUserName);
+            if (result == null || !Integer.valueOf(200).equals(result.get("code"))) {
+                passResult = result != null && result.get("message") != null ? result.get("message").toString() : "下发命令失败";
+                throw new BusinessException(passResult);
+            }
+            passResult = result.get("message") != null ? result.get("message").toString() : "下发命令成功";
+            return result;
+        } finally {
+            saveRecord(device.getId(), tenantId, commandUserName, passType, passResult);
+        }
+    }
 
-        if(categoryType == 3){
-            return remoteTransferService.deviceControl(productCode, gatewayUuid, JSON.toJSONString(map), tenantId, commandUserId, commandUserName);
-        }else{
-            return remoteTransferService.deviceControl(productCode, deviceUuid, JSON.toJSONString(map), tenantId, commandUserId, commandUserName);
+    /**
+     * 插入通行记录,失败不影响主流程
+     */
+    private void saveRecord(Integer deviceId, Integer tenantId, String userName, Integer passType, String passResult) {
+        try {
+            EgRecord record = new EgRecord();
+            record.setEgDeviceId(deviceId);
+            record.setTenantId(tenantId);
+            record.setPassTime(LocalDateTime.now());
+            record.setCreateTime(LocalDateTime.now());
+            record.setPassResult(passResult);
+            record.setUserName(userName);
+            record.setPassType(passType);
+            egRecordMapper.insert(record);
+        } catch (Exception ignored) {}
+    }
+
+    @Override
+    public CommonPage<EgDeviceBindFacePersonVO> pageBindFacePersons(EgDeviceBindFacePersonQueryVO queryVO) {
+        Integer cur = queryVO.getCurrent();
+        Integer sz = queryVO.getSize();
+        IPage<SysPerson> page = new Page<>(cur, sz);
+
+        List<Integer> deviceIds = new ArrayList<>();
+        if (StringUtils.isNotBlank(queryVO.getDeviceCode())) {
+            EgDevice tempDevice = egDeviceMapper.selectOne(Wrappers.<EgDevice>lambdaQuery()
+                    .eq(EgDevice::getDeviceCode, queryVO.getDeviceCode()));
+            if (tempDevice == null) {
+                return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+            }
+            EgDevice device = egDeviceMapper.selectOne(Wrappers.<EgDevice>lambdaQuery()
+                    .eq(EgDevice::getDeviceCode, queryVO.getDeviceCode())
+                    .eq(EgDevice::getTenantId, tempDevice.getTenantId()));
+            if (device == null || device.getId() == null) {
+                return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+            }
+            deviceIds.add(device.getId());
+        } else {
+            List<EgDevice> devices = egDeviceMapper.selectList(Wrappers.<EgDevice>lambdaQuery()
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId()));
+            if (!CollectionUtils.isEmpty(devices)) {
+                for (EgDevice d : devices) {
+                    if (d != null && d.getId() != null) {
+                        deviceIds.add(d.getId());
+                    }
+                }
+            }
+        }
+
+        if (deviceIds.isEmpty()) {
+            return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+        }
+
+        List<EgDevicePersonBind> bindList = egDevicePersonBindMapper.selectByDeviceIds(deviceIds);
+        LinkedHashSet<Integer> personIdSet = new LinkedHashSet<>();
+        if (!CollectionUtils.isEmpty(bindList)) {
+            for (EgDevicePersonBind b : bindList) {
+                if (b.getPersonId() != null) {
+                    personIdSet.add(b.getPersonId());
+                }
+            }
+        }
+        if (personIdSet.isEmpty()) {
+            return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+        }
+        List<Integer> personIds = new ArrayList<>(personIdSet);
+
+        LambdaQueryWrapper<SysPerson> personWrapper = Wrappers.lambdaQuery();
+        personWrapper.in(SysPerson::getId, personIds);
+        if (StringUtils.isNotBlank(queryVO.getFullName())) {
+            personWrapper.like(SysPerson::getFullName, queryVO.getFullName());
+        }
+        Integer faceStatusFilter = queryVO.getFaceStatus();
+        if (faceStatusFilter != null) {
+            personWrapper.eq(SysPerson::getFaceStatus, faceStatusFilter.byteValue());
+        }
+        personWrapper.orderByAsc(SysPerson::getId);
+        page = sysPersonMapper.selectPage(page, personWrapper);
+
+        List<SysPerson> records = page.getRecords();
+        List<EgDeviceBindFacePersonVO> rows = new ArrayList<>(records.size());
+        for (SysPerson p : records) {
+            EgDeviceBindFacePersonVO vo = new EgDeviceBindFacePersonVO();
+            vo.setFullName(p.getFullName());
+            vo.setAge(p.getAge());
+            vo.setGender(p.getGender());
+            vo.setLinkPhone(p.getLinkPhone());
+            vo.setFaceBase(p.getFaceBase());
+            vo.setVefNum(p.getVefNum());
+            vo.setFaceStatus(p.getFaceStatus());
+            vo.setCardNum(p.getCardNum());
+            rows.add(vo);
+        }
+        return new CommonPage<>(rows, page.getTotal(), sz, cur);
+    }
+
+    @Override
+    public void bindPerson(Integer deviceId, String personIds, Integer isLoginNotify) {
+        if (deviceId == null) {
+            throw new BusinessException("设备ID不能为空");
+        }
+
+        // 先校验设备是否存在
+        EgDevice device = this.getById(deviceId);
+        Optional.ofNullable(device).orElseThrow(() -> new BusinessException("门禁设备信息不存在"));
+
+        // 先清空原有绑定关系
+        egDevicePersonBindMapper.deleteByDeviceId(deviceId);
+
+        // 为空则视为解绑所有人员
+        if (!StringUtils.isNotBlank(personIds)) {
+            return;
+        }
+
+        String[] personIdArr = personIds.split(",");
+        int loginNotify = (isLoginNotify == null) ? 0 : isLoginNotify;
+        for (String personIdStr : personIdArr) {
+            if (!StringUtils.isNotBlank(personIdStr)) {
+                continue;
+            }
+            EgDevicePersonBind bind = new EgDevicePersonBind();
+            bind.setDeviceId(deviceId);
+            bind.setPersonId(Integer.parseInt(personIdStr.trim()));
+            bind.setIsLoginNotify(loginNotify);
+            egDevicePersonBindMapper.insert(bind);
         }
     }
 }

+ 17 - 10
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgRecordServiceImpl.java

@@ -47,23 +47,31 @@ public class EgRecordServiceImpl extends AbstractCrudService<EgRecordMapper, EgR
     @Override
     public void add(EgRecord egRecord){
         Integer tenantId;
-        if(StringUtils.isNotBlank(egRecord.getDomain())){
-            tenantId = egDeviceMapper.sysTenantId(egRecord.getDomain());
-        }else{
-            tenantId = SecurityUtils.getTenantId();
-        }
-
+        
+        // 通过deviceUuid查询设备获取tenantId
         LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
         if(StringUtils.isBlank(egRecord.getDeviceUuid())){
             throw new BusinessException("设备Uuid不能为空");
         }
-        queryWrapper.eq(EgDevice::getDeviceUuid,egRecord.getDeviceUuid());
+        queryWrapper.select(EgDevice::getId, EgDevice::getTenantId)
+                .eq(EgDevice::getDeviceUuid,egRecord.getDeviceUuid());
         EgDevice one = egDeviceService.getOne(queryWrapper);
+        if(one == null){
+            throw new BusinessException("设备未注册,请先注册");
+        }
+        tenantId = one.getTenantId();
         egRecord.setEgDeviceId(one.getId());
-
         egRecord.setCreateTime(LocalDateTime.now());
         egRecord.setTenantId(tenantId);
-        egRecord.setDeptId(SecurityUtils.getLoginUser().getSysUser().getDeptId().intValue());
+        
+        // 安全获取部门ID
+        Integer deptId = null;
+        if(SecurityUtils.getLoginUser() != null 
+                && SecurityUtils.getLoginUser().getSysUser() != null 
+                && SecurityUtils.getLoginUser().getSysUser().getDeptId() != null){
+            deptId = SecurityUtils.getLoginUser().getSysUser().getDeptId().intValue();
+        }
+        egRecord.setDeptId(deptId);
 
         this.save(egRecord);
     }
@@ -114,7 +122,6 @@ public class EgRecordServiceImpl extends AbstractCrudService<EgRecordMapper, EgR
             }
         }
 
-
         return new CommonPage<>(page.getRecords(),page.getTotal(),requestVO.getSize(),requestVO.getCurrent());
     }
 }

+ 21 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonQueryVO.java

@@ -0,0 +1,21 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/** 设备绑定人员分页查询参数 */
+@Data
+public class EgDeviceBindFacePersonQueryVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer current;
+    private Integer size;
+    /** 设备编码,空则当前租户全部设备 */
+    private String deviceCode;
+    /** 姓名模糊,可空 */
+    private String fullName;
+    /** 人脸状态 0/1,可空 */
+    private Integer faceStatus;
+}

+ 21 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonVO.java

@@ -0,0 +1,21 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/** 绑定人员列表项 */
+@Data
+public class EgDeviceBindFacePersonVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private String fullName;
+    private Integer age;
+    private Integer gender;
+    private String linkPhone;
+    private String faceBase;
+    private Integer vefNum;
+    private Byte faceStatus;
+    private String cardNum;
+}

+ 9 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceRequestVO.java

@@ -45,4 +45,13 @@ public class EgDeviceRequestVO implements Serializable {
      * 设备uuid
      */
     private String deviceUuid;
+
+    /**
+     * 设备编码
+     */
+    private String deviceCode;
+    /**
+     * 设备状态
+     */
+    private Integer deviceStatus;
 }

+ 20 - 0
service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceHeartbeatMapper.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.eg.mapper.EgDeviceHeartbeatMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.usky.eg.domain.EgDeviceHeartbeat">
+        <id column="id" property="id"/>
+        <result column="device_code" property="deviceCode"/>
+        <result column="ip_addr" property="ipAddr"/>
+        <result column="mac_addr" property="macAddr"/>
+        <result column="device_type" property="deviceType"/>
+        <result column="create_time" property="createTime"/>
+        <result column="model" property="model"/>
+        <result column="manu_facturer"  property="manuFacturer"/>
+        <result column="version" property="version"/>
+        <result column="sdk" property="sdk"/>
+        <result column="device_status" property="deviceStatus"/>
+    </resultMap>
+
+</mapper>

+ 1 - 2
service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceMapper.xml

@@ -11,9 +11,8 @@
         <result column="install_address" property="installAddress" />
         <result column="service_status" property="serviceStatus" />
         <result column="device_ip" property="deviceIp" />
-        <result column="device_port" property="devicePort" />
-        <result column="eg_number" property="egNumber" />
         <result column="bind_face" property="bindFace" />
+        <result column="bind_person" property="bindPerson" />
         <result column="create_by" property="createBy" />
         <result column="update_by" property="updateBy" />
         <result column="create_time" property="createTime" />

+ 62 - 0
service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDevicePersonBindMapper.xml

@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.eg.mapper.EgDevicePersonBindMapper">
+
+    <resultMap id="BaseResultMap" type="com.usky.eg.domain.EgDevicePersonBind">
+        <result column="device_id" property="deviceId" />
+        <result column="person_id" property="personId" />
+        <result column="is_login_notify" property="isLoginNotify" />
+    </resultMap>
+
+    <select id="selectByDeviceIds" resultMap="BaseResultMap">
+        select device_id, person_id, is_login_notify
+        from eg_device_person_bind
+        <where>
+            <if test="deviceIds != null and deviceIds.size() > 0">
+                and device_id in
+                <foreach collection="deviceIds" item="id" open="(" separator="," close=")">
+                    #{id}
+                </foreach>
+            </if>
+        </where>
+    </select>
+
+    <select id="countByDeviceId" resultType="java.lang.Integer">
+        select count(1)
+        from eg_device_person_bind
+        where device_id = #{deviceId}
+    </select>
+
+    <!--
+        通过 sys_user_person 关联 sys_person,最终关联到 eg_device_person_bind,判断某个用户是否对某设备有绑定关系
+        假定字段:sys_user_person(user_id, person_id),sys_person(id),eg_device_person_bind(person_id, device_id)
+    -->
+    <select id="countByUserIdAndDeviceId" resultType="java.lang.Integer">
+        select count(1)
+        from eg_device_person_bind b
+                 join sys_person p on b.person_id = p.id
+                 join sys_user_person up on up.person_id = p.id
+        where up.user_id = #{userId}
+          and b.device_id = #{deviceId}
+    </select>
+
+    <!-- 根据用户ID查询其绑定的设备ID列表 -->
+    <select id="selectDeviceIdsByUserId" resultType="java.lang.Integer">
+        select distinct b.device_id
+        from eg_device_person_bind b
+                 join sys_person p on b.person_id = p.id
+                 join sys_user_person up on up.person_id = p.id
+        where up.user_id = #{userId}
+    </select>
+
+    <delete id="deleteByDeviceId">
+        delete from eg_device_person_bind
+        where device_id = #{deviceId}
+    </delete>
+
+    <insert id="insert" parameterType="com.usky.eg.domain.EgDevicePersonBind">
+        insert into eg_device_person_bind (device_id, person_id, is_login_notify)
+        values (#{deviceId}, #{personId}, #{isLoginNotify})
+    </insert>
+
+</mapper>

+ 32 - 0
service-eg/service-eg-biz/src/main/resources/mapper/eg/SysPersonMapper.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.eg.mapper.SysPersonMapper">
+
+    <resultMap id="BaseResultMap" type="com.usky.eg.domain.SysPerson">
+        <id column="id" property="id"/>
+        <result column="full_name" property="fullName"/>
+        <result column="age" property="age"/>
+        <result column="gender" property="gender"/>
+        <result column="address" property="address"/>
+        <result column="education_degree" property="educationDegree"/>
+        <result column="id_number" property="idNumber"/>
+        <result column="link_phone" property="linkPhone"/>
+        <result column="post_id" property="postId"/>
+        <result column="dept_id" property="deptId"/>
+        <result column="entry_time" property="entryTime"/>
+        <result column="certificate_url1" property="certificateUrl1"/>
+        <result column="certificate_url2" property="certificateUrl2"/>
+        <result column="certificate_url3" property="certificateUrl3"/>
+        <result column="creator" property="creator"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_person" property="updatePerson"/>
+        <result column="update_time" property="updateTime"/>
+        <result column="face_base" property="faceBase"/>
+        <result column="vef_num" property="vefNum"/>
+        <result column="face_name" property="faceName"/>
+        <result column="remark" property="remark"/>
+        <result column="face_status" property="faceStatus"/>
+        <result column="card_num" property="cardNum"/>
+    </resultMap>
+
+</mapper>

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

+ 6 - 0
service-job/pom.xml

@@ -88,6 +88,12 @@
             <version>0.0.1</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-eg-api</artifactId>
+            <version>0.0.1</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
     <build>

+ 9 - 0
service-job/src/main/java/com/ruoyi/job/task/RyTask.java

@@ -3,6 +3,7 @@ package com.ruoyi.job.task;
 import com.usky.cdi.AlarmDataSyncTaskService;
 import com.usky.cdi.RemotecdiTaskService;
 import com.usky.common.core.utils.StringUtils;
+import com.usky.eg.RemoteEgService;
 import com.usky.meeting.RemoteMeetingService;
 import com.usky.ems.RemoteEmsTaskService;
 import com.usky.fire.RemoteFireService;
@@ -39,6 +40,8 @@ public class RyTask {
     @Autowired
     private RemoteEmsTaskService remoteEmsTaskService;
 
+    @Autowired
+    private RemoteEgService remoteEgService;
 
     public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) {
         System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
@@ -121,4 +124,10 @@ public class RyTask {
         remoteEmsTaskService.sendEnergyHeartbeat();
     }
 
+    // 门禁设备心跳状态
+    public void egDeviceStatus() {
+        System.out.println("egDeviceStatus start......");
+        remoteEgService.egDeviceStatus();
+    }
+
 }

+ 0 - 10
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/EgDevice.java

@@ -57,16 +57,6 @@ public class EgDevice implements Serializable {
      */
     private String deviceIp;
 
-    /**
-     * 端口
-     */
-    private Integer devicePort;
-
-    /**
-     * 门禁号
-     */
-    private String egNumber;
-
     /**
      * 创建者
      */

Some files were not shown because too many files changed in this diff