瀏覽代碼

解决冲突

fuyuchuan 1 天之前
父節點
當前提交
d64e8e9161
共有 64 個文件被更改,包括 4443 次插入175 次删除
  1. 65 15
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAnalysisController.java
  2. 0 37
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAuthController.java
  3. 32 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsDeviceEventController.java
  4. 4 4
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsGatewayController.java
  5. 34 25
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsModelController.java
  6. 99 4
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsOverviewController.java
  7. 64 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsProjectController.java
  8. 20 14
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsReportController.java
  9. 31 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsSpaceController.java
  10. 92 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/DmpProduct.java
  11. 47 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDataUploadShanghaiProduct.java
  12. 71 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDeviceEvent.java
  13. 61 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsEnergyConsumptionFormula.java
  14. 47 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProjectConfiguration.java
  15. 59 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProjectConversionFactor.java
  16. 44 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProjectDeviceSystem.java
  17. 82 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/enums/TemporalTypeEnum.java
  18. 55 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/enums/ValueTypeEnum.java
  19. 11 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/DmpProductMapper.java
  20. 12 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDataUploadShanghaiProductMapper.java
  21. 11 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDeviceEventMapper.java
  22. 11 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsEnergyConsumptionFormulaMapper.java
  23. 11 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectConfigurationMapper.java
  24. 11 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectConversionFactorMapper.java
  25. 11 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectDeviceSystemMapper.java
  26. 23 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/DmpProductService.java
  27. 2 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsAnalysisService.java
  28. 0 14
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsAuthService.java
  29. 18 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsDeviceEventService.java
  30. 25 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsDeviceReportService.java
  31. 19 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsEnergyItemCodeService.java
  32. 7 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsModelService.java
  33. 44 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsOverviewService.java
  34. 11 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsProjectConfigurationService.java
  35. 36 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsProjectService.java
  36. 31 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsSpaceService.java
  37. 75 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/DmpProductServiceImpl.java
  38. 1103 16
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAnalysisServiceImpl.java
  39. 0 30
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAuthServiceImpl.java
  40. 174 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsDeviceEventServiceImpl.java
  41. 272 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsDeviceReportServiceImpl.java
  42. 110 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsEnergyItemCodeServiceImpl.java
  43. 70 2
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsModelServiceImpl.java
  44. 647 5
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsOverviewServiceImpl.java
  45. 16 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectConfigurationServiceImpl.java
  46. 63 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectServiceImpl.java
  47. 136 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsSpaceServiceImpl.java
  48. 46 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceEnergyReportItemVO.java
  49. 37 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceEnergyReportRequest.java
  50. 25 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceEventDTO.java
  51. 24 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceEventVO.java
  52. 29 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceFunctionLastValueDTO.java
  53. 27 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceFunctionOneValueDTO.java
  54. 36 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceFunctionValueReportDTO.java
  55. 54 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsAverageResponse.java
  56. 20 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsOverviewDeviceSystemStatVO.java
  57. 20 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsOverviewEnergyItemVO.java
  58. 94 7
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsProjectResponse.java
  59. 44 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsSpaceForestNodeVO.java
  60. 32 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EnergyItemCodeVO.java
  61. 28 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EnergyTypeWrapperProductVO.java
  62. 28 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/ProductFunctionVO.java
  63. 22 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/SimpleProductVO.java
  64. 10 2
      service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasMapServiceImpl.java

+ 65 - 15
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAnalysisController.java

@@ -9,53 +9,103 @@ import org.springframework.web.bind.annotation.*;
 /**
  * 能耗分析接口
  * 趋势、指标、分类占比、区域分析、对比分析
+ * 基础路径前缀:/analysis(网关再加 /prod-api/service-ems)
  */
 @RestController
-@RequestMapping("/prod-api/service-ems")
+@RequestMapping("/analysis")
 public class EmsAnalysisController {
 
     @Autowired
     private EmsAnalysisService emsAnalysisService;
+    
+    /**
+     * 获取当前日期的默认值
+     */
+    private String getCurrentDefaultTimeDimension() {
+        return "D"; // 按日统计
+    }
+    
+    /**
+     * 获取当前日期的默认值(根据时间维度返回对应格式)
+     */
+    private String getCurrentDefaultTimeValue(String timeDimension) {
+        java.time.LocalDate now = java.time.LocalDate.now();
+        int year = now.getYear();
+        int month = now.getMonthValue();
+        int day = now.getDayOfMonth();
+        if ("D".equals(timeDimension)) {
+            // 按日:返回年月日格式,如 2026-03-09
+            return year + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day;
+        } else if ("M".equals(timeDimension)) {
+            // 按月:返回年月格式,如 2026-03
+            return year + "-" + (month < 10 ? "0" : "") + month;
+        } else {
+            // 按年:返回年格式,如 2026
+            return String.valueOf(year);
+        }
+    }
 
-    @GetMapping("/analysis/trend")
+    @GetMapping("/trend")
     public ApiResult<EmsTrendResponse> getTrend(
             @RequestParam(required = false) Long projectId,
-            @RequestParam String timeDimension,
-            @RequestParam String timeValue,
+            @RequestParam(required = false) String timeDimension,
+            @RequestParam(required = false) String timeValue,
             @RequestParam(required = false) Long energyTypeId) {
+        if (timeDimension == null) timeDimension = getCurrentDefaultTimeDimension();
+        if (timeValue == null) timeValue = getCurrentDefaultTimeValue(timeDimension);
         return ApiResult.success(emsAnalysisService.getTrend(projectId, timeDimension, timeValue, energyTypeId));
     }
 
-    @GetMapping("/analysis/trend/indicators")
+    @GetMapping("/trend/indicators")
     public ApiResult<EmsTrendIndicatorsResponse> getTrendIndicators(
             @RequestParam(required = false) Long projectId,
-            @RequestParam String timeDimension,
-            @RequestParam String timeValue,
+            @RequestParam(required = false) String timeDimension,
+            @RequestParam(required = false) String timeValue,
             @RequestParam(required = false) Long energyTypeId) {
+        if (timeDimension == null) timeDimension = getCurrentDefaultTimeDimension();
+        if (timeValue == null) timeValue = getCurrentDefaultTimeValue(timeDimension);
         return ApiResult.success(emsAnalysisService.getTrendIndicators(projectId, timeDimension, timeValue, energyTypeId));
     }
 
-    @GetMapping("/analysis/trend/category")
+    @GetMapping("/trend/category")
     public ApiResult<EmsTrendCategoryResponse> getTrendCategory(
             @RequestParam(required = false) Long projectId,
-            @RequestParam String timeDimension,
-            @RequestParam String timeValue,
+            @RequestParam(required = false) String timeDimension,
+            @RequestParam(required = false) String timeValue,
             @RequestParam(required = false) Long energyTypeId) {
+        if (timeDimension == null) timeDimension = getCurrentDefaultTimeDimension();
+        if (timeValue == null) timeValue = getCurrentDefaultTimeValue(timeDimension);
         return ApiResult.success(emsAnalysisService.getTrendCategory(projectId, timeDimension, timeValue, energyTypeId));
     }
 
-    @GetMapping("/analysis/region")
+    @GetMapping("/region")
     public ApiResult<EmsRegionAnalysisResponse> getRegionAnalysis(
             @RequestParam(required = false) Long projectId,
             @RequestParam(required = false) String regionIds,
-            @RequestParam String timeDimension,
-            @RequestParam String timeValue,
+            @RequestParam(required = false) String timeDimension,
+            @RequestParam(required = false) String timeValue,
             @RequestParam(required = false) Long energyTypeId) {
+        if (timeDimension == null) timeDimension = getCurrentDefaultTimeDimension();
+        if (timeValue == null) timeValue = getCurrentDefaultTimeValue(timeDimension);
         return ApiResult.success(emsAnalysisService.getRegionAnalysis(projectId, regionIds, timeDimension, timeValue, energyTypeId));
     }
 
-    @PostMapping("/analysis/compare")
+    @PostMapping("/compare")
     public ApiResult<EmsCompareResponse> getCompare(@RequestBody EmsCompareRequest request) {
         return ApiResult.success(emsAnalysisService.getCompare(request));
     }
-}
+
+    @GetMapping("/average")
+    public ApiResult<EmsAverageResponse> getAverage(
+            @RequestParam(required = false) Long projectId,
+            @RequestParam(required = false) String timeDimension,
+            @RequestParam(required = false) String timeValue,
+            @RequestParam(required = false) Long energyTypeId) {
+        // 设置默认值:如果没有提供参数,使用默认的项目和能源类型
+        if (projectId == null) projectId = 1L;  // 默认项目ID
+        if (energyTypeId == null) energyTypeId = 1L;  // 默认电能源类型
+        if (timeDimension == null) timeDimension = getCurrentDefaultTimeDimension();
+        if (timeValue == null) timeValue = getCurrentDefaultTimeValue(timeDimension);
+        return ApiResult.success(emsAnalysisService.getAverage(projectId, timeDimension, timeValue, energyTypeId));
+    }
+}

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

@@ -1,37 +0,0 @@
-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();
-    }
-}

+ 32 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsDeviceEventController.java

@@ -0,0 +1,32 @@
+package com.usky.ems.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.ems.service.EmsDeviceEventService;
+import com.usky.ems.service.vo.DeviceEventDTO;
+import com.usky.ems.service.vo.DeviceEventVO;
+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;
+
+import java.util.List;
+
+/**
+ * 设备事件接口
+ *
+ * POST /device-event/list  查询设备事件列表
+ */
+@RestController
+@RequestMapping("/device-event")
+public class EmsDeviceEventController {
+
+    @Autowired
+    private EmsDeviceEventService emsDeviceEventService;
+
+    @PostMapping("/list")
+    public ApiResult<List<DeviceEventVO>> list(@RequestBody DeviceEventDTO dto) {
+        return ApiResult.success(emsDeviceEventService.list(dto));
+    }
+}
+

+ 4 - 4
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsGatewayController.java

@@ -11,10 +11,10 @@ import org.springframework.web.bind.annotation.*;
 
 /**
  * 能源能耗 - 网关/设备查询模块 API
- * 基础路径:/prod-api/service-ems
+ * 基础路径前缀:/device/gateway(网关再加 /prod-api/service-ems
  */
 @RestController
