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

需求是要求查询数据库多条明细数据,将多条数据根据定好的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.csdn.net/download/weixin_43832166/89893033

相关推荐
弗拉唐32 分钟前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
CodeCraft Studio1 小时前
【实用技能】使用 TX Text Control 创建带有嵌入式附件的 PDF 文档
pdf·asp.net·.net
oi771 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
少说多做3431 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀1 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
蓝黑20202 小时前
IntelliJ IDEA常用快捷键
java·ide·intellij-idea
Ysjt | 深2 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
shuangrenlong2 小时前
slice介绍slice查看器
java·ubuntu
牧竹子2 小时前
对原jar包解压后修改原class文件后重新打包为jar
java·jar