java 根据word模板,实现数据动态插入,包括二维码图片插入,并合并多个word文档,最终转为pdf导出

时间:2024-10-17 07:22:14

需求是要求查询数据库多条明细数据,将多条数据根据定好的word 模板,生成多个word文档(其中word文档中包含有二维码图片),并且需要将多个文档合并成一个文档,最终转换为pdf供前端导出和预览
我的实现逻辑如下:先根据查询的明细数据,在本地文件夹中生成多个word文档,并且将其一个个编号,同时每个文档对应的二维码图片也生成好,供后续插入word文档使用。然后分批次合并成一个word文档,最后转为pdf格式导出。为什么要分批次合并(如50个word合并一次),因为如果查询的明细条数太多的话,会造成阻塞。所以不仅要分批次合并,生成一个个word时还需要多线程执行。直接上代码:
首先是controller 层 没啥好说的

@ApiOperation("导出缴费单Pdf列表")
    @GetMapping("/exportwordPdf")
    public ResponseEntity<FileSystemResource> exportwordPdf(Long id, Date payDate, Date payDate2)
    {
        return paymentService.exportwordPdf(id,payDate,payDate2);
    }

重点说下impl实现层 注意 this.gettargetFiles(id,payDate,payDate2) 方法 这里是生成一个个word文档的方法,模板中以{{}} 作为占位符,然后一个个替换,我这里使用线程池实现多线程

@Value("${temp.outputPath}")
    private String outputPath; // 文档导出路径
    @Value("${temp.MODELPATH}")
    private String MODELPATH; // 模板文档临时存储路径
    @Value("${temp.qrcodeFont}") //二维码前缀
    private String qrcodeFont;
    private static final int BATCH_SIZE = 50;

@SneakyThrows
    @Override
    public ResponseEntity<FileSystemResource> exportwordPdf(Long id,Date payDate,Date payDate2) {
        List<File> targetFiles = this.gettargetFiles(id,payDate,payDate2);
        //合并文档
        if (targetFiles.size() > BATCH_SIZE) {
            return exportBatch(targetFiles, id, payDate, payDate2,true);
        } else {
            // 合并文档
            // 合并后文档路径
            File hbfile = new File(outputPath + File.separator+id+File.separator+ "output.docx");
            DocxMerge.appendDocx(hbfile, targetFiles);
            System.out.println("----合并成功-----");
            return createResponseEntity(hbfile,id,true);
        }
    }

