Browse Source

1、添加ai智能报表

dev_bl
25604 2 weeks ago
parent
commit
24eb9a0506
  1. 22
      mh-admin/pom.xml
  2. 47
      mh-admin/src/main/java/com/mh/web/controller/ai/AsyncReportController.java
  3. 39
      mh-admin/src/main/java/com/mh/web/controller/ai/FileDownloadController.java
  4. 9
      mh-admin/src/main/resources/application-dev.yml
  5. 2
      mh-admin/src/main/resources/application.yml
  6. 80
      mh-admin/src/test/java/com/mh/MHApplicationTest.java
  7. 46
      mh-common/pom.xml
  8. 35
      mh-common/src/main/java/com/mh/common/config/FileStorageUtil.java
  9. 24
      mh-common/src/main/java/com/mh/common/core/domain/dto/AsyncTaskDTO.java
  10. 99
      mh-common/src/main/java/com/mh/common/core/domain/dto/EnergyReportDTO.java
  11. 21
      mh-common/src/main/java/com/mh/common/core/domain/dto/ReportRequestDTO.java
  12. 5
      mh-framework/pom.xml
  13. 29
      mh-framework/src/main/java/com/mh/framework/config/AsyncConfig.java
  14. 4
      mh-quartz/pom.xml
  15. 39
      mh-system/pom.xml
  16. 17
      mh-system/src/main/java/com/mh/system/service/ai/IEnergyReportService.java
  17. 20
      mh-system/src/main/java/com/mh/system/service/ai/IFileGenerationService.java
  18. 15
      mh-system/src/main/java/com/mh/system/service/ai/INLPService.java
  19. 114
      mh-system/src/main/java/com/mh/system/service/ai/impl/AsyncReportServiceImpl.java
  20. 189
      mh-system/src/main/java/com/mh/system/service/ai/impl/EnergyReportServiceImpl.java
  21. 204
      mh-system/src/main/java/com/mh/system/service/ai/impl/ExcelGenerationServiceImpl.java
  22. 52
      mh-system/src/main/java/com/mh/system/service/ai/impl/NLPServiceImpl.java
  23. 287
      mh-system/src/main/java/com/mh/system/service/ai/impl/PdfGenerationServiceImpl.java
  24. 43
      mh-system/src/main/java/com/mh/system/service/ai/impl/ReportOrchestrationService.java
  25. 34
      mh-system/src/main/java/com/mh/system/service/ai/impl/TaskStoreService.java
  26. 328
      mh-system/src/main/java/com/mh/system/service/ai/impl/WordGenerationServiceImpl.java
  27. 31
      pom.xml

22
mh-admin/pom.xml

@ -75,6 +75,28 @@
<artifactId>mh-generator</artifactId> <artifactId>mh-generator</artifactId>
</dependency> </dependency>
<!-- spring-retry -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!-- Spring AI OpenAI (排除高版本依赖) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jackson</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-jackson</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies> </dependencies>
<build> <build>

47
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<Map<String, String>> 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<AsyncTaskDTO> getTask(@PathVariable String taskId) {
AsyncTaskDTO task = taskStore.get(taskId);
return task != null ? ResponseEntity.ok(task) : ResponseEntity.notFound().build();
}
}

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

9
mh-admin/src/main/resources/application-dev.yml

@ -39,6 +39,15 @@ logging:
# Spring配置 # Spring配置
spring: spring:
# AI配置
ai:
openai:
base-url: https://api.deepseek.com
api-key: sk-18202beb1c8e4208a6f52b335fef5edf
chat:
options:
model: deepseek-chat
temperature: 0.1
# 资源信息 # 资源信息
messages: messages:
# 国际化资源文件路径 # 国际化资源文件路径

2
mh-admin/src/main/resources/application.yml

@ -1,6 +1,6 @@
spring: spring:
profiles: profiles:
active: prod active: dev
# 用户配置 # 用户配置
user: user:

80
mh-admin/src/test/java/com/mh/MHApplicationTest.java

