From 0579af014feb4318cecef1c4b8376752344a6edb Mon Sep 17 00:00:00 2001 From: 25604 Date: Sat, 28 Feb 2026 16:35:53 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=B5=8B=E8=AF=95=E4=BC=98=E5=8C=96a?= =?UTF-8?q?i=E6=99=BA=E8=83=BD=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/ai/AiFileController.java | 84 ++++ .../controller/ai/AsyncReportController.java | 5 +- .../controller/ai/FileDownloadController.java | 39 -- .../src/main/resources/application-dev.yml | 2 +- .../com/mh/common/config/FileStorageUtil.java | 4 +- .../core/domain/dto/AsyncTaskEvent.java | 22 + .../mh/framework/config/ResourcesConfig.java | 7 +- .../mh/framework/config/SecurityConfig.java | 7 +- .../AdvancedWordGenerationServiceImpl.java | 471 ++++++++++++++++++ .../ai/impl/AsyncReportServiceImpl.java | 58 ++- .../ai/impl/EnergyReportServiceImpl.java | 4 +- .../ai/impl/ReportOrchestrationService.java | 23 +- .../service/ai/impl/TaskStoreService.java | 33 +- 13 files changed, 697 insertions(+), 62 deletions(-) create mode 100644 mh-admin/src/main/java/com/mh/web/controller/ai/AiFileController.java delete 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/core/domain/dto/AsyncTaskEvent.java create mode 100644 mh-system/src/main/java/com/mh/system/service/ai/impl/AdvancedWordGenerationServiceImpl.java diff --git a/mh-admin/src/main/java/com/mh/web/controller/ai/AiFileController.java b/mh-admin/src/main/java/com/mh/web/controller/ai/AiFileController.java new file mode 100644 index 0000000..8f4828c --- /dev/null +++ b/mh-admin/src/main/java/com/mh/web/controller/ai/AiFileController.java @@ -0,0 +1,84 @@ +package com.mh.web.controller.ai; + +import com.mh.common.config.MHConfig; +import com.mh.common.core.controller.BaseController; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.File; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * AI文件下载控制器 + * 处理AI生成的报表文件下载请求 + * + * @author mh + */ +@RestController +@RequestMapping("/ai/files") +public class AiFileController extends BaseController { + + /** + * 下载AI生成的文件 + * + * @param filename 文件名 + * @return 文件响应 + */ + @GetMapping("/{filename:.+}") + public ResponseEntity downloadFile(@PathVariable String filename) { + return getResourceResponseEntity(filename); + } + + private ResponseEntity getResourceResponseEntity(@PathVariable String filename) { + try { + // 构建文件路径 + String storageDir = MHConfig.getProfile(); + File file = new File(storageDir, filename); + + // 检查文件是否存在 + if (!file.exists()) { + return ResponseEntity.notFound().build(); + } + + // 创建资源 + Resource resource = new FileSystemResource(file); + + // 使用RFC 5987标准格式,支持UTF-8编码 + String encodedFileName = URLEncoder.encode(filename, StandardCharsets.UTF_8); + String disposition = String.format("attachment; filename*=UTF-8''%s", encodedFileName); + + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_DISPOSITION, disposition); + headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION); + + return ResponseEntity.ok() + .headers(headers) + .contentLength(file.length()) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(resource); + + } catch (Exception e) { + return ResponseEntity.internalServerError().build(); + } + } + + /** + * 设备分析导出 + * + * @param filename 文件名 + * @return 文件响应 + */ + @PostMapping("/{filename:.+}") + public ResponseEntity deviceAnalyzeExport(@PathVariable String filename) { + return getResourceResponseEntity(filename); + } +} \ No newline at end of file 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 index 50172a8..0f3afd0 100644 --- 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 @@ -1,5 +1,6 @@ package com.mh.web.controller.ai; +import com.mh.common.core.controller.BaseController; import com.mh.common.core.domain.dto.AsyncTaskDTO; import com.mh.system.service.ai.impl.AsyncReportServiceImpl; import com.mh.system.service.ai.impl.TaskStoreService; @@ -18,8 +19,8 @@ import java.util.Map; * @date 2026-02-27 11:36:57 */ @RestController -@RequestMapping("/api/reports/async") -public class AsyncReportController { +@RequestMapping("/ai/reports/async") +public class AsyncReportController extends BaseController { private final AsyncReportServiceImpl asyncReportService; private final TaskStoreService taskStore; 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 deleted file mode 100644 index 4df1e0d..0000000 --- a/mh-admin/src/main/java/com/mh/web/controller/ai/FileDownloadController.java +++ /dev/null @@ -1,39 +0,0 @@ -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 4ff0e96..d2d36d6 100644 --- a/mh-admin/src/main/resources/application-dev.yml +++ b/mh-admin/src/main/resources/application-dev.yml @@ -107,7 +107,7 @@ spring: # 主库数据源 master: #添加allowMultiQueries=true 在批量更新时才不会出错 - url: jdbc:postgresql://127.0.0.1:5432/eemcs_gh_ers_dev + url: jdbc:postgresql://127.0.0.1:5432/eemcs # url: jdbc:postgresql://106.55.173.225:5505/eemcs username: postgres password: mh@803 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 index 14e8fab..8d00bca 100644 --- a/mh-common/src/main/java/com/mh/common/config/FileStorageUtil.java +++ b/mh-common/src/main/java/com/mh/common/config/FileStorageUtil.java @@ -18,7 +18,7 @@ import java.nio.file.Paths; */ @Component public class FileStorageUtil { - @Value("${mh.profile:/tmp/energy-reports}") + @Value("${mh.profile:/}") private String storageDir; public String saveFile(String filename, byte[] content) throws IOException { @@ -28,7 +28,7 @@ public class FileStorageUtil { Files.write(filePath, content); // 生成可下载的URL(假设当前应用提供静态资源访问或下载接口) return ServletUriComponentsBuilder.fromCurrentContextPath() - .path("/api/files/") + .path("/ai/files/") .path(filename) .toUriString(); } diff --git a/mh-common/src/main/java/com/mh/common/core/domain/dto/AsyncTaskEvent.java b/mh-common/src/main/java/com/mh/common/core/domain/dto/AsyncTaskEvent.java new file mode 100644 index 0000000..2f0b703 --- /dev/null +++ b/mh-common/src/main/java/com/mh/common/core/domain/dto/AsyncTaskEvent.java @@ -0,0 +1,22 @@ +package com.mh.common.core.domain.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author LJF + * @version 1.0 + * @project EEMCS + * @description 异步任务事件dto + * @date 2026-02-28 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AsyncTaskEvent { + + private String event; // 事件类型:reasoning, COMPLETED, FAILED + private Object data; // 事件数据 + +} diff --git a/mh-framework/src/main/java/com/mh/framework/config/ResourcesConfig.java b/mh-framework/src/main/java/com/mh/framework/config/ResourcesConfig.java index 8bfe9e7..f544a15 100644 --- a/mh-framework/src/main/java/com/mh/framework/config/ResourcesConfig.java +++ b/mh-framework/src/main/java/com/mh/framework/config/ResourcesConfig.java @@ -36,7 +36,12 @@ public class ResourcesConfig implements WebMvcConfigurer /** swagger配置 */ registry.addResourceHandler("/swagger-ui/**") .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") - .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());; + .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic()); + + /** AI文件下载路径 */ + registry.addResourceHandler("/ai/files/**") + .addResourceLocations("file:" + MHConfig.getProfile() + "/") + .setCacheControl(CacheControl.noCache());; } /** diff --git a/mh-framework/src/main/java/com/mh/framework/config/SecurityConfig.java b/mh-framework/src/main/java/com/mh/framework/config/SecurityConfig.java index f637007..8b26ad8 100644 --- a/mh-framework/src/main/java/com/mh/framework/config/SecurityConfig.java +++ b/mh-framework/src/main/java/com/mh/framework/config/SecurityConfig.java @@ -114,7 +114,12 @@ public class SecurityConfig requests.requestMatchers("/login", "/register", "/captchaImage").permitAll() // 静态资源,可匿名访问 .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll() - .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() +// .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll() + // 文件下载路径,允许匿名访问 + .requestMatchers(HttpMethod.GET, "/ai/files/**").permitAll() + // AI报表异步处理路径,允许匿名访问 + .requestMatchers(HttpMethod.POST, "/ai/reports/async").permitAll() + .requestMatchers(HttpMethod.GET, "/ai/reports/async/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); }) diff --git a/mh-system/src/main/java/com/mh/system/service/ai/impl/AdvancedWordGenerationServiceImpl.java b/mh-system/src/main/java/com/mh/system/service/ai/impl/AdvancedWordGenerationServiceImpl.java new file mode 100644 index 0000000..a042fcd --- /dev/null +++ b/mh-system/src/main/java/com/mh/system/service/ai/impl/AdvancedWordGenerationServiceImpl.java @@ -0,0 +1,471 @@ +package com.mh.system.service.ai.impl; + +import org.apache.poi.xwpf.model.XWPFHeaderFooterPolicy; +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.openxmlformats.schemas.wordprocessingml.x2006.main.*; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; + +/** + * @author MH + * @version 2.0 + * @project EEMCS + * @description 高级Word文档生成服务 - 支持表格和水印 + * @date 2026-02-28 + */ +@Service +public class AdvancedWordGenerationServiceImpl implements IFileGenerationService { + + @Override + public byte[] generateReport(EnergyReportDTO report, ReportRequestDTO request) throws IOException { + try (XWPFDocument document = new XWPFDocument()) { + // 添加专业水印 + if (request.getWatermark()) { + addProfessionalWatermark(document, request.getWatermarkText()); + } + + // 创建专业的文档结构 + createProfessionalDocument(document, report); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.write(baos); + return baos.toByteArray(); + } + } + + /** + * 创建专业的文档结构 + */ + private void createProfessionalDocument(XWPFDocument document, EnergyReportDTO report) { + // 添加封面页 + addCoverPage(document, report); + + // 添加目录(可选) + addTableOfContents(document); + + // 添加正文内容 + addMainContent(document, report); + } + + /** + * 添加封面页 + */ + private void addCoverPage(XWPFDocument document, EnergyReportDTO report) { + // 标题 + XWPFParagraph titlePara = document.createParagraph(); + titlePara.setAlignment(ParagraphAlignment.CENTER); + XWPFRun titleRun = titlePara.createRun(); + titleRun.setText("中央空调能效分析报告"); + titleRun.setBold(true); + titleRun.setFontSize(28); + titleRun.addBreak(); + + // 副标题 + XWPFParagraph subtitlePara = document.createParagraph(); + subtitlePara.setAlignment(ParagraphAlignment.CENTER); + XWPFRun subtitleRun = subtitlePara.createRun(); + subtitleRun.setText("Central Air Conditioning Energy Efficiency Analysis Report"); + subtitleRun.setFontSize(16); + subtitleRun.setColor("666666"); + subtitleRun.addBreak(); + subtitleRun.addBreak(); + + // 报告日期 + XWPFParagraph datePara = document.createParagraph(); + datePara.setAlignment(ParagraphAlignment.CENTER); + XWPFRun dateRun = datePara.createRun(); + dateRun.setText("报告日期: " + report.getReportDate()); + dateRun.setFontSize(14); + dateRun.addBreak(); + dateRun.addBreak(); + dateRun.addBreak(); + + // 添加分页 + document.createParagraph().createRun().addBreak(BreakType.PAGE); + } + + /** + * 添加目录 + */ + private void addTableOfContents(XWPFDocument document) { + XWPFParagraph tocTitle = document.createParagraph(); + tocTitle.setAlignment(ParagraphAlignment.CENTER); + XWPFRun tocRun = tocTitle.createRun(); + tocRun.setText("目 录"); + tocRun.setBold(true); + tocRun.setFontSize(18); + tocRun.addBreak(); + + // 目录项 + addTocItem(document, "1. 基本信息", 1); + addTocItem(document, "2. 设备能效明细", 1); + addTocItem(document, "3. 异常情况分析", 1); + addTocItem(document, "4. 优化建议", 1); + + document.createParagraph().createRun().addBreak(BreakType.PAGE); + } + + private void addTocItem(XWPFDocument document, String text, int level) { + XWPFParagraph para = document.createParagraph(); + if (level > 1) { + para.setIndentationLeft(400); + } + XWPFRun run = para.createRun(); + run.setText(text); + run.setFontSize(12); + } + + /** + * 添加主要内容 + */ + private void addMainContent(XWPFDocument document, EnergyReportDTO report) { + // 第一章:基本信息 + addChapter(document, "第一章 基本信息", report); + + // 第二章:设备能效明细(包含表格) + addDeviceDetailsChapter(document, report); + + // 第三章:异常情况分析 + addAnomaliesChapter(document, report); + + // 第四章:优化建议 + addSuggestionsChapter(document, report); + } + + /** + * 添加章节标题 + */ + private void addChapter(XWPFDocument document, String title, EnergyReportDTO report) { + XWPFParagraph chapterPara = document.createParagraph(); + chapterPara.setStyle("Heading1"); + XWPFRun chapterRun = chapterPara.createRun(); + chapterRun.setText(title); + chapterRun.setBold(true); + chapterRun.setFontSize(16); + chapterRun.addBreak(); + + // 添加基本信息内容 + addBasicInfoContent(document, report); + document.createParagraph().createRun().addBreak(); + } + + /** + * 添加基本信息内容 + */ + private void addBasicInfoContent(XWPFDocument document, EnergyReportDTO report) { + String[] infoItems = { + "报表日期: " + report.getReportDate(), + "整体能效评分: " + report.getOverallScore(), + "系统综合能效比(EER): " + report.getSystemEER(), + "总能耗: " + formatDouble(report.getTotalEnergyConsumption()) + " kWh" + }; + + for (String item : infoItems) { + XWPFParagraph para = document.createParagraph(); + para.setIndentationLeft(400); + XWPFRun run = para.createRun(); + run.setText("• " + item); + run.setFontSize(12); + } + } + + /** + * 添加设备明细章节(包含专业表格) + */ + private void addDeviceDetailsChapter(XWPFDocument document, EnergyReportDTO report) { + XWPFParagraph chapterPara = document.createParagraph(); + chapterPara.setStyle("Heading1"); + XWPFRun chapterRun = chapterPara.createRun(); + chapterRun.setText("第二章 设备能效明细"); + chapterRun.setBold(true); + chapterRun.setFontSize(16); + chapterRun.addBreak(); + + EnergyReportDTO.DeviceEnergy[] devices = report.getDeviceDetails().toArray(new EnergyReportDTO.DeviceEnergy[0]); + if (devices != null && devices.length > 0) { + createDeviceDetailsTable(document, devices); + } else { + XWPFParagraph para = document.createParagraph(); + para.setIndentationLeft(400); + XWPFRun run = para.createRun(); + run.setText("暂无设备数据"); + run.setFontSize(12); + } + document.createParagraph().createRun().addBreak(); + } + + /** + * 创建专业的设备明细表格 + */ + private void createDeviceDetailsTable(XWPFDocument document, EnergyReportDTO.DeviceEnergy[] devices) { + // 创建表格 (行数 = 表头1行 + 数据行数) + int rows = devices.length + 1; + int cols = 5; + XWPFTable table = document.createTable(rows, cols); + + // 设置表格样式 + setTableStyle(table); + + // 设置表头 + String[] headers = {"设备名称", "设备类型", "能耗(kWh)", "效率(%)", "偏差率(%)"}; + XWPFTableRow headerRow = table.getRow(0); + for (int i = 0; i < headers.length; i++) { + XWPFTableCell cell = headerRow.getCell(i); + cell.setColor("3366FF"); + XWPFParagraph para = cell.getParagraphs().get(0); + para.setAlignment(ParagraphAlignment.CENTER); + XWPFRun run = para.createRun(); + run.setText(headers[i]); + run.setBold(true); + run.setColor("FFFFFF"); + run.setFontSize(12); + } + + // 填充数据 + for (int i = 0; i < devices.length; i++) { + EnergyReportDTO.DeviceEnergy device = devices[i]; + XWPFTableRow row = table.getRow(i + 1); + + String[] values = { + device.getDeviceName(), + device.getDeviceType(), + formatDouble(device.getEnergyConsumption()), + formatDouble(device.getEfficiency()), + formatDouble(device.getDeviationRate()) + }; + + for (int j = 0; j < values.length; j++) { + XWPFTableCell cell = row.getCell(j); + XWPFParagraph para = cell.getParagraphs().get(0); + para.setAlignment(ParagraphAlignment.CENTER); + XWPFRun run = para.createRun(); + run.setText(values[j]); + run.setFontSize(11); + + // 根据偏差率设置背景色 + if (j == 4) { // 偏差率列 + double deviation = device.getDeviationRate() != null ? device.getDeviationRate() : 0; + if (deviation > 10) { + cell.setColor("FFCCCC"); // 红色背景表示高偏差 + } else if (deviation > 5) { + cell.setColor("FFE5CC"); // 橙色背景表示中等偏差 + } + } + } + } + } + + /** + * 添加异常情况分析章节 + */ + private void addAnomaliesChapter(XWPFDocument document, EnergyReportDTO report) { + XWPFParagraph chapterPara = document.createParagraph(); + chapterPara.setStyle("Heading1"); + XWPFRun chapterRun = chapterPara.createRun(); + chapterRun.setText("第三章 异常情况分析"); + chapterRun.setBold(true); + chapterRun.setFontSize(16); + chapterRun.addBreak(); + + EnergyReportDTO.Anomaly[] anomalies = report.getAnomalies().toArray(new EnergyReportDTO.Anomaly[0]); + if (anomalies != null && anomalies.length > 0) { + createAnomaliesTable(document, anomalies); + } else { + XWPFParagraph para = document.createParagraph(); + para.setIndentationLeft(400); + XWPFRun run = para.createRun(); + run.setText("系统运行正常,无异常情况"); + run.setFontSize(12); + } + document.createParagraph().createRun().addBreak(); + } + + /** + * 创建异常情况表格 + */ + private void createAnomaliesTable(XWPFDocument document, EnergyReportDTO.Anomaly[] anomalies) { + int rows = anomalies.length + 1; + int cols = 5; + XWPFTable table = document.createTable(rows, cols); + setTableStyle(table); + + // 表头 + String[] headers = {"设备名称", "异常类型", "异常描述", "严重程度", "预估损失"}; + XWPFTableRow headerRow = table.getRow(0); + for (int i = 0; i < headers.length; i++) { + XWPFTableCell cell = headerRow.getCell(i); + cell.setColor("FF6666"); + XWPFParagraph para = cell.getParagraphs().get(0); + para.setAlignment(ParagraphAlignment.CENTER); + XWPFRun run = para.createRun(); + run.setText(headers[i]); + run.setBold(true); + run.setColor("FFFFFF"); + run.setFontSize(12); + } + + // 数据行 + for (int i = 0; i < anomalies.length; i++) { + EnergyReportDTO.Anomaly anomaly = anomalies[i]; + XWPFTableRow row = table.getRow(i + 1); + + String[] values = { + anomaly.getDeviceName(), + anomaly.getAnomalyType(), + anomaly.getDescription(), + anomaly.getSeverity(), + formatDouble(anomaly.getEstimatedLoss()) + }; + + for (int j = 0; j < values.length; j++) { + XWPFTableCell cell = row.getCell(j); + XWPFParagraph para = cell.getParagraphs().get(0); + para.setAlignment(ParagraphAlignment.LEFT); + XWPFRun run = para.createRun(); + run.setText(values[j]); + run.setFontSize(11); + } + } + } + + /** + * 添加优化建议章节 + */ + private void addSuggestionsChapter(XWPFDocument document, EnergyReportDTO report) { + XWPFParagraph chapterPara = document.createParagraph(); + chapterPara.setStyle("Heading1"); + XWPFRun chapterRun = chapterPara.createRun(); + chapterRun.setText("第四章 优化建议"); + chapterRun.setBold(true); + chapterRun.setFontSize(16); + chapterRun.addBreak(); + + EnergyReportDTO.Suggestion[] suggestions = report.getSuggestions().toArray(new EnergyReportDTO.Suggestion[0]); + if (suggestions != null && suggestions.length > 0) { + for (int i = 0; i < suggestions.length; i++) { + addSuggestionItem(document, suggestions[i], i + 1); + } + } else { + XWPFParagraph para = document.createParagraph(); + para.setIndentationLeft(400); + XWPFRun run = para.createRun(); + run.setText("暂无优化建议"); + run.setFontSize(12); + } + } + + /** + * 添加建议项 + */ + private void addSuggestionItem(XWPFDocument document, EnergyReportDTO.Suggestion suggestion, int index) { + // 建议标题 + XWPFParagraph titlePara = document.createParagraph(); + titlePara.setIndentationLeft(400); + XWPFRun titleRun = titlePara.createRun(); + titleRun.setText(index + ". " + suggestion.getTitle()); + titleRun.setBold(true); + titleRun.setFontSize(13); + titleRun.addBreak(); + + // 建议详情 + String[] details = { + "优先级: " + suggestion.getPriority(), + "预期节能: " + formatDouble(suggestion.getExpectedSaving()) + "%", + "建议操作: " + suggestion.getAction(), + "详细说明: " + suggestion.getDescription() + }; + + for (String detail : details) { + XWPFParagraph para = document.createParagraph(); + para.setIndentationLeft(800); + XWPFRun run = para.createRun(); + run.setText("• " + detail); + run.setFontSize(11); + } + + document.createParagraph().createRun().addBreak(); + } + + /** + * 添加专业水印 + */ + private void addProfessionalWatermark(XWPFDocument document, String watermarkText) { + try { + // 使用POI的水印API(适用于较新版本) + // 注意:POI 4.1.2对水印支持有限,这里提供兼容性实现 + + // 方法1:在页眉中添加水印文本 + addWatermarkToHeader(document, watermarkText); + + } catch (Exception e) { + System.err.println("添加水印时出错: " + e.getMessage()); + // 水印失败不影响文档生成 + } + } + + /** + * 在页眉中添加水印文本 + */ + private void addWatermarkToHeader(XWPFDocument document, String watermarkText) { + try { + // 获取或创建页眉 + XWPFHeaderFooterPolicy policy = document.getHeaderFooterPolicy(); + if (policy == null) { + policy = document.createHeaderFooterPolicy(); + } + + // 创建页眉 + XWPFHeader header = policy.getDefaultHeader(); + if (header == null) { + header = policy.createHeader(XWPFHeaderFooterPolicy.DEFAULT); + } + + // 添加水印段落 + XWPFParagraph watermarkPara = header.createParagraph(); + watermarkPara.setAlignment(ParagraphAlignment.CENTER); + + XWPFRun watermarkRun = watermarkPara.createRun(); + watermarkRun.setText(watermarkText); + watermarkRun.setFontSize(48); + watermarkRun.setColor("D3D3D3"); + watermarkRun.setBold(true); + + } catch (Exception e) { + System.err.println("添加页眉水印失败: " + e.getMessage()); + } + } + + /** + * 设置表格样式 + */ + private void setTableStyle(XWPFTable table) { + // 设置表格边框 + table.setTableAlignment(TableRowAlign.CENTER); + + // 设置表格宽度 + table.setWidth("80%"); + + // 设置行高 + for (XWPFTableRow row : table.getRows()) { + row.setHeight(300); + } + } + + /** + * 格式化双精度数字 + */ + private String formatDouble(Double value) { + return value != null ? String.format("%.2f", value) : "-"; + } + + @Override + public String getFileExtension() { + return ".docx"; + } +} \ No newline at end of file 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 index 4b03ca3..0c27522 100644 --- 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 @@ -2,6 +2,7 @@ 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.AsyncTaskEvent; import com.mh.common.core.domain.dto.EnergyReportDTO; import com.mh.common.core.domain.dto.ReportRequestDTO; import com.mh.common.utils.uuid.UUID; @@ -14,6 +15,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.time.LocalDateTime; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -51,8 +53,23 @@ public class AsyncReportServiceImpl { 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)); + + // 发送缓存的事件 + List bufferedEvents = taskStore.getAndClearEvents(taskId); + if (bufferedEvents != null) { + for (AsyncTaskEvent event : bufferedEvents) { + sendEventInternal(taskId, emitter, event.getEvent(), event.getData()); + } + } + + emitter.onCompletion(() -> { + emitters.remove(taskId); + taskStore.removeTask(taskId); + }); + emitter.onTimeout(() -> { + emitters.remove(taskId); + taskStore.removeTask(taskId); + }); return emitter; } @@ -71,9 +88,16 @@ public class AsyncReportServiceImpl { public void processTask(String taskId, String userMessage) { try { // 解析自然语言 - ReportRequestDTO request = nlpService.parseUserIntent(userMessage); +// ReportRequestDTO request = nlpService.parseUserIntent(userMessage); + ReportRequestDTO request = new ReportRequestDTO(); + request.setReportType("Energy"); + request.setFormat("word"); + request.setTimeRange("2026-02-26"); + request.setWatermark(true); + request.setWatermarkText("铭汉"); // 获取原始数据(模拟) - String rawData = fetchRawData(request.getTimeRange()); +// String rawData = fetchRawData(request.getTimeRange()); + String rawData = ""; // 生成报表并实时推送推理过程 EnergyReportDTO report = energyReportService.generateReportWithReasoning(rawData, reasoningChunk -> sendEvent(taskId, "reasoning", reasoningChunk)); @@ -94,15 +118,25 @@ public class AsyncReportServiceImpl { 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); + sendEventInternal(taskId, emitter, event, data); + } else { + // 如果还没有注册 emitter,先缓冲事件 + log.info(" emitter 未注册,缓冲事件:{},数据:{}", event, data); + taskStore.bufferEvent(taskId, event, data); + } + } + + private void sendEventInternal(String taskId, SseEmitter emitter, String event, Object data) { + try { + // 打印输出推送值 + log.info("推送事件:{},数据:{}", event, data); + 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); } } 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 index 35e02aa..3730466 100644 --- 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 @@ -101,7 +101,6 @@ public class EnergyReportServiceImpl implements IEnergyReportService { // } // } // }).blockLast(); - log.info("AI原始格式输出:{}", fullResponse); // String jsonStr = extractJson(fullResponse.toString()); String jsonStr = "{\n" + @@ -173,6 +172,9 @@ public class EnergyReportServiceImpl implements IEnergyReportService { " ]\n" + "}"; log.info("AI优化格式输出:{}", jsonStr); + if (reasoningConsumer != null) { + reasoningConsumer.accept(jsonStr); + } try { return objectMapper.readValue(jsonStr, EnergyReportDTO.class); } catch (Exception e) { 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 index b56f3ae..2663ef1 100644 --- 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 @@ -16,13 +16,16 @@ import java.io.IOException; */ @Service public class ReportOrchestrationService { + private final AdvancedWordGenerationServiceImpl advancedWordService; private final WordGenerationServiceImpl wordService; private final ExcelGenerationServiceImpl excelService; private final PdfGenerationServiceImpl pdfService; - public ReportOrchestrationService(WordGenerationServiceImpl wordService, + public ReportOrchestrationService(AdvancedWordGenerationServiceImpl advancedWordService, + WordGenerationServiceImpl wordService, ExcelGenerationServiceImpl excelService, PdfGenerationServiceImpl pdfService) { + this.advancedWordService = advancedWordService; this.wordService = wordService; this.excelService = excelService; this.pdfService = pdfService; @@ -30,7 +33,14 @@ public class ReportOrchestrationService { public ReportFile generateFile(EnergyReportDTO report, ReportRequestDTO request) throws IOException { IFileGenerationService generator = switch (request.getFormat().toLowerCase()) { - case "word" -> wordService; + case "word" -> { + // 根据请求参数决定使用哪种Word生成服务 + if (shouldUseAdvancedWordService(request)) { + yield advancedWordService; + } else { + yield wordService; + } + } case "excel" -> excelService; default -> pdfService; }; @@ -39,5 +49,14 @@ public class ReportOrchestrationService { return new ReportFile(filename, content); } + /** + * 判断是否应该使用高级Word生成服务 + */ + private boolean shouldUseAdvancedWordService(ReportRequestDTO request) { + // 可以根据请求参数或其他条件决定 + // 比如:需要表格、水印、复杂格式等 + return request.getWatermark() != null && request.getWatermark(); + } + 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 index af0aff0..d635879 100644 --- 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 @@ -1,9 +1,12 @@ package com.mh.system.service.ai.impl; import com.mh.common.core.domain.dto.AsyncTaskDTO; +import com.mh.common.core.domain.dto.AsyncTaskEvent; import org.springframework.stereotype.Component; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -18,9 +21,15 @@ import java.util.concurrent.ConcurrentHashMap; public class TaskStoreService { private final Map tasks = new ConcurrentHashMap<>(); + private final Map> eventBuffer = new ConcurrentHashMap<>(); + + public void save(AsyncTaskDTO task) { + tasks.put(task.getTaskId(), task); + eventBuffer.putIfAbsent(task.getTaskId(), new ArrayList<>()); + } - 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) { @@ -31,4 +40,26 @@ public class TaskStoreService { } } + public void bufferEvent(String taskId, String event, Object data) { + List events = eventBuffer.get(taskId); + if (events != null) { + events.add(new AsyncTaskEvent(event, data)); + } + } + + public List getAndClearEvents(String taskId) { + List events = eventBuffer.get(taskId); + if (events != null && !events.isEmpty()) { + List result = new ArrayList<>(events); + events.clear(); + return result; + } + return null; + } + + public void removeTask(String taskId) { + tasks.remove(taskId); + eventBuffer.remove(taskId); + } + }