@SneakyThrows
    private List<File> gettargetFiles (Long id, Date payDate, Date payDate2){
        //根据id查询缴费单详情列表
        FeeDetail feeDetail=new FeeDetail();
        feeDetail.setPaymentId(id);
        List<String> statusList = new ArrayList<>();
        statusList.add("1");
        statusList.add("2");
        feeDetail.setStatusList(statusList);
        List<FeeDetail> feeDetailslist = feeDetailService.selectFeeDetailList(feeDetail);
        // 假设这是从数据库查询出的多条记录,每条记录是一个Map
        List<Map<String, String>> allReplacements = new ArrayList<>();
        feeDetailslist.forEach(vo->{
            Map<String, String> record = new HashMap<>();
            record.put("{{start}}", DateUtils.parseDateToStr("yyyy年M月d日",vo.getStartDate()));
            record.put("{{end}}",  DateUtils.parseDateToStr("M月d日",vo.getEndDate()));
            record.put("{{shopNo}}", vo.getShopNo());
            record.put("{{feeDetailNo}}", vo.getFeeDetailNo());
            record.put("{{contractArea}}", "0");
            record.put("{{monthlyRent}}", "0");
            record.put("{{rent}}", vo.getRent()==null?"0":vo.getRent().stripTrailingZeros().toPlainString());
            record.put("{{waterAndEle}}", vo.getWaterAndElectricityFee()==null?"0":vo.getWaterAndElectricityFee().stripTrailingZeros().toPlainString());
            record.put("{{airCond}}", vo.getAirConditioningFee()==null?"0":vo.getAirConditioningFee().stripTrailingZeros().toPlainString());
            record.put("{{adFee}}", vo.getAdvertisingFee()==null?"0":vo.getAdvertisingFee().stripTrailingZeros().toPlainString());
            record.put("{{manageFee}}", vo.getManagementFee()==null?"0":vo.getManagementFee().stripTrailingZeros().toPlainString());
            record.put("{{lampFee}}", vo.getLampFee()==null?"0":vo.getLampFee().stripTrailingZeros().toPlainString());
            record.put("{{totalFee}}", vo.getTotalFee()==null?"0":vo.getTotalFee().stripTrailingZeros().toPlainString());
            record.put("{{otherFee}}", vo.getPropertyManagementFee()==null?"0":vo.getPropertyManagementFee().stripTrailingZeros().toPlainString());  //其他费用 物管费
            record.put("{{totalFeebig}}", Convert.digitToChinese(vo.getTotalFee()));
            record.put("{{startdate}}", DateUtils.parseDateToStr("yyyy.M.d",vo.getStartDate()));
            record.put("{{enddate}}", DateUtils.parseDateToStr("yyyy.M.d",vo.getEndDate()));
            record.put("{{paydate}}", DateUtils.parseDateToStr("M月d日",payDate));
            record.put("{{paydate2}}", DateUtils.parseDateToStr("M月d日",payDate2));
            allReplacements.add(record);
        });

        String modelPath =MODELPATH;
        //导出目录不存在则新建
        File folder = new File(outputPath+ File.separator +id);
        if(!folder.exists()){
            folder.mkdirs();
        }
        //需要合并的文档集合
        List<File> targetFiles = new ArrayList<>();
        ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        List<Future<File>> futures = new ArrayList<>();
        for (int i = 0; i < allReplacements.size(); i++) {
            int index = i; // 将 i 的值存储在一个局部变量中
            Map<String, String> replacement = allReplacements.get(i); // 将获取的 Map 存储在局部变量中
            futures.add(executor.submit(() -> {
                XWPFDocument doc = new XWPFDocument(new FileInputStream(modelPath));
                WordWithQRCode.replaceInParagraphs(doc, replacement); //替换占位符
                //生成二维码图片
                String value=replacement.get("{{feeDetailNo}}");
                String params="{\"input_1717459797397\":\""+ value + "\"}";
                String QRCODE=qrcodeFont+ Base64Utilt.encoder64(params)  ;
                String codePath = outputPath + File.separator +id +File.separator+ index + ".png";
                WordWithQRCode.generateQRCode(QRCODE, codePath, 150, 150);
                Map<String, String> imageMap = new HashMap<>();
                imageMap.put("{{qrcode}}", codePath);
                WordWithQRCode.replaceImages(doc, imageMap);
                String outpath = outputPath + File.separator +id +File.separator+ index + ".docx";
                FileOutputStream out = new FileOutputStream(outpath);
                doc.write(out);
                return new File(outpath);
            }));
        }
        // 等待所有文档生成完成
        for (Future<File> future : futures) {
            targetFiles.add(future.get());
        }
        // 关闭线程池
        executor.shutdown();
        System.out.print("----word生成成功-----");
        return targetFiles;
    }

其中 WordWithQRCode 是生成word的主要方法,我将其写成了一个工具类, WordWithQRCode.replaceInParagraphs 方法是替换模板占位符生成word文档,
WordWithQRCode.generateQRCode 方法是生成二维码图片的功能,
WordWithQRCode.replaceImages 方法是将生成的二维码图片插入文档占位符的方法,
我的模板中涉及到金额的中文大小写转换的功能,你不需要就删掉
全部代码如下:

package com.nrx.contract.utils;

import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.nrx.common.utils.StringUtils;
import lombok.SneakyThrows;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;

/**
 * 根据word模板生成 word文档
 * */
public class WordWithQRCode {

