|
@@ -0,0 +1,322 @@
|
|
|
+package com.rongwei.zhsw.system.service.impl;
|
|
|
+
|
|
|
+import com.rongwei.rwcommon.utils.StringUtils;
|
|
|
+import com.rongwei.rwcommoncomponent.file.service.SysFileItemService;
|
|
|
+import com.rongwei.rwcommonentity.commonservers.domain.SysFileItemDo;
|
|
|
+import com.rongwei.zhsw.system.service.SwCollectionNoticeService;
|
|
|
+import org.apache.poi.util.IOUtils;
|
|
|
+import org.apache.poi.util.Units;
|
|
|
+import org.apache.poi.xwpf.usermodel.*;
|
|
|
+import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D;
|
|
|
+import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.*;
|
|
|
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDrawing;
|
|
|
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+// 必须导入的类(POI 4.1.1)
|
|
|
+import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTPosH;
|
|
|
+import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTPosV;
|
|
|
+import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STRelFromH;
|
|
|
+import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STRelFromV;
|
|
|
+import javax.servlet.http.HttpServletResponse;
|
|
|
+import java.io.*;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class SwCollectionNoticeServiceImpl implements SwCollectionNoticeService {
|
|
|
+ @Autowired
|
|
|
+ private SysFileItemService sysFileItemService;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void printCollectionBill(List<Map<String, String>> userList,HttpServletResponse response) {
|
|
|
+
|
|
|
+
|
|
|
+ // 1. 获取模板文件
|
|
|
+ final String templateId = "eec8b33cc8db4ed3b04a632f6c6f1e6f";
|
|
|
+ SysFileItemDo templateFile = sysFileItemService.getById(templateId);
|
|
|
+ if (templateFile == null) {
|
|
|
+ throw new RuntimeException("模板文件未找到");
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 验证模板路径
|
|
|
+ String templatePath = templateFile.getFullpath();
|
|
|
+ if (StringUtils.isBlank(templatePath)) {
|
|
|
+ throw new RuntimeException("模板文件路径为空");
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ File templateFileObj = new File(templatePath);
|
|
|
+ if (!templateFileObj.exists() || !templateFileObj.isFile()) {
|
|
|
+ throw new RuntimeException("模板文件不存在");
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 预加载模板到内存(提升性能)
|
|
|
+ byte[] templateBytes;
|
|
|
+ try (FileInputStream fis = new FileInputStream(templateFileObj)) {
|
|
|
+ templateBytes = IOUtils.toByteArray(fis);
|
|
|
+ } catch (IOException e) {
|
|
|
+
|
|
|
+ throw new RuntimeException("模板加载失败");
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 创建输出目录
|
|
|
+ String outputDirectory = "output" + File.separator;
|
|
|
+ File dir = new File(outputDirectory);
|
|
|
+ if (!dir.exists() && !dir.mkdirs()) {
|
|
|
+
|
|
|
+ throw new RuntimeException("目录创建失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 生成合并文档
|
|
|
+ String outputFileName = outputDirectory + "催收单合并文档_" + System.currentTimeMillis() + ".docx";
|
|
|
+ String pdfFilePath = outputFileName.replace(".docx", ".pdf");
|
|
|
+ try (XWPFDocument mergedDocument = new XWPFDocument();
|
|
|
+ FileOutputStream out = new FileOutputStream(outputFileName)) {
|
|
|
+
|
|
|
+// 预加载模板图片到合并文档(避免重复复制)
|
|
|
+ try (InputStream is = new ByteArrayInputStream(templateBytes);
|
|
|
+ XWPFDocument templateDoc = new XWPFDocument(is)) {
|
|
|
+ copyPictures(templateDoc, mergedDocument);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (int i = 0; i < userList.size(); i++) {
|
|
|
+ Map<String, String> user = userList.get(i);
|
|
|
+ try (InputStream is = new ByteArrayInputStream(templateBytes);
|
|
|
+ XWPFDocument document = new XWPFDocument(is)) {
|
|
|
+
|
|
|
+ replaceTextSafely(document, "${USERNUMBER}", user.get("USERNUMBER"));
|
|
|
+ replaceTextSafely(document, "${USERNAME}", user.get("USERNAME"));
|
|
|
+ replaceTextSafely(document, "${YEAR}", user.get("YEAR"));
|
|
|
+// 遍历源文档中的段落
|
|
|
+ for (XWPFParagraph srcPara : document.getParagraphs()) {
|
|
|
+ XWPFParagraph newPara = mergedDocument.createParagraph();
|
|
|
+
|
|
|
+ copyParagraphStyle(srcPara,newPara);
|
|
|
+
|
|
|
+
|
|
|
+ for (XWPFRun srcRun : srcPara.getRuns()) {
|
|
|
+ if (!srcRun.getEmbeddedPictures().isEmpty()) {
|
|
|
+ for (XWPFPicture srcPic : srcRun.getEmbeddedPictures()) {
|
|
|
+ XWPFPictureData picData = srcPic.getPictureData();
|
|
|
+ byte[] data = picData.getData();
|
|
|
+ int picType = picData.getPictureType();
|
|
|
+ XWPFRun newRun = newPara.createRun();
|
|
|
+
|
|
|
+ // 插入浮动式图片(关键修改)
|
|
|
+ int widthEMU = Units.toEMU(200);
|
|
|
+ int heightEMU = Units.toEMU(110);
|
|
|
+ newRun.addPicture(new ByteArrayInputStream(data), picType, "image", widthEMU, heightEMU);
|
|
|
+
|
|
|
+ // 强制设置为浮动式并配置环绕
|
|
|
+ CTR ctr = newRun.getCTR();
|
|
|
+ CTDrawing drawing = ctr.getDrawingArray(0);
|
|
|
+ CTInline inline = drawing.getInlineArray(0);
|
|
|
+ CTAnchor anchor = convertInlineToAnchor(inline);
|
|
|
+ drawing.setAnchorArray(new CTAnchor[]{anchor});
|
|
|
+ configureAnchor(anchor, newPara);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ // 复制普通文本
|
|
|
+ XWPFRun newRun = newPara.createRun();
|
|
|
+ String text = srcRun.getText(0);
|
|
|
+ if (text != null) {
|
|
|
+ newRun.setText(text, 0);
|
|
|
+ }
|
|
|
+ copyRunStyle(srcRun, newRun);
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ // 分页符
|
|
|
+ if (i < userList.size() - 1) {
|
|
|
+ mergedDocument.createParagraph().createRun().addBreak(BreakType.PAGE);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ mergedDocument.write(out);
|
|
|
+ } catch (Exception e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ throw new RuntimeException("生成文件失败", e);
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ com.aspose.words.Document doc = new com.aspose.words.Document(outputFileName);
|
|
|
+ doc.save(pdfFilePath, com.aspose.words.SaveFormat.PDF);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("Word 转 PDF 失败");
|
|
|
+ }
|
|
|
+ File pdfFile = new File(pdfFilePath);
|
|
|
+ if (!pdfFile.exists()) {
|
|
|
+ throw new RuntimeException("PDF 文件未生成");
|
|
|
+ }
|
|
|
+ try (FileInputStream fis = new FileInputStream(pdfFile);
|
|
|
+ OutputStream os = response.getOutputStream()) {
|
|
|
+
|
|
|
+ response.setContentType("application/pdf");
|
|
|
+ response.setHeader("Content-Disposition", "inline; filename=催收单.pdf");
|
|
|
+ IOUtils.copy(fis, os);
|
|
|
+ os.flush();
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new RuntimeException("PDF 预览失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void setPictureToTightWrapLeft(XWPFRun run, XWPFParagraph paragraph) {
|
|
|
+ CTR ctr = run.getCTR();
|
|
|
+ if (ctr.sizeOfDrawingArray() > 0) {
|
|
|
+ CTDrawing drawing = ctr.getDrawingArray(0);
|
|
|
+
|
|
|
+ // 处理浮动式图片(CTAnchor)
|
|
|
+ if (drawing.sizeOfAnchorArray() > 0) {
|
|
|
+ CTAnchor anchor = drawing.getAnchorArray(0);
|
|
|
+ configureAnchor(anchor, paragraph);
|
|
|
+ }
|
|
|
+ // 处理嵌入式图片(CTInline)转换为浮动式
|
|
|
+ else if (drawing.sizeOfInlineArray() > 0) {
|
|
|
+ CTInline inline = drawing.getInlineArray(0);
|
|
|
+ CTAnchor anchor = convertInlineToAnchor(inline);
|
|
|
+ drawing.setAnchorArray(new CTAnchor[]{anchor});
|
|
|
+ configureAnchor(anchor, paragraph);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ private void configureAnchor(CTAnchor anchor, XWPFParagraph paragraph) {
|
|
|
+ // 尺寸
|
|
|
+ CTPositiveSize2D extent = anchor.getExtent();
|
|
|
+ extent.setCx(Units.toEMU(200));
|
|
|
+ extent.setCy(Units.toEMU(110));
|
|
|
+
|
|
|
+ // 水平位置(相对于段落)
|
|
|
+ CTPosH posH = anchor.addNewPositionH();
|
|
|
+ posH.setRelativeFrom(STRelFromH.Enum.forString("character")); // 字符串匹配
|
|
|
+ posH.setPosOffset(Units.toEMU(0));
|
|
|
+
|
|
|
+
|
|
|
+ CTPosV posV = anchor.addNewPositionV();
|
|
|
+ posV.setRelativeFrom(STRelFromV.Enum.forString("line"));
|
|
|
+ posV.setPosOffset(Units.toEMU(0));
|
|
|
+
|
|
|
+ // 环绕
|
|
|
+ anchor.addNewWrapTight();
|
|
|
+
|
|
|
+ // 间距
|
|
|
+ anchor.setDistT(0);
|
|
|
+ anchor.setDistB(0);
|
|
|
+ anchor.setDistL(0);
|
|
|
+ anchor.setDistR(Units.toEMU(10));
|
|
|
+
|
|
|
+ // 层级
|
|
|
+ anchor.setBehindDoc(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 嵌入式转浮动式(保持不变)
|
|
|
+ private CTAnchor convertInlineToAnchor(CTInline inline) {
|
|
|
+ CTAnchor anchor = CTAnchor.Factory.newInstance();
|
|
|
+ anchor.setDocPr(inline.getDocPr());
|
|
|
+ anchor.setGraphic(inline.getGraphic());
|
|
|
+ anchor.setExtent(inline.getExtent());
|
|
|
+ anchor.setDistT(0);
|
|
|
+ anchor.setDistB(0);
|
|
|
+ anchor.setDistL(0);
|
|
|
+ anchor.setDistR(0);
|
|
|
+ anchor.setLocked(false);
|
|
|
+ anchor.setRelativeHeight(0);
|
|
|
+ return anchor;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安全替换文本(跳过含图片的段落)
|
|
|
+ */
|
|
|
+ private void replaceTextSafely(XWPFDocument document, String placeholder, String replacement) {
|
|
|
+ if (document == null) {
|
|
|
+ throw new IllegalArgumentException("Document cannot be null.");
|
|
|
+ }
|
|
|
+ if (placeholder == null || placeholder.isEmpty()) {
|
|
|
+ throw new IllegalArgumentException("Placeholder cannot be null or empty.");
|
|
|
+ }
|
|
|
+ if (replacement == null) {
|
|
|
+ throw new IllegalArgumentException("Replacement cannot be null.");
|
|
|
+ }
|
|
|
+ for (XWPFParagraph para : document.getParagraphs()) {
|
|
|
+ // 跳过包含图片的段落(保护布局)
|
|
|
+ if (para.getRuns().stream().anyMatch(run -> !run.getEmbeddedPictures().isEmpty())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 手动实现文本替换
|
|
|
+ String fullText = para.getText(); // 获取段落全文
|
|
|
+ if (fullText != null && fullText.contains(placeholder)) {
|
|
|
+ // 清空原有 Runs
|
|
|
+ List<XWPFRun> runs = para.getRuns();
|
|
|
+ for (XWPFRun run : runs) {
|
|
|
+ run.setText("", 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新 Run 并设置替换后的文本
|
|
|
+ XWPFRun newRun = para.createRun();
|
|
|
+ newRun.setText(fullText.replace(placeholder, replacement));
|
|
|
+ copyRunStyle(runs.get(0), newRun); // 继承第一个 Run 的样式
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void copyRunStyle(XWPFRun source, XWPFRun target) {
|
|
|
+ target.setColor(source.getColor());
|
|
|
+ target.setBold(source.isBold());
|
|
|
+ target.setItalic(source.isItalic());
|
|
|
+ target.setFontFamily(source.getFontFamily());
|
|
|
+ target.setFontSize(source.getFontSize());
|
|
|
+ target.setUnderline(source.getUnderline());
|
|
|
+ target.setStrikeThrough(source.isStrikeThrough());
|
|
|
+ target.setSubscript(source.getSubscript());
|
|
|
+ target.setTextPosition(source.getTextPosition());
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void copyParagraphStyle(XWPFParagraph srcPara, XWPFParagraph newPara) {
|
|
|
+ // 仅复制段落级样式(对齐、缩进、间距等)
|
|
|
+ newPara.setAlignment(srcPara.getAlignment());
|
|
|
+ newPara.setVerticalAlignment(srcPara.getVerticalAlignment());
|
|
|
+ newPara.setSpacingAfter(srcPara.getSpacingAfter());
|
|
|
+ newPara.setSpacingBefore(srcPara.getSpacingBefore());
|
|
|
+ newPara.setIndentationFirstLine(srcPara.getIndentationFirstLine());
|
|
|
+ newPara.setIndentationLeft(srcPara.getIndentationLeft());
|
|
|
+ newPara.setIndentationRight(srcPara.getIndentationRight());
|
|
|
+ if (srcPara.getStyleID() != null) newPara.setStyle(srcPara.getStyleID());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void copyPictures(XWPFDocument sourceDoc, XWPFDocument targetDoc) {
|
|
|
+ for (XWPFPictureData sourcePic : sourceDoc.getAllPictures()) {
|
|
|
+ try {
|
|
|
+ byte[] data = sourcePic.getData();
|
|
|
+ int picType = sourcePic.getPictureType();
|
|
|
+
|
|
|
+ // 直接添加图片并自动管理引用
|
|
|
+ targetDoc.addPictureData(data, picType);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("图片复制失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|