-@RequestMapping("/prod-api/service-ems")
+@RequestMapping("/device/gateway")
 public class EmsGatewayController {
 
     @Autowired
@@ -23,7 +23,7 @@ public class EmsGatewayController {
     /**
      * 网关列表(分页)
      */
-    @GetMapping("/device/gateway/list")
+    @GetMapping("/list")
     public ApiResult<CommonPage<EmsGatewayListItem>> gatewayList(EmsGatewayPageRequest request) {
         return ApiResult.success(emsGatewayQueryService.listGateways(request));
     }
@@ -31,7 +31,7 @@ public class EmsGatewayController {
     /**
      * 网关详情
      */
-    @GetMapping("/device/gateway/{id}")
+    @GetMapping("/{id}")
     public ApiResult<EmsGatewayDetailResponse> getGateway(@PathVariable String id) {
         return ApiResult.success(emsGatewayQueryService.getGatewayById(id));
     }

+ 34 - 25
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsModelController.java

@@ -10,10 +10,10 @@ import java.util.List;
 
 /**
  * 能源能耗 - 模型/结构模块 API(项目层级树、能源类型、建筑/区域/楼层/网关/通道/设备/属性点位 CRUD)
- * 基础路径:/prod-api/service-ems
+ * 基础路径前缀/model(网关再加 /prod-api/service-ems
  */
 @RestController
-@RequestMapping("/prod-api/service-ems")
+@RequestMapping("/model")
 public class EmsModelController {
 
     @Autowired
@@ -22,7 +22,7 @@ public class EmsModelController {
     /**
      * 获取项目层级树(建筑、区域、楼层、网关)
      */
-    @GetMapping("/model/structure/tree")
+    @GetMapping("/structure/tree")
     public ApiResult<EmsStructureTreeNode> getStructureTree(
             @RequestParam(required = false) Long projectId,
             @RequestParam(required = false, defaultValue = "true") Boolean includeGateway) {
@@ -32,132 +32,141 @@ public class EmsModelController {
     /**
      * 能源类型列表(电、水、气)
      */
-    @GetMapping("/model/energy-type/list")
+    @GetMapping("/energy-type/list")
     public ApiResult<List<EmsEnergyTypeVO>> getEnergyTypeList() {
         return ApiResult.success(emsModelService.getEnergyTypeList());
     }
 
-    @PostMapping("/model/building")
+    /**
+     * 展示已关联的能源类型及其下属分项/产品列表
+     * 数据来源:leo.ems_energy_item_code.energy_type 分组
+     */
+    @GetMapping("/energy-type/associated")
+    public ApiResult<List<EnergyTypeWrapperProductVO>> showAssociatedEnergyTypes() {
+        return ApiResult.success(emsModelService.showAssociatedEnergyTypes());
+    }
+
+    @PostMapping("/building")
     public ApiResult<EmsIdResponse> createBuilding(@RequestBody EmsModelSaveRequest request) {
         Long id = emsModelService.createBuilding(request);
         return ApiResult.success(new EmsIdResponse(id));
     }
 
-    @PutMapping("/model/building/{id}")
+    @PutMapping("/building/{id}")
     public ApiResult<Void> updateBuilding(@PathVariable Long id, @RequestBody EmsModelSaveRequest request) {
         emsModelService.updateBuilding(id, request);
         return ApiResult.success();
     }
 
-    @DeleteMapping("/model/building/{id}")
+    @DeleteMapping("/building/{id}")
     public ApiResult<Void> deleteBuilding(@PathVariable Long id) {
         emsModelService.deleteBuilding(id);
         return ApiResult.success();
     }
 
-    @PostMapping("/model/region")
+    @PostMapping("/region")
     public ApiResult<EmsIdResponse> createRegion(@RequestBody EmsModelSaveRequest request) {
         Long id = emsModelService.createRegion(request);
         return ApiResult.success(new EmsIdResponse(id));
     }
 
-    @PutMapping("/model/region/{id}")
+    @PutMapping("/region/{id}")
     public ApiResult<Void> updateRegion(@PathVariable Long id, @RequestBody EmsModelSaveRequest request) {
         emsModelService.updateRegion(id, request);
         return ApiResult.success();
     }
 
-    @DeleteMapping("/model/region/{id}")
+    @DeleteMapping("/region/{id}")
     public ApiResult<Void> deleteRegion(@PathVariable Long id) {
         emsModelService.deleteRegion(id);
         return ApiResult.success();
     }
 
-    @PostMapping("/model/floor")
+    @PostMapping("/floor")
     public ApiResult<EmsIdResponse> createFloor(@RequestBody EmsModelSaveRequest request) {
         Long id = emsModelService.createFloor(request);
         return ApiResult.success(new EmsIdResponse(id));
     }
 
-    @PutMapping("/model/floor/{id}")
+    @PutMapping("/floor/{id}")
     public ApiResult<Void> updateFloor(@PathVariable Long id, @RequestBody EmsModelSaveRequest request) {
         emsModelService.updateFloor(id, request);
         return ApiResult.success();
     }
 
-    @DeleteMapping("/model/floor/{id}")
+    @DeleteMapping("/floor/{id}")
     public ApiResult<Void> deleteFloor(@PathVariable Long id) {
         emsModelService.deleteFloor(id);
         return ApiResult.success();
     }
 
-    @PostMapping("/model/gateway")
+    @PostMapping("/gateway")
     public ApiResult<EmsIdResponse> createGateway(@RequestBody EmsModelSaveRequest request) {
         String id = emsModelService.createGateway(request);
         return ApiResult.success(new EmsIdResponse(id));
     }
 
-    @PutMapping("/model/gateway/{id}")
+    @PutMapping("/gateway/{id}")
     public ApiResult<Void> updateGateway(@PathVariable String id, @RequestBody EmsModelSaveRequest request) {
         emsModelService.updateGateway(id, request);
         return ApiResult.success();
     }
 
-    @DeleteMapping("/model/gateway/{id}")
+    @DeleteMapping("/gateway/{id}")
     public ApiResult<Void> deleteGateway(@PathVariable String id) {
         emsModelService.deleteGateway(id);
         return ApiResult.success();
     }
 
-    @PostMapping("/model/channel")
+    @PostMapping("/channel")
     public ApiResult<EmsIdResponse> createChannel(@RequestBody EmsModelSaveRequest request) {
         Long id = emsModelService.createChannel(request);
         return ApiResult.success(new EmsIdResponse(id));
     }
 
-    @PutMapping("/model/channel/{id}")
+    @PutMapping("/channel/{id}")
     public ApiResult<Void> updateChannel(@PathVariable Long id, @RequestBody EmsModelSaveRequest request) {
         emsModelService.updateChannel(id, request);
         return ApiResult.success();
     }
 
-    @DeleteMapping("/model/channel/{id}")
+    @DeleteMapping("/channel/{id}")
     public ApiResult<Void> deleteChannel(@PathVariable Long id) {
         emsModelService.deleteChannel(id);
         return ApiResult.success();
     }
 
-    @PostMapping("/model/device")
+    @PostMapping("/device")
     public ApiResult<EmsIdResponse> createDevice(@RequestBody EmsModelSaveRequest request) {
         String id = emsModelService.createDevice(request);
         return ApiResult.success(new EmsIdResponse(id));
     }
 
-    @PutMapping("/model/device/{id}")
+    @PutMapping("/device/{id}")
     public ApiResult<Void> updateDevice(@PathVariable String id, @RequestBody EmsModelSaveRequest request) {
         emsModelService.updateDevice(id, request);
         return ApiResult.success();
     }
 
-    @DeleteMapping("/model/device/{id}")
+    @DeleteMapping("/device/{id}")
     public ApiResult<Void> deleteDevice(@PathVariable String id) {
         emsModelService.deleteDevice(id);
         return ApiResult.success();
     }
 
-    @PostMapping("/model/attribute-point")
+    @PostMapping("/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}")
+    @PutMapping("/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}")
+    @DeleteMapping("/attribute-point/{id}")
     public ApiResult<Void> deleteAttributePoint(@PathVariable Long id) {
         emsModelService.deleteAttributePoint(id);
         return ApiResult.success();

+ 99 - 4
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsOverviewController.java

@@ -5,15 +5,17 @@ import com.usky.ems.service.EmsOverviewService;
 import com.usky.ems.service.vo.EmsProjectResponse;
 import com.usky.ems.service.vo.EmsSummaryRequest;
 import com.usky.ems.service.vo.EmsSummaryResponse;
+import com.usky.ems.service.vo.EmsOverviewDeviceSystemStatVO;
+import com.usky.ems.service.vo.EmsOverviewEnergyItemVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 /**
  * 能源能耗 - 概览模块 API
- * 基础路径:/prod-api/service-ems
+ * 基础路径前缀/overview(网关再加 /prod-api/service-ems
  */
 @RestController
-@RequestMapping("/prod-api/service-ems")
+@RequestMapping("/overview")
 public class EmsOverviewController {
 
     @Autowired
@@ -22,7 +24,7 @@ public class EmsOverviewController {
     /**
      * 获取项目信息
      */
-    @GetMapping("/overview/project")
+    @GetMapping("/project")
     public ApiResult<EmsProjectResponse> getProject(@RequestParam(required = false) Long projectId) {
         return ApiResult.success(emsOverviewService.getProject(projectId));
     }
@@ -30,9 +32,102 @@ public class EmsOverviewController {
     /**
      * 获取项目数据概括(时间维度联动)
      */
-    @GetMapping("/overview/summary")
+    @GetMapping("/summary")
     public ApiResult<EmsSummaryResponse> getSummary(EmsSummaryRequest request) {
         return ApiResult.success(emsOverviewService.getSummary(
                 request.getProjectId(), request.getTimeDimension(), request.getTimeValue()));
     }
+
+    /**
+     * 获取概览页能源类型条目(如电/水/气)
+     */
+    @GetMapping("/item")
+    public ApiResult<java.util.List<EmsOverviewEnergyItemVO>> queryOverviewItem(
+            @RequestParam(required = false) Long projectId) {
+        return ApiResult.success(emsOverviewService.queryOverviewItem(projectId));
+    }
+
+    /**
+     * 获取概览页设备系统统计(每个系统下设备数量)
+     */
+    @GetMapping("/device-info")
+    public ApiResult<java.util.List<EmsOverviewDeviceSystemStatVO>> queryOverviewDeviceInfo(
+            @RequestParam(required = false) Long projectId) {
+        return ApiResult.success(emsOverviewService.queryOverviewDeviceInfo(projectId));
+    }
+
+    /**
+     * 分类能耗统计(模拟数据)
+     * 参数说明:
+     * - dateType:时间类型(1-日,2-月,3-年等,暂仅作为占位,不影响当前模拟结果)
+     * - itemCode:能耗条目编码
+     * - energyType:能耗类型
+     * - projectId:项目 ID(可选,当前实现未做真实查询,仅占位)
+     */
+    @GetMapping("/classification-energy")
+    public ApiResult<java.util.Map<String, Object>> queryClassificationEnergy(
+            @RequestParam Integer dateType,
+            @RequestParam String itemCode,
+            @RequestParam Integer energyType,
+            @RequestParam(required = false) Long projectId) {
+        return ApiResult.success(emsOverviewService.queryClassificationEnergy(dateType, itemCode, energyType, projectId));
+    }
+
+    /**
+     * 能耗用能趋势(模拟数据)
+     * 参数说明:
+     * - dateType:时间类型(1-日,2-月,3-年)
+     * - itemCode:能耗条目编码
+     * - spaceId:空间ID(可选,当前模拟实现未使用)
+     */
+    @GetMapping("/energy-trend")
+    public ApiResult<java.util.List<java.util.Map<String, Object>>> queryEnergyTrend(
+            @RequestParam Integer dateType,
+            @RequestParam String itemCode,
+            @RequestParam(required = false) Long spaceId) {
+        return ApiResult.success(emsOverviewService.queryEnergyTrend(dateType, itemCode, spaceId));
+    }
+
+    /**
+     * 建筑能耗分析(模拟数据)
+     * 参数说明:
+     * - dateType:时间类型(1-日,2-月,3-年)
+     * - itemCode:能耗条目编码
+     * - spaceId:空间ID(可选,当前模拟实现未使用)
+     */
+    @GetMapping("/building-ranking")
+    public ApiResult<java.util.List<java.util.Map<String, Object>>> queryBuildingRanking(
+            @RequestParam Integer dateType,
+            @RequestParam String itemCode,
+            @RequestParam(required = false) Long spaceId) {
+        return ApiResult.success(emsOverviewService.queryBuildingRanking(dateType, itemCode, spaceId));
+    }
+
+    /**
+     * 单位综合能耗、综合累计能耗、分类能耗占比(总览顶部)
+     * 参数说明:
+     * - dateType:时间类型(1-日,2-月,3-年)
+     * - projectId:项目ID(可选,不传则取第一个项目)
+     */
+    @GetMapping("/top")
+    public ApiResult<java.util.Map<String, Object>> queryOverviewTop(
+            @RequestParam Integer dateType,
+            @RequestParam(required = false) Long projectId) {
+        return ApiResult.success(emsOverviewService.queryOverviewTop(dateType, projectId));
+    }
+
+    /**
+     * 分项能耗占比
+     * 参数说明:
+     * - dateType:时间类型(1-日,2-月,3-年)
+     * - itemCode:分项编码(作为父项,展开下级分项)
+     * - projectId:项目ID(可选,不传则取第一个项目)
+     */
+    @GetMapping("/item-ratio")
+    public ApiResult<java.util.List<java.util.Map<String, Object>>> queryItemRatio(
+            @RequestParam Integer dateType,
+            @RequestParam String itemCode,
+            @RequestParam(required = false) Long projectId) {
+        return ApiResult.success(emsOverviewService.queryItemRatio(dateType, itemCode, projectId));
+    }
 }

+ 64 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsProjectController.java

@@ -0,0 +1,64 @@
+package com.usky.ems.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.ems.domain.EmsProject;
+import com.usky.ems.service.EmsProjectService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 项目管理接口(ems_project)
+ *
+ * POST   /project       新增项目
+ * PUT    /project/{id}  修改项目
+ * DELETE /project/{id}  删除项目
+ * GET    /project/config/status  获取项目配置开关状态
+ */
+@RestController
+@RequestMapping("/project")
+public class EmsProjectController {
+
+    @Autowired
+    private EmsProjectService emsProjectService;
+
+    /**
+     * 新增项目
+     */
+    @PostMapping
+    public ApiResult<Long> create(@RequestBody EmsProject project) {
+        Long id = emsProjectService.create(project);
+        return ApiResult.success(id);
+    }
+
+    /**
+     * 修改项目
+     */
+    @PutMapping("/{id}")
+    public ApiResult<Void> update(@PathVariable Long id, @RequestBody EmsProject project) {
+        project.setId(id);
+        emsProjectService.update(project);
+        return ApiResult.success();
+    }
+
+    /**
+     * 删除项目
+     */
+    @DeleteMapping("/{id}")
+    public ApiResult<Void> delete(@PathVariable Long id) {
+        emsProjectService.delete(id);
+        return ApiResult.success();
+    }
+
+    /**
+     * 获取项目配置开关状态
+     *
+     * @param projectId 项目ID
+     * @param name      配置名称
+     */
+    @GetMapping("/config/status")
+    public ApiResult<Boolean> getConfigurationsStatus(@RequestParam Long projectId,
+                                                      @RequestParam String name) {
+        return ApiResult.success(emsProjectService.getConfigurationsStatus(projectId, name));
+    }
+}
+

+ 20 - 14
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsReportController.java

@@ -2,6 +2,7 @@ package com.usky.ems.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.ems.service.EmsReportService;
+import com.usky.ems.service.EmsModelService;
 import com.usky.ems.service.vo.*;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -12,29 +13,34 @@ import java.util.List;
 /**
  * 统计报表接口
  * 能源报表、区域报表、采集报表
+ * 基础路径前缀:/report(网关再加 /prod-api/service-ems)
  */
 @RestController
-@RequestMapping("/prod-api/service-ems")
+@RequestMapping("/report")
 public class EmsReportController {
 
     @Autowired
     private EmsReportService emsReportService;
 
+    @Autowired
+    private EmsModelService emsModelService;
+
     // ---------- 能源报表 ----------
-    @GetMapping("/report/energy/devices")
-    public ApiResult<EmsReportDevicesResponse> getEnergyDevices(
-            @RequestParam Long energyTypeId,
+    @GetMapping("/energy/devices")
+    public ApiResult<List<EnergyTypeWrapperProductVO>> getEnergyDevices(
+            @RequestParam(required = false) Long energyTypeId,
             @RequestParam(required = false) String keyword,
             @RequestParam(required = false) Long projectId) {
-        return ApiResult.success(emsReportService.getEnergyDevices(energyTypeId, keyword, projectId));
+        // TODO: 如需按 energyTypeId/keyword/projectId 过滤,可在 EmsModelService 中扩展方法
+        return ApiResult.success(emsModelService.showAssociatedEnergyTypes());
     }
 
-    @PostMapping("/report/energy/statistics")
+    @PostMapping("/energy/statistics")
     public ApiResult<EmsEnergyStatisticsResponse> getEnergyStatistics(@RequestBody EmsEnergyStatisticsRequest request) {
         return ApiResult.success(emsReportService.getEnergyStatistics(request));
     }
 
-    @GetMapping("/report/energy/export")
+    @GetMapping("/energy/export")
     public void exportEnergy(
             @RequestParam String deviceIds,
             @RequestParam(required = false) String attributePointIds,
@@ -46,7 +52,7 @@ public class EmsReportController {
     }
 
     // ---------- 区域报表 ----------
-    @GetMapping("/report/region/devices")
+    @GetMapping("/region/devices")
     public ApiResult<EmsReportDevicesResponse> getRegionDevices(
             @RequestParam Long energyTypeId,
             @RequestParam(required = false) Long regionId,
@@ -55,14 +61,14 @@ public class EmsReportController {
         return ApiResult.success(emsReportService.getRegionDevices(energyTypeId, regionId, keyword, projectId));
     }
 
-    @PostMapping("/report/region/statistics")
+    @PostMapping("/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")
+    @GetMapping("/region/export")
     public void exportRegion(
             @RequestParam String deviceIds,
             @RequestParam(required = false) String attributePointIds,
@@ -75,7 +81,7 @@ public class EmsReportController {
     }
 
     // ---------- 采集报表 ----------
-    @GetMapping("/report/collection/devices")
+    @GetMapping("/collection/devices")
     public ApiResult<EmsReportDevicesResponse> getCollectionDevices(
             @RequestParam Long energyTypeId,
             @RequestParam(required = false) String keyword,
@@ -83,17 +89,17 @@ public class EmsReportController {
         return ApiResult.success(emsReportService.getCollectionDevices(energyTypeId, keyword, projectId));
     }
 
-    @PostMapping("/report/collection/realtime")
+    @PostMapping("/collection/realtime")
     public ApiResult<EmsCollectionRealtimeResponse> getCollectionRealtime(@RequestBody EmsCollectionRealtimeRequest request) {
         return ApiResult.success(emsReportService.getCollectionRealtime(request));
     }
 
-    @PostMapping("/report/collection/statistics")
+    @PostMapping("/collection/statistics")
     public ApiResult<EmsEnergyStatisticsResponse> getCollectionStatistics(@RequestBody EmsEnergyStatisticsRequest request) {
         return ApiResult.success(emsReportService.getCollectionStatistics(request));
     }
 
-    @GetMapping("/report/collection/export")
+    @GetMapping("/collection/export")
     public void exportCollection(
             @RequestParam String deviceIds,
             @RequestParam(required = false) String attributePointIds,

+ 31 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsSpaceController.java

@@ -0,0 +1,31 @@
+package com.usky.ems.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.ems.service.EmsSpaceService;
+import com.usky.ems.service.vo.EmsSpaceForestNodeVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 空间树接口
+ *
+ * GET /space/tree  获取空间树
+ */
+@RestController
+@RequestMapping("/space")
+public class EmsSpaceController {
+
+    @Autowired
+    private EmsSpaceService emsSpaceService;
+
+    /**
+     * 获取空间树(根据 leo.ems_space 构建)
+     */
+    @GetMapping("/tree")
+    public ApiResult<EmsSpaceForestNodeVO> tree() {
+        return ApiResult.success(emsSpaceService.tree());
+    }
+}
+

+ 92 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/DmpProduct.java

@@ -0,0 +1,92 @@
+package com.usky.ems.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;
+
+/**
+ * 产品信息表(dmp_product)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("dmp_product")
+public class DmpProduct implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键id */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /** 产品名称 */
+    private String productName;
+
+    /** 接入方式(1 设备直连;2 网关接入) */
+    private Integer accessMode;
+
+    /** 网络类型(1 WIFI;2 移动蜂窝数据;3 NB-IoT;4 以太网) */
+    private Integer networkType;
+
+    /** 设备类型(501 监控系统;502 门禁系统;503 梯控系统;504 机房系统;509 环境系统;510 照明系统) */
+    private Integer deviceType;
+
+    /** 通信协议(1 MQTT;2 TCP 设备直连;3 HTTP) */
+    private Integer comProtocol;
+
+    /** 认证方式 */
+    private String authMode;
+
+    /** 设备型号 */
+    private String deviceModel;
+
+    /** 产品描述 */
+    private String productDescribe;
+
+    /** 厂家名称 */
+    private String factoryName;
+
+    /** 厂家联系人 */
+    private String factoryPerson;
+
+    /** 厂家联系电话 */
+    private String factoryPhone;
+
+    /** 资质证书1 */
+    private String certificateUrl1;
+
+    /** 资质证书2 */
+    private String certificateUrl2;
+
+    /** 资质证书3 */
+    private String certificateUrl3;
+
+    /** 协议文档 */
+    private String agreementUrl;
+
+    /** 删除标识 */
+    private Integer deleteFlag;
+
+    /** 创建人 */
+    private String createdBy;
+
+    /** 创建时间 */
+    private LocalDateTime createdTime;
+
+    /** 更新人 */
+    private String updatedBy;
+
+    /** 更新时间 */
+    private LocalDateTime updatedTime;
+
+    /** 租户号 */
+    private Integer tenantId;
+
+    /** 产品编码 */
+    private String productCode;
+}
+

+ 47 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDataUploadShanghaiProduct.java

@@ -0,0 +1,47 @@
+package com.usky.ems.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;
+
+/**
+ * 上海市区平台数据上传(产品功能标识对应的区平台功能标识)
+ * 表:ems_data_upload_shanghai_product
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_data_upload_shanghai_product")
+public class EmsDataUploadShanghaiProduct implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /** 产品ID */
+    private Long productId;
+
+    /** 功能标识 */
+    private String identifier;
+
+    /** 自定义标识符 */
+    private String customIdentifier;
+
+    /** 更新人 */
+    private Long updatedBy;
+
+    /** 记录更新时间 */
+    private LocalDateTime updateTime;
+
+    /** 创建人 */
+    private Long createdBy;
+
+    /** 记录创建时间 */
+    private LocalDateTime createTime;
+}
+

+ 71 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDeviceEvent.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.time.LocalDateTime;
+
+/**
+ * 设备事件记录(leo.ems_device_event)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_device_event")
+public class EmsDeviceEvent implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("project_id")
+    private Long projectId;
+
+    private String sn;
+
+    @TableField("device_id")
+    private String deviceId;
+
+    private String identifier;
+
+    /** 设备类型 1:设备 2:网关 */
+    @TableField("device_type")
+    private Integer deviceType;
+
+    /** 事件类型 1:通讯告警 2:功能告警 3:数据异常 */
+    @TableField("event_type")
+    private Integer eventType;
+
+    @TableField("suppressed_to")
+    private LocalDateTime suppressedTo;
+
+    /** 确认类型 0:未确认 1:自动确认 2:人工确认 */
+    @TableField("confirmation_type")
+    private Integer confirmationType;
+
+    @TableField("task_sn")
+    private String taskSn;
+
+    private String content;
+
+    @TableField("start_time")
+    private LocalDateTime startTime;
+
+    @TableField("updated_by")
+    private Long updatedBy;
+
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+
+    @TableField("created_by")
+    private Long createdBy;
+
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}
+

+ 61 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsEnergyConsumptionFormula.java

@@ -0,0 +1,61 @@
+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;
+
+/**
+ * 空间能耗计算公式(ems_energy_consumption_formula)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_energy_consumption_formula")
+public class EmsEnergyConsumptionFormula implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("space_id")
+    private Long spaceId;
+
+    @TableField("item_code")
+    private String itemCode;
+
+    private String name;
+
+    private String formula;
+
+    @TableField("compute_start_time")
+    private LocalDateTime computeStartTime;
+
+    @TableField("compute_end_time")
+    private LocalDateTime computeEndTime;
+
+    @TableField("data_time")
+    private LocalDateTime dataTime;
+
+    private Integer status;
+
+    private String cause;
+
+    @TableField("updated_by")
+    private Long updatedBy;
+
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+
+    @TableField("created_by")
+    private Long createdBy;
+
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}
+

+ 47 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProjectConfiguration.java

@@ -0,0 +1,47 @@
+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_project_configuration)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_project_configuration")
+public class EmsProjectConfiguration implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("project_id")
+    private Long projectId;
+
+    private String name;
+
+    private String value;
+
+    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;
+}
+

+ 59 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProjectConversionFactor.java

@@ -0,0 +1,59 @@
+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_conversion_factor)
+ * 用于将不同能源类型折算为标准煤等统一口径。
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_project_conversion_factor")
+public class EmsProjectConversionFactor implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("project_id")
+    private Long projectId;
+
+    /**
+     * 能源类型编码,对应能源类型 ID(如 1=电,2=水,3=气)
+     */
+    @TableField("energy_type")
+    private Integer energyType;
+
+    /**
+     * 系数名称,例如 "coal"、"price" 等
+     */
+    private String name;
+
+    /**
+     * 系数数值
+     */
+    private BigDecimal value;
+
+    @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/EmsProjectDeviceSystem.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_project_device_system)
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("ems_project_device_system")
+public class EmsProjectDeviceSystem implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @TableField("project_id")
+    private Long projectId;
+
+    @TableField("device_system")
+    private Integer deviceSystem;
+
+    @TableField("updated_by")
+    private Long updatedBy;
+
+    @TableField("update_time")
+    private LocalDateTime updateTime;
+
+    @TableField("created_by")
+    private Long createdBy;
+
+    @TableField("create_time")
+    private LocalDateTime createTime;
+}
+

+ 82 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/enums/TemporalTypeEnum.java

@@ -0,0 +1,82 @@
+package com.usky.ems.enums;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+/**
+ * 时间维度枚举:D-日 / M-月 / Y-年
+ */
+public enum TemporalTypeEnum {
+
+    DAY("D"),
+    MONTH("M"),
+    YEAR("Y");
+
+    private final String code;
+
+    TemporalTypeEnum(String code) {
+        this.code = code;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public static TemporalTypeEnum get(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (TemporalTypeEnum e : values()) {
+            if (e.code.equalsIgnoreCase(code)) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 将开始时间归一化到当前时间维度的起点
+     */
+    public LocalDateTime normalizeStart(LocalDateTime time) {
+        if (time == null) {
+            return null;
+        }
+        LocalDate date = time.toLocalDate();
+        switch (this) {
+            case DAY:
+                return LocalDateTime.of(date, LocalTime.MIN);
+            case MONTH:
+                LocalDate firstDayOfMonth = date.withDayOfMonth(1);
+                return LocalDateTime.of(firstDayOfMonth, LocalTime.MIN);
+            case YEAR:
+                LocalDate firstDayOfYear = date.withDayOfYear(1);
+                return LocalDateTime.of(firstDayOfYear, LocalTime.MIN);
+            default:
+                return time;
+        }
+    }
+
+    /**
+     * 将结束时间归一化到当前时间维度的结束
+     */
+    public LocalDateTime normalizeEnd(LocalDateTime time) {
+        if (time == null) {
+            return null;
+        }
+        LocalDate date = time.toLocalDate();
+        switch (this) {
+            case DAY:
+                return LocalDateTime.of(date, LocalTime.MAX);
+            case MONTH:
+                LocalDate lastDayOfMonth = date.withDayOfMonth(date.lengthOfMonth());
+                return LocalDateTime.of(lastDayOfMonth, LocalTime.MAX);
+            case YEAR:
+                LocalDate lastDayOfYear = date.withDayOfYear(date.lengthOfYear());
+                return LocalDateTime.of(lastDayOfYear, LocalTime.MAX);
+            default:
+                return time;
+        }
+    }
+}
+

+ 55 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/enums/ValueTypeEnum.java

@@ -0,0 +1,55 @@
+package com.usky.ems.enums;
+
+import java.util.Objects;
+
+/**
+ * 数值类型枚举,对应 leo.sql 中 ems_device_function_formula.value_type 字段:
+ * 0: 其他  1: 累计值  2: 波动值
+ */
+public enum ValueTypeEnum {
+
+    OTHER("其他", 0),
+    CUMULATIVE("累计值", 1),
+    FLUCTUATION("波动值", 2);
+
+    private final String name;
+    private final Short value;
+
+    ValueTypeEnum(String name, Integer value) {
+        this.name = name;
+        this.value = value.shortValue();
+    }
+
+    /**
+     * 根据数值获取名称
+     */
+    public static String getName(Short type) {
+        for (ValueTypeEnum e : values()) {
+            if (Objects.equals(e.getValue(), type)) {
+                return e.getName();
+            }
+        }
+        return "";
+    }
+
+    /**
+     * 根据数值获取枚举实例;如果不存在则抛出异常
+     */
+    public static ValueTypeEnum getInstance(Short type) {
+        for (ValueTypeEnum e : values()) {
+            if (Objects.equals(e.getValue(), type)) {
+                return e;
+            }
+        }
+        throw new IllegalArgumentException("错误的值类型");
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Short getValue() {
+        return value;
+    }
+}
+

+ 11 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/DmpProductMapper.java

@@ -0,0 +1,11 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.DmpProduct;
+
+/**
+ * 产品信息表 Mapper(dmp_product)
+ */
+public interface DmpProductMapper extends CrudMapper<DmpProduct> {
+}
+

+ 12 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDataUploadShanghaiProductMapper.java

@@ -0,0 +1,12 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsDataUploadShanghaiProduct;
+
+/**
+ * 上海市区平台数据上传(产品功能标识对应的区平台功能标识)Mapper
+ * 表:ems_data_upload_shanghai_product
+ */
+public interface EmsDataUploadShanghaiProductMapper extends CrudMapper<EmsDataUploadShanghaiProduct> {
+}
+

+ 11 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsDeviceEventMapper.java

@@ -0,0 +1,11 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsDeviceEvent;
+
+/**
+ * 设备事件记录 Mapper(leo.ems_device_event)
+ */
+public interface EmsDeviceEventMapper extends CrudMapper<EmsDeviceEvent> {
+}
+

+ 11 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsEnergyConsumptionFormulaMapper.java

@@ -0,0 +1,11 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsEnergyConsumptionFormula;
+
+/**
+ * 空间能耗计算公式 Mapper(ems_energy_consumption_formula)
+ */
+public interface EmsEnergyConsumptionFormulaMapper extends CrudMapper<EmsEnergyConsumptionFormula> {
+}
+

+ 11 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectConfigurationMapper.java

@@ -0,0 +1,11 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsProjectConfiguration;
+
+/**
+ * 项目配置信息 Mapper(leo.ems_project_configuration)
+ */
+public interface EmsProjectConfigurationMapper extends CrudMapper<EmsProjectConfiguration> {
+}
+

+ 11 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectConversionFactorMapper.java

@@ -0,0 +1,11 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsProjectConversionFactor;
+
+/**
+ * 项目能耗折算系数 Mapper(leo.ems_project_conversion_factor)
+ */
+public interface EmsProjectConversionFactorMapper extends CrudMapper<EmsProjectConversionFactor> {
+}
+

+ 11 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/mapper/EmsProjectDeviceSystemMapper.java

@@ -0,0 +1,11 @@
+package com.usky.ems.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.ems.domain.EmsProjectDeviceSystem;
+
+/**
+ * 项目设备系统 Mapper(leo.ems_project_device_system)
+ */
+public interface EmsProjectDeviceSystemMapper extends CrudMapper<EmsProjectDeviceSystem> {
+}
+

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

@@ -0,0 +1,23 @@
+package com.usky.ems.service;
+
+import com.usky.common.mybatis.core.CrudService;
+import com.usky.ems.domain.DmpProduct;
+import com.usky.ems.enums.ValueTypeEnum;
+import com.usky.ems.service.vo.ProductFunctionVO;
+
+import java.util.List;
+
+/**
+ * 产品信息表 服务接口(service-ems)
+ */
+public interface DmpProductService extends CrudService<DmpProduct> {
+
+    /**
+     * 根据产品 ID 与数值类型获取功能列表
+     * 说明:当前实现基于 leo.sql 中 ems_data_upload_shanghai_product 表,
+     * 仅按 productId 过滤,不区分 valueTypeEnum。
+     */
+    List<ProductFunctionVO> getFunctions(Long id, ValueTypeEnum valueTypeEnum);
+
+}
+

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

@@ -16,4 +16,6 @@ public interface EmsAnalysisService {
     EmsRegionAnalysisResponse getRegionAnalysis(Long projectId, String regionIds, String timeDimension, String timeValue, Long energyTypeId);
 
     EmsCompareResponse getCompare(EmsCompareRequest request);
+
+    EmsAverageResponse getAverage(Long projectId, String timeDimension, String timeValue, Long energyTypeId);
 }

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

@@ -1,14 +0,0 @@
-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();
-}

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

@@ -0,0 +1,18 @@
+package com.usky.ems.service;
+
+import com.usky.ems.service.vo.DeviceEventDTO;
+import com.usky.ems.service.vo.DeviceEventVO;
+
+import java.util.List;
+
+/**
+ * 设备事件服务(EmsDeviceEvent 子模块)
+ */
+public interface EmsDeviceEventService {
+
+    /**
+     * 按条件查询设备事件列表
+     */
+    List<DeviceEventVO> list(DeviceEventDTO dto);
+}
+

+ 25 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsDeviceReportService.java

@@ -0,0 +1,25 @@
+package com.usky.ems.service;
+
+import com.usky.ems.service.vo.DeviceEnergyReportRequest;
+import com.usky.ems.service.vo.DeviceEnergyReportItemVO;
+import com.usky.ems.service.vo.DeviceFunctionValueReportDTO;
+
+import java.util.List;
+
+/**
+ * 设备能耗报表相关服务(EmsDevice 子模块)
+ */
+public interface EmsDeviceReportService {
+
+    /**
+     * 查询能耗报表用的设备列表,并按名称进行排序。
+     */
+    List<DeviceEnergyReportItemVO> showEnergyReportDevices(DeviceEnergyReportRequest request);
+
+    /**
+     * 展示设备能耗报表数据(按时间维度与功能点进行统计)。
+     * 返回值结构后续可根据实际前端展示需求进行扩展,这里使用 Object 以保持灵活性。
+     */
+    Object showEnergyReportData(DeviceFunctionValueReportDTO dto);
+}
+

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

@@ -0,0 +1,19 @@
+package com.usky.ems.service;
+
+import com.usky.common.mybatis.core.CrudService;
+import com.usky.ems.domain.EmsEnergyItemCode;
+import com.usky.ems.service.vo.EnergyItemCodeVO;
+
+import java.util.List;
+
+/**
+ * 能源分项编码 服务接口
+ */
+public interface EmsEnergyItemCodeService extends CrudService<EmsEnergyItemCode> {
+
+    /**
+     * 查询指定空间下的分项能耗配置(基于能耗公式和分项编码组装树形结构)。
+     */
+    List<EnergyItemCodeVO> querySpaceEnergyItem(Long spaceId);
+}
+

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

@@ -5,6 +5,7 @@ import com.usky.ems.service.vo.EmsEnergyTypeVO;
 import com.usky.ems.service.vo.EmsGatewayDeviceTreeNode;
 import com.usky.ems.service.vo.EmsModelSaveRequest;
 import com.usky.ems.service.vo.EmsStructureTreeNode;
+import com.usky.ems.service.vo.EnergyTypeWrapperProductVO;
 
 import java.util.List;
 
@@ -23,6 +24,12 @@ public interface EmsModelService {
      */
     List<EmsEnergyTypeVO> getEnergyTypeList();
 
+    /**
+     * 展示已关联的能源类型及其下属分项/产品列表
+     * 数据来源:leo.ems_energy_item_code.energy_type 分组
+     */
+    List<EnergyTypeWrapperProductVO> showAssociatedEnergyTypes();
+
     /** 建筑:新增 */
     Long createBuilding(EmsModelSaveRequest request);
     /** 建筑:编辑 */

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

@@ -2,6 +2,10 @@ package com.usky.ems.service;
 
 import com.usky.ems.service.vo.EmsProjectResponse;
 import com.usky.ems.service.vo.EmsSummaryResponse;
+import com.usky.ems.service.vo.EmsOverviewEnergyItemVO;
+import com.usky.ems.service.vo.EmsOverviewDeviceSystemStatVO;
+
+import java.util.Map;
 
 /**
  * 能源总览服务(overview)
@@ -17,4 +21,44 @@ public interface EmsOverviewService {
      * 获取项目数据概括(时间维度联动)
      */
     EmsSummaryResponse getSummary(Long projectId, String timeDimension, String timeValue);
+
+    /**
+     * 获取概览页能源类型条目(如电/水/气)
+     */
+    java.util.List<EmsOverviewEnergyItemVO> queryOverviewItem(Long projectId);
+
+    /**
+     * 获取概览页设备系统统计(每个系统下设备数量)
+     */
+    java.util.List<EmsOverviewDeviceSystemStatVO> queryOverviewDeviceInfo(Long projectId);
+
+    /**
+     * 分类能耗统计(按时间维度、能耗类型等)
+     * 先按 Map 结构返回,后续可以再封装 VO。
+     */
+    Map<String, Object> queryClassificationEnergy(Integer dateType, String itemCode, Integer energyType, Long projectId);
+
+    /**
+     * 能耗用能趋势(按时间维度:日/月/年)
+     * 返回当前期与去年同期的用能趋势数据列表(当前为模拟数据实现)。
+     */
+    java.util.List<java.util.Map<String, Object>> queryEnergyTrend(Integer dateType, String itemCode, Long spaceId);
+
+    /**
+     * 建筑能耗分析(建筑/楼层能耗排名)
+     * 返回按能耗从高到低排序的建筑(或楼层)名称及其能耗值列表(当前为模拟数据实现)。
+     */
+    java.util.List<java.util.Map<String, Object>> queryBuildingRanking(Integer dateType, String itemCode, Long spaceId);
+
+    /**
+     * 单位综合能耗、综合累计能耗、分类能耗占比(总览页顶部)
+     * 部分数据来自真实表(项目、折标系数),能耗数值按规则模拟。
+     */
+    Map<String, Object> queryOverviewTop(Integer dateType, Long projectId);
+
+    /**
+     * 分项能耗占比(按分项编码 itemCode 展开下级条目)
+     * 部分数据(分项定义)来自 leo.ems_energy_item_code,能耗数值按规则模拟。
+     */
+    java.util.List<java.util.Map<String, Object>> queryItemRatio(Integer dateType, String itemCode, Long projectId);
 }

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

@@ -0,0 +1,11 @@
+package com.usky.ems.service;
+
+import com.usky.common.mybatis.core.CrudService;
+import com.usky.ems.domain.EmsProjectConfiguration;
+
+/**
+ * 项目配置信息服务
+ */
+public interface EmsProjectConfigurationService extends CrudService<EmsProjectConfiguration> {
+}
+

+ 36 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsProjectService.java

@@ -0,0 +1,36 @@
+package com.usky.ems.service;
+
+import com.usky.common.mybatis.core.CrudService;
+import com.usky.ems.domain.EmsProject;
+import com.usky.ems.domain.EmsProjectConfiguration;
+
+/**
+ * 项目服务(leo.ems_project)
+ */
+public interface EmsProjectService extends CrudService<EmsProject> {
+
+    /**
+     * 新增项目
+     */
+    Long create(EmsProject project);
+
+    /**
+     * 修改项目
+     */
+    void update(EmsProject project);
+
+    /**
+     * 删除项目
+     */
+    void delete(Long id);
+
+    /**
+     * 获取项目配置开关状态:根据当前项目ID与配置名称,判断 value 是否为 "1"
+     *
+     * @param projectId 项目 ID
+     * @param name      配置名称
+     * @return true 表示开启(value="1"),false 表示未开启/未配置
+     */
+    Boolean getConfigurationsStatus(Long projectId, String name);
+}
+

+ 31 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsSpaceService.java

@@ -0,0 +1,31 @@
+package com.usky.ems.service;
+
+import com.usky.common.mybatis.core.CrudService;
+import com.usky.ems.domain.EmsSpace;
+import com.usky.ems.service.vo.EmsSpaceForestNodeVO;
+import java.util.List;
+
+/**
+ * 空间服务(leo.ems_space)
+ */
+public interface EmsSpaceService extends CrudService<EmsSpace> {
+
+    /**
+     * 构建空间树
+     *
+     * @return 根节点(如存在多个根,则返回一个虚拟根节点)
+     */
+    EmsSpaceForestNodeVO tree();
+
+    /**
+     * 获取当前用户在指定空间下有权限访问的空间ID列表
+     * 目前简单返回传入的 spaceId 本身,后续可根据实际权限模型扩展
+     */
+    List<Long> getAuthorizedSpaceIds(Long spaceId);
+
+    /**
+     * 递归获取某空间节点下的所有子节点(不包含自身)
+     */
+    List<EmsSpace> recursiveAllChildrenNode(Long spaceId);
+}
+

+ 75 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/DmpProductServiceImpl.java

@@ -0,0 +1,75 @@
+package com.usky.ems.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.ems.domain.DmpProduct;
+import com.usky.ems.domain.EmsDataUploadShanghaiProduct;
+import com.usky.ems.enums.ValueTypeEnum;
+import com.usky.ems.mapper.DmpProductMapper;
+import com.usky.ems.mapper.EmsDataUploadShanghaiProductMapper;
+import com.usky.ems.service.DmpProductService;
+import com.usky.ems.service.vo.ProductFunctionVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 产品信息表 服务实现类(service-ems)
+ */
+@Service
+public class DmpProductServiceImpl extends AbstractCrudService<DmpProductMapper, DmpProduct> implements DmpProductService {
+
+    @Autowired
+    private EmsDataUploadShanghaiProductMapper shanghaiProductMapper;
+
+    @Override
+    public List<ProductFunctionVO> getFunctions(Long id, ValueTypeEnum valueTypeEnum) {
+        // 参照原有实现:valueTypeEnum 为空返回全部,否则按值类型过滤
+        if (valueTypeEnum == null) {
+            LambdaQueryWrapper<EmsDataUploadShanghaiProduct> wrapper =
+                    Wrappers.<EmsDataUploadShanghaiProduct>lambdaQuery()
+                            .eq(EmsDataUploadShanghaiProduct::getProductId, id)
+                            .orderByAsc(EmsDataUploadShanghaiProduct::getIdentifier)
+                            .orderByDesc(EmsDataUploadShanghaiProduct::getUpdateTime);
+
+            List<EmsDataUploadShanghaiProduct> list = shanghaiProductMapper.selectList(wrapper);
+            return list.stream()
+                    .map(item -> new ProductFunctionVO(
+                            item.getId(),
+                            item.getProductId(),
+                            item.getIdentifier(),
+                            item.getCustomIdentifier()
+                    ))
+                    .collect(Collectors.toList());
+        }
+
+        // 预留:按值类型过滤,目前表结构中暂无 value_type 字段,仅根据 productId 查询
+        return getByProductIdAndValueType(id, valueTypeEnum.getValue());
+    }
+
+    /**
+     * 根据产品 ID 与值类型获取功能列表。
+     * 当前实现仅按 productId 过滤,后续可在 ems_data_upload_shanghai_product 表中增加 value_type 字段后扩展。
+     */
+    private List<ProductFunctionVO> getByProductIdAndValueType(Long id, Short valueType) {
+        LambdaQueryWrapper<EmsDataUploadShanghaiProduct> wrapper =
+                Wrappers.<EmsDataUploadShanghaiProduct>lambdaQuery()
+                        .eq(EmsDataUploadShanghaiProduct::getProductId, id)
+                        .orderByAsc(EmsDataUploadShanghaiProduct::getIdentifier)
+                        .orderByDesc(EmsDataUploadShanghaiProduct::getUpdateTime);
+
+        List<EmsDataUploadShanghaiProduct> list = shanghaiProductMapper.selectList(wrapper);
+        return list.stream()
+                .map(item -> new ProductFunctionVO(
+                        item.getId(),
+                        item.getProductId(),
+                        item.getIdentifier(),
+                        item.getCustomIdentifier()
+                ))
+                .collect(Collectors.toList());
+    }
+}
+

+ 1103 - 16
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAnalysisServiceImpl.java

@@ -1,61 +1,1148 @@
 package com.usky.ems.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.ems.mapper.EmsDeviceMapper;
+import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
+import com.usky.ems.mapper.EmsProjectMapper;
+import com.usky.ems.mapper.EmsSpaceMapper;
 import com.usky.ems.service.EmsAnalysisService;
+import com.usky.ems.service.TdengineService;
 import com.usky.ems.service.vo.*;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import javax.sql.DataSource;
 import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.text.SimpleDateFormat;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
 
 /**
- * 能耗分析服务实现(占位数据,后续对接时序/聚合数据)
+ * 能耗分析服务实现
  */
 @Service
+@DS("mysql")
 public class EmsAnalysisServiceImpl implements EmsAnalysisService {
 
     private static final String[] ENERGY_TYPE_NAMES = {"", "电", "水", "气"};
+    
+    // 标准煤转换系数
+    private static final Map<Long, BigDecimal> STANDARD_COAL_FACTORS = new HashMap<Long, BigDecimal>();
+    static {
+        STANDARD_COAL_FACTORS.put(Long.valueOf(1), new BigDecimal("0.1229")); // 电
+        STANDARD_COAL_FACTORS.put(Long.valueOf(2), new BigDecimal("0.0857")); // 水  
+        STANDARD_COAL_FACTORS.put(Long.valueOf(3), new BigDecimal("0.2143")); // 气
+    }
+    
+    // 碳排放转换系数
+    private static final Map<Long, BigDecimal> CARBON_EMISSION_FACTORS = new HashMap<Long, BigDecimal>();
+    static {
+        CARBON_EMISSION_FACTORS.put(Long.valueOf(1), new BigDecimal("0.7850")); // 电
+        CARBON_EMISSION_FACTORS.put(Long.valueOf(2), new BigDecimal("0.2600")); // 水
+        CARBON_EMISSION_FACTORS.put(Long.valueOf(3), new BigDecimal("0.4920")); // 气
+    }
+    
+    // 能源单位
+    private static final Map<Long, String> ENERGY_UNITS = new HashMap<Long, String>();
+    static {
+        ENERGY_UNITS.put(Long.valueOf(1), "kWh"); // 电
+        ENERGY_UNITS.put(Long.valueOf(2), "m³");   // 水
+        ENERGY_UNITS.put(Long.valueOf(3), "m³");   // 气
+    }
+
+    @Autowired
+    private TdengineService tdengineService;
+    
+    @Autowired
+    private EmsProjectMapper emsProjectMapper;
+    
+    @Autowired
+    private EmsSpaceMapper emsSpaceMapper;
+    
+    @Autowired
+    private EmsDeviceMapper emsDeviceMapper;
+    
+    @Autowired
+    private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
+    
+    @Autowired
+    private DataSource mysqlDataSource;
 
     @Override
     public EmsTrendResponse getTrend(Long projectId, String timeDimension, String timeValue, Long energyTypeId) {
         EmsTrendResponse resp = new EmsTrendResponse();
         resp.setTimeDimension(timeDimension);
         resp.setTimeValue(timeValue);
+
+        try {
+            // 获取项目下的所有设备
+            List<String> deviceIds = getDeviceIdsByProject(projectId, energyTypeId);
+            if (deviceIds.isEmpty()) {
+                return resp;
+            }
+
+            // 根据时间维度生成时间点
+            List<String> timePoints = generateTimePoints(timeDimension, timeValue);
+            
+            // 查询每个时间点的能耗数据
+            List<EmsTrendItemVO> trendItems = new ArrayList<>();
+            for (String timePoint : timePoints) {
+                EmsTrendItemVO item = calculateTrendItem(deviceIds, timeDimension, timePoint, energyTypeId);
+                trendItems.add(item);
+            }
+            
+            resp.setTrend(trendItems);
+        } catch (Exception e) {
+            // 日志记录错误,返回空数据
+            e.printStackTrace();
+        }
+        
         return resp;
     }
 
+    /**
+     * 生成时间点列表
+     */
+    private List<String> generateTimePoints(String timeDimension, String timeValue) {
+        List<String> timePoints = new ArrayList<>();
+        
+        try {
+            switch (timeDimension.toUpperCase()) {
+                case "D": // 日维度,生成该日每小时
+                    timePoints = generateHourlyPoints(timeValue);
+                    break;
+                case "M": // 月维度,生成该月每天
+                    timePoints = generateDailyPoints(timeValue);
+                    break;
+                case "Y": // 年维度,生成该年每月
+                    timePoints = generateMonthlyPoints(timeValue);
+                    break;
+                default:
+                    timePoints.add(timeValue);
+            }
+        } catch (Exception e) {
+            // 如果解析出错,返回原始时间值
+            timePoints.add(timeValue);
+        }
+        
+        return timePoints;
+    }
+    
+    /**
+     * 生成日维度时间点
+     */
+    private List<String> generateDailyPoints(String yearMonth) {
+        List<String> days = new ArrayList<>();
+        try {
+            String[] parts = yearMonth.split("-");
+            int year = Integer.parseInt(parts[0]);
+            int month = Integer.parseInt(parts[1]);
+            
+            java.time.YearMonth ym = java.time.YearMonth.of(year, month);
+            int daysInMonth = ym.lengthOfMonth();
+            
+            for (int day = 1; day <= daysInMonth; day++) {
+                days.add(year + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day);
+            }
+        } catch (Exception e) {
+            days.add(yearMonth);
+        }
+        return days;
+    }
+    
+    /**
+     * 生成月维度时间点
+     */
+    private List<String> generateMonthlyPoints(String year) {
+        List<String> months = new ArrayList<>();
+        try {
+            int yearInt = Integer.parseInt(year);
+            for (int month = 1; month <= 12; month++) {
+                months.add(yearInt + "-" + (month < 10 ? "0" : "") + month);
+            }
+        } catch (Exception e) {
+            months.add(year);
+        }
+        return months;
+    }
+    
+    /**
+     * 生成小时维度时间点
+     */
+    private List<String> generateHourlyPoints(String date) {
+        List<String> hours = new ArrayList<>();
+        try {
+            String[] parts = date.split("-");
+            int year = Integer.parseInt(parts[0]);
+            int month = Integer.parseInt(parts[1]);
+            int day = Integer.parseInt(parts[2]);
+            
+            for (int hour = 0; hour < 24; hour++) {
+                hours.add(year + "-" + (month < 10 ? "0" : "") + month + "-" + (day < 10 ? "0" : "") + day + " " + (hour < 10 ? "0" : "") + hour + ":00:00");
+            }
+        } catch (Exception e) {
+            hours.add(date);
+        }
+        return hours;
+    }
+    
+    /**
+     * 计算单个时间点的趋势数据
+     */
+    private EmsTrendItemVO calculateTrendItem(List<String> deviceIds, String timeDimension, String timePoint, Long energyTypeId) {
+        EmsTrendItemVO item = new EmsTrendItemVO();
+        item.setTimeLabel(timePoint);
+        
+        try {
+        // 查询设备能耗数据
+        BigDecimal totalUsage = queryDeviceEnergyConsumption(deviceIds, timeDimension, timePoint, energyTypeId);
+            
+            // 计算标准煤和碳排放
+            BigDecimal standardCoal = BigDecimal.ZERO;
+            BigDecimal carbonEmission = BigDecimal.ZERO;
+            
+            if (energyTypeId != null && totalUsage != null && totalUsage.compareTo(BigDecimal.ZERO) > 0) {
+                BigDecimal coalFactor = STANDARD_COAL_FACTORS.getOrDefault(energyTypeId, BigDecimal.ZERO);
+                BigDecimal carbonFactor = CARBON_EMISSION_FACTORS.getOrDefault(energyTypeId, BigDecimal.ZERO);
+                
+                standardCoal = totalUsage.multiply(coalFactor).setScale(2, RoundingMode.HALF_UP);
+                carbonEmission = totalUsage.multiply(carbonFactor).setScale(2, RoundingMode.HALF_UP);
+            }
+            
+            item.setUsage(totalUsage != null ? totalUsage.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
+            item.setStandardCoal(standardCoal);
+            item.setCarbonEmission(carbonEmission);
+            
+        } catch (Exception e) {
+            // 出错时设置默认值
+            item.setUsage(BigDecimal.ZERO);
+            item.setStandardCoal(BigDecimal.ZERO);
+            item.setCarbonEmission(BigDecimal.ZERO);
+        }
+        
+        return item;
+    }
+    
+    /**
+     * 查询设备能耗消耗量
+     */
+    private BigDecimal queryDeviceEnergyConsumption(List<String> deviceIds, String timeDimension, String timePoint, Long energyTypeId) {
+        BigDecimal totalUsage = BigDecimal.ZERO;
+        
+        System.out.println("Querying energy for timePoint: " + timePoint + ", deviceIds: " + deviceIds + ", energyTypeId: " + energyTypeId);
+        
+        try (Connection conn = mysqlDataSource.getConnection()) {
+            String sql = buildEnergyQuerySQL(deviceIds, timeDimension, energyTypeId != null);
+    
+            System.out.println("Time range for " + timePoint + ": " + String.join(" | ", getTimeRange(timeDimension, timePoint)));
+            
+            try (PreparedStatement stmt = conn.prepareStatement(sql)) {
+                int paramIndex = 1;
+                
+                // 设置设备ID参数
+                System.out.println("Setting device IDs:");
+                for (String deviceId : deviceIds) {
+                    System.out.println("  - " + deviceId);
+                    stmt.setString(paramIndex++, deviceId);
+                }
+                
+                // 设置时间参数
+                String[] timeRange = getTimeRange(timeDimension, timePoint);
+                System.out.println("Setting time range: start=" + timeRange[0] + ", end=" + timeRange[1]);
+                stmt.setString(paramIndex++, timeRange[0]);
+                stmt.setString(paramIndex++, timeRange[1]);
+                
+                if (energyTypeId != null) {
+                    System.out.println("Setting energyTypeId: " + energyTypeId);
+                    stmt.setLong(paramIndex++, energyTypeId);
+                }
+                
+                try (ResultSet rs = stmt.executeQuery()) {
+                    if (rs.next()) {
+                        double usage = rs.getDouble("total_usage");
+                        if (!rs.wasNull()) {
+                            totalUsage = new BigDecimal(usage);
+                            System.out.println("Found usage: " + totalUsage + " for timePoint: " + timePoint);
+                        } else {
+                            System.out.println("No usage data (wasNull=true) for timePoint: " + timePoint);
+                        }
+                    } else {
+                        System.out.println("No result set for timePoint: " + timePoint);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            System.err.println("Error querying energy consumption for timePoint: " + timePoint);
+            e.printStackTrace();
+        }
+        
+        System.out.println("Final totalUsage for " + timePoint + ": " + totalUsage);
+        return totalUsage;
+    }
+    
+    /**
+     * 构建能耗查询SQL
+     */
+    private String buildEnergyQuerySQL(List<String> deviceIds, String timeDimension, boolean hasEnergyTypeFilter) {
+        StringBuilder sql = new StringBuilder();
+        sql.append("SELECT SUM(eed.end_value - eed.start_value) as total_usage ");
+        sql.append("FROM ems_energy_consumption_device_data eed ");
+        
+        // 如果需要能源类型过滤,关联设备表和产品能源类型表
+        if (hasEnergyTypeFilter) {
+            sql.append("INNER JOIN ems_device ed ON eed.device_id = ed.id ");
+            sql.append("INNER JOIN ems_product_energy_type epe ON ed.product_id = epe.product_id ");
+        }
+        
+        sql.append("WHERE eed.device_id IN (");
+        
+        // 为每个设备ID添加占位符
+        for (int i = 0; i < deviceIds.size(); i++) {
+            if (i > 0) {
+                sql.append(", ");
+            }
+            sql.append("?");
+        }
+        sql.append(") ");
+        
+        // 添加时间条件
+        sql.append("AND eed.start_time >= ? AND eed.end_time <= ? ");
+        
+        if (hasEnergyTypeFilter) {
+            sql.append("AND epe.energy_type = ? ");
+        }
+        
+        System.out.println("Generated SQL: " + sql.toString());
+        
+        return sql.toString();
+    }
+    
+    /**
+     * 获取时间范围
+     */
+    private String[] getTimeRange(String timeDimension, String timePoint) {
+        String[] range = new String[2];
+        try {
+            switch (timeDimension.toUpperCase()) {
+                case "D":
+                    // 对于小时级数据,timePoint已经包含具体时间,格式为 "YYYY-MM-DD HH:00:00"
+                    if (timePoint.contains(" ")) {
+                        // 提取日期和时间部分,格式:YYYY-MM-DD HH
+                        String datePart = timePoint.split(" ")[0];
+                        String hourPart = timePoint.split(" ")[1].split(":")[0];
+                        
+                        // 设置整小时范围:HH:00:00 到 HH:59:59
+                        range[0] = datePart + " " + hourPart + ":00:00";
+                        range[1] = datePart + " " + hourPart + ":59:59";
+                    } else {
+                        // 如果没有时间,默认为全天
+                        range[0] = timePoint + " 00:00:00";
+                        range[1] = timePoint + " 23:59:59";
+                    }
+                    break;
+                case "M":
+                    range[0] = timePoint + " 00:00:00";
+                    range[1] = timePoint + " 23:59:59";
+                    break;
+                case "Y":
+                    // For yearly dimension, timePoint is in format "YYYY-MM", need full month
+                    String[] yearMonth = timePoint.split("-");
+                    if (yearMonth.length == 2) {
+                        String yearPart = yearMonth[0];
+                        String monthPart = yearMonth[1];
+                        range[0] = yearPart + "-" + monthPart + "-01 00:00:00";
+                        range[1] = yearPart + "-" + monthPart + "-31 23:59:59";
+                    } else {
+                        range[0] = timePoint + "-01-01 00:00:00";
+                        range[1] = timePoint + "-12-31 23:59:59";
+                    }
+                    break;
+                default:
+                    range[0] = timePoint;
+                    range[1] = timePoint;
+            }
+        } catch (Exception e) {
+            range[0] = timePoint;
+            range[1] = timePoint;
+        }
+        
+        return range;
+    }
+    
+    /**
+     * 根据项目ID获取设备列表
+     */
+    private List<String> getDeviceIdsByProject(Long projectId, Long energyTypeId) {
+        List<String> deviceIds = new ArrayList<>();
+        try (Connection conn = mysqlDataSource.getConnection()) {
+            StringBuilder sql = new StringBuilder();
+            sql.append("SELECT DISTINCT ed.id ");
+            sql.append("FROM ems_device ed ");
+            
+            if (energyTypeId != null) {
+                sql.append("INNER JOIN ems_product_energy_type epe ON ed.product_id = epe.product_id ");
+            }
+            
+            sql.append("WHERE 1=1 ");
+            
+            if (projectId != null) {
+                sql.append("AND ed.project_id = ? ");
+            }
+            
+            if (energyTypeId != null) {
+                sql.append("AND epe.energy_type = ? ");
+            }
+            
+            try (PreparedStatement stmt = conn.prepareStatement(sql.toString())) {
+                int paramIndex = 1;
+                
+                if (projectId != null) {
+                    System.out.println("Setting projectId parameter: " + projectId);
+                    stmt.setLong(paramIndex++, projectId);
+                }
+                
+                if (energyTypeId != null) {
+                    System.out.println("Setting energyTypeId parameter: " + energyTypeId);
+                    stmt.setLong(paramIndex++, energyTypeId);
+                }
+                
+                System.out.println("Device query SQL: " + sql.toString());
+                
+                try (ResultSet rs = stmt.executeQuery()) {
+                    while (rs.next()) {
+                        String deviceId = rs.getString("id");
+                        deviceIds.add(deviceId);
+                        System.out.println("Found device ID: " + deviceId);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            System.err.println("Error getting device IDs: " + e.getMessage());
+            e.printStackTrace();
+        }
+        
+        System.out.println("Total device IDs found: " + deviceIds.size());
+        return deviceIds;
+    }
+
     @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);
+
+        try {
+            // 获取项目下的所有设备
+            List<String> deviceIds = getDeviceIdsByProject(projectId, energyTypeId);
+            if (deviceIds.isEmpty()) {
+                return resp;
+            }
+
+            // 计算总用量
+            String[] timeRange = getTimeRange(timeDimension, timeValue);
+            BigDecimal totalUsage = queryTotalEnergyConsumption(deviceIds, timeRange[0], timeRange[1], energyTypeId);
+            
+            // 计算标准煤和碳排放
+            BigDecimal standardCoal = BigDecimal.ZERO;
+            BigDecimal carbonEmission = BigDecimal.ZERO;
+            
+            if (energyTypeId != null && totalUsage.compareTo(BigDecimal.ZERO) > 0) {
+                BigDecimal coalFactor = STANDARD_COAL_FACTORS.getOrDefault(energyTypeId, BigDecimal.ZERO);
+                BigDecimal carbonFactor = CARBON_EMISSION_FACTORS.getOrDefault(energyTypeId, BigDecimal.ZERO);
+                
+                standardCoal = totalUsage.multiply(coalFactor).setScale(2, RoundingMode.HALF_UP);
+                carbonEmission = totalUsage.multiply(carbonFactor).setScale(2, RoundingMode.HALF_UP);
+            }
+            
+            // 计算单位面积能耗和人均能耗
+            Map<String, BigDecimal> areaAndPopulation = getProjectAreaAndPopulation(projectId);
+            BigDecimal totalArea = areaAndPopulation.get("area");
+            BigDecimal totalPopulation = areaAndPopulation.get("population");
+            
+            BigDecimal areaUsage = BigDecimal.ZERO;
+            BigDecimal perCapitaUsage = BigDecimal.ZERO;
+            
+            if (totalArea.compareTo(BigDecimal.ZERO) > 0) {
+                areaUsage = totalUsage.divide(totalArea, 2, RoundingMode.HALF_UP);
+            }
+            
+            if (totalPopulation.compareTo(BigDecimal.ZERO) > 0) {
+                perCapitaUsage = totalUsage.divide(totalPopulation, 2, RoundingMode.HALF_UP);
+            }
+            
+            // 评定等级
+            Map<String, String> rating = calculateRating(areaUsage, energyTypeId);
+            
+            resp.setTotalUsage(totalUsage.setScale(2, RoundingMode.HALF_UP));
+            resp.setStandardCoal(standardCoal);
+            resp.setCarbonEmission(carbonEmission);
+            resp.setAreaUsage(areaUsage);
+            resp.setPerCapitaUsage(perCapitaUsage);
+            resp.setRating(rating);
+            
+        } catch (Exception e) {
+            System.err.println("Error getting trend indicators: " + e.getMessage());
+        }
+        
         return resp;
     }
+    
+    /**
+     * 查询总能耗
+     */
+    private BigDecimal queryTotalEnergyConsumption(List<String> deviceIds, String startTime, String endTime, Long energyTypeId) {
+        BigDecimal totalUsage = BigDecimal.ZERO;
+        
+        try (Connection conn = mysqlDataSource.getConnection()) {
+            StringBuilder sql = new StringBuilder();
+            sql.append("SELECT SUM(eed.end_value - eed.start_value) as total_usage ");
+            sql.append("FROM ems_energy_consumption_device_data eed ");
+            if (energyTypeId != null) {
+                sql.append("INNER JOIN ems_device ed ON eed.device_id = ed.id ");
+                sql.append("INNER JOIN ems_product_energy_type epe ON ed.product_id = epe.product_id ");
+            }
+            sql.append("WHERE eed.device_id IN (")
+                .append(String.join(",", Collections.nCopies(deviceIds.size(), "?")))
+                .append(") ");
+            sql.append("AND eed.start_time >= ? AND eed.end_time <= ? ");
 
-    @Override
-    public EmsTrendCategoryResponse getTrendCategory(Long projectId, String timeDimension, String timeValue, Long energyTypeId) {
+            if (energyTypeId != null) {
+                sql.append("AND epe.energy_type = ? ");
+            }
+            
+            try (PreparedStatement stmt = conn.prepareStatement(sql.toString())) {
+                int paramIndex = 1;
+                
+                // 设置设备ID参数
+                for (String deviceId : deviceIds) {
+                    stmt.setString(paramIndex++, deviceId);
+                }
+                
+                // 设置时间参数
+                stmt.setString(paramIndex++, startTime);
+                stmt.setString(paramIndex++, endTime);
+                
+                if (energyTypeId != null) {
+                    stmt.setLong(paramIndex++, energyTypeId);
+                }
+                
+                try (ResultSet rs = stmt.executeQuery()) {
+                    if (rs.next()) {
+                        double usage = rs.getDouble("total_usage");
+                        if (!rs.wasNull()) {
+                            totalUsage = new BigDecimal(usage);
+                        }
+                    } else {
+                    }
+                }
+            }
+        } catch (Exception e) {
+            System.err.println("Error querying total energy consumption: " + e.getMessage());
+        }
+        
+        return totalUsage;
+    }
+    
+    /**
+     * 获取项目总面积和总人数
+     */
+    private Map<String, BigDecimal> getProjectAreaAndPopulation(Long projectId) {
+        Map<String, BigDecimal> result = new HashMap<>();
+        result.put("area", BigDecimal.ZERO);
+        result.put("population", BigDecimal.ZERO);
+        
+        if (projectId == null) {
+            return result;
+        }
+        
+        System.out.println("Getting project area and population for projectId: " + projectId);
+        
+        try (Connection conn = mysqlDataSource.getConnection()) {
+            String sql = "SELECT SUM(area) as total_area, SUM(resident_population) as total_population " +
+                        "FROM ems_space_building " +
+                        "WHERE space_id IN (SELECT id FROM ems_space WHERE root_id = ?)";
+            
+            System.out.println("Project area SQL: " + sql);
+            System.out.println("Setting projectId parameter: " + projectId);
+            
+            try (PreparedStatement stmt = conn.prepareStatement(sql)) {
+                stmt.setLong(1, projectId);
+                
+                try (ResultSet rs = stmt.executeQuery()) {
+                    if (rs.next()) {
+                        double area = rs.getDouble("total_area");
+                        double population = rs.getDouble("total_population");
+                        
+                        result.put("area", rs.wasNull() || Double.isNaN(area) ? BigDecimal.ZERO : new BigDecimal(area));
+                        result.put("population", rs.wasNull() || Double.isNaN(population) ? BigDecimal.ZERO : new BigDecimal(population));
+                    }
+                }
+            }
+        } catch (Exception e) {
+            System.err.println("Error getting project area and population: " + e.getMessage());
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 计算能耗评定等级
+     */
+    private Map<String, String> calculateRating(BigDecimal areaUsage, Long energyTypeId) {
+        Map<String, String> rating = new HashMap<>();
+        
+        // 简单的评定逻辑,可以根据实际需求调整
+        String level;
+        String description = "单位面积能耗评定";
+        
+        if (energyTypeId == null) {
+            level = "良好";
+        } else {
+            switch (energyTypeId.intValue()) {
+                case 1: // 电
+                    if (areaUsage.compareTo(new BigDecimal("10")) < 0) {
+                        level = "优秀";
+                    } else if (areaUsage.compareTo(new BigDecimal("20")) < 0) {
+                        level = "良好";
+                    } else if (areaUsage.compareTo(new BigDecimal("30")) < 0) {
+                        level = "一般";
+                    } else {
+                        level = "较差";
+                    }
+                    break;
+                case 2: // 水
+                    if (areaUsage.compareTo(new BigDecimal("2")) < 0) {
+                        level = "优秀";
+                    } else if (areaUsage.compareTo(new BigDecimal("5")) < 0) {
+                        level = "良好";
+                    } else if (areaUsage.compareTo(new BigDecimal("8")) < 0) {
+                        level = "一般";
+                    } else {
+                        level = "较差";
+                    }
+                    break;
+                case 3: // 气
+                    if (areaUsage.compareTo(new BigDecimal("5")) < 0) {
+                        level = "优秀";
+                    } else if (areaUsage.compareTo(new BigDecimal("10")) < 0) {
+                        level = "良好";
+                    } else if (areaUsage.compareTo(new BigDecimal("15")) < 0) {
+                        level = "一般";
+                    } else {
+                        level = "较差";
+                    }
+                    break;
+                default:
+                    level = "良好";
+            }
+        }
+        
+        rating.put("level", level);
+        rating.put("description", description);
+        
+         return rating;
+     }
+     
+     /**
+      * 获取所有能源类型
+      */
+     private List<Long> getAllEnergyTypes(Long specificEnergyType) {
+         List<Long> energyTypes = new ArrayList<>();
+         
+         if (specificEnergyType != null) {
+             energyTypes.add(specificEnergyType);
+         } else {
+             // 返回所有支持的能源类型(1:电, 2:水, 3:气)
+             energyTypes.addAll(Arrays.asList(1L, 2L, 3L));
+         }
+         
+         return energyTypes;
+     }
+     
+     /**
+      * 解析区域ID
+      */
+     private List<Long> parseRegionIds(String regionIds, Long projectId) {
+         List<Long> regionIdList = new ArrayList<>();
+         
+         if (regionIds != null && !regionIds.trim().isEmpty()) {
+             // 解析逗号分隔的区域ID
+             try {
+                 regionIdList = Arrays.stream(regionIds.split(","))
+                     .map(String::trim)
+                     .map(new java.util.function.Function<String, Long>() {
+                         @Override
+                         public Long apply(String s) {
+                             return Long.valueOf(s);
+                         }
+                     })
+                     .collect(Collectors.toList());
+             } catch (Exception e) {
+                 System.err.println("Error parsing region IDs: " + e.getMessage());
+                 return regionIdList;
+             }
+         } else if (projectId != null) {
+             // 如果没有指定区域ID,则获取项目下的所有区域
+             try (Connection conn = mysqlDataSource.getConnection()) {
+                 // 查找该项目下的所有区域(type=2表示区域类型)
+                 String sql = "SELECT id FROM ems_space WHERE root_id = ? AND type = 2";
+                 
+                 try (PreparedStatement stmt = conn.prepareStatement(sql)) {
+                     stmt.setLong(1, projectId);
+                     
+                     try (ResultSet rs = stmt.executeQuery()) {
+                         while (rs.next()) {
+                             regionIdList.add(Long.valueOf(rs.getLong("id")));
+                         }
+                     }
+                 }
+             } catch (Exception e) {
+                 System.err.println("Error getting region IDs by project: " + e.getMessage());
+             }
+         }
+         
+         return regionIdList;
+     }
+     
+     /**
+      * 分析区域能耗情况
+      */
+     private EmsRegionAnalysisItemVO analyzeRegionEnergyConsumption(Long regionId, String startTime, String endTime, 
+                                                                   Long energyTypeId, String timeDimension, String timeValue) {
+         try (Connection conn = mysqlDataSource.getConnection()) {
+             // 获取区域基本信息
+             String regionName = getRegionName(regionId, conn);
+             if (regionName == null) {
+                 return null;
+             }
+             
+             // 获取区域面积
+             BigDecimal area = getRegionArea(regionId, conn);
+             
+             // 获取该区域下的设备能耗
+             List<String> deviceIds = getDeviceIdsByRegion(regionId, energyTypeId, conn);
+             BigDecimal totalUsage = BigDecimal.ZERO;
+             String unit = "kWh";
+             
+             if (!deviceIds.isEmpty()) {
+                 totalUsage = queryTotalEnergyConsumption(deviceIds, startTime, endTime, energyTypeId);
+                 if (energyTypeId != null) {
+                     unit = ENERGY_UNITS.getOrDefault(energyTypeId, "kWh");
+                 }
+             }
+             
+             // 计算单位面积能耗
+             BigDecimal areaUsage = BigDecimal.ZERO;
+             if (area.compareTo(BigDecimal.ZERO) > 0) {
+                 areaUsage = totalUsage.divide(area, 2, RoundingMode.HALF_UP);
+             }
+             
+             EmsRegionAnalysisItemVO item = new EmsRegionAnalysisItemVO();
+             item.setRegionId(regionId);
+             item.setRegionName(regionName);
+             item.setArea(area.setScale(2, RoundingMode.HALF_UP));
+             item.setTotalUsage(totalUsage.setScale(2, RoundingMode.HALF_UP));
+             item.setAreaUsage(areaUsage);
+             item.setUnit(unit);
+             item.setTimeDimension(timeDimension);
+             item.setTimeValue(timeValue);
+             
+             return item;
+         } catch (Exception e) {
+             System.err.println("Error analyzing region energy consumption: " + e.getMessage());
+             return null;
+         }
+     }
+     
+     /**
+      * 获取区域名称
+      */
+     private String getRegionName(Long regionId, Connection conn) {
+         try {
+             String sql = "SELECT name FROM ems_space WHERE id = ?";
+             try (PreparedStatement stmt = conn.prepareStatement(sql)) {
+                 stmt.setLong(1, regionId);
+                 try (ResultSet rs = stmt.executeQuery()) {
+                     if (rs.next()) {
+                         return rs.getString("name");
+                     }
+                 }
+             }
+         } catch (Exception e) {
+             System.err.println("Error getting region name: " + e.getMessage());
+         }
+         return "未知区域";
+     }
+     
+     /**
+      * 获取区域面积
+      */
+     private BigDecimal getRegionArea(Long regionId, Connection conn) {
+         try {
+             String sql = "SELECT SUM(area) as total_area FROM ems_space_building WHERE space_id = ?";
+             try (PreparedStatement stmt = conn.prepareStatement(sql)) {
+                 stmt.setLong(1, regionId);
+                 try (ResultSet rs = stmt.executeQuery()) {
+                     if (rs.next()) {
+                         double area = rs.getDouble("total_area");
+                         if (!rs.wasNull()) {
+                             return new BigDecimal(area);
+                         }
+                     }
+                 }
+             }
+         } catch (Exception e) {
+             System.err.println("Error getting region area: " + e.getMessage());
+         }
+         return BigDecimal.ZERO;
+     }
+     
+     /**
+      * 获取区域下的设备ID列表
+      */
+     private List<String> getDeviceIdsByRegion(Long regionId, Long energyTypeId, Connection conn) {
+         List<String> deviceIds = new ArrayList<>();
+         
+         try {
+             StringBuilder sql = new StringBuilder();
+             sql.append("SELECT DISTINCT ed.id ");
+             sql.append("FROM ems_device ed ");
+             
+             if (energyTypeId != null) {
+                 sql.append("INNER JOIN ems_product_energy_type epe ON ed.product_id = epe.product_id ");
+             }
+             
+             // 查找该区域下的建筑,然后在这些建筑中查找设备
+             sql.append("WHERE ed.monitoring_location IN ");
+             sql.append("(SELECT id FROM ems_space_building WHERE space_id = ?) ");
+             
+             if (energyTypeId != null) {
+                 sql.append("AND epe.energy_type = ? ");
+             }
+             
+             try (PreparedStatement stmt = conn.prepareStatement(sql.toString())) {
+                 int paramIndex = 1;
+                 stmt.setLong(paramIndex++, regionId);
+                 
+                 if (energyTypeId != null) {
+                     stmt.setLong(paramIndex++, energyTypeId);
+                 }
+                 
+                 try (ResultSet rs = stmt.executeQuery()) {
+                     while (rs.next()) {
+                         deviceIds.add(rs.getString("id"));
+                     }
+                 }
+             }
+         } catch (Exception e) {
+             System.err.println("Error getting device IDs by region: " + e.getMessage());
+         }
+         
+         return deviceIds;
+     }
+
+     @Override
+     public EmsTrendCategoryResponse getTrendCategory(Long projectId, String timeDimension, String timeValue, Long energyTypeId) {
         EmsTrendCategoryResponse resp = new EmsTrendCategoryResponse();
         resp.setTimeDimension(timeDimension);
         resp.setTimeValue(timeValue);
+
+        try {
+            // 获取所有能源类型
+            List<Long> energyTypes = getAllEnergyTypes(energyTypeId);
+            
+            // 查询每个能源类型的用量
+            List<EmsCategoryRatioItemVO> categoryItems = new ArrayList<>();
+            BigDecimal totalAllUsage = BigDecimal.ZERO;
+            Map<Long, BigDecimal> energyTypeUsage = new HashMap<>();
+            
+            for (Long typeId : energyTypes) {
+                List<String> deviceIds = getDeviceIdsByProject(projectId, typeId);
+                if (!deviceIds.isEmpty()) {
+                    String[] timeRange = getTimeRange(timeDimension, timeValue);
+                    BigDecimal usage = queryTotalEnergyConsumption(deviceIds, timeRange[0], timeRange[1], typeId);
+                    energyTypeUsage.put(typeId, usage);
+                    totalAllUsage = totalAllUsage.add(usage);
+                } else {
+                    energyTypeUsage.put(typeId, BigDecimal.ZERO);
+                }
+            }
+            
+            // 计算占比和构建响应
+            for (Long typeId : energyTypes) {
+                BigDecimal usage = energyTypeUsage.get(typeId);
+                BigDecimal ratio = BigDecimal.ZERO;
+                
+                if (totalAllUsage.compareTo(BigDecimal.ZERO) > 0) {
+                    ratio = usage.divide(totalAllUsage, 4, RoundingMode.HALF_UP)
+                                 .multiply(new BigDecimal("100"))
+                                 .setScale(1, RoundingMode.HALF_UP);
+                }
+                
+                 EmsCategoryRatioItemVO item = new EmsCategoryRatioItemVO();
+                 item.setEnergyTypeName(typeId >= 1 && typeId <= 3 ? ENERGY_TYPE_NAMES[typeId.intValue()] : "未知");
+                 item.setRatio(ratio);
+                 item.setUsage(usage.setScale(2, RoundingMode.HALF_UP));
+                 item.setUnit(ENERGY_UNITS.get(typeId));
+                
+                categoryItems.add(item);
+            }
+            
+            resp.setCategoryRatio(categoryItems);
+            
+        } catch (Exception e) {
+            System.err.println("Error getting trend category: " + e.getMessage());
+        }
+        
         return resp;
     }
 
-    @Override
-    public EmsRegionAnalysisResponse getRegionAnalysis(Long projectId, String regionIds, String timeDimension, String timeValue, Long energyTypeId) {
-        return new EmsRegionAnalysisResponse();
-    }
+     @Override
+     public EmsRegionAnalysisResponse getRegionAnalysis(Long projectId, String regionIds, String timeDimension, String timeValue, Long energyTypeId) {
+         EmsRegionAnalysisResponse resp = new EmsRegionAnalysisResponse();
+ 
+         try {
+             // 简化实现:如果没有指定区域ID,返回项目整体数据作为一个区域
+             List<EmsRegionAnalysisItemVO> items = new ArrayList<>();
+             String[] timeRange = getTimeRange(timeDimension, timeValue);
+             
+             // 获取项目下的所有设备
+             List<String> deviceIds = getDeviceIdsByProject(projectId, energyTypeId);
+             if (!deviceIds.isEmpty()) {
+                 // 计算项目总体能耗
+                 BigDecimal totalUsage = queryTotalEnergyConsumption(deviceIds, timeRange[0], timeRange[1], energyTypeId);
+                 
+                 // 获取项目面积
+                 Map<String, BigDecimal> areaAndPopulation = getProjectAreaAndPopulation(projectId);
+                 BigDecimal totalArea = areaAndPopulation.get("area");
+                 
+                 // 计算单位面积能耗
+                 BigDecimal areaUsage = BigDecimal.ZERO;
+                 if (totalArea.compareTo(BigDecimal.ZERO) > 0) {
+                     areaUsage = totalUsage.divide(totalArea, 2, RoundingMode.HALF_UP);
+                 }
+                 
+                 // 设置单位
+                 String unit = "kWh";
+                 if (energyTypeId != null) {
+                     unit = ENERGY_UNITS.getOrDefault(energyTypeId, "kWh");
+                 }
+                 
+                 EmsRegionAnalysisItemVO item = new EmsRegionAnalysisItemVO();
+                 item.setRegionId(projectId != null ? projectId : 1L);
+                 item.setRegionName(projectId != null ? "项目" + projectId : "默认项目");
+                 item.setArea(totalArea.setScale(2, RoundingMode.HALF_UP));
+                 item.setTotalUsage(totalUsage.setScale(2, RoundingMode.HALF_UP));
+                 item.setAreaUsage(areaUsage);
+                 item.setUnit(unit);
+                 item.setTimeDimension(timeDimension);
+                 item.setTimeValue(timeValue);
+                 
+                 items.add(item);
+             }
+             
+             resp.setItems(items);
+             
+         } catch (Exception e) {
+             System.err.println("Error getting region analysis: " + e.getMessage());
+         }
+         
+         return resp;
+     }
 
     @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] : "");
+        
+        try {
+            if (request == null || request.getDeviceIds() == null || request.getDeviceIds().isEmpty() || 
+                request.getTimeValues() == null || request.getTimeValues().isEmpty()) {
+                return resp;
+            }
+            
+            // 设置能源类型名称
+            if (request.getEnergyTypeId() != null) {
+                long id = request.getEnergyTypeId();
+                resp.setEnergyTypeName(id >= 1 && id <= 3 ? ENERGY_TYPE_NAMES[(int) id] : "");
+            }
+            
+            // 设置时间维度
+            resp.setDimension(request.getTimeDimension());
+            
+            // 为每个设备查询对比数据
+            List<EmsCompareSeriesItemVO> series = new ArrayList<>();
+            
+            for (String deviceId : request.getDeviceIds()) {
+                EmsCompareSeriesItemVO seriesItem = getDeviceCompareData(deviceId, request.getEnergyTypeId(), 
+                                                                       request.getTimeDimension(), request.getTimeValues());
+                if (seriesItem != null) {
+                    series.add(seriesItem);
+                }
+            }
+            
+            resp.setSeries(series);
+            
+        } catch (Exception e) {
+            System.err.println("Error getting compare data: " + e.getMessage());
+        }
+        
+        return resp;
+    }
+    
+    /**
+     * 获取设备对比数据
+     */
+    private EmsCompareSeriesItemVO getDeviceCompareData(String deviceId, Long energyTypeId, String timeDimension, List<String> timeValues) {
+        try (Connection conn = mysqlDataSource.getConnection()) {
+            // 获取设备名称
+            String deviceName = getDeviceName(deviceId, conn);
+            
+            List<EmsCompareValueVO> values = new ArrayList<>();
+            
+            // 为每个时间点查询数据
+            for (String timeValue : timeValues) {
+                String[] timeRange = getTimeRange(timeDimension, timeValue);
+                BigDecimal usage = queryDeviceEnergyForCompare(deviceId, energyTypeId, timeRange[0], timeRange[1], conn);
+                
+                EmsCompareValueVO valueItem = new EmsCompareValueVO();
+                valueItem.setTimeValue(timeValue);
+                valueItem.setUsage(usage.setScale(2, RoundingMode.HALF_UP));
+                values.add(valueItem);
+            }
+            
+            EmsCompareSeriesItemVO seriesItem = new EmsCompareSeriesItemVO();
+            seriesItem.setDeviceId(deviceId);
+            seriesItem.setDeviceName(deviceName);
+            seriesItem.setValues(values);
+            
+            return seriesItem;
+            
+        } catch (Exception e) {
+            System.err.println("Error getting device compare data: " + e.getMessage());
+            return null;
+        }
+    }
+    
+    /**
+     * 获取设备名称
+     */
+    private String getDeviceName(String deviceId, Connection conn) {
+        try {
+            String sql = "SELECT name FROM ems_device WHERE id = ?";
+            try (PreparedStatement stmt = conn.prepareStatement(sql)) {
+                stmt.setString(1, deviceId);
+                try (ResultSet rs = stmt.executeQuery()) {
+                    if (rs.next()) {
+                        return rs.getString("name");
+                    }
+                }
+            }
+        } catch (Exception e) {
+            System.err.println("Error getting device name: " + e.getMessage());
+        }
+        return "未知设备";
+    }
+    
+    /**
+     * 查询设备能耗(用于对比)
+     */
+    private BigDecimal queryDeviceEnergyForCompare(String deviceId, Long energyTypeId, String startTime, String endTime, Connection conn) {
+        try {
+            StringBuilder sql = new StringBuilder();
+            sql.append("SELECT SUM(eed.end_value - eed.start_value) as total_usage ");
+sql.append("FROM ems_energy_consumption_device_data eed ");
+        if (energyTypeId != null) {
+            sql.append("INNER JOIN ems_device ed ON eed.device_id = ed.id ");
+            sql.append("INNER JOIN ems_product_energy_type epe ON ed.product_id = epe.product_id ");
         }
-        if (request != null) resp.setDimension(request.getTimeDimension());
+        sql.append("WHERE eed.device_id = ? ");
+            sql.append("AND eed.start_time >= ? AND eed.end_time <= ? ");
+            
+            if (energyTypeId != null) {
+                sql.append("AND epe.energy_type = ? ");
+            }
+            
+            try (PreparedStatement stmt = conn.prepareStatement(sql.toString())) {
+                int paramIndex = 1;
+                stmt.setString(paramIndex++, deviceId);
+                stmt.setString(paramIndex++, startTime);
+                stmt.setString(paramIndex++, endTime);
+                
+                if (energyTypeId != null) {
+                    stmt.setLong(paramIndex++, energyTypeId);
+                }
+                
+                try (ResultSet rs = stmt.executeQuery()) {
+                    if (rs.next()) {
+                        double usage = rs.getDouble("total_usage");
+                        if (!rs.wasNull()) {
+                            return new BigDecimal(usage);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            System.err.println("Error querying device energy for compare: " + e.getMessage());
+        }
+        
+        return BigDecimal.ZERO;
+    }
+
+    @Override
+    public EmsAverageResponse getAverage(Long projectId, String timeDimension, String timeValue, Long energyTypeId) {
+        EmsAverageResponse resp = new EmsAverageResponse();
+        resp.setDimension(timeDimension);
+        resp.setTimeValue(timeValue);
+
+        try {
+            // 获取项目下的所有设备
+            List<String> deviceIds = getDeviceIdsByProject(projectId, energyTypeId);
+            if (deviceIds.isEmpty()) {
+                return resp;
+            }
+
+            // 查询总能耗
+            String[] timeRange = getTimeRange(timeDimension, timeValue);
+            BigDecimal totalUsage = queryTotalEnergyConsumption(deviceIds, timeRange[0], timeRange[1], energyTypeId);
+
+            // 获取项目面积和人口信息
+            Map<String, BigDecimal> areaAndPopulation = getProjectAreaAndPopulation(projectId);
+            BigDecimal totalArea = areaAndPopulation.get("area");
+            BigDecimal totalPopulation = areaAndPopulation.get("population");
+
+            // 计算单位面积能耗和人均能耗
+            BigDecimal areaUsage = BigDecimal.ZERO;
+            BigDecimal perCapitaUsage = BigDecimal.ZERO;
+
+            if (totalArea.compareTo(BigDecimal.ZERO) > 0) {
+                areaUsage = totalUsage.divide(totalArea, 2, RoundingMode.HALF_UP);
+            }
+
+            if (totalPopulation.compareTo(BigDecimal.ZERO) > 0) {
+                perCapitaUsage = totalUsage.divide(totalPopulation, 2, RoundingMode.HALF_UP);
+            }
+
+            // 设置单位
+            String unit = "kWh";
+            if (energyTypeId != null) {
+                unit = ENERGY_UNITS.getOrDefault(energyTypeId, "kWh");
+            }
+
+            resp.setAreaUsage(areaUsage);
+            resp.setPerCapitaUsage(perCapitaUsage);
+            resp.setUnit(unit);
+
+        } catch (Exception e) {
+            System.err.println("Error getting average data: " + e.getMessage());
+        }
+
         return resp;
     }
 }

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

@@ -1,30 +0,0 @@
-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 失效
-    }
-}

+ 174 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsDeviceEventServiceImpl.java

@@ -0,0 +1,174 @@
+package com.usky.ems.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.ems.domain.EmsDevice;
+import com.usky.ems.domain.EmsDeviceEvent;
+import com.usky.ems.domain.EmsGateway;
+import com.usky.ems.domain.EmsSpace;
+import com.usky.ems.mapper.EmsDeviceEventMapper;
+import com.usky.ems.mapper.EmsDeviceMapper;
+import com.usky.ems.mapper.EmsGatewayMapper;
+import com.usky.ems.service.EmsDeviceEventService;
+import com.usky.ems.service.EmsSpaceService;
+import com.usky.ems.service.vo.DeviceEventDTO;
+import com.usky.ems.service.vo.DeviceEventVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 设备事件服务实现(全部逻辑在 Service 层)
+ */
+@Service
+public class EmsDeviceEventServiceImpl implements EmsDeviceEventService {
+
+    @Autowired
+    private EmsSpaceService emsSpaceService;
+
+    @Autowired
+    private EmsDeviceMapper emsDeviceMapper;
+
+    @Autowired
+    private EmsGatewayMapper emsGatewayMapper;
+
+    @Autowired
+    private EmsDeviceEventMapper emsDeviceEventMapper;
+
+    @Override
+    public List<DeviceEventVO> list(DeviceEventDTO dto) {
+        if (dto == null || dto.getInstallationLocation() == null) {
+            throw new BusinessException("安装位置不存在");
+        }
+
+        EmsSpace space = emsSpaceService.getById(dto.getInstallationLocation());
+        if (space == null) {
+            throw new BusinessException("安装位置不存在");
+        }
+
+        // 1) 递归获取所有子空间 + 本空间
+        List<EmsSpace> spaceList = new ArrayList<>(emsSpaceService.recursiveAllChildrenNode(dto.getInstallationLocation()));
+        spaceList.add(space);
+
+        List<Long> spaceIds = spaceList.stream()
+                .map(EmsSpace::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        Map<Long, String> spaceMap = spaceList.stream()
+                .filter(s -> s.getId() != null)
+                .collect(Collectors.toMap(EmsSpace::getId, EmsSpace::getName, (a, b) -> a));
+
+        // 2) 根据 deviceType 取设备/网关列表及其名称、空间映射
+        Integer deviceType = dto.getDeviceType();
+        if (deviceType == null || (deviceType != 1 && deviceType != 2)) {
+            throw new BusinessException("设备类型错误");
+        }
+
+        List<String> ids;
+        Map<String, String> nameMap;
+        Map<String, Long> spaceIdMap;
+
+        if (deviceType == 1) {
+            List<EmsDevice> devices = emsDeviceMapper.selectList(new LambdaQueryWrapper<EmsDevice>()
+                    .in(EmsDevice::getInstallationLocation, spaceIds));
+            if (devices == null || devices.isEmpty()) {
+                return Collections.emptyList();
+            }
+            ids = devices.stream().map(EmsDevice::getId).filter(Objects::nonNull).collect(Collectors.toList());
+            nameMap = devices.stream().filter(d -> d.getId() != null)
+                    .collect(Collectors.toMap(EmsDevice::getId, EmsDevice::getName, (a, b) -> a));
+            spaceIdMap = devices.stream().filter(d -> d.getId() != null)
+                    .collect(Collectors.toMap(EmsDevice::getId, EmsDevice::getInstallationLocation, (a, b) -> a));
+        } else {
+            List<EmsGateway> gateways = emsGatewayMapper.selectList(new LambdaQueryWrapper<EmsGateway>()
+                    .in(EmsGateway::getSpaceId, spaceIds));
+            if (gateways == null || gateways.isEmpty()) {
+                return Collections.emptyList();
+            }
+            ids = gateways.stream().map(EmsGateway::getId).filter(Objects::nonNull).collect(Collectors.toList());
+            nameMap = gateways.stream().filter(g -> g.getId() != null)
+                    .collect(Collectors.toMap(EmsGateway::getId, EmsGateway::getName, (a, b) -> a));
+            spaceIdMap = gateways.stream().filter(g -> g.getId() != null)
+                    .collect(Collectors.toMap(EmsGateway::getId, EmsGateway::getSpaceId, (a, b) -> a));
+        }
+
+        if (ids.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        // 3) 查事件表 ems_device_event
+        LambdaQueryWrapper<EmsDeviceEvent> eventWrapper = new LambdaQueryWrapper<>();
+        eventWrapper.eq(EmsDeviceEvent::getDeviceType, deviceType)
+                .in(EmsDeviceEvent::getDeviceId, ids);
+
+        if (dto.getStartTime() != null && dto.getEndTime() != null) {
+            eventWrapper.between(EmsDeviceEvent::getStartTime, dto.getStartTime(), dto.getEndTime());
+        } else if (dto.getStartTime() != null) {
+            eventWrapper.ge(EmsDeviceEvent::getStartTime, dto.getStartTime());
+        } else if (dto.getEndTime() != null) {
+            eventWrapper.le(EmsDeviceEvent::getStartTime, dto.getEndTime());
+        }
+
+        List<EmsDeviceEvent> eventList = emsDeviceEventMapper.selectList(eventWrapper);
+        if (eventList == null || eventList.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        List<DeviceEventVO> result = new ArrayList<>(eventList.size());
+        for (EmsDeviceEvent deviceEvent : eventList) {
+            DeviceEventVO vo = new DeviceEventVO();
+            vo.setDeviceId(deviceEvent.getDeviceId());
+            vo.setDeviceName(nameMap.get(deviceEvent.getDeviceId()));
+            vo.setContent(deviceEvent.getContent());
+            Long sid = spaceIdMap.get(deviceEvent.getDeviceId());
+            vo.setInstallationLocation(sid == null ? null : spaceMap.get(sid));
+            vo.setEventType(deviceEvent.getEventType());
+            vo.setStartTime(deviceEvent.getStartTime());
+            vo.setIdentifier(deviceEvent.getIdentifier());
+            vo.setDuration(formatDuration(deviceEvent.getStartTime(), now));
+            result.add(vo);
+        }
+
+        result.sort(Comparator.comparing(DeviceEventVO::getDeviceName,
+                Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
+
+        return result;
+    }
+
+    private String formatDuration(LocalDateTime start, LocalDateTime end) {
+        if (start == null || end == null) {
+            return null;
+        }
+        Duration duration = Duration.between(start, end);
+        long minutes = duration.toMinutes();
+        if (minutes < 0) {
+            minutes = 0;
+        }
+        if (minutes >= 1440L) {
+            long days = minutes / 1440L;
+            long hours = (minutes % 1440L) / 60L;
+            long mins = minutes % 60L;
+            return days + "天" + hours + "小时" + mins + "分钟";
+        }
+        if (minutes >= 60L) {
+            long hours = minutes / 60L;
+            long mins = minutes % 60L;
+            return hours + "小时" + mins + "分钟";
+        }
+        return minutes + "分钟";
+    }
+}
+

+ 272 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsDeviceReportServiceImpl.java

@@ -0,0 +1,272 @@
+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.enums.TemporalTypeEnum;
+import com.usky.ems.mapper.EmsDeviceFunctionMapper;
+import com.usky.ems.mapper.EmsDeviceMapper;
+import com.usky.ems.service.EmsDeviceReportService;
+import com.usky.ems.service.EmsSpaceService;
+import com.usky.ems.service.vo.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 设备能耗报表相关服务实现(全部逻辑在 Service 层,Mapper 仅用标准 CRUD)
+ */
+@Service
+public class EmsDeviceReportServiceImpl implements EmsDeviceReportService {
+
+    @Autowired
+    private EmsSpaceService emsSpaceService;
+
+    @Autowired
+    private EmsDeviceMapper emsDeviceMapper;
+
+    @Autowired
+    private EmsDeviceFunctionMapper emsDeviceFunctionMapper;
+
+    @Override
+    public List<DeviceEnergyReportItemVO> showEnergyReportDevices(DeviceEnergyReportRequest dto) {
+        if (dto == null || dto.getProjectId() == null) {
+            return Collections.emptyList();
+        }
+
+        // 1. 授权空间
+        List<Long> spaceIds = emsSpaceService.getAuthorizedSpaceIds(dto.getSpaceId());
+
+        // 2. 在 Service 层用 LambdaQueryWrapper 查 ems_device
+        LambdaQueryWrapper<EmsDevice> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(EmsDevice::getProjectId, dto.getProjectId());
+
+        if (dto.getProductIdList() != null && !dto.getProductIdList().isEmpty()) {
+            wrapper.in(EmsDevice::getProductId, dto.getProductIdList());
+        }
+        if (dto.getDeviceSystem() != null) {
+            wrapper.eq(EmsDevice::getDeviceSystem, dto.getDeviceSystem());
+        }
+        if (spaceIds != null && !spaceIds.isEmpty()) {
+            wrapper.and(w -> w.in(EmsDevice::getInstallationLocation, spaceIds)
+                    .or().in(EmsDevice::getMonitoringLocation, spaceIds));
+        }
+        if (dto.getGatewayId() != null && !dto.getGatewayId().isEmpty()) {
+            wrapper.eq(EmsDevice::getGatewayId, dto.getGatewayId());
+        }
+        if (dto.getChannelId() != null) {
+            wrapper.eq(EmsDevice::getChannelId, dto.getChannelId());
+        }
+
+        List<EmsDevice> devices = emsDeviceMapper.selectList(wrapper);
+        if (devices == null || devices.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        // 3. 转为 List<DeviceEnergyReportItemVO>
+        List<DeviceEnergyReportItemVO> deviceList = devices.stream()
+                .map(this::deviceToVo)
+                .collect(Collectors.toList());
+
+        // 4. 按 identifier 过滤:只保留在 ems_device_function 中存在该 identifier 的设备
+        if (dto.getIdentifier() != null && !dto.getIdentifier().isEmpty()) {
+            List<String> deviceIds = deviceList.stream()
+                    .map(DeviceEnergyReportItemVO::getId)
+                    .filter(Objects::nonNull)
+                    .filter(id -> !id.isEmpty())
+                    .collect(Collectors.toList());
+
+            if (!deviceIds.isEmpty()) {
+                LambdaQueryWrapper<EmsDeviceFunction> funcWrapper = new LambdaQueryWrapper<>();
+                funcWrapper.select(EmsDeviceFunction::getDeviceId)
+                        .in(EmsDeviceFunction::getDeviceId, deviceIds)
+                        .eq(EmsDeviceFunction::getIdentifier, dto.getIdentifier());
+                List<EmsDeviceFunction> deviceFunctions = emsDeviceFunctionMapper.selectList(funcWrapper);
+                if (deviceFunctions != null && !deviceFunctions.isEmpty()) {
+                    Set<String> meetDeviceIds = deviceFunctions.stream()
+                            .map(EmsDeviceFunction::getDeviceId)
+                            .collect(Collectors.toSet());
+                    deviceList = deviceList.stream()
+                            .filter(vo -> meetDeviceIds.contains(vo.getId()))
+                            .collect(Collectors.toList());
+                }
+            }
+        }
+
+        // 5. 按 name 排序(不区分大小写)
+        if (!deviceList.isEmpty()) {
+            deviceList.sort(Comparator.comparing(
+                    DeviceEnergyReportItemVO::getName,
+                    Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)
+            ));
+        }
+
+        return deviceList;
+    }
+
+    @Override
+    public Object showEnergyReportData(DeviceFunctionValueReportDTO dto) {
+        checkParams(dto);
+        TemporalTypeEnum temporalTypeEnum = TemporalTypeEnum.get(dto.getTimeType());
+        LocalDateTime startLocalTime = dto.getStartTime();
+        LocalDateTime endLocalTime = dto.getEndTime();
+        String identifier = getDeviceFunctionIdentifierByEnergyType(dto.getEnergyType());
+        if (temporalTypeEnum != null) {
+            checkDateTime(startLocalTime, endLocalTime, temporalTypeEnum);
+            startLocalTime = temporalTypeEnum.normalizeStart(startLocalTime);
+            endLocalTime = temporalTypeEnum.normalizeEnd(endLocalTime);
+            DeviceFunctionLastValueDTO paramDTO = convertParamDTO(dto, startLocalTime, endLocalTime, temporalTypeEnum, identifier);
+            return asyncProcessData(dto, paramDTO, temporalTypeEnum);
+        } else {
+            DeviceFunctionOneValueDTO paramDTO = convertParamOneLastValueDTO(dto, startLocalTime, endLocalTime, identifier);
+            return getCustomizeEnergyReportVOList(dto, paramDTO, startLocalTime);
+        }
+    }
+
+    private void checkParams(DeviceFunctionValueReportDTO dto) {
+        if (dto == null) {
+            throw new IllegalArgumentException("请求参数不能为空");
+        }
+        if (dto.getProjectId() == null) {
+            throw new IllegalArgumentException("projectId 不能为空");
+        }
+        if (dto.getStartTime() == null || dto.getEndTime() == null) {
+            throw new IllegalArgumentException("开始时间和结束时间不能为空");
+        }
+    }
+
+    private void checkDateTime(LocalDateTime start, LocalDateTime end, TemporalTypeEnum type) {
+        if (start.isAfter(end)) {
+            throw new IllegalArgumentException("开始时间不能晚于结束时间");
+        }
+        // 如需限制时间跨度,可在此扩展逻辑(例如:按日最多查询31天)。
+    }
+
+    private String getDeviceFunctionIdentifierByEnergyType(Integer energyType) {
+        if (energyType == null) {
+            return "total_kwh";
+        }
+        switch (energyType) {
+            case 1:
+                return "total_kwh";
+            case 2:
+                return "total_water";
+            case 3:
+                return "total_gas";
+            default:
+                return "total_kwh";
+        }
+    }
+
+    private DeviceFunctionLastValueDTO convertParamDTO(DeviceFunctionValueReportDTO src,
+                                                       LocalDateTime start,
+                                                       LocalDateTime end,
+                                                       TemporalTypeEnum temporalTypeEnum,
+                                                       String identifier) {
+        DeviceFunctionLastValueDTO dto = new DeviceFunctionLastValueDTO();
+        dto.setProjectId(src.getProjectId());
+        dto.setDeviceIds(src.getDeviceIds());
+        dto.setAttributePointIds(src.getAttributePointIds());
+        dto.setTimeType(temporalTypeEnum != null ? temporalTypeEnum.getCode() : null);
+        dto.setStartTime(start);
+        dto.setEndTime(end);
+        dto.setIdentifier(identifier);
+        return dto;
+    }
+
+    private DeviceFunctionOneValueDTO convertParamOneLastValueDTO(DeviceFunctionValueReportDTO src,
+                                                                  LocalDateTime start,
+                                                                  LocalDateTime end,
+                                                                  String identifier) {
+        DeviceFunctionOneValueDTO dto = new DeviceFunctionOneValueDTO();
+        dto.setProjectId(src.getProjectId());
+        dto.setDeviceIds(src.getDeviceIds());
+        dto.setAttributePointIds(src.getAttributePointIds());
+        dto.setStartTime(start);
+        dto.setEndTime(end);
+        dto.setIdentifier(identifier);
+        return dto;
+    }
+
+    private Object asyncProcessData(DeviceFunctionValueReportDTO origin,
+                                    DeviceFunctionLastValueDTO paramDTO,
+                                    TemporalTypeEnum temporalTypeEnum) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("timeType", temporalTypeEnum != null ? temporalTypeEnum.getCode() : null);
+        result.put("startTime", paramDTO.getStartTime());
+        result.put("endTime", paramDTO.getEndTime());
+
+        // 模拟数据:根据 deviceIds 与 attributePointIds 生成简单的时间序列数据
+        List<Map<String, Object>> items = new ArrayList<>();
+        List<String> deviceIds = paramDTO.getDeviceIds() != null ? paramDTO.getDeviceIds() : Collections.emptyList();
+        List<Long> attrIds = paramDTO.getAttributePointIds() != null ? paramDTO.getAttributePointIds() : Collections.emptyList();
+
+        int index = 0;
+        for (String deviceId : deviceIds) {
+            for (Long attrId : attrIds) {
+                Map<String, Object> item = new HashMap<>();
+                item.put("deviceId", deviceId);
+                item.put("attributePointId", attrId);
+                item.put("identifier", paramDTO.getIdentifier());
+                // 简单模拟一个数值:100 + 10 * index
+                item.put("value", 100 + 10 * index);
+                item.put("unit", "kWh");
+                items.add(item);
+                index++;
+            }
+        }
+
+        result.put("items", items);
+        return result;
+    }
+
+    private Object getCustomizeEnergyReportVOList(DeviceFunctionValueReportDTO origin,
+                                                  DeviceFunctionOneValueDTO paramDTO,
+                                                  LocalDateTime startLocalTime) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("time", startLocalTime);
+
+        List<Map<String, Object>> items = new ArrayList<>();
+        List<String> deviceIds = paramDTO.getDeviceIds() != null ? paramDTO.getDeviceIds() : Collections.emptyList();
+        List<Long> attrIds = paramDTO.getAttributePointIds() != null ? paramDTO.getAttributePointIds() : Collections.emptyList();
+
+        int index = 0;
+        for (String deviceId : deviceIds) {
+            for (Long attrId : attrIds) {
+                Map<String, Object> item = new HashMap<>();
+                item.put("deviceId", deviceId);
+                item.put("attributePointId", attrId);
+                item.put("identifier", paramDTO.getIdentifier());
+                // 单点场景模拟一个数值:200 + 5 * index
+                item.put("value", 200 + 5 * index);
+                item.put("unit", "kWh");
+                items.add(item);
+                index++;
+            }
+        }
+
+        result.put("items", items);
+        return result;
+    }
+
+    private DeviceEnergyReportItemVO deviceToVo(EmsDevice d) {
+        DeviceEnergyReportItemVO vo = new DeviceEnergyReportItemVO();
+        vo.setId(d.getId());
+        vo.setName(d.getName());
+        vo.setNumber(d.getNumber());
+        vo.setProjectId(d.getProjectId());
+        vo.setProductId(d.getProductId());
+        vo.setInstallationLocation(d.getInstallationLocation());
+        vo.setMonitoringLocation(d.getMonitoringLocation());
+        vo.setChannelId(d.getChannelId());
+        vo.setGatewayId(d.getGatewayId());
+        vo.setDeviceSystem(d.getDeviceSystem());
+        vo.setStatus(d.getStatus());
+        vo.setLocation(d.getLocation());
+        return vo;
+    }
+}
+

+ 110 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsEnergyItemCodeServiceImpl.java

@@ -0,0 +1,110 @@
+package com.usky.ems.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.ems.domain.EmsEnergyConsumptionFormula;
+import com.usky.ems.domain.EmsEnergyItemCode;
+import com.usky.ems.mapper.EmsEnergyConsumptionFormulaMapper;
+import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
+import com.usky.ems.service.EmsEnergyItemCodeService;
+import com.usky.ems.service.vo.EnergyItemCodeVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 能源分项编码 服务实现类
+ */
+@Service
+public class EmsEnergyItemCodeServiceImpl
+        extends AbstractCrudService<EmsEnergyItemCodeMapper, EmsEnergyItemCode>
+        implements EmsEnergyItemCodeService {
+
+    @Autowired
+    private EmsEnergyConsumptionFormulaMapper energyConsumptionFormulaMapper;
+
+    @Override
+    public List<EnergyItemCodeVO> querySpaceEnergyItem(Long spaceId) {
+        if (spaceId == null) {
+            return Collections.emptyList();
+        }
+
+        LambdaQueryWrapper<EmsEnergyConsumptionFormula> lambdaQuery =
+                Wrappers.lambdaQuery(EmsEnergyConsumptionFormula.class)
+                        .eq(EmsEnergyConsumptionFormula::getSpaceId, spaceId);
+        List<EmsEnergyConsumptionFormula> formulaList = energyConsumptionFormulaMapper.selectList(lambdaQuery);
+        if (formulaList == null || formulaList.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<String> codes = formulaList.stream()
+                .map(EmsEnergyConsumptionFormula::getItemCode)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+
+        if (codes.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        // 直接从 ems_energy_item_code 表根据编码列表查询
+        List<EmsEnergyItemCode> itemCodeList = this.list(
+                Wrappers.<EmsEnergyItemCode>lambdaQuery()
+                        .in(EmsEnergyItemCode::getCode, codes)
+        );
+        if (itemCodeList == null || itemCodeList.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        // PO -> VO
+        List<EnergyItemCodeVO> itemCodes = itemCodeList.stream().map(po -> {
+            EnergyItemCodeVO vo = new EnergyItemCodeVO();
+            vo.setCode(po.getCode());
+            vo.setParentCode(po.getParentCode());
+            vo.setName(po.getName());
+            vo.setUnit(po.getUnit());
+            vo.setUnitName(po.getUnitName());
+            return vo;
+        }).collect(Collectors.toList());
+
+        // 仅保留配置了公式的分项
+        itemCodes = itemCodes.stream()
+                .filter(vo -> codes.contains(vo.getCode()))
+                .collect(Collectors.toList());
+
+        if (itemCodes.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Map<String, List<EnergyItemCodeVO>> listMap = itemCodes.stream()
+                .collect(Collectors.groupingBy(EnergyItemCodeVO::getParentCode));
+
+        Map<String, EmsEnergyConsumptionFormula> formulaMap = formulaList.stream()
+                .collect(Collectors.toMap(EmsEnergyConsumptionFormula::getItemCode, f -> f, (a, b) -> a));
+
+        List<EnergyItemCodeVO> result = new ArrayList<>(itemCodes);
+
+        for (EnergyItemCodeVO codeVO : itemCodes) {
+            List<EnergyItemCodeVO> children = listMap.get(codeVO.getCode());
+            if (children != null && !children.isEmpty()) {
+                codeVO.setChildren(children);
+                result.removeAll(children);
+            }
+
+            EmsEnergyConsumptionFormula formula = formulaMap.get(codeVO.getCode());
+            if (formula != null) {
+                codeVO.setId(formula.getId());
+                codeVO.setFormula(formula.getFormula());
+                if (formula.getName() != null) {
+                    codeVO.setName(formula.getName());
+                }
+            }
+        }
+
+        return result;
+    }
+}
+

+ 70 - 2
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsModelServiceImpl.java

@@ -4,16 +4,22 @@ 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.EnergyTypeWrapperProductVO;
 import com.usky.ems.service.vo.EmsAttributePointVO;
 import com.usky.ems.service.vo.EmsEnergyTypeVO;
 import com.usky.ems.service.vo.EmsGatewayDeviceTreeNode;
 import com.usky.ems.service.vo.EmsModelSaveRequest;
 import com.usky.ems.service.vo.EmsStructureTreeNode;
+import com.usky.ems.service.vo.SimpleProductVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * 基础建模服务实现(结构树、能源类型、建筑/区域/楼层/网关/通道/设备/属性点位 CRUD)
@@ -40,7 +46,7 @@ public class EmsModelServiceImpl implements EmsModelService {
     @Autowired
     private EmsDeviceFunctionMapper emsDeviceFunctionMapper;
     @Autowired
-    private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
+    private DmpProductMapper dmpProductMapper;
 
     private static final int SPACE_TYPE_PROJECT = 1;
     private static final int SPACE_TYPE_REGION = 2;
@@ -125,6 +131,68 @@ public class EmsModelServiceImpl implements EmsModelService {
         return result;
     }
 
+    /**
+     * 展示已关联的类型及其下属产品列表
+     * 这里按 dmp_product.deviceType 进行分组,每个产品作为一个 SimpleProductVO 返回。
+     */
+    @Override
+    public List<EnergyTypeWrapperProductVO> showAssociatedEnergyTypes() {
+        List<DmpProduct> products = dmpProductMapper.selectList(
+                new LambdaQueryWrapper<DmpProduct>()
+                        .eq(DmpProduct::getDeleteFlag, 0)
+        );
+        if (products == null || products.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Map<Integer, List<SimpleProductVO>> energyMaps = products.stream()
+                .collect(Collectors.groupingBy(
+                        DmpProduct::getDeviceType,
+                        Collectors.mapping(item -> new SimpleProductVO(
+                                item.getProductCode(),
+                                item.getProductName()
+                        ), Collectors.toList())
+                ));
+
+        List<EnergyTypeWrapperProductVO> result = new ArrayList<>();
+        energyMaps.forEach((type, list) -> {
+            String typeName = deviceTypeName(type);
+            EnergyTypeWrapperProductVO vo = new EnergyTypeWrapperProductVO(type, typeName);
+            vo.setProductList(list);
+            result.add(vo);
+        });
+
+        result.sort((o1, o2) -> {
+            if (o1.getEnergyType() == null) return -1;
+            if (o2.getEnergyType() == null) return 1;
+            return o1.getEnergyType().compareTo(o2.getEnergyType());
+        });
+        return result;
+    }
+
+    /** 根据 dmp_product.deviceType 获取类型名称 */
+    private String deviceTypeName(Integer deviceType) {
+        if (deviceType == null) {
+            return "";
+        }
+        switch (deviceType) {
+            case 501:
+                return "监控系统";
+            case 502:
+                return "门禁系统";
+            case 503:
+                return "梯控系统";
+            case 504:
+                return "机房系统";
+            case 509:
+                return "环境系统";
+            case 510:
+                return "照明系统";
+            default:
+                return "";
+        }
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createBuilding(EmsModelSaveRequest request) {

+ 647 - 5
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsOverviewServiceImpl.java

@@ -1,14 +1,39 @@
 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.EmsProject;
+import com.usky.ems.domain.EmsSpace;
+import com.usky.ems.domain.EmsProjectDeviceSystem;
+import com.usky.ems.domain.EmsProjectConversionFactor;
+import com.usky.ems.domain.EmsEnergyItemCode;
+import com.usky.ems.mapper.EmsDeviceMapper;
 import com.usky.ems.mapper.EmsProjectMapper;
+import com.usky.ems.mapper.EmsProjectDeviceSystemMapper;
+import com.usky.ems.mapper.EmsProjectConversionFactorMapper;
+import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
+import com.usky.ems.service.EmsModelService;
 import com.usky.ems.service.EmsOverviewService;
+import com.usky.ems.service.EmsSpaceService;
+import com.usky.ems.service.vo.EmsEnergyTypeVO;
+import com.usky.ems.service.vo.EmsOverviewDeviceSystemStatVO;
+import com.usky.ems.service.vo.EmsOverviewEnergyItemVO;
 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.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * 能源总览服务实现
@@ -19,6 +44,24 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
     @Autowired
     private EmsProjectMapper emsProjectMapper;
 
+    @Autowired
+    private EmsProjectDeviceSystemMapper emsProjectDeviceSystemMapper;
+
+    @Autowired
+    private EmsModelService emsModelService;
+
+    @Autowired
+    private EmsDeviceMapper emsDeviceMapper;
+
+    @Autowired
+    private EmsSpaceService emsSpaceService;
+
+    @Autowired
+    private EmsProjectConversionFactorMapper emsProjectConversionFactorMapper;
+
+    @Autowired
+    private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
+
     @Override
     public EmsProjectResponse getProject(Long projectId) {
         EmsProject project;
@@ -33,13 +76,57 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
         }
         EmsProjectResponse resp = new EmsProjectResponse();
         resp.setId(project.getId());
+        resp.setSpaceId(project.getSpaceId());
         resp.setName(project.getName());
-        resp.setCode(project.getAbbreviation());
-        resp.setDescription(project.getIntroduction());
+        resp.setAbbreviation(project.getAbbreviation());
         resp.setArea(project.getArea());
-        resp.setResidentPopulation(project.getResidentPopulation());
-        resp.setCreateTime(project.getCreateTime());
-        resp.setUpdateTime(project.getUpdateTime());
+        resp.setCommonArea(project.getCommonArea());
+        resp.setAirConditionedArea(project.getAirConditionedArea());
+        // 源字段为 Integer,这里转为 BigDecimal 以匹配 VO 定义
+        if (project.getResidentPopulation() != null) {
+            resp.setResidentPopulation(java.math.BigDecimal.valueOf(project.getResidentPopulation()));
+        }
+
+        // 设备系统集合
+        List<EmsProjectDeviceSystem> sysList = emsProjectDeviceSystemMapper.selectList(
+                new LambdaQueryWrapper<EmsProjectDeviceSystem>()
+                        .eq(EmsProjectDeviceSystem::getProjectId, project.getId())
+        );
+        if (sysList != null && !sysList.isEmpty()) {
+            resp.setDeviceSystemList(sysList.stream()
+                    .map(EmsProjectDeviceSystem::getDeviceSystem)
+                    .collect(Collectors.toList()));
+        }
+
+        resp.setProvinceCode(project.getProvinceCode());
+        resp.setCityCode(project.getCityCode());
+        resp.setDistrictCode(project.getDistrictCode());
+        resp.setProvinceName(project.getProvinceName());
+        resp.setCityName(project.getCityName());
+        resp.setDistrictName(project.getDistrictName());
+        resp.setLocation(project.getLocation());
+        resp.setAddress(project.getAddress());
+        resp.setTypeId(project.getTypeId());
+        resp.setTypeName(project.getTypeName());
+        resp.setImage(project.getImage());
+        // imageUrl 视实际文件服务而定,这里暂不拼接,直接沿用 image 字段或留空
+        resp.setImageUrl(project.getImage());
+        resp.setIntroduction(project.getIntroduction());
+        resp.setPlatformName(project.getPlatformName());
+        resp.setLogo(project.getLogo());
+        resp.setLogoUrl(project.getLogo());
+        resp.setLogoMin(project.getLogoMin());
+        resp.setLogoMinUrl(project.getLogoMin());
+        resp.setUpdatedBy(project.getUpdatedBy());
+        resp.setCreatedBy(project.getCreatedBy());
+        if (project.getUpdateTime() != null) {
+            resp.setUpdateTime(project.getUpdateTime().toString());
+        }
+        if (project.getCreateTime() != null) {
+            resp.setCreateTime(project.getCreateTime().toString());
+        }
+        // 单位能耗暂无法从现有表直接计算,先置为空,后续可按业务补充
+        resp.setAreaCoal(null);
         return resp;
     }
 
@@ -50,4 +137,559 @@ public class EmsOverviewServiceImpl implements EmsOverviewService {
         resp.setTimeValue(timeValue);
         return resp;
     }
+
+    @Override
+    public List<EmsOverviewEnergyItemVO> queryOverviewItem(Long projectId) {
+        // 当前实现:直接基于能源类型列表(电/水/气)构造概览条目
+        List<EmsEnergyTypeVO> types = emsModelService.getEnergyTypeList();
+        return types.stream().map(t -> {
+            EmsOverviewEnergyItemVO vo = new EmsOverviewEnergyItemVO();
+            vo.setName(t.getName());
+            vo.setItemCode(t.getCode());
+            vo.setEnergyType(t.getId() != null ? t.getId().intValue() : null);
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<EmsOverviewDeviceSystemStatVO> queryOverviewDeviceInfo(Long projectId) {
+        Long pid = resolveProjectId(projectId);
+        if (pid == null) {
+            return java.util.Collections.emptyList();
+        }
+
+        // 1. 查询项目已配置的设备系统
+        List<EmsProjectDeviceSystem> systemList = emsProjectDeviceSystemMapper.selectList(
+                new LambdaQueryWrapper<EmsProjectDeviceSystem>()
+                        .eq(EmsProjectDeviceSystem::getProjectId, pid)
+        );
+        if (systemList == null || systemList.isEmpty()) {
+            return java.util.Collections.emptyList();
+        }
+        List<Integer> systems = systemList.stream()
+                .map(EmsProjectDeviceSystem::getDeviceSystem)
+                .collect(Collectors.toList());
+
+        // 2. 查询设备:同项目、启用状态、设备系统在上述集合中
+        List<EmsDevice> devices = emsDeviceMapper.selectList(
+                new LambdaQueryWrapper<EmsDevice>()
+                        .eq(EmsDevice::getProjectId, pid)
+                        .eq(EmsDevice::getStatus, 1)
+                        .in(EmsDevice::getDeviceSystem, systems)
+        );
+        java.util.Map<Integer, Long> deviceCountMap = devices.stream()
+                .filter(d -> d.getDeviceSystem() != null)
+                .collect(Collectors.groupingBy(EmsDevice::getDeviceSystem, Collectors.counting()));
+
+        // 3. 组装统计 VO
+        List<EmsOverviewDeviceSystemStatVO> result = new java.util.ArrayList<>();
+        for (EmsProjectDeviceSystem s : systemList) {
+            Integer code = s.getDeviceSystem();
+            EmsOverviewDeviceSystemStatVO vo = new EmsOverviewDeviceSystemStatVO();
+            vo.setDeviceSystem(code);
+            vo.setName(deviceSystemName(code));
+            vo.setValue(deviceCountMap.getOrDefault(code, 0L).intValue());
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 分类能耗统计(模拟数据版本)
+     * 说明:
+     * - 目前不接真实能耗库,仅按固定示例数据做一套完整计算逻辑,保证前端联调正常。
+     * - 返回结构与原系统保持一致:Map<String, Object>。
+     */
+    @Override
+    public Map<String, Object> queryClassificationEnergy(Integer dateType, String itemCode, Integer energyType, Long projectId) {
+        Map<String, Object> result = new HashMap<>();
+
+        // 基础模拟数据:本期/环比/同比能耗(单位:kWh 或折算后的统一单位)
+        BigDecimal consume = new BigDecimal("100.00");
+        BigDecimal sequentialCon = new BigDecimal("80.00");   // 上一期
+        BigDecimal pariPassCon = new BigDecimal("90.00");     // 同期
+
+        // 模拟项目能耗折标系数、单价
+        BigDecimal coal = new BigDecimal("0.70");              // 折标系数(每单位能耗折算标煤)
+        BigDecimal unitPrice = new BigDecimal("1.00");         // 单价(每单位能耗金额)
+
+        // 本期费用 = 本期能耗 * 单价
+        BigDecimal cost = consume.multiply(unitPrice).setScale(2, RoundingMode.UP);
+        BigDecimal sequentialCost = sequentialCon.multiply(unitPrice).setScale(2, RoundingMode.UP);
+        BigDecimal pariPassCost = pariPassCon.multiply(unitPrice).setScale(2, RoundingMode.UP);
+
+        // 标煤量 = 能耗 * 折标系数
+        BigDecimal coalAmount = consume.multiply(coal).setScale(2, RoundingMode.UP);
+        BigDecimal sequentialCoal = sequentialCon.multiply(coal).setScale(2, RoundingMode.UP);
+        BigDecimal pariPassCoal = pariPassCon.multiply(coal).setScale(2, RoundingMode.UP);
+
+        // CO2 排放量 = 标煤量 * 系数 2.4589(固定常量,模拟用)
+        BigDecimal co2Factor = new BigDecimal("2.4589");
+        BigDecimal co2Amount = coalAmount.multiply(co2Factor).setScale(2, RoundingMode.UP);
+        BigDecimal sequentialCo2 = sequentialCoal.multiply(co2Factor).setScale(2, RoundingMode.UP);
+        BigDecimal pariPassCo2 = pariPassCoal.multiply(co2Factor).setScale(2, RoundingMode.UP);
+
+        result.put("consume", consume);
+        result.put("sequentialCon", sequentialCon);
+        result.put("pariPassCon", pariPassCon);
+        result.put("cost", cost);
+        result.put("sequentialCost", sequentialCost);
+        result.put("pariPassCost", pariPassCost);
+        result.put("coalAmount", coalAmount);
+        result.put("sequentialCoal", sequentialCoal);
+        result.put("pariPassCoal", pariPassCoal);
+        result.put("co2Amount", co2Amount);
+        result.put("sequentialCo2", sequentialCo2);
+        result.put("pariPassCo2", pariPassCo2);
+
+        return result;
+    }
+
+    /**
+     * 能耗用能趋势(模拟数据版本)
+     * 参考原系统 queryEnergyTrend 逻辑,仅保留时间维度与同比的结构,数据采用固定规则模拟。
+     */
+    @Override
+    public List<Map<String, Object>> queryEnergyTrend(Integer dateType, String itemCode, Long spaceId) {
+        List<Map<String, Object>> list = new ArrayList<>();
+
+        if (dateType == null || itemCode == null || itemCode.trim().isEmpty()) {
+            return list;
+        }
+
+        // 当前期与去年同期的时间范围
+        LocalDateTime endThisTime;
+        LocalDateTime startThisTime;
+        LocalDateTime endOldTime;
+        LocalDateTime startOldTime;
+
+        if (dateType == 1) {
+            // 日:按小时
+            endThisTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
+        } else if (dateType == 2) {
+            // 月:按天
+            endThisTime = LocalDateTime.of(LocalDate.now().with(TemporalAdjusters.lastDayOfMonth()), LocalTime.MAX);
+        } else if (dateType == 3) {
+            // 年:按月
+            endThisTime = LocalDateTime.of(LocalDate.now().with(TemporalAdjusters.lastDayOfYear()), LocalTime.MAX);
+        } else {
+            return list;
+        }
+
+        startThisTime = getStartTime(dateType, endThisTime);
+        endOldTime = endThisTime.minusYears(1);
+        startOldTime = getStartTime(dateType, endOldTime);
+
+        // 按时间步长循环生成当前期与去年同期的模拟趋势数据
+        LocalDateTime curTime = startThisTime;
+        LocalDateTime oldTime = startOldTime;
+        int index = 0;
+
+        while (!curTime.isAfter(endThisTime)) {
+            Map<String, Object> row = new HashMap<>();
+            // 时间标签:当前期时间
+            row.put("time", curTime.toString());
+            // 当前期用能:示例 100 + index * 5
+            BigDecimal currentValue = BigDecimal.valueOf(100 + index * 5L);
+            // 去年同期用能:示例 80 + index * 5
+            BigDecimal lastYearValue = BigDecimal.valueOf(80 + index * 5L);
+            row.put("currentValue", currentValue);
+            row.put("lastYearValue", lastYearValue);
+            // 去年同期时间标签
+            row.put("lastYearTime", oldTime.toString());
+
+            list.add(row);
+
+            // 根据时间类型推进时间
+            if (dateType == 1) {
+                curTime = curTime.plusHours(1);
+                oldTime = oldTime.plusHours(1);
+            } else if (dateType == 2) {
+                curTime = curTime.plusDays(1);
+                oldTime = oldTime.plusDays(1);
+            } else {
+                curTime = curTime.plusMonths(1);
+                oldTime = oldTime.plusMonths(1);
+            }
+
+            index++;
+        }
+
+        return list;
+    }
+
+    /**
+     * 计算指定时间类型的开始时间
+     * dateType: 1-日(当天 00:00:00)、2-月(当月第一天 00:00:00)、3-年(当年第一天 00:00:00)
+     */
+    private LocalDateTime getStartTime(Integer dateType, LocalDateTime endTime) {
+        if (dateType == null || endTime == null) {
+            return null;
+        }
+        if (dateType == 1) {
+            return LocalDateTime.of(endTime.toLocalDate(), LocalTime.MIN);
+        } else if (dateType == 2) {
+            LocalDate firstDayOfMonth = endTime.toLocalDate().withDayOfMonth(1);
+            return LocalDateTime.of(firstDayOfMonth, LocalTime.MIN);
+        } else if (dateType == 3) {
+            LocalDate firstDayOfYear = endTime.toLocalDate().with(TemporalAdjusters.firstDayOfYear());
+            return LocalDateTime.of(firstDayOfYear, LocalTime.MIN);
+        }
+        return LocalDateTime.of(endTime.toLocalDate(), LocalTime.MIN);
+    }
+
+    /**
+     * 建筑能耗分析(模拟数据版本)
+     * 参考原系统 queryBuildingRanking:按指定时间维度与能耗条目,对建筑/楼层进行能耗排名。
+     * 当前实现:空间信息(建筑/楼层)从 leo.ems_space 表真实查询,仅能耗值使用规则模拟。
+     */
+    @Override
+    public List<Map<String, Object>> queryBuildingRanking(Integer dateType, String itemCode, Long spaceId) {
+        List<Map<String, Object>> resultList = new ArrayList<>();
+
+        if (dateType == null || itemCode == null || itemCode.trim().isEmpty()) {
+            return resultList;
+        }
+
+        // 1. 确定项目及根空间
+        Long projectId = resolveProjectId(null);
+        if (projectId == null) {
+            return resultList;
+        }
+        EmsProject project = emsProjectMapper.selectById(projectId);
+        if (project == null || project.getSpaceId() == null) {
+            return resultList;
+        }
+        Long rootSpaceId = project.getSpaceId();
+
+        // 2. 查询空间列表:
+        // - 未指定 spaceId:取根空间下 type=3 的建筑
+        // - 指定 spaceId:取该空间下 type=4 的楼层
+        List<EmsSpace> spaceList;
+        if (spaceId == null) {
+            spaceList = emsSpaceService.list(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EmsSpace>()
+                            .eq(EmsSpace::getRootId, rootSpaceId)
+                            .eq(EmsSpace::getType, 3) // 3 = 建筑
+            );
+        } else {
+            spaceList = emsSpaceService.list(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EmsSpace>()
+                            .eq(EmsSpace::getParentId, spaceId)
+                            .eq(EmsSpace::getType, 4) // 4 = 楼层
+            );
+        }
+        if (spaceList == null || spaceList.isEmpty()) {
+            return resultList;
+        }
+
+        // 3. 为每个空间模拟能耗值(真实系统中此处应从 TSDB 或能耗统计表获取)
+        BigDecimal base = new BigDecimal("1000");
+        BigDecimal step = new BigDecimal("150");
+        BigDecimal factor;
+        if (dateType == 1) {
+            factor = BigDecimal.ONE;
+        } else if (dateType == 2) {
+            factor = new BigDecimal("1.5");
+        } else if (dateType == 3) {
+            factor = new BigDecimal("2.0");
+        } else {
+            factor = BigDecimal.ONE;
+        }
+
+        for (int i = 0; i < spaceList.size(); i++) {
+            EmsSpace space = spaceList.get(i);
+            if (space == null) {
+                continue;
+            }
+            Map<String, Object> map = new HashMap<>();
+            map.put("name", space.getName());
+            BigDecimal value = base.subtract(step.multiply(BigDecimal.valueOf(i))).multiply(factor);
+            map.put("value", value);
+            resultList.add(map);
+        }
+
+        // 4. 按 value 从大到小排序,形成能耗排名
+        resultList.sort((m1, m2) -> {
+            BigDecimal v1 = (BigDecimal) m1.get("value");
+            BigDecimal v2 = (BigDecimal) m2.get("value");
+            if (v1 == null && v2 == null) return 0;
+            if (v1 == null) return 1;
+            if (v2 == null) return -1;
+            return v2.compareTo(v1);
+        });
+
+        return resultList;
+    }
+
+    /**
+     * 单位综合能耗、综合累计能耗、分类能耗占比(总览顶部)
+     * - 能源类型来自 EmsModelService.getEnergyTypeList(电/水/气)
+     * - 折算系数来自 leo.ems_project_conversion_factor(优先取项目专属,其次 project_id=0 的公共配置)
+     * - 真实 TSDB 能耗数据暂不可用,当前按规则模拟各能源类型的能耗值。
+     */
+    @Override
+    public Map<String, Object> queryOverviewTop(Integer dateType, Long projectId) {
+        Map<String, Object> resultMap = new HashMap<>();
+        if (dateType == null) {
+            return resultMap;
+        }
+
+        // 时间范围,目前仅用于保持与原接口结构一致
+        LocalDateTime endTime = LocalDateTime.now();
+        LocalDateTime startTime = getStartTime(dateType, endTime);
+        if (startTime == null) {
+            return resultMap;
+        }
+
+        Long pid = resolveProjectId(projectId);
+        if (pid == null) {
+            return resultMap;
+        }
+
+        // 项目信息(含面积、根空间 ID)
+        EmsProjectResponse project = getProject(pid);
+        if (project == null || project.getSpaceId() == null) {
+            return resultMap;
+        }
+
+        // 1. 能源类型列表(电/水/气)
+        List<EmsEnergyTypeVO> energyTypes = emsModelService.getEnergyTypeList();
+        if (energyTypes == null || energyTypes.isEmpty()) {
+            return resultMap;
+        }
+
+        List<Integer> typeIds = energyTypes.stream()
+                .filter(t -> t.getId() != null)
+                .map(t -> t.getId().intValue())
+                .collect(Collectors.toList());
+        if (typeIds.isEmpty()) {
+            return resultMap;
+        }
+
+        // 2. 查询折算系数(项目专属 + 公共)
+        List<EmsProjectConversionFactor> factorList = emsProjectConversionFactorMapper.selectList(
+                new LambdaQueryWrapper<EmsProjectConversionFactor>()
+                        .in(EmsProjectConversionFactor::getEnergyType, typeIds)
+                        .in(EmsProjectConversionFactor::getProjectId, pid, 0L)
+        );
+        if (factorList == null || factorList.isEmpty()) {
+            return resultMap;
+        }
+
+        // 优先使用项目专属配置;若没有则退回公共配置
+        Map<Integer, List<EmsProjectConversionFactor>> groupedByType = factorList.stream()
+                .collect(Collectors.groupingBy(EmsProjectConversionFactor::getEnergyType));
+
+        // 3. 模拟各能源类型能耗值(真实实现应从 TSDB 或历史统计表获取)
+        Map<Integer, BigDecimal> consumeMap = new HashMap<>();
+        BigDecimal base = new BigDecimal("1000");
+        BigDecimal step = new BigDecimal("200");
+        BigDecimal factor;
+        if (dateType == 1) {
+            factor = BigDecimal.ONE;
+        } else if (dateType == 2) {
+            factor = new BigDecimal("1.5");
+        } else if (dateType == 3) {
+            factor = new BigDecimal("2.0");
+        } else {
+            factor = BigDecimal.ONE;
+        }
+
+        int index = 0;
+        for (Integer typeId : typeIds) {
+            BigDecimal value = base.subtract(step.multiply(BigDecimal.valueOf(index))).multiply(factor);
+            if (value.compareTo(BigDecimal.ZERO) < 0) {
+                value = BigDecimal.ZERO;
+            }
+            consumeMap.put(typeId, value);
+            index++;
+        }
+
+        BigDecimal totalCon = consumeMap.values().stream()
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        // 4. 计算各能源类型折算标煤与碳排放、占比
+        BigDecimal coalTotal = BigDecimal.ZERO;
+        BigDecimal co2Total = BigDecimal.ZERO;
+        List<Map<String, Object>> ratioList = new ArrayList<>();
+
+        for (EmsEnergyTypeVO typeVO : energyTypes) {
+            if (typeVO.getId() == null) {
+                continue;
+            }
+            Integer typeId = typeVO.getId().intValue();
+            BigDecimal con = consumeMap.get(typeId);
+            if (con == null) {
+                continue;
+            }
+
+            List<EmsProjectConversionFactor> conversionFactorList = groupedByType.get(typeId);
+            if (conversionFactorList == null || conversionFactorList.isEmpty()) {
+                continue;
+            }
+
+            // 先筛出项目专属配置
+            List<EmsProjectConversionFactor> projectFactors = conversionFactorList.stream()
+                    .filter(f -> pid.equals(f.getProjectId()))
+                    .collect(Collectors.toList());
+            List<EmsProjectConversionFactor> effectiveFactors = projectFactors.isEmpty()
+                    ? conversionFactorList
+                    : projectFactors;
+
+            Map<String, BigDecimal> factorMap = effectiveFactors.stream()
+                    .filter(f -> f.getName() != null && f.getValue() != null)
+                    .collect(Collectors.toMap(EmsProjectConversionFactor::getName, EmsProjectConversionFactor::getValue,
+                            (oldVal, newVal) -> oldVal));
+
+            BigDecimal coal = factorMap.get("coal");
+            if (coal == null) {
+                continue;
+            }
+
+            BigDecimal coalAmount = coal.multiply(con).setScale(2, RoundingMode.UP);
+            BigDecimal co2Amount = coalAmount.multiply(new BigDecimal("2.4589")).setScale(2, RoundingMode.UP);
+            coalTotal = coalTotal.add(coalAmount);
+            co2Total = co2Total.add(co2Amount);
+
+            Map<String, Object> ratioMap = new HashMap<>();
+            ratioMap.put("name", typeVO.getName());
+            ratioMap.put("consume", coalAmount);
+            ratioMap.put("totalConsume", totalCon.multiply(coal).setScale(2, RoundingMode.UP));
+            ratioList.add(ratioMap);
+        }
+
+        // 5. 单位综合能耗(每平米标煤量)
+        BigDecimal unitCoal = BigDecimal.ZERO;
+        if (project.getArea() != null
+                && project.getArea().compareTo(BigDecimal.ZERO) != 0
+                && coalTotal.compareTo(BigDecimal.ZERO) != 0) {
+            unitCoal = coalTotal.divide(project.getArea(), 2, RoundingMode.UP);
+        }
+
+        resultMap.put("unitCoal", unitCoal);
+        resultMap.put("ratioList", ratioList);
+        resultMap.put("coalTotal", coalTotal);
+        resultMap.put("co2Total", co2Total);
+        return resultMap;
+    }
+
+    /**
+     * 分项能耗占比
+     * - 分项定义来自 leo.ems_energy_item_code:给定 itemCode 作为父项,按 parent_code 查询下级分项;
+     * - 若无下级分项,则仅返回父项自身;
+     * - 各分项及汇总能耗数值暂按规则模拟。
+     */
+    @Override
+    public List<Map<String, Object>> queryItemRatio(Integer dateType, String itemCode, Long projectId) {
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        if (dateType == null || itemCode == null || itemCode.trim().isEmpty()) {
+            return resultList;
+        }
+
+        Long pid = resolveProjectId(projectId);
+        if (pid == null) {
+            return resultList;
+        }
+
+        // 当前实现中 startTime/spaceId 仅为预留,不参与模拟计算
+        LocalDateTime endTime = LocalDateTime.now();
+        LocalDateTime startTime = getStartTime(dateType, endTime);
+        if (startTime == null) {
+            return resultList;
+        }
+
+        // 查询父分项编码
+        EmsEnergyItemCode parent = emsEnergyItemCodeMapper.selectOne(
+                new LambdaQueryWrapper<EmsEnergyItemCode>()
+                        .eq(EmsEnergyItemCode::getCode, itemCode)
+        );
+        if (parent == null) {
+            return resultList;
+        }
+
+        // 模拟父分项总能耗
+        BigDecimal total = simulateItemConsume(itemCode, dateType, 0);
+
+        // 查询子分项列表(按 parent_code)
+        List<EmsEnergyItemCode> children = emsEnergyItemCodeMapper.selectList(
+                new LambdaQueryWrapper<EmsEnergyItemCode>()
+                        .eq(EmsEnergyItemCode::getParentCode, parent.getCode())
+        );
+
+        if (children == null || children.isEmpty()) {
+            // 无下级分项:仅返回父项自身
+            Map<String, Object> map = new HashMap<>();
+            map.put("name", parent.getName());
+            map.put("consume", total);
+            map.put("total", total);
+            resultList.add(map);
+            return resultList;
+        }
+
+        int index = 0;
+        for (EmsEnergyItemCode child : children) {
+            if (child == null) continue;
+            BigDecimal consume = simulateItemConsume(child.getCode(), dateType, index);
+            Map<String, Object> map = new HashMap<>();
+            map.put("name", child.getName());
+            map.put("consume", consume);
+            map.put("total", total);
+            resultList.add(map);
+            index++;
+        }
+
+        return resultList;
+    }
+
+    /**
+     * 分项能耗模拟函数
+     * 根据 dateType 与 index 返回不同量级的示例值。
+     */
+    private BigDecimal simulateItemConsume(String code, Integer dateType, int index) {
+        BigDecimal base = new BigDecimal("500");
+        BigDecimal step = new BigDecimal("80");
+        BigDecimal factor;
+        if (dateType != null && dateType == 1) {
+            factor = BigDecimal.ONE;
+        } else if (dateType != null && dateType == 2) {
+            factor = new BigDecimal("1.5");
+        } else if (dateType != null && dateType == 3) {
+            factor = new BigDecimal("2.0");
+        } else {
+            factor = BigDecimal.ONE;
+        }
+        BigDecimal value = base.subtract(step.multiply(BigDecimal.valueOf(index))).multiply(factor);
+        if (value.compareTo(BigDecimal.ZERO) < 0) {
+            value = BigDecimal.ZERO;
+        }
+        return value.setScale(2, RoundingMode.UP);
+    }
+
+    private Long resolveProjectId(Long projectId) {
+        if (projectId != null) return projectId;
+        List<EmsProject> list = emsProjectMapper.selectList(null);
+        return list.isEmpty() ? null : list.get(0).getId();
+    }
+
+    /** 根据设备系统编码返回名称(与 dmp_product.deviceType 含义保持一致) */
+    private String deviceSystemName(Integer deviceSystem) {
+        if (deviceSystem == null) return "";
+        switch (deviceSystem) {
+            case 501:
+                return "监控系统";
+            case 502:
+                return "门禁系统";
+            case 503:
+                return "梯控系统";
+            case 504:
+                return "机房系统";
+            case 509:
+                return "环境系统";
+            case 510:
+                return "照明系统";
+            default:
+                return "";
+        }
+    }
 }

+ 16 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectConfigurationServiceImpl.java

@@ -0,0 +1,16 @@
+package com.usky.ems.service.impl;
+
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.ems.domain.EmsProjectConfiguration;
+import com.usky.ems.mapper.EmsProjectConfigurationMapper;
+import com.usky.ems.service.EmsProjectConfigurationService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 项目配置信息服务实现
+ */
+@Service
+public class EmsProjectConfigurationServiceImpl extends AbstractCrudService<EmsProjectConfigurationMapper, EmsProjectConfiguration>
+        implements EmsProjectConfigurationService {
+}
+

+ 63 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectServiceImpl.java

@@ -0,0 +1,63 @@
+package com.usky.ems.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.ems.domain.EmsProject;
+import com.usky.ems.domain.EmsProjectConfiguration;
+import com.usky.ems.mapper.EmsProjectMapper;
+import com.usky.ems.mapper.EmsProjectConfigurationMapper;
+import com.usky.ems.service.EmsProjectService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+/**
+ * 项目服务实现(leo.ems_project)
+ */
+@Service
+public class EmsProjectServiceImpl extends AbstractCrudService<EmsProjectMapper, EmsProject> implements EmsProjectService {
+
+    @Autowired
+    private EmsProjectConfigurationMapper emsProjectConfigurationMapper;
+
+    @Override
+    public Long create(EmsProject project) {
+        LocalDateTime now = LocalDateTime.now();
+        project.setCreateTime(now);
+        project.setUpdateTime(now);
+        this.save(project);
+        return project.getId();
+    }
+
+    @Override
+    public void update(EmsProject project) {
+        if (project.getId() == null) {
+            return;
+        }
+        project.setUpdateTime(LocalDateTime.now());
+        this.updateById(project);
+    }
+
+    @Override
+    public void delete(Long id) {
+        if (id == null) {
+            return;
+        }
+        this.removeById(id);
+    }
+
+    @Override
+    public Boolean getConfigurationsStatus(Long projectId, String name) {
+        if (projectId == null || name == null) {
+            return false;
+        }
+        LambdaQueryWrapper<EmsProjectConfiguration> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(EmsProjectConfiguration::getProjectId, projectId)
+                .eq(EmsProjectConfiguration::getName, name)
+                .last("LIMIT 1");
+        EmsProjectConfiguration cfg = emsProjectConfigurationMapper.selectOne(wrapper);
+        return cfg != null && "1".equals(cfg.getValue());
+    }
+}
+

+ 136 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsSpaceServiceImpl.java

@@ -0,0 +1,136 @@
+package com.usky.ems.service.impl;
+
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.ems.domain.EmsSpace;
+import com.usky.ems.mapper.EmsSpaceMapper;
+import com.usky.ems.service.EmsSpaceService;
+import com.usky.ems.service.vo.EmsSpaceForestNodeVO;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 空间服务实现(leo.ems_space)
+ */
+@Service
+public class EmsSpaceServiceImpl extends AbstractCrudService<EmsSpaceMapper, EmsSpace> implements EmsSpaceService {
+
+    @Override
+    public EmsSpaceForestNodeVO tree() {
+        List<EmsSpace> allSpaces = this.list();
+        if (allSpaces == null || allSpaces.isEmpty()) {
+            return null;
+        }
+
+        // 1. 转为 VO 列表
+        List<EmsSpaceForestNodeVO> allNodes = allSpaces.stream()
+                .map(this::toNode)
+                .collect(Collectors.toList());
+
+        // 2. 按名称排序,尽量保持稳定的展示顺序
+        allNodes.sort(Comparator.comparing(
+                EmsSpaceForestNodeVO::getName,
+                Comparator.nullsFirst(String::compareToIgnoreCase)
+        ));
+
+        // 3. 建立 id -> 节点 映射,方便父子关联
+        Map<Long, EmsSpaceForestNodeVO> idNodeMap = new HashMap<>();
+        for (EmsSpaceForestNodeVO node : allNodes) {
+            if (node.getId() != null) {
+                idNodeMap.put(node.getId(), node);
+            }
+        }
+
+        // 4. 组装父子关系,并收集根节点
+        List<EmsSpaceForestNodeVO> roots = new ArrayList<>();
+        for (EmsSpaceForestNodeVO node : allNodes) {
+            Long parentId = node.getParentId();
+            if (parentId != null && parentId != 0L) {
+                EmsSpaceForestNodeVO parent = idNodeMap.get(parentId);
+                if (parent != null) {
+                    node.setParentSpaceName(parent.getName());
+                    parent.getChildren().add(node);
+                } else {
+                    // 找不到父节点时,视为根节点,避免数据丢失
+                    roots.add(node);
+                }
+            } else {
+                // parentId 为空或为 0,视为根节点
+                roots.add(node);
+            }
+        }
+
+        if (roots.isEmpty()) {
+            return null;
+        }
+
+        // 5. 若只有一个根节点,直接返回;否则构造一个虚拟根节点
+        List<EmsSpaceForestNodeVO> distinctRoots = roots.stream()
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+
+        if (distinctRoots.size() == 1) {
+            return distinctRoots.get(0);
+        }
+
+        EmsSpaceForestNodeVO virtualRoot = new EmsSpaceForestNodeVO();
+        virtualRoot.setId(0L);
+        virtualRoot.setName("ROOT");
+        virtualRoot.setChildren(distinctRoots);
+        return virtualRoot;
+    }
+
+    @Override
+    public List<Long> getAuthorizedSpaceIds(Long spaceId) {
+        if (spaceId == null) {
+            return Collections.emptyList();
+        }
+        return Collections.singletonList(spaceId);
+    }
+
+    @Override
+    public List<EmsSpace> recursiveAllChildrenNode(Long spaceId) {
+        if (spaceId == null) {
+            return Collections.emptyList();
+        }
+        List<EmsSpace> result = new ArrayList<>();
+        collectChildren(spaceId, result);
+        return result;
+    }
+
+    private void collectChildren(Long parentId, List<EmsSpace> out) {
+        List<EmsSpace> children = this.list(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EmsSpace>()
+                .eq(EmsSpace::getParentId, parentId));
+        if (children == null || children.isEmpty()) {
+            return;
+        }
+        out.addAll(children);
+        for (EmsSpace child : children) {
+            if (child != null && child.getId() != null) {
+                collectChildren(child.getId(), out);
+            }
+        }
+    }
+
+    private EmsSpaceForestNodeVO toNode(EmsSpace space) {
+        EmsSpaceForestNodeVO vo = new EmsSpaceForestNodeVO();
+        vo.setId(space.getId());
+        vo.setName(space.getName());
+        vo.setParentId(space.getParentId());
+        vo.setType(space.getType());
+        vo.setRootId(space.getRootId());
+        vo.setPath(space.getPath());
+        vo.setPathName(space.getPathName());
+        vo.setDeep(space.getDeep());
+        return vo;
+    }
+}
+

+ 46 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceEnergyReportItemVO.java

@@ -0,0 +1,46 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+/**
+ * 能耗报表设备项(用于 showEnergyReportDevices 返回结构)
+ */
+@Data
+public class DeviceEnergyReportItemVO {
+
+    /** 设备ID */
+    private String id;
+
+    /** 设备名称 */
+    private String name;
+
+    /** 设备编号 */
+    private String number;
+
+    /** 项目ID */
+    private Long projectId;
+
+    /** 产品ID */
+    private Long productId;
+
+    /** 安装位置(空间ID) */
+    private Long installationLocation;
+
+    /** 监测位置(空间ID) */
+    private Long monitoringLocation;
+
+    /** 通道ID */
+    private Long channelId;
+
+    /** 网关ID */
+    private String gatewayId;
+
+    /** 设备系统 */
+    private Integer deviceSystem;
+
+    /** 状态 */
+    private Integer status;
+
+    /** 位置描述 */
+    private String location;
+}

+ 37 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceEnergyReportRequest.java

@@ -0,0 +1,37 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 设备能耗报表查询请求(EmsDevice 子模块)
+ */
+@Data
+public class DeviceEnergyReportRequest {
+
+    /** 空间ID(可选,用于授权空间过滤) */
+    private Long spaceId;
+
+    /** 产品ID列表(可选,对应 ems_device.product_id 或 dmp_product.id) */
+    private List<Long> productIdList;
+
+    /** 能源类型(可选,自定义约定) */
+    private Integer energyType;
+
+    /** 设备系统(可选,对应 ems_project_device_system.device_system 或自定义枚举) */
+    private Integer deviceSystem;
+
+    /** 项目ID(必填,用于多项目场景区分数据) */
+    private Long projectId;
+
+    /** 网关ID(可选) */
+    private String gatewayId;
+
+    /** 通道ID(可选) */
+    private Long channelId;
+
+    /** 功能标识符(可选,如 total_kwh,用于过滤具备某属性点位的设备) */
+    private String identifier;
+}
+

+ 25 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceEventDTO.java

@@ -0,0 +1,25 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 设备事件查询参数
+ */
+@Data
+public class DeviceEventDTO {
+
+    /** 安装位置(空间ID) */
+    private Long installationLocation;
+
+    /** 设备类型 1:设备 2:网关 */
+    private Integer deviceType;
+
+    /** 查询开始时间 */
+    private LocalDateTime startTime;
+
+    /** 查询结束时间 */
+    private LocalDateTime endTime;
+}
+

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

@@ -0,0 +1,24 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 设备事件展示对象
+ */
+@Data
+public class DeviceEventVO {
+
+    private String deviceId;
+    private String deviceName;
+    private String content;
+    /** 安装位置名称 */
+    private String installationLocation;
+    private Integer eventType;
+    private LocalDateTime startTime;
+    private String identifier;
+    /** 持续时长(中文展示) */
+    private String duration;
+}
+

+ 29 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceFunctionLastValueDTO.java

@@ -0,0 +1,29 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 设备功能点最后值查询参数 DTO(区间统计场景)
+ */
+@Data
+public class DeviceFunctionLastValueDTO {
+
+    private Long projectId;
+
+    private List<String> deviceIds;
+
+    private List<Long> attributePointIds;
+
+    private String timeType;
+
+    private LocalDateTime startTime;
+
+    private LocalDateTime endTime;
+
+    /** 功能标识符,如 total_kwh */
+    private String identifier;
+}
+

+ 27 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceFunctionOneValueDTO.java

@@ -0,0 +1,27 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 设备功能点单点值查询参数 DTO(自定义时间点场景)
+ */
+@Data
+public class DeviceFunctionOneValueDTO {
+
+    private Long projectId;
+
+    private List<String> deviceIds;
+
+    private List<Long> attributePointIds;
+
+    private LocalDateTime startTime;
+
+    private LocalDateTime endTime;
+
+    /** 功能标识符,如 total_kwh */
+    private String identifier;
+}
+

+ 36 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/DeviceFunctionValueReportDTO.java

@@ -0,0 +1,36 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 设备功能值报表查询请求 DTO
+ * 对应能耗报表数据统计接口的入参抽象。
+ */
+@Data
+public class DeviceFunctionValueReportDTO {
+
+    /** 项目ID */
+    private Long projectId;
+
+    /** 能源类型(电/水/气) */
+    private Integer energyType;
+
+    /** 时间维度:D-日 / M-月 / Y-年;对应 TemporalTypeEnum.code */
+    private String timeType;
+
+    /** 开始时间 */
+    private LocalDateTime startTime;
+
+    /** 结束时间 */
+    private LocalDateTime endTime;
+
+    /** 设备ID列表 */
+    private List<String> deviceIds;
+
+    /** 功能点ID列表(属性点位) */
+    private List<Long> attributePointIds;
+}
+

+ 54 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsAverageResponse.java

@@ -0,0 +1,54 @@
+package com.usky.ems.service.vo;
+
+import java.math.BigDecimal;
+
+/**
+ * 平均能耗响应
+ */
+public class EmsAverageResponse {
+    private String dimension;
+    private String timeValue;
+    private BigDecimal areaUsage;
+    private BigDecimal perCapitaUsage;
+    private String unit;
+
+    public String getDimension() {
+        return dimension;
+    }
+
+    public void setDimension(String dimension) {
+        this.dimension = dimension;
+    }
+
+    public String getTimeValue() {
+        return timeValue;
+    }
+
+    public void setTimeValue(String timeValue) {
+        this.timeValue = timeValue;
+    }
+
+    public BigDecimal getAreaUsage() {
+        return areaUsage;
+    }
+
+    public void setAreaUsage(BigDecimal areaUsage) {
+        this.areaUsage = areaUsage;
+    }
+
+    public BigDecimal getPerCapitaUsage() {
+        return perCapitaUsage;
+    }
+
+    public void setPerCapitaUsage(BigDecimal perCapitaUsage) {
+        this.perCapitaUsage = perCapitaUsage;
+    }
+
+    public String getUnit() {
+        return unit;
+    }
+
+    public void setUnit(String unit) {
+        this.unit = unit;
+    }
+}

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

@@ -0,0 +1,20 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+/**
+ * 概览页设备系统统计条目
+ */
+@Data
+public class EmsOverviewDeviceSystemStatVO {
+
+    /** 设备系统编码 */
+    private Integer deviceSystem;
+
+    /** 设备系统名称(如监控系统、门禁系统等) */
+    private String name;
+
+    /** 设备数量 */
+    private Integer value;
+}
+

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

@@ -0,0 +1,20 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+/**
+ * 概览页能源类型条目(类似 EnergyTypeEnum)
+ */
+@Data
+public class EmsOverviewEnergyItemVO {
+
+    /** 名称,例如:电、水、气 */
+    private String name;
+
+    /** 条目编码,例如:electric、water、gas */
+    private String itemCode;
+
+    /** 能源类型编码,例如:1=电、2=水、3=气 */
+    private Integer energyType;
+}
+

+ 94 - 7
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsProjectResponse.java

@@ -1,23 +1,110 @@
 package com.usky.ems.service.vo;
 
+import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 
 import java.math.BigDecimal;
-import java.time.LocalDateTime;
+import java.util.List;
 
 /**
  * 项目信息响应(数据总览 - 获取项目信息)
- * 字段名与 leo.ems_project 一致
  */
 @Data
 public class EmsProjectResponse {
 
+    @ApiModelProperty("自增ID")
     private Long id;
+
+    @ApiModelProperty("空间ID")
+    private Long spaceId;
+
+    @ApiModelProperty("名称")
     private String name;
-    private String code;
-    private String description;
+
+    @ApiModelProperty("简称")
+    private String abbreviation;
+
+    @ApiModelProperty("项目面积")
     private BigDecimal area;
-    private Integer residentPopulation;
-    private LocalDateTime createTime;
-    private LocalDateTime updateTime;
+
+    @ApiModelProperty("公区面积")
+    private BigDecimal commonArea;
+
+    @ApiModelProperty("空调面积")
+    private BigDecimal airConditionedArea;
+
+    @ApiModelProperty("常驻人数")
+    private BigDecimal residentPopulation;
+
+    @ApiModelProperty("设备系统集合")
+    private List<Integer> deviceSystemList;
+
+    @ApiModelProperty("省code")
+    private String provinceCode;
+
+    @ApiModelProperty("市code")
+    private String cityCode;
+
+    @ApiModelProperty("区code")
+    private String districtCode;
+
+    @ApiModelProperty("省")
+    private String provinceName;
+
+    @ApiModelProperty("市")
+    private String cityName;
+
+    @ApiModelProperty("区")
+    private String districtName;
+
+    @ApiModelProperty("地理位置")
+    private String location;
+
+    @ApiModelProperty("项目地址")
+    private String address;
+
+    @ApiModelProperty("项目类型ID")
+    private Integer typeId;
+
+    @ApiModelProperty("项目类型")
+    private String typeName;
+
+    @ApiModelProperty("项目图片")
+    private String image;
+
+    @ApiModelProperty("项目图片url")
+    private String imageUrl;
+
+    @ApiModelProperty("项目简介")
+    private String introduction;
+
+    @ApiModelProperty("平台名称")
+    private String platformName;
+
+    @ApiModelProperty("平台大logo")
+    private String logo;
+
+    @ApiModelProperty("平台大logo地址")
+    private String logoUrl;
+
+    @ApiModelProperty("平台小logo")
+    private String logoMin;
+
+    @ApiModelProperty("平台小logo地址")
+    private String logoMinUrl;
+
+    @ApiModelProperty("更新人")
+    private Long updatedBy;
+
+    @ApiModelProperty("记录更新时间")
+    private String updateTime;
+
+    @ApiModelProperty("创建人")
+    private Long createdBy;
+
+    @ApiModelProperty("记录创建时间")
+    private String createTime;
+
+    @ApiModelProperty("单位能耗")
+    private BigDecimal areaCoal;
 }

+ 44 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsSpaceForestNodeVO.java

@@ -0,0 +1,44 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 空间树节点(对应 leo.ems_space)
+ */
+@Data
+public class EmsSpaceForestNodeVO {
+
+    /** 节点 ID(ems_space.id) */
+    private Long id;
+
+    /** 名称(ems_space.name 或扩展名称) */
+    private String name;
+
+    /** 父节点 ID(ems_space.parent_id) */
+    private Long parentId;
+
+    /** 空间类型:1项目 2区域 3建筑 4楼层 5房间(ems_space.type) */
+    private Integer type;
+
+    /** 根节点 ID(ems_space.root_id) */
+    private Long rootId;
+
+    /** 节点路径(ems_space.path) */
+    private String path;
+
+    /** 节点路径名称(ems_space.path_name) */
+    private String pathName;
+
+    /** 深度(ems_space.deep) */
+    private Integer deep;
+
+    /** 父空间名称(便于前端展示) */
+    private String parentSpaceName;
+
+    /** 子节点列表 */
+    private List<EmsSpaceForestNodeVO> children = new ArrayList<>();
+}
+

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

@@ -0,0 +1,32 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 能源分项编码视图对象
+ */
+@Data
+public class EnergyItemCodeVO {
+
+    private Long id;
+
+    private String code;
+
+    private String parentCode;
+
+    private String name;
+
+    private String unit;
+
+    private String unitName;
+
+    /** 计算公式 */
+    private String formula;
+
+    /** 子分项 */
+    private List<EnergyItemCodeVO> children = new ArrayList<>();
+}
+

+ 28 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EnergyTypeWrapperProductVO.java

@@ -0,0 +1,28 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 能源/设备类型包装视图对象:一个类型下挂载多个产品
+ */
+@Data
+public class EnergyTypeWrapperProductVO {
+
+    /** 类型编码:可表示能源类型或设备类型 */
+    private Integer energyType;
+
+    /** 类型名称,如“电”“水”“气”或“监控系统”等 */
+    private String energyTypeName;
+
+    /** 当前能源类型下的产品/分项列表 */
+    private List<SimpleProductVO> productList = new ArrayList<>();
+
+    public EnergyTypeWrapperProductVO(Integer energyType, String energyTypeName) {
+        this.energyType = energyType;
+        this.energyTypeName = energyTypeName;
+    }
+}
+

+ 28 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/ProductFunctionVO.java

@@ -0,0 +1,28 @@
+package com.usky.ems.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 产品功能标识视图对象
+ * 主要承载 ems_data_upload_shanghai_product 中的一条记录
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ProductFunctionVO {
+
+    /** 记录ID */
+    private Long id;
+
+    /** 产品ID */
+    private Long productId;
+
+    /** 功能标识 */
+    private String identifier;
+
+    /** 自定义标识符 */
+    private String customIdentifier;
+}
+

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

@@ -0,0 +1,22 @@
+package com.usky.ems.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 简单产品视图对象
+ * 在本模块中,用于承载某个能源类型/设备类型下的产品信息(来源于 dmp_product)
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class SimpleProductVO {
+
+    /** 产品标识,这里使用 dmp_product.productCode */
+    private String productId;
+
+    /** 产品名称,这里使用 dmp_product.productName */
+    private String productName;
+}
+

+ 10 - 2
service-sas/service-sas-biz/src/main/java/com/usky/sas/service/impl/SasMapServiceImpl.java

@@ -85,6 +85,10 @@ public class SasMapServiceImpl extends AbstractCrudService<SasMapsMapper, SasMap
     public CommonPage<MapListItem> page(MapPageRequest request) {
         IPage<SasMaps> page = new Page<>(request.getCurrent(), request.getSize());
         LambdaQueryWrapper<SasMaps> wrapper = new LambdaQueryWrapper<>();
+        // 电子地图配置不展示:视频导出防护(1003)、实时电子巡检(1008) 模块的地图
+        wrapper.notIn(SasMaps::getType,
+                SystemTypeCodeEnum.usbalarm.getCode(),
+                SystemTypeCodeEnum.patrol.getCode());
         if (request.getParentId() != null && !request.getParentId().isEmpty()) {
             wrapper.eq(SasMaps::getParentId, request.getParentId());
         }
@@ -329,8 +333,12 @@ public class SasMapServiceImpl extends AbstractCrudService<SasMapsMapper, SasMap
 
     @Override
     public List<MapTreeItem> getMapTree() {
-        // 查询所有地图节点
-        List<SasMaps> maps = this.list();
+        // 电子地图配置不展示:视频导出防护(1003)、实时电子巡检(1008) 模块的地图
+        LambdaQueryWrapper<SasMaps> wrapper = new LambdaQueryWrapper<>();
+        wrapper.notIn(SasMaps::getType,
+                SystemTypeCodeEnum.usbalarm.getCode(),
+                SystemTypeCodeEnum.patrol.getCode());
+        List<SasMaps> maps = this.list(wrapper);
         if (maps == null || maps.isEmpty()) {
             return Collections.emptyList();
         }