liujie
2026-01-26 8be8b3ab626d769cc1745b6b3ec5506fd489831c
admin-server/src/main/java/com/estate/expert/admin/controller/stat/ReportController.java
@@ -5,24 +5,42 @@
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.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
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
@@ -41,14 +59,18 @@
    @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,
                          HttpServletResponse response) throws IOException {
                              @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);
            download(doc, response, "模糊评价", checkTaskId, templateId,uuid);
        }
    }
@@ -59,7 +81,7 @@
                          @ApiParam("报告模版ID") @RequestParam(value = "templateId") Integer templateId,
                          HttpServletResponse response) throws IOException {
        try (var doc = reportService.scoreReport(checkTaskId, templateId)) {
            download(doc, response, "打分评价", checkTaskId, templateId);
            download(doc, response, "打分评价", checkTaskId, templateId,null);
        }
    }
@@ -69,9 +91,30 @@
    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);
            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")
@@ -91,53 +134,513 @@
        downloadPdf(checkTaskId, templateId, response, "打分评价", false);
    }
    @ApiOperation("检查报告转换状态")
    @GetMapping("/checkConversionStatus")
    @ResponseBody
    @IgnoreAuth
    public Result<ConversionStatus> 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) throws IOException {
        OutputStream out;
        BufferedOutputStream bos;
                          Integer checkTaskId, Integer templateId,String uuid) throws IOException {
        ByteArrayOutputStream docOutputStream = null;
        OutputStream out = null;
        BufferedOutputStream bos = null;
        var checkTask = checkTaskService.get(checkTaskId, true);
        var reportTemplate = reportTemplateService.get(templateId);
        BizResultCode.TASK_NOT_FOUND.check(checkTask == 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\"");
            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\"");
        // HttpServletResponse response
        out = response.getOutputStream();
        bos = new BufferedOutputStream(out);
        doc.write(bos);
        bos.flush();
        out.flush();
            // 先将文档写入字节数组
            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 {
        OutputStream out;
        BufferedOutputStream bos;
        ByteArrayOutputStream pdfOutputStream = null;
        OutputStream out = null;
        BufferedOutputStream bos = null;
        var checkTask = checkTaskService.get(checkTaskId, true);
        var reportTemplate = reportTemplateService.get(templateId);
        BizResultCode.TASK_NOT_FOUND.check(checkTask == 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\"");
            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\"");
        out = response.getOutputStream();
        bos = new BufferedOutputStream(out);
            // 先将PDF写入字节数组
            pdfOutputStream = new ByteArrayOutputStream();
            if (isFuzzy) {
                reportService.fuzzyReportPdf(checkTaskId, templateId, pdfOutputStream);
            } else {
                reportService.scoreReportPdf(checkTaskId, templateId, pdfOutputStream);
            }
            byte[] pdfBytes = pdfOutputStream.toByteArray();
        if (isFuzzy) {
            reportService.fuzzyReportPdf(checkTaskId, templateId, bos);
        } else {
            reportService.scoreReportPdf(checkTaskId, templateId, bos);
            // 写入响应流供用户下载
            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;
        }
        bos.flush();
        out.flush();
        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;
        }
    }
}