69 Комити 424c64ddad ... 5c68fd3496

Аутор SHA1 Порука Датум
  zhaojinyu 5c68fd3496 优化安防应用模块客户端id生成逻辑 пре 3 дана
  hanzhengyi 157e19b1ff Merge branch 'usky-zyj' of uskycloud/usky-modules into master пре 6 дана
  james e7708cda71 优化推送能耗数据到普陀能耗平台程序逻辑,调整推送数据结构 пре 6 дана
  hanzhengyi a0dd47fe60 Merge branch 'fyc-data' of uskycloud/usky-modules into master пре 6 дана
  fuyuchuan 650b67c587 数据中心 пре 6 дана
  hanzhengyi 609f749f02 Merge branch 'fyc-issue' of uskycloud/usky-modules into master пре 1 недеља
  fuyuchuan 68968c1bb2 Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-issue пре 1 недеља
  hanzhengyi 234e539503 Merge branch 'usky-zyj' of uskycloud/usky-modules into master пре 1 недеља
  fuyuchuan 9d4f949f00 代码优化 пре 1 недеља
  fuyuchuan 0feb4230e9 云平台代码提交 пре 1 недеља
  fuyuchuan 01d5b3c8fc 代码优化 пре 1 недеља
  fuyuchuan 1dff4dd673 云平台代码提交 пре 1 недеља
  fuyuchuan 776e034e58 云平台代码提交 пре 1 недеља
  fuyuchuan 8482c12326 Merge branch 'server-165' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-issue пре 1 недеља
  fuyuchuan 9b40fa4299 解决冲突 пре 1 недеља
  fuyuchuan f50be517e2 回退部分代码 пре 1 недеља
  fuyuchuan 47cd30308a 云平台代码提交 пре 1 недеља
  james dad7ce2631 Merge branch 'usky-zyj' into server-165 пре 1 недеља
  james 7df4424947 优化产品信息表-列表,修改产品IdList пре 1 недеља
  james c6ee58f938 Merge branch 'usky-zyj' into server-165 пре 1 недеља
  james 563ef6f12a 优化产品信息表-列表和设备信息表-列表接口,增加产品list查询条件 пре 1 недеља
  hanzhengyi cc362b8cba Merge branch 'usky-ems' of uskycloud/usky-modules into master пре 1 недеља
  james dc0bd09c37 解决冲突 пре 1 недеља
  hanzhengyi aeae33b905 Merge branch 'usky-zyj' of uskycloud/usky-modules into master пре 1 недеља
  james 32572f0487 Merge branch 'usky-ems' into server-165 пре 1 недеља
  james 6b316d4110 修改能源分项编码表字段 пре 1 недеља
  hanzhengyi cddea72e6d Merge branch 'fyc-ems' of uskycloud/usky-modules into master пре 1 недеља
  james a547066ba2 解决冲突 пре 1 недеља
  james d28d00d959 Merge branch 'usky-ems' of http://47.111.81.118:3000/uskycloud/usky-modules into usky-ems пре 1 недеља
  james 764031b7b2 查询ems_device_item_code表中符合要求的list;开发区域设备信息和开发区域设备信息两个接口; пре 1 недеља
  fuyuchuan 66e6c755be 修复报错 пре 1 недеља
  fuyuchuan f29842eb33 Merge branch 'fyc-ems' of uskycloud/usky-modules into usky-ems пре 1 недеља
  fuyuchuan 953ff911d5 解决报错 пре 1 недеља
  fuyuchuan f66e9fa042 能耗接口报错修复、优化 пре 1 недеља
  fuyuchuan 79973180f4 Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-issue пре 1 недеља
  james fa868d81e4 Merge branch 'usky-zyj' into server-165 пре 1 недеља
  james fa1a662192 优化网关配置信息-导出接口,调整按照map结构返回数据 пре 1 недеља
  james fdedb1c5f8 Merge branch 'usky-zyj' into server-165 пре 1 недеља
  james 02de973bf7 1、解决时序数据库插入不存在表报错的问题; пре 1 недеља
  james a48f5c53d0 Merge branch 'usky-zyj' into server-165 пре 1 недеља
  james 388eb1e218 调整所属网关uuid刷选条件逻辑 пре 1 недеља
  james fe1642e5e7 Merge branch 'usky-zyj' into server-165 пре 1 недеља
  james 14c41c71dc 解决发布启动失败的问题 пре 1 недеља
  james c0178bcc07 Merge branch 'usky-zyj' into server-165 пре 1 недеља
  james db5a081802 优化设备信息表-新增和设备信息表-修改两个接口,增加地址码字段值的录入;优化设备信息表-分页和设备信息表-分页-白名单两个接口,增加所属网关的刷选条件;优化网关配置信息-导出接口,调整导出文件格式和导出方式; пре 1 недеља
  fuyuchuan a5a296b7a4 Merge branch 'fyc-ems' of uskycloud/usky-modules into server-165 пре 2 недеља
  fuyuchuan 8633fb644b 能耗分析接口代码提交 пре 2 недеља
  hanzhengyi 3fe30ca9f6 Merge branch 'fyc-cdi' of uskycloud/usky-modules into master пре 2 недеља
  fuyuchuan 7d906a5b0a 人防代码更新,优化部分代码,添加新设备 пре 2 недеља
  james a1cb34bec4 Merge branch 'usky-ems' into server-165 пре 2 недеља
  james 4bdf0a3903 优化能耗类型列表接口,增加能耗类型绑定产品id集合字段取值;优化能耗分项列表接口,增加能耗分项绑定设备uuid集合字段取值; пре 2 недеља
  james b34ddc716e 解决冲突 пре 2 недеља
  james 2b9f5deaa2 开发能耗类型列表、能耗分项列表、产品关联的能源类型和设备关联的能源分项四个接口 пре 2 недеља
  hanzhengyi 60e4456b63 Merge branch 'modules-zjy' of uskycloud/usky-modules into master пре 2 недеља
  fuyuchuan 23e5b25539 Merge branch 'fyc-ems' of uskycloud/usky-modules into server-165 пре 2 недеља
  fuyuchuan b040096f34 Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-ems пре 3 недеља
  fuyuchuan 65b94f36d8 Merge branch 'usky-ems' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-ems пре 3 недеља
  fuyuchuan ce253da0c7 能耗报表接口 пре 3 недеља
  fuyuchuan 441e718cda 能耗接口代码提交 пре 3 недеља
  fuyuchuan 260358b651 能耗报表代码提交口 пре 3 недеља
  fuyuchuan f6ff0de8df 能耗报表分析三个接口代码提交 пре 3 недеља
  fuyuchuan 1a9094dc0f Merge branch 'usky-ems' of http://47.111.81.118:3000/uskycloud/usky-modules into fyc-ems пре 3 недеља
  fuyuchuan 6e1c7c25a4 能耗报表接口代码提交 пре 4 недеља
  fuyuchuan e651c777b0 能耗报表接口代码提交 пре 4 недеља
  fuyuchuan e4aff9199a 报错修复 пре 4 недеља
  fuyuchuan ccee3935df 集成中心代码提交 пре 4 недеља
  fuyuchuan cacb4a038e 云平台集成代码提交 пре 1 месец
  fuyuchuan 9b031df0d7 集成中心代码提交 пре 1 месец
  fuyuchuan 3a530e6955 集成中心代码提交 пре 1 месец
100 измењених фајлова са 7059 додато и 636 уклоњено
  1. 1 1
      pom.xml
  2. 3 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java
  3. 109 10
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java
  4. 93 15
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java
  5. 12 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java
  6. 49 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/GeneratorMonitoringVO.java
  7. 17 0
      service-data/pom.xml
  8. 33 0
      service-data/service-data-api/pom.xml
  9. 96 0
      service-data/service-data-biz/pom.xml
  10. 50 0
      service-data/service-data-biz/src/main/java/com/usky/data/DataApplicationRun.java
  11. 106 0
      service-data/service-data-biz/src/main/java/com/usky/data/MybatisGenerator.java
  12. 222 0
      service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataInterfaceController.java
  13. 21 0
      service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataInterfaceLogController.java
  14. 229 0
      service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataLinkController.java
  15. 179 0
      service-data/service-data-biz/src/main/java/com/usky/data/domain/DataInterface.java
  16. 122 0
      service-data/service-data-biz/src/main/java/com/usky/data/domain/DataInterfaceLog.java
  17. 136 0
      service-data/service-data-biz/src/main/java/com/usky/data/domain/DataLink.java
  18. 16 0
      service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataInterfaceLogMapper.java
  19. 16 0
      service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataInterfaceMapper.java
  20. 34 0
      service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataLinkMapper.java
  21. 34 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/DataInterfaceModel.java
  22. 52 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/DbLinkBaseForm.java
  23. 24 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/DbLinkCreUpForm.java
  24. 24 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/FieldModel.java
  25. 14 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/PaginationDbLink.java
  26. 26 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/ParamModel.java
  27. 16 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/DataInterfaceLogService.java
  28. 49 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/DataInterfaceService.java
  29. 47 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/DataLinkService.java
  30. 20 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataInterfaceLogServiceImpl.java
  31. 291 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataInterfaceServiceImpl.java
  32. 120 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataLinkServiceImpl.java
  33. 421 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/util/InterfaceExecutor.java
  34. 49 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkInfoVO.java
  35. 22 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkListVO.java
  36. 14 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkSelectorListVO.java
  37. 17 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/ListVO.java
  38. 19 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/PageListVO.java
  39. 14 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/PaginationVO.java
  40. 25 0
      service-data/service-data-biz/src/main/resources/bootstrap.yml
  41. 108 0
      service-data/service-data-biz/src/main/resources/doc/index.adoc
  42. 74 0
      service-data/service-data-biz/src/main/resources/logback.xml
  43. 28 0
      service-data/service-data-biz/src/main/resources/mapper/data/DataInterfaceLogMapper.xml
  44. 35 0
      service-data/service-data-biz/src/main/resources/mapper/data/DataInterfaceMapper.xml
  45. 56 0
      service-data/service-data-biz/src/main/resources/mapper/data/DataLinkMapper.xml
  46. 15 0
      service-data/service-data-biz/src/main/resources/smart-doc.json
  47. 9 12
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAnalysisController.java
  48. 60 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsProjectController.java
  49. 58 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsReportController.java
  50. 20 4
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDeviceItemCode.java
  51. 22 6
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsEnergyItemCode.java
  52. 16 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProductEnergyType.java
  53. 10 7
      service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProjectConversionFactor.java
  54. 2 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsAnalysisService.java
  55. 10 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsEnergyItemCodeService.java
  56. 26 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsProjectService.java
  57. 34 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsReportService.java
  58. 254 471
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAnalysisServiceImpl.java
  59. 84 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsEnergyItemCodeServiceImpl.java
  60. 170 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectServiceImpl.java
  61. 1782 7
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsReportServiceImpl.java
  62. 29 29
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EnergyDataServiceImpl.java
  63. 20 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsAveragePointVO.java
  64. 48 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsAverageRequest.java
  65. 23 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsAverageResponseVO.java
  66. 17 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsAverageSpaceItemVO.java
  67. 4 1
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareResponse.java
  68. 22 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsDeviceItemCodeSaveRequest.java
  69. 19 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsProductEnergyTypeSaveRequest.java
  70. 93 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EnergyReportRequest.java
  71. 81 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EnergyReportResponse.java
  72. 50 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/ItemReportRequest.java
  73. 46 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/ItemReportResponse.java
  74. 63 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/SpaceReportRequest.java
  75. 30 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/SpaceReportResponse.java
  76. 3 3
      service-iot/service-iot-biz/src/main/java/com/usky/iot/controller/web/DmpDeviceInfoController.java
  77. 5 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/domain/DmpDeviceInfo.java
  78. 2 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/mapper/DmpProductInfoMapper.java
  79. 2 2
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/DmpDeviceInfoService.java
  80. 100 49
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpDeviceInfoServiceImpl.java
  81. 1 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpProductInfoServiceImpl.java
  82. 11 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/vo/DmpDeviceInfoRequest.java
  83. 7 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/vo/DmpProductInfoRequest.java
  84. 13 0
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/vo/GatewayDeviceExportItemVO.java
  85. 0 10
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/vo/GatewayDeviceInfoRequestVO.java
  86. 10 0
      service-iot/service-iot-biz/src/main/resources/mapper/iot/DmpProductInfoMapper.xml
  87. 11 0
      service-issue/service-issue-biz/pom.xml
  88. 8 7
      service-issue/service-issue-biz/src/main/java/com/usky/issue/IssueApplication.java
  89. 194 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java
  90. 68 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudConfigController.java
  91. 43 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java
  92. 49 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudTriggerEventController.java
  93. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/sys_user.sql
  94. 36 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/CloudAuditEntity.java
  95. 44 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueCloudConfig.java
  96. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueOperationLog.java
  97. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncCursor.java
  98. 38 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncData.java
  99. 32 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDetail.java
  100. 34 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDownQueue.java

+ 1 - 1
pom.xml

@@ -89,7 +89,7 @@
 
         <module>service-rule</module>
 
-        <!--    <module>service-data</module>-->
+            <module>service-data</module>
 
         <module>service-sas</module>
 

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

@@ -171,7 +171,7 @@ public class BaseDataTransferService {
                 vo.setPublishTime(getCurrentTime());
             }
 
-            String imagePath = "D://BSP0-0103.jpg";
+            String imagePath = "D://142.jpg";
             // 将图片文件读取为字节数组
             byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
 
@@ -264,6 +264,8 @@ public class BaseDataTransferService {
             // userIdToName.put(713, 36);
             userIdToName.put(714, 37);
             userIdToName.put(716, 26);
+            // 柴油发电机蓄电池监测传感器
+            userIdToName.put(720, 12);
 
             HashMap<String, Object> map = new HashMap<>();
             map.put("dataPacketID", generateDataPacketID());

+ 109 - 10
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java

@@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Value;
 import javax.annotation.PostConstruct;
 import javax.imageio.ImageIO;
 import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -636,19 +637,70 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         String time = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
 
         List<FloorPlaneVO> result = new ArrayList<>(filteredPlaneList.size());
+        int skipCount = 0;
+
         for (BaseBuildPlane buildPlane : filteredPlaneList) {
-            String planeViewUrl = buildPlane.getPlaneViewUrl();
-            FloorPlaneVO vo = new FloorPlaneVO();
-            checkFileSize(vo, planeViewUrl);
-            fillImageInfo(vo, planeViewUrl);
+            try {
+                String planeViewUrl = buildPlane.getPlaneViewUrl();
 
-            vo.setDataPacketID(generateDataPacketID());
-            vo.setEngineeringID(engineeringId);
-            vo.setFloor(buildPlane.getFloor());
-            vo.setFloorFileID(Long.valueOf(buildPlane.getId()));
-            vo.setPublishTime(time);
+                // ✅ 修复1: 增加详细的处理日志
+                log.info("正在处理楼层平面图:ID={}, Floor={}, URL={}",
+                        buildPlane.getId(), buildPlane.getFloor(), planeViewUrl);
 
-            result.add(vo);
+                FloorPlaneVO vo = new FloorPlaneVO();
+
+                // ✅ 修复2: 单条记录异常不中断整个批次
+                if (StrUtil.isBlank(planeViewUrl)) {
+                    log.warn("楼层平面图URL为空,跳过该记录:ID={}, Floor={}", buildPlane.getId(), buildPlane.getFloor());
+                    skipCount++;
+                    continue;
+                }
+
+                // 读取文件并校验大小
+                checkFileSize(vo, planeViewUrl);
+
+                // ✅ 修复3: 复用已读取的文件数据来获取图片尺寸(避免重复I/O)
+                fillImageInfoWithBytes(vo, planeViewUrl, vo.getFloorFile());
+
+                // 设置基础字段
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setEngineeringID(engineeringId);
+                vo.setFloor(convertFloor(buildPlane.getFloor()));
+                vo.setFloorFileID(Long.valueOf(buildPlane.getId()));
+                vo.setPublishTime(time);
+
+                // ✅ 修复4: 校验关键字段完整性
+                if (vo.getFloorFile() == null || vo.getFloorFile().length == 0) {
+                    log.warn("楼层平面图文件为空,跳过该记录:ID={}, Floor={}", buildPlane.getId(), buildPlane.getFloor());
+                    skipCount++;
+                    continue;
+                }
+
+                // ✅ 修复5: 记录成功处理的详细信息
+                log.info("楼层平面图处理成功:FileID={}, Floor={}, FileName={}, FileSize={}bytes, Pix={}x{}",
+                        vo.getFloorFileID(),
+                        vo.getFloor(),
+                        vo.getFloorFileName(),
+                        vo.getFloorFile().length,
+                        vo.getFilePixWidth(),
+                        vo.getFilePixHeight());
+
+                result.add(vo);
+
+            } catch (BusinessException e) {
+                log.error("楼层平面图业务校验失败,跳过该记录:ID={}, Floor={}, 错误={}",
+                        buildPlane.getId(), buildPlane.getFloor(), e.getMessage());
+                skipCount++;
+            } catch (Exception e) {
+                log.error("楼层平面图处理异常,跳过该记录:ID={}, Floor={}",
+                        buildPlane.getId(), buildPlane.getFloor(), e);
+                skipCount++;
+            }
+        }
+
+        if (skipCount > 0) {
+            log.warn("楼层平面图处理完成:成功{}条,跳过{}条(文件不存在/超限/读取失败等)",
+                    result.size(), skipCount);
         }
 
         return result;
@@ -720,6 +772,21 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         }
     }
 