@ -2,6 +2,9 @@ package com.mh;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
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.DeviceReport;
import com.mh.common.core.domain.entity.SysParams; import com.mh.common.core.domain.entity.SysParams;
import com.mh.common.core.domain.entity.SysUser; import com.mh.common.core.domain.entity.SysUser;
@ -116,4 +119,81 @@ public class MHApplicationTest {
System.out.println(data); System.out.println(data);
dataProcessMapper.insertTable(data, "data_hour2025"); 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);
}
} }

46
mh-common/pom.xml

@ -72,9 +72,20 @@
</dependency> </dependency>
<!-- excel工具 --> <!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.poi</groupId> <groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId> <artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>${poi.version}</version>
</dependency> </dependency>
<!-- yml解析器 --> <!-- yml解析器 -->
@ -155,16 +166,43 @@
<artifactId>easyexcel</artifactId> <artifactId>easyexcel</artifactId>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.github.binarywang/weixin-java-mp -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.7.6.B</version>
</dependency>
<!-- Spring AI OpenAI Starter (兼容 DeepSeek) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-retry</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Lombok -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.github.binarywang/weixin-java-mp --> <!-- iText 7 for PDF -->
<dependency> <dependency>
<groupId>com.github.binarywang</groupId> <groupId>com.itextpdf</groupId>
<artifactId>weixin-java-mp</artifactId> <artifactId>itextpdf</artifactId>
<version>4.7.6.B</version> <version>5.5.13.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.2.10</version>
</dependency> </dependency>
</dependencies> </dependencies>

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

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

99
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<DeviceEnergy> deviceDetails;
@JsonPropertyDescription("异常情况列表")
private List<Anomaly> anomalies;
@JsonPropertyDescription("优化建议")
private List<Suggestion> 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;
}
}

21
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; // 自定义水印文字,若为空则使用默认
}

5
mh-framework/pom.xml

@ -63,6 +63,11 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId> <artifactId>spring-boot-starter-amqp</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>

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

4
mh-quartz/pom.xml

@ -39,6 +39,10 @@
<groupId>com.mh</groupId> <groupId>com.mh</groupId>
<artifactId>mh-framework</artifactId> <artifactId>mh-framework</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies> </dependencies>

39
mh-system/pom.xml

@ -22,6 +22,45 @@
<groupId>com.mh</groupId> <groupId>com.mh</groupId>
<artifactId>mh-common</artifactId> <artifactId>mh-common</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-client-chat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.2.10</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>${poi.version}</version>
</dependency>
</dependencies> </dependencies>

17
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<String> reasoningConsumer);
}

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

15
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);
}

114
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<String, SseEmitter> 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}";
}
}

189
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<String> 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<String, Object> params = Map.of(
"rawData", rawData,
"currentDate", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE)
);
StringBuilder fullResponse = new StringBuilder();
// Flux<String> 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;
}
}

204
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";
}
}

52
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<ReportRequestDTO> 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;
}
}

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

43
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) {}
}

34
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<String, AsyncTaskDTO> 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());
}
}
}

328
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";
}
}

31
pom.xml

@ -42,6 +42,8 @@
<message.version>6.2.1</message.version> <message.version>6.2.1</message.version>
<easyexcel.version>3.2.1</easyexcel.version> <easyexcel.version>3.2.1</easyexcel.version>
<amqp.version>3.4.2</amqp.version> <amqp.version>3.4.2</amqp.version>
<spring-ai.version>1.1.2</spring-ai.version>
<spring-retry.version>2.0.10</spring-retry.version>
</properties> </properties>
<!-- 依赖声明 --> <!-- 依赖声明 -->
@ -159,11 +161,26 @@
</dependency> </dependency>
<!-- excel工具 --> <!-- excel工具 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.poi</groupId> <groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId> <artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version> <version>${poi.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.xmlbeans</groupId>
<artifactId>xmlbeans</artifactId>
<version>3.1.0</version>
</dependency>
<!-- velocity代码生成使用模板 --> <!-- velocity代码生成使用模板 -->
<dependency> <dependency>
@ -282,6 +299,20 @@
<version>${amqp.version}</version> <version>${amqp.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring-retry -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>${spring-retry.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

Loading…
Cancel
Save