需求是要求查询数据库多条明细数据,将多条数据根据定好的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