    public static void main(String[] args) throws IOException {
        String templatePath = "C:\\Users\\hc\\Desktop\\bushu\\model\\jfd.docx"; // 模板文件路径
        String outputPath = "C:\\Users\\hc\\Desktop\\tx"; // 输出文件路径
        String qrcode="哈哈哈哈,我是二维码";


        // 假设这是从数据库查询出的多条记录,每条记录是一个Map
        List<Map<String, String>> allReplacements = new ArrayList<>();
        Map<String, String> record1 = new HashMap<>();
        record1.put("{{start}}", "2024-05-01");
        record1.put("{{end}}", "2024-05-22");
        record1.put("{{shopNo}}", "001");
        record1.put("{{feeDetailNo}}", "No111");
        record1.put("{{contractArea}}", "111");
        record1.put("{{monthlyRent}}", "211");
        record1.put("{{rent}}", "311");
        record1.put("{{waterAndEle}}", "411");
        record1.put("{{airCond}}", "511");
        record1.put("{{adFee}}", "611");
        record1.put("{{manageFee}}", "711");
        record1.put("{{lampFee}}", "811");
        record1.put("{{totalFee}}", "911");
        record1.put("{{otherFee}}", "0");
        record1.put("{{totalFeebig}}", toChinese("911", true));
        record1.put("{{startdate}}", "2024.05.01");
        record1.put("{{enddate}}", "2024.05.22");
        allReplacements.add(record1);

        Map<String, String> record2 = new HashMap<>();
        record2.put("{{start}}", "2024-05-02");
        record2.put("{{end}}", "2024-05-23");
        record2.put("{{shopNo}}", "002");
        record2.put("{{feeDetailNo}}", "No222");
        record2.put("{{contractArea}}", "112");
        record2.put("{{monthlyRent}}", "212");
        record2.put("{{rent}}", "312");
        record2.put("{{waterAndEle}}", "412");
        record2.put("{{airCond}}", "512");
        record2.put("{{adFee}}", "612");
        record2.put("{{manageFee}}", "712");
        record2.put("{{lampFee}}", "812");
        record2.put("{{totalFee}}", "912");
        record2.put("{{otherFee}}", "0");
        record2.put("{{totalFeebig}}", toChinese("911",true));
        record2.put("{{startdate}}", "2024.05.02");
        record2.put("{{enddate}}", "2024.05.23");
        allReplacements.add(record2);

        //需要合并的文档集合
        List<File> targetFile = new ArrayList<>();
        //生成每个word
        for (int i=0;i<allReplacements.size();i++){
            XWPFDocument doc = new XWPFDocument(new FileInputStream(templatePath));
            // 替换段落
            replaceInParagraphs(doc, allReplacements.get(i));

            //生成二维码
            String codePath=outputPath+File.separator+i+".png";
            generateQRCode(qrcode,codePath,100,100);
            //如果有图片占位符
            Map<String, String> imageMap = new HashMap<>();
            imageMap.put("{{qrcode}}", codePath);
            replaceImages(doc, imageMap);

            //文档输出路径
            String outpath=outputPath+File.separator+i+".docx";
            FileOutputStream out = new FileOutputStream(outpath);
            doc.write(out);
            targetFile.add(new File(outpath));
        }
        System.out.printf("----生成成功-----");
        //合并文档
        //合并后文档路径
        File hbfile = new File(outputPath+File.separator+"output.docx");
        DocxMerge.appendDocx(hbfile, targetFile);
        System.out.printf("----合并成功-----");
        //转pdf文档
        Word2PdfUtil.wordConvertPdfFile(hbfile.getPath(),outputPath+File.separator+"output.pdf");

        System.out.println("Word文档已成功转换为PDF格式。");
    }


