liujie
2026-01-24 13392606c16362d23478e84e0d43e869813d3095
update
2个文件已添加
3个文件已修改
1182 ■■■■■ 已修改文件
admin-server/pom.xml 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/config/WebMvcConfig.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/controller/stat/ReportController.java 628 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/utils/FileDownloader.java 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/utils/WordToPdfConverter.java 332 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/pom.xml
@@ -22,6 +22,25 @@
    <dependencies>
        <!-- JODConverter for LibreOffice integration -->
        <dependency>
            <groupId>org.jodconverter</groupId>
            <artifactId>jodconverter-core</artifactId>
            <version>4.4.6</version>
        </dependency>
        <dependency>
            <groupId>org.jodconverter</groupId>
            <artifactId>jodconverter-local</artifactId>
            <version>4.4.6</version>
        </dependency>
        <!-- Apache HttpClient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-spring-boot-starter</artifactId>
@@ -165,6 +184,13 @@
            <version>2.0.4</version>
        </dependency>
        <!-- PDF to Image conversion -->
        <dependency>
            <groupId>org.apache.pdfbox</groupId>
            <artifactId>pdfbox</artifactId>
            <version>2.0.29</version>
        </dependency>
        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
admin-server/src/main/java/com/estate/expert/admin/config/WebMvcConfig.java
@@ -1,10 +1,13 @@
package com.estate.expert.admin.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@@ -14,16 +17,37 @@
 * 很奇怪,需要自定义一个WebMvcConfig,才能让Jackson2HttpMessageConverter生效
 * https://www.cnblogs.com/joeblackzqq/p/17716810.html
 */
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Resource
    private ObjectMapper objectMapper;
    @Value("${file.upload.dir}")
    private String uploadDir;
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.removeIf(o -> o instanceof MappingJackson2HttpMessageConverter);
        converters.add(new MappingJackson2HttpMessageConverter(objectMapper));
        converters.add(new ResourceHttpMessageConverter());
    }
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // 配置报告文件访问路径
        String reportPath = "file:" + uploadDir + "/reports/";
        registry.addResourceHandler("/files/reports/**")
                .addResourceLocations(reportPath);
        // 配置报告图片访问路径
        String imagePath = "file:" + uploadDir + "/reports/images/";
        registry.addResourceHandler("/files/reports/images/**")
                .addResourceLocations(imagePath);
        log.info("静态资源映射配置完成:");
        log.info("  - /files/reports/** -> {}", reportPath);
        log.info("  - /files/reports/images/** -> {}", imagePath);
    }
}
admin-server/src/main/java/com/estate/expert/admin/controller/stat/ReportController.java
@@ -5,24 +5,38 @@
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.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.List;
@Api(tags = "管理后台/统计/报告下载")
@Slf4j
@@ -41,14 +55,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 +77,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 +87,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 WordToPdfConverter wordToPdfConverter;
    @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 {
        downloadPdfFromWordUrl(wordUrl, fileName, response);
        try {
            wordToPdfConverter.convertUrlToPdf(wordUrl,"C:\\Users\\Admin\\Desktop\\test.pdf");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    @ApiOperation("模糊评价报告PDF下载")
    @GetMapping("/fuzzyReportPdf")
@@ -91,53 +130,552 @@
        downloadPdf(checkTaskId, templateId, response, "打分评价", false);
    }
    @ApiOperation("获取报告图片列表")
    @GetMapping("/getReportImages")
    @ResponseBody
    @IgnoreAuth
    public Result<List<String>> getReportImages(@ApiParam("文件UUID") @RequestParam(value = "uuid") String uuid) {
        try {
            List<String> imageFiles = new ArrayList<>();
            Path imageDir = Path.of(REPORT_UPLOAD_DIR, "images").toAbsolutePath().normalize();
            if (!Files.exists(imageDir)) {
                return Result.success(imageFiles);
            }
            // 查找所有匹配的图片文件
            try (var stream = Files.list(imageDir)) {
                stream.filter(path -> {
                    String fileName = path.getFileName().toString();
                    return fileName.startsWith(uuid + "_page_") && fileName.endsWith(".png");
                })
                .sorted((p1, p2) -> {
                    // 按页码排序
                    String name1 = p1.getFileName().toString();
                    String name2 = p2.getFileName().toString();
                    int page1 = extractPageNumber(name1);
                    int page2 = extractPageNumber(name2);
                    return Integer.compare(page1, page2);
                })
                .forEach(path -> imageFiles.add(path.getFileName().toString()));
            }
            log.info("查询报告图片列表: uuid={}, 共{}张图片", uuid, imageFiles.size());
            return Result.success(imageFiles);
        } catch (Exception e) {
            log.error("查询报告图片列表失败: uuid={}", uuid, e);
            return Result.error("查询图片列表失败: " + e.getMessage());
        }
    }
    @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;
        }
    }
}
admin-server/src/main/java/com/estate/expert/admin/utils/FileDownloader.java
New file
@@ -0,0 +1,172 @@
package com.estate.expert.admin.utils;
import java.io.*;
import java.net.*;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Map;
import java.util.HashMap;
public class FileDownloader {
    /**
     * 增强的文件下载方法
     */
    public byte[] downloadFile(String fileUrl) throws Exception {
        // 验证URL
        if (fileUrl == null || fileUrl.trim().isEmpty()) {
            throw new IllegalArgumentException("URL不能为空");
        }
        URL url = new URL(fileUrl);
        // 设置超时和请求头
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestMethod("GET");
        connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
        connection.setRequestProperty("Accept", "application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/msword");
        connection.setConnectTimeout(30000);
        connection.setReadTimeout(60000);
        connection.setInstanceFollowRedirects(true);
        int responseCode = connection.getResponseCode();
        if (responseCode != HttpURLConnection.HTTP_OK) {
            throw new IOException("HTTP请求失败,状态码: " + responseCode);
        }
        // 检查内容类型
        String contentType = connection.getContentType();
        if (contentType == null ||
            (!contentType.contains("word") &&
             !contentType.contains("octet-stream") &&
             !contentType.contains("document"))) {
            System.out.println("警告: 内容类型可能不是Word文档: " + contentType);
        }
        // 获取文件大小
        int contentLength = connection.getContentLength();
        System.out.println("文件大小: " + (contentLength > 0 ? contentLength + " 字节" : "未知"));
        // 下载文件
        try (InputStream inputStream = connection.getInputStream();
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            long totalBytes = 0;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                totalBytes += bytesRead;
                if (contentLength > 0) {
                    int progress = (int) ((totalBytes * 100) / contentLength);
                    if (progress % 10 == 0) {
                        System.out.print("\r下载进度: " + progress + "%");
                    }
                }
            }
            byte[] fileData = outputStream.toByteArray();
            if (fileData.length == 0) {
                throw new IOException("下载的文件内容为空");
            }
            System.out.println("\n下载完成,实际大小: " + fileData.length + " 字节");
            // 验证文件格式(检查是否是有效的Word文档)
            if (!isValidWordDocument(fileData)) {
                System.out.println("警告: 下载的文件可能不是有效的Word文档");
            }
            return fileData;
        } finally {
            connection.disconnect();
        }
    }
    /**
     * 验证是否为有效的Word文档
     */
    private boolean isValidWordDocument(byte[] data) {
        if (data.length < 4) {
            return false;
        }
        // 检查.docx格式 (ZIP格式)
        if (data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04) {
            return true;
        }
        // 检查.doc格式 (OLE格式)
        if (data[0] == (byte)0xD0 && data[1] == (byte)0xCF &&
            data[2] == 0x11 && data[3] == (byte)0xE0) {
            return true;
        }
        return false;
    }
    /**
     * 下载并保存到文件
     */
    public File downloadToFile(String fileUrl, String outputPath) throws Exception {
        byte[] data = downloadFile(fileUrl);
        File outputFile = new File(outputPath);
        File parentDir = outputFile.getParentFile();
        if (parentDir != null && !parentDir.exists()) {
            parentDir.mkdirs();
        }
        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
            fos.write(data);
        }
        System.out.println("文件已保存到: " + outputFile.getAbsolutePath());
        return outputFile;
    }
    /**
     * 批量下载
     */
    public Map<String, byte[]> batchDownload(String[] urls) {
        Map<String, byte[]> results = new HashMap<>();
        for (String url : urls) {
            try {
                System.out.println("下载: " + url);
                byte[] data = downloadFile(url);
                results.put(url, data);
                System.out.println("✓ 下载成功");
            } catch (Exception e) {
                System.err.println("✗ 下载失败: " + e.getMessage());
                results.put(url, null);
            }
        }
        return results;
    }
    /**
     * 使用NIO下载(更高效)
     */
    public void downloadWithNIO(String fileUrl, String outputPath) throws Exception {
        URL url = new URL(fileUrl);
        try (InputStream inputStream = url.openStream();
             ReadableByteChannel rbc = Channels.newChannel(inputStream);
             FileOutputStream fos = new FileOutputStream(outputPath)) {
            fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
        }
        System.out.println("NIO下载完成: " + outputPath);
    }
}
admin-server/src/main/java/com/estate/expert/admin/utils/WordToPdfConverter.java
New file
@@ -0,0 +1,332 @@
package com.estate.expert.admin.utils;
import com.estate.expert.admin.utils.FileDownloader;
import org.springframework.stereotype.Component;
import java.io.*;
import java.nio.file.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
@Component
public class WordToPdfConverter {
    private FileDownloader downloader;
    public WordToPdfConverter() {
        this.downloader = new FileDownloader();
    }
    /**
     * 获取LibreOffice可执行文件路径
     */
    private String getLibreOfficeExecutable() {
        String os = System.getProperty("os.name").toLowerCase();
        // 尝试多个可能的路径
        if (os.contains("win")) {
            // Windows
            String[] possiblePaths = {
                    "C:\\Program Files\\LibreOffice\\program\\soffice.exe",
                    "C:\\Program Files (x86)\\LibreOffice\\program\\soffice.exe",
                    "D:\\Program Files\\LibreOffice\\program\\soffice.exe",
                    System.getenv("LIBREOFFICE_HOME") + "\\program\\soffice.exe"
            };
            for (String path : possiblePaths) {
                if (path != null && new File(path).exists()) {
                    return path;
                }
            }
            // 尝试在PATH中查找
            return "soffice";
        } else if (os.contains("mac")) {
            // macOS
            return "/Applications/LibreOffice.app/Contents/MacOS/soffice";
        } else {
            // Linux/Unix
            return "libreoffice";
        }
    }
    /**
     * 检查LibreOffice是否可用
     */
    public boolean checkLibreOffice() {
        try {
            String command = getLibreOfficeExecutable() + " --version";
            ProcessBuilder pb = new ProcessBuilder();
            if (System.getProperty("os.name").toLowerCase().contains("win")) {
                pb.command("cmd.exe", "/c", command);
            } else {
                pb.command("bash", "-c", command);
            }
            Process process = pb.start();
            // 读取输出
            StringBuilder output = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    output.append(line).append("\n");
                }
            }
            int exitCode = process.waitFor();
            if (exitCode == 0 && output.toString().contains("LibreOffice")) {
                System.out.println("✓ LibreOffice 检测成功");
                System.out.println("版本信息: " + output.toString().trim());
                return true;
            }
        } catch (Exception e) {
            System.err.println("✗ LibreOffice 检测失败: " + e.getMessage());
        }
        System.err.println("请安装 LibreOffice:");
        System.err.println("Windows: https://www.libreoffice.org/download/download/");
        System.err.println("Mac: brew install libreoffice");
        System.err.println("Linux: sudo apt-get install libreoffice");
        return false;
    }
    /**
     * 使用LibreOffice转换Word到PDF
     */
    public boolean convertWithLibreOffice(File inputFile, String outputPath) {
        if (!inputFile.exists() || inputFile.length() == 0) {
            System.err.println("输入文件不存在或为空");
            return false;
        }
        try {
            // 准备输出目录
            File outputFile = new File(outputPath);
            File outputDir = outputFile.getParentFile();
            if (outputDir != null && !outputDir.exists()) {
                outputDir.mkdirs();
            }
            String libreOfficePath = getLibreOfficeExecutable();
            System.out.println("输入文件: " + inputFile.getAbsolutePath());
            System.out.println("输出文件: " + outputPath);
            System.out.println("LibreOffice路径: " + libreOfficePath);
            // 构建命令
            ProcessBuilder pb = new ProcessBuilder();
            String[] command;
            if (System.getProperty("os.name").toLowerCase().contains("win")) {
                command = new String[] {
                        "cmd.exe", "/c",
                        "\"" + libreOfficePath + "\"",
                        "--headless",
                        "--convert-to", "pdf",
                        "--outdir", outputDir.getAbsolutePath(),
                        inputFile.getAbsolutePath()
                };
            } else {
                command = new String[] {
                        libreOfficePath,
                        "--headless",
                        "--convert-to", "pdf",
                        "--outdir", outputDir.getAbsolutePath(),
                        inputFile.getAbsolutePath()
                };
            }
            pb.command(command);
            pb.redirectErrorStream(true);
            System.out.println("执行命令: " + String.join(" ", command));
            Process process = pb.start();
            // 异步读取输出
            ExecutorService executor = Executors.newSingleThreadExecutor();
            Future<String> future = executor.submit(new StreamReader(process.getInputStream()));
            // 等待进程完成
            boolean completed = process.waitFor(5, TimeUnit.MINUTES);
            if (!completed) {
                process.destroyForcibly();
                System.err.println("转换超时");
                return false;
            }
            int exitCode = process.exitValue();
            String output = future.get();
            executor.shutdown();
            System.out.println("LibreOffice输出:\n" + output);
            System.out.println("退出代码: " + exitCode);
            if (exitCode == 0) {
                // 检查输出文件是否存在
                String baseName = inputFile.getName();
                String fileNameWithoutExt = baseName.substring(0, baseName.lastIndexOf('.'));
                String expectedPdf = outputDir.getAbsolutePath() +
                        File.separator + fileNameWithoutExt + ".pdf";
                File pdfFile = new File(expectedPdf);
                if (pdfFile.exists()) {
                    // 重命名为指定名称(如果需要)
                    if (!expectedPdf.equals(outputPath)) {
                        Files.move(pdfFile.toPath(), outputFile.toPath(),
                                StandardCopyOption.REPLACE_EXISTING);
                    }
                    System.out.println("✓ 转换成功: " + outputFile.getAbsolutePath());
                    System.out.println("文件大小: " + outputFile.length() + " 字节");
                    return true;
                } else {
                    // 检查是否直接生成了指定名称的文件
                    if (outputFile.exists()) {
                        System.out.println("✓ 转换成功: " + outputFile.getAbsolutePath());
                        return true;
                    } else {
                        System.err.println("✗ 输出文件未生成");
                        return false;
                    }
                }
            } else {
                System.err.println("✗ LibreOffice转换失败");
                return false;
            }
        } catch (Exception e) {
            System.err.println("转换过程异常: " + e.getMessage());
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 异步读取进程输出
     */
    private static class StreamReader implements Callable<String> {
        private InputStream inputStream;
        public StreamReader(InputStream inputStream) {
            this.inputStream = inputStream;
        }
        @Override
        public String call() throws Exception {
            StringBuilder output = new StringBuilder();
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(inputStream))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    output.append(line).append("\n");
                }
            }
            return output.toString();
        }
    }
    /**
     * 完整的URL到PDF转换流程
     */
    public boolean convertUrlToPdf(String wordUrl, String pdfOutputPath) {
        System.out.println("=== 开始Word到PDF转换 ===");
        System.out.println("源URL: " + wordUrl);
        System.out.println("目标PDF: " + pdfOutputPath);
        try {
            // 1. 检查LibreOffice
            if (!checkLibreOffice()) {
                return false;
            }
            // 2. 下载Word文档
            System.out.println("\n=== 下载Word文档 ===");
            byte[] wordData = downloader.downloadFile(wordUrl);
            if (wordData == null || wordData.length == 0) {
                System.err.println("下载的文件内容为空");
                return false;
            }
            // 3. 保存为临时文件
            System.out.println("\n=== 准备临时文件 ===");
            Path tempFile = Files.createTempFile("word_", ".docx");
            Files.write(tempFile, wordData);
            System.out.println("临时文件: " + tempFile.toString());
            // 4. 转换为PDF
            System.out.println("\n=== 开始转换 ===");
            boolean success = convertWithLibreOffice(tempFile.toFile(), pdfOutputPath);
            // 5. 清理临时文件
            System.out.println("\n=== 清理临时文件 ===");
            try {
                Files.deleteIfExists(tempFile);
                System.out.println("临时文件已清理");
            } catch (IOException e) {
                System.err.println("清理临时文件失败: " + e.getMessage());
            }
            return success;
        } catch (Exception e) {
            System.err.println("\n✗ 转换失败: " + e.getMessage());
            e.printStackTrace();
            return false;
        }
    }
    /**
     * 批量转换
     */
    public void batchConvert(String[] urls, String outputDir) {
        System.out.println("=== 批量转换开始 ===");
        System.out.println("文件数量: " + urls.length);
        System.out.println("输出目录: " + outputDir);
        File outputDirFile = new File(outputDir);
        if (!outputDirFile.exists()) {
            outputDirFile.mkdirs();
        }
        int successCount = 0;
        int failCount = 0;
        for (int i = 0; i < urls.length; i++) {
            String url = urls[i];
            String fileName = "document_" + (i + 1) + ".pdf";
            String outputPath = outputDir + File.separator + fileName;
            System.out.println("\n--- 处理文件 " + (i + 1) + "/" + urls.length + " ---");
            System.out.println("URL: " + url);
            boolean success = convertUrlToPdf(url, outputPath);
            if (success) {
                successCount++;
                System.out.println("✓ 转换成功: " + fileName);
            } else {
                failCount++;
                System.err.println("✗ 转换失败: " + fileName);
            }
        }
        System.out.println("\n=== 批量转换完成 ===");
        System.out.println("成功: " + successCount);
        System.out.println("失败: " + failCount);
        System.out.println("输出目录: " + outputDir);
    }
}