+    private String convertFloor(String floor) {
+        if (floor == null || floor.trim().isEmpty()) {
+            return floor;
+        }
+        try {
+            int floorNum = Integer.parseInt(floor.trim());
+            if (floorNum < 0) {
+                return "B" + Math.abs(floorNum);
+            }
+        } catch (NumberFormatException e) {
+            log.warn("楼层格式转换失败,原始值: {}", floor);
+        }
+        return floor;
+    }
+
     private String encodeUrl(String url) throws UnsupportedEncodingException {
         if (StrUtil.isBlank(url)) {
             return url;
@@ -753,6 +820,38 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         return protocol + hostAndPort + encodedPath.toString();
     }
 
+    /**
+     * 填充图片信息(使用已读取的字节数组,避免重复I/O)
+     * @param vo 楼层平面图VO
+     * @param imageUrl 图片URL或本地路径(用于提取文件名和后缀)
+     * @param imageBytes 已读取的图片字节数组
+     */
+    private void fillImageInfoWithBytes(FloorPlaneVO vo, String imageUrl, byte[] imageBytes) {
+        if (StrUtil.isBlank(imageUrl)) {
+            return;
+        }
+
+        // 从URL/路径中提取文件名和后缀
+        String fileName = FileUtil.getName(imageUrl);
+        vo.setFloorFileName(FileUtil.mainName(fileName));
+        vo.setFloorFileSuffix(FileUtil.extName(fileName));
+
+        // ✅ 优化:直接从已读取的byte[]获取尺寸,避免重复读取文件
+        try {
+            if (imageBytes != null && imageBytes.length > 0) {
+                ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
+                BufferedImage image = ImageIO.read(bais);
+
+                if (image != null) {
+                    vo.setFilePixWidth(image.getWidth());
+                    vo.setFilePixHeight(image.getHeight());
+                }
+            }
+        } catch (IOException e) {
+            log.error("从字节数组获取图片尺寸失败: {}", imageUrl, e);
+        }
+    }
+
     private void fillImageInfo(FloorPlaneVO vo, String imageUrl) {
         if (StrUtil.isBlank(imageUrl)) {
             return;

+ 93 - 15
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java

@@ -1091,7 +1091,7 @@ public class IotDataTransferService {
 
         try {
 
-            log.info("开始推送集水井水位数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+            log.info("开始推送集水井水位数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
                     deviceType, totalDevices, deviceData.size());
 
             if (deviceData.isEmpty()) {
@@ -1154,6 +1154,92 @@ public class IotDataTransferService {
         }
     }
 
+    /**
+     * 发送发电机蓄电池监测数据(720)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendGeneratorMonitoring(IotDataTransferVO transferVO) {
+        Map<String, Integer> result = new HashMap<>();
+        result.put("successCount", 0);
+        result.put("failureCount", 0);
+
+        if (!validateMqttGateway(transferVO.getUsername())) {
+            return result;
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        long startTime = System.currentTimeMillis();
+        long endTime;
+
+        List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+        Integer deviceType = transferVO.getDeviceType();
+        Integer totalDevices = transferVO.getDevices().size();
+
+        try {
+
+            log.info("开始推送【战时柴油发电机每日启动时长】数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+                    deviceType, totalDevices, deviceData.size());
+
+            if (deviceData.isEmpty()) {
+                log.warn("没有获取到战时柴油发电机每日启动时长数据!设备类型:{}", deviceType);
+                result.put("failureCount", totalDevices);
+                return result;
+            }
+
+            Long engineeringId = transferVO.getEngineeringId();
+            for (JSONObject deviceDataItem : deviceData) {
+                LocalDateTime dataEndTime = parseDataTime(deviceDataItem);
+                if (dataEndTime == null) {
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+                Integer startDuration = deviceDataItem.get("startDuration") == null ? 0 : deviceDataItem.getIntValue("startDuration");
+
+                GeneratorMonitoringVO vo = new GeneratorMonitoringVO();
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setSensorID(deviceId);
+                vo.setEngineeringID(engineeringId);
+                vo.setPublishTime(getCurrentTime());
+                vo.setDataEndTime(dataEndTime);
+                vo.setSensorDate(dataEndTime.toLocalDate());
+                vo.setStartDuration(startDuration);
+                if (deviceDataItem.containsKey("batteryVoltage")) {
+                    vo.setBatteryVoltage(deviceDataItem.getDouble("batteryVoltage"));
+                }
+
+                try {
+                    log.info("【战时柴油发电机每日启动时长】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
+                    sendMqttMessage(MqttTopics.IotInfo.ALTERNATOR_STARTUP_TIME.getTopic(), vo, MqttTopics.IotInfo.ALTERNATOR_STARTUP_TIME.getDesc(), transferVO.getUsername());
+                    result.put("successCount", result.get("successCount") + 1);
+                } catch (Exception e) {
+                    log.warn("设备{}的战时柴油发电机每日启动时长数据推送失败:{}", deviceId, e.getMessage());
+                    result.put("failureCount", result.get("failureCount") + 1);
+                }
+            }
+
+            log.info("战时柴油发电机每日启动时长数据推送完成,设备类型:{},成功:{},失败:{}",
+                    deviceType, result.get("successCount"), result.get("failureCount"));
+
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 1, SecurityUtils.getUsername());
+            return result;
+        } catch (Exception e) {
+            log.error("战时柴油发电机每日启动时长数据推送发生异常", e);
+            result.put("failureCount", transferVO.getDevices().size());
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 0, SecurityUtils.getUsername());
+            return result;
+        }
+    }
+
     /**
      * 同步设备数据
      * @param tenantId 租户ID
@@ -1247,6 +1333,7 @@ public class IotDataTransferService {
                     }
                     result = sendPersonPresence(transferVO);
                     break;
+                case 705:
                 case 704:
                     if (tenantId == 1205) {
                         // 设置默认值,避免空指针
@@ -1271,6 +1358,9 @@ public class IotDataTransferService {
                 case 719:
                     result = sendPersonCount(transferVO);
                     break;
+                case 720:
+                    result = sendGeneratorMonitoring(transferVO);
+                    break;
                 default:
                     log.debug("不支持的设备类型:{}", deviceType);
                     continue;
@@ -1428,8 +1518,7 @@ public class IotDataTransferService {
      * 序列化楼层平面图VO(将 floorFile byte[] 转为 Base64 字符串)
      */
     private String serializeFloorPlaneVO(com.usky.cdi.service.vo.info.FloorPlaneVO vo) {
-        com.alibaba.fastjson.JSONObject jsonObject = new com.alibaba.fastjson.JSONObject();
-
+        Map<String, Object> jsonObject = new HashMap<>();
         jsonObject.put("dataPacketID", vo.getDataPacketID());
         jsonObject.put("engineeringID", vo.getEngineeringID());
         jsonObject.put("floor", vo.getFloor());
@@ -1439,18 +1528,7 @@ public class IotDataTransferService {
         jsonObject.put("filePixWidth", vo.getFilePixWidth());
         jsonObject.put("filePixHeight", vo.getFilePixHeight());
         jsonObject.put("publishTime", vo.getPublishTime());
-
-        // 关键:将 byte[] 转为 Base64 字符串
-        if (vo.getFloorFile() != null) {
-            String base64File = java.util.Base64.getEncoder().encodeToString(vo.getFloorFile());
-            jsonObject.put("floorFile", base64File);
-            log.info("平面图文件转换Base64成功,FileID: {}, 原始大小: {} bytes, Base64长度: {}",
-                    vo.getFloorFileID(), vo.getFloorFile().length, base64File.length());
-        } else {
-            jsonObject.put("floorFile", "");
-            log.warn("平面图文件为空,FileID: {}", vo.getFloorFileID());
-        }
-
+        jsonObject.put("floorFile", vo.getFloorFile());
         Gson gson = new Gson();
         return gson.toJson(jsonObject);
     }

+ 12 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java

@@ -501,7 +501,12 @@ public class DeviceDataQuery {
                         double value = FixedWaterLevelGenerator.getSensorValue(deviceId1);
                         simData.put("sensorValue", formatNumber(value, FORMAT_1_2));
                         break;
-
+                    // case 719:
+                    //     break;
+                    case 720:
+                        simData.put("startDuration", 0);
+                        simData.put("batteryVoltage", randomDouble261_266());
+                        break;
                     default:
                         log.warn("未知设备类型:{},无法生成模拟数据", deviceType);
                         continue;
@@ -515,6 +520,12 @@ public class DeviceDataQuery {
         return simulationList;
     }
 
+    private double randomDouble261_266() {
+        Random random = new Random();
+        int intVal = 2610 + random.nextInt(51);
+        return intVal / 100.0;
+    }
+
     private void addNoiseToNumericFields(JSONObject data, Integer deviceType) {
         Map<String, Double> noiseConfig = getNoiseConfigByDeviceType(deviceType);
 

+ 49 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/GeneratorMonitoringVO.java

@@ -0,0 +1,49 @@
+package com.usky.cdi.service.vo.info;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.usky.cdi.service.vo.base.BaseEnvMonitorPushVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDate;
+
+/**
+ * 发电机蓄电池监测数据
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/6/18
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class GeneratorMonitoringVO extends BaseEnvMonitorPushVO {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 监测日期(必填)
+     * 类型:Date(yyyy-MM-dd)
+     */
+    @JSONField(format = "yyyy-MM-dd")
+    private LocalDate sensorDate;
+
+    /**
+     * 启动时长(必填)
+     * 类型:Int,长度4,单位分钟
+     */
+    private Integer startDuration;
+
+    /**
+     * 蓄电池电压(非必填)
+     * 类型:Float(2,2),单位V
+     */
+    private Double batteryVoltage;
+
+    @Override
+    public Number getSensorValue() {
+        return null;
+    }
+
+    @Override
+    protected void validateSensorValue() {
+
+    }
+}

+ 17 - 0
service-data/pom.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>usky-modules</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-data</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>service-data-biz</module>
+        <module>service-data-api</module>
+  </modules>
+
+</project>

+ 33 - 0
service-data/service-data-api/pom.xml

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

+ 96 - 0
service-data/service-data-biz/pom.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>service-data</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-data-biz</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>common-cloud-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <!-- Pagehelper -->
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>ruoyi-common-swagger</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+
+        <!--Redis依赖-->
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.boot</groupId>-->
+<!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
+<!--        </dependency>-->
+
+<!--        <dependency>-->
+<!--            <groupId>com.usky</groupId>-->
+<!--            <artifactId>usky-common-redis</artifactId>-->
+<!--        </dependency>-->
+
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.2.6.RELEASE</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.github.shalousun</groupId>
+                <artifactId>smart-doc-maven-plugin</artifactId>
+                <version>2.1.1</version>
+                <configuration>
+                    <!--指定生成文档的使用的配置文件,配置文件放在自己的项目中-->
+                    <configFile>./src/main/resources/smart-doc.json</configFile>
+                    <!--指定项目名称-->
+                    <projectName>test</projectName>
+                    <!--                    <excludes>-->
+                    <!--                        <exclude>com.bizmatics:product-service-provider</exclude>-->
+                    <!--                        <exclude>cn.afterturn:easypoi-web</exclude>-->
+                    <!--                    </excludes>-->
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 50 - 0
service-data/service-data-biz/src/main/java/com/usky/data/DataApplicationRun.java

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

+ 106 - 0
service-data/service-data-biz/src/main/java/com/usky/data/MybatisGenerator.java

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

+ 222 - 0
service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataInterfaceController.java

@@ -0,0 +1,222 @@
+package com.usky.data.controller.web;
+
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.data.domain.DataInterface;
+import com.usky.data.service.DataInterfaceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * 数据中心_数据接口表 前端控制器
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@RestController
+@RequestMapping("/dataInterface")
+public class DataInterfaceController {
+
+    @Autowired
+    private DataInterfaceService dataInterfaceService;
+
+    /**
+     * 获取数据接口列表
+     *
+     * @param currentPage 当前页
+     * @param pageSize    每页数量
+     * @param keyword     关键字
+     * @param type        接口类型 1:SQL操作, 2:静态数据, 3:API操作
+     * @param enabledMark 启用状态 0:未启用,1:已启用
+     * @param category    接口分类
+     * @return
+     */
+    @RequestMapping
+    @GetMapping
+    public ApiResult<CommonPage<DataInterface>> getDataInterfaceList(@RequestParam(value = "currentPage", defaultValue = "1") Integer currentPage,
+                                                                     @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize,
+                                                                     @RequestParam(value = "keyword", required = false) String keyword,
+                                                                     @RequestParam(value = "type", required = false) String type,
+                                                                     @RequestParam(value = "enabledMark", required = false) Integer enabledMark,
+                                                                     @RequestParam(value = "category", required = false) String category
+    ) {
+        return ApiResult.success(dataInterfaceService.getDataInterfaceList(currentPage, pageSize, keyword, type, enabledMark, category));
+    }
+
+    /**
+     * 获取接口列表(工作流选择时调用)
+     *
+     * @param currentPage 当前页
+     * @param pageSize    每页数量
+     * @param keyword     关键字
+     * @param type        接口类型 1:静态数据, 2:SQL操作, 3:API操作
+     * @param enabledMark 启用状态 0:未启用,1:已启用
+     * @return
+     */
+    @GetMapping("/getList")
+    public ApiResult<CommonPage<DataInterface>> getWorkflowList(@RequestParam(value = "currentPage", defaultValue = "1") Integer currentPage,
+                                                                @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize,
+                                                                @RequestParam(value = "keyword", required = false) String keyword,
+                                                                @RequestParam(value = "type", required = false) String type,
+                                                                @RequestParam(value = "enabledMark", required = false) Integer enabledMark
+    ) {
+        return ApiResult.success(dataInterfaceService.getWorkflowList(currentPage, pageSize, keyword, type, enabledMark));
+    }
+
+    /**
+     * 获取接口列表下拉框
+     *
+     * @return
+     */
+    @GetMapping("/Selector")
+    public ApiResult<List<DataInterface>> getSelector() {
+        return ApiResult.success(dataInterfaceService.getSelectorList());
+    }
+
+    /**
+     * 获取接口参数列表下拉框
+     *
+     * @param id 主键
+     * @return
+     */
+    @GetMapping("/getParam/{id}")
+    public ApiResult<String> getInterfaceParams(@PathVariable("id") String id) {
+        return ApiResult.success(dataInterfaceService.getInterfaceParameters(id));
+    }
+
+    /**
+     * 获取接口数据
+     *
+     * @param id 主键
+     * @return
+     */
+    @GetMapping("/{id}")
+    public ApiResult<DataInterface> getInterfaceInfo(@PathVariable("id") String id) {
+        return ApiResult.success(dataInterfaceService.getById(id));
+    }
+
+    /**
+     * 添加接口
+     *
+     * @param dataInterface 接口实体
+     * @return
+     */
+    @PostMapping
+    public ApiResult<Boolean> addInterface(@RequestBody DataInterface dataInterface) {
+        return ApiResult.success(dataInterfaceService.addInterface(dataInterface));
+    }
+
+    /**
+     * 修改接口
+     *
+     * @param dataInterface 接口实体
+     * @return
+     */
+    @PutMapping
+    public ApiResult<Boolean> updateInterface(@RequestBody DataInterface dataInterface) {
+        return ApiResult.success(dataInterfaceService.updateInterface(dataInterface));
+    }
+
+    /**
+     * 删除接口
+     *
+     * @param id 主键
+     * @return
+     */
+    @DeleteMapping("/{id}")
+    public ApiResult<Boolean> deleteInterface(@PathVariable("id") Long id) {
+        return ApiResult.success(dataInterfaceService.deleteInterface(id));
+    }
+
+    /**
+     * 更新接口状态
+     *
+     * @param id 主键
+     * @return
+     */
+    @PutMapping("/{id}/Actions/State")
+    public ApiResult<Boolean> updateInterfaceState(@PathVariable("id") String id) {
+        return ApiResult.success(dataInterfaceService.updateInterfaceStatus(id));
+    }
+
+    /**
+     * 测试接口
+     *
+     * @param id     主键
+     * @param params 参数、参数值对象
+     * @return
+     */
+    @PostMapping("/{id}/actions/preview")
+    public ApiResult<?> testInterface(@PathVariable("id") String id, @RequestBody(required = false) Map<String, Object> params) {
+        return dataInterfaceService.testInterface(id, params);
+    }
+
+    /**
+     * 访问接口GET
+     *
+     * @param id     主键
+     * @param params 参数、参数值对象
+     * @return
+     */
+    @GetMapping("/{id}/actions/response")
+    public ApiResult<?> getInterfaceResponse(@PathVariable("id") String id, @RequestParam(required = false) Map<String, String> params) {
+        return dataInterfaceService.getInterfaceResponse(id, params);
+    }
+
+    /**
+     * 访问接口POST
+     *
+     * @param id     主键
+     * @param params 参数、参数值对象
+     * @return
+     */
+    @PostMapping("/{id}/actions/response")
+    public ApiResult<?> postInterfaceResponse(@PathVariable("id") String id, @RequestBody(required = false) Map<String, String> params) {
+        return dataInterfaceService.postInterfaceResponse(id, params);
+    }
+
+    /**
+     * 获取接口分页数据
+     *
+     * @param id         主键
+     * @param pageParams 分页参数
+     * @return
+     */
+    @PostMapping("/{id}/actions/list")
+    public ApiResult<?> getInterfacePageData(@PathVariable("id") String id, @RequestBody Map<String, Object> pageParams) {
+        return dataInterfaceService.getInterfacePageData(id, pageParams);
+    }
+
+    /**
+     * 获取接口详情数据
+     *
+     * @param id         主键
+     * @param pageParams 分页参数
+     * @return
+     */
+    @PostMapping("/{id}/actions/infoByIds")
+    public ApiResult<List<Map<String, Object>>> getInterfaceDetailData(@PathVariable("id") String id, @RequestBody Map<String, Object> pageParams) {
+        return dataInterfaceService.getInterfaceDetailData(id, pageParams);
+    }
+
+    /**
+     * 获取接口字段
+     *
+     * @param id         主键
+     * @param pageParams 参数、参数值
+     * @return
+     */
+    @PostMapping("/{id}/actions/getFields")
+    public ApiResult<List<String>> getInterfaceFields(@PathVariable("id") String id, @RequestBody(required = false) Map<String, Object> pageParams) {
+        return dataInterfaceService.getInterfaceFields(id, pageParams);
+    }
+}
+

+ 21 - 0
service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataInterfaceLogController.java

@@ -0,0 +1,21 @@
+package com.usky.data.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表 前端控制器
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Controller
+@RequestMapping("/dataInterfaceLog")
+public class DataInterfaceLogController {
+
+}
+

+ 229 - 0
service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataLinkController.java

@@ -0,0 +1,229 @@
+package com.usky.data.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.data.domain.DataLink;
+import com.usky.data.model.DbLinkBaseForm;
+import com.usky.data.model.DbLinkCreUpForm;
+import com.usky.data.model.PaginationDbLink;
+import com.usky.data.service.DataLinkService;
+import com.usky.data.vo.*;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 数据源配置控制器
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+@Tag(name = "数据连接", description = "DataSource")
+@RestController
+@RequestMapping("/dataSource")
+public class DataLinkController {
+
+    @Autowired
+    private DataLinkService dataLinkService;
+
+    /**
+     * 获取数据连接下拉框列表
+     */
+    @GetMapping("/selector")
+    @Operation(summary = "获取数据连接下拉框列表")
+    public ApiResult<ListVO<DbLinkSelectorListVO>> selectorList(@RequestParam(value = "dbType") String type) {
+        List<DbLinkListVO> modelAll = new LinkedList<>();
+        List<DataLink> dbLinkList = dataLinkService.getList();
+
+        // 转换为VO列表
+        for (DataLink entity : dbLinkList) {
+            DbLinkListVO model = new DbLinkListVO();
+            BeanUtils.copyProperties(entity, model);
+            modelAll.add(model);
+        }
+
+        // 按数据库类型分组
+        Map<String, List<DbLinkListVO>> groupByType = modelAll.stream()
+                .collect(Collectors.groupingBy(DbLinkListVO::getDbType));
+
+        List<DbLinkSelectorListVO> list = new ArrayList<>();
+        // type为空时返回默认库
+        if (type == null) {
+            DbLinkListVO dbLink = new DbLinkListVO();
+            dbLink.setFullName("默认数据库");
+            dbLink.setId(0);
+            // 默认数据库类型
+            dbLink.setDbType("mysql");
+            DbLinkSelectorListVO defaultDb = new DbLinkSelectorListVO();
+            defaultDb.setFullName("");
+            defaultDb.setChildren(Collections.singletonList(dbLink));
+            list.add(defaultDb);
+        }
+
+        // 添加分组后的数据源
+        for (Map.Entry<String, List<DbLinkListVO>> entry : groupByType.entrySet()) {
+            DbLinkSelectorListVO selector = new DbLinkSelectorListVO();
+            selector.setFullName(entry.getKey());
+            selector.setChildren(entry.getValue());
+            list.add(selector);
+        }
+
+        return ApiResult.success(new ListVO<>(list));
+    }
+
+    /**
+     * 获取数据连接列表
+     */
+    @GetMapping
+    @Operation(summary = "获取数据连接列表")
+    public ApiResult<PageListVO<DbLinkListVO>> getList(@RequestBody PaginationDbLink page) {
+        List<DataLink> data = dataLinkService.getList(
+                page.getCurrentPage(),
+                page.getPageSize(),
+                page.getKeyword()
+        );
+
+        // 转换为VO列表
+        List<DbLinkListVO> jsonToList = new ArrayList<>();
+        for (DataLink entity : data) {
+            DbLinkListVO vo = new DbLinkListVO();
+            BeanUtils.copyProperties(entity, vo);
+            jsonToList.add(vo);
+        }
+
+        // 构建分页信息
+        PaginationVO paginationVO = new PaginationVO();
+        paginationVO.setCurrentPage(page.getCurrentPage());
+        paginationVO.setPageSize(page.getPageSize());
+        paginationVO.setTotalRows(data.size());
+        paginationVO.setTotalPages((int) Math.ceil((double) data.size() / page.getPageSize()));
+
+        return ApiResult.success(new PageListVO<>(jsonToList, paginationVO));
+    }
+
+    /**
+     * 获取数据连接列表分组
+     */
+    @GetMapping("/getGroup")
+    @Operation(summary = "获取数据连接列表分组")
+    public ApiResult<List<Map<String, Object>>> getGroup(@RequestBody PaginationDbLink page) {
+        ApiResult<PageListVO<DbLinkListVO>> result = getList(page);
+        List<DbLinkListVO> voList = result.getData().getList();
+
+        // 按host分组
+        Set<String> hostSet = voList.stream()
+                .sorted(Comparator.comparing(DbLinkListVO::getHost))
+                .map(DbLinkListVO::getHost)
+                .collect(Collectors.toSet());
+
+        List<Map<String, Object>> groupList = new ArrayList<>();
+        for (String host : hostSet) {
+            Map<String, Object> groupMap = new HashMap<>();
+            groupMap.put("host", host);
+            groupMap.put("options", voList.stream()
+                    .filter(vo -> vo.getHost().equals(host))
+                    .toArray());
+            groupList.add(groupMap);
+        }
+
+        return ApiResult.success(groupList);
+    }
+
+    /**
+     * 获取单条数据连接
+     */
+    @GetMapping("/{id}")
+    @Operation(summary = "获取数据连接")
+    @Parameters({
+            @Parameter(name = "id", description = "主键")
+    })
+    public ApiResult<DbLinkInfoVO> get(@PathVariable("id") Integer id) {
+        DataLink entity = dataLinkService.getById(id);
+        if (entity == null) {
+            return ApiResult.error("数据源不存在");
+        }
+        DbLinkInfoVO vo = new DbLinkInfoVO();
+        return ApiResult.success(vo.getDbLinkInfoVO(entity));
+    }
+
+    /**
+     * 新建数据连接
+     */
+    @PostMapping
+    @Operation(summary = "添加数据连接")
+    @Parameters({
+            @Parameter(name = "dbLinkCreUpForm", description = "新建数据连接表单对象", required = true)
+    })
+    public ApiResult<String> create(@RequestBody @Validated DbLinkCreUpForm dbLinkCreUpForm) {
+        DataLink entity = dbLinkCreUpForm.getDbLinkEntity(dbLinkCreUpForm);
+        if (dataLinkService.isExistByFullName(entity.getFullName(), null)) {
+            return ApiResult.error("连接名称已存在");
+        }
+        return ApiResult.success(dataLinkService.save(entity) ? "新增成功" : "新增失败");
+    }
+
+    /**
+     * 更新数据连接
+     */
+    @PutMapping("/{id}")
+    @Operation(summary = "修改数据连接")
+    @Parameters({
+            @Parameter(name = "id", description = "主键", required = true),
+            @Parameter(name = "dbLinkCreUpForm", description = "更新数据连接表单对象", required = true)
+    })
+    public ApiResult<String> update(@PathVariable("id") Integer id, @RequestBody @Validated DbLinkCreUpForm dbLinkCreUpForm) {
+        dbLinkCreUpForm.setId(id);
+        DataLink entity = dbLinkCreUpForm.getDbLinkEntity(dbLinkCreUpForm);
+        if (dataLinkService.isExistByFullName(entity.getFullName(), id)) {
+            throw new BusinessException("连接名称已存在");
+        }
+        if (!dataLinkService.updateById(entity)) {
+            return ApiResult.error("更新失败");
+        }
+        return ApiResult.success("更新成功");
+    }
+
+    /**
+     * 删除数据连接
+     */
+    @DeleteMapping("/{id}")
+    @Operation(summary = "删除数据连接")
+    @Parameters({
+            @Parameter(name = "id", description = "主键", required = true)
+    })
+    public ApiResult<String> delete(@PathVariable("id") Integer id) {
+        DataLink entity = dataLinkService.getById(id);
+        if (entity != null) {
+            dataLinkService.removeById(entity);
+            return ApiResult.success("删除成功");
+        }
+        return ApiResult.error("数据源不存在");
+    }
+
+    /**
+     * 测试连接
+     */
+    @PostMapping("/actions/test")
+    @Operation(summary = "测试连接")
+    @Parameters({
+            @Parameter(name = "dbLinkBaseForm", description = "数据连接参数", required = true)
+    })
+    public ApiResult<String> test(@RequestBody DbLinkBaseForm dbLinkBaseForm) throws Exception {
+        boolean data = dataLinkService.testDbConnection(dbLinkBaseForm.getDbLinkEntity(dbLinkBaseForm));
+        if (data) {
+            return ApiResult.success("连接成功");
+        } else {
+            return ApiResult.error("连接失败");
+        }
+    }
+}
+

+ 179 - 0
service-data/service-data-biz/src/main/java/com/usky/data/domain/DataInterface.java

@@ -0,0 +1,179 @@
+package com.usky.data.domain;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+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 javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * <p>
+ * 数据中心_数据接口表
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DataInterface implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 接口名称
+     */
+    @NotBlank(message = "接口名称不能为空")
+    private String fullName;
+
+    /**
+     * 接口编码
+     */
+    @NotBlank(message = "接口编码不能为空")
+    private String enCode;
+
+    /**
+     * 接口分类
+     */
+    private String category;
+
+    /**
+     * 接口类型(1:静态数据, 2:SQL操作, 3:API操作)
+     */
+    @NotNull(message = "接口类型不能为空")
+    private Integer type;
+
+    /**
+     * 操作类型()
+     */
+    private Integer action;
+
+    /**
+     * 分页(0-禁用,1-启用)
+     */
+    private Integer hasPage;
+
+    /**
+     * 后置接口(0-否 1-是)
+     */
+    private Integer isPostposition;
+
+    /**
+     * 字段JSON
+     */
+    private String fieldJson;
+
+    /**
+     * 数据配置JSON
+     */
+    @TableField("config_json")
+    @NotBlank(message = "数据配置JSON不能为空")
+    private String dataConfigJson;
+
+    /**
+     * 数据统计json
+     */
+    @TableField("count_json")
+    @NotBlank(message = "数据统计json不能为空")
+    private String dataCountJson;
+
+    /**
+     * 数据回显json
+     */
+    @TableField("echo_json")
+    @NotBlank(message = "数据回显json不能为空")
+    private String dataEchoJson;
+
+    /**
+     * 数据处理json
+     */
+    @TableField("js_json")
+    @NotBlank(message = "数据处理json不能为空")
+    private String dataJsJson;
+
+    /**
+     * 参数配置JSON
+     */
+    private String parameterJson;
+
+    /**
+     * 异常验证JSON
+     */
+    private String exceptionJson;
+
+    /**
+     * 描述或说明
+     */
+    private String description;
+
+    /**
+     * 启用状态(0-禁用,1-启用)
+     */
+    private Integer enabledMark;
+
+    /**
+     * 排序
+     */
+    private Long sortCode;
+
+    /**
+     * 创建者
+     */
+    @NotBlank(message = "创建人不能为空")
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    @NotNull(message = "创建时间不能为空")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    @NotBlank(message = "更新人不能为空")
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    @NotNull(message = "更新时间不能为空")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除时间
+     */
+    @NotNull(message = "删除时间不能为空")
+    private LocalDateTime deleteTime;
+
+    /**
+     * 删除用户
+     */
+    @NotBlank(message = "删除人不能为空")
+    private String deleteBy;
+
+    /**
+     * 是否删除 0:未删除,1:已删除
+     */
+    private Integer isDelete;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+
+}

+ 122 - 0
service-data/service-data-biz/src/main/java/com/usky/data/domain/DataInterfaceLog.java

@@ -0,0 +1,122 @@
+package com.usky.data.domain;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DataInterfaceLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 调用接口ID
+     */
+    private Long interfaceId;
+
+    /**
+     * 执行时间
+     */
+    private LocalDateTime executeTime;
+
+    /**
+     * 操作用户ID
+     */
+    private String userId;
+
+    /**
+     * 请求ip
+     */
+    private String requestIp;
+
+    /**
+     * 请求设备
+     */
+    private String requestDevice;
+
+    /**
+     * 请求类型
+     */
+    private String requestType;
+
+    /**
+     * 请求耗时
+     */
+    private Integer requestWasteTime;
+
+    /**
+     * 接口授权AppId
+     */
+    private String oauthAppId;
+
+    /**
+     * 排序
+     */
+    private Long sortCode;
+
+    /**
+     * 执行结果
+     */
+    private String executeResult;
+
+    /**
+     * 执行状态
+     */
+    private Integer executeState;
+
+    /**
+     * 执行消息
+     */
+    private String executeMsg;
+
+    /**
+     * 创建者
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+
+}

+ 136 - 0
service-data/service-data-biz/src/main/java/com/usky/data/domain/DataLink.java

@@ -0,0 +1,136 @@
+package com.usky.data.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 数据中心_数据连接表
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DataLink implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 连接名称
+     */
+    private String fullName;
+
+    /**
+     * 连接驱动
+     */
+    private String dbType;
+
+    /**
+     * 主机地址
+     */
+    private String host;
+
+    /**
+     * 端口
+     */
+    private Integer port;
+
+    /**
+     * 数据库用户名
+     */
+    private String dbName;
+
+    /**
+     * 密码
+     */
+    private String password;
+
+    /**
+     * 服务名称
+     */
+    private String serviceName;
+
+    /**
+     * 描述或说明
+     */
+    private String description;
+
+    /**
+     * 模式
+     */
+    private String dbSchema;
+
+    /**
+     * 表空间
+     */
+    private String tableSpace;
+
+    /**
+     * oracle连接参数
+     */
+    private String oracleParam;
+
+    /**
+     * Oracle扩展开关 1:开启 0:关闭
+     */
+    private Integer oracleExtend;
+
+    /**
+     * 排序
+     */
+    private Integer sortCode;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 创建用户
+     */
+    private String createBy;
+
+    /**
+     * 修改时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 修改用户
+     */
+    private String updateBy;
+
+    /**
+     * 删除时间
+     */
+    private LocalDateTime deleteTime;
+
+    /**
+     * 删除用户
+     */
+    private String deleteBy;
+
+    /**
+     * 是否删除 0:未删除,1:已删除
+     */
+    private Integer isDelete;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+
+}

+ 16 - 0
service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataInterfaceLogMapper.java

@@ -0,0 +1,16 @@
+package com.usky.data.mapper;
+
+import com.usky.data.domain.DataInterfaceLog;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表 Mapper 接口
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+public interface DataInterfaceLogMapper extends CrudMapper<DataInterfaceLog> {
+
+}

+ 16 - 0
service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataInterfaceMapper.java

@@ -0,0 +1,16 @@
+package com.usky.data.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.data.domain.DataInterface;
+
+/**
+ * <p>
+ * 数据中心_数据接口表 Mapper 接口
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+public interface DataInterfaceMapper extends CrudMapper<DataInterface> {
+
+}

+ 34 - 0
service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataLinkMapper.java

@@ -0,0 +1,34 @@
+package com.usky.data.mapper;
+
+import com.usky.data.domain.DataLink;
+import com.usky.common.mybatis.core.CrudMapper;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 数据中心_数据连接表 Mapper 接口
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+public interface DataLinkMapper extends CrudMapper<DataLink> {
+
+    /**
+     * 根据连接名称统计数量(排除指定ID)
+     * @param fullName 连接名称
+     * @param id 排除的主键ID
+     * @return 数量
+     */
+    int countByFullName(String fullName, Integer id);
+
+    /**
+     * 分页查询数据源列表
+     * @param startIndex 起始索引
+     * @param pageSize 每页条数
+     * @param keyword 关键字
+     * @return 数据源列表
+     */
+    List<DataLink> selectPageList(int startIndex, int pageSize, String keyword);
+}

+ 34 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/DataInterfaceModel.java

@@ -0,0 +1,34 @@
+package com.usky.data.model;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class DataInterfaceModel extends ParamModel implements Serializable {
+
+    /**
+     * 是否为空(0允许,1不允许)
+     */
+    @Schema(description = "是否为空(0允许,1不允许)")
+    private Integer required;
+
+    /**
+     * 参数名称
+     */
+    @Schema(description = "参数名称")
+    private String parameter;
+    /**
+     * 参数来源
+     */
+    @Schema(description = "参数来源(1-字段,2-自定义,3-为空,4-系统变量)")
+    private Integer sourceType = 1;
+
+    /**
+     * 表单字段
+     */
+    @Schema(description = "表单字段")
+    private String relationField;
+}

+ 52 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/DbLinkBaseForm.java

@@ -0,0 +1,52 @@
+package com.usky.data.model;
+
+import com.usky.data.domain.DataLink;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 数据源基础表单
+ */
+@Data
+public class DbLinkBaseForm {
+    @NotBlank(message = "连接名称不能为空")
+    private String fullName;
+    @NotBlank(message = "数据库类型不能为空")
+    private String dbType;
+    @NotBlank(message = "主机地址不能为空")
+    private String host;
+    private Integer port;
+    @NotBlank(message = "数据库用户名不能为空")
+    private String dbName;
+    @NotBlank(message = "密码不能为空")
+    private String password;
+    private String serviceName;
+    private String description;
+    private String dbSchema;
+    private String tableSpace;
+    private String oracleParam;
+    private Integer oracleExtend;
+    private Integer sortCode;
+
+    /**
+     * 转换为实体对象
+     */
+    public DataLink getDbLinkEntity(DbLinkBaseForm form) {
+        DataLink dataLink = new DataLink();
+        dataLink.setFullName(form.getFullName());
+        dataLink.setDbType(form.getDbType());
+        dataLink.setHost(form.getHost());
+        dataLink.setPort(form.getPort());
+        dataLink.setDbName(form.getDbName());
+        dataLink.setPassword(form.getPassword());
+        dataLink.setServiceName(form.getServiceName());
+        dataLink.setDescription(form.getDescription());
+        dataLink.setDbSchema(form.getDbSchema());
+        dataLink.setTableSpace(form.getTableSpace());
+        dataLink.setOracleParam(form.getOracleParam());
+        dataLink.setOracleExtend(form.getOracleExtend());
+        dataLink.setSortCode(form.getSortCode());
+        return dataLink;
+    }
+}

+ 24 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/DbLinkCreUpForm.java

@@ -0,0 +1,24 @@
+package com.usky.data.model;
+
+import com.usky.data.domain.DataLink;
+import lombok.Data;
+
+/**
+ * 数据源创建/更新表单
+ */
+@Data
+public class DbLinkCreUpForm extends DbLinkBaseForm {
+    private Integer id;
+
+    /**
+     * 转换为实体对象
+     */
+    @Override
+    public DataLink getDbLinkEntity(DbLinkBaseForm form) {
+        DataLink dataLink = super.getDbLinkEntity(form);
+        if (this.getId() != null) {
+            dataLink.setId(this.getId());
+        }
+        return dataLink;
+    }
+}

+ 24 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/FieldModel.java

@@ -0,0 +1,24 @@
+package com.usky.data.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+@Data
+public class FieldModel implements Serializable {
+    @Schema(description = "主键")
+    private String id;
+
+    @Schema(description = "参数名称")
+    private String field;
+
+    @Schema(description = "默认值")
+    private String defaultValue;
+
+    public void setDefaultValue(Object defaultValue) {
+        if (Objects.isNull(defaultValue) || defaultValue.toString().trim().isEmpty()) return;
+        this.defaultValue = defaultValue.toString();
+    }
+}

+ 14 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/PaginationDbLink.java

@@ -0,0 +1,14 @@
+package com.usky.data.model;
+
+import lombok.Data;
+
+/**
+ * 数据源分页查询参数
+ */
+@Data
+public class PaginationDbLink {
+    private Integer currentPage = 1;
+    private Integer pageSize = 20;
+    private String keyword;
+    private String type;
+}

+ 26 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/ParamModel.java

@@ -0,0 +1,26 @@
+package com.usky.data.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class ParamModel extends FieldModel implements Serializable {
+
+    @Schema(description = "列说明")
+    private String fieldName;
+
+    /**
+     * 参数类型
+     * 字符串
+     * 整型
+     * 日期时间
+     * 浮点
+     * 长整型
+     * 文本
+     */
+    @Schema(description = "参数类型")
+    private String dataType;
+
+}

+ 16 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/DataInterfaceLogService.java

@@ -0,0 +1,16 @@
+package com.usky.data.service;
+
+import com.usky.data.domain.DataInterfaceLog;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表 服务类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+public interface DataInterfaceLogService extends CrudService<DataInterfaceLog> {
+
+}

+ 49 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/DataInterfaceService.java

@@ -0,0 +1,49 @@
+package com.usky.data.service;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.mybatis.core.CrudService;
+import com.usky.data.domain.DataInterface;
+
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * 数据中心_数据接口表 服务类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+public interface DataInterfaceService extends CrudService<DataInterface> {
+
+    CommonPage<DataInterface> getDataInterfaceList(Integer currentPage, Integer pageSize, String keyword, String type, Integer enabledMark, String category);
+
+    CommonPage<DataInterface> getWorkflowList(Integer currentPage, Integer pageSize, String keyword, String type, Integer enabledMark);
+
+    List<DataInterface> getSelectorList();
+
+    String getInterfaceParameters(String id);
+
+    boolean updateInterfaceStatus(String id);
+
+    ApiResult<?> testInterface(String id, Map<String, Object> params);
+
+    ApiResult<?> getInterfaceResponse(String id, Map<String, String> params);
+
+    ApiResult<?> postInterfaceResponse(String id, Map<String, String> params);
+
+    ApiResult<?> getInterfacePageData(String id, Map<String, Object> pageParams);
+
+    ApiResult<List<Map<String, Object>>> getInterfaceDetailData(String id, Map<String, Object> pageParams);
+
+    ApiResult<List<String>> getInterfaceFields(String id, Map<String, Object> pageParams);
+
+    Boolean deleteInterface(Long id);
+
+    Boolean addInterface(DataInterface dataInterface);
+
+    Boolean updateInterface(DataInterface dataInterface);
+}

+ 47 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/DataLinkService.java

@@ -0,0 +1,47 @@
+package com.usky.data.service;
+
+import com.usky.data.domain.DataLink;
+import com.usky.common.mybatis.core.CrudService;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 数据中心_数据连接表 服务类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+public interface DataLinkService extends CrudService<DataLink> {
+
+    /**
+     * 测试数据库连接
+     * @param dataLink 数据源信息
+     * @return 是否连接成功
+     */
+    boolean testDbConnection(DataLink dataLink);
+
+    /**
+     * 根据连接名称检查是否存在
+     * @param fullName 连接名称
+     * @param id 排除的主键ID
+     * @return 是否存在
+     */
+    boolean isExistByFullName(String fullName, Integer id);
+
+    /**
+     * 获取所有数据源列表
+     * @return 数据源列表
+     */
+    List<DataLink> getList();
+
+    /**
+     * 分页获取数据源列表
+     * @param page 页码
+     * @param pageSize 每页条数
+     * @param keyword 关键字
+     * @return 数据源列表
+     */
+    List<DataLink> getList(int page, int pageSize, String keyword);
+}

+ 20 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataInterfaceLogServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.data.service.impl;
+
+import com.usky.data.domain.DataInterfaceLog;
+import com.usky.data.mapper.DataInterfaceLogMapper;
+import com.usky.data.service.DataInterfaceLogService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表 服务实现类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Service
+public class DataInterfaceLogServiceImpl extends AbstractCrudService<DataInterfaceLogMapper, DataInterfaceLog> implements DataInterfaceLogService {
+
+}

+ 291 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataInterfaceServiceImpl.java

@@ -0,0 +1,291 @@
+package com.usky.data.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.data.domain.DataInterface;
+import com.usky.data.mapper.DataInterfaceMapper;
+import com.usky.data.service.DataInterfaceService;
+import com.usky.data.service.util.InterfaceExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * 数据中心_数据接口表 服务实现类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Service
+@Slf4j
+public class DataInterfaceServiceImpl extends AbstractCrudService<DataInterfaceMapper, DataInterface> implements DataInterfaceService {
+
+    @Override
+    public CommonPage<DataInterface> getDataInterfaceList(Integer currentPage, Integer pageSize, String keyword, String type, Integer enabledMark, String category) {
+
+        // if (StringUtils.isBlank(category)) {
+        //     throw new BusinessException("接口分类不能为空!");
+        // }
+
+        IPage<DataInterface> page = new Page<>(currentPage, pageSize);
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StringUtils.isNotBlank(category), DataInterface::getCategory, category)
+                .eq(DataInterface::getTenantId, SecurityUtils.getTenantId())
+                .eq(DataInterface::getIsDelete, 0)
+                .orderByDesc(DataInterface::getSortCode);
+        queryWrapper.eq(StringUtils.isNotBlank(type), DataInterface::getType, type)
+                .eq(enabledMark != null, DataInterface::getEnabledMark, enabledMark);
+        if (StringUtils.isNotBlank(keyword)) {
+            queryWrapper.like(DataInterface::getFullName, keyword)
+                    .or()
+                    .like(DataInterface::getEnCode, keyword);
+        }
+        page = baseMapper.selectPage(page, queryWrapper);
+        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+    }
+
+    @Override
+    public CommonPage<DataInterface> getWorkflowList(Integer currentPage, Integer pageSize, String keyword, String type, Integer enabledMark) {
+        IPage<DataInterface> page = new Page<>(currentPage, pageSize);
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(DataInterface::getTenantId, SecurityUtils.getTenantId())
+                .like(StringUtils.isNotBlank(keyword), DataInterface::getFullName, keyword)
+                .like(StringUtils.isNotBlank(keyword), DataInterface::getEnCode, keyword)
+                .eq(StringUtils.isNotBlank(type), DataInterface::getType, type)
+                .eq(enabledMark != null, DataInterface::getEnabledMark, enabledMark)
+                .eq(DataInterface::getEnabledMark, 1); // 工作流选择时只显示启用的接口
+        page = baseMapper.selectPage(page, queryWrapper);
+        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+    }
+
+    @Override
+    public List<DataInterface> getSelectorList() {
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(DataInterface::getTenantId, SecurityUtils.getTenantId())
+                .eq(DataInterface::getEnabledMark, 1); // 只返回启用的接口
+        return baseMapper.selectList(queryWrapper);
+    }
+
+    @Override
+    public String getInterfaceParameters(String id) {
+        DataInterface entity = baseMapper.selectById(id);
+        return entity != null ? entity.getParameterJson() : "[]";
+    }
+
+    @Override
+    public boolean updateInterfaceStatus(String id) {
+        DataInterface entity = baseMapper.selectById(id);
+        if (entity == null) {
+            return false;
+        }
+        // 切换启用状态
+        entity.setEnabledMark(entity.getEnabledMark() == 1 ? 0 : 1);
+        return baseMapper.updateById(entity) > 0;
+    }
+
+    @Override
+    public ApiResult<?> testInterface(String id, Map<String, Object> params) {
+        // 根据ID获取接口配置
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 执行接口逻辑
+            Object result = InterfaceExecutor.executeInterface(interfaceEntity, params);
+            return ApiResult.success(result);
+        } catch (Exception e) {
+            return ApiResult.error("接口测试失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<?> getInterfaceResponse(String id, Map<String, String> params) {
+        // 这里是访问接口GET的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 将String类型的Map转换为Object类型的Map
+            Map<String, Object> objectParams = new HashMap<>();
+            if (params != null) {
+                params.forEach((k, v) -> objectParams.put(k, v));
+            }
+
+            // 执行接口逻辑
+            Object result = InterfaceExecutor.executeInterface(interfaceEntity, objectParams);
+            return ApiResult.success(result);
+        } catch (Exception e) {
+            return ApiResult.error("接口访问失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<?> postInterfaceResponse(String id, Map<String, String> params) {
+        // 这里是访问接口POST的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 将String类型的Map转换为Object类型的Map
+            Map<String, Object> objectParams = new HashMap<>();
+            if (params != null) {
+                params.forEach((k, v) -> objectParams.put(k, v));
+            }
+
+            // 执行接口逻辑
+            Object result = InterfaceExecutor.executeInterface(interfaceEntity, objectParams);
+            return ApiResult.success(result);
+        } catch (Exception e) {
+            return ApiResult.error("接口访问失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<?> getInterfacePageData(String id, Map<String, Object> pageParams) {
+        // 这里是获取接口分页数据的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 获取接口分页数据
+            Map<String, Object> result = InterfaceExecutor.getInterfacePageData(interfaceEntity, pageParams);
+            return ApiResult.success(result);
+        } catch (Exception e) {
+            return ApiResult.error("获取接口分页数据失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<List<Map<String, Object>>> getInterfaceDetailData(String id, Map<String, Object> pageParams) {
+        // 这里是获取接口详情数据的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 执行接口逻辑获取数据
+            Object result = InterfaceExecutor.executeInterface(interfaceEntity, pageParams);
+
+            // 转换结果为List<Map<String, Object>>类型
+            List<Map<String, Object>> resultList = new ArrayList<>();
+            if (result instanceof List) {
+                List<?> listResult = (List<?>) result;
+                for (Object item : listResult) {
+                    if (item instanceof Map) {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> mapItem = (Map<String, Object>) item;
+                        resultList.add(mapItem);
+                    }
+                }
+            }
+
+            return ApiResult.success(resultList);
+        } catch (Exception e) {
+            return ApiResult.error("获取接口详情数据失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<List<String>> getInterfaceFields(String id, Map<String, Object> pageParams) {
+        // 这里是获取接口字段的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 获取接口字段
+            List<String> fields = InterfaceExecutor.getInterfaceFields(interfaceEntity, pageParams);
+            return ApiResult.success(fields);
+        } catch (Exception e) {
+            return ApiResult.error("获取接口字段失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional
+    public Boolean deleteInterface(Long id) {
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(DataInterface::getId, id);
+        DataInterface entity = baseMapper.selectOne(queryWrapper);
+        String username = SecurityUtils.getUsername();
+
+        if (entity == null) {
+            log.warn("尝试删除不存在的接口,ID: {},操作人{}", id, username);
+            throw new BusinessException("接口不存在!");
+        }
+
+        entity.setIsDelete(1);
+        entity.setDeleteTime(LocalDateTime.now());
+        entity.setDeleteBy(username);
+
+        return baseMapper.update(entity, queryWrapper) > 0;
+    }
+
+
+    @Override
+    @Transactional
+    public Boolean addInterface(DataInterface dataInterface) {
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (!selectNameAndCode(dataInterface.getFullName(), dataInterface.getEnCode(), tenantId).isEmpty()) {
+            throw new BusinessException("接口名称或编码已存在");
+        }
+
+        dataInterface.setCreateBy(SecurityUtils.getUsername());
+        dataInterface.setCreateTime(LocalDateTime.now());
+        dataInterface.setIsDelete(0);
+        dataInterface.setEnabledMark(1);
+        dataInterface.setTenantId(tenantId);
+        return baseMapper.insert(dataInterface) > 0;
+    }
+
+    private List<DataInterface> selectNameAndCode(String name, String code, Integer tenantId) {
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(DataInterface::getTenantId, tenantId)
+                .and(w -> w.eq(DataInterface::getFullName, name)
+                        .or()
+                        .eq(DataInterface::getEnCode, code));
+        return baseMapper.selectList(queryWrapper);
+    }
+
+
+    @Override
+    public Boolean updateInterface(DataInterface dataInterface) {
+
+        if (dataInterface == null || dataInterface.getId() <= 0) {
+            throw new BusinessException("接口id为空或不合规");
+        }
+
+        if (!selectNameAndCode(dataInterface.getFullName(), dataInterface.getEnCode(), dataInterface.getTenantId()).isEmpty()) {
+            throw new BusinessException("接口名称或编码已存在");
+        }
+
+        dataInterface.setUpdateBy(SecurityUtils.getUsername());
+        dataInterface.setUpdateTime(LocalDateTime.now());
+        return baseMapper.updateById(dataInterface) > 0;
+    }
+}

+ 120 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataLinkServiceImpl.java

@@ -0,0 +1,120 @@
+package com.usky.data.service.impl;
+
+import com.usky.data.domain.DataLink;
+import com.usky.data.mapper.DataLinkMapper;
+import com.usky.data.service.DataLinkService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.util.List;
+
+/**
+ * <p>
+ * 数据中心_数据连接表 服务实现类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+@Service
+public class DataLinkServiceImpl extends AbstractCrudService<DataLinkMapper, DataLink> implements DataLinkService {
+
+    @Autowired
+    private DataLinkMapper dataLinkMapper;
+
+    @Override
+    public boolean testDbConnection(DataLink dataLink) {
+        Connection connection = null;
+        try {
+            // 根据数据库类型加载驱动
+            String driverClass = getDriverClass(dataLink.getDbType());
+            Class.forName(driverClass);
+
+            // 构建连接URL
+            String url = buildJdbcUrl(dataLink);
+
+            // 建立连接
+            connection = DriverManager.getConnection(url, dataLink.getDbName(), dataLink.getPassword());
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        } finally {
+            if (connection != null) {
+                try {
+                    connection.close();
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean isExistByFullName(String fullName, Integer id) {
+        int count = dataLinkMapper.countByFullName(fullName, id);
+        return count > 0;
+    }
+
+    @Override
+    public List<DataLink> getList() {
+        return dataLinkMapper.selectList(null);
+    }
+
+    @Override
+    public List<DataLink> getList(int page, int pageSize, String keyword) {
+        // 计算起始索引
+        int startIndex = (page - 1) * pageSize;
+        return dataLinkMapper.selectPageList(startIndex, pageSize, keyword);
+    }
+
+    /**
+     * 根据数据库类型获取驱动类名
+     */
+    private String getDriverClass(String dbType) {
+        switch (dbType.toLowerCase()) {
+            case "mysql":
+                return "com.mysql.cj.jdbc.Driver";
+            case "oracle":
+                return "oracle.jdbc.OracleDriver";
+            case "sqlserver":
+                return "com.microsoft.sqlserver.jdbc.SQLServerDriver";
+            case "postgresql":
+                return "org.postgresql.Driver";
+            default:
+                throw new IllegalArgumentException("不支持的数据库类型: " + dbType);
+        }
+    }
+
+    /**
+     * 构建JDBC连接URL
+     */
+    private String buildJdbcUrl(DataLink dataLink) {
+        String dbType = dataLink.getDbType().toLowerCase();
+        String host = dataLink.getHost();
+        Integer port = dataLink.getPort();
+        String dbName = dataLink.getDbName();
+
+        switch (dbType) {
+            case "mysql":
+                return String.format("jdbc:mysql://%s:%d/%s?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%%2B8",
+                        host, port, dbName);
+            case "oracle":
+                // Oracle支持SID和Service Name两种方式
+                if (dataLink.getServiceName() != null && !dataLink.getServiceName().isEmpty()) {
+                    return String.format("jdbc:oracle:thin:@//%s:%d/%s", host, port, dataLink.getServiceName());
+                } else {
+                    return String.format("jdbc:oracle:thin:@%s:%d:%s", host, port, dbName);
+                }
+            case "sqlserver":
+                return String.format("jdbc:sqlserver://%s:%d;databaseName=%s;encrypt=false", host, port, dbName);
+            case "postgresql":
+                return String.format("jdbc:postgresql://%s:%d/%s", host, port, dbName);
+            default:
+                throw new IllegalArgumentException("不支持的数据库类型: " + dbType);
+        }
+    }
+}

+ 421 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/util/InterfaceExecutor.java

@@ -0,0 +1,421 @@
+package com.usky.data.service.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.usky.data.domain.DataInterface;
+import org.apache.commons.lang3.StringUtils;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 接口执行器,用于处理不同类型接口的执行逻辑
+ * <p>
+ * 支持三种接口类型:
+ * 1. SQL操作(type=1)
+ * 2. 静态数据(type=2)
+ * 3. API操作(type=3)
+ * </p>
+ */
+public class InterfaceExecutor {
+
+    /**
+     * 执行接口逻辑,参考原始infoToId方法
+     *
+     * @param dataInterface 接口实体
+     * @param params        请求参数
+     * @return 执行结果
+     */
+    public static Object executeInterface(DataInterface dataInterface, Map<String, Object> params) {
+        if (dataInterface == null) {
+            throw new IllegalArgumentException("接口配置不能为空");
+        }
+
+        // 转换params为Map<String, String>用于参数处理
+        Map<String, String> stringParams = new HashMap<>();
+        if (params != null) {
+            params.forEach((k, v) -> stringParams.put(k, v != null ? v.toString() : null));
+        }
+
+        // 替换默认值
+        replaceDefaultValue(dataInterface.getParameterJson(), stringParams);
+
+        // 验证参数必填或类型
+        String checkResult = checkRequestParams(dataInterface.getParameterJson(), stringParams);
+        if (StringUtils.isNotEmpty(checkResult)) {
+            throw new IllegalArgumentException(checkResult);
+        }
+
+        Object result = null;
+        try {
+            // 根据接口类型执行不同逻辑
+            Integer type = dataInterface.getType();
+            switch (type) {
+                case 1:
+                    // 静态数据
+                    result = callStaticData(dataInterface.getDataConfigJson());
+                    break;
+                case 3:
+                    // API调用
+                    result = callApi(dataInterface.getDataConfigJson(), stringParams);
+                    break;
+                case 2:
+                    // SQL操作
+                    result = executeSql(dataInterface, stringParams);
+                    break;
+                default:
+                    throw new IllegalArgumentException("不支持的接口类型: " + type);
+            }
+
+            // 处理结果,执行JS脚本
+            if (StringUtils.isNotEmpty(dataInterface.getDataJsJson())) {
+                // 这里模拟JS脚本执行,实际项目中需要引入JS引擎
+                result = processResult(result, dataInterface.getDataJsJson());
+            }
+
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException("接口执行失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 替换默认值
+     *
+     * @param parameterJson 参数配置JSON
+     * @param params        请求参数
+     */
+    private static void replaceDefaultValue(String parameterJson, Map<String, String> params) {
+        if (StringUtils.isEmpty(parameterJson)) {
+            return;
+        }
+
+        JSONArray paramArray = JSON.parseArray(parameterJson);
+        if (paramArray == null || paramArray.isEmpty()) {
+            return;
+        }
+
+        for (Object obj : paramArray) {
+            JSONObject paramObj = (JSONObject) obj;
+            String field = paramObj.getString("field");
+            String defaultValue = paramObj.getString("defaultValue");
+
+            // 如果参数中没有该字段或值为空,则设置默认值
+            if (!params.containsKey(field) || StringUtils.isEmpty(params.get(field))) {
+                params.put(field, defaultValue == null ? "" : defaultValue);
+            }
+        }
+    }
+
+    /**
+     * 检查请求参数
+     *
+     * @param parameterJson 参数配置JSON
+     * @param params        请求参数
+     * @return 检查结果,空字符串表示通过
+     */
+    private static String checkRequestParams(String parameterJson, Map<String, String> params) {
+        if (StringUtils.isEmpty(parameterJson)) {
+            return "";
+        }
+
+        JSONArray paramArray = JSON.parseArray(parameterJson);
+        if (paramArray == null || paramArray.isEmpty()) {
+            return "";
+        }
+
+        StringBuilder message = new StringBuilder();
+        for (Object obj : paramArray) {
+            JSONObject paramObj = (JSONObject) obj;
+
+            // 验证必填字段
+            Integer required = paramObj.getInteger("required");
+            if (required != null && required == 1) {
+                String field = paramObj.getString("field");
+                String value = params.get(field);
+                if (StringUtils.isEmpty(value)) {
+                    message.append(field).append("不能为空;");
+                }
+            }
+
+            // 验证数据类型
+            String dataType = paramObj.getString("dataType");
+            if (StringUtils.isNotEmpty(dataType)) {
+                String field = paramObj.getString("field");
+                String value = params.get(field);
+                if (StringUtils.isEmpty(value)) {
+                    continue;
+                }
+                try {
+                    if ("int".equals(dataType)) {
+                        Integer.parseInt(value);
+                    } else if ("decimal".equals(dataType)) {
+                        Double.parseDouble(value);
+                    } else if ("datetime".equals(dataType)) {
+                        try {
+                            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                            formatter.parse(value);
+                        } catch (Exception e) {
+                            // 尝试解析时间戳
+                            Long.parseLong(value);
+                        }
+                    }
+                } catch (Exception e) {
+                    message.append(field).append("类型必须为").append(dataType).append(";");
+                }
+            }
+        }
+
+        return message.length() > 0 ? message.substring(0, message.length() - 1) : "";
+    }
+
+    /**
+     * 执行静态数据类型接口
+     *
+     * @param dataConfigJson 数据配置JSON
+     * @return 静态数据结果
+     */
+    private static Object callStaticData(String dataConfigJson) {
+        JSONObject configJson = JSON.parseObject(dataConfigJson);
+        String staticData = configJson.getString("staticData");
+
+        Object obj;
+        try {
+            Object parse = JSON.parse(staticData);
+            if (parse instanceof JSONArray) {
+                obj = JSON.parseArray(staticData, Map.class);
+            } else {
+                obj = JSON.parseObject(staticData, Map.class);
+            }
+        } catch (Exception e) {
+            obj = staticData;
+        }
+
+        if (obj == null) {
+            return new ArrayList<>();
+        }
+
+        return obj;
+    }
+
+    /**
+     * 执行API类型接口
+     *
+     * @param dataConfigJson 数据配置JSON
+     * @param params        请求参数
+     * @return API调用结果
+     */
+    private static Object callApi(String dataConfigJson, Map<String, String> params) {
+        JSONObject configJson = JSON.parseObject(dataConfigJson);
+        JSONObject apiData = configJson.getJSONObject("apiData");
+
+        if (apiData == null) {
+            throw new IllegalArgumentException("API配置不能为空");
+        }
+
+        String url = apiData.getString("url");
+        String method = apiData.getString("method");
+        JSONObject headers = apiData.getJSONObject("headers");
+        JSONObject apiParams = apiData.getJSONObject("params");
+
+        // 合并参数
+        if (apiParams == null) {
+            apiParams = new JSONObject();
+        }
+        if (params != null) {
+            JSONObject finalApiParams = apiParams;
+            params.forEach((k, v) -> {
+                if (StringUtils.isNotEmpty(v)) {
+                    finalApiParams.put(k, v);
+                }
+            });
+        }
+
+        // 模拟API调用,实际项目中需要使用HTTP客户端
+        Map<String, Object> result = new HashMap<>();
+        result.put("success", true);
+        result.put("message", "API调用成功");
+        result.put("data", apiParams);
+        result.put("url", url);
+        result.put("method", method);
+
+        return result;
+    }
+
+    /**
+     * 执行SQL类型接口
+     *
+     * @param dataInterface 接口实体
+     * @param params        请求参数
+     * @return SQL执行结果
+     */
+    private static Object executeSql(DataInterface dataInterface, Map<String, String> params) {
+        JSONObject configJson = JSON.parseObject(dataInterface.getDataConfigJson());
+        JSONObject sqlData = configJson.getJSONObject("sqlData");
+
+        if (sqlData == null) {
+            throw new IllegalArgumentException("SQL配置不能为空");
+        }
+
+        String sql = sqlData.getString("sql");
+        if (StringUtils.isEmpty(sql)) {
+            throw new IllegalArgumentException("SQL语句不能为空");
+        }
+
+        // 替换SQL中的参数占位符
+        sql = replaceSqlParams(sql, params);
+
+        // 模拟SQL执行结果
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            Map<String, Object> map = new HashMap<>();
+            map.put("id", i + 1);
+            map.put("name", "测试数据" + (i + 1));
+            map.put("value", i * 100);
+            map.put("created_at", "2026-01-24 10:00:00");
+            resultList.add(map);
+        }
+
+        // 如果是分页查询,处理分页信息
+        if (dataInterface.getHasPage() != null && dataInterface.getHasPage() == 1) {
+            Map<String, Object> pageResult = new HashMap<>();
+            pageResult.put("list", resultList);
+            pageResult.put("total", 100);
+            pageResult.put("current", params.getOrDefault("currentPage", "1"));
+            pageResult.put("pageSize", params.getOrDefault("pageSize", "20"));
+            return pageResult;
+        }
+
+        return resultList;
+    }
+
+    /**
+     * 替换SQL中的参数占位符
+     *
+     * @param sql    SQL语句
+     * @param params 请求参数
+     * @return 替换后的SQL语句
+     */
+    private static String replaceSqlParams(String sql, Map<String, String> params) {
+        if (StringUtils.isEmpty(sql) || params == null || params.isEmpty()) {
+            return sql;
+        }
+
+        // 匹配 {参数名} 格式的占位符
+        Pattern pattern = Pattern.compile("\\{(\\w+)\\}");
+        Matcher matcher = pattern.matcher(sql);
+
+        StringBuffer sb = new StringBuffer();
+        while (matcher.find()) {
+            String paramName = matcher.group(1);
+            String paramValue = params.getOrDefault(paramName, "");
+            matcher.appendReplacement(sb, paramValue);
+        }
+        matcher.appendTail(sb);
+
+        return sb.toString();
+    }
+
+    /**
+     * 处理结果,执行JS脚本
+     *
+     * @param result    原始结果
+     * @param jsScript  JS脚本
+     * @return 处理后的结果
+     */
+    private static Object processResult(Object result, String jsScript) {
+        // 这里模拟JS脚本执行,实际项目中需要引入JS引擎
+        // 例如使用javax.script.ScriptEngine
+        return result;
+    }
+
+    /**
+     * 判断是否为内部接口返回格式
+     *
+     * @param jsonObject JSON对象
+     * @return 是否为内部接口格式
+     */
+    private static boolean isInternal(JSONObject jsonObject) {
+        if (jsonObject != null) {
+            return jsonObject.size() == 3 &&
+                    jsonObject.containsKey("code") &&
+                    jsonObject.containsKey("msg") &&
+                    jsonObject.containsKey("data");
+        }
+        return false;
+    }
+
+    /**
+     * 获取接口字段信息
+     *
+     * @param dataInterface 接口实体
+     * @param params        请求参数
+     * @return 接口字段列表
+     */
+    public static List<String> getInterfaceFields(DataInterface dataInterface, Map<String, Object> params) {
+        // 执行接口获取数据
+        Object result = executeInterface(dataInterface, params);
+
+        // 解析结果获取字段列表
+        List<String> fields = new ArrayList<>();
+        if (result instanceof List) {
+            List<?> resultList = (List<?>) result;
+            if (!resultList.isEmpty() && resultList.get(0) instanceof Map) {
+                Map<?, ?> firstItem = (Map<?, ?>) resultList.get(0);
+                for (Object key : firstItem.keySet()) {
+                    fields.add(key.toString());
+                }
+            }
+        } else if (result instanceof Map) {
+            Map<?, ?> resultMap = (Map<?, ?>) result;
+            // 如果是分页结果,获取list中的字段
+            if (resultMap.containsKey("list") && resultMap.get("list") instanceof List) {
+                List<?> list = (List<?>) resultMap.get("list");
+                if (!list.isEmpty() && list.get(0) instanceof Map) {
+                    Map<?, ?> firstItem = (Map<?, ?>) list.get(0);
+                    for (Object key : firstItem.keySet()) {
+                        fields.add(key.toString());
+                    }
+                }
+            } else {
+                // 直接获取map中的字段
+                for (Object key : resultMap.keySet()) {
+                    fields.add(key.toString());
+                }
+            }
+        }
+
+        return fields;
+    }
+
+    /**
+     * 获取接口分页数据
+     *
+     * @param dataInterface 接口实体
+     * @param params        请求参数
+     * @return 分页数据结果
+     */
+    public static Map<String, Object> getInterfacePageData(DataInterface dataInterface, Map<String, Object> params) {
+        // 执行接口获取数据
+        Object result = executeInterface(dataInterface, params);
+
+        // 处理分页
+        Map<String, Object> pageResult = new HashMap<>();
+
+        if (result instanceof Map && ((Map<?, ?>) result).containsKey("list")) {
+            // 如果已经是分页格式,直接返回
+            pageResult.putAll((Map<? extends String, ?>) result);
+        } else {
+            // 否则包装成分页格式
+            pageResult.put("list", result);
+            pageResult.put("total", result instanceof List ? ((List<?>) result).size() : 1);
+            pageResult.put("current", params != null ? params.getOrDefault("currentPage", 1) : 1);
+            pageResult.put("pageSize", params != null ? params.getOrDefault("pageSize", 20) : 20);
+        }
+
+        return pageResult;
+    }
+}

+ 49 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkInfoVO.java

@@ -0,0 +1,49 @@
+package com.usky.data.vo;
+
+import com.usky.data.domain.DataLink;
+import lombok.Data;
+
+/**
+ * 数据源详细信息VO
+ */
+@Data
+public class DbLinkInfoVO {
+    private Integer id;
+    private String fullName;
+    private String dbType;
+    private String host;
+    private Integer port;
+    private String dbName;
+    private String serviceName;
+    private String description;
+    private String dbSchema;
+    private String tableSpace;
+    private String oracleParam;
+    private Integer oracleExtend;
+    private Integer sortCode;
+    private String createBy;
+    private String updateBy;
+
+    /**
+     * 转换为VO对象
+     */
+    public DbLinkInfoVO getDbLinkInfoVO(DataLink dataLink) {
+        DbLinkInfoVO vo = new DbLinkInfoVO();
+        vo.setId(dataLink.getId());
+        vo.setFullName(dataLink.getFullName());
+        vo.setDbType(dataLink.getDbType());
+        vo.setHost(dataLink.getHost());
+        vo.setPort(dataLink.getPort());
+        vo.setDbName(dataLink.getDbName());
+        vo.setServiceName(dataLink.getServiceName());
+        vo.setDescription(dataLink.getDescription());
+        vo.setDbSchema(dataLink.getDbSchema());
+        vo.setTableSpace(dataLink.getTableSpace());
+        vo.setOracleParam(dataLink.getOracleParam());
+        vo.setOracleExtend(dataLink.getOracleExtend());
+        vo.setSortCode(dataLink.getSortCode());
+        vo.setCreateBy(dataLink.getCreateBy());
+        vo.setUpdateBy(dataLink.getUpdateBy());
+        return vo;
+    }
+}

+ 22 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkListVO.java

@@ -0,0 +1,22 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+/**
+ * 数据源列表VO
+ */
+@Data
+public class DbLinkListVO {
+    private Integer id;
+    private String fullName;
+    private String dbType;
+    private String host;
+    private Integer port;
+    private String dbName;
+    private String serviceName;
+    private String description;
+    private String dbSchema;
+    private String tableSpace;
+    private String createBy;
+    private String updateBy;
+}

+ 14 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkSelectorListVO.java

@@ -0,0 +1,14 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 数据源选择器列表VO
+ */
+@Data
+public class DbLinkSelectorListVO {
+    private String fullName;
+    private List<DbLinkListVO> children;
+}

+ 17 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/ListVO.java

@@ -0,0 +1,17 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 通用列表包装类
+ */
+@Data
+public class ListVO<T> {
+    private List<T> list;
+
+    public ListVO(List<T> list) {
+        this.list = list;
+    }
+}

+ 19 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/PageListVO.java

@@ -0,0 +1,19 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 分页列表包装类
+ */
+@Data
+public class PageListVO<T> {
+    private List<T> list;
+    private PaginationVO pagination;
+
+    public PageListVO(List<T> list, PaginationVO pagination) {
+        this.list = list;
+        this.pagination = pagination;
+    }
+}

+ 14 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/PaginationVO.java

@@ -0,0 +1,14 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+/**
+ * 分页信息包装类
+ */
+@Data
+public class PaginationVO {
+    private int currentPage;
+    private int pageSize;
+    private int totalRows;
+    private int totalPages;
+}

+ 25 - 0
service-data/service-data-biz/src/main/resources/bootstrap.yml

@@ -0,0 +1,25 @@
+# Tomcat
+server:
+  port: 9902
+
+# Spring
+spring: 
+  application:
+    # 应用名称
+    name: service-data
+  profiles:
+    # 环境配置
+    active: dev
+  cloud:
+    nacos:
+      discovery:
+        # 服务注册地址
+        server-addr: usky-cloud-nacos:8848
+      config:
+        # 配置中心地址
+        server-addr: usky-cloud-nacos:8848
+        # 配置文件格式
+        file-extension: yml
+        # 共享配置
+        shared-configs:
+          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

+ 108 - 0
service-data/service-data-biz/src/main/resources/doc/index.adoc

@@ -0,0 +1,108 @@
+= 安防项目
+
+[width="100%",options="header"]
+[stripes=even]
+|====================
+|Version |  Update Time  | Status | Author |  Description
+|v2022-04-21 16:57:08|2022-04-21 16:57:08|auto|@yq|Created by smart-doc
+|====================
+
+
+== &lt;p&gt;参数配置表 前端控制器&lt;/p&gt;
+== &lt;p&gt;部门信息&lt;/p&gt;
+=== 查看部门信息
+*URL:* http:10.23.39.1:8082/sysDept/list
+
+*Type:* POST
+
+*Author:* ya
+
+*Content-Type:* application/json; charset=utf-8
+
+
+
+
+*Body-parameters:*
+
+[width="100%",options="header"]
+[stripes=even]
+|====================
+|Parameter | Type|Description|Required|Since
+|deptId|int64|部门id|false|-
+|parentId|int64|父部门id|false|-
+|ancestors|string|祖级列表|false|-
+|deptName|string|部门名称|false|-
+|orderNum|int32|显示顺序|false|-
+|leader|string|负责人|false|-
+|phone|string|联系电话|false|-
+|email|string|邮箱|false|-
+|status|string|部门状态(0正常 1停用)|false|-
+|delFlag|string|删除标志(0代表存在 2代表删除)|false|-
+|createBy|string|创建者|false|-
+|createTime|string|创建时间|false|-
+|updateBy|string|更新者|false|-
+|updateTime|string|更新时间|false|-
+|bId|int64|建筑id|false|-
+|====================
+
+*Response-fields:*
+
+[width="100%",options="header"]
+[stripes=even]
+|====================
+|Field | Type|Description|Since
+|status|object|No comments found.|-
+|code|string|No comments found.|-
+|msg|string|No comments found.|-
+|data|object|No comments found.|-
+|└─deptId|int64|部门id|-
+|└─parentId|int64|父部门id|-
+|└─ancestors|string|祖级列表|-
+|└─deptName|string|部门名称|-
+|└─orderNum|int32|显示顺序|-
+|└─leader|string|负责人|-
+|└─phone|string|联系电话|-
+|└─email|string|邮箱|-
+|└─status|string|部门状态(0正常 1停用)|-
+|└─delFlag|string|删除标志(0代表存在 2代表删除)|-
+|└─createBy|string|创建者|-
+|└─createTime|string|创建时间|-
+|└─updateBy|string|更新者|-
+|└─updateTime|string|更新时间|-
+|└─bId|int64|建筑id|-
+|exception|string|No comments found.|-
+|====================
+
+*Response-example:*
+----
+{
+	"status": {
+		
+	},
+	"code": "97564",
+	"msg": "wnr5qt",
+	"data": [
+		{
+			"deptId": 540,
+			"parentId": 858,
+			"ancestors": "o5lg60",
+			"deptName": "文.沈",
+			"orderNum": 260,
+			"leader": "ufz93p",
+			"phone": "17852835049",
+			"email": "智渊.徐@yahoo.com",
+			"status": "nu6cnp",
+			"delFlag": "72oiji",
+			"createBy": "5fxr6j",
+			"createTime": "2022-04-21 16:57:10",
+			"updateBy": "4kcs4e",
+			"updateTime": "2022-04-21 16:57:10",
+			"bId": 977
+		}
+	],
+	"exception": "53u6bg"
+}
+----
+
+== &lt;p&gt;用户信息表 前端控制器&lt;/p&gt;
+

+ 74 - 0
service-data/service-data-biz/src/main/resources/logback.xml

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="/var/log/uskycloud/usky-data" />
+   <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+    <!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+
+    <!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+
+    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 系统模块日志级别控制  -->
+	<logger name="com.usky" level="info" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+	
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+</configuration>

+ 28 - 0
service-data/service-data-biz/src/main/resources/mapper/data/DataInterfaceLogMapper.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.data.mapper.DataInterfaceLogMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.usky.data.domain.DataInterfaceLog">
+        <id column="id" property="id" />
+        <result column="interface_id" property="interfaceId" />
+        <result column="execute_time" property="executeTime" />
+        <result column="user_id" property="userId" />
+        <result column="request_ip" property="requestIp" />
+        <result column="request_device" property="requestDevice" />
+        <result column="request_type" property="requestType" />
+        <result column="request_waste_time" property="requestWasteTime" />
+        <result column="oauth_app_id" property="oauthAppId" />
+        <result column="sort_code" property="sortCode" />
+        <result column="execute_result" property="executeResult" />
+        <result column="execute_state" property="executeState" />
+        <result column="execute_msg" property="executeMsg" />
+        <result column="create_by" property="createBy" />
+        <result column="create_time" property="createTime" />
+        <result column="update_by" property="updateBy" />
+        <result column="update_time" property="updateTime" />
+        <result column="dept_id" property="deptId" />
+        <result column="tenant_id" property="tenantId" />
+    </resultMap>
+
+</mapper>

+ 35 - 0
service-data/service-data-biz/src/main/resources/mapper/data/DataInterfaceMapper.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.data.mapper.DataInterfaceMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.usky.data.domain.DataInterface">
+        <id column="id" property="id"/>
+        <result column="full_name" property="fullName"/>
+        <result column="en_code" property="enCode"/>
+        <result column="category" property="category"/>
+        <result column="type" property="type"/>
+        <result column="action" property="action"/>
+        <result column="has_page" property="hasPage"/>
+        <result column="is_postposition" property="isPostposition"/>
+        <result column="config_json" property="dataConfigJson"/>
+        <result column="count_json" property="dataCountJson"/>
+        <result column="echo_json" property="dataEchoJson"/>
+        <result column="js_json" property="dataJsJson"/>
+        <result column="field_json" property="fieldJson"/>
+        <result column="parameter_json" property="parameterJson"/>
+        <result column="exception_json" property="exceptionJson"/>
+        <result column="description" property="description"/>
+        <result column="enabled_mark" property="enabledMark"/>
+        <result column="sort_code" property="sortCode"/>
+        <result column="create_by" property="createBy"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_by" property="updateBy"/>
+        <result column="update_time" property="updateTime"/>
+        <result column="delete_time" property="deleteTime"/>
+        <result column="delete_by" property="deleteBy"/>
+        <result column="is_delete" property="isDelete"/>
+        <result column="tenant_id" property="tenantId"/>
+    </resultMap>
+
+</mapper>

+ 56 - 0
service-data/service-data-biz/src/main/resources/mapper/data/DataLinkMapper.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.data.mapper.DataLinkMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.usky.data.domain.DataLink">
+        <id column="id" property="id" />
+        <result column="full_name" property="fullName" />
+        <result column="db_type" property="dbType" />
+        <result column="host" property="host" />
+        <result column="port" property="port" />
+        <result column="db_name" property="dbName" />
+        <result column="password" property="password" />
+        <result column="service_name" property="serviceName" />
+        <result column="description" property="description" />
+        <result column="db_schema" property="dbSchema" />
+        <result column="table_space" property="tableSpace" />
+        <result column="oracle_param" property="oracleParam" />
+        <result column="oracle_extend" property="oracleExtend" />
+        <result column="sort_code" property="sortCode" />
+        <result column="create_time" property="createTime" />
+        <result column="create_by" property="createBy" />
+        <result column="update_time" property="updateTime" />
+        <result column="update_by" property="updateBy" />
+        <result column="delete_time" property="deleteTime" />
+        <result column="delete_by" property="deleteBy" />
+        <result column="is_delete" property="isDelete" />
+        <result column="tenant_id" property="tenantId" />
+    </resultMap>
+
+    <!-- 根据连接名称统计数量(排除指定ID) -->
+    <select id="countByFullName" resultType="int">
+        SELECT COUNT(*)
+        FROM data_link
+        WHERE full_name = #{fullName}
+        AND is_delete = 0
+        <if test="id != null">
+            AND id != #{id}
+        </if>
+    </select>
+
+    <!-- 分页查询数据源列表 -->
+    <select id="selectPageList" resultMap="BaseResultMap">
+        SELECT *
+        FROM data_link
+        WHERE is_delete = 0
+        <if test="keyword != null and keyword != ''">
+            AND (full_name LIKE CONCAT('%', #{keyword}, '%')
+            OR host LIKE CONCAT('%', #{keyword}, '%')
+            OR db_name LIKE CONCAT('%', #{keyword}, '%'))
+        </if>
+        ORDER BY create_time DESC
+        LIMIT #{startIndex}, #{pageSize}
+    </select>
+
+</mapper>

+ 15 - 0
service-data/service-data-biz/src/main/resources/smart-doc.json

@@ -0,0 +1,15 @@
+{
+  "outPath":"./src/main/resources/doc",
+  "serverUrl": "http:10.23.39.1:9902/",
+  "isStrict": false,
+  "coverOld": true,
+  "allInOne": true,
+  "packageFilters": "com.usky.data.controller.web",
+  "requestExample":"false",
+  "responseExample":"true",
+  "projectName": "数据中心",
+  "appKey": "20211216921084883495813120",
+  "appToken":"36bde2426ad546a5a50311bb747e7e61",
+  "secret": "N@Pd,KXAHki*BW3=zK.XPNykf!=CM79J",
+  "openUrl": "http://101.133.214.75:7700/api"
+}

+ 9 - 12
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsAnalysisController.java

@@ -19,14 +19,14 @@ public class EmsAnalysisController {
 
     @Autowired
     private EmsAnalysisService emsAnalysisService;
-    
+
     /**
      * 获取当前日期的默认值
      */
     private String getCurrentDefaultTimeDimension() {
         return "D"; // 按日统计
     }
-    
+
     /**
      * 获取当前日期的默认值(根据时间维度返回对应格式)
      */
@@ -80,16 +80,13 @@ public class EmsAnalysisController {
         return ApiResult.success(emsAnalysisService.getTrendCategory(projectId, timeDimension, timeValue, energyTypeId));
     }
 
-    @GetMapping("/region")
-    public ApiResult<EmsRegionAnalysisResponse> getRegionAnalysis(
-            @RequestParam(required = false) Long projectId,
-            @RequestParam(required = false) String regionIds,
-            @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("/region")
+    public ApiResult<EmsAverageResponseVO> getRegionAnalysis(@RequestBody EmsAverageRequest request) {
+        return ApiResult.success(emsAnalysisService.getRegionalAnalysis(request));
     }
 
     @PostMapping("/compare")

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

@@ -1,12 +1,19 @@
 package com.usky.ems.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
+import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.EmsEnergyItemCode;
+import com.usky.ems.service.EmsEnergyItemCodeService;
 import com.usky.ems.service.EmsProjectService;
+import com.usky.ems.service.vo.EmsDeviceItemCodeSaveRequest;
+import com.usky.ems.service.vo.EmsProductEnergyTypeSaveRequest;
 import com.usky.ems.service.vo.EmsProjectResponse;
 import com.usky.ems.service.vo.EmsProjectSaveRequest;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
 /**
  * 项目(ems_project)新增、修改、删除
  * 基础路径前缀:/project(网关再加 /prod-api/service-ems)
@@ -17,6 +24,59 @@ public class EmsProjectController {
 
     @Autowired
     private EmsProjectService emsProjectService;
+    @Autowired
+    private EmsEnergyItemCodeService emsEnergyItemCodeService;
+
+    /**
+     * 能耗类型列表(SELECT * FROM ems_energy_item_code WHERE parent_code = 0)
+     */
+    @GetMapping("/energy-type/list")
+    public ApiResult<List<EmsEnergyItemCode>> listEnergyTypes(
+            @RequestParam(required = false) Integer energyType) {
+        return ApiResult.success(emsEnergyItemCodeService.listEnergyTypes(energyType));
+    }
+
+    /**
+     * 能耗分项列表(parent_code 为能耗类型编码)
+     */
+    @GetMapping("/energy-item/list")
+    public ApiResult<List<EmsEnergyItemCode>> listEnergySubItems() {
+        return ApiResult.success(emsEnergyItemCodeService.listEnergySubItems());
+    }
+
+    /**
+     * 区域设备信息:按空间及其子空间关联网关,查询 dmp_device 子设备列表
+     */
+    @GetMapping("/area/device/list")
+    public ApiResult<List<DmpDevice>> listAreaDevices(@RequestParam Integer spaceId) {
+        return ApiResult.success(emsProjectService.listAreaDevices(spaceId));
+    }
+
+    /**
+     * 区域能耗分项类型信息:按区域设备关联的分项编码查询 ems_energy_item_code
+     */
+    @GetMapping("/area/device-item-code/list")
+    public ApiResult<List<EmsEnergyItemCode>> listAreaDeviceItemCodes(@RequestParam Integer spaceId) {
+        return ApiResult.success(emsProjectService.listAreaDeviceItemCodes(spaceId));
+    }
+
+    /**
+     * 产品关联的能源类型(ems_product_energy_type)
+     */
+    @PostMapping("/product-energy-type")
+    public ApiResult<Void> saveProductEnergyTypes(@RequestBody EmsProductEnergyTypeSaveRequest request) {
+        emsProjectService.saveProductEnergyTypes(request);
+        return ApiResult.success();
+    }
+
+    /**
+     * 设备关联的能源分项(ems_device_item_code)
+     */
+    @PostMapping("/device-item-code")
+    public ApiResult<Void> saveDeviceItemCodes(@RequestBody EmsDeviceItemCodeSaveRequest request) {
+        emsProjectService.saveDeviceItemCodes(request);
+        return ApiResult.success();
+    }
 
     @PostMapping
     public ApiResult<EmsProjectResponse> create(@RequestBody EmsProjectSaveRequest request) {

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

@@ -4,6 +4,7 @@ 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 io.swagger.annotations.ApiOperation;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -109,4 +110,61 @@ public class EmsReportController {
             HttpServletResponse response) {
         emsReportService.exportCollection(deviceIds, attributePointIds, timeDimension, timeValue, format, response);
     }
+
+    /**
+     * 1. 能源报表
+     * 支持按设备列表、功能点、时间类型(时/日/月/年)聚合数据
+     *
+     * @param request 能源报表请求参数
+     * @return 能源报表响应数据
+     */
+    @PostMapping("/energy")
+    @ApiOperation("能源报表")
+    public ApiResult<EnergyReportResponse> getEnergyReport(@RequestBody EnergyReportRequest request) {
+        return ApiResult.success(emsReportService.getEnergyReport(request));
+    }
+
+    /**
+     * 能源报表导出 Excel(请求体与 /report/energy 一致)
+     */
+    @PostMapping("/energy/export")
+    @ApiOperation("能源报表导出")
+    public void exportEnergyReport(@RequestBody EnergyReportRequest request, HttpServletResponse response) {
+        emsReportService.exportEnergyReport(request, response);
+    }
+
+    /**
+     * 2. 分项报表
+     * 支持按空间ID、分项编码、日期类型统计能耗
+     *
+     * @param request 分项报表请求参数
+     * @return 分项报表响应数据
+     */
+    @PostMapping("/item")
+    @ApiOperation("分项报表")
+    public ApiResult<ItemReportResponse> getItemReport(@RequestBody ItemReportRequest request) {
+        return ApiResult.success(emsReportService.getItemReport(request));
+    }
+
+    /**
+     * 3. 区域报表
+     * 支持按区域、分项、日期类型统计能耗
+     *
+     * @param request 区域报表请求参数
+     * @return 区域报表响应数据
+     */
+    @PostMapping("/space")
+    @ApiOperation("区域报表")
+    public ApiResult<SpaceReportResponse> getSpaceReport(@RequestBody SpaceReportRequest request) {
+        return ApiResult.success(emsReportService.getSpaceReport(request));
+    }
+
+    /**
+     * 区域报表导出 Excel(请求体与 /report/space 一致)
+     */
+    @PostMapping("/space/export")
+    @ApiOperation("区域报表导出")
+    public void exportSpaceReport(@RequestBody SpaceReportRequest request, HttpServletResponse response) {
+        emsReportService.exportSpaceReport(request, response);
+    }
 }

+ 20 - 4
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsDeviceItemCode.java

@@ -8,6 +8,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 import java.io.Serializable;
+import java.time.LocalDateTime;
 
 /**
  * 设备与能耗分项关联(ems_device_item_code)
@@ -22,12 +23,27 @@ public class EmsDeviceItemCode implements Serializable {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
-    @TableField("device_id")
-    private String deviceId;
+    @TableField("device_uuid")
+    private String deviceUuid;
 
     @TableField("item_code")
     private String itemCode;
 
-    @TableField("project_id")
-    private Long projectId;
+    @TableField("energy_type")
+    private Integer energyType;
+
+    @TableField("created_by")
+    private String createdBy;
+
+    @TableField("created_time")
+    private LocalDateTime createdTime;
+
+    @TableField("updated_by")
+    private String updatedBy;
+
+    @TableField("updated_time")
+    private LocalDateTime updatedTime;
+
+    /** 租户号 */
+    private Integer tenantId;
 }

+ 22 - 6
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsEnergyItemCode.java

@@ -9,6 +9,7 @@ import lombok.EqualsAndHashCode;
 
 import java.io.Serializable;
 import java.time.LocalDateTime;
+import java.util.List;
 
 /**
  * 能源分项编码(leo.ems_energy_item_code),用于能源类型列表
@@ -33,12 +34,27 @@ public class EmsEnergyItemCode implements Serializable {
     private String identifier;
     @TableField("energy_type")
     private Integer energyType;
+
+    @TableField("created_by")
+    private String createdBy;
+
+    @TableField("created_time")
+    private LocalDateTime createdTime;
+
     @TableField("updated_by")
     private Long updatedBy;
-    @TableField("update_time")
-    private LocalDateTime updateTime;
-    @TableField("created_by")
-    private Long createdBy;
-    @TableField("create_time")
-    private LocalDateTime createTime;
+
+    @TableField("updated_time")
+    private LocalDateTime updatedTime;
+
+    /** 租户号 */
+    private Integer tenantId;
+
+    /** 能耗绑定产品id集合 **/
+    @TableField(exist = false)
+    private List<Long> productIdList;
+
+    /** 能耗分项绑定设备uuid集合 **/
+    @TableField(exist = false)
+    private List<String> deviceUuidList;
 }

+ 16 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProductEnergyType.java

@@ -8,6 +8,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 import java.io.Serializable;
+import java.time.LocalDateTime;
 
 /**
  * 产品能源类型关联(ems_product_energy_type)
@@ -27,4 +28,19 @@ public class EmsProductEnergyType implements Serializable {
 
     @TableField("energy_type")
     private Integer energyType;
+
+    @TableField("created_by")
+    private String createdBy;
+
+    @TableField("created_time")
+    private LocalDateTime createdTime;
+
+    @TableField("updated_by")
+    private String updatedBy;
+
+    @TableField("updated_time")
+    private LocalDateTime updatedTime;
+
+    /** 租户号 */
+    private Integer tenantId;
 }

+ 10 - 7
service-ems/service-ems-biz/src/main/java/com/usky/ems/domain/EmsProjectConversionFactor.java

@@ -34,15 +34,18 @@ public class EmsProjectConversionFactor implements Serializable {
 
     private BigDecimal value;
 
+    @TableField("created_by")
+    private String createdBy;
+
+    @TableField("created_time")
+    private LocalDateTime createdTime;
+
     @TableField("updated_by")
     private String updatedBy;
 
-    @TableField("update_time")
-    private LocalDateTime updateTime;
-
-    @TableField("created_by")
-    private String createdBy;
+    @TableField("updated_time")
+    private LocalDateTime updatedTime;
 
-    @TableField("create_time")
-    private LocalDateTime createTime;
+    /** 租户号 */
+    private Integer tenantId;
 }

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

@@ -18,4 +18,6 @@ public interface EmsAnalysisService {
     EmsCompareResponse getCompare(EmsCompareRequest request);
 
     EmsAverageResponse getAverage(Long projectId, String timeDimension, String timeValue, Long energyTypeId);
+
+    EmsAverageResponseVO getRegionalAnalysis(EmsAverageRequest request);
 }

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

@@ -15,5 +15,15 @@ public interface EmsEnergyItemCodeService extends CrudService<EmsEnergyItemCode>
      * 查询指定空间下的分项能耗配置(基于能耗公式和分项编码组装树形结构)。
      */
     List<EnergyItemCodeVO> querySpaceEnergyItem(Long spaceId);
+
+    /**
+     * 能耗类型列表:parent_code = 0,可按 energyType 筛选
+     */
+    List<EmsEnergyItemCode> listEnergyTypes(Integer energyType);
+
+    /**
+     * 能耗分项列表:parent_code 为能耗类型(parent_code = 0)的编码
+     */
+    List<EmsEnergyItemCode> listEnergySubItems();
 }
 

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

@@ -1,8 +1,14 @@
 package com.usky.ems.service;
 
+import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.EmsEnergyItemCode;
+import com.usky.ems.service.vo.EmsDeviceItemCodeSaveRequest;
+import com.usky.ems.service.vo.EmsProductEnergyTypeSaveRequest;
 import com.usky.ems.service.vo.EmsProjectResponse;
 import com.usky.ems.service.vo.EmsProjectSaveRequest;
 
+import java.util.List;
+
 /**
  * 项目(ems_project)维护:新增、修改、删除(含空间树、省市区校验、设备系统关联)
  */
@@ -13,4 +19,24 @@ public interface EmsProjectService {
     EmsProjectResponse update(EmsProjectSaveRequest request);
 
     void remove(Long spaceId);
+
+    /**
+     * 保存产品关联的能源类型:先删除当前租户下该能耗类型的记录,再按产品列表插入
+     */
+    void saveProductEnergyTypes(EmsProductEnergyTypeSaveRequest request);
+
+    /**
+     * 保存设备关联的能源分项:先删除当前租户下该分项编码的记录,再按设备列表插入
+     */
+    void saveDeviceItemCodes(EmsDeviceItemCodeSaveRequest request);
+
+    /**
+     * 区域设备信息:按空间及其子空间关联网关,查询 dmp_device 子设备列表
+     */
+    List<DmpDevice> listAreaDevices(Integer spaceId);
+
+    /**
+     * 区域能耗分项类型信息:按区域设备关联的分项编码查询 ems_energy_item_code
+     */
+    List<EmsEnergyItemCode> listAreaDeviceItemCodes(Integer spaceId);
 }

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

@@ -29,4 +29,38 @@ public interface EmsReportService {
     EmsEnergyStatisticsResponse getCollectionStatistics(EmsEnergyStatisticsRequest request);
 
     void exportCollection(String deviceIds, String attributePointIds, String timeDimension, String timeValue, String format, HttpServletResponse response);
+
+    /**
+     * 获取能源报表数据
+     *
+     * @param request 请求参数
+     * @return 能源报表响应
+     */
+    EnergyReportResponse getEnergyReport(EnergyReportRequest request);
+
+    /**
+     * 获取分项报表数据
+     *
+     * @param request 请求参数
+     * @return 分项报表响应
+     */
+    ItemReportResponse getItemReport(ItemReportRequest request);
+
+    /**
+     * 获取区域报表数据
+     *
+     * @param request 请求参数
+     * @return 区域报表响应
+     */
+    SpaceReportResponse getSpaceReport(SpaceReportRequest request);
+
+    /**
+     * 导出能源报表 Excel
+     */
+    void exportEnergyReport(EnergyReportRequest request, HttpServletResponse response);
+
+    /**
+     * 导出区域报表 Excel
+     */
+    void exportSpaceReport(SpaceReportRequest request, HttpServletResponse response);
 }

Разлика између датотеке није приказан због своје велике величине
+ 254 - 471
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsAnalysisServiceImpl.java


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

@@ -3,10 +3,15 @@ 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.common.security.utils.SecurityUtils;
+import com.usky.ems.domain.EmsDeviceItemCode;
 import com.usky.ems.domain.EmsEnergyConsumptionFormula;
 import com.usky.ems.domain.EmsEnergyItemCode;
+import com.usky.ems.domain.EmsProductEnergyType;
+import com.usky.ems.mapper.EmsDeviceItemCodeMapper;
 import com.usky.ems.mapper.EmsEnergyConsumptionFormulaMapper;
 import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
+import com.usky.ems.mapper.EmsProductEnergyTypeMapper;
 import com.usky.ems.service.EmsEnergyItemCodeService;
 import com.usky.ems.service.vo.EnergyItemCodeVO;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -25,6 +30,10 @@ public class EmsEnergyItemCodeServiceImpl
 
     @Autowired
     private EmsEnergyConsumptionFormulaMapper energyConsumptionFormulaMapper;
+    @Autowired
+    private EmsProductEnergyTypeMapper emsProductEnergyTypeMapper;
+    @Autowired
+    private EmsDeviceItemCodeMapper emsDeviceItemCodeMapper;
 
     @Override
     public List<EnergyItemCodeVO> querySpaceEnergyItem(Long spaceId) {
@@ -106,5 +115,80 @@ public class EmsEnergyItemCodeServiceImpl
 
         return result;
     }
+
+    @Override
+    public List<EmsEnergyItemCode> listEnergyTypes(Integer energyType) {
+        List<EmsEnergyItemCode> energyTypes = listRootEnergyTypes(energyType);
+        if (energyTypes.isEmpty()) {
+            return energyTypes;
+        }
+        Map<Integer, List<Long>> productIdsByEnergyType = loadProductIdsByEnergyType(SecurityUtils.getTenantId());
+        for (EmsEnergyItemCode energyType1 : energyTypes) {
+            energyType1.setProductIdList(
+                    productIdsByEnergyType.getOrDefault(energyType1.getEnergyType(), Collections.emptyList()));
+        }
+        return energyTypes;
+    }
+
+    @Override
+    public List<EmsEnergyItemCode> listEnergySubItems() {
+        List<String> rootCodes = listRootEnergyTypes(null).stream()
+                .map(EmsEnergyItemCode::getCode)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        if (rootCodes.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<EmsEnergyItemCode> energySubItems = this.list(
+                Wrappers.<EmsEnergyItemCode>lambdaQuery()
+                        .in(EmsEnergyItemCode::getParentCode, rootCodes)
+                        .orderByAsc(EmsEnergyItemCode::getParentCode, EmsEnergyItemCode::getCode));
+        if (energySubItems.isEmpty()) {
+            return energySubItems;
+        }
+        Map<String, List<String>> deviceUuidsByItemCode = loadDeviceUuidsByItemCode(SecurityUtils.getTenantId());
+        for (EmsEnergyItemCode energySubItem : energySubItems) {
+            energySubItem.setDeviceUuidList(
+                    deviceUuidsByItemCode.getOrDefault(energySubItem.getCode(), Collections.emptyList()));
+        }
+        return energySubItems;
+    }
+
+    private List<EmsEnergyItemCode> listRootEnergyTypes(Integer energyType) {
+        LambdaQueryWrapper<EmsEnergyItemCode> wrapper = Wrappers.<EmsEnergyItemCode>lambdaQuery()
+                .eq(EmsEnergyItemCode::getParentCode, "0")
+                .eq(energyType != null, EmsEnergyItemCode::getEnergyType, energyType)
+                .orderByAsc(EmsEnergyItemCode::getCode);
+        return this.list(wrapper);
+    }
+
+    private Map<Integer, List<Long>> loadProductIdsByEnergyType(Integer tenantId) {
+        List<EmsProductEnergyType> bindings = emsProductEnergyTypeMapper.selectList(
+                Wrappers.<EmsProductEnergyType>lambdaQuery()
+                        .eq(EmsProductEnergyType::getTenantId, tenantId));
+        if (bindings.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return bindings.stream()
+                .filter(binding -> binding.getEnergyType() != null && binding.getProductId() != null)
+                .collect(Collectors.groupingBy(
+                        EmsProductEnergyType::getEnergyType,
+                        Collectors.mapping(EmsProductEnergyType::getProductId, Collectors.toList())));
+    }
+
+    private Map<String, List<String>> loadDeviceUuidsByItemCode(Integer tenantId) {
+        List<EmsDeviceItemCode> bindings = emsDeviceItemCodeMapper.selectList(
+                Wrappers.<EmsDeviceItemCode>lambdaQuery()
+                        .eq(EmsDeviceItemCode::getTenantId, tenantId));
+        if (bindings.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return bindings.stream()
+                .filter(binding -> binding.getItemCode() != null && binding.getDeviceUuid() != null)
+                .collect(Collectors.groupingBy(
+                        EmsDeviceItemCode::getItemCode,
+                        Collectors.mapping(EmsDeviceItemCode::getDeviceUuid, Collectors.toList())));
+    }
 }
 

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

@@ -1,16 +1,29 @@
 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.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.ems.domain.BaseSpace;
+import com.usky.ems.domain.BaseSpaceGateway;
+import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.EmsDeviceItemCode;
+import com.usky.ems.domain.EmsEnergyItemCode;
+import com.usky.ems.domain.EmsProductEnergyType;
 import com.usky.ems.domain.EmsProject;
 import com.usky.ems.domain.EmsProjectDeviceSystem;
+import com.usky.ems.mapper.BaseSpaceGatewayMapper;
+import com.usky.ems.mapper.DmpDeviceMapper;
+import com.usky.ems.mapper.EmsDeviceItemCodeMapper;
+import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
+import com.usky.ems.mapper.EmsProductEnergyTypeMapper;
 import com.usky.ems.mapper.EmsProjectDeviceSystemMapper;
 import com.usky.ems.mapper.EmsProjectMapper;
 import com.usky.ems.service.BaseSpaceService;
 import com.usky.ems.service.EmsProjectService;
 import com.usky.ems.service.EmsSystemDictRegionService;
+import com.usky.ems.service.vo.EmsDeviceItemCodeSaveRequest;
+import com.usky.ems.service.vo.EmsProductEnergyTypeSaveRequest;
 import com.usky.ems.service.vo.EmsProjectResponse;
 import com.usky.ems.service.vo.EmsProjectSaveRequest;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -20,9 +33,11 @@ import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
 
 import java.time.LocalDateTime;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 /**
  * 项目维护实现(逻辑参考原 ProjectServiceImpl:省市区校验、创建项目空间、设备系统关联、删除前校验叶子节点等)
@@ -40,6 +55,16 @@ public class EmsProjectServiceImpl implements EmsProjectService {
     private BaseSpaceService baseSpaceService;
     @Autowired
     private EmsSystemDictRegionService emsSystemDictRegionService;
+    @Autowired
+    private EmsProductEnergyTypeMapper emsProductEnergyTypeMapper;
+    @Autowired
+    private EmsDeviceItemCodeMapper emsDeviceItemCodeMapper;
+    @Autowired
+    private BaseSpaceGatewayMapper baseSpaceGatewayMapper;
+    @Autowired
+    private DmpDeviceMapper dmpDeviceMapper;
+    @Autowired
+    private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -103,6 +128,151 @@ public class EmsProjectServiceImpl implements EmsProjectService {
         baseSpaceService.removeById(spaceId);
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void saveProductEnergyTypes(EmsProductEnergyTypeSaveRequest request) {
+        if (request == null || request.getEnergyType() == null) {
+            throw new BusinessException("能耗类型不能为空");
+        }
+        Integer tenantId = SecurityUtils.getTenantId();
+        emsProductEnergyTypeMapper.delete(
+                new LambdaQueryWrapper<EmsProductEnergyType>()
+                        .eq(EmsProductEnergyType::getTenantId, tenantId)
+                        .eq(EmsProductEnergyType::getEnergyType, request.getEnergyType()));
+        if (CollectionUtils.isEmpty(request.getProductIdList())) {
+            return;
+        }
+        LocalDateTime now = LocalDateTime.now();
+        String auditUser = SecurityUtils.getUsername();
+        for (Long productId : request.getProductIdList()) {
+            if (productId == null) {
+                continue;
+            }
+            EmsProductEnergyType row = new EmsProductEnergyType();
+            row.setProductId(productId);
+            row.setEnergyType(request.getEnergyType());
+            row.setTenantId(tenantId);
+            row.setCreatedBy(auditUser);
+            row.setUpdatedBy(auditUser);
+            row.setCreatedTime(now);
+            row.setUpdatedTime(now);
+            emsProductEnergyTypeMapper.insert(row);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void saveDeviceItemCodes(EmsDeviceItemCodeSaveRequest request) {
+        if (request == null || request.getEnergyType() == null) {
+            throw new BusinessException("能耗类型不能为空");
+        }
+        if (!StringUtils.hasText(request.getItemCode())) {
+            throw new BusinessException("能耗分项编码不能为空");
+        }
+        Integer tenantId = SecurityUtils.getTenantId();
+        String itemCode = request.getItemCode().trim();
+        emsDeviceItemCodeMapper.delete(
+                new LambdaQueryWrapper<EmsDeviceItemCode>()
+                        .eq(EmsDeviceItemCode::getTenantId, tenantId)
+                        .eq(EmsDeviceItemCode::getItemCode, itemCode));
+        if (CollectionUtils.isEmpty(request.getDeviceUuidList())) {
+            return;
+        }
+        LocalDateTime now = LocalDateTime.now();
+        String auditUser = SecurityUtils.getUsername();
+        for (String deviceUuid : request.getDeviceUuidList()) {
+            if (!StringUtils.hasText(deviceUuid)) {
+                continue;
+            }
+            EmsDeviceItemCode row = new EmsDeviceItemCode();
+            row.setDeviceUuid(deviceUuid.trim());
+            row.setItemCode(itemCode);
+            row.setEnergyType(request.getEnergyType());
+            row.setTenantId(tenantId);
+            row.setCreatedBy(auditUser);
+            row.setUpdatedBy(auditUser);
+            row.setCreatedTime(now);
+            row.setUpdatedTime(now);
+            emsDeviceItemCodeMapper.insert(row);
+        }
+    }
+
+    @Override
+    public List<DmpDevice> listAreaDevices(Integer spaceId) {
+        List<String> gatewayUuids = resolveGatewayUuidsBySpaceId(spaceId);
+        if (gatewayUuids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return dmpDeviceMapper.selectList(
+                Wrappers.<DmpDevice>lambdaQuery()
+                        .in(DmpDevice::getGatewayUuid, gatewayUuids)
+                        .eq(DmpDevice::getCategoryType, 3)
+                        .eq(DmpDevice::getDeleteFlag, 0));
+    }
+
+    @Override
+    public List<EmsEnergyItemCode> listAreaDeviceItemCodes(Integer spaceId) {
+        List<DmpDevice> devices = listAreaDevices(spaceId);
+        if (devices.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<String> deviceUuids = devices.stream()
+                .map(DmpDevice::getDeviceUuid)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
+        if (deviceUuids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<EmsDeviceItemCode> deviceItemCodes = emsDeviceItemCodeMapper.selectList(
+                Wrappers.<EmsDeviceItemCode>lambdaQuery()
+                        .in(EmsDeviceItemCode::getDeviceUuid, deviceUuids)
+                        .eq(EmsDeviceItemCode::getTenantId, SecurityUtils.getTenantId()));
+        if (deviceItemCodes.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<String> itemCodes = deviceItemCodes.stream()
+                .map(EmsDeviceItemCode::getItemCode)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
+        if (itemCodes.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return emsEnergyItemCodeMapper.selectList(
+                Wrappers.<EmsEnergyItemCode>lambdaQuery()
+                        .in(EmsEnergyItemCode::getCode, itemCodes)
+                        .orderByAsc(EmsEnergyItemCode::getCode));
+    }
+
+    private List<String> resolveGatewayUuidsBySpaceId(Integer spaceId) {
+        if (spaceId == null) {
+            throw new BusinessException("空间id不能为空");
+        }
+        BaseSpace space = baseSpaceService.getById(spaceId.longValue());
+        if (space == null) {
+            throw new BusinessException("空间不存在");
+        }
+        List<Long> spaceIds = baseSpaceService.getAuthorizedSpaceIds(space.getId());
+        if (spaceIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<BaseSpaceGateway> links = baseSpaceGatewayMapper.selectList(
+                Wrappers.<BaseSpaceGateway>lambdaQuery()
+                        .in(BaseSpaceGateway::getSpaceId, spaceIds));
+        if (CollectionUtils.isEmpty(links)) {
+            return Collections.emptyList();
+        }
+        return links.stream()
+                .map(BaseSpaceGateway::getGatewayUuid)
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
     /**
      * 对应原 UserService.hasLoggedUser(projectId)。接入统一用户/权限模块后可在此查询用户与项目绑定关系。
      */

+ 1782 - 7
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsReportServiceImpl.java

@@ -1,15 +1,32 @@
 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.core.bean.ApiResult;
+import com.usky.common.core.exception.BusinessException;
 import com.usky.common.security.utils.SecurityUtils;
-import com.usky.ems.domain.DmpDevice;
-import com.usky.ems.domain.EmsDevice;
-import com.usky.ems.domain.EmsDeviceFunction;
-import com.usky.ems.mapper.DmpDeviceMapper;
-import com.usky.ems.mapper.EmsDeviceFunctionMapper;
-import com.usky.ems.mapper.EmsDeviceMapper;
+import com.usky.demo.RemoteTsdbProxyService;
+import com.usky.demo.domain.HistorysInnerRequestVO;
+import com.usky.demo.domain.HistorysInnerResultVO;
+import com.usky.demo.domain.MetricVO;
+import com.usky.ems.domain.*;
+import com.usky.ems.mapper.*;
 import com.usky.ems.service.EmsReportService;
 import com.usky.ems.service.vo.*;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.FillPatternType;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.VerticalAlignment;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
@@ -17,7 +34,15 @@ import org.springframework.util.StringUtils;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -25,14 +50,46 @@ import java.util.stream.Collectors;
  * 统计报表服务实现(设备列表基于 leo 设备与属性点位,统计与导出为占位/预留)
  */
 @Service
+@Slf4j
 public class EmsReportServiceImpl implements EmsReportService {
 
     private static final String[] ENERGY_TYPE_NAMES = {"", "电", "水", "气"};
 
+    private static final String ENERGY_NO_DATA = "-";
+    private static final String ENERGY_ZERO = "0.00";
+
+    private static final Set<String> CUMULATIVE_ENERGY_METRICS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            "totalactiveenergyf",
+            "totalactiveenergyr",
+            "totalreactiveenergyf",
+            "totalreactiveenergyr"
+    )));
+
+    private enum MetricAggregateType {
+        /** 累计表计:时段内末次读数 - 首次读数 */
+        CUMULATIVE_DELTA,
+        /** 瞬时量:时段内算术平均 */
+        INSTANTANEOUS_AVG
+    }
+
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
     @Autowired
     private DmpDeviceMapper dmpDeviceMapper;
     @Autowired
     private EmsDeviceFunctionMapper emsDeviceFunctionMapper;
+    @Autowired
+    private RemoteTsdbProxyService remoteTsdbProxyService;
+    @Autowired
+    private BaseSpaceGatewayMapper baseSpaceGatewayMapper;
+    @Autowired
+    private EmsDeviceItemCodeMapper emsDeviceItemCodeMapper;
+    @Autowired
+    private EmsProductEnergyTypeMapper emsProductEnergyTypeMapper;
+    @Autowired
+    private BaseSpaceMapper baseSpaceMapper;
+    @Autowired
+    private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
 
     private String energyTypeName(Long energyTypeId) {
         if (energyTypeId == null || energyTypeId < 1 || energyTypeId > 3) return "";
@@ -41,7 +98,7 @@ public class EmsReportServiceImpl implements EmsReportService {
 
     @Override
     public EmsReportDevicesResponse getEnergyDevices(Long energyTypeId, String keyword, Long projectId) {
-        LambdaQueryWrapper<DmpDevice> q = new LambdaQueryWrapper<DmpDevice>().eq(DmpDevice::getDeleteFlag,0).eq(DmpDevice::getTenantId, SecurityUtils.getTenantId());
+        LambdaQueryWrapper<DmpDevice> q = new LambdaQueryWrapper<DmpDevice>().eq(DmpDevice::getDeleteFlag, 0).eq(DmpDevice::getTenantId, SecurityUtils.getTenantId());
         if (StringUtils.hasText(keyword)) {
             q.and(w -> w.like(DmpDevice::getDeviceName, keyword));
         }
@@ -150,4 +207,1722 @@ public class EmsReportServiceImpl implements EmsReportService {
             throw new RuntimeException("导出失败", e);
         }
     }
+
+    @Override
+    public EnergyReportResponse getEnergyReport(EnergyReportRequest request) {
+        if (request == null || request.getDeviceList() == null || request.getDeviceList().isEmpty()) {
+            return createEmptyEnergyReport();
+        }
+
+        // 解析时间
+        LocalDateTime startTime = parseDateTime(request.getStartTime());
+        LocalDateTime endTime = parseDateTime(request.getEndTime());
+        if (startTime == null || endTime == null) {
+            return createEmptyEnergyReport();
+        }
+
+        // 校验时间参数
+        validateTimeParams(startTime, endTime, request.getTimeType());
+
+        // 生成列定义
+        List<EnergyReportResponse.ColumnItem> columnList = generateEnergyReportColumns(request.getTimeType(), startTime, endTime);
+
+        // 生成数据列表(按设备分组的二维数组)
+        List<List<EnergyReportResponse.ValueItem>> valueList = generateEnergyReportData(request, startTime, endTime);
+
+        EnergyReportResponse response = new EnergyReportResponse();
+        response.setColumnList(columnList);
+        response.setValueList(valueList);
+
+        return response;
+    }
+
+    @Override
+    public ItemReportResponse getItemReport(ItemReportRequest request) {
+        if (request == null || request.getItemCodes() == null || request.getItemCodes().isEmpty()) {
+            return createEmptyItemReport();
+        }
+
+        // 解析时间
+        LocalDateTime startTime = parseDateTime(request.getStartTime());
+        LocalDateTime endTime = parseDateTime(request.getEndTime());
+        if (startTime == null || endTime == null) {
+            return createEmptyItemReport();
+        }
+
+        // 校验时间参数
+        validateTimeParams(startTime, endTime, request.getTimeType());
+
+        // 生成标题列
+        List<ItemReportResponse.TitleItem> titleList = generateItemReportTitles(request.getTimeType(), startTime, endTime);
+
+        // 生成数据列表
+        List<Map<String, Object>> dataList = generateItemReportData(request, startTime, endTime);
+
+        ItemReportResponse response = new ItemReportResponse();
+        response.setTitleList(titleList);
+        response.setDataList(dataList);
+
+        return response;
+    }
+
+    @Override
+    public SpaceReportResponse getSpaceReport(SpaceReportRequest request) {
+        if (request == null || request.getSpaces() == null || request.getSpaces().isEmpty()) {
+            return createEmptySpaceReport();
+        }
+
+        // 解析时间
+        LocalDateTime startTime = parseDateTime(request.getStartTime());
+        LocalDateTime endTime = parseDateTime(request.getEndTime());
+        if (startTime == null || endTime == null) {
+            return createEmptySpaceReport();
+        }
+
+        // 校验时间参数
+        validateTimeParams(startTime, endTime, request.getTimeType());
+
+        // 生成列定义
+        List<EnergyReportResponse.ColumnItem> columnList = generateSpaceReportColumns(request.getTimeType(), startTime, endTime);
+
+        // 生成数据列表(按区域/空间分组的二维数组)
+        List<List<Map<String, Object>>> valueList = generateSpaceReportData(request, startTime, endTime);
+
+        // 计算总计
+        Number total = calculateSpaceReportTotal(valueList);
+
+        SpaceReportResponse response = new SpaceReportResponse();
+        response.setColumnList(columnList);
+        response.setValueList(valueList);
+        response.setTotal(total);
+
+        return response;
+    }
+
+    @Override
+    public void exportEnergyReport(EnergyReportRequest request, HttpServletResponse response) {
+        EnergyReportResponse report = getEnergyReport(request);
+        List<EnergyReportResponse.ColumnItem> columns = report.getColumnList() != null
+                ? report.getColumnList() : Collections.emptyList();
+        List<List<EnergyReportResponse.ValueItem>> groupedRows = report.getValueList() != null
+                ? report.getValueList() : Collections.emptyList();
+        writeEnergyReportExcel(response, buildExportFileName("能源报表"), columns, groupedRows);
+    }
+
+    @Override
+    public void exportSpaceReport(SpaceReportRequest request, HttpServletResponse response) {
+        SpaceReportResponse report = getSpaceReport(request);
+        List<EnergyReportResponse.ColumnItem> columns = report.getColumnList() != null
+                ? report.getColumnList() : Collections.emptyList();
+        List<List<Map<String, Object>>> groupedRows = report.getValueList() != null
+                ? report.getValueList() : Collections.emptyList();
+        writeSpaceReportExcel(response, buildExportFileName("区域报表"), columns, groupedRows);
+    }
+
+    // ==================== 能源报表私有方法 ====================
+
+    /**
+     * 生成能源报表列定义
+     */
+    private List<EnergyReportResponse.ColumnItem> generateEnergyReportColumns(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        List<EnergyReportResponse.ColumnItem> columns = new ArrayList<>();
+
+        // 固定列
+        columns.add(createColumn("设备名称", "deviceName", true));
+        columns.add(createColumn("通讯地址", "commAddress", true));
+        columns.add(createColumn("功能点", "identifier", true));
+        columns.add(createColumn("合计", "total", true));
+
+        // 自定义查询仅返回合计,不生成动态时间列
+        if (isCustomTimeType(timeType)) {
+            return columns;
+        }
+
+        // 动态时间列
+        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
+        for (String label : timeLabels) {
+            columns.add(createColumn(label, "_" + label.replace("时", "").replace("日", "").replace("月", ""), false));
+        }
+
+        return columns;
+    }
+
+    /**
+     * 生成能源报表数据(按设备分组的二维数组)
+     * 外层 List 代表不同的设备
+     * 内层 List 包含该设备下所有请求的功能点(属性)数据
+     */
+    private List<List<EnergyReportResponse.ValueItem>> generateEnergyReportData(EnergyReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
+        List<List<EnergyReportResponse.ValueItem>> groupedValueList = new ArrayList<>();
+
+        // 获取设备uuid列表
+        List<Integer> deviceIds = request.getDeviceList().stream()
+                .filter(device -> device != null && device.getId() != null)
+                .map(EnergyReportRequest.DeviceItem::getId)
+                .collect(Collectors.toList());
+        List<String> deviceUuids = getUuidList(deviceIds);
+
+        // 查询tsdb历史数据
+        HistorysInnerRequestVO historyRequestVO = new HistorysInnerRequestVO();
+        historyRequestVO.setDeviceuuid(deviceUuids);
+        historyRequestVO.setStartTime(request.getStartTime());
+        historyRequestVO.setEndTime(request.getEndTime());
+        historyRequestVO.setMetrics(request.getFuncList() != null && !request.getFuncList().isEmpty()
+                ? request.getFuncList().stream().map(EnergyReportRequest.FuncItem::getIdentifier)
+                .map(String::toLowerCase)
+                .collect(Collectors.toList())
+                : new ArrayList<>());
+        log.info("历史数据接口请求:{}", historyRequestVO);
+        ApiResult<List<HistorysInnerResultVO>> listHistoryData = remoteTsdbProxyService.queryHistoryDeviceData(historyRequestVO);
+
+        // 判断历史数据是否为空
+        boolean hasData = !isHistoryDataEmpty(listHistoryData);
+        //log.warn("历史数据为空,设备uuid:{}", deviceUuids);
+        log.info("历史数据接口返回:{}", listHistoryData);
+
+        // 如果有数据,解析TSDB返回的结果
+        Map<String, Map<String, List<Map<String, Object>>>> tsdbDataMap = new HashMap<>();
+        Map<String, String> deviceIdMap = new HashMap<>(); // deviceuuid -> device_id
+        if (hasData) {
+            tsdbDataMap = parseTsdbData(listHistoryData.getData());
+            deviceIdMap = extractDeviceIdMap(listHistoryData.getData());
+        }
+        log.info("设备id映射:{}", deviceIdMap);
+        log.info("tsdb数据:{}", tsdbDataMap);
+
+        boolean customQuery = isCustomTimeType(request.getTimeType());
+
+        for (EnergyReportRequest.DeviceItem device : request.getDeviceList()) {
+            if (device == null || StringUtils.isEmpty(device.getId())) {
+                continue;
+            }
+
+            // 为当前设备创建一个子列表
+            List<EnergyReportResponse.ValueItem> deviceValueList = new ArrayList<>();
+
+            // 获取设备UUID用于匹配TSDB数据
+            DmpDevice dmpDevice = dmpDeviceMapper.selectById(device.getId());
+            String deviceUuid = dmpDevice != null ? dmpDevice.getDeviceUuid() : null;
+
+            // 如果没有功能点列表,为设备创建一个默认记录
+            if (request.getFuncList() == null || request.getFuncList().isEmpty()) {
+                if (customQuery) {
+                    String total = ENERGY_NO_DATA;
+                    if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
+                        Map<String, List<Map<String, Object>>> deviceMetrics = tsdbDataMap.get(deviceUuid);
+                        Map.Entry<String, List<Map<String, Object>>> firstMetric = deviceMetrics.entrySet().iterator().next();
+                        total = calculateCustomPeriodTotal(
+                                firstMetric.getValue(), firstMetric.getKey(), startTime, endTime);
+                    }
+                    deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, "-", total, null));
+                } else {
+                    Map<String, String> timeData;
+                    String total;
+                    if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
+                        Map<String, List<Map<String, Object>>> deviceMetrics = tsdbDataMap.get(deviceUuid);
+                        Map.Entry<String, List<Map<String, Object>>> firstMetric = deviceMetrics.entrySet().iterator().next();
+                        timeData = buildEnergyTimeDataForMetricItems(
+                                firstMetric.getValue(), firstMetric.getKey(),
+                                request.getTimeType(), startTime, endTime);
+                        total = calculateReportTotal(
+                                timeData, firstMetric.getKey(), firstMetric.getValue(), startTime, endTime);
+                    } else {
+                        timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
+                        total = ENERGY_NO_DATA;
+                    }
+                    deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, "-", total, timeData));
+                }
+            } else {
+                for (EnergyReportRequest.FuncItem func : request.getFuncList()) {
+                    if (func == null || StringUtils.isEmpty(func.getIdentifier())) {
+                        continue;
+                    }
+
+                    String identifier = func.getIdentifierName() != null ? func.getIdentifierName() : func.getIdentifier();
+                    if (customQuery) {
+                        String total = ENERGY_NO_DATA;
+                        if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
+                            List<Map<String, Object>> metricItems = getMetricItemsIgnoreCase(
+                                    tsdbDataMap.get(deviceUuid), func.getIdentifier());
+                            total = calculateCustomPeriodTotal(
+                                    metricItems, func.getIdentifier(), startTime, endTime);
+                        }
+                        deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, identifier, total, null));
+                    } else {
+                        Map<String, String> timeData;
+                        String total;
+                        List<Map<String, Object>> metricItems = null;
+                        if (hasData && deviceUuid != null && tsdbDataMap.containsKey(deviceUuid)) {
+                            metricItems = getMetricItemsIgnoreCase(tsdbDataMap.get(deviceUuid), func.getIdentifier());
+                            timeData = buildTimeDataFromTsdbForMetric(
+                                    tsdbDataMap.get(deviceUuid),
+                                    func.getIdentifier(),
+                                    request.getTimeType(),
+                                    startTime,
+                                    endTime);
+                            total = calculateReportTotal(
+                                    timeData, func.getIdentifier(), metricItems, startTime, endTime);
+                        } else {
+                            timeData = generateEmptyTimeData(request.getTimeType(), startTime, endTime);
+                            total = ENERGY_NO_DATA;
+                        }
+                        deviceValueList.add(buildEnergyValueItem(device, deviceUuid, deviceIdMap, identifier, total, timeData));
+                    }
+                }
+            }
+
+            groupedValueList.add(deviceValueList);
+        }
+
+        return groupedValueList;
+    }
+
+    private EnergyReportResponse.ValueItem buildEnergyValueItem(EnergyReportRequest.DeviceItem device,
+                                                                String deviceUuid,
+                                                                Map<String, String> deviceIdMap,
+                                                                String identifier,
+                                                                String total,
+                                                                Map<String, String> timeData) {
+        EnergyReportResponse.ValueItem item = new EnergyReportResponse.ValueItem();
+        item.setDeviceId(deviceIdMap.getOrDefault(deviceUuid, null));
+        item.setDeviceName(device.getName());
+        item.setCommAddress(device.getCommAddress());
+        item.setIdentifier(identifier);
+        item.setTotal(total);
+        item.setTimeData(timeData);
+        return item;
+    }
+
+    private boolean isCustomTimeType(String timeType) {
+        return timeType != null && "custom".equalsIgnoreCase(timeType);
+    }
+
+    /**
+     * 自定义查询合计:按指标类型聚合(累计类求各分段差值之和,瞬时类求全区间平均)
+     */
+    private String calculateCustomPeriodTotal(List<Map<String, Object>> metricItems,
+                                              String metricIdentifier,
+                                              LocalDateTime startTime,
+                                              LocalDateTime endTime) {
+        if (metricItems == null || metricItems.isEmpty()) {
+            return ENERGY_NO_DATA;
+        }
+        String segmentTimeType = resolveSegmentTimeTypeForCustom(startTime, endTime);
+        if ("custom_range".equals(segmentTimeType)) {
+            return calculateEnergySegmentValue(metricItems, metricIdentifier, startTime, endTime, true);
+        }
+        Map<String, String> timeData = buildEnergyTimeDataForMetricItems(
+                metricItems, metricIdentifier, segmentTimeType, startTime, endTime);
+        return calculateReportTotal(timeData, metricIdentifier, metricItems, startTime, endTime);
+    }
+
+    /**
+     * 自定义查询推断分段粒度,保证与 month/date/year 的合计逻辑一致
+     */
+    private String resolveSegmentTimeTypeForCustom(LocalDateTime startTime, LocalDateTime endTime) {
+        if (startTime.toLocalDate().equals(endTime.toLocalDate())) {
+            return "date";
+        }
+        if (startTime.getYear() == endTime.getYear() && startTime.getMonthValue() == endTime.getMonthValue()) {
+            return "month";
+        }
+        if (startTime.getYear() == endTime.getYear()) {
+            return "year";
+        }
+        return "custom_range";
+    }
+
+    private Map<String, String> buildEnergyTimeDataForMetricItems(List<Map<String, Object>> metricItems,
+                                                                  String metricIdentifier,
+                                                                  String timeType,
+                                                                  LocalDateTime startTime,
+                                                                  LocalDateTime endTime) {
+        Map<String, String> timeData = new LinkedHashMap<>();
+        List<TimeSegmentBound> segments = buildReportTimeSegments(timeType, startTime, endTime);
+        for (TimeSegmentBound segment : segments) {
+            String key = toEnergyTimeDataKey(segment.getLabel());
+            timeData.put(key, calculateEnergySegmentValue(
+                    metricItems, metricIdentifier,
+                    segment.getStartInclusive(), segment.getEndExclusive(), false));
+        }
+        return timeData;
+    }
+
+    private String toEnergyTimeDataKey(String label) {
+        return "_" + label.replace("时", "").replace("日", "").replace("月", "");
+    }
+
+    private MetricAggregateType resolveMetricAggregateType(String identifier) {
+        if (!StringUtils.hasText(identifier)) {
+            return MetricAggregateType.INSTANTANEOUS_AVG;
+        }
+        if (CUMULATIVE_ENERGY_METRICS.contains(identifier.toLowerCase())) {
+            return MetricAggregateType.CUMULATIVE_DELTA;
+        }
+        return MetricAggregateType.INSTANTANEOUS_AVG;
+    }
+
+    /**
+     * 单个时间段聚合:累计类取差值,瞬时类取平均(date 按小时、month 按天、year 按月)
+     */
+    private String calculateEnergySegmentValue(List<Map<String, Object>> metricItems,
+                                               String metricIdentifier,
+                                               LocalDateTime startInclusive,
+                                               LocalDateTime endExclusive,
+                                               boolean endInclusive) {
+        TreeMap<LocalDateTime, BigDecimal> readings = endInclusive
+                ? parseMetricReadingsInRangeInclusive(metricItems, startInclusive, endExclusive)
+                : parseMetricReadingsInRange(metricItems, startInclusive, endExclusive);
+        if (readings.isEmpty()) {
+            return ENERGY_NO_DATA;
+        }
+        if (resolveMetricAggregateType(metricIdentifier) == MetricAggregateType.CUMULATIVE_DELTA) {
+            return calculateCumulativeSegmentDelta(readings);
+        }
+        return calculateInstantaneousSegmentAverage(readings);
+    }
+
+    /** 累计表计:时段末次读数 - 首次读数(至少 2 条读数) */
+    private String calculateCumulativeSegmentDelta(TreeMap<LocalDateTime, BigDecimal> readings) {
+        if (readings.size() < 2) {
+            return ENERGY_NO_DATA;
+        }
+        BigDecimal first = readings.firstEntry().getValue();
+        BigDecimal last = readings.lastEntry().getValue();
+        BigDecimal usage = last.subtract(first);
+        if (usage.compareTo(BigDecimal.ZERO) < 0) {
+            usage = last;
+        }
+        return formatEnergyReportValue(usage);
+    }
+
+    /** 瞬时量:时段内读数算术平均 */
+    private String calculateInstantaneousSegmentAverage(TreeMap<LocalDateTime, BigDecimal> readings) {
+        BigDecimal sum = BigDecimal.ZERO;
+        for (BigDecimal value : readings.values()) {
+            sum = sum.add(value);
+        }
+        return formatEnergyReportValue(sum.divide(BigDecimal.valueOf(readings.size()), 4, RoundingMode.HALF_UP));
+    }
+
+    private String formatEnergyReportValue(BigDecimal value) {
+        return value.setScale(2, RoundingMode.HALF_UP).toPlainString();
+    }
+
+    private List<Map<String, Object>> getMetricItemsIgnoreCase(Map<String, List<Map<String, Object>>> metricDataMap,
+                                                               String identifier) {
+        if (metricDataMap == null || !StringUtils.hasText(identifier)) {
+            return null;
+        }
+        List<Map<String, Object>> items = metricDataMap.get(identifier);
+        if (items != null) {
+            return items;
+        }
+        for (Map.Entry<String, List<Map<String, Object>>> entry : metricDataMap.entrySet()) {
+            if (identifier.equalsIgnoreCase(entry.getKey())) {
+                return entry.getValue();
+            }
+        }
+        return null;
+    }
+
+    private TreeMap<LocalDateTime, BigDecimal> parseMetricReadingsInRange(List<Map<String, Object>> metricItems,
+                                                                          LocalDateTime startInclusive,
+                                                                          LocalDateTime endExclusive) {
+        TreeMap<LocalDateTime, BigDecimal> readings = new TreeMap<>();
+        if (metricItems == null || metricItems.isEmpty() || !startInclusive.isBefore(endExclusive)) {
+            return readings;
+        }
+
+        for (Map<String, Object> item : metricItems) {
+            if (item == null || !item.containsKey("timestamp") || !item.containsKey("value")) {
+                continue;
+            }
+            try {
+                LocalDateTime timestamp = parseMetricTimestamp(item.get("timestamp"));
+                if (timestamp == null || timestamp.isBefore(startInclusive) || !timestamp.isBefore(endExclusive)) {
+                    continue;
+                }
+                readings.put(timestamp, new BigDecimal(item.get("value").toString()));
+            } catch (Exception e) {
+                log.warn("解析能源报表历史读数失败:{}", item, e);
+            }
+        }
+        return readings;
+    }
+
+    private TreeMap<LocalDateTime, BigDecimal> parseMetricReadingsInRangeInclusive(List<Map<String, Object>> metricItems,
+                                                                                   LocalDateTime startInclusive,
+                                                                                   LocalDateTime endInclusive) {
+        TreeMap<LocalDateTime, BigDecimal> readings = new TreeMap<>();
+        if (metricItems == null || metricItems.isEmpty()) {
+            return readings;
+        }
+
+        for (Map<String, Object> item : metricItems) {
+            if (item == null || !item.containsKey("timestamp") || !item.containsKey("value")) {
+                continue;
+            }
+            try {
+                LocalDateTime timestamp = parseMetricTimestamp(item.get("timestamp"));
+                if (timestamp == null || timestamp.isBefore(startInclusive) || timestamp.isAfter(endInclusive)) {
+                    continue;
+                }
+                readings.put(timestamp, new BigDecimal(item.get("value").toString()));
+            } catch (Exception e) {
+                log.warn("解析能源报表历史读数失败:{}", item, e);
+            }
+        }
+        return readings;
+    }
+
+    private LocalDateTime parseMetricTimestamp(Object timestampObj) {
+        if (timestampObj == null) {
+            return null;
+        }
+        String timestampStr = timestampObj.toString();
+        if (timestampStr.endsWith(".0")) {
+            timestampStr = timestampStr.substring(0, timestampStr.length() - 2);
+        }
+        return parseDateTime(timestampStr);
+    }
+
+    /**
+     * 生成空的时间数据结构(用于无数据时填充)
+     */
+    private Map<String, String> generateEmptyTimeData(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        Map<String, String> data = new LinkedHashMap<>();
+        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
+        for (String label : timeLabels) {
+            data.put(toEnergyTimeDataKey(label), ENERGY_ZERO);
+        }
+        return data;
+    }
+
+    /**
+     * 计算合计:累计类为各时段差值之和,瞬时类为查询区间内全量读数平均
+     */
+    private String calculateReportTotal(Map<String, String> timeData,
+                                        String metricIdentifier,
+                                        List<Map<String, Object>> metricItems,
+                                        LocalDateTime startTime,
+                                        LocalDateTime endTime) {
+        if (resolveMetricAggregateType(metricIdentifier) == MetricAggregateType.CUMULATIVE_DELTA) {
+            return calculateCumulativeTotal(timeData);
+        }
+        return calculateInstantaneousPeriodAverage(metricItems, startTime, endTime);
+    }
+
+    /**
+     * 累计类合计:各时段差值之和
+     */
+    private String calculateCumulativeTotal(Map<String, String> timeData) {
+        if (timeData == null || timeData.isEmpty()) {
+            return ENERGY_NO_DATA;
+        }
+
+        BigDecimal sum = BigDecimal.ZERO;
+        boolean hasNumeric = false;
+        boolean allMissing = true;
+        for (String value : timeData.values()) {
+            if (ENERGY_NO_DATA.equals(value)) {
+                continue;
+            }
+            allMissing = false;
+            try {
+                sum = sum.add(new BigDecimal(value));
+                hasNumeric = true;
+            } catch (NumberFormatException e) {
+                // 忽略无效数据
+            }
+        }
+
+        if (allMissing) {
+            return ENERGY_NO_DATA;
+        }
+        return hasNumeric ? formatEnergyReportValue(sum) : ENERGY_NO_DATA;
+    }
+
+    /**
+     * 瞬时类合计:查询区间内所有读数的算术平均
+     */
+    private String calculateInstantaneousPeriodAverage(List<Map<String, Object>> metricItems,
+                                                       LocalDateTime startTime,
+                                                       LocalDateTime endTime) {
+        if (metricItems == null || metricItems.isEmpty()) {
+            return ENERGY_NO_DATA;
+        }
+        TreeMap<LocalDateTime, BigDecimal> readings = parseMetricReadingsInRangeInclusive(
+                metricItems, startTime, endTime);
+        if (readings.isEmpty()) {
+            return ENERGY_NO_DATA;
+        }
+        return calculateInstantaneousSegmentAverage(readings);
+    }
+
+    // ==================== TSDB数据解析方法 ====================
+
+    /**
+     * 解析TSDB返回的数据
+     * 返回结构:Map<deviceuuid, Map<metric, List<metricItems>>>
+     */
+    private Map<String, Map<String, List<Map<String, Object>>>> parseTsdbData(List<HistorysInnerResultVO> tsdbResultList) {
+        Map<String, Map<String, List<Map<String, Object>>>> dataMap = new HashMap<>();
+
+        if (tsdbResultList == null || tsdbResultList.isEmpty()) {
+            return dataMap;
+        }
+
+        for (HistorysInnerResultVO result : tsdbResultList) {
+            if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
+                continue;
+            }
+
+            String deviceUuid = result.getDeviceuuid();
+            Map<String, List<Map<String, Object>>> metricMap = new HashMap<>();
+
+            if (result.getMetrics() != null && !result.getMetrics().isEmpty()) {
+                for (MetricVO metricVO : result.getMetrics()) {
+                    if (metricVO != null && StringUtils.hasText(metricVO.getMetric())) {
+                        metricMap.put(metricVO.getMetric(), metricVO.getMetricItems());
+                    }
+                }
+            }
+
+            dataMap.put(deviceUuid, metricMap);
+        }
+
+        return dataMap;
+    }
+
+    /**
+     * 从TSDB返回结果中提取device_id映射
+     * 返回结构:Map<deviceuuid, device_id>
+     */
+    private Map<String, String> extractDeviceIdMap(List<HistorysInnerResultVO> tsdbResultList) {
+        Map<String, String> deviceIdMap = new HashMap<>();
+
+        if (tsdbResultList == null || tsdbResultList.isEmpty()) {
+            return deviceIdMap;
+        }
+
+        for (HistorysInnerResultVO result : tsdbResultList) {
+            if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
+                continue;
+            }
+
+            String deviceUuid = result.getDeviceuuid();
+            // 从tags中获取device_id
+            if (result.getTags() != null && result.getTags().containsKey("device_id")) {
+                String deviceId = (String) result.getTags().get("device_id");
+                deviceIdMap.put(deviceUuid, deviceId);
+            }
+        }
+
+        return deviceIdMap;
+    }
+
+    /**
+     * 从TSDB数据中构建时间数据(取第一个功能点)
+     */
+    private Map<String, String> buildTimeDataFromTsdb(Map<String, List<Map<String, Object>>> metricDataMap,
+                                                      String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        if (metricDataMap == null || metricDataMap.isEmpty()) {
+            return generateEmptyTimeData(timeType, startTime, endTime);
+        }
+        Map.Entry<String, List<Map<String, Object>>> firstMetric = metricDataMap.entrySet().iterator().next();
+        return buildEnergyTimeDataForMetricItems(
+                firstMetric.getValue(), firstMetric.getKey(), timeType, startTime, endTime);
+    }
+
+    /**
+     * 从TSDB数据中构建指定功能点的时间数据
+     */
+    private Map<String, String> buildTimeDataFromTsdbForMetric(Map<String, List<Map<String, Object>>> metricDataMap,
+                                                               String metric,
+                                                               String timeType,
+                                                               LocalDateTime startTime,
+                                                               LocalDateTime endTime) {
+        List<Map<String, Object>> metricItems = getMetricItemsIgnoreCase(metricDataMap, metric);
+        if (metricItems == null || metricItems.isEmpty()) {
+            Map<String, String> empty = new LinkedHashMap<>();
+            for (TimeSegmentBound segment : buildReportTimeSegments(timeType, startTime, endTime)) {
+                empty.put(toEnergyTimeDataKey(segment.getLabel()), ENERGY_NO_DATA);
+            }
+            return empty;
+        }
+        return buildEnergyTimeDataForMetricItems(metricItems, metric, timeType, startTime, endTime);
+    }
+
+    // ==================== 分项报表私有方法 ====================
+
+    /**
+     * 生成分项报表标题列
+     */
+    private List<ItemReportResponse.TitleItem> generateItemReportTitles(String dateType, LocalDateTime startTime, LocalDateTime endTime) {
+        List<ItemReportResponse.TitleItem> titles = new ArrayList<>();
+
+        // 固定列
+        titles.add(createTitleItem("name", true, "分项名称", null));
+        titles.add(createTitleItem("total", true, "合计", null));
+
+        // 动态时间列
+        List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
+        int index = 1;
+        for (String label : timeLabels) {
+            titles.add(createTitleItem("_" + index, false, label, index));
+            index++;
+        }
+
+        return titles;
+    }
+
+    /**
+     * 生成分项报表数据
+     */
+    private List<Map<String, Object>> generateItemReportData(ItemReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
+        List<Map<String, Object>> dataList = new ArrayList<>();
+
+        for (ItemReportRequest.ItemCodeItem itemCode : request.getItemCodes()) {
+            if (itemCode == null || StringUtils.isEmpty(itemCode.getCode())) {
+                continue;
+            }
+
+            Map<String, Object> row = new LinkedHashMap<>();
+            row.put("name", itemCode.getName());
+
+            // 查询分项能耗数据(模拟)
+            Map<String, Number> timeData = queryItemEnergyData(itemCode.getCode(), request.getSpaceId(),
+                    startTime, endTime, request.getTimeType());
+
+            // 计算合计
+            Number total = calculateItemTotal(timeData);
+            row.put("total", total);
+
+            // 添加时间列数据
+            int index = 1;
+            for (Number value : timeData.values()) {
+                row.put("_" + index, value);
+                index++;
+            }
+
+            dataList.add(row);
+        }
+
+        return dataList;
+    }
+
+    /**
+     * 查询分项能耗数据(模拟实现)
+     */
+    private Map<String, Number> queryItemEnergyData(String itemCode, Long spaceId,
+                                                    LocalDateTime startTime, LocalDateTime endTime, String dateType) {
+        Map<String, Number> data = new LinkedHashMap<>();
+
+        // TODO: 实际应从数据库查询真实数据
+        List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
+        for (String label : timeLabels) {
+            data.put(label, 0); // 模拟数据为0
+        }
+
+        return data;
+    }
+
+    /**
+     * 计算分项合计
+     */
+    private Number calculateItemTotal(Map<String, Number> timeData) {
+        if (timeData == null || timeData.isEmpty()) {
+            return 0;
+        }
+        return sumUsageValues(timeData.values());
+    }
+
+    // ==================== 区域报表私有方法 ====================
+
+    /**
+     * 生成区域报表列定义
+     */
+    private List<EnergyReportResponse.ColumnItem> generateSpaceReportColumns(String dateType, LocalDateTime startTime, LocalDateTime endTime) {
+        List<EnergyReportResponse.ColumnItem> columns = new ArrayList<>();
+
+        // 固定列
+        columns.add(createColumn("区域名称", "spaceName", true));
+        columns.add(createColumn("分项名称", "itemName", true));
+        columns.add(createColumn("合计", "total", true));
+
+        // 动态时间列(prop 与前端约定:date/month 保留「时/日」后缀,year 去掉「月」)
+        List<String> timeLabels = generateTimeLabels(dateType, startTime, endTime);
+        for (String label : timeLabels) {
+            columns.add(createColumn(label, toSpaceReportColumnProp(label, dateType), false));
+        }
+
+        return columns;
+    }
+
+    /**
+     * 生成区域报表数据(按区域/空间分组的二维数组)
+     * 外层 List 代表不同的区域
+     * 内层 List 包含该区域下所有设备的详细数据记录
+     */
+    private List<List<Map<String, Object>>> generateSpaceReportData(SpaceReportRequest request, LocalDateTime startTime, LocalDateTime endTime) {
+        List<List<Map<String, Object>>> groupedValueList = new ArrayList<>();
+        String timeType = request.getTimeType() != null ? request.getTimeType().toLowerCase() : "date";
+        List<String> timeLabels = generateTimeLabels(timeType, startTime, endTime);
+
+        for (SpaceReportRequest.SpaceItem space : request.getSpaces()) {
+            if (space == null || space.getId() == null) {
+                continue;
+            }
+
+            Map<String, Map<String, Number>> itemCodeTimeData = querySpaceItemEnergyData(space, request, startTime, endTime);
+            List<Map<String, Object>> spaceValueList = new ArrayList<>();
+
+            for (SpaceReportRequest.ItemCodeItem itemCode : request.getItemCodes()) {
+                if (itemCode == null || StringUtils.isEmpty(itemCode.getCode())) {
+                    continue;
+                }
+
+                Map<String, Number> timeData = itemCodeTimeData.getOrDefault(
+                        buildSpaceItemKey(itemCode),
+                        createEmptySpaceTimeData(timeType, startTime, endTime));
+
+                Map<String, Object> row = new LinkedHashMap<>();
+                row.put("spaceName", space.getName());
+                row.put("itemName", itemCode.getName());
+
+                List<Number> periodValues = new ArrayList<>();
+                for (String label : timeLabels) {
+                    Number value = timeData.getOrDefault(label, 0);
+                    Number roundedValue = toUsageDecimal(value);
+                    row.put(toSpaceReportValueKey(label), roundedValue.doubleValue());
+                    periodValues.add(roundedValue);
+                }
+                row.put("total", sumUsageValues(periodValues));
+                spaceValueList.add(row);
+            }
+
+            groupedValueList.add(spaceValueList);
+        }
+
+        return groupedValueList;
+    }
+
+    /**
+     * 查询区域分项能耗数据,按分项(code+name)-> 时间标签 -> 用量 返回
+     */
+    private Map<String, Map<String, Number>> querySpaceItemEnergyData(SpaceReportRequest.SpaceItem space,
+                                                                      SpaceReportRequest request,
+                                                                      LocalDateTime startTime,
+                                                                      LocalDateTime endTime) {
+        String timeType = request.getTimeType() != null ? request.getTimeType().toLowerCase() : "date";
+        List<TimeSegmentBound> timeSegments = buildReportTimeSegments(timeType, startTime, endTime);
+
+        Map<String, Map<String, Number>> result = new LinkedHashMap<>();
+        List<SpaceReportRequest.ItemCodeItem> requestedItems = request.getItemCodes() == null
+                ? Collections.emptyList()
+                : request.getItemCodes().stream()
+                .filter(item -> item != null && StringUtils.hasText(item.getCode()))
+                .collect(Collectors.toList());
+
+        for (SpaceReportRequest.ItemCodeItem item : requestedItems) {
+            result.put(buildSpaceItemKey(item), createEmptySpaceTimeData(timeType, startTime, endTime));
+        }
+
+        if (space == null || space.getId() == null || requestedItems.isEmpty()) {
+            return result;
+        }
+
+        List<Long> spaceIds = spaceIds(Collections.singletonList(space.getId()));
+        List<String> gatewayUuids = getGatewayUuidsBySpaceIds(spaceIds).stream()
+                .map(BaseSpaceGateway::getGatewayUuid)
+                .collect(Collectors.toList());
+        List<String> spaceDeviceUuids = getDeviceUuidsByGatewayIds(gatewayUuids).stream()
+                .map(DmpDevice::getDeviceUuid)
+                .collect(Collectors.toList());
+
+        if (spaceDeviceUuids.isEmpty()) {
+            log.warn("区域下无设备,spaceId:{}", space.getId());
+            return result;
+        }
+
+        Integer energyType = getEnergyTypeByDeviceUuid(spaceDeviceUuids.get(0));
+        Long productId = getProductIdByEnergyType(energyType);
+        if (productId != 360L) {
+            throw new BusinessException("暂不支持该设备类型");
+        }
+
+        Map<String, List<String>> itemCodeToDeviceUuids = getItemCodeByDeviceUuid(spaceDeviceUuids);
+
+        List<String> metrics = Collections.singletonList("totalactiveenergyf");
+        HistorysInnerRequestVO historyRequestVO = new HistorysInnerRequestVO();
+        historyRequestVO.setDeviceuuid(spaceDeviceUuids);
+        historyRequestVO.setStartTime(request.getStartTime());
+        historyRequestVO.setEndTime(request.getEndTime());
+        historyRequestVO.setMetrics(metrics);
+        log.info("历史数据接口请求:{}", historyRequestVO);
+        ApiResult<List<HistorysInnerResultVO>> listHistoryData = remoteTsdbProxyService.queryHistoryDeviceData(historyRequestVO);
+        log.info("历史数据接口返回:{}", listHistoryData);
+
+        if (isHistoryDataEmpty(listHistoryData)) {
+            log.warn("历史数据为空,设备uuid:{}", spaceDeviceUuids);
+            return result;
+        }
+
+        Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricTimeValueMap =
+                parseDeviceMetricTimeValueMap(listHistoryData.getData());
+
+        for (SpaceReportRequest.ItemCodeItem itemCodeItem : requestedItems) {
+            List<String> itemDeviceUuids = resolveSpaceItemDeviceUuids(spaceDeviceUuids, itemCodeToDeviceUuids, itemCodeItem);
+            if (itemDeviceUuids.isEmpty()) {
+                log.warn("分项未匹配到区域设备,spaceId:{},itemCode:{},itemName:{}",
+                        space.getId(), itemCodeItem.getCode(), itemCodeItem.getName());
+                continue;
+            }
+
+            Map<String, Number> timeUsageMap = result.get(buildSpaceItemKey(itemCodeItem));
+            for (String deviceUuid : itemDeviceUuids) {
+                Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap = deviceMetricTimeValueMap.get(deviceUuid);
+                if (metricTimeValueMap == null) {
+                    continue;
+                }
+                TreeMap<LocalDateTime, Double> timeValueMap = metricTimeValueMap.get("totalactiveenergyf");
+                if (timeValueMap == null || timeValueMap.isEmpty()) {
+                    continue;
+                }
+
+                for (TimeSegmentBound segment : timeSegments) {
+                    double usage = calculateSegmentUsage(timeValueMap, segment.getStartInclusive(), segment.getEndExclusive());
+                    timeUsageMap.put(segment.getLabel(), addUsage(timeUsageMap.get(segment.getLabel()), usage).doubleValue());
+                }
+            }
+        }
+
+        return result;
+    }
+
+    private String buildSpaceItemKey(SpaceReportRequest.ItemCodeItem item) {
+        return item.getCode() + "::" + (item.getName() != null ? item.getName() : "");
+    }
+
+    /**
+     * 解析分项对应设备:优先按分项名称解析编码,再按 code 匹配,返回区域内全部关联设备
+     */
+    private List<String> resolveSpaceItemDeviceUuids(List<String> spaceDeviceUuids,
+                                                     Map<String, List<String>> itemCodeToDeviceUuids,
+                                                     SpaceReportRequest.ItemCodeItem itemCodeItem) {
+        Set<String> spaceDeviceSet = new HashSet<>(spaceDeviceUuids);
+        String requestCode = itemCodeItem.getCode().trim();
+
+        String resolvedCode = resolveEnergyItemDbCode(requestCode, itemCodeItem.getName());
+        if (StringUtils.hasText(resolvedCode)) {
+            List<String> matched = findDevicesByItemCode(resolvedCode, itemCodeToDeviceUuids, spaceDeviceSet);
+            if (!matched.isEmpty()) {
+                return matched;
+            }
+        }
+
+        List<String> matched = findDevicesByItemCode(requestCode, itemCodeToDeviceUuids, spaceDeviceSet);
+        if (!matched.isEmpty()) {
+            return matched;
+        }
+
+        return Collections.emptyList();
+    }
+
+    private List<String> findDevicesByItemCode(String itemCode,
+                                               Map<String, List<String>> itemCodeToDeviceUuids,
+                                               Set<String> spaceDeviceSet) {
+        List<String> matched = filterDevicesInSpace(itemCodeToDeviceUuids.get(itemCode), spaceDeviceSet);
+        if (!matched.isEmpty()) {
+            return matched;
+        }
+        return filterDevicesInSpace(
+                emsDeviceItemCodeMapper.selectDeviceUuidsByItemCode(SecurityUtils.getTenantId(), itemCode),
+                spaceDeviceSet);
+    }
+
+    private List<String> filterDevicesInSpace(List<String> deviceUuids, Set<String> spaceDeviceSet) {
+        if (deviceUuids == null || deviceUuids.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return deviceUuids.stream()
+                .filter(StringUtils::hasText)
+                .map(String::trim)
+                .filter(spaceDeviceSet::contains)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 将请求中的 code/identifier/name 解析为 ems_energy_item_code 表中的 code
+     */
+    private String resolveEnergyItemDbCode(String requestCode, String itemName) {
+        if (StringUtils.hasText(itemName)) {
+            EmsEnergyItemCode byName = emsEnergyItemCodeMapper.selectOne(
+                    Wrappers.lambdaQuery(EmsEnergyItemCode.class)
+                            .eq(EmsEnergyItemCode::getName, itemName)
+                            .last("LIMIT 1"));
+            if (byName != null) {
+                return byName.getCode();
+            }
+        }
+
+        if (StringUtils.hasText(requestCode)) {
+            EmsEnergyItemCode byCode = emsEnergyItemCodeMapper.selectOne(
+                    Wrappers.lambdaQuery(EmsEnergyItemCode.class)
+                            .eq(EmsEnergyItemCode::getCode, requestCode)
+                            .last("LIMIT 1"));
+            if (byCode != null) {
+                return byCode.getCode();
+            }
+
+            EmsEnergyItemCode byIdentifier = emsEnergyItemCodeMapper.selectOne(
+                    Wrappers.lambdaQuery(EmsEnergyItemCode.class)
+                            .eq(EmsEnergyItemCode::getIdentifier, requestCode)
+                            .last("LIMIT 1"));
+            if (byIdentifier != null) {
+                return byIdentifier.getCode();
+            }
+        }
+        return null;
+    }
+
+    private TreeMap<LocalDateTime, Double> extractSegmentReadings(TreeMap<LocalDateTime, Double> source,
+                                                                  LocalDateTime startInclusive,
+                                                                  LocalDateTime endExclusive) {
+        TreeMap<LocalDateTime, Double> result = new TreeMap<>();
+        if (source == null || source.isEmpty() || !startInclusive.isBefore(endExclusive)) {
+            return result;
+        }
+        for (Map.Entry<LocalDateTime, Double> entry : source.entrySet()) {
+            LocalDateTime ts = entry.getKey();
+            if (!ts.isBefore(startInclusive) && ts.isBefore(endExclusive)) {
+                result.put(ts, entry.getValue());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 解析 TSDB 历史数据为 设备 -> 指标 -> 时间 -> 值 结构
+     */
+    private Map<String, Map<String, TreeMap<LocalDateTime, Double>>> parseDeviceMetricTimeValueMap(
+            List<HistorysInnerResultVO> historyDataList) {
+        Map<String, Map<String, TreeMap<LocalDateTime, Double>>> deviceMetricTimeValueMap = new HashMap<>();
+
+        if (historyDataList == null || historyDataList.isEmpty()) {
+            return deviceMetricTimeValueMap;
+        }
+
+        for (HistorysInnerResultVO result : historyDataList) {
+            if (result == null || !StringUtils.hasText(result.getDeviceuuid())) {
+                continue;
+            }
+            String deviceUuid = result.getDeviceuuid();
+            Map<String, TreeMap<LocalDateTime, Double>> metricTimeValueMap =
+                    deviceMetricTimeValueMap.computeIfAbsent(deviceUuid, k -> new HashMap<>());
+
+            if (result.getMetrics() == null) {
+                continue;
+            }
+            for (MetricVO metric : result.getMetrics()) {
+                if (metric == null || !StringUtils.hasText(metric.getMetric()) || metric.getMetricItems() == null) {
+                    continue;
+                }
+                String metricName = metric.getMetric();
+                TreeMap<LocalDateTime, Double> timeValueMap =
+                        metricTimeValueMap.computeIfAbsent(metricName, k -> new TreeMap<>());
+
+                for (Map<String, Object> point : metric.getMetricItems()) {
+                    try {
+                        String timestampStr = point.get("timestamp").toString();
+                        if (timestampStr.endsWith(".0")) {
+                            timestampStr = timestampStr.substring(0, timestampStr.length() - 2);
+                        }
+                        LocalDateTime timestamp = LocalDateTime.parse(timestampStr, DATE_TIME_FORMATTER);
+                        Double value = Double.parseDouble(point.get("value").toString());
+                        timeValueMap.put(timestamp, value);
+                    } catch (Exception e) {
+                        log.error("解析历史数据失败,设备:{},指标:{},值:{}",
+                                deviceUuid, metricName, point.get("value"), e);
+                    }
+                }
+            }
+        }
+        return deviceMetricTimeValueMap;
+    }
+
+    /**
+     * 时间段用量 = 该时间段内最后一个读数 - 第一个读数(至少 2 条读数)
+     */
+    private double calculateSegmentUsage(TreeMap<LocalDateTime, Double> timeValueMap,
+                                         LocalDateTime startInclusive,
+                                         LocalDateTime endExclusive) {
+        if (timeValueMap == null || timeValueMap.isEmpty()) {
+            return 0;
+        }
+
+        TreeMap<LocalDateTime, Double> segmentData = extractSegmentReadings(timeValueMap, startInclusive, endExclusive);
+        if (segmentData.size() < 2) {
+            return 0;
+        }
+
+        double firstValue = segmentData.firstEntry().getValue();
+        double lastValue = segmentData.lastEntry().getValue();
+        return formatUsageValue(calculateReadingDelta(firstValue, lastValue));
+    }
+
+    private double calculateReadingDelta(double firstValue, double lastValue) {
+        double usage = lastValue - firstValue;
+        if (usage < 0) {
+            return lastValue;
+        }
+        return usage;
+    }
+
+    private double formatUsageValue(double usage) {
+        return toUsageDecimal(usage).doubleValue();
+    }
+
+    private static final int USAGE_SCALE = 2;
+
+    private BigDecimal toUsageDecimal(Number value) {
+        if (value == null) {
+            return BigDecimal.ZERO.setScale(USAGE_SCALE, RoundingMode.HALF_UP);
+        }
+        return BigDecimal.valueOf(value.doubleValue()).setScale(USAGE_SCALE, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal addUsage(Number a, Number b) {
+        return toUsageDecimal(a).add(toUsageDecimal(b)).setScale(USAGE_SCALE, RoundingMode.HALF_UP);
+    }
+
+    private double sumUsageValues(Iterable<? extends Number> values) {
+        BigDecimal sum = BigDecimal.ZERO;
+        if (values != null) {
+            for (Number value : values) {
+                sum = sum.add(toUsageDecimal(value));
+            }
+        }
+        return sum.setScale(USAGE_SCALE, RoundingMode.HALF_UP).doubleValue();
+    }
+
+    private Map<String, Number> createEmptySpaceTimeData(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        Map<String, Number> data = new LinkedHashMap<>();
+        for (String label : generateTimeLabels(timeType, startTime, endTime)) {
+            data.put(label, 0);
+        }
+        return data;
+    }
+
+    /**
+     * 报表时间分段(左闭右开),与 columnList 标签一一对应
+     */
+    private static class TimeSegmentBound {
+        private final String label;
+        private final LocalDateTime startInclusive;
+        private final LocalDateTime endExclusive;
+
+        TimeSegmentBound(String label, LocalDateTime startInclusive, LocalDateTime endExclusive) {
+            this.label = label;
+            this.startInclusive = startInclusive;
+            this.endExclusive = endExclusive;
+        }
+
+        String getLabel() {
+            return label;
+        }
+
+        LocalDateTime getStartInclusive() {
+            return startInclusive;
+        }
+
+        LocalDateTime getEndExclusive() {
+            return endExclusive;
+        }
+    }
+
+    private List<TimeSegmentBound> buildReportTimeSegments(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        List<TimeSegmentBound> segments = new ArrayList<>();
+        if (startTime == null || endTime == null || !startTime.isBefore(endTime)) {
+            return segments;
+        }
+
+        String type = timeType != null ? timeType.toLowerCase() : "date";
+        switch (type) {
+            case "date":
+                buildHourSegments(segments, startTime, endTime);
+                break;
+            case "month":
+                buildDaySegments(segments, startTime, endTime);
+                break;
+            case "year":
+                buildMonthSegments(segments, startTime, endTime);
+                break;
+            case "custom":
+                break;
+            default:
+                buildHourSegments(segments, startTime, endTime);
+                break;
+        }
+        return segments;
+    }
+
+    /** date:按小时分段 [HH:00, HH+1:00),与传参起止小时一致 */
+    private void buildHourSegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
+        int startHour = startTime.getHour();
+        int endHour = endTime.getHour();
+        for (int hour = startHour; hour <= endHour; hour++) {
+            String label = hour + "时";
+            LocalDateTime hourStart = startTime.withHour(hour).withMinute(0).withSecond(0).withNano(0);
+            LocalDateTime hourEndExclusive = hourStart.plusHours(1);
+            LocalDateTime segmentStart = hourStart.isBefore(startTime) ? startTime : hourStart;
+            LocalDateTime segmentEndExclusive = hourEndExclusive.isAfter(endTime) ? endTime : hourEndExclusive;
+            if (segmentStart.isBefore(segmentEndExclusive)) {
+                segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
+            } else {
+                segments.add(new TimeSegmentBound(label, segmentStart, segmentStart.plusNanos(1)));
+            }
+        }
+    }
+
+    /** month:按自然日分段 [00:00, 次日00:00) */
+    private void buildDaySegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
+        LocalDate current = startTime.toLocalDate();
+        LocalDate lastDate = resolveLastDateInRange(startTime, endTime);
+        while (!current.isAfter(lastDate)) {
+            String label = String.format("%02d日", current.getDayOfMonth());
+            LocalDateTime dayStart = current.atStartOfDay();
+            LocalDateTime dayEndExclusive = current.plusDays(1).atStartOfDay();
+            LocalDateTime segmentStart = dayStart.isBefore(startTime) ? startTime : dayStart;
+            LocalDateTime segmentEndExclusive = dayEndExclusive.isAfter(endTime) ? endTime : dayEndExclusive;
+            if (segmentStart.isBefore(segmentEndExclusive)) {
+                segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
+            }
+            current = current.plusDays(1);
+        }
+    }
+
+    /** year:按自然月分段 [月初00:00, 下月初00:00) */
+    private void buildMonthSegments(List<TimeSegmentBound> segments, LocalDateTime startTime, LocalDateTime endTime) {
+        int year = startTime.getYear();
+        int month = startTime.getMonthValue();
+        int endYear = endTime.getYear();
+        int endMonth = endTime.getMonthValue();
+
+        while (year < endYear || (year == endYear && month <= endMonth)) {
+            String label = month + "月";
+            LocalDateTime monthStart = LocalDateTime.of(year, month, 1, 0, 0, 0);
+            LocalDateTime monthEndExclusive = monthStart.plusMonths(1);
+            LocalDateTime segmentStart = monthStart.isBefore(startTime) ? startTime : monthStart;
+            LocalDateTime segmentEndExclusive = monthEndExclusive.isAfter(endTime) ? endTime : monthEndExclusive;
+            if (segmentStart.isBefore(segmentEndExclusive)) {
+                segments.add(new TimeSegmentBound(label, segmentStart, segmentEndExclusive));
+            }
+            month++;
+            if (month > 12) {
+                month = 1;
+                year++;
+            }
+        }
+    }
+
+    /**
+     * 结束时间为某日 00:00:00 时,表示不含该日,最后一天取前一日
+     */
+    private LocalDate resolveLastDateInRange(LocalDateTime startTime, LocalDateTime endTime) {
+        LocalDate endDate = endTime.toLocalDate();
+        if (endTime.toLocalTime().equals(LocalTime.MIDNIGHT) && endDate.isAfter(startTime.toLocalDate())) {
+            return endDate.minusDays(1);
+        }
+        return endDate;
+    }
+
+    private String toSpaceReportColumnProp(String label, String timeType) {
+        String type = timeType != null ? timeType.toLowerCase() : "date";
+        if ("year".equals(type)) {
+            return "_" + label.replace("月", "");
+        }
+        return "_" + label;
+    }
+
+    private String toSpaceReportValueKey(String label) {
+        String numPart = label.replace("时", "").replace("日", "").replace("月", "");
+        try {
+            return "_" + Integer.parseInt(numPart);
+        } catch (NumberFormatException e) {
+            return "_" + numPart;
+        }
+    }
+
+    /**
+     * 查询所有区域id(包含子区域id)
+     */
+    private List<Long> spaceIds(List<Long> spaceId) {
+        return baseSpaceMapper.selectList(
+                new LambdaQueryWrapper<BaseSpace>()
+                        .in(BaseSpace::getId, spaceId)
+                        .or().in(BaseSpace::getParentId, spaceId)
+        ).stream().map(BaseSpace::getId).collect(Collectors.toList());
+    }
+
+    /**
+     * 通过energy_type获取product_id
+     */
+    private Long getProductIdByEnergyType(Integer energyType) {
+        return emsProductEnergyTypeMapper.selectOne(
+                new LambdaQueryWrapper<EmsProductEnergyType>()
+                        .eq(EmsProductEnergyType::getEnergyType, energyType)
+        ).getProductId();
+    }
+
+    /**
+     * 通过device_uuid按item_code分组
+     */
+    private Map<String, List<String>> getItemCodeByDeviceUuid(List<String> deviceUuids) {
+        return emsDeviceItemCodeMapper.selectList(
+                new LambdaQueryWrapper<EmsDeviceItemCode>()
+                        .in(EmsDeviceItemCode::getDeviceUuid, deviceUuids)
+        ).stream().collect(Collectors.groupingBy(
+                EmsDeviceItemCode::getItemCode,
+                Collectors.mapping(
+                        EmsDeviceItemCode::getDeviceUuid,
+                        Collectors.toList()
+                )
+        ));
+    }
+
+    /**
+     * 通过device_uuid获取energy_type
+     */
+    private Integer getEnergyTypeByDeviceUuid(String deviceUuid) {
+        return emsDeviceItemCodeMapper.selectList(
+                new LambdaQueryWrapper<EmsDeviceItemCode>()
+                        .eq(EmsDeviceItemCode::getDeviceUuid, deviceUuid)
+        ).get(0).getEnergyType();
+    }
+
+    /**
+     * 根据网关id获取设备UUID列表
+     */
+    private List<DmpDevice> getDeviceUuidsByGatewayIds(List<String> gatewayIds) {
+        return new ArrayList<>(dmpDeviceMapper.selectList(
+                new LambdaQueryWrapper<DmpDevice>()
+                        .in(DmpDevice::getGatewayUuid, gatewayIds)));
+    }
+
+    /**
+     * 根据区域id获取网关UUID列表
+     */
+    private List<BaseSpaceGateway> getGatewayUuidsBySpaceIds(List<Long> spaceIds) {
+        return new ArrayList<>(baseSpaceGatewayMapper.selectList(
+                new LambdaQueryWrapper<BaseSpaceGateway>()
+                        .in(BaseSpaceGateway::getSpaceId, spaceIds)));
+    }
+
+    /**
+     * 计算区域报表总计(适配二维数组结构)
+     */
+    private Number calculateSpaceReportTotal(List<List<Map<String, Object>>> groupedValueList) {
+        if (groupedValueList == null || groupedValueList.isEmpty()) {
+            return 0;
+        }
+
+        List<Number> rowTotals = new ArrayList<>();
+        for (List<Map<String, Object>> group : groupedValueList) {
+            if (group == null || group.isEmpty()) {
+                continue;
+            }
+            for (Map<String, Object> row : group) {
+                Object total = row.get("total");
+                if (total instanceof Number) {
+                    rowTotals.add((Number) total);
+                }
+            }
+        }
+        return sumUsageValues(rowTotals);
+    }
+
+    // ==================== 通用工具方法 ====================
+
+    /**
+     * 校验时间参数
+     * 1. 结束时间不得早于开始时间
+     * 2. 按日查询:起止时间必须是同一天
+     * 3. 按月查询:起止时间必须是同一月
+     * 4. 按年查询:起止时间必须是同一年
+     */
+    private void validateTimeParams(LocalDateTime startTime, LocalDateTime endTime, String timeType) {
+        // 基本校验:结束时间不得早于开始时间
+        if (endTime.isBefore(startTime)) {
+            throw new BusinessException("结束时间不得早于开始时间");
+        }
+
+        String type = timeType != null ? timeType.toLowerCase() : "date";
+
+        switch (type) {
+            case "date":
+                // 按日查询:必须是同一天
+                if (!startTime.toLocalDate().isEqual(endTime.toLocalDate())) {
+                    throw new BusinessException("按日查询时,起止时间必须是同一天");
+                }
+                break;
+            case "month":
+                // 按月查询:必须是同一月
+                if (startTime.getYear() != endTime.getYear() ||
+                        startTime.getMonthValue() != endTime.getMonthValue()) {
+                    throw new BusinessException("按月查询时,起止时间必须是同一月");
+                }
+                break;
+            case "year":
+                // 按年查询:必须是同一年
+                if (startTime.getYear() != endTime.getYear()) {
+                    throw new BusinessException("按年查询时,起止时间必须是同一年");
+                }
+                break;
+            case "custom":
+                // 自定义查询:不进行时间校验
+                break;
+            default:
+                throw new BusinessException("查询时间类型错误!请重试");
+        }
+    }
+
+    /**
+     * 创建列定义
+     */
+    private EnergyReportResponse.ColumnItem createColumn(String label, String prop, Boolean fixed) {
+        EnergyReportResponse.ColumnItem column = new EnergyReportResponse.ColumnItem();
+        column.setLabel(label);
+        column.setProp(prop);
+        column.setFixed(fixed);
+        column.setChildren(null);
+        return column;
+    }
+
+    /**
+     * 创建标题项
+     */
+    private ItemReportResponse.TitleItem createTitleItem(String prop, Boolean fixed, String label, Integer value) {
+        ItemReportResponse.TitleItem item = new ItemReportResponse.TitleItem();
+        item.setProp(prop);
+        item.setFixed(fixed);
+        item.setLabel(label);
+        item.setValue(value);
+        return item;
+    }
+
+    /**
+     * 生成时间标签列表
+     * 规则:
+     * - 按日(date)查询 → 返回小时数据(根据起止时间的小时范围)
+     * - 按月(month)查询 → 返回日数据(01日-31日)
+     * - 按年(year)查询 → 返回月数据(1月-12月)
+     */
+    private List<String> generateTimeLabels(String timeType, LocalDateTime startTime, LocalDateTime endTime) {
+        if (startTime == null || endTime == null) {
+            return new ArrayList<>();
+        }
+        return buildReportTimeSegments(timeType, startTime, endTime).stream()
+                .map(TimeSegmentBound::getLabel)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 解析日期时间字符串
+     */
+    private LocalDateTime parseDateTime(String dateTimeStr) {
+        if (!StringUtils.hasText(dateTimeStr)) {
+            return null;
+        }
+        try {
+            return LocalDateTime.parse(dateTimeStr, DATE_TIME_FORMATTER);
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取设备UUID列表
+     */
+    private List<String> getUuidList(List<Integer> deviceIds) {
+        return deviceIds.stream().map(deviceId -> {
+            DmpDevice device = dmpDeviceMapper.selectById(deviceId);
+            return device != null ? device.getDeviceUuid() : null;
+        }).collect(Collectors.toList());
+    }
+
+    private boolean isHistoryDataEmpty(ApiResult<?> result) {
+        return result == null
+                || !result.isSuccess()
+                || result.getStatus() != ApiResult.ResultStatus.SUCCESS
+                || result.getData() == null;
+    }
+
+    // ==================== 报表 Excel 导出 ====================
+
+    private String buildExportFileName(String prefix) {
+        return prefix + "_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+    }
+
+    private String resolveEnergyCellValue(EnergyReportResponse.ValueItem item,
+                                          EnergyReportResponse.ColumnItem column) {
+        if (item == null || column == null || !StringUtils.hasText(column.getProp())) {
+            return "";
+        }
+        String prop = column.getProp();
+        switch (prop) {
+            case "deviceName":
+                return formatExportCellValue(item.getDeviceName());
+            case "commAddress":
+                return formatExportCellValue(item.getCommAddress());
+            case "identifier":
+                return formatExportCellValue(item.getIdentifier());
+            case "total":
+                return formatExportCellValue(item.getTotal());
+            default:
+                Map<String, String> timeData = item.getTimeData();
+                if (timeData != null && timeData.containsKey(prop)) {
+                    return formatExportCellValue(timeData.get(prop));
+                }
+                return ENERGY_NO_DATA;
+        }
+    }
+
+    private String resolveSpaceCellValue(Map<String, Object> rowMap, EnergyReportResponse.ColumnItem column) {
+        if (rowMap == null || column == null || !StringUtils.hasText(column.getProp())) {
+            return "";
+        }
+        Object value = rowMap.get(column.getProp());
+        if (value == null && StringUtils.hasText(column.getLabel())) {
+            value = rowMap.get(toSpaceReportValueKey(column.getLabel()));
+        }
+        return formatExportCellValue(value);
+    }
+
+    private String formatExportCellValue(Object value) {
+        return value == null ? "" : value.toString();
+    }
+
+    private void writeEnergyReportExcel(HttpServletResponse response,
+                                        String fileName,
+                                        List<EnergyReportResponse.ColumnItem> columns,
+                                        List<List<EnergyReportResponse.ValueItem>> groupedRows) {
+        try (Workbook workbook = new XSSFWorkbook()) {
+            Sheet sheet = workbook.createSheet("能源报表");
+            CellStyle headerStyle = createExportHeaderStyle(workbook);
+            CellStyle textStyle = createExportDataStyle(workbook, HorizontalAlignment.LEFT);
+            CellStyle numberStyle = createExportDataStyle(workbook, HorizontalAlignment.CENTER);
+
+            writeExportHeaderRow(sheet, columns, headerStyle);
+
+            int deviceNameCol = findColumnIndex(columns, "deviceName");
+            int commAddressCol = findColumnIndex(columns, "commAddress");
+            int rowIndex = 1;
+            for (List<EnergyReportResponse.ValueItem> deviceRows : groupedRows) {
+                if (deviceRows == null || deviceRows.isEmpty()) {
+                    continue;
+                }
+                int groupStartRow = rowIndex;
+                for (EnergyReportResponse.ValueItem item : deviceRows) {
+                    Row row = sheet.createRow(rowIndex++);
+                    writeEnergyReportDataRow(row, item, columns, textStyle, numberStyle);
+                }
+                int groupEndRow = rowIndex - 1;
+                if (groupEndRow > groupStartRow) {
+                    mergeExportColumn(sheet, groupStartRow, groupEndRow, deviceNameCol);
+                    mergeExportColumn(sheet, groupStartRow, groupEndRow, commAddressCol);
+                }
+            }
+
+            autoSizeExportColumns(sheet, columns.size());
+            writeExcelToResponse(response, fileName, workbook);
+        } catch (IOException e) {
+            throw new BusinessException("导出失败", e);
+        }
+    }
+
+    private void writeSpaceReportExcel(HttpServletResponse response,
+                                       String fileName,
+                                       List<EnergyReportResponse.ColumnItem> columns,
+                                       List<List<Map<String, Object>>> groupedRows) {
+        try (Workbook workbook = new XSSFWorkbook()) {
+            Sheet sheet = workbook.createSheet("区域报表");
+            CellStyle headerStyle = createExportHeaderStyle(workbook);
+            CellStyle textStyle = createExportDataStyle(workbook, HorizontalAlignment.LEFT);
+            CellStyle numberStyle = createExportDataStyle(workbook, HorizontalAlignment.CENTER);
+
+            writeExportHeaderRow(sheet, columns, headerStyle);
+
+            int spaceNameCol = findColumnIndex(columns, "spaceName");
+            int rowIndex = 1;
+            for (List<Map<String, Object>> spaceRows : groupedRows) {
+                if (spaceRows == null || spaceRows.isEmpty()) {
+                    continue;
+                }
+                int groupStartRow = rowIndex;
+                for (Map<String, Object> rowMap : spaceRows) {
+                    Row row = sheet.createRow(rowIndex++);
+                    writeSpaceReportDataRow(row, rowMap, columns, textStyle, numberStyle);
+                }
+                int groupEndRow = rowIndex - 1;
+                if (groupEndRow > groupStartRow) {
+                    mergeExportColumn(sheet, groupStartRow, groupEndRow, spaceNameCol);
+                }
+            }
+
+            autoSizeExportColumns(sheet, columns.size());
+            writeExcelToResponse(response, fileName, workbook);
+        } catch (IOException e) {
+            throw new BusinessException("导出失败", e);
+        }
+    }
+
+    private void writeExportHeaderRow(Sheet sheet,
+                                      List<EnergyReportResponse.ColumnItem> columns,
+                                      CellStyle headerStyle) {
+        Row headerRow = sheet.createRow(0);
+        for (int i = 0; i < columns.size(); i++) {
+            Cell cell = headerRow.createCell(i);
+            cell.setCellValue(columns.get(i).getLabel());
+            cell.setCellStyle(headerStyle);
+        }
+    }
+
+    private void writeEnergyReportDataRow(Row row,
+                                          EnergyReportResponse.ValueItem item,
+                                          List<EnergyReportResponse.ColumnItem> columns,
+                                          CellStyle textStyle,
+                                          CellStyle numberStyle) {
+        for (int colIndex = 0; colIndex < columns.size(); colIndex++) {
+            EnergyReportResponse.ColumnItem column = columns.get(colIndex);
+            Cell cell = row.createCell(colIndex);
+            cell.setCellValue(resolveEnergyCellValue(item, column));
+            cell.setCellStyle(isExportTextColumn(column) ? textStyle : numberStyle);
+        }
+    }
+
+    private void writeSpaceReportDataRow(Row row,
+                                         Map<String, Object> rowMap,
+                                         List<EnergyReportResponse.ColumnItem> columns,
+                                         CellStyle textStyle,
+                                         CellStyle numberStyle) {
+        for (int colIndex = 0; colIndex < columns.size(); colIndex++) {
+            EnergyReportResponse.ColumnItem column = columns.get(colIndex);
+            Cell cell = row.createCell(colIndex);
+            cell.setCellValue(resolveSpaceCellValue(rowMap, column));
+            cell.setCellStyle(isExportTextColumn(column) ? textStyle : numberStyle);
+        }
+    }
+
+    private boolean isExportTextColumn(EnergyReportResponse.ColumnItem column) {
+        if (column == null || !StringUtils.hasText(column.getProp())) {
+            return true;
+        }
+        switch (column.getProp()) {
+            case "deviceName":
+            case "commAddress":
+            case "identifier":
+            case "spaceName":
+            case "itemName":
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private int findColumnIndex(List<EnergyReportResponse.ColumnItem> columns, String prop) {
+        for (int i = 0; i < columns.size(); i++) {
+            if (prop.equals(columns.get(i).getProp())) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private void mergeExportColumn(Sheet sheet, int firstRow, int lastRow, int columnIndex) {
+        if (columnIndex < 0 || firstRow >= lastRow) {
+            return;
+        }
+        sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, columnIndex, columnIndex));
+    }
+
+    private void autoSizeExportColumns(Sheet sheet, int columnCount) {
+        for (int i = 0; i < columnCount; i++) {
+            sheet.autoSizeColumn(i);
+            int width = sheet.getColumnWidth(i);
+            sheet.setColumnWidth(i, Math.min(width + 512, 255 * 256));
+        }
+    }
+
+    private void writeExcelToResponse(HttpServletResponse response, String fileName, Workbook workbook)
+            throws IOException {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        response.setHeader("Content-Disposition",
+                "attachment; filename=\"" + encodeExportFileName(fileName) + ".xlsx\"");
+        workbook.write(response.getOutputStream());
+        response.getOutputStream().flush();
+    }
+
+    private String encodeExportFileName(String fileName) {
+        try {
+            return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
+        } catch (UnsupportedEncodingException e) {
+            return fileName;
+        }
+    }
+
+    private CellStyle createExportHeaderStyle(Workbook workbook) {
+        CellStyle style = createBorderedCellStyle(workbook);
+        Font font = workbook.createFont();
+        font.setBold(true);
+        style.setFont(font);
+        style.setAlignment(HorizontalAlignment.CENTER);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+        return style;
+    }
+
+    private CellStyle createExportDataStyle(Workbook workbook, HorizontalAlignment alignment) {
+        CellStyle style = createBorderedCellStyle(workbook);
+        style.setAlignment(alignment);
+        style.setVerticalAlignment(VerticalAlignment.CENTER);
+        return style;
+    }
+
+    private CellStyle createBorderedCellStyle(Workbook workbook) {
+        CellStyle style = workbook.createCellStyle();
+        style.setBorderTop(BorderStyle.THIN);
+        style.setBorderBottom(BorderStyle.THIN);
+        style.setBorderLeft(BorderStyle.THIN);
+        style.setBorderRight(BorderStyle.THIN);
+        return style;
+    }
+
+    // ==================== 空响应创建方法 ====================
+
+    private EnergyReportResponse createEmptyEnergyReport() {
+        EnergyReportResponse response = new EnergyReportResponse();
+        response.setColumnList(new ArrayList<>());
+        response.setValueList(new ArrayList<>());
+        return response;
+    }
+
+    private ItemReportResponse createEmptyItemReport() {
+        ItemReportResponse response = new ItemReportResponse();
+        response.setTitleList(new ArrayList<>());
+        response.setDataList(new ArrayList<>());
+        return response;
+    }
+
+    private SpaceReportResponse createEmptySpaceReport() {
+        SpaceReportResponse response = new SpaceReportResponse();
+        response.setColumnList(new ArrayList<>());
+        response.setValueList(new ArrayList<>());
+        response.setTotal(0);
+        return response;
+    }
 }

+ 29 - 29
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EnergyDataServiceImpl.java

@@ -186,7 +186,7 @@ public class EnergyDataServiceImpl implements EnergyDataService {
             }
 
             Map<String, Object> meter = new HashMap<>();
-            meter.put("id", buildingId + device.getDeviceId().substring(device.getDeviceId().length() - 3));
+            meter.put("id", buildingId + "M" + device.getDeviceId().substring(device.getDeviceId().length() - 3));
             meter.put("name", device.getDeviceName());
 
             List<Map<String, String>> functions = new ArrayList<>();
@@ -196,30 +196,30 @@ public class EnergyDataServiceImpl implements EnergyDataService {
                 functions.add(createFunction("WPP", String.valueOf(data.getTotalActiveEnergyF()), null));
             }
             
-            // 正向有功功率
-            if (data.getTotalActivePower() != null) {
-                functions.add(createFunction("Ps", String.valueOf(data.getTotalActivePower()), null));
-            }
-            
-            // 正向无功功率
-            if (data.getTotalReactiveEnergyF() != null) {
-                functions.add(createFunction("Qs", String.valueOf(data.getTotalReactiveEnergyF()), null));
-            }
-            
-            // A相电压
-            if (data.getVoltageA() != null) {
-                functions.add(createFunction("Ua", String.valueOf(data.getVoltageA()), null));
-            }
-            
-            // B相电压
-            if (data.getVoltageB() != null) {
-                functions.add(createFunction("Ub", String.valueOf(data.getVoltageB()), null));
-            }
-            
-            // C相电压
-            if (data.getVoltageC() != null) {
-                functions.add(createFunction("Uc", String.valueOf(data.getVoltageC()), null));
-            }
+//            // 正向有功功率
+//            if (data.getTotalActivePower() != null) {
+//                functions.add(createFunction("Ps", String.valueOf(data.getTotalActivePower()), null));
+//            }
+//
+//            // 正向无功功率
+//            if (data.getTotalReactiveEnergyF() != null) {
+//                functions.add(createFunction("Qs", String.valueOf(data.getTotalReactiveEnergyF()), null));
+//            }
+//
+//            // A相电压
+//            if (data.getVoltageA() != null) {
+//                functions.add(createFunction("Ua", String.valueOf(data.getVoltageA()), null));
+//            }
+//
+//            // B相电压
+//            if (data.getVoltageB() != null) {
+//                functions.add(createFunction("Ub", String.valueOf(data.getVoltageB()), null));
+//            }
+//
+//            // C相电压
+//            if (data.getVoltageC() != null) {
+//                functions.add(createFunction("Uc", String.valueOf(data.getVoltageC()), null));
+//            }
             
             // A相电流
             if (data.getCurrentA() != null) {
@@ -236,10 +236,10 @@ public class EnergyDataServiceImpl implements EnergyDataService {
                 functions.add(createFunction("Ic", String.valueOf(data.getCurrentC()), null));
             }
             
-            // 功率因数
-            if (data.getTotalPowerFactor() != null) {
-                functions.add(createFunction("PFs", String.valueOf(data.getTotalPowerFactor()), null));
-            }
+//            // 功率因数
+//            if (data.getTotalPowerFactor() != null) {
+//                functions.add(createFunction("PFs", String.valueOf(data.getTotalPowerFactor()), null));
+//            }
 
             meter.put("functions", functions);
             meters.add(meter);

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

@@ -0,0 +1,20 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+/**
+ * 能耗分析-单时间点数据
+ */
+@Data
+public class EmsAveragePointVO {
+
+    /**
+     * 用量值,无数据时为 "-"
+     */
+    private String value;
+
+    /**
+     * 时间点,与 timeData 一致(如 2026年6月3日0时)
+     */
+    private String time;
+}

+ 48 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsAverageRequest.java

@@ -0,0 +1,48 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 能耗分析-区域时序数据请求
+ */
+@Data
+public class EmsAverageRequest {
+
+    /**
+     * 开始时间,格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String startTime;
+
+    /**
+     * 结束时间,格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String endTime;
+
+    /**
+     * 时间粒度:hour、day、month、year
+     */
+    private String dateType;
+
+    /**
+     * 区域列表
+     */
+    private List<SpaceItem> spaces;
+
+    /**
+     * 查询类型:0-总量/KWh,1-单位面积用量(kWh/㎡)
+     */
+    private Integer type;
+
+    /**
+     * 能耗分项编码
+     */
+    private String code;
+
+    @Data
+    public static class SpaceItem {
+        private Long id;
+        private String name;
+    }
+}

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

@@ -0,0 +1,23 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 能耗分析-区域时序数据响应
+ */
+@Data
+public class EmsAverageResponseVO {
+
+    /**
+     * 时间轴展示标签
+     */
+    private List<String> timeData = new ArrayList<>();
+
+    /**
+     * 各区域时序数据
+     */
+    private List<EmsAverageSpaceItemVO> dataList = new ArrayList<>();
+}

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

@@ -0,0 +1,17 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 能耗分析-区域时序数据
+ */
+@Data
+public class EmsAverageSpaceItemVO {
+
+    private Long spaceId;
+    private String name;
+    private List<EmsAveragePointVO> list = new ArrayList<>();
+}

+ 4 - 1
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EmsCompareResponse.java

@@ -9,6 +9,9 @@ import java.util.List;
 public class EmsCompareResponse {
 
     private String energyTypeName;
-    private String dimension;
+    /** 查询粒度:D/M/Y */
+    private String timeDimension;
+    /** 时间轴标签,如日 2026-06-01…2026-06-25 */
+    private List<String> dimension = new ArrayList<>();
     private List<EmsCompareSeriesItemVO> series = new ArrayList<>();
 }

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

@@ -0,0 +1,22 @@
+package com.usky.ems.service.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 设备关联能源分项保存请求(ems_device_item_code)
+ */
+@Data
+public class EmsDeviceItemCodeSaveRequest {
+
+    @ApiModelProperty(value = "选中的设备UUID集合", required = true)
+    private List<String> deviceUuidList;
+
+    @ApiModelProperty(value = "能耗类型", required = true)
+    private Integer energyType;
+
+    @ApiModelProperty(value = "能耗分项编码", required = true)
+    private String itemCode;
+}

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

@@ -0,0 +1,19 @@
+package com.usky.ems.service.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 产品关联能源类型保存请求(ems_product_energy_type)
+ */
+@Data
+public class EmsProductEnergyTypeSaveRequest {
+
+    @ApiModelProperty(value = "选中的产品ID集合", required = true)
+    private List<Long> productIdList;
+
+    @ApiModelProperty(value = "能耗类型", required = true)
+    private Integer energyType;
+}

+ 93 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EnergyReportRequest.java

@@ -0,0 +1,93 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 能源报表请求VO
+ */
+@Data
+public class EnergyReportRequest {
+
+    /**
+     * 开始时间,格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String startTime;
+
+    /**
+     * 结束时间,格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String endTime;
+
+    /**
+     * 能源类型:1-电,2-水,3-气
+     */
+    private Integer energyType;
+
+    /**
+     * 设备列表
+     */
+    private List<DeviceItem> deviceList;
+
+    /**
+     * 时间类型:hour-按小时,date-按日,month-按月,year-按年
+     */
+    private String timeType;
+
+    /**
+     * 功能点列表
+     */
+    private List<FuncItem> funcList;
+
+    @Data
+    public static class DeviceItem {
+        /**
+         * 通讯地址
+         */
+        private String commAddress;
+
+        /**
+         * 产品ID
+         */
+        private Long productId;
+
+        /**
+         * 设备名称
+         */
+        private String name;
+
+        /**
+         * 设备主键ID
+         */
+        private Integer id;
+    }
+
+    @Data
+    public static class FuncItem {
+        /**
+         * 产品ID列表
+         */
+        private List<Long> productIds;
+
+        /**
+         * 标识符
+         */
+        private String identifier;
+
+        /**
+         * 标识符名称
+         */
+        private String identifierName;
+
+        /**
+         * 标签
+         */
+        private String label;
+
+        /**
+         * 值
+         */
+        private String value;
+    }
+}

+ 81 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/EnergyReportResponse.java

@@ -0,0 +1,81 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 能源报表响应VO
+ */
+@Data
+public class EnergyReportResponse {
+
+    /**
+     * 列定义列表
+     */
+    private List<ColumnItem> columnList;
+
+    /**
+     * 数据值列表(按设备分组的二维数组)
+     * 外层 List 代表不同的设备
+     * 内层 List 包含该设备下所有请求的功能点(属性)数据
+     */
+    private List<List<ValueItem>> valueList;
+
+    @Data
+    public static class ColumnItem {
+        /**
+         * 列标签
+         */
+        private String label;
+
+        /**
+         * 列属性名
+         */
+        private String prop;
+
+        /**
+         * 是否固定列
+         */
+        private Boolean fixed;
+
+        /**
+         * 子列(用于多级表头)
+         */
+        private List<ColumnItem> children;
+    }
+
+    @Data
+    public static class ValueItem {
+        /**
+         * 设备ID
+         */
+        private String deviceId;
+
+        /**
+         * 设备名称
+         */
+        private String deviceName;
+
+        /**
+         * 通讯地址
+         */
+        private String commAddress;
+
+        /**
+         * 功能点标识符
+         */
+        private String identifier;
+
+        /**
+         * 合计值
+         */
+        private String total;
+
+        /**
+         * 动态时间列数据,key为 _0, _1, _2... 或 _1, _2...31 等
+         * 使用Map存储动态列
+         */
+        private java.util.Map<String, String> timeData;
+    }
+}

+ 50 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/ItemReportRequest.java

@@ -0,0 +1,50 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 分项报表请求VO
+ */
+@Data
+public class ItemReportRequest {
+
+    /**
+     * 空间ID
+     */
+    private Long spaceId;
+
+    /**
+     * 开始时间,格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String startTime;
+
+    /**
+     * 结束时间,格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String endTime;
+
+    /**
+     * 分项编码列表
+     */
+    private List<ItemCodeItem> itemCodes;
+
+    /**
+     * 日期类型:day-按日,month-按月,year-按年
+     */
+    private String timeType;
+
+    @Data
+    public static class ItemCodeItem {
+        /**
+         * 分项编码
+         */
+        private String code;
+
+        /**
+         * 分项名称
+         */
+        private String name;
+    }
+}

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

@@ -0,0 +1,46 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 分项报表响应VO
+ */
+@Data
+public class ItemReportResponse {
+
+    /**
+     * 数据列表
+     */
+    private List<Map<String, Object>> dataList;
+
+    /**
+     * 标题列定义列表
+     */
+    private List<TitleItem> titleList;
+
+    @Data
+    public static class TitleItem {
+        /**
+         * 列属性名
+         */
+        private String prop;
+
+        /**
+         * 是否固定列
+         */
+        private Boolean fixed;
+
+        /**
+         * 列标签
+         */
+        private String label;
+
+        /**
+         * 值(可选,用于序号等)
+         */
+        private Integer value;
+    }
+}

+ 63 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/SpaceReportRequest.java

@@ -0,0 +1,63 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 区域报表请求VO
+ */
+@Data
+public class SpaceReportRequest {
+
+    /**
+     * 区域列表
+     */
+    private List<SpaceItem> spaces;
+
+    /**
+     * 开始时间,格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String startTime;
+
+    /**
+     * 结束时间,格式:yyyy-MM-dd HH:mm:ss
+     */
+    private String endTime;
+
+    /**
+     * 分项编码列表
+     */
+    private List<ItemCodeItem> itemCodes;
+
+    /**
+     * 日期类型:month-按月,year-按年
+     */
+    private String timeType;
+
+    @Data
+    public static class SpaceItem {
+        /**
+         * 区域ID
+         */
+        private Long id;
+
+        /**
+         * 区域名称
+         */
+        private String name;
+    }
+
+    @Data
+    public static class ItemCodeItem {
+        /**
+         * 分项编码
+         */
+        private String code;
+
+        /**
+         * 分项名称
+         */
+        private String name;
+    }
+}

+ 30 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/vo/SpaceReportResponse.java

@@ -0,0 +1,30 @@
+package com.usky.ems.service.vo;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 区域报表响应VO
+ */
+@Data
+public class SpaceReportResponse {
+
+    /**
+     * 列定义列表
+     */
+    private List<EnergyReportResponse.ColumnItem> columnList;
+
+    /**
+     * 数据值列表(按区域/空间分组的二维数组)
+     * 外层 List 代表不同的区域
+     * 内层 List 包含该区域下所有设备的详细数据记录
+     */
+    private List<List<Map<String, Object>>> valueList;
+
+    /**
+     * 合计值
+     */
+    private Number total;
+}

+ 3 - 3
service-iot/service-iot-biz/src/main/java/com/usky/iot/controller/web/DmpDeviceInfoController.java

@@ -138,13 +138,13 @@ public class DmpDeviceInfoController {
     }
 
     /**
-     * 网关配置导出
+     * 网关配置查询
      * @param gatewayDeviceInfoRequestVO
      * @return
      */
     @PostMapping("/gatewayExport")
-    public void gatewayExport(@RequestBody GatewayDeviceInfoRequestVO gatewayDeviceInfoRequestVO) {
-        dmpDeviceInfoService.gatewayExport(gatewayDeviceInfoRequestVO);
+    public ApiResult<Map<String, Map<String, GatewayDeviceExportItemVO>>> gatewayExport(@RequestBody GatewayDeviceInfoRequestVO gatewayDeviceInfoRequestVO) {
+        return ApiResult.success(dmpDeviceInfoService.gatewayExport(gatewayDeviceInfoRequestVO));
     }
 
     /**

+ 5 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/domain/DmpDeviceInfo.java

@@ -157,6 +157,11 @@ public class DmpDeviceInfo implements Serializable {
      */
     private Integer buildId;
 
+    /**
+     * 地址码
+     */
+    private String addressCode;
+
     /**
      * 设备状态;1:在线,2:离线
      */

+ 2 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/mapper/DmpProductInfoMapper.java

@@ -21,4 +21,6 @@ public interface DmpProductInfoMapper extends CrudMapper<DmpProductInfo> {
 
 
     List<Map<String,Object>> selectCollect(@Param("tenantId") Integer tenantId);
+
+    List<Map<String, Object>> selectDeviceTypeByProductIds(@Param("productIds") List<Integer> productIds);
 }

+ 2 - 2
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/DmpDeviceInfoService.java

@@ -69,10 +69,10 @@ public interface DmpDeviceInfoService extends CrudService<DmpDeviceInfo> {
     void export(DmpDeviceInfoRequest dmpDeviceInfoRequest, HttpServletResponse response);
 
     /**
-     * 网关设备信息导出
+     * 网关设备配置查询
      * @param gatewayDeviceInfoRequestVO
      */
-    void gatewayExport(GatewayDeviceInfoRequestVO gatewayDeviceInfoRequestVO);
+    Map<String, Map<String, GatewayDeviceExportItemVO>> gatewayExport(GatewayDeviceInfoRequestVO gatewayDeviceInfoRequestVO);
 
     CommonPage<Object> deviceCurrentDataList(String deviceName,String installAddress,String productCode,Integer pageNum,Integer pageSize);
 

+ 100 - 49
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpDeviceInfoServiceImpl.java

@@ -8,7 +8,6 @@ import cn.afterturn.easypoi.excel.entity.ImportParams;
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
-import cn.hutool.core.io.FileUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -38,6 +37,7 @@ import com.usky.iot.domain.*;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.iot.mapper.DmpDeviceInfoMapper;
 import com.usky.iot.mapper.DmpProductAttributeMapper;
+import com.usky.iot.mapper.DmpProductInfoMapper;
 import com.usky.iot.service.*;
 import com.usky.iot.service.config.DeviceOperate;
 import com.usky.iot.service.enums.TopicInfo;
@@ -53,10 +53,7 @@ import org.springframework.web.multipart.MultipartFile;
 
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
-import java.io.File;
 import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
 import java.text.DecimalFormat;
 import java.time.Instant;
 import java.time.LocalDateTime;
@@ -106,6 +103,12 @@ public class DmpDeviceInfoServiceImpl extends AbstractCrudService<DmpDeviceInfoM
     @Autowired
     private DeviceOperate deviceOperate;
 
+    @Autowired
+    private DmpProductInfoMapper dmpProductInfoMapper;
+
+    @Autowired
+    private DmpDeviceTypeService dmpDeviceTypeService;
+
     private static final String ALARM_HTTP_URL = "/service-alarm/baseAlarm/alarmInfo";
 
 
@@ -654,14 +657,18 @@ public class DmpDeviceInfoServiceImpl extends AbstractCrudService<DmpDeviceInfoM
             spaceGatewayLambdaQueryWrapper.eq(BaseSpaceGateway::getSpaceId, diRequest.getSpaceId());
             gatewayUuidList = baseSpaceGatewayService.list(spaceGatewayLambdaQueryWrapper).stream().map(BaseSpaceGateway::getGatewayUuid).collect(Collectors.toList());
         }
+        if(StringUtils.isNotBlank(diRequest.getGatewayUuid())){
+            gatewayUuidList.add(diRequest.getGatewayUuid());
+        }
 
         if (diRequest.getDeviceStatus() != null) {
-            if (StringUtils.isNotBlank(diRequest.getDeviceId()) || StringUtils.isNotBlank(diRequest.getDeviceUuid()) || StringUtils.isNotBlank(diRequest.getDeviceName()) || diRequest.getProductId() != null || StringUtils.isNotBlank(diRequest.getProductCode()) || diRequest.getServiceStatus() != null || diRequest.getCategoryType() != null) {
+            if (StringUtils.isNotBlank(diRequest.getDeviceId()) || StringUtils.isNotBlank(diRequest.getDeviceUuid()) || StringUtils.isNotBlank(diRequest.getDeviceName()) || diRequest.getProductId() != null || CollectionUtils.isNotEmpty(diRequest.getProductIdList()) || StringUtils.isNotBlank(diRequest.getProductCode()) || diRequest.getServiceStatus() != null || diRequest.getCategoryType() != null) {
                 LambdaQueryWrapper<DmpDeviceInfo> queryWrapper = Wrappers.lambdaQuery();
                 queryWrapper.eq(StringUtils.isNotBlank(diRequest.getDeviceId()), DmpDeviceInfo::getDeviceId, diRequest.getDeviceId())
                         .eq(StringUtils.isNotBlank(diRequest.getDeviceUuid()), DmpDeviceInfo::getDeviceUuid, diRequest.getDeviceUuid())
                         .like(StringUtils.isNotBlank(diRequest.getDeviceName()), DmpDeviceInfo::getDeviceName, diRequest.getDeviceName())
                         .eq(diRequest.getProductId() != null, DmpDeviceInfo::getProductId, diRequest.getProductId())
+                        .in(CollectionUtils.isNotEmpty(diRequest.getProductIdList()), DmpDeviceInfo::getProductId, diRequest.getProductIdList())
                         .like(StringUtils.isNotBlank(diRequest.getProductCode()), DmpDeviceInfo::getProductCode, diRequest.getProductCode())
                         .eq(diRequest.getServiceStatus() != null, DmpDeviceInfo::getServiceStatus, diRequest.getServiceStatus())
                         .eq(diRequest.getCategoryType() != null, DmpDeviceInfo::getCategoryType, diRequest.getCategoryType())
@@ -774,6 +781,7 @@ public class DmpDeviceInfoServiceImpl extends AbstractCrudService<DmpDeviceInfoM
                     .eq(StringUtils.isNotBlank(diRequest.getDeviceUuid()), DmpDeviceInfo::getDeviceUuid, diRequest.getDeviceUuid())
                     .like(StringUtils.isNotBlank(diRequest.getDeviceName()), DmpDeviceInfo::getDeviceName, diRequest.getDeviceName())
                     .eq(diRequest.getProductId() != null, DmpDeviceInfo::getProductId, diRequest.getProductId())
+                    .in(CollectionUtils.isNotEmpty(diRequest.getProductIdList()), DmpDeviceInfo::getProductId, diRequest.getProductIdList())
                     .like(StringUtils.isNotBlank(diRequest.getProductCode()), DmpDeviceInfo::getProductCode, diRequest.getProductCode())
                     .eq(diRequest.getServiceStatus() != null, DmpDeviceInfo::getServiceStatus, diRequest.getServiceStatus())
                     .eq(diRequest.getCategoryType() != null, DmpDeviceInfo::getCategoryType, diRequest.getCategoryType())
@@ -860,13 +868,17 @@ public class DmpDeviceInfoServiceImpl extends AbstractCrudService<DmpDeviceInfoM
             spaceGatewayLambdaQueryWrapper.eq(BaseSpaceGateway::getSpaceId, diRequest.getSpaceId());
             gatewayUuidList = baseSpaceGatewayService.list(spaceGatewayLambdaQueryWrapper).stream().map(BaseSpaceGateway::getGatewayUuid).collect(Collectors.toList());
         }
+        if(StringUtils.isNotBlank(diRequest.getGatewayUuid())){
+            gatewayUuidList.add(diRequest.getGatewayUuid());
+        }
 
         if (diRequest.getDeviceStatus() != null) {
-            if (StringUtils.isNotBlank(diRequest.getDeviceId()) || StringUtils.isNotBlank(diRequest.getDeviceName()) || diRequest.getProductId() != null || StringUtils.isNotBlank(diRequest.getProductCode()) || diRequest.getServiceStatus() != null || diRequest.getCategoryType() != null) {
+            if (StringUtils.isNotBlank(diRequest.getDeviceId()) || StringUtils.isNotBlank(diRequest.getDeviceName()) || diRequest.getProductId() != null || CollectionUtils.isNotEmpty(diRequest.getProductIdList()) || StringUtils.isNotBlank(diRequest.getProductCode()) || diRequest.getServiceStatus() != null || diRequest.getCategoryType() != null) {
                 LambdaQueryWrapper<DmpDeviceInfo> queryWrapper = Wrappers.lambdaQuery();
                 queryWrapper.eq(StringUtils.isNotBlank(diRequest.getDeviceId()), DmpDeviceInfo::getDeviceId, diRequest.getDeviceId())
                         .like(StringUtils.isNotBlank(diRequest.getDeviceName()), DmpDeviceInfo::getDeviceName, diRequest.getDeviceName())
                         .eq(diRequest.getProductId() != null, DmpDeviceInfo::getProductId, diRequest.getProductId())
+                        .in(CollectionUtils.isNotEmpty(diRequest.getProductIdList()), DmpDeviceInfo::getProductId, diRequest.getProductIdList())
                         .like(StringUtils.isNotBlank(diRequest.getProductCode()), DmpDeviceInfo::getProductCode, diRequest.getProductCode())
                         .eq(diRequest.getServiceStatus() != null, DmpDeviceInfo::getServiceStatus, diRequest.getServiceStatus())
                         .eq(diRequest.getCategoryType() != null, DmpDeviceInfo::getCategoryType, diRequest.getCategoryType())
@@ -978,6 +990,7 @@ public class DmpDeviceInfoServiceImpl extends AbstractCrudService<DmpDeviceInfoM
             queryWrapper.eq(StringUtils.isNotBlank(diRequest.getDeviceId()), DmpDeviceInfo::getDeviceId, diRequest.getDeviceId())
                     .like(StringUtils.isNotBlank(diRequest.getDeviceName()), DmpDeviceInfo::getDeviceName, diRequest.getDeviceName())
                     .eq(diRequest.getProductId() != null, DmpDeviceInfo::getProductId, diRequest.getProductId())
+                    .in(CollectionUtils.isNotEmpty(diRequest.getProductIdList()), DmpDeviceInfo::getProductId, diRequest.getProductIdList())
                     .like(StringUtils.isNotBlank(diRequest.getProductCode()), DmpDeviceInfo::getProductCode, diRequest.getProductCode())
                     .eq(diRequest.getServiceStatus() != null, DmpDeviceInfo::getServiceStatus, diRequest.getServiceStatus())
                     .eq(diRequest.getCategoryType() != null, DmpDeviceInfo::getCategoryType, diRequest.getCategoryType())
@@ -1180,58 +1193,96 @@ public class DmpDeviceInfoServiceImpl extends AbstractCrudService<DmpDeviceInfoM
     }
 
     @Override
-    public void gatewayExport(GatewayDeviceInfoRequestVO gatewayDeviceInfoRequestVO){
-        try {
-            // 1. 查询数据库中的设备信息
-            LambdaQueryWrapper<DmpDeviceInfo> queryWrapper = Wrappers.lambdaQuery();
-            queryWrapper.eq(DmpDeviceInfo::getCategoryType,3)
-                    .eq(DmpDeviceInfo::getDeleteFlag, 0)
-                    .eq(StringUtils.isNotBlank(gatewayDeviceInfoRequestVO.getGatewayUuid()), DmpDeviceInfo::getGatewayUuid, gatewayDeviceInfoRequestVO.getGatewayUuid())
-                    .eq(StringUtils.isNotBlank(gatewayDeviceInfoRequestVO.getProductCode()), DmpDeviceInfo::getProductCode, gatewayDeviceInfoRequestVO.getProductCode())
-                    .eq(DmpDeviceInfo::getTenantId, SecurityUtils.getTenantId())
-                    .orderByDesc(DmpDeviceInfo::getId);
-            List<DmpDeviceInfo> gatewayDeviceList = this.list(queryWrapper);
-
-            // 2. 定义服务器本地保存路径(建议配置在application.yml中,这里简化写死)
-            // 注意:要确保该目录存在,否则会报错;Windows路径用\\,Linux/Mac用/
-            String localPath = gatewayDeviceInfoRequestVO.getFilePath()+ "/" + gatewayDeviceInfoRequestVO.getProductCode() + ".cfg";
-
-            // 3. 调用exportToLocalFile导出到本地
-            File file = FileUtil.file(localPath);
-            exportDeviceToTxt(gatewayDeviceList, FileUtil.getOutputStream(file));
-        } catch (Exception e) {
-            throw new RuntimeException("导出网关设备配置信息失败", e);
+    public Map<String, Map<String, GatewayDeviceExportItemVO>> gatewayExport(GatewayDeviceInfoRequestVO gatewayDeviceInfoRequestVO) {
+        if (StringUtils.isBlank(gatewayDeviceInfoRequestVO.getGatewayUuid())) {
+            throw new BusinessException("网关UUID不能为空");
         }
+        LambdaQueryWrapper<DmpDeviceInfo> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.eq(DmpDeviceInfo::getCategoryType, 3)
+                .eq(DmpDeviceInfo::getDeleteFlag, 0)
+                .eq(DmpDeviceInfo::getGatewayUuid, gatewayDeviceInfoRequestVO.getGatewayUuid())
+                .eq(DmpDeviceInfo::getTenantId, SecurityUtils.getTenantId())
+                .orderByDesc(DmpDeviceInfo::getId);
+        List<DmpDeviceInfo> gatewayDeviceList = this.list(queryWrapper);
+        if (CollectionUtil.isEmpty(gatewayDeviceList)) {
+            throw new BusinessException("未查询到网关子设备信息");
+        }
+        return buildGatewayDeviceExportMap(gatewayDeviceList);
     }
 
     /**
-     * 导出设备信息到TXT文件
-     * @param deviceList 设备列表
-     * @param outputStream 输出流(用于下载)
+     * 按设备类型简称 -> 地址码 构建网关子设备配置
      */
-    public static void exportDeviceToTxt(List<DmpDeviceInfo> deviceList, OutputStream outputStream) {
-        // 1. 构建TXT内容(自定义格式,按行写入)
-        StringBuilder content = new StringBuilder();
-        // 写入表头
-        content.append("{");
-        // 写入数据行
+    private Map<String, Map<String, GatewayDeviceExportItemVO>> buildGatewayDeviceExportMap(List<DmpDeviceInfo> deviceList) {
+        List<Integer> productIds = deviceList.stream()
+                .map(DmpDeviceInfo::getProductId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        Map<Integer, String> productTypeAbbreviaMap = getProductTypeAbbreviaMap(productIds);
+
+        Map<String, Map<String, GatewayDeviceExportItemVO>> grouped = new LinkedHashMap<>();
         for (DmpDeviceInfo device : deviceList) {
-            content.append("\n\t\"").append(device.getImsiCode()).append("\":")
-                    .append("{").append("\"").append("deviceid").append("\"").append(":").append("\"").append(device.getDeviceId()).append("\"").append(",")
-                    .append("\"").append("deviceUuid").append("\"").append(":").append("\"").append(device.getDeviceUuid()).append("\"").append(",")
-                    .append("\"").append("productCode").append("\"").append(":").append("\"").append(device.getProductCode()).append("\"")
-                    .append("}").append(",");
+            if (StringUtils.isBlank(device.getAddressCode())) {
+                continue;
+            }
+            String typeAbbrevia = productTypeAbbreviaMap.get(device.getProductId());
+            if (StringUtils.isBlank(typeAbbrevia)) {
+                throw new BusinessException("产品ID " + device.getProductId() + " 未找到对应设备类型简称");
+            }
+            GatewayDeviceExportItemVO deviceInfo = new GatewayDeviceExportItemVO();
+            deviceInfo.setDeviceid(device.getDeviceId());
+            deviceInfo.setDeviceUuid(device.getDeviceUuid());
+            deviceInfo.setProductCode(device.getProductCode());
+            grouped.computeIfAbsent(typeAbbrevia, key -> new LinkedHashMap<>())
+                    .put(device.getAddressCode(), deviceInfo);
         }
-        content.delete(content.length()-1, content.length());
-        content.append("\n}");
+        if (grouped.isEmpty()) {
+            throw new BusinessException("网关子设备地址码为空,无法导出配置");
+        }
+        return grouped;
+    }
 
-        // 2. 将内容写入输出流(UTF-8编码避免乱码)
-        try {
-            outputStream.write(content.toString().getBytes(StandardCharsets.UTF_8));
-            outputStream.flush();
-        } catch (Exception e) {
-            throw new RuntimeException("导出TXT文件失败", e);
+    /**
+     * 根据产品ID批量查询设备类型简称
+     */
+    private Map<Integer, String> getProductTypeAbbreviaMap(List<Integer> productIds) {
+        if (CollectionUtil.isEmpty(productIds)) {
+            return Collections.emptyMap();
         }
+        List<Map<String, Object>> products = dmpProductInfoMapper.selectDeviceTypeByProductIds(productIds);
+        if (CollectionUtil.isEmpty(products)) {
+            return Collections.emptyMap();
+        }
+
+        Map<Integer, Integer> productDeviceTypeMap = products.stream()
+                .collect(Collectors.toMap(
+                        map -> Integer.valueOf(map.get("id").toString()),
+                        map -> Integer.valueOf(map.get("deviceType").toString()),
+                        (a, b) -> a));
+
+        Set<String> typeCodes = productDeviceTypeMap.values().stream()
+                .filter(Objects::nonNull)
+                .map(String::valueOf)
+                .collect(Collectors.toSet());
+        if (CollectionUtil.isEmpty(typeCodes)) {
+            return Collections.emptyMap();
+        }
+
+        LambdaQueryWrapper<DmpDeviceType> typeQuery = Wrappers.lambdaQuery();
+        typeQuery.select(DmpDeviceType::getTypeCode, DmpDeviceType::getTypeAbbrevia)
+                .in(DmpDeviceType::getTypeCode, typeCodes);
+        Map<String, String> typeCodeAbbreviaMap = dmpDeviceTypeService.list(typeQuery).stream()
+                .collect(Collectors.toMap(DmpDeviceType::getTypeCode, DmpDeviceType::getTypeAbbrevia, (a, b) -> a));
+
+        Map<Integer, String> result = new HashMap<>();
+        productDeviceTypeMap.forEach((productId, deviceType) -> {
+            String typeAbbrevia = typeCodeAbbreviaMap.get(String.valueOf(deviceType));
+            if (StringUtils.isNotBlank(typeAbbrevia)) {
+                result.put(productId, typeAbbrevia);
+            }
+        });
+        return result;
     }
 
     public void exportExcel(Workbook workbook, HttpServletResponse response) {

+ 1 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpProductInfoServiceImpl.java

@@ -260,6 +260,7 @@ public class DmpProductInfoServiceImpl extends AbstractCrudService<DmpProductInf
                 .like(StringUtils.isNotBlank(piRequest.getProductName()),DmpProductInfo::getProductName,piRequest.getProductName())
                 .eq(StringUtils.isNotBlank(piRequest.getDeviceModel()),DmpProductInfo::getDeviceModel,piRequest.getDeviceModel())
                 .eq(null != piRequest.getId(),DmpProductInfo::getId,piRequest.getId())
+                .in(CollectionUtils.isNotEmpty(piRequest.getIdList()), DmpProductInfo::getId, piRequest.getIdList())
                 .eq(DmpProductInfo::getTenantId,SecurityUtils.getTenantId())
                 .eq(DmpProductInfo::getDeleteFlag,0);
         List<DmpProductInfo> records = this.list(lambdaQuery);

+ 11 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/vo/DmpDeviceInfoRequest.java

@@ -3,6 +3,7 @@ package com.usky.iot.service.vo;
 import lombok.Data;
 
 import java.util.Date;
+import java.util.List;
 
 @Data
 public class DmpDeviceInfoRequest {
@@ -21,6 +22,11 @@ public class DmpDeviceInfoRequest {
      */
     private Integer productId;
 
+    /**
+     * 产品ID集合
+     */
+    private List<Integer> productIdList;
+
     /**
      * 产品编码
      */
@@ -66,4 +72,9 @@ public class DmpDeviceInfoRequest {
      */
     private Integer spaceId;
 
+    /**
+     * 所属网关
+     */
+    private String gatewayUuid;
+
 }

+ 7 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/vo/DmpProductInfoRequest.java

@@ -2,6 +2,8 @@ package com.usky.iot.service.vo;
 
 import lombok.Data;
 
+import java.util.List;
+
 @Data
 public class DmpProductInfoRequest {
 
@@ -26,4 +28,9 @@ public class DmpProductInfoRequest {
      * id
      */
     private Integer id;
+
+    /**
+     * 产品ID集合
+     */
+    private List<Integer> idList;
 }

+ 13 - 0
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/vo/GatewayDeviceExportItemVO.java

@@ -0,0 +1,13 @@
+package com.usky.iot.service.vo;
+
+import lombok.Data;
+
+@Data
+public class GatewayDeviceExportItemVO {
+
+    private String deviceid;
+
+    private String deviceUuid;
+
+    private String productCode;
+}

+ 0 - 10
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/vo/GatewayDeviceInfoRequestVO.java

@@ -5,18 +5,8 @@ import lombok.Data;
 @Data
 public class GatewayDeviceInfoRequestVO {
 
-    /**
-     * 产品编码
-     */
-    private String productCode;
-
     /**
      * 网关设备Uuid
      */
     private String gatewayUuid;
-
-    /**
-     * 文件路径
-     */
-    private String filePath;
 }

+ 10 - 0
service-iot/service-iot-biz/src/main/resources/mapper/iot/DmpProductInfoMapper.xml

@@ -43,4 +43,14 @@
         group by product_name
     </select>
 
+    <select id="selectDeviceTypeByProductIds" resultType="java.util.Map">
+        select id, device_type as deviceType
+        from dmp_product
+        where delete_flag = 0
+        and id in
+        <foreach collection="productIds" item="productId" open="(" separator="," close=")">
+            #{productId}
+        </foreach>
+    </select>
+
 </mapper>

+ 11 - 0
service-issue/service-issue-biz/pom.xml

@@ -66,6 +66,17 @@
             <artifactId>ruoyi-common-swagger</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 8 - 7
service-issue/service-issue-biz/src/main/java/com/usky/issue/RuoYiSystemApplication.java → service-issue/service-issue-biz/src/main/java/com/usky/issue/IssueApplication.java

@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.core.env.Environment;
@@ -24,25 +25,25 @@ import java.net.UnknownHostException;
 
 //@EnableSwagger2
 @EnableCustomSwagger2
+@EnableScheduling
 @EnableFeignClients(basePackages = "com.usky")
 @MapperScan(value = "com.usky.issue.mapper")
 @ComponentScan("com.usky")
 @SpringBootApplication
-public class RuoYiSystemApplication
-{
-    private static final Logger LOGGER = LoggerFactory.getLogger(RuoYiSystemApplication.class);
+public class IssueApplication {
+    private static final Logger LOGGER = LoggerFactory.getLogger(IssueApplication.class);
 
     public static void main(String[] args) throws UnknownHostException {
-        ConfigurableApplicationContext application = SpringApplication.run(RuoYiSystemApplication.class, args);
+        ConfigurableApplicationContext application = SpringApplication.run(IssueApplication.class, args);
         Environment env = application.getEnvironment();
         String ip = InetAddress.getLocalHost().getHostAddress();
         String port = env.getProperty("server.port");
         String path = env.getProperty("server.servlet.context-path");
         LOGGER.info("\n----------------------------------------------------------\n\t" +
                 "Application is running! Access URLs:\n\t" +
-                "Local: \t\thttp://localhost:" + port + (null==path?"":path) + "/\n\t" +
-                "External: \thttp://" + ip + ":" + port + (null==path?"":path) + "/\n\t" +
-                "Api: \t\thttp://" + ip + ":" + port + (null==path?"":path) + "/swagger-ui/index.html\n\t" +
+                "Local: \t\thttp://localhost:" + port + (null == path ? "" : path) + "/\n\t" +
+                "External: \thttp://" + ip + ":" + port + (null == path ? "" : path) + "/\n\t" +
+                "Api: \t\thttp://" + ip + ":" + port + (null == path ? "" : path) + "/swagger-ui/index.html\n\t" +
                 "----------------------------------------------------------");
     }
 }

+ 194 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java

@@ -0,0 +1,194 @@
+package com.usky.issue.controller.api;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.issue.service.CloudAuthService;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudSyncReceiveService;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.service.vo.CloudConnectionTestResponse;
+import com.usky.issue.service.vo.SyncPacket;
+import com.usky.issue.service.vo.SyncPollRequest;
+import com.usky.issue.service.vo.SyncResponse;
+import com.usky.issue.service.util.CredentialMaskUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 云平台集成 API(供本地服务器调用:连通性测试 + 双向同步)
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Slf4j
+@RestController
+@RequestMapping("/cloud/integration")
+public class CloudIntegrationApiController {
+
+    @Autowired
+    private CloudConfigService cloudConfigService;
+    @Autowired
+    private CloudAuthService cloudAuthService;
+    @Autowired
+    private CloudSyncReceiveService cloudSyncReceiveService;
+
+    /**
+     * 网络层探测
+     */
+    @GetMapping("/ping")
+    public String ping() {
+        return "PONG";
+    }
+
+    /**
+     * 认证层 + 租户层检测
+     */
+    @GetMapping("/auth-check")
+    public ResponseEntity<String> authCheck(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestParam Integer tenantId) {
+        if (!cloudAuthService.validateToken(apiKey, tenantId)) {
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("AUTH_INVALID");
+        }
+        CloudConnectionTestResponse tenantResult = cloudConfigService.validateTenantConnection(tenantId);
+        if (!tenantResult.isSuccess()) {
+            return ResponseEntity.ok(CloudIntegrationConstants.AUTH_CHECK_TENANT_MISSING);
+        }
+        return ResponseEntity.ok(CloudIntegrationConstants.AUTH_CHECK_VALID);
+    }
+
+    /**
+     * 连通性测试入口(兼容旧调用方式)
+     */
+    @PostMapping("/testConnection")
+    public ApiResult<CloudConnectionTestResponse> testConnection(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_TENANT_ID, required = false) String tenantIdHeader,
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey) {
+        Integer tenantId = parseTenantId(tenantIdHeader);
+        log.info("[连接测试-云端] 收到请求 tenantIdHeader={}, tenantId={}, apiKey={}",
+                tenantIdHeader, tenantId, CredentialMaskUtil.mask(apiKey));
+
+        if (tenantId != null && !cloudAuthService.validateToken(apiKey, tenantId)) {
+            log.warn("[连接测试-云端] 认证失败 tenantId={}, apiKey={}", tenantId, CredentialMaskUtil.mask(apiKey));
+            CloudConnectionTestResponse response = new CloudConnectionTestResponse();
+            response.setSuccess(false);
+            response.setConnectionStatus(2);
+            response.setMessage(CloudIntegrationConstants.MSG_AUTH_INVALID);
+            response.setNetworkReachable(true);
+            response.setAuthValid(false);
+            response.setTenantExists(false);
+            return ApiResult.success(response);
+        }
+        CloudConnectionTestResponse response = cloudConfigService.validateTenantConnection(tenantId);
+        log.info("[连接测试-云端] 租户校验 success={}, message={}", response.isSuccess(), response.getMessage());
+        response.setNetworkReachable(true);
+        response.setAuthValid(tenantId == null || cloudAuthService.validateToken(apiKey, tenantId));
+        response.setTenantExists(response.isSuccess());
+        log.info("[连接测试-云端] 返回 success={}, authValid={}, tenantExists={}, message={}",
+                response.isSuccess(), response.getAuthValid(), response.getTenantExists(), response.getMessage());
+        return ApiResult.success(response);
+    }
+
+    /**
+     * 接收本地推送数据(本地→云端)
+     */
+    @PostMapping("/sync/receive")
+    public SyncResponse receive(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_SIGN, required = false) String sign,
+            @RequestBody SyncPacket packet) {
+
+        if (!cloudAuthService.validate(apiKey, sign, packet)) {
+            return SyncResponse.builder().success(false).message("认证失败或请求已过期").build();
+        }
+        if (!cloudSyncReceiveService.validateTenant(packet.getTenantId())) {
+            return SyncResponse.builder().success(false).message("租户不存在或已停用").build();
+        }
+        try {
+            int count = cloudSyncReceiveService.batchUpsert(packet);
+            return SyncResponse.builder()
+                    .success(true)
+                    .message("接收成功")
+                    .acceptedCount(count)
+                    .confirmedVersion(packet.getBatchVersion())
+                    .build();
+        } catch (Exception e) {
+            return SyncResponse.builder().success(false).message("写入失败:" + e.getMessage()).build();
+        }
+    }
+
+    /**
+     * 下发数据轮询(云端→本地),POST 供网关/offline-api 路由(与 testConnection 一致)
+     */
+    @PostMapping("/sync/poll")
+    public ApiResult<SyncPacket> pollPost(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestBody SyncPollRequest request) {
+        return doPoll(apiKey, request.getTenantId(), request.getTableName(),
+                request.getLastVersion(), request.isForceBackfill());
+    }
+
+    /**
+     * 下发数据轮询(GET 兼容旧调用)
+     */
+    @GetMapping("/sync/poll")
+    public ApiResult<SyncPacket> poll(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestParam String tenantId,
+            @RequestParam String tableName,
+            @RequestParam(required = false) Long lastVersion,
+            @RequestParam(required = false, defaultValue = "false") boolean forceBackfill) {
+        return doPoll(apiKey, tenantId, tableName, lastVersion, forceBackfill);
+    }
+
+    private ApiResult<SyncPacket> doPoll(String apiKey, String tenantId, String tableName,
+                                         Long lastVersion, boolean forceBackfill) {
+        if (!cloudAuthService.validateToken(apiKey, Integer.valueOf(tenantId))) {
+            log.warn("[poll] 认证失败 tenantId={}, tableName={}", tenantId, tableName);
+            return ApiResult.error(CloudIntegrationConstants.MSG_POLL_AUTH_FAILED);
+        }
+        log.info("[poll] tenantId={}, tableName={}, lastVersion={}, forceBackfill={}",
+                tenantId, tableName, lastVersion, forceBackfill);
+        return ApiResult.success(cloudSyncReceiveService.pollDownQueue(
+                tenantId, tableName, lastVersion, forceBackfill));
+    }
+
+    /**
+     * 确认下发数据已消费
+     */
+    @PostMapping("/sync/poll/ack")
+    public SyncResponse ackPoll(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_TENANT_ID, required = false) String tenantIdHeader,
+            @RequestBody List<Long> queueIds) {
+
+        Integer tenantId = parseTenantId(tenantIdHeader);
+        if (tenantId != null && !cloudAuthService.validateToken(apiKey, tenantId)) {
+            return SyncResponse.builder().success(false).message("认证失败").build();
+        }
+        cloudSyncReceiveService.ackDownQueue(queueIds);
+        return SyncResponse.builder().success(true).message("确认成功").build();
+    }
+
+    private Integer parseTenantId(String tenantIdHeader) {
+        if (!StringUtils.hasText(tenantIdHeader)) {
+            return null;
+        }
+        try {
+            return Integer.valueOf(tenantIdHeader.trim());
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+}

+ 68 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudConfigController.java

@@ -0,0 +1,68 @@
+package com.usky.issue.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.issue.service.vo.CloudConfigResponse;
+import com.usky.issue.service.vo.CloudConfigSaveRequest;
+import com.usky.issue.service.vo.CloudConnectionTestRequest;
+import com.usky.issue.service.vo.CloudConnectionTestResponse;
+import com.usky.issue.service.vo.CloudDisableResponse;
+import com.usky.issue.service.CloudConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+
+/**
+ * 云平台配置接口
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Validated
+@RestController
+@RequestMapping("/cloud")
+public class CloudConfigController {
+
+    @Autowired
+    private CloudConfigService cloudConfigService;
+
+    /**
+     * 获取云平台配置
+     */
+    @GetMapping("/config")
+    public ApiResult<CloudConfigResponse> getConfig() {
+        return ApiResult.success(cloudConfigService.getConfig());
+    }
+
+    /**
+     * 保存、修改云平台配置
+     */
+    @PutMapping("/config")
+    public ApiResult<CloudConfigResponse> saveConfig(
+            @Valid @RequestBody CloudConfigSaveRequest request) {
+        return ApiResult.success(cloudConfigService.saveConfig(request));
+    }
+
+
+    /**
+     * 分层连通性测试:网络层 → 认证层 → 租户层
+     */
+    @PostMapping("/config/testConnection")
+    public ApiResult<CloudConnectionTestResponse> testConnection(
+            @Valid @RequestBody CloudConnectionTestRequest request
+    ) {
+        return ApiResult.success(cloudConfigService.testConnection(request));
+    }
+
+    /**
+     * 启用/禁用云平台(status: 0-禁用, 1-启用)
+     */
+    @PutMapping("/config/disable")
+    public ApiResult<CloudDisableResponse> disable(@RequestParam Integer status) {
+        return ApiResult.success(cloudConfigService.disable(status));
+    }
+
+}

+ 43 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java

@@ -0,0 +1,43 @@
+package com.usky.issue.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.issue.service.vo.ManualSyncRequest;
+import com.usky.issue.service.vo.SyncStatusResponse;
+import com.usky.issue.service.vo.SyncTaskResponse;
+import com.usky.issue.service.CloudSyncService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 云平台数据同步接口
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@RestController
+@Validated
+@RequestMapping("/cloud")
+public class CloudSyncController {
+
+    @Autowired
+    private CloudSyncService cloudSyncService;
+
+    @GetMapping("/sync/status")
+    public ApiResult<List<SyncStatusResponse>> listStatus() {
+        return ApiResult.success(cloudSyncService.listSyncStatus());
+    }
+
+    @PostMapping("/sync")
+    public ApiResult<SyncTaskResponse> manualSync(@RequestBody ManualSyncRequest request) {
+        return ApiResult.success(cloudSyncService.triggerManualSync(request.getType(), request.getDirection()));
+    }
+
+    @GetMapping("/sync/task/{taskId}")
+    public ApiResult<SyncTaskResponse> taskProgress(@PathVariable Long taskId) {
+        return ApiResult.success(cloudSyncService.getTaskProgress(taskId));
+    }
+}

+ 49 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudTriggerEventController.java

@@ -0,0 +1,49 @@
+package com.usky.issue.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.issue.service.vo.TriggerEventResponse;
+import com.usky.issue.service.vo.TriggerEventSaveRequest;
+import com.usky.issue.service.CloudTriggerEventService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 云平台触发事件接口
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Validated
+@RestController
+@RequestMapping("/cloud")
+public class CloudTriggerEventController {
+
+    @Autowired
+    private CloudTriggerEventService cloudTriggerEventService;
+
+    @GetMapping("/triggerEvents")
+    public ApiResult<List<TriggerEventResponse>> listEvents() {
+        return ApiResult.success(cloudTriggerEventService.listEvents());
+    }
+
+    @PutMapping("/triggerEvents")
+    public ApiResult<Void> saveEvents(@Valid @RequestBody TriggerEventSaveRequest request,
+                                      HttpServletRequest httpRequest) {
+        cloudTriggerEventService.saveEvents(request, resolveOperator(httpRequest), httpRequest.getRemoteAddr());
+        return ApiResult.success();
+    }
+
+    private String resolveOperator(HttpServletRequest request) {
+        String user = request.getHeader("X-User-Name");
+        return user != null ? user : "system";
+    }
+}

+ 25 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/sys_user.sql

@@ -0,0 +1,25 @@
+CREATE TABLE `sys_user` (
+  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
+  `dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID',
+  `user_name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户账号',
+  `nick_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户昵称',
+  `user_type` varchar(2) COLLATE utf8mb4_unicode_ci DEFAULT '00' COMMENT '用户类型(00系统用户, 01 租户管理员)',
+  `email` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '用户邮箱',
+  `phonenumber` varchar(11) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '手机号码',
+  `sex` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
+  `full_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '姓名',
+  `avatar` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '头像地址',
+  `password` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '密码',
+  `status` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
+  `del_flag` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
+  `login_ip` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '最后登录IP',
+  `login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
+  `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
+  `tenant_id` int(11) DEFAULT NULL COMMENT '租户ID',
+  `address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
+  PRIMARY KEY (`user_id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1825 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户信息表';

+ 36 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/CloudAuditEntity.java

@@ -0,0 +1,36 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 云平台实体审计字段基类
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public abstract class CloudAuditEntity implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 租户ID */
+    private Integer tenantId;
+
+    private String createdBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createdTime;
+
+    private String updatedBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updatedTime;
+
+    @TableLogic(value = "0", delval = "1")
+    private Integer isDeleted;
+}

+ 44 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueCloudConfig.java

@@ -0,0 +1,44 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 云平台配置
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_cloud_config")
+public class IssueCloudConfig extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /** 云平台地址 */
+    private String cloudAddress;
+
+    /** AES-256-GCM加密后的凭证密钥 */
+    private String credentialKey;
+
+    /** 0禁用 1启用 */
+    private Integer status;
+
+    /** 0未知 1成功 2失败 */
+    private Integer connectionStatus;
+
+    // @Version
+    private Integer version;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime lastTestTime;
+
+    @TableField(exist = false)
+    private String token;
+}

+ 30 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueOperationLog.java

@@ -0,0 +1,30 @@
+package com.usky.issue.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;

+

+/**

+ * 操作审计日志

+ *

+ * @author fyc

+ * @date 2026-05-21

+ */

+@Data

+@EqualsAndHashCode(callSuper = true)

+@TableName("issue_operation_log")

+public class IssueOperationLog extends CloudAuditEntity {

+

+    @TableId(value = "id", type = IdType.AUTO)

+    private Long id;

+

+    private Long configId;

+

+    private String operationType;

+

+    private String operationDetail;

+

+    private String requestIp;

+}


+ 33 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncCursor.java

@@ -0,0 +1,33 @@
+package com.usky.issue.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.time.LocalDateTime;
+
+/**
+ * 同步游标
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_cursor")
+public class IssueSyncCursor extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private String tableName;
+
+    /** UP/DOWN */
+    private String direction;
+
+    private Long lastSyncVersion;
+
+    private LocalDateTime lastSyncTime;
+}

+ 38 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncData.java

@@ -0,0 +1,38 @@
+package com.usky.issue.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.time.LocalDateTime;
+
+/**
+ * 本地同步业务数据
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_data")
+public class IssueSyncData extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private String tableName;
+
+    private String dataKey;
+
+    private String payload;
+
+    private Long syncVersion;
+
+    /** 0:未同步 1:已推送 2:推送失败 */
+    private Integer syncStatus;
+
+    /** LOCAL/CLOUD */
+    private String syncDirection;
+}

+ 32 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDetail.java

@@ -0,0 +1,32 @@
+package com.usky.issue.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;
+
+/**
+ * 同步任务明细
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_detail")
+public class IssueSyncDetail extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private Long taskId;
+
+    private String dataId;
+
+    private String detailStatus;
+
+    private Integer retryCount;
+
+    private String errorMessage;
+}

+ 34 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDownQueue.java

@@ -0,0 +1,34 @@
+package com.usky.issue.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;
+
+/**
+ * 云端下发队列(云端→本地)
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_down_queue")
+public class IssueSyncDownQueue extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private String tableName;
+
+    /** INSERT/UPDATE/DELETE */
+    private String operation;
+
+    private String payload;
+
+    private Long syncVersion;
+
+    /** 0:待拉取 1:已确认 2:失败 */
+    private Integer status;
+}

Неке датотеке нису приказане због велике количине промена