package com.estate.expert.admin.controller.stat; import com.deepoove.poi.XWPFTemplate; import com.estate.expert.admin.enums.BizResultCode; import com.estate.expert.admin.service.CheckTaskService; import com.estate.expert.admin.service.ReportService; import com.estate.expert.admin.service.ReportTemplateService; import com.estate.expert.admin.utils.WordToPdfConverter; import com.estate.expert.admin.utils.wordtopdf.PdfUtils; import com.estate.expert.admin.utils.wordtopdf.WordUtil; import com.estate.expert.common.auth.IgnoreAuth; import com.estate.expert.common.rpc.Result; import fr.opensagres.poi.xwpf.converter.pdf.PdfConverter; import fr.opensagres.poi.xwpf.converter.pdf.PdfOptions; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Api(tags = "管理后台/统计/报告下载") @Slf4j @CrossOrigin @Controller @RequestMapping("/admin/stat/report") @Validated public class ReportController { @Resource private ReportService reportService; @Resource private CheckTaskService checkTaskService; @Resource private ReportTemplateService reportTemplateService; @Value("${file.upload.dir}/reports") private String REPORT_UPLOAD_DIR; @ApiOperation("模糊评价报告下载") @GetMapping("/fuzzyReport") @IgnoreAuth public void fuzzyReport(@ApiParam("检查任务ID") @RequestParam(value = "checkTaskId") Integer checkTaskId, @ApiParam("报告模版ID") @RequestParam(value = "templateId") Integer templateId, @ApiParam("uuid") @RequestParam(value = "uuid",required = false) String uuid, HttpServletResponse response) throws IOException { try (var doc = reportService.fuzzyReport(checkTaskId, templateId)) { download(doc, response, "模糊评价", checkTaskId, templateId,uuid); } } @ApiOperation("打分评价报告下载") @GetMapping("/scoreReport") @IgnoreAuth public void scoreReport(@ApiParam("检查任务ID") @RequestParam(value = "checkTaskId") Integer checkTaskId, @ApiParam("报告模版ID") @RequestParam(value = "templateId") Integer templateId, HttpServletResponse response) throws IOException { try (var doc = reportService.scoreReport(checkTaskId, templateId)) { download(doc, response, "打分评价", checkTaskId, templateId,null); } } @ApiOperation("模版示例下载") @GetMapping("/example") @IgnoreAuth public void example(@ApiParam("报告模版ID") @RequestParam(value = "templateId") Integer templateId, HttpServletResponse response) throws IOException { try (var doc = reportService.fuzzyReport(1, templateId)) { download(doc, response, "示例",1, templateId,null); } } @Autowired private PdfUtils pdfUtils; @ApiOperation("模糊评价报告PDF下载(新接口)- 通过Word链接转换") @GetMapping("/fuzzyReportPdfNew") @IgnoreAuth public void fuzzyReportPdfNew(@ApiParam("Word文档URL") @RequestParam(value = "wordUrl") String wordUrl, @ApiParam("文件名(可选)") @RequestParam(value = "fileName", required = false) String fileName, HttpServletResponse response) throws Exception { try { String test = pdfUtils.test("1769239688785.docx"); } catch (Exception e) { throw new RuntimeException(e); } } @ApiOperation("模糊评价报告PDF下载") @GetMapping("/fuzzyReportPdf") @IgnoreAuth public void fuzzyReportPdf(@ApiParam("检查任务ID") @RequestParam(value = "checkTaskId") Integer checkTaskId, @ApiParam("报告模版ID") @RequestParam(value = "templateId") Integer templateId, HttpServletResponse response) throws IOException { downloadPdf(checkTaskId, templateId, response, "模糊评价", true); } @ApiOperation("打分评价报告PDF下载") @GetMapping("/scoreReportPdf") @IgnoreAuth public void scoreReportPdf(@ApiParam("检查任务ID") @RequestParam(value = "checkTaskId") Integer checkTaskId, @ApiParam("报告模版ID") @RequestParam(value = "templateId") Integer templateId, HttpServletResponse response) throws IOException { downloadPdf(checkTaskId, templateId, response, "打分评价", false); } @ApiOperation("检查报告转换状态") @GetMapping("/checkConversionStatus") @ResponseBody @IgnoreAuth public Result checkConversionStatus(@ApiParam("文件UUID") @RequestParam(value = "uuid") String uuid) { try { ConversionStatus status = new ConversionStatus(); status.setUuid(uuid); // 检查Word文件是否存在 Path wordPath = Path.of(REPORT_UPLOAD_DIR).resolve(uuid + ".docx"); status.setWordFileExists(Files.exists(wordPath)); // 检查图片文件 Path imageDir = Path.of(REPORT_UPLOAD_DIR, "images"); if (Files.exists(imageDir)) { try (var stream = Files.list(imageDir)) { long imageCount = stream.filter(path -> { String fileName = path.getFileName().toString(); return fileName.startsWith(uuid + "_page_") && fileName.endsWith(".png"); }).count(); status.setImageCount((int) imageCount); status.setConversionCompleted(imageCount > 0); } } else { status.setImageCount(0); status.setConversionCompleted(false); } return Result.success(status); } catch (Exception e) { log.error("检查转换状态失败: uuid={}", uuid, e); return Result.error("检查转换状态失败: " + e.getMessage()); } } private void download(XWPFTemplate doc, HttpServletResponse response, String typeName, Integer checkTaskId, Integer templateId,String uuid) throws IOException { ByteArrayOutputStream docOutputStream = null; OutputStream out = null; BufferedOutputStream bos = null; try { var checkTask = checkTaskService.get(checkTaskId, true); var reportTemplate = reportTemplateService.get(templateId); BizResultCode.TASK_NOT_FOUND.check(checkTask == null); var docName = checkTask.getProject().getName() + "_" + reportTemplate.getName() + "_" + typeName; String encodedFileName = URLEncoder.encode(docName, "UTF-8"); response.setContentType("application/octet-stream"); response.setHeader("Content-disposition", "attachment;filename=\"" + encodedFileName + ".docx\""); // 先将文档写入字节数组 docOutputStream = new ByteArrayOutputStream(); doc.write(docOutputStream); byte[] docBytes = docOutputStream.toByteArray(); // 写入响应流供用户下载 out = response.getOutputStream(); bos = new BufferedOutputStream(out); bos.write(docBytes); bos.flush(); out.flush(); // 保存文件到服务器 // if(uuid!=null && !uuid.isEmpty()){ // saveFileToServer(docBytes, docName + ".docx",uuid); // } } catch (Exception e) { log.error("文档下载失败", e); throw new IOException("文档下载失败: " + e.getMessage(), e); } finally { if (docOutputStream != null) { try { docOutputStream.close(); } catch (Exception e) { log.warn("关闭输出流失败", e); } } } } private void downloadPdf(Integer checkTaskId, Integer templateId, HttpServletResponse response, String typeName, boolean isFuzzy) throws IOException { ByteArrayOutputStream pdfOutputStream = null; OutputStream out = null; BufferedOutputStream bos = null; try { var checkTask = checkTaskService.get(checkTaskId, true); var reportTemplate = reportTemplateService.get(templateId); BizResultCode.TASK_NOT_FOUND.check(checkTask == null); var docName = checkTask.getProject().getName() + "_" + reportTemplate.getName() + "_" + typeName; String encodedFileName = URLEncoder.encode(docName, "UTF-8"); response.setContentType("application/pdf"); response.setHeader("Content-disposition", "attachment;filename=\"" + encodedFileName + ".pdf\""); // 先将PDF写入字节数组 pdfOutputStream = new ByteArrayOutputStream(); if (isFuzzy) { reportService.fuzzyReportPdf(checkTaskId, templateId, pdfOutputStream); } else { reportService.scoreReportPdf(checkTaskId, templateId, pdfOutputStream); } byte[] pdfBytes = pdfOutputStream.toByteArray(); // 写入响应流供用户下载 out = response.getOutputStream(); bos = new BufferedOutputStream(out); bos.write(pdfBytes); bos.flush(); out.flush(); // 保存文件到服务器 saveFileToServer(pdfBytes, docName + ".pdf",null); } catch (Exception e) { log.error("PDF下载失败", e); throw new IOException("PDF下载失败: " + e.getMessage(), e); } finally { if (pdfOutputStream != null) { try { pdfOutputStream.close(); } catch (Exception e) { log.warn("关闭输出流失败", e); } } } } /** * 将Word文档转换为PDF并下载 * @param doc Word文档模板 * @param response HTTP响应 * @param typeName 报告类型名称 * @param checkTaskId 检查任务ID * @param templateId 模板ID * @throws IOException IO异常 */ private void downloadAsPdf(XWPFTemplate doc, HttpServletResponse response, String typeName, Integer checkTaskId, Integer templateId) throws IOException { ByteArrayOutputStream docOutputStream = null; ByteArrayInputStream docInputStream = null; ByteArrayOutputStream pdfOutputStream = null; XWPFDocument document = null; OutputStream out = null; BufferedOutputStream bos = null; try { var checkTask = checkTaskService.get(checkTaskId, true); var reportTemplate = reportTemplateService.get(templateId); BizResultCode.TASK_NOT_FOUND.check(checkTask == null); var docName = checkTask.getProject().getName() + "_" + reportTemplate.getName() + "_" + typeName; String encodedFileName = URLEncoder.encode(docName, "UTF-8"); response.setContentType("application/pdf"); response.setHeader("Content-disposition", "attachment;filename=\"" + encodedFileName + ".pdf\""); // 将Word模板写入字节数组 docOutputStream = new ByteArrayOutputStream(); doc.write(docOutputStream); // 从字节数组读取Word文档 docInputStream = new ByteArrayInputStream(docOutputStream.toByteArray()); document = new XWPFDocument(docInputStream); // 配置PDF选项 PdfOptions options = PdfOptions.create(); options.fontEncoding("UTF-8"); // 先将PDF转换到字节数组 pdfOutputStream = new ByteArrayOutputStream(); PdfConverter.getInstance().convert(document, pdfOutputStream, options); byte[] pdfBytes = pdfOutputStream.toByteArray(); // 写入响应流供用户下载 out = response.getOutputStream(); bos = new BufferedOutputStream(out); bos.write(pdfBytes); bos.flush(); out.flush(); // 保存文件到服务器 saveFileToServer(pdfBytes, docName + ".pdf",null); } catch (Exception e) { log.error("Word转PDF失败", e); throw new IOException("Word转PDF失败: " + e.getMessage(), e); } finally { // 关闭资源 if (document != null) { try { document.close(); } catch (Exception e) { log.warn("关闭Word文档失败", e); } } if (pdfOutputStream != null) { try { pdfOutputStream.close(); } catch (Exception e) { log.warn("关闭PDF输出流失败", e); } } if (docInputStream != null) { try { docInputStream.close(); } catch (Exception e) { log.warn("关闭输入流失败", e); } } if (docOutputStream != null) { try { docOutputStream.close(); } catch (Exception e) { log.warn("关闭输出流失败", e); } } } } /** * 保存文件到服务器 * @param fileBytes 文件字节数组 * @param fileName 文件名 */ private String saveFileToServer(byte[] fileBytes, String fileName,String uuid) { try { // 确保目录存在 Path reportDir = Path.of(REPORT_UPLOAD_DIR).toAbsolutePath().normalize(); if (!Files.exists(reportDir)) { Files.createDirectories(reportDir); } //文件名随机 fileName =uuid+".docx"; // 保存文件 Path filePath = reportDir.resolve(fileName); Files.write(filePath, fileBytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); log.info("文件已保存到服务器: {}", filePath.toString()); return fileName; } catch (Exception e) { log.error("保存文件到服务器失败: {}", fileName, e); // 不抛出异常,避免影响用户下载 } return null; } /** * 将Word文档转换为图片并保存到服务器 * @param docBytes Word文档字节数组 * @param uuid 文件唯一标识 */ private void convertWordToImagesAndSave(byte[] docBytes, String uuid) { ByteArrayInputStream docInputStream = null; XWPFDocument document = null; ByteArrayOutputStream pdfOutputStream = null; PDDocument pdfDocument = null; try { // 1. 将Word文档转换为PDF docInputStream = new ByteArrayInputStream(docBytes); document = new XWPFDocument(docInputStream); pdfOutputStream = new ByteArrayOutputStream(); PdfOptions options = PdfOptions.create(); options.fontEncoding("UTF-8"); PdfConverter.getInstance().convert(document, pdfOutputStream, options); byte[] pdfBytes = pdfOutputStream.toByteArray(); // 2. 将PDF转换为图片 ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes); pdfDocument = PDDocument.load(pdfInputStream); PDFRenderer pdfRenderer = new PDFRenderer(pdfDocument); // 确保图片目录存在 Path imageDir = Path.of(REPORT_UPLOAD_DIR, "images").toAbsolutePath().normalize(); if (!Files.exists(imageDir)) { Files.createDirectories(imageDir); } // 转换每一页为图片 int pageCount = pdfDocument.getNumberOfPages(); log.info("开始转换Word为图片,共{}页", pageCount); for (int i = 0; i < pageCount; i++) { // 渲染PDF页面为图片,DPI设置为300以获得高质量图片 BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300, ImageType.RGB); // 保存图片 String imageFileName = uuid + "_page_" + (i + 1) + ".png"; Path imagePath = imageDir.resolve(imageFileName); ImageIO.write(image, "PNG", imagePath.toFile()); log.info("页面{}已转换为图片: {}", i + 1, imagePath.toString()); } log.info("Word文档已成功转换为{}张图片", pageCount); } catch (Exception e) { log.error("Word转图片失败: uuid={}", uuid, e); // 不抛出异常,避免影响用户下载 } finally { // 关闭资源 if (pdfDocument != null) { try { pdfDocument.close(); } catch (Exception e) { log.warn("关闭PDF文档失败", e); } } if (document != null) { try { document.close(); } catch (Exception e) { log.warn("关闭Word文档失败", e); } } if (pdfOutputStream != null) { try { pdfOutputStream.close(); } catch (Exception e) { log.warn("关闭PDF输出流失败", e); } } if (docInputStream != null) { try { docInputStream.close(); } catch (Exception e) { log.warn("关闭文档输入流失败", e); } } } } /** * 从文件名中提取页码 * @param fileName 文件名,格式:uuid_page_N.png * @return 页码 */ private int extractPageNumber(String fileName) { try { // 文件名格式:uuid_page_N.png String[] parts = fileName.replace(".png", "").split("_page_"); if (parts.length == 2) { return Integer.parseInt(parts[1]); } } catch (Exception e) { log.warn("解析页码失败: {}", fileName, e); } return 0; } /** * 通过Word文档URL转换为PDF并下载 * @param wordUrl Word文档的URL地址 * @param fileName 文件名(可选) * @param response HTTP响应 * @throws IOException IO异常 */ private void downloadPdfFromWordUrl(String wordUrl, String fileName, HttpServletResponse response) throws IOException { InputStream wordInputStream = null; XWPFDocument document = null; ByteArrayOutputStream pdfOutputStream = null; OutputStream out = null; BufferedOutputStream bos = null; HttpURLConnection connection = null; try { log.info("开始从URL下载Word文档: {}", wordUrl); // 1. 从URL下载Word文档 URL url = new URL(wordUrl); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(30000); // 30秒连接超时 connection.setReadTimeout(60000); // 60秒读取超时 connection.setRequestProperty("User-Agent", "Mozilla/5.0"); int responseCode = connection.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_OK) { throw new IOException("下载Word文档失败,HTTP状态码: " + responseCode); } wordInputStream = connection.getInputStream(); log.info("Word文档下载成功,开始转换为PDF"); // 2. 读取Word文档 document = new XWPFDocument(wordInputStream); // 3. 转换为PDF pdfOutputStream = new ByteArrayOutputStream(); PdfOptions options = PdfOptions.create(); options.fontEncoding("UTF-8"); // 确保图表不丢失的关键配置 PdfConverter.getInstance().convert(document, pdfOutputStream, options); byte[] pdfBytes = pdfOutputStream.toByteArray(); log.info("PDF转换成功,大小: {} bytes", pdfBytes.length); // 4. 设置响应头(修复中文乱码) String downloadFileName = fileName != null && !fileName.isEmpty() ? fileName : "report_" + System.currentTimeMillis(); if (!downloadFileName.toLowerCase().endsWith(".pdf")) { downloadFileName += ".pdf"; } // 使用RFC 5987规范处理中文文件名 String encodedFileName = URLEncoder.encode(downloadFileName, "UTF-8").replaceAll("\\+", "%20"); response.setContentType("application/pdf"); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName); response.setContentLength(pdfBytes.length); // 5. 写入响应流 out = response.getOutputStream(); bos = new BufferedOutputStream(out); bos.write(pdfBytes); bos.flush(); out.flush(); log.info("PDF文档下载成功: {}", downloadFileName); } catch (Exception e) { log.error("从Word URL转换PDF失败: url={}", wordUrl, e); throw new IOException("从Word URL转换PDF失败: " + e.getMessage(), e); } finally { // 关闭资源 if (document != null) { try { document.close(); } catch (Exception e) { log.warn("关闭Word文档失败", e); } } if (pdfOutputStream != null) { try { pdfOutputStream.close(); } catch (Exception e) { log.warn("关闭PDF输出流失败", e); } } if (wordInputStream != null) { try { wordInputStream.close(); } catch (Exception e) { log.warn("关闭Word输入流失败", e); } } if (connection != null) { try { connection.disconnect(); } catch (Exception e) { log.warn("断开HTTP连接失败", e); } } } } /** * 转换状态内部类 */ public static class ConversionStatus { private String uuid; private boolean wordFileExists; private int imageCount; private boolean conversionCompleted; public String getUuid() { return uuid; } public void setUuid(String uuid) { this.uuid = uuid; } public boolean isWordFileExists() { return wordFileExists; } public void setWordFileExists(boolean wordFileExists) { this.wordFileExists = wordFileExists; } public int getImageCount() { return imageCount; } public void setImageCount(int imageCount) { this.imageCount = imageCount; } public boolean isConversionCompleted() { return conversionCompleted; } public void setConversionCompleted(boolean conversionCompleted) { this.conversionCompleted = conversionCompleted; } } }