From fc450900c7a16082ecdb448e19c29669072fc99e Mon Sep 17 00:00:00 2001 From: 25604 Date: Sat, 28 Feb 2026 08:54:24 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=B7=BB=E5=8A=A0ai=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mh-admin/pom.xml | 22 ++ .../controller/ai/AsyncReportController.java | 47 +++ .../controller/ai/FileDownloadController.java | 39 +++ .../src/main/resources/application-dev.yml | 9 + mh-admin/src/main/resources/application.yml | 2 +- .../test/java/com/mh/MHApplicationTest.java | 217 +++++------- mh-common/pom.xml | 46 ++- .../com/mh/common/config/FileStorageUtil.java | 35 ++ .../common/core/domain/dto/AsyncTaskDTO.java | 24 ++ .../core/domain/dto/EnergyReportDTO.java | 99 ++++++ .../core/domain/dto/ReportRequestDTO.java | 21 ++ mh-framework/pom.xml | 5 + .../com/mh/framework/config/AsyncConfig.java | 29 ++ mh-quartz/pom.xml | 4 + mh-system/pom.xml | 39 +++ .../service/ai/IEnergyReportService.java | 17 + .../service/ai/IFileGenerationService.java | 20 ++ .../com/mh/system/service/ai/INLPService.java | 15 + .../ai/impl/AsyncReportServiceImpl.java | 114 ++++++ .../ai/impl/EnergyReportServiceImpl.java | 189 ++++++++++ .../ai/impl/ExcelGenerationServiceImpl.java | 204 +++++++++++ .../service/ai/impl/NLPServiceImpl.java | 52 +++ .../ai/impl/PdfGenerationServiceImpl.java | 287 +++++++++++++++ .../ai/impl/ReportOrchestrationService.java | 43 +++ .../service/ai/impl/TaskStoreService.java | 34 ++ .../ai/impl/WordGenerationServiceImpl.java | 328 ++++++++++++++++++ pom.xml | 31 ++ 27 files changed, 1830 insertions(+), 142 deletions(-) create mode 100644 mh-admin/src/main/java/com/mh/web/controller/ai/AsyncReportController.java create mode 100644 mh-admin/src/main/java/com/mh/web/controller/ai/FileDownloadController.java create mode 100644 mh-common/src/main/java/com/mh/common/config/FileStorageUtil.java create mode 100644 mh-common/src/main/java/com/mh/common/core/domain/dto/AsyncTaskDTO.java create mode 100644 mh-common/src/main/java/com/mh/common/core/domain/dto/EnergyReportDTO.java create mode 100644 mh-common/src/main/java/com/mh/common/core/domain/dto/ReportRequestDTO.java create mode 100644 mh-framework/src/main/java/com/mh/framework/config/AsyncConfig.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/IEnergyReportService.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/IFileGenerationService.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/INLPService.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/impl/AsyncReportServiceImpl.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/impl/EnergyReportServiceImpl.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/impl/ExcelGenerationServiceImpl.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/impl/NLPServiceImpl.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/impl/PdfGenerationServiceImpl.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/impl/ReportOrchestrationService.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/impl/TaskStoreService.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/impl/WordGenerationServiceImpl.java diff --git a/mh-admin/pom.xml b/mh-admin/pom.xml index 9b63aa2..fc66bfb 100644 --- a/mh-admin/pom.xml +++ b/mh-admin/pom.xml @@ -75,6 +75,28 @@ mh-generator + + + org.springframework.retry + spring-retry + + + + + org.springframework.ai + spring-ai-starter-model-openai + + + org.springframework.boot + spring-boot-starter-jackson + + + org.springframework.boot + spring-boot-jackson + + + + diff --git a/mh-admin/src/main/java/com/mh/web/controller/ai/AsyncReportController.java b/mh-admin/src/main/java/com/mh/web/controller/ai/AsyncReportController.java new file mode 100644 index 0000000..50172a8 --- /dev/null +++ b/mh-admin/src/main/java/com/mh/web/controller/ai/AsyncReportController.java @@ -0,0 +1,47 @@ +package com.mh.web.controller.ai; + +import com.mh.common.core.domain.dto.AsyncTaskDTO; +import com.mh.system.service.ai.impl.AsyncReportServiceImpl; +import com.mh.system.service.ai.impl.TaskStoreService; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 一步报表controller + * @date 2026-02-27 11:36:57 + */ +@RestController +@RequestMapping("/api/reports/async") +public class AsyncReportController { + private final AsyncReportServiceImpl asyncReportService; + private final TaskStoreService taskStore; + + public AsyncReportController(AsyncReportServiceImpl asyncReportService, TaskStoreService taskStore) { + this.asyncReportService = asyncReportService; + this.taskStore = taskStore; + } + + @PostMapping + public ResponseEntity> submitTask(@RequestBody String userMessage) { + String taskId = asyncReportService.createAsyncTask(userMessage); + return ResponseEntity.accepted().body(Map.of("taskId", taskId)); + } + + @GetMapping(value = "/{taskId}/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter subscribe(@PathVariable String taskId) { + return asyncReportService.registerEmitter(taskId); + } + + @GetMapping("/{taskId}") + public ResponseEntity getTask(@PathVariable String taskId) { + AsyncTaskDTO task = taskStore.get(taskId); + return task != null ? ResponseEntity.ok(task) : ResponseEntity.notFound().build(); + } +} diff --git a/mh-admin/src/main/java/com/mh/web/controller/ai/FileDownloadController.java b/mh-admin/src/main/java/com/mh/web/controller/ai/FileDownloadController.java new file mode 100644 index 0000000..4df1e0d --- /dev/null +++ b/mh-admin/src/main/java/com/mh/web/controller/ai/FileDownloadController.java @@ -0,0 +1,39 @@ +package com.mh.web.controller.ai; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 文件下载链接 + * @date 2026-02-27 11:40:07 + */ +@RequestMapping("/api/files") +public class FileDownloadController { + @Value("${mh.profile:/tmp/energy-reports}") + private String storageDir; + + @GetMapping("/{filename}") + public ResponseEntity downloadFile(@PathVariable String filename) throws Exception { + Path filePath = Paths.get(storageDir).resolve(filename); + Resource resource = new UrlResource(filePath.toUri()); + if (resource.exists()) { + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"") + .body(resource); + } + return ResponseEntity.notFound().build(); + } + +} \ No newline at end of file diff --git a/mh-admin/src/main/resources/application-dev.yml b/mh-admin/src/main/resources/application-dev.yml index ddcd913..4ff0e96 100644 --- a/mh-admin/src/main/resources/application-dev.yml +++ b/mh-admin/src/main/resources/application-dev.yml @@ -39,6 +39,15 @@ logging: # Spring配置 spring: + # AI配置 + ai: + openai: + base-url: https://api.deepseek.com + api-key: sk-18202beb1c8e4208a6f52b335fef5edf + chat: + options: + model: deepseek-chat + temperature: 0.1 # 资源信息 messages: # 国际化资源文件路径 diff --git a/mh-admin/src/main/resources/application.yml b/mh-admin/src/main/resources/application.yml index 44f7920..c3ae7c3 100644 --- a/mh-admin/src/main/resources/application.yml +++ b/mh-admin/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: profiles: - active: prod + active: dev # 用户配置 user: diff --git a/mh-admin/src/test/java/com/mh/MHApplicationTest.java b/mh-admin/src/test/java/com/mh/MHApplicationTest.java index 29d889d..046767f 100644 --- a/mh-admin/src/test/java/com/mh/MHApplicationTest.java +++ b/mh-admin/src/test/java/com/mh/MHApplicationTest.java @@ -2,26 +2,21 @@ package com.mh; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; -import com.mh.common.core.domain.dto.ProProfileDTO; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mh.common.core.domain.dto.EnergyReportDTO; import com.mh.common.core.domain.entity.DeviceReport; import com.mh.common.core.domain.entity.SysParams; import com.mh.common.core.domain.entity.SysUser; import com.mh.common.core.domain.entity.WeatherData; -import com.mh.common.core.domain.vo.EnergyQueryVO; import com.mh.common.utils.DateUtils; import com.mh.common.utils.StringUtils; import com.mh.quartz.task.DealDataTask; import com.mh.quartz.task.GetWeatherDataTask; -import com.mh.quartz.task.HotWaterTask; import com.mh.system.mapper.device.DataProcessMapper; import com.mh.system.service.ISysParamsService; import com.mh.system.service.ISysUserService; import com.mh.system.service.device.IDeviceQrManageService; -import com.mh.system.service.operation.IAlarmRecordsService; -import com.mh.system.service.overview.IProOverviewService; -import com.mh.system.service.report.IComprehensiveReportService; -import com.mh.system.service.report.IMeterReadingsHisService; -import com.mh.system.service.report.IReportHotWaterService; import jakarta.annotation.Resource; import org.checkerframework.checker.units.qual.A; import org.junit.jupiter.api.Test; @@ -48,135 +43,6 @@ public class MHApplicationTest { @Autowired private ISysUserService sysUserService; - @Autowired - private HotWaterTask hotWaterTask; - - @Autowired - private IAlarmRecordsService alarmRecordsService; - - @Autowired - private IMeterReadingsHisService meterReadingsHisService; - - @Autowired - private IReportHotWaterService reportHotWaterService; - - @Autowired - private IProOverviewService proOverviewService; - - @Autowired - private IComprehensiveReportService comprehensiveReportService; - - @Test - public void comprehensiveReport() { - long startTime = System.currentTimeMillis(); - EnergyQueryVO vo = new EnergyQueryVO(); - vo.setStartTime("2025-12-24 00:00:00"); - vo.setEndTime("2025-12-24 23:59:59"); - vo.setPageNum(1); - vo.setPageSize(10); - vo.setTimeType("hour"); - System.out.println("开始查询"); - List report = comprehensiveReportService.report(vo); - System.out.println("报表耗时:" + (System.currentTimeMillis() - startTime) + "ms"); - } - - @Test - public void testHome() throws Exception { - // 开始计时 - long startTime = System.currentTimeMillis(); - List proProfile = proOverviewService.getProProfile(); - System.out.println("耗时:" + (System.currentTimeMillis() - startTime) + "ms"); - } - - @Test - public void reportHotWater() { - reportHotWaterService.execProRunParamHis(); - } - - @Test - public void testExecProMeterReadingsHis() { - meterReadingsHisService.execProMeterReadingsHis("2025-12-11"); - } - - @Test - public void createAlarmTask() { - alarmRecordsService.insertOrUpdateAlarmRecord("e1a3034edw6a9b3a79a86332886b24896"); - } - - @Test - public void calcAnalysisData() { - for (int i = 9; i < 10; i++) { - hotWaterTask.calcAnalysisData("2025-07-0"+i); - } - } - - @Test - public void testDate() { - Date date = new Date(); - boolean sameDay = DateUtils.isSameDay(DateUtils.stringToDate("2025-09-24 00:00:00", "yyyy-MM-dd HH:mm:ss"), date); - System.out.println(sameDay); - } - - @Test - public void calcEnergyData() { - - for (int i = 1; i < 17; i++) { - // i < 10,则前面添加0 - if (i < 10) { - hotWaterTask.calcEnergyData("2025-10-0"+i+" 00:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 01:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 02:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 03:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 04:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 05:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 06:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 07:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 08:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 09:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 10:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 11:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 12:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 13:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 14:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 15:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 16:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 17:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 18:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 19:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 20:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 21:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 22:00:00"); - hotWaterTask.calcEnergyData("2025-10-0"+i+" 23:00:00"); - } else { - hotWaterTask.calcEnergyData("2025-10-" + i + " 00:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 01:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 02:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 03:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 04:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 05:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 06:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 07:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 08:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 09:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 10:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 11:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 12:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 13:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 14:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 15:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 16:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 17:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 18:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 19:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 20:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 21:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 22:00:00"); - hotWaterTask.calcEnergyData("2025-10-" + i + " 23:00:00"); - } - } - - } - @Test public void test() throws Exception { SysUser sysUser = sysUserService.selectUserById(1L); @@ -253,4 +119,81 @@ public class MHApplicationTest { System.out.println(data); dataProcessMapper.insertTable(data, "data_hour2025"); } + + @Autowired + private ObjectMapper objectMapper; + + @Test + public void testReportDTO() throws JsonProcessingException { + String jsonStr = "{\n" + + " \"reportDate\": \"2026-02-27\",\n" + + " \"overallScore\": 75,\n" + + " \"systemEER\": 3.5,\n" + + " \"totalEnergyConsumption\": 242.9,\n" + + " \"deviceDetails\": [\n" + + " {\n" + + " \"deviceName\": \"冷机-1\",\n" + + " \"deviceType\": \"冷机\",\n" + + " \"energyConsumption\": 170.0,\n" + + " \"efficiency\": 82.0,\n" + + " \"deviationRate\": -18.0\n" + + " },\n" + + " {\n" + + " \"deviceName\": \"冷冻泵-1\",\n" + + " \"deviceType\": \"冷冻泵\",\n" + + " \"energyConsumption\": 24.3,\n" + + " \"efficiency\": 65.0,\n" + + " \"deviationRate\": -23.5\n" + + " },\n" + + " {\n" + + " \"deviceName\": \"冷却泵-1\",\n" + + " \"deviceType\": \"冷却泵\",\n" + + " \"energyConsumption\": 29.1,\n" + + " \"efficiency\": 70.0,\n" + + " \"deviationRate\": -17.6\n" + + " },\n" + + " {\n" + + " \"deviceName\": \"冷却塔-1\",\n" + + " \"deviceType\": \"冷却塔\",\n" + + " \"energyConsumption\": 19.4,\n" + + " \"efficiency\": 75.0,\n" + + " \"deviationRate\": -16.7\n" + + " }\n" + + " ],\n" + + " \"anomalies\": [\n" + + " {\n" + + " \"deviceName\": \"冷冻泵-1\",\n" + + " \"anomalyType\": \"效率低下\",\n" + + " \"description\": \"运行效率为65%,低于基准效率85%的80%(即68%),存在能效异常\",\n" + + " \"severity\": \"中\",\n" + + " \"estimatedLoss\": 5.8\n" + + " }\n" + + " ],\n" + + " \"suggestions\": [\n" + + " {\n" + + " \"title\": \"优化冷冻泵运行效率\",\n" + + " \"description\": \"冷冻泵效率仅65%,低于基准值,建议检查泵体磨损、电机负载或阀门开度,必要时进行维护或更换高效电机\",\n" + + " \"expectedSaving\": 4.5,\n" + + " \"priority\": \"高\",\n" + + " \"action\": \"安排设备巡检与性能测试\"\n" + + " },\n" + + " {\n" + + " \"title\": \"调整冷却塔运行策略\",\n" + + " \"description\": \"冷却塔效率75%,略低于基准,可优化风机转速或水流分配,提升散热效果以降低冷机能耗\",\n" + + " \"expectedSaving\": 2.0,\n" + + " \"priority\": \"中\",\n" + + " \"action\": \"调整风机变频设置并清洗填料\"\n" + + " },\n" + + " {\n" + + " \"title\": \"系统整体能效监控\",\n" + + " \"description\": \"当前系统EER为3.5,处于行业平均水平。建议安装实时监测系统,动态调整设备运行参数,提升综合能效\",\n" + + " \"expectedSaving\": 6.0,\n" + + " \"priority\": \"中\",\n" + + " \"action\": \"部署能效管理平台并制定运行优化策略\"\n" + + " }\n" + + " ]\n" + + "}"; + EnergyReportDTO energyReportDTO = objectMapper.readValue(jsonStr, EnergyReportDTO.class); + System.out.println(energyReportDTO); + } } diff --git a/mh-common/pom.xml b/mh-common/pom.xml index d257339..0b48645 100644 --- a/mh-common/pom.xml +++ b/mh-common/pom.xml @@ -72,9 +72,20 @@ + + org.apache.poi + poi + ${poi.version} + org.apache.poi poi-ooxml + ${poi.version} + + + org.apache.poi + poi-ooxml-schemas + ${poi.version} @@ -155,9 +166,30 @@ easyexcel + + + com.github.binarywang + weixin-java-mp + 4.7.6.B + + + + + org.springframework.ai + spring-ai-starter-model-openai + + + org.springframework.ai + spring-ai-retry + + + + + org.projectlombok lombok + true com.google.guava @@ -166,11 +198,17 @@ compile - + - com.github.binarywang - weixin-java-mp - 4.7.6.B + com.itextpdf + itextpdf + 5.5.13.5 + compile + + + org.springframework + spring-webmvc + 6.2.10 diff --git a/mh-common/src/main/java/com/mh/common/config/FileStorageUtil.java b/mh-common/src/main/java/com/mh/common/config/FileStorageUtil.java new file mode 100644 index 0000000..14e8fab --- /dev/null +++ b/mh-common/src/main/java/com/mh/common/config/FileStorageUtil.java @@ -0,0 +1,35 @@ +package com.mh.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 文件服务类 + * @date 2026-02-27 11:32:04 + */ +@Component +public class FileStorageUtil { + @Value("${mh.profile:/tmp/energy-reports}") + private String storageDir; + + public String saveFile(String filename, byte[] content) throws IOException { + Path dir = Paths.get(storageDir); + if (!Files.exists(dir)) Files.createDirectories(dir); + Path filePath = dir.resolve(filename); + Files.write(filePath, content); + // 生成可下载的URL(假设当前应用提供静态资源访问或下载接口) + return ServletUriComponentsBuilder.fromCurrentContextPath() + .path("/api/files/") + .path(filename) + .toUriString(); + } +} \ No newline at end of file diff --git a/mh-common/src/main/java/com/mh/common/core/domain/dto/AsyncTaskDTO.java b/mh-common/src/main/java/com/mh/common/core/domain/dto/AsyncTaskDTO.java new file mode 100644 index 0000000..88044f3 --- /dev/null +++ b/mh-common/src/main/java/com/mh/common/core/domain/dto/AsyncTaskDTO.java @@ -0,0 +1,24 @@ +package com.mh.common.core.domain.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 任务状态dto + * @date 2026-02-27 10:22:40 + */ +@Data +public class AsyncTaskDTO { + + private String taskId; + private String status; // PROCESSING, COMPLETED, FAILED + private String message; // 成功时的消息或失败原因 + private String downloadUrl; // 生成文件的下载URL(成功后填充) + private LocalDateTime createTime; + private LocalDateTime completeTime; + +} diff --git a/mh-common/src/main/java/com/mh/common/core/domain/dto/EnergyReportDTO.java b/mh-common/src/main/java/com/mh/common/core/domain/dto/EnergyReportDTO.java new file mode 100644 index 0000000..4ace80c --- /dev/null +++ b/mh-common/src/main/java/com/mh/common/core/domain/dto/EnergyReportDTO.java @@ -0,0 +1,99 @@ +package com.mh.common.core.domain.dto; + +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description ai机房能效报表 + * @date 2026-02-27 10:11:57 + */ +@Data +@Builder +public class EnergyReportDTO { + + @JsonPropertyDescription("报表生成日期") + private String reportDate; + + @JsonPropertyDescription("整体能效评分 (0-100)") + private Integer overallScore; + + @JsonPropertyDescription("系统综合能效比") + private Double systemEER; + + @JsonPropertyDescription("总能耗 (kWh)") + private Double totalEnergyConsumption; + + @JsonPropertyDescription("各设备能耗明细") + private List deviceDetails; + + @JsonPropertyDescription("异常情况列表") + private List anomalies; + + @JsonPropertyDescription("优化建议") + private List suggestions; + + @Data + @Builder + public static class DeviceEnergy { + @JsonPropertyDescription("设备名称") + private String deviceName; + + @JsonPropertyDescription("设备类型 (冷机/冷冻泵/冷却泵/冷却塔)") + private String deviceType; + + @JsonPropertyDescription("能耗 (kWh)") + private Double energyConsumption; + + @JsonPropertyDescription("运行效率 (%)") + private Double efficiency; + + @JsonPropertyDescription("对比基准的偏差率") + private Double deviationRate; + } + + @Data + @Builder + public static class Anomaly { + @JsonPropertyDescription("异常设备") + private String deviceName; + + @JsonPropertyDescription("异常类型") + private String anomalyType; + + @JsonPropertyDescription("异常描述") + private String description; + + @JsonPropertyDescription("严重程度 (高/中/低)") + private String severity; + + @JsonPropertyDescription("预估能耗损失 (kWh)") + private Double estimatedLoss; + } + + @Data + @Builder + public static class Suggestion { + @JsonPropertyDescription("建议标题") + private String title; + + @JsonPropertyDescription("建议详细内容") + private String description; + + @JsonPropertyDescription("预期节能效果 (kWh)") + private Double expectedSaving; + + @JsonPropertyDescription("优先级 (高/中/低)") + private String priority; + + @JsonPropertyDescription("建议执行的操作 (如:检查/维修/调整参数)") + private String action; + } + + +} diff --git a/mh-common/src/main/java/com/mh/common/core/domain/dto/ReportRequestDTO.java b/mh-common/src/main/java/com/mh/common/core/domain/dto/ReportRequestDTO.java new file mode 100644 index 0000000..0a1dd06 --- /dev/null +++ b/mh-common/src/main/java/com/mh/common/core/domain/dto/ReportRequestDTO.java @@ -0,0 +1,21 @@ +package com.mh.common.core.domain.dto; + +import lombok.Data; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 接收前端经过deepseek结构化后的请求 + * @date 2026-02-27 10:18:04 + */ +@Data +public class ReportRequestDTO { + + private String reportType; // 报表类型,如 "能效报表" + private String timeRange; // 时间范围,如 "昨天"、"2026-02-01至2026-02-07" + private String format; // 输出格式: word, excel, pdf + private Boolean watermark; // 是否需要水印 (默认true) + private String watermarkText; // 自定义水印文字,若为空则使用默认 + +} diff --git a/mh-framework/pom.xml b/mh-framework/pom.xml index 19073e1..626e8c7 100644 --- a/mh-framework/pom.xml +++ b/mh-framework/pom.xml @@ -63,6 +63,11 @@ org.springframework.boot spring-boot-starter-amqp + + org.projectlombok + lombok + provided + diff --git a/mh-framework/src/main/java/com/mh/framework/config/AsyncConfig.java b/mh-framework/src/main/java/com/mh/framework/config/AsyncConfig.java new file mode 100644 index 0000000..b39ca6f --- /dev/null +++ b/mh-framework/src/main/java/com/mh/framework/config/AsyncConfig.java @@ -0,0 +1,29 @@ +package com.mh.framework.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 自定义线程池 + * @date 2026-02-27 11:41:52 + */ +@Configuration +@EnableAsync +public class AsyncConfig { + // 可选:自定义线程池 + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("async-"); + executor.initialize(); + return executor; + } +} diff --git a/mh-quartz/pom.xml b/mh-quartz/pom.xml index 29176e5..0e851ef 100644 --- a/mh-quartz/pom.xml +++ b/mh-quartz/pom.xml @@ -39,6 +39,10 @@ com.mh mh-framework + + org.projectlombok + lombok + diff --git a/mh-system/pom.xml b/mh-system/pom.xml index 4618b45..677ef5a 100644 --- a/mh-system/pom.xml +++ b/mh-system/pom.xml @@ -22,6 +22,45 @@ com.mh mh-common + + org.springframework.ai + spring-ai-client-chat + + + org.springframework.retry + spring-retry + + + com.itextpdf + itextpdf + 5.5.13.5 + compile + + + org.projectlombok + lombok + provided + + + org.springframework + spring-webmvc + 6.2.10 + + + org.apache.poi + poi + ${poi.version} + + + org.apache.poi + poi-ooxml + ${poi.version} + + + org.apache.poi + poi-ooxml-schemas + ${poi.version} + diff --git a/mh-system/src/main/java/com/mh/system/service/ai/IEnergyReportService.java b/mh-system/src/main/java/com/mh/system/service/ai/IEnergyReportService.java new file mode 100644 index 0000000..b7dedc4 --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/IEnergyReportService.java @@ -0,0 +1,17 @@ +package com.mh.system.service.ai; + +import com.mh.common.core.domain.dto.EnergyReportDTO; + +import java.util.function.Consumer; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 流式推理版 + * @date 2026-02-27 10:36:03 + */ +public interface IEnergyReportService { + + public EnergyReportDTO generateReportWithReasoning(String rawData, Consumer reasoningConsumer); +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/IFileGenerationService.java b/mh-system/src/main/java/com/mh/system/service/ai/IFileGenerationService.java new file mode 100644 index 0000000..d6e6bb5 --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/IFileGenerationService.java @@ -0,0 +1,20 @@ +package com.mh.system.service.ai; + +import com.mh.common.core.domain.dto.EnergyReportDTO; +import com.mh.common.core.domain.dto.ReportRequestDTO; + +import java.io.IOException; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 报表生成服务类 + * @date 2026-02-27 10:50:42 + */ +public interface IFileGenerationService { + + byte[] generateReport(EnergyReportDTO report, ReportRequestDTO request) throws IOException; + String getFileExtension(); + +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/INLPService.java b/mh-system/src/main/java/com/mh/system/service/ai/INLPService.java new file mode 100644 index 0000000..569bc03 --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/INLPService.java @@ -0,0 +1,15 @@ +package com.mh.system.service.ai; + +import com.mh.common.core.domain.dto.ReportRequestDTO; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 自然语言解析服务类 + * @date 2026-02-27 10:25:40 + */ +public interface INLPService { + + public ReportRequestDTO parseUserIntent(String userMessage); +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/impl/AsyncReportServiceImpl.java b/mh-system/src/main/java/com/mh/system/service/ai/impl/AsyncReportServiceImpl.java new file mode 100644 index 0000000..4b03ca3 --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/impl/AsyncReportServiceImpl.java @@ -0,0 +1,114 @@ +package com.mh.system.service.ai.impl; + +import com.mh.common.config.FileStorageUtil; +import com.mh.common.core.domain.dto.AsyncTaskDTO; +import com.mh.common.core.domain.dto.EnergyReportDTO; +import com.mh.common.core.domain.dto.ReportRequestDTO; +import com.mh.common.utils.uuid.UUID; +import com.mh.system.service.ai.IEnergyReportService; +import com.mh.system.service.ai.INLPService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 异步任务服务 + * @date 2026-02-27 11:27:21 + */ +@Service +@Slf4j +public class AsyncReportServiceImpl { + + private final INLPService nlpService; + private final IEnergyReportService energyReportService; + private final ReportOrchestrationService orchestrationService; + private final TaskStoreService taskStore; + private final FileStorageUtil fileStorageUtil; + + private final Map emitters = new ConcurrentHashMap<>(); + + public AsyncReportServiceImpl(INLPService nlpService, + IEnergyReportService energyReportService, + ReportOrchestrationService orchestrationService, + TaskStoreService taskStore, + FileStorageUtil fileStorageUtil) { + this.nlpService = nlpService; + this.energyReportService = energyReportService; + this.orchestrationService = orchestrationService; + this.taskStore = taskStore; + this.fileStorageUtil = fileStorageUtil; + } + + public SseEmitter registerEmitter(String taskId) { + SseEmitter emitter = new SseEmitter(300_000L); + emitters.put(taskId, emitter); + emitter.onCompletion(() -> emitters.remove(taskId)); + emitter.onTimeout(() -> emitters.remove(taskId)); + return emitter; + } + + public String createAsyncTask(String userMessage) { + String taskId = UUID.randomUUID().toString(); + AsyncTaskDTO task = new AsyncTaskDTO(); + task.setTaskId(taskId); + task.setStatus("PROCESSING"); + task.setCreateTime(LocalDateTime.now()); + taskStore.save(task); + processTask(taskId, userMessage); + return taskId; + } + + @Async + public void processTask(String taskId, String userMessage) { + try { + // 解析自然语言 + ReportRequestDTO request = nlpService.parseUserIntent(userMessage); + // 获取原始数据(模拟) + String rawData = fetchRawData(request.getTimeRange()); + // 生成报表并实时推送推理过程 + EnergyReportDTO report = energyReportService.generateReportWithReasoning(rawData, + reasoningChunk -> sendEvent(taskId, "reasoning", reasoningChunk)); + // 生成文件 + ReportOrchestrationService.ReportFile reportFile = orchestrationService.generateFile(report, request); + // 保存文件并获取下载URL + String downloadUrl = fileStorageUtil.saveFile(reportFile.filename(), reportFile.content()); + // 更新任务状态 + taskStore.updateStatus(taskId, "COMPLETED", "报表生成成功", downloadUrl); + sendEvent(taskId, "COMPLETED", downloadUrl); + } catch (Exception e) { + log.error("任务处理失败", e); + taskStore.updateStatus(taskId, "FAILED", e.getMessage(), null); + sendEvent(taskId, "FAILED", e.getMessage()); + } + } + + private void sendEvent(String taskId, String event, Object data) { + SseEmitter emitter = emitters.get(taskId); + if (emitter != null) { + try { + emitter.send(SseEmitter.event().name(event).data(data)); + if ("COMPLETED".equals(event) || "FAILED".equals(event)) { + emitter.complete(); + } + } catch (IOException e) { + log.error("推送事件失败", e); + emitter.completeWithError(e); + } + } + } + + private String fetchRawData(String timeRange) { + // 模拟数据,实际应从数据库或IoT平台获取 + return "{\"date\":\"2026-02-26\",\"outdoorTemp\":32.5,\"coolingLoad\":850}"; + } + +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/impl/EnergyReportServiceImpl.java b/mh-system/src/main/java/com/mh/system/service/ai/impl/EnergyReportServiceImpl.java new file mode 100644 index 0000000..35e02aa --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/impl/EnergyReportServiceImpl.java @@ -0,0 +1,189 @@ +package com.mh.system.service.ai.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mh.common.core.domain.dto.EnergyReportDTO; +import com.mh.system.service.ai.IEnergyReportService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 流式推理版实现类 + * @date 2026-02-27 10:37:03 + */ +@Slf4j +@Service +public class EnergyReportServiceImpl implements IEnergyReportService { + + private final ChatClient chatClient; + private final ObjectMapper objectMapper; + + public EnergyReportServiceImpl(ChatClient.Builder builder, ObjectMapper objectMapper) { + this.chatClient = builder.build(); + this.objectMapper = objectMapper; + } + + public EnergyReportDTO generateReportWithReasoning(String rawData, Consumer reasoningConsumer) { + String prompt = """ + 你是一位专业的中央空调系统能效分析专家。 + 原始数据:{rawData} + 分析要求: + 1. 计算系统综合能效比 (EER = 总制冷量 / 总电耗) + 2. 识别能效异常的设备 (效率低于基准值80%视为异常) + 3. 评估各设备能耗占比和运行效率 + 4. 提供具体的优化建议 + + 请先逐步推理你的分析过程,然后输出最终的 JSON 报表。输出格式如下: + + 推理过程: + (分析步骤) + + 请根据数据生成一份能效分析报表,输出必须为严格的 JSON 格式,包含以下字段: + - reportDate: 报表生成日期,格式 yyyy-MM-dd + - overallScore: 整体能效评分 (0-100 的整数) + - systemEER: 系统综合能效比(数值,保留一位小数) + - totalEnergyConsumption: 总能耗,单位 kWh + - deviceDetails: 数组,每个元素包含: + deviceName: 设备名称 + deviceType: 设备类型(冷机/冷冻泵/冷却泵/冷却塔) + energyConsumption: 能耗 (kWh) + efficiency: 运行效率 (%) + deviationRate: 对比基准的偏差率 (%) + - anomalies: 数组,每个元素包含: + deviceName: 异常设备 + anomalyType: 异常类型 + description: 异常描述 + severity: 严重程度 (高/中/低) + estimatedLoss: 预估能耗损失 (kWh) + - suggestions: 数组,每个元素包含: + title: 建议标题 + description: 建议详细内容 + expectedSaving: 预期节能效果 (kWh) + priority: 优先级 (高/中/低) + action: 建议执行的操作 + 请先逐步推理你的分析过程,然后输出最终的 JSON 报表。 + 最终报表: + ```json + <严格按照 EnergyReportDTO JSON 格式输出> + ``` + 今天的日期:{currentDate} + """; + + Map params = Map.of( + "rawData", rawData, + "currentDate", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE) + ); + + StringBuilder fullResponse = new StringBuilder(); + +// Flux flux = chatClient.prompt() +// .system("你是一个JSON API,但请先详细推理再输出最终JSON。") +// .user(userSpec -> userSpec.text(prompt).params(params)) +// .stream() +// .content(); +// +// flux.doOnNext(chunk -> { +// fullResponse.append(chunk); +// if (!fullResponse.toString().contains("最终报表")) { +// if (reasoningConsumer != null) { +// reasoningConsumer.accept(chunk); +// } +// } +// }).blockLast(); + + log.info("AI原始格式输出:{}", fullResponse); +// String jsonStr = extractJson(fullResponse.toString()); + String jsonStr = "{\n" + + " \"reportDate\": \"2026-02-27\",\n" + + " \"overallScore\": 70,\n" + + " \"systemEER\": 4.0,\n" + + " \"totalEnergyConsumption\": 212.5,\n" + + " \"deviceDetails\": [\n" + + " {\n" + + " \"deviceName\": \"冷机-1\",\n" + + " \"deviceType\": \"冷机\",\n" + + " \"energyConsumption\": 138.1,\n" + + " \"efficiency\": 75.0,\n" + + " \"deviationRate\": -25.0\n" + + " },\n" + + " {\n" + + " \"deviceName\": \"冷冻泵-1\",\n" + + " \"deviceType\": \"冷冻泵\",\n" + + " \"energyConsumption\": 31.9,\n" + + " \"efficiency\": 95.0,\n" + + " \"deviationRate\": -5.0\n" + + " },\n" + + " {\n" + + " \"deviceName\": \"冷却泵-1\",\n" + + " \"deviceType\": \"冷却泵\",\n" + + " \"energyConsumption\": 21.3,\n" + + " \"efficiency\": 92.0,\n" + + " \"deviationRate\": -8.0\n" + + " },\n" + + " {\n" + + " \"deviceName\": \"冷却塔-1\",\n" + + " \"deviceType\": \"冷却塔\",\n" + + " \"energyConsumption\": 21.3,\n" + + " \"efficiency\": 88.0,\n" + + " \"deviationRate\": -12.0\n" + + " }\n" + + " ],\n" + + " \"anomalies\": [\n" + + " {\n" + + " \"deviceName\": \"冷机-1\",\n" + + " \"anomalyType\": \"能效低下\",\n" + + " \"description\": \"冷机运行效率为75%,低于基准值80%,可能由于换热器污垢或制冷剂不足导致能耗增加。\",\n" + + " \"severity\": \"高\",\n" + + " \"estimatedLoss\": 34.5\n" + + " }\n" + + " ],\n" + + " \"suggestions\": [\n" + + " {\n" + + " \"title\": \"清洗冷机换热器\",\n" + + " \"description\": \"检查并清洗冷凝器和蒸发器,清除污垢和沉积物,以提升热交换效率。\",\n" + + " \"expectedSaving\": 20.0,\n" + + " \"priority\": \"高\",\n" + + " \"action\": \"安排专业维护人员进行化学清洗和物理清理。\"\n" + + " },\n" + + " {\n" + + " \"title\": \"优化冷机运行参数\",\n" + + " \"description\": \"根据室外温度(32.5°C)调整冷机设定温度和运行模式,避免过度冷却。\",\n" + + " \"expectedSaving\": 10.0,\n" + + " \"priority\": \"中\",\n" + + " \"action\": \"调整冷机出水温度设定值至合理范围(如7°C以上),并启用节能模式。\"\n" + + " },\n" + + " {\n" + + " \"title\": \"检查冷冻泵与冷却泵\",\n" + + " \"description\": \"定期检查水泵轴承和密封状态,确保无异常磨损或泄漏,维持高效运行。\",\n" + + " \"expectedSaving\": 5.0,\n" + + " \"priority\": \"低\",\n" + + " \"action\": \"每月进行一次振动和噪音检测,每年更换一次润滑剂。\"\n" + + " }\n" + + " ]\n" + + "}"; + log.info("AI优化格式输出:{}", jsonStr); + try { + return objectMapper.readValue(jsonStr, EnergyReportDTO.class); + } catch (Exception e) { + throw new RuntimeException("解析AI输出失败", e); + } + } + + private String extractJson(String response) { + Pattern pattern = Pattern.compile("```json\\s*(\\{.*?\\})\\s*```", Pattern.DOTALL); + Matcher matcher = pattern.matcher(response); + return matcher.find() ? matcher.group(1) : response; + } + +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/impl/ExcelGenerationServiceImpl.java b/mh-system/src/main/java/com/mh/system/service/ai/impl/ExcelGenerationServiceImpl.java new file mode 100644 index 0000000..ca4d675 --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/impl/ExcelGenerationServiceImpl.java @@ -0,0 +1,204 @@ +package com.mh.system.service.ai.impl; + +import com.mh.common.core.domain.dto.EnergyReportDTO; +import com.mh.common.core.domain.dto.ReportRequestDTO; +import com.mh.system.service.ai.IFileGenerationService; +import org.apache.poi.hssf.usermodel.*; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.streaming.SXSSFSheet; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.*; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description excel添加水印 + * @date 2026-02-27 10:55:55 + */ +@Service +public class ExcelGenerationServiceImpl implements IFileGenerationService { + @Override + public byte[] generateReport(EnergyReportDTO report, ReportRequestDTO request) throws IOException { + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("能效报表"); + + // 设置水印(作为背景图片) + if (request.getWatermark()) { + addWatermark(workbook, sheet, request.getWatermarkText()); + } + + // 创建标题行 + Row titleRow = sheet.createRow(0); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellValue("中央空调能效报表"); + CellStyle titleStyle = workbook.createCellStyle(); + Font titleFont = workbook.createFont(); + titleFont.setBold(true); + titleFont.setFontHeightInPoints((short) 16); + titleStyle.setFont(titleFont); + titleCell.setCellStyle(titleStyle); + + // 基本信息 + int rowIdx = 2; + createCell(sheet, rowIdx++, 0, "报表日期: " + report.getReportDate()); + createCell(sheet, rowIdx++, 0, "整体能效评分: " + report.getOverallScore()); + createCell(sheet, rowIdx++, 0, "系统EER: " + report.getSystemEER()); + createCell(sheet, rowIdx++, 0, "总能耗(kWh): " + report.getTotalEnergyConsumption()); + + // 设备明细表头 + rowIdx++; + String[] headers = {"设备名称", "设备类型", "能耗(kWh)", "效率(%)", "偏差率(%)"}; + Row headerRow = sheet.createRow(rowIdx++); + for (int i = 0; i < headers.length; i++) { + headerRow.createCell(i).setCellValue(headers[i]); + } + + // 填充设备数据 + for (EnergyReportDTO.DeviceEnergy device : report.getDeviceDetails()) { + Row dataRow = sheet.createRow(rowIdx++); + dataRow.createCell(0).setCellValue(device.getDeviceName()); + dataRow.createCell(1).setCellValue(device.getDeviceType()); + dataRow.createCell(2).setCellValue(device.getEnergyConsumption()); + dataRow.createCell(3).setCellValue(device.getEfficiency()); + dataRow.createCell(4).setCellValue(device.getDeviationRate()); + } + + // 自动调整列宽 + for (int i = 0; i < headers.length; i++) { + sheet.autoSizeColumn(i); + } + + // 异常情况表格 + rowIdx += 2; + addSectionTitle(sheet, rowIdx++, "异常情况分析", workbook); + rowIdx++; + + if (report.getAnomalies() != null && !report.getAnomalies().isEmpty()) { + String[] anomalyHeaders = {"设备名称", "异常类型", "异常描述", "严重程度", "预估损失(kWh)"}; + Row anomalyHeaderRow = sheet.createRow(rowIdx++); + for (int i = 0; i < anomalyHeaders.length; i++) { + anomalyHeaderRow.createCell(i).setCellValue(anomalyHeaders[i]); + } + + for (EnergyReportDTO.Anomaly anomaly : report.getAnomalies()) { + Row dataRow = sheet.createRow(rowIdx++); + dataRow.createCell(0).setCellValue(anomaly.getDeviceName()); + dataRow.createCell(1).setCellValue(anomaly.getAnomalyType()); + dataRow.createCell(2).setCellValue(anomaly.getDescription()); + dataRow.createCell(3).setCellValue(anomaly.getSeverity()); + dataRow.createCell(4).setCellValue(anomaly.getEstimatedLoss() != null ? + anomaly.getEstimatedLoss() : 0.0); + } + + // 调整异常表格列宽 + for (int i = 0; i < anomalyHeaders.length; i++) { + sheet.autoSizeColumn(i); + } + } else { + createCell(sheet, rowIdx++, 0, "无异常情况"); + } + + // 优化建议表格 + rowIdx += 2; + addSectionTitle(sheet, rowIdx++, "优化建议", workbook); + rowIdx++; + + if (report.getSuggestions() != null && !report.getSuggestions().isEmpty()) { + String[] suggestionHeaders = {"优先级", "建议标题", "详细内容", "预期节能(kWh)", "建议操作"}; + Row suggestionHeaderRow = sheet.createRow(rowIdx++); + for (int i = 0; i < suggestionHeaders.length; i++) { + suggestionHeaderRow.createCell(i).setCellValue(suggestionHeaders[i]); + } + + for (EnergyReportDTO.Suggestion suggestion : report.getSuggestions()) { + Row dataRow = sheet.createRow(rowIdx++); + dataRow.createCell(0).setCellValue(suggestion.getPriority()); + dataRow.createCell(1).setCellValue(suggestion.getTitle()); + dataRow.createCell(2).setCellValue(suggestion.getDescription()); + dataRow.createCell(3).setCellValue(suggestion.getExpectedSaving() != null ? + suggestion.getExpectedSaving() : 0.0); + dataRow.createCell(4).setCellValue(suggestion.getAction()); + } + + // 调整建议表格列宽 + for (int i = 0; i < suggestionHeaders.length; i++) { + sheet.autoSizeColumn(i); + } + } else { + createCell(sheet, rowIdx++, 0, "暂无优化建议"); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + workbook.write(baos); + return baos.toByteArray(); + } + } + + private void createCell(Sheet sheet, int row, int col, String value) { + Row r = sheet.getRow(row); + if (r == null) r = sheet.createRow(row); + r.createCell(col).setCellValue(value); + } + + private void addSectionTitle(Sheet sheet, int row, String title, Workbook workbook) { + Row titleRow = sheet.createRow(row); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellValue(title); + + CellStyle titleStyle = workbook.createCellStyle(); + Font titleFont = workbook.createFont(); + titleFont.setBold(true); + titleFont.setFontHeightInPoints((short) 12); + titleStyle.setFont(titleFont); + titleStyle.setAlignment(HorizontalAlignment.LEFT); + titleCell.setCellStyle(titleStyle); + } + + /** + * Excel水印(使用背景图片方式) + * 注意:XSSF支持设置背景图片,但水印文字需预先绘制成图片 + * 简化:此处仅演示思路,实际需要将文字转为BufferedImage再设为背景 + */ + private void addWatermark(Workbook workbook, Sheet sheet, String watermarkText) { + try { + // 最简单的水印实现:在工作表顶部添加灰色文本 + Row watermarkRow = sheet.createRow(0); + Cell watermarkCell = watermarkRow.createCell(0); + watermarkCell.setCellValue(watermarkText); + + // 创建水印样式 + CellStyle watermarkStyle = workbook.createCellStyle(); + Font watermarkFont = workbook.createFont(); + watermarkFont.setColor(IndexedColors.GREY_25_PERCENT.getIndex()); + watermarkFont.setFontHeightInPoints((short) 14); + watermarkFont.setItalic(true); // 斜体效果 + watermarkStyle.setFont(watermarkFont); + watermarkStyle.setAlignment(HorizontalAlignment.CENTER); + + // 设置背景色为浅灰色(模拟水印效果) + watermarkStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); + watermarkStyle.setFillPattern(FillPatternType.NO_FILL); + + watermarkCell.setCellStyle(watermarkStyle); + + // 合并单元格让水印覆盖更多区域 + sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 5)); + + } catch (Exception e) { + // 水印添加失败时不中断主流程 + System.out.println("水印添加警告: " + e.getMessage()); + } + } + + + @Override + public String getFileExtension() { + return ".xlsx"; + } +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/impl/NLPServiceImpl.java b/mh-system/src/main/java/com/mh/system/service/ai/impl/NLPServiceImpl.java new file mode 100644 index 0000000..f1f3f69 --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/impl/NLPServiceImpl.java @@ -0,0 +1,52 @@ +package com.mh.system.service.ai.impl; + +import com.mh.common.core.domain.dto.ReportRequestDTO; +import com.mh.system.service.ai.INLPService; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.converter.BeanOutputConverter; +import org.springframework.stereotype.Service; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 自然语言解析服务实现类 + * @date 2026-02-27 10:29:30 + */ +@Service +public class NLPServiceImpl implements INLPService { + + private final ChatClient chatClient; + + public NLPServiceImpl(ChatClient.Builder builder) { + this.chatClient = builder.build(); + } + + @Override + public ReportRequestDTO parseUserIntent(String userMessage) { + BeanOutputConverter converter = new BeanOutputConverter<>(ReportRequestDTO.class); + String systemPrompt = """ + 你是一个智能助手,负责解析用户关于生成报表的请求。 + 请从用户消息中提取以下信息: + - reportType: 报表类型 (如 "能效报表"、"故障报表") + - timeRange: 时间范围 (如 "昨天"、"2026-02-01至2026-02-07") + - format: 输出格式 (word/excel/pdf) + - watermark: 是否需要水印 (布尔值) + - watermarkText: 水印文字 + + 默认值:format="pdf", watermark=true, watermarkText="内部资料" + 请以严格的JSON格式输出。 + """ + converter.getFormat(); + + ReportRequestDTO request = chatClient.prompt() + .system(systemPrompt) + .user(userMessage) + .call() + .entity(converter); + + if (request.getFormat() == null) request.setFormat("pdf"); + if (request.getWatermark() == null) request.setWatermark(true); + if (request.getWatermarkText() == null) request.setWatermarkText("内部资料"); + return request; + } +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/impl/PdfGenerationServiceImpl.java b/mh-system/src/main/java/com/mh/system/service/ai/impl/PdfGenerationServiceImpl.java new file mode 100644 index 0000000..ccc9de7 --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/impl/PdfGenerationServiceImpl.java @@ -0,0 +1,287 @@ +package com.mh.system.service.ai.impl; + +import com.itextpdf.text.*; +import com.itextpdf.text.pdf.*; +import com.mh.common.core.domain.dto.EnergyReportDTO; +import com.mh.common.core.domain.dto.ReportRequestDTO; +import com.mh.system.service.ai.IFileGenerationService; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description PDF添加水印 + * @date 2026-02-27 11:06:19 + */ +@Service +public class PdfGenerationServiceImpl implements IFileGenerationService { + + @Override + public byte[] generateReport(EnergyReportDTO report, ReportRequestDTO request) throws IOException { + Document document = new Document(PageSize.A4); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PdfWriter writer = null; + try { + writer = PdfWriter.getInstance(document, baos); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + + // 设置水印事件处理器 + if (request.getWatermark()) { + writer.setPageEvent(new WatermarkPageEvent(request.getWatermarkText())); + } + + document.open(); + + // 添加标题 + Font titleFont = new Font(Font.FontFamily.HELVETICA, 20, Font.BOLD); + Paragraph title = new Paragraph("中央空调能效报表", titleFont); + title.setAlignment(Element.ALIGN_CENTER); + title.setSpacingAfter(20); + try { + document.add(title); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + + // 添加基本信息 + Font normalFont = new Font(Font.FontFamily.HELVETICA, 12); + try { + document.add(new Paragraph("报表日期: " + report.getReportDate(), normalFont)); + document.add(new Paragraph("整体能效评分: " + report.getOverallScore(), normalFont)); + document.add(new Paragraph("系统EER: " + report.getSystemEER(), normalFont)); + document.add(new Paragraph("总能耗(kWh): " + report.getTotalEnergyConsumption(), normalFont)); + document.add(new Paragraph(" ")); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + + // 创建表格 + PdfPTable table = new PdfPTable(5); + table.setWidthPercentage(100); + table.setSpacingBefore(10f); + table.setSpacingAfter(10f); + + // 设置列宽 + float[] columnWidths = {2f, 2f, 2f, 2f, 2f}; + try { + table.setWidths(columnWidths); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + + // 添加表头 + Font headerFont = new Font(Font.FontFamily.HELVETICA, 12, Font.BOLD); + addTableHeader(table, "设备名称", headerFont); + addTableHeader(table, "设备类型", headerFont); + addTableHeader(table, "能耗(kWh)", headerFont); + addTableHeader(table, "效率(%)", headerFont); + addTableHeader(table, "偏差率(%)", headerFont); + + // 添加数据行 + Font dataFont = new Font(Font.FontFamily.HELVETICA, 10); + for (EnergyReportDTO.DeviceEnergy device : report.getDeviceDetails()) { + addTableCell(table, device.getDeviceName(), dataFont); + addTableCell(table, device.getDeviceType(), dataFont); + addTableCell(table, String.valueOf(device.getEnergyConsumption()), dataFont); + addTableCell(table, String.valueOf(device.getEfficiency()), dataFont); + addTableCell(table, String.valueOf(device.getDeviationRate()), dataFont); + } + + try { + document.add(table); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + + // 异常情况分析 + try { + addSectionTitle(document, "异常情况分析", headerFont); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + if (report.getAnomalies() != null && !report.getAnomalies().isEmpty()) { + PdfPTable anomalyTable = new PdfPTable(5); + anomalyTable.setWidthPercentage(100); + anomalyTable.setSpacingBefore(10f); + anomalyTable.setSpacingAfter(10f); + + float[] anomalyColumnWidths = {2f, 2f, 3f, 1.5f, 2f}; + try { + anomalyTable.setWidths(anomalyColumnWidths); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + + addTableHeader(anomalyTable, "设备名称", headerFont); + addTableHeader(anomalyTable, "异常类型", headerFont); + addTableHeader(anomalyTable, "异常描述", headerFont); + addTableHeader(anomalyTable, "严重程度", headerFont); + addTableHeader(anomalyTable, "预估损失(kWh)", headerFont); + + for (EnergyReportDTO.Anomaly anomaly : report.getAnomalies()) { + addTableCell(anomalyTable, anomaly.getDeviceName(), dataFont); + addTableCell(anomalyTable, anomaly.getAnomalyType(), dataFont); + addTableCell(anomalyTable, anomaly.getDescription(), dataFont); + addTableCell(anomalyTable, anomaly.getSeverity(), dataFont); + addTableCell(anomalyTable, anomaly.getEstimatedLoss() != null ? + String.format("%.2f", anomaly.getEstimatedLoss()) : "-", dataFont); + } + + try { + document.add(anomalyTable); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + } else { + try { + document.add(new Paragraph("无异常情况", normalFont)); + document.add(new Paragraph(" ")); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + } + + // 优化建议 + try { + addSectionTitle(document, "优化建议", headerFont); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + if (report.getSuggestions() != null && !report.getSuggestions().isEmpty()) { + PdfPTable suggestionTable = new PdfPTable(5); + suggestionTable.setWidthPercentage(100); + suggestionTable.setSpacingBefore(10f); + suggestionTable.setSpacingAfter(10f); + + float[] suggestionColumnWidths = {1.5f, 2f, 3f, 2f, 2f}; + try { + suggestionTable.setWidths(suggestionColumnWidths); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + + addTableHeader(suggestionTable, "优先级", headerFont); + addTableHeader(suggestionTable, "建议标题", headerFont); + addTableHeader(suggestionTable, "详细内容", headerFont); + addTableHeader(suggestionTable, "预期节能(kWh)", headerFont); + addTableHeader(suggestionTable, "建议操作", headerFont); + + for (EnergyReportDTO.Suggestion suggestion : report.getSuggestions()) { + addTableCell(suggestionTable, suggestion.getPriority(), dataFont); + addTableCell(suggestionTable, suggestion.getTitle(), dataFont); + addTableCell(suggestionTable, suggestion.getDescription(), dataFont); + addTableCell(suggestionTable, suggestion.getExpectedSaving() != null ? + String.format("%.2f", suggestion.getExpectedSaving()) : "-", dataFont); + addTableCell(suggestionTable, suggestion.getAction(), dataFont); + } + + try { + document.add(suggestionTable); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + } else { + try { + document.add(new Paragraph("暂无优化建议", normalFont)); + } catch (DocumentException e) { + throw new RuntimeException(e); + } + } + + document.close(); + + return baos.toByteArray(); + } + + private void addTableHeader(PdfPTable table, String headerText, Font font) { + PdfPCell header = new PdfPCell(new Phrase(headerText, font)); + header.setHorizontalAlignment(Element.ALIGN_CENTER); + header.setVerticalAlignment(Element.ALIGN_MIDDLE); + header.setBackgroundColor(BaseColor.LIGHT_GRAY); + table.addCell(header); + } + + private void addTableCell(PdfPTable table, String text, Font font) { + PdfPCell cell = new PdfPCell(new Phrase(text, font)); + cell.setHorizontalAlignment(Element.ALIGN_CENTER); + cell.setVerticalAlignment(Element.ALIGN_MIDDLE); + table.addCell(cell); + } + + private void addSectionTitle(Document document, String title, Font font) throws DocumentException { + Paragraph sectionTitle = new Paragraph(title, font); + sectionTitle.setAlignment(Element.ALIGN_LEFT); + sectionTitle.setSpacingBefore(20f); + sectionTitle.setSpacingAfter(10f); + document.add(sectionTitle); + } + + @Override + public String getFileExtension() { + return ".pdf"; + } + + /** + * PDF水印页面事件处理器 + */ + private static class WatermarkPageEvent extends PdfPageEventHelper { + private final String watermarkText; + private final Font watermarkFont; + + public WatermarkPageEvent(String watermarkText) { + this.watermarkText = watermarkText; + // 创建水印字体 + this.watermarkFont = new Font(Font.FontFamily.HELVETICA, 50, Font.NORMAL, BaseColor.LIGHT_GRAY); + } + + @Override + public void onEndPage(PdfWriter writer, Document document) { + try { + PdfContentByte canvas = writer.getDirectContentUnder(); + Rectangle pageSize = document.getPageSize(); + + // 保存当前图形状态 + canvas.saveState(); + + // 设置透明度 + PdfGState gState = new PdfGState(); + gState.setFillOpacity(0.2f); // 20%不透明度 + canvas.setGState(gState); + + // 设置字体和颜色 + canvas.beginText(); + canvas.setFontAndSize(watermarkFont.getBaseFont(), 50); + canvas.setColorFill(BaseColor.LIGHT_GRAY); + + // 计算水印位置(页面中心) + float x = (pageSize.getLeft() + pageSize.getRight()) / 2; + float y = (pageSize.getTop() + pageSize.getBottom()) / 2; + + // 设置文字矩阵并旋转45度 + canvas.setTextMatrix( + (float) Math.cos(Math.toRadians(45)), + (float) Math.sin(Math.toRadians(45)), + (float) -Math.sin(Math.toRadians(45)), + (float) Math.cos(Math.toRadians(45)), + x, y + ); + + // 显示水印文字 + canvas.showTextAligned(Element.ALIGN_CENTER, watermarkText, 0, 0, 0); + canvas.endText(); + + // 恢复图形状态 + canvas.restoreState(); + + } catch (Exception e) { + System.err.println("添加PDF水印失败: " + e.getMessage()); + } + } + } +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/impl/ReportOrchestrationService.java b/mh-system/src/main/java/com/mh/system/service/ai/impl/ReportOrchestrationService.java new file mode 100644 index 0000000..b56f3ae --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/impl/ReportOrchestrationService.java @@ -0,0 +1,43 @@ +package com.mh.system.service.ai.impl; + +import com.mh.common.core.domain.dto.EnergyReportDTO; +import com.mh.common.core.domain.dto.ReportRequestDTO; +import com.mh.system.service.ai.IFileGenerationService; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 文件生成服务 + * @date 2026-02-27 11:24:45 + */ +@Service +public class ReportOrchestrationService { + private final WordGenerationServiceImpl wordService; + private final ExcelGenerationServiceImpl excelService; + private final PdfGenerationServiceImpl pdfService; + + public ReportOrchestrationService(WordGenerationServiceImpl wordService, + ExcelGenerationServiceImpl excelService, + PdfGenerationServiceImpl pdfService) { + this.wordService = wordService; + this.excelService = excelService; + this.pdfService = pdfService; + } + + public ReportFile generateFile(EnergyReportDTO report, ReportRequestDTO request) throws IOException { + IFileGenerationService generator = switch (request.getFormat().toLowerCase()) { + case "word" -> wordService; + case "excel" -> excelService; + default -> pdfService; + }; + byte[] content = generator.generateReport(report, request); + String filename = "能效报表_" + report.getReportDate() + generator.getFileExtension(); + return new ReportFile(filename, content); + } + + public record ReportFile(String filename, byte[] content) {} +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/impl/TaskStoreService.java b/mh-system/src/main/java/com/mh/system/service/ai/impl/TaskStoreService.java new file mode 100644 index 0000000..af0aff0 --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/impl/TaskStoreService.java @@ -0,0 +1,34 @@ +package com.mh.system.service.ai.impl; + +import com.mh.common.core.domain.dto.AsyncTaskDTO; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 任务存储服务类 + * @date 2026-02-27 11:30:06 + */ +@Component +public class TaskStoreService { + + private final Map tasks = new ConcurrentHashMap<>(); + + public void save(AsyncTaskDTO task) { tasks.put(task.getTaskId(), task); } + public AsyncTaskDTO get(String taskId) { return tasks.get(taskId); } + public void updateStatus(String taskId, String status, String message, String downloadUrl) { + AsyncTaskDTO task = tasks.get(taskId); + if (task != null) { + task.setStatus(status); + task.setMessage(message); + task.setDownloadUrl(downloadUrl); + task.setCompleteTime(LocalDateTime.now()); + } + } + +} diff --git a/mh-system/src/main/java/com/mh/system/service/ai/impl/WordGenerationServiceImpl.java b/mh-system/src/main/java/com/mh/system/service/ai/impl/WordGenerationServiceImpl.java new file mode 100644 index 0000000..69c2a13 --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/impl/WordGenerationServiceImpl.java @@ -0,0 +1,328 @@ +package com.mh.system.service.ai.impl; + +import org.apache.poi.xwpf.usermodel.*; +import com.mh.common.core.domain.dto.EnergyReportDTO; +import com.mh.common.core.domain.dto.ReportRequestDTO; +import com.mh.system.service.ai.IFileGenerationService; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description Word文档生成服务 + * @date 2026-02-27 10:51:51 + */ +@Service +public class WordGenerationServiceImpl implements IFileGenerationService { + + @Override + public byte[] generateReport(EnergyReportDTO report, ReportRequestDTO request) throws IOException { + try (XWPFDocument document = new XWPFDocument()) { + // 添加水印 + if (request.getWatermark()) { + addWatermark(document, request.getWatermarkText()); + } + + // 创建标题 + addTitle(document, "中央空调能效报表"); + + // 添加基本信息 + addBasicInfo(document, report); + + // 设备明细表格 + addDeviceDetailsTable(document, report.getDeviceDetails().toArray(new EnergyReportDTO.DeviceEnergy[0])); + + // 异常情况分析 + addAnomaliesSection(document, report.getAnomalies().toArray(new EnergyReportDTO.Anomaly[0])); + + // 优化建议 + addSuggestionsSection(document, report.getSuggestions().toArray(new EnergyReportDTO.Suggestion[0])); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.write(baos); + return baos.toByteArray(); + } + } + + private void addTitle(XWPFDocument document, String title) { + XWPFParagraph para = document.createParagraph(); + para.setAlignment(ParagraphAlignment.CENTER); + XWPFRun run = para.createRun(); + run.setText(title); + // 使用反射安全设置样式 + setRunBold(run, true); + setRunFontSize(run, 20); + } + + private void addBasicInfo(XWPFDocument document, EnergyReportDTO report) { + XWPFParagraph para = document.createParagraph(); + XWPFRun run = para.createRun(); + run.setText("报表日期: " + report.getReportDate()); + setRunFontSize(run, 12); + + para = document.createParagraph(); + run = para.createRun(); + run.setText("整体能效评分: " + report.getOverallScore()); + setRunFontSize(run, 12); + + para = document.createParagraph(); + run = para.createRun(); + run.setText("系统综合能效比(EER): " + report.getSystemEER()); + setRunFontSize(run, 12); + + para = document.createParagraph(); + run = para.createRun(); + run.setText("总能耗: " + formatDouble(report.getTotalEnergyConsumption()) + " kWh"); + setRunFontSize(run, 12); + + para = document.createParagraph(); + run = para.createRun(); + run.setText(" "); + setRunFontSize(run, 8); + } + + private void addDeviceDetailsTable(XWPFDocument document, EnergyReportDTO.DeviceEnergy[] devices) { + if (devices == null || devices.length == 0) { + return; + } + + // 使用纯文本方式替代表格,完全避免POI表格API + addSectionTitle(document, "设备明细"); + + // 添加表头 + XWPFParagraph headerPara = document.createParagraph(); + XWPFRun headerRun = headerPara.createRun(); + headerRun.setText("设备名称\t设备类型\t能耗\t效率(%)\t偏差率(%)"); + setRunBold(headerRun, true); + + // 添加数据行 + for (EnergyReportDTO.DeviceEnergy device : devices) { + XWPFParagraph dataPara = document.createParagraph(); + XWPFRun dataRun = dataPara.createRun(); + String rowData = String.format("%s\t%s\t%s\t%s\t%s", + device.getDeviceName(), + device.getDeviceType(), + formatDouble(device.getEnergyConsumption()), + formatDouble(device.getEfficiency()), + formatDouble(device.getDeviationRate())); + dataRun.setText(rowData); + } + + document.createParagraph(); + } + + private void addAnomaliesSection(XWPFDocument document, EnergyReportDTO.Anomaly[] anomalies) { + addSectionTitle(document, "异常情况分析"); + + if (anomalies != null && anomalies.length > 0) { + // 使用列表方式替代表格 + XWPFParagraph headerPara = document.createParagraph(); + XWPFRun headerRun = headerPara.createRun(); + headerRun.setText("设备名称 | 异常类型 | 异常描述 | 严重程度 | 预估损失"); + setRunBold(headerRun, true); + + for (EnergyReportDTO.Anomaly anomaly : anomalies) { + XWPFParagraph itemPara = document.createParagraph(); + XWPFRun itemRun = itemPara.createRun(); + String itemText = String.format("%s | %s | %s | %s | %s", + anomaly.getDeviceName(), + anomaly.getAnomalyType(), + anomaly.getDescription(), + anomaly.getSeverity(), + formatDouble(anomaly.getEstimatedLoss())); + itemRun.setText(itemText); + } + } else { + XWPFParagraph para = document.createParagraph(); + XWPFRun run = para.createRun(); + run.setText("无异常情况"); + setRunFontSize(run, 12); + } + + document.createParagraph(); + } + + private void addSuggestionsSection(XWPFDocument document, EnergyReportDTO.Suggestion[] suggestions) { + addSectionTitle(document, "优化建议"); + + if (suggestions != null && suggestions.length > 0) { + // 使用编号列表方式展示建议 + for (int i = 0; i < suggestions.length; i++) { + EnergyReportDTO.Suggestion suggestion = suggestions[i]; + + // 添加建议标题 + XWPFParagraph titlePara = document.createParagraph(); + XWPFRun titleRun = titlePara.createRun(); + titleRun.setText((i + 1) + ". " + suggestion.getTitle()); + setRunBold(titleRun, true); + + // 添加详细信息 + XWPFParagraph detailPara = document.createParagraph(); + XWPFRun detailRun = detailPara.createRun(); + String detailText = String.format("优先级: %s | 预期节能: %s | 建议操作: %s\n详细内容: %s", + suggestion.getPriority(), + formatDouble(suggestion.getExpectedSaving()), + suggestion.getAction(), + suggestion.getDescription()); + detailRun.setText(detailText); + setRunFontSize(detailRun, 11); + } + } else { + XWPFParagraph para = document.createParagraph(); + XWPFRun run = para.createRun(); + run.setText("暂无优化建议"); + setRunFontSize(run, 12); + } + + document.createParagraph(); + } + + private void addSectionTitle(XWPFDocument document, String title) { + XWPFParagraph para = document.createParagraph(); + setParagraphSpacing(para, 200, 100); + XWPFRun run = para.createRun(); + run.setText(title); + setRunBold(run, true); + setRunFontSize(run, 14); + } + + /** + * 安全设置段落间距 + */ + private void setParagraphSpacing(XWPFParagraph para, int before, int after) { + try { + para.setSpacingBefore(before); + para.setSpacingAfter(after); + } catch (NoSuchMethodError | NoClassDefFoundError e) { + System.err.println("设置段落间距失败,跳过: " + e.getMessage()); + } catch (Exception e) { + // 静默处理 + } + } + + + private void addWatermark(XWPFDocument document, String watermarkText) { + try { + // 使用完全兼容POI 4.1.2的水印实现 + addCompatibleWatermark(document, watermarkText); + } catch (Exception e) { + // 水印添加失败时静默处理,不影响文档生成 + System.err.println("添加水印失败: " + e.getMessage()); + } + } + + /** + * 完全兼容POI 4.1.2的水印实现 + */ + private void addCompatibleWatermark(XWPFDocument doc, String watermarkText) { + try { + // 在文档开头添加水印文本作为替代方案 + XWPFParagraph watermarkPara = doc.createParagraph(); + watermarkPara.setAlignment(ParagraphAlignment.CENTER); + + XWPFRun watermarkRun = watermarkPara.createRun(); + watermarkRun.setText(watermarkText); + + // 使用兼容的方法设置样式 + setRunFontSize(watermarkRun, 24); + setRunColor(watermarkRun, "CCCCCC"); + + // 添加间距 + setParagraphSpacing(watermarkPara, 100, 100); + + } catch (Exception e) { + System.err.println("兼容水印添加失败: " + e.getMessage()); + } + } + + /** + * 安全设置字体大小(使用 try-catch 避免版本兼容性问题) + */ + private void setRunFontSize(XWPFRun run, int size) { + try { + run.setFontSize(size); + } catch (NoSuchMethodError | NoClassDefFoundError e) { + System.err.println("设置字体大小失败,跳过: " + e.getMessage()); + } catch (Exception e) { + // 静默处理 + } + } + + /** + * 安全设置粗体 + */ + private void setRunBold(XWPFRun run, boolean bold) { + try { + run.setBold(bold); + } catch (NoSuchMethodError | NoClassDefFoundError e) { + System.err.println("设置粗体失败,跳过: " + e.getMessage()); + } catch (Exception e) { + // 静默处理 + } + } + + /** + * 安全设置斜体 + */ + private void setRunItalic(XWPFRun run, boolean italic) { + try { + run.setItalic(italic); + } catch (NoSuchMethodError | NoClassDefFoundError e) { + System.err.println("设置斜体失败,跳过: " + e.getMessage()); + } catch (Exception e) { + // 静默处理 + } + } + + /** + * 安全设置字体 + */ + private void setRunFontFamily(XWPFRun run, String fontFamily) { + try { + run.setFontFamily(fontFamily); + } catch (NoSuchMethodError | NoClassDefFoundError e) { + System.err.println("设置字体失败,跳过: " + e.getMessage()); + } catch (Exception e) { + // 静默处理 + } + } + + /** + * 安全设置颜色 + */ + private void setRunColor(XWPFRun run, String color) { + try { + run.setColor(color); + } catch (NoSuchMethodError | NoClassDefFoundError e) { + System.err.println("设置颜色失败,跳过: " + e.getMessage()); + } catch (Exception e) { + // 静默处理 + } + } + + + /** + * 安全的段落创建方法 + */ + private XWPFParagraph createSafeParagraph(XWPFDocument document) { + try { + return document.createParagraph(); + } catch (Exception e) { + System.err.println("创建段落失败: " + e.getMessage()); + return null; + } + } + + private String formatDouble(Double value) { + return value != null ? String.format("%.2f", value) : "-"; + } + + @Override + public String getFileExtension() { + return ".docx"; + } +} diff --git a/pom.xml b/pom.xml index 30a5645..faab1d7 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,8 @@ 6.2.1 3.2.1 3.4.2 + 1.1.2 + 2.0.10 @@ -159,11 +161,26 @@ + + org.apache.poi + poi + ${poi.version} + org.apache.poi poi-ooxml ${poi.version} + + org.apache.poi + poi-ooxml-schemas + ${poi.version} + + + org.apache.xmlbeans + xmlbeans + 3.1.0 + @@ -282,6 +299,20 @@ ${amqp.version} + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + org.springframework.retry + spring-retry + ${spring-retry.version} +