    //生成二维码
    public static void generateQRCode(String text, String filePath, int width, int height) {
        try {
            BitMatrix bitMatrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height);

            Path path = FileSystems.getDefault().getPath(filePath);
            MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);
        } catch (WriterException | IOException e) {
            e.printStackTrace();
        }
    }





    public static void replaceInParagraphs(XWPFDocument doc, Map<String, String> params) {
        // 替换文档中的所有段落
        replaceInParagraphs(doc.getParagraphs(), params);
        // 替换表格中的内容(如果有表格)
        replaceInTables(doc.getTables(), params);
        // 替换页眉中的内容(如果有页眉)
        replaceInHeaders(doc.getHeaderList(), params);
        // 替换页脚中的内容(如果有页脚)
        replaceInFooters(doc.getFooterList(), params);
    }
    private static void replaceInParagraphs(List<XWPFParagraph> paragraphs, Map<String, String> replacements) {
        for (XWPFParagraph paragraph : paragraphs) {
            replaceText(paragraph, replacements);
        }
    }

    private static void replaceInTables(List<XWPFTable> tables, Map<String, String> replacements) {
        for (XWPFTable table : tables) {
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    replaceInParagraphs(cell.getParagraphs(), replacements);
                }
            }
        }
    }

    private static void replaceInHeaders(List<XWPFHeader> headers, Map<String, String> replacements) {
        for (XWPFHeader header : headers) {
            replaceInParagraphs(header.getParagraphs(), replacements);
        }
    }

    private static void replaceInFooters(List<XWPFFooter> footers, Map<String, String> replacements) {
        for (XWPFFooter footer : footers) {
            replaceInParagraphs(footer.getParagraphs(), replacements);
        }
    }

    private static void replaceText(XWPFParagraph paragraph, Map<String, String> replacements) {
        StringBuilder fullText = new StringBuilder();
        List<XWPFRun> runs = paragraph.getRuns();

        if (runs != null) {
            for (XWPFRun run : runs) {
                String text = run.getText(0);
                if (text != null) {
                    fullText.append(text);
                }
            }

            // 替换文本中的占位符
            String replacedText = fullText.toString();
            for (Map.Entry<String, String> entry : replacements.entrySet()) {
                replacedText = replacedText.replace(entry.getKey(), entry.getValue());
            }

            // 设置替换后的文本
            for (XWPFRun run : runs) {
                run.setText("", 0); // 清空原有文本
            }
            if (runs.size() > 0) {
                runs.get(0).setText(replacedText, 0);
            }
        }
    }
    @SneakyThrows
    public static void replaceImages(XWPFDocument document, Map<String, String> imageMap) throws IOException {
        for (XWPFTable table : document.getTables()) {
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    for (XWPFParagraph paragraph : cell.getParagraphs()) {
                        for (XWPFRun run : paragraph.getRuns()) {
                            String text = run.getText(0);
                            if (text != null) {
                                for (Map.Entry<String, String> entry : imageMap.entrySet()) {
                                    String placeholder = entry.getKey();
                                    String imagePath = entry.getValue();

                                    if (text.contains(placeholder)) {
                                        // Replace placeholder text with empty string
                                        run.setText(text.replace(placeholder, ""), 0);

                                        // Load and add image
                                        byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
                                        try {
                                            String pictureIndex = document.addPictureData(imageBytes, 5);
                                            // 下面参数5是图片类型,这里是png对应的数字,如果是其它的可以自行百度,
                                            // 200设置的是图片高度,400是图片宽度
                                            run.addPicture(new ByteArrayInputStream(imageBytes), 5, "image.png", Units.toEMU(100), Units.toEMU(100));
                                        } catch (InvalidFormatException e) {
                                            throw new RuntimeException(e);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }


    /**
     * 繁体大写数字************************************************************
     */
    private static final String[] NUMBERS = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"};
    /**
     * 繁体整数部分的单位
     */
    private static final String[] IUNIT = {"元", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿", "拾", "佰", "仟"};
    /**
     * 繁体小数部分的单位
     */
    private static final String[] DUNIT = {"角", "分"};
    /**
     * 简体数字
     */
    private static final String[] CN_NUMBERS = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"};
    /**
     * 简体数字单位
     */
    private static final String[] CN_IUNIT = {"", "十", "百", "千", "万", "十", "百", "千", "亿", "十", "百", "千"};


    /**
     *  转换为大写的中文金额,支持负数
     * @param amount 金额
     * @param isSimplified 是否简体中文:true:简体,false:繁体
     * @return
     */
    public static String toChinese(String amount, boolean isSimplified) {
        // 判断输入的金额字符串是否符合要求
        if (StringUtils.isBlank(amount) || !amount.matches("(-)?[\\d]*(.)?[\\d]*")) {
            throw new RuntimeException("请输入数字");
        }

        if ("0".equals(amount) || "0.00".equals(amount) || "0.0".equals(amount)) {
            return isSimplified ? "零" : "零元";
        }

        // 判断金额数字中是否存在负号"-"
        boolean flag = false;
        if (amount.startsWith("-")) {
            // 标志位,标志此金额数字为负数
            flag = true;
            amount = amount.replaceAll("-", "");
        }
        // 去掉金额数字中的逗号","
        amount = amount.replaceAll(",", "");
        // 初始化:分离整数部分和小数部分
        String[] separateNum = separateNum(amount);
        // 整数部分数字
        String integerStr = separateNum[0];
        // 小数部分数字
        String decimalStr = separateNum[1];
        // beyond超出计算能力,直接返回
        if (integerStr.length() > IUNIT.length) {
            throw new RuntimeException("输入数字超限");
        }
        // 整数部分数字
        int[] integers = toIntArray(integerStr);
        // 判断整数部分是否存在输入012的情况
        if (integers.length > 1 && integers[0] == 0) {
            throw new RuntimeException("输入数字不符合要求");
        }
        // 设置万单位
        boolean isWan = isWan5(integerStr);
        // 小数部分数字
        int[] decimals = toIntArray(decimalStr);
        // 返回最终的大写金额
        String result = "";
        String chineseInteger = getChineseInteger(integers, isWan, isSimplified);
        String chineseDecimal = getChineseDecimal(decimals, isSimplified);
        if (decimals.length > 0 && isSimplified) {
            result = chineseInteger;
            if (!chineseDecimal.equals("零零")) {
                result = result + "点" + chineseDecimal;
            }
        } else {
            result = chineseInteger + chineseDecimal;

        }
        if (flag) {
            // 如果是负数,加上"负"
            return "负" + result;
        } else {
            return result;
        }
    }

    /**
     * 分离整数部分和小数部分
     * @param str
     * @return
     */
    private static String[] separateNum(String str) {
        String integerStr;// 整数部分数字
        String decimalStr;// 小数部分数字
        if (str.indexOf('.') >= 1) {
            integerStr = str.substring(0, str.indexOf('.'));
            decimalStr = str.substring(str.indexOf('.') + 1);
            if (decimalStr.length() > 2) {
                decimalStr = decimalStr.substring(0, 2);
            }
        } else if (str.indexOf('.') == 0) {
            integerStr = "";
            decimalStr = str.substring(1);
        } else {
            integerStr = str;
            decimalStr = "";
        }
        return new String[] {integerStr, decimalStr};
    }

    /**
     *  将字符串转为int数组
     * @param number  数字
     * @return
     */
    private static int[] toIntArray(String number) {
        int[] array = new int[number.length()];
        for (int i = 0; i < number.length(); i++) {
            array[i] = Integer.parseInt(number.substring(i, i + 1));
        }
        return array;
    }

    /**
     *  将整数部分转为大写的金额
     * @param integers 整数部分数字
     * @param isWan  整数部分是否已经是达到【万】
     * @return
     */
    private static String getChineseInteger(int[] integers, boolean isWan, boolean isSimplified) {

        int length = integers.length;
        if (!isSimplified && length == 1 && integers[0] == 0) {
            return "";
        }
        if (!isSimplified) {
            return traditionalChineseInteger(integers, isWan);
        } else {
            return simplifiedChineseInteger(integers, isWan);
        }
    }

    /**
     * 繁体中文整数
     * @param integers
     * @param isWan
     * @return
     */
    private static String traditionalChineseInteger(int[] integers, boolean isWan) {
        StringBuilder chineseInteger = new StringBuilder("");
        int length = integers.length;
        for (int i = 0; i < length; i++) {
            String key = "";
            if (integers[i] == 0) {
                if ((length - i) == 13)// 万(亿)
                    key = IUNIT[4];
                else if ((length - i) == 9) {// 亿
                    key = IUNIT[8];
                } else if ((length - i) == 5 && isWan) {// 万
                    key = IUNIT[4];
                } else if ((length - i) == 1) {// 元
                    key = IUNIT[0];
                }
                if ((length - i) > 1 && integers[i + 1] != 0) {
                    key += NUMBERS[0];
                }
            }
            chineseInteger.append(integers[i] == 0 ? key : (NUMBERS[integers[i]] + IUNIT[length - i - 1]));
        }
        return chineseInteger.toString();
    }

    /**
     * 简体中文整数
     * @param integers
     * @param isWan
     * @return
     */
    private static String simplifiedChineseInteger(int[] integers, boolean isWan) {
        StringBuilder chineseInteger = new StringBuilder("");
        int length = integers.length;
        for (int i = 0; i < length; i++) {
            String key = "";
            if (integers[i] == 0) {
                if ((length - i) == 13) {// 万(亿)
                    key = CN_IUNIT[4];
                } else if ((length - i) == 9) {// 亿
                    key = CN_IUNIT[8];
                } else if ((length - i) == 5 && isWan) {// 万
                    key = CN_IUNIT[4];
                } else if ((length - i) == 1) {// 元
                    key = CN_IUNIT[0];
                }
                if ((length - i) > 1 && integers[i + 1] != 0) {
                    key += CN_NUMBERS[0];
                }
                if (length == 1 && integers[i] == 0) {
                    key += CN_NUMBERS[0];
                }
            }
            chineseInteger.append(integers[i] == 0 ? key : (CN_NUMBERS[integers[i]] + CN_IUNIT[length - i - 1]));
        }
        return chineseInteger.toString();
    }

    /**
     *  将小数部分转为大写的金额
     * @param decimals 小数部分的数字
     * @return
     */
    private static String getChineseDecimal(int[] decimals, boolean isSimplified) {
        StringBuilder chineseDecimal = new StringBuilder("");
        if (!isSimplified) {
            for (int i = 0; i < decimals.length; i++) {
                String key = "";

                if ((decimals.length - i) > 1 && decimals[i + 1] != 0) {
                    key += NUMBERS[0];
                }

                chineseDecimal.append(decimals[i] == 0 ? key : (NUMBERS[decimals[i]] + DUNIT[i]));
            }
        } else {
            for (int i = 0; i < decimals.length; i++) {
                chineseDecimal.append(CN_NUMBERS[decimals[i]]);
            }

        }
        return chineseDecimal.toString();
    }

    /**
     *  判断当前整数部分是否已经是达到【万】
     * @param integerStr  整数部分数字
     * @return
     */
    private static boolean isWan5(String integerStr) {
        int length = integerStr.length();
        if (length > 4) {
            String subInteger = "";
            if (length > 8) {
                subInteger = integerStr.substring(length - 8, length - 4);
            } else {
                subInteger = integerStr.substring(0, length - 4);
            }
            return Integer.parseInt(subInteger) > 0;
        } else {
            return false;
        }
    }
    /**
     *************************************************************
     */

    /**
     * 删除文件夹下文件
     * */
    public static void deleteFile(String path) {
        File directory = new File("path");
        if(directory.listFiles()!=null){
            for (File file: directory.listFiles()) {
                if (!file.isDirectory()) {
                    file.delete();
                }
            }
        }

    }

}

简单说一下exportBatch() 方法,此方法是用于分批次合并word文档的里面的DocxMerge.appendDocx() 才是真正合并word文档的方法,阈值为BATCH_SIZE 如果大于则分批次,不大于则直接执行DocxMerge.appendDocx()方法 如下:

 @SneakyThrows
    private ResponseEntity<FileSystemResource> exportBatch(List<File> targetFiles, Long id, Date payDate, Date payDate2, boolean changePdf) {
        System.out.println("----大于"+BATCH_SIZE+"分批次合并中-----");
        List<List<File>> batches = new ArrayList<>();
        for (int i = 0; i < targetFiles.size(); i += BATCH_SIZE) {
            batches.add(new ArrayList<>(targetFiles.subList(i, Math.min(i + BATCH_SIZE, targetFiles.size()))));
        }

        File finalMergedFile = new File(outputPath + File.separator +id+File.separator+ "final_output.docx");

        List<File> allIntermediateFiles = new ArrayList<>(); // 用于存储所有中间文件

        // 创建第一个中间文件,并将其作为临时最终文件的基础
        List<File> firstBatch = batches.get(0);
        File tempFinalMergedFile = new File(outputPath + File.separator +id+File.separator+ "intermediate_0.docx");
        DocxMerge.appendDocx(tempFinalMergedFile, firstBatch);
        allIntermediateFiles.add(tempFinalMergedFile);

        // 合并剩余的中间文件
        for (int i = 1; i < batches.size(); i++) {
            List<File> batch = batches.get(i);
            File intermediateMergedFile = new File(outputPath + File.separator +id+File.separator+ "intermediate_" + i + ".docx");
            DocxMerge.appendDocx(intermediateMergedFile, batch);
            allIntermediateFiles.add(intermediateMergedFile);
        }

        // 最终合并所有中间文件到最终文件
        DocxMerge.appendDocx(finalMergedFile, allIntermediateFiles);

        // 清理中间文件
        for (File file : allIntermediateFiles) {
            file.delete();
        }

        System.out.println("----分批次合并成功-----");
        return createResponseEntity(finalMergedFile,id, changePdf);
    }

 private ResponseEntity<FileSystemResource> createResponseEntity(File file,Long id,boolean changepdf) {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
        headers.add("Pragma", "no-cache");
        headers.add("Expires", "0");
        headers.add("Last-Modified", new Date().toString());
        headers.add("ETag", String.valueOf(System.currentTimeMillis()));

        if(changepdf){
            //转换为pdf
            File hbfilepdf = new File(outputPath+File.separator+id+File.separator+"output.pdf");
            Word2PdfUtil.wordConvertPdfFile(file.getPath(),hbfilepdf.getPath());
            System.out.println("----转换为pdf成功-----");
            headers.add("Content-Disposition", "attachment; filename=" + UriUtils.encode(hbfilepdf.getName(), "UTF-8"));
            return ResponseEntity
                    .ok()
                    .headers(headers)
                    .contentLength(hbfilepdf.length())
                    .contentType(MediaType.parseMediaType("application/octet-stream"))
                    .body(new FileSystemResource(hbfilepdf));
        }else{
            headers.add("Content-Disposition", "attachment; filename=" + UriUtils.encode(file.getName(), "UTF-8"));
            return ResponseEntity
                    .ok()
                    .headers(headers)
                    .contentLength(file.length())
                    .contentType(MediaType.parseMediaType("application/octet-stream"))
                    .body(new FileSystemResource(file));
        }
    }

DocxMerge.appendDocx 合并word文档的工具方法:此方法很重要,代码如下:

package com.nrx.contract.utils;
import lombok.SneakyThrows;
import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;

/**
 * @Author mischen
 * @Description 文件合并 只支持.docx
 * @Date 2022/11/21 16:30
 * @Version 1.0
 */
public class DocxMerge {
    public static void main(String[] args) {
        File file1 = new File("C:\\Users\\hc\\Desktop\\tx\\output.docx");
        List<File> targetFile1 = new ArrayList<>();
        targetFile1.add(new File("C:\\Users\\hc\\Desktop\\tx\\0.docx"));
        targetFile1.add(new File("C:\\Users\\hc\\Desktop\\tx\\1.docx"));

        appendDocx(file1, targetFile1);
        System.out.println("合并成功!!!");
    }

    /**
     * 把多个docx文件合并成一个
     *
     * @param outfile    输出文件
     * @param targetFile 目标文件
     */
   
    /**
     * 把多个docx文件合并成一个
     *
     * @param outfile    输出文件
     * @param targetFile 目标文件
     */
   public static void appendDocx(File outfile, List<File> targetFile) {
        try {

            OutputStream dest = new FileOutputStream(outfile);
            ArrayList<XWPFDocument> documentList = new ArrayList<>();

            for (int i = 0; i < targetFile.size(); i++) {
                ZipSecureFile.setMinInflateRatio(-1.0d);
                FileInputStream in = new FileInputStream(targetFile.get(i).getPath());
                OPCPackage open = OPCPackage.open(in);
                XWPFDocument document = new XWPFDocument(open);
                documentList.add(document);
                in.close();
            }
            //取出第一个用作基础
            XWPFDocument doc =new XWPFDocument();
            if(!documentList.isEmpty()){
                doc= documentList.get(0);
                for (int i = 1; i < documentList.size(); i++) {
                    //解决word合并完后,所有表格都紧紧挨在一起,没有分页。加上了分页符可解决
                    //  insertPageBreak(documentList.get(i));
                    //在第一个后面追加
                    appendBody(doc, documentList.get(i));
                    System.out.println("----合并中---已处理"+i);
                }
            }

            doc.write(dest);
            dest.close();
            doc.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void insertPageBreak(XWPFDocument document) {
        XWPFParagraph paragraph = document.createParagraph();
        XWPFRun run = paragraph.createRun();
        run.addBreak(BreakType.PAGE);
    }


    public static void appendBody(XWPFDocument src, XWPFDocument append) throws Exception {
        CTBody src1Body = src.getDocument().getBody();
        CTBody src2Body = append.getDocument().getBody();

        List<XWPFPictureData> allPictures = append.getAllPictures();
        // 记录图片合并前及合并后的ID
        Map<String, String> map = new HashMap<>();
        for (XWPFPictureData picture : allPictures) {
            String before = append.getRelationId(picture);
            //将原文档中的图片加入到目标文档中
            String after = src.addPictureData(picture.getData(), Document.PICTURE_TYPE_PNG);
            map.put(before, after);
        }
        //这个代码主要解决合并word报错,解析抛出压缩炸弹
        ZipSecureFile.setMinInflateRatio(-1.0d);
        appendBody(src1Body, src2Body, map);

    }

    private static void appendBody(CTBody src, CTBody append, Map<String, String> map) throws Exception {
        XmlOptions optionsOuter = new XmlOptions();
        optionsOuter.setSaveOuter();
        String appendString = append.xmlText(optionsOuter);

        String srcString = src.xmlText();
        String prefix = srcString.substring(0, srcString.indexOf(">") + 1);
        String mainPart = srcString.substring(srcString.indexOf(">") + 1, srcString.lastIndexOf("<"));
        String sufix = srcString.substring(srcString.lastIndexOf("<"));
        String addPart = appendString.substring(appendString.indexOf(">") + 1, appendString.lastIndexOf("<"));
        //下面这部分可以去掉,我加上的原因是合并的时候,有时候出现打不开的情况,对照document.xml将某些标签去掉就可以正常打开了
        addPart = addPart.replaceAll("w14:paraId=\"[A-Za-z0-9]{1,10}\"", "");
        addPart = addPart.replaceAll("w14:textId=\"[A-Za-z0-9]{1,10}\"", "");
        addPart = addPart.replaceAll("w:rsidP=\"[A-Za-z0-9]{1,10}\"", "");
        addPart = addPart.replaceAll("w:rsidRPr=\"[A-Za-z0-9]{1,10}\"", "");
        addPart = addPart.replace("<w:headerReference r:id=\"rId8\" w:type=\"default\"/>","");
        addPart = addPart.replace("<w:footerReference r:id=\"rId9\" w:type=\"default\"/>","");
        addPart = addPart.replace("xsi:nil=\"true\"","");

        if (map != null && !map.isEmpty()) {
            //对xml字符串中图片ID进行替换
            for (Map.Entry<String, String> set : map.entrySet()) {
                addPart = addPart.replace(set.getKey(), set.getValue());
            }
        }
        //将两个文档的xml内容进行拼接
        XmlObject makeBody = CTBody.Factory.parse(prefix + mainPart + addPart + sufix);

        src.set(makeBody);
    }
}

Word2PdfUtil.wordConvertPdfFile 是word转换为pdf文档的工具类方法

package com.nrx.contract.utils;

import com.aspose.words.Document;
import com.aspose.words.License;
import com.aspose.words.SaveFormat;

import java.io.*;

/**
 * word转pdf工具类
 *
 * @author shmily
 */
public class Word2PdfUtil {

    /**
     * 许可证字符串(可以放到resource下的xml文件中也可)
     */
    private static final String LICENSE = "<License>" +
            "<Data>" +
            "<Products><Product>Aspose.Total for Java</Product><Product>Aspose.Words for Java</Product></Products>" +
            "<EditionType>Enterprise</EditionType>" +
            "<SubscriptionExpiry>20991231</SubscriptionExpiry>" +
            "<LicenseExpiry>20991231</LicenseExpiry>" +
            "<SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>" +
            "</Data>" +
            "<Signature>sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=</Signature>" +
            "</License>";


    /**
     * 设置 license 去除水印
     */
    private static void setLicense() {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(LICENSE.getBytes());
        License license = new License();
        try {
            license.setLicense(byteArrayInputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * word 转 pdf 生成至指定路径,pdf为空则上传至word同级目录
     *
     * @param wordPath word文件路径
     * @param pdfPath  pdf文件路径
     */
    public static void wordConvertPdfFile(String wordPath, String pdfPath) {
        FileOutputStream fileOutputStream = null;
        try {
            pdfPath = pdfPath == null ? getPdfFilePath(wordPath) : pdfPath;
            setLicense();
            File file = new File(pdfPath);
            fileOutputStream = new FileOutputStream(file);
            Document doc = new Document(wordPath);
            doc.save(fileOutputStream, SaveFormat.PDF);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                assert fileOutputStream != null;
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }


    /**
     * word 转 pdf 生成byte字节流
     *
     * @param wordPath word所在的目录地址
     * @return
     */
    public static byte[] wordConvertPdfByte(String wordPath) {
        ByteArrayOutputStream fileOutputStream = null;
        try {
            setLicense();
            fileOutputStream = new ByteArrayOutputStream();
            Document doc = new Document(wordPath);
            doc.save(fileOutputStream, SaveFormat.PDF);
            return fileOutputStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                assert fileOutputStream != null;
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return null;
    }



    /**
     * 获取 生成的 pdf 文件路径,默认与源文件同一目录
     *
     * @param wordPath word文件
     * @return 生成的 pdf 文件
     */
    private static String getPdfFilePath(String wordPath) {
        int lastIndexOfPoint = wordPath.lastIndexOf(".");
        String pdfFilePath = "";
        if (lastIndexOfPoint > -1) {
            pdfFilePath = wordPath.substring(0, lastIndexOfPoint);
        }
        return pdfFilePath + ".pdf";
    }

}

后面就不一一说明了,大家可以直接看资源:https://download.****.net/download/weixin_43832166/89893033