Browse Source

Merge branch 'build-20250616-createPdf'

# Conflicts:
#	easier-report-biz/src/main/java/com/yaoyicloud/controller/CsoReportController.java
#	easier-report-biz/src/main/java/com/yaoyicloud/render/AntiBriberyRender.java
#	easier-report-biz/src/main/java/com/yaoyicloud/service/CsoReportService.java
#	easier-report-biz/src/main/java/com/yaoyicloud/service/impl/CsoReportServiceImpl.java
#	easier-report-biz/src/main/java/com/yaoyicloud/tools/OfficeUtil.java
#	easier-report-biz/src/main/resources/application.yml
#	easier-report-biz/src/test/java/com/yaoyicloud/render/test/TestPdf.java
mamingxu 2 days ago
parent
commit
921b6e647a

+ 59 - 36
easier-report-biz/src/main/java/com/yaoyicloud/controller/CsoReportController.java

@@ -7,14 +7,23 @@ import com.yaoyicloud.config.CommonDataCache;
 import com.yaoyicloud.config.RelationCounterRedisUtil;
 import com.yaoyicloud.entity.ReportGenerationResult;
 import com.yaoyicloud.service.CsoReportService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import com.yaoyicloud.dto.ReportDTO;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 
+import java.io.ByteArrayOutputStream;
+
+
 import static com.yaoyicloud.config.SessionInterceptor.SESSION_MAP;
 
 
@@ -29,44 +38,58 @@ import static com.yaoyicloud.config.SessionInterceptor.SESSION_MAP;
 @Slf4j
 public class CsoReportController {
     private final CsoReportService csoReportService;
-    private final CommonDataCache commonDataCache;
-    private final RelationCounterRedisUtil relationCounterRedisUtil;
-    /**
-     * 创建Plus版本审核报告
-     *
-     * @param resource 请求参数
-     * @return {@link Boolean } 结果
-     */
-    @PostMapping("/report/create-report-cso")
-    public Map<String, Object>
-    createPlusVersionCheckReport(@Validated @RequestBody ReportDTO.CsoReport resource,
-                                 HttpServletRequest request) throws Exception {
+	/**
+	 * 创建Plus版本审核报告
+	 *
+	 * @param resource 请求参数
+	 * @return {@link Boolean } 结果
+	 */
+	@PostMapping("/report/create-report-cso")
+	public Map<String, Object>
+	createPlusVersionCheckReport(@Validated @RequestBody ReportDTO.CsoReport resource,
+	                             HttpServletRequest request) throws Exception {
+
+		String relationId = request.getHeader("relationId");
+		Boolean mergeflag = resource.getMergeflag();
+		synchronized (this) {
+			ReportGenerationResult plusVersionCheckReport = csoReportService.createCsoCheckReport(
+					resource.getData(),
+					Long.valueOf(relationId),
+					resource.getModuleType(),
+					mergeflag
+			);
+			String sessionId = SESSION_MAP.get(relationId);
+
+			// 3. 构建响应
+			Map<String, Object> response = new HashMap<>();
+			// 根据是否是最后一个模块决定返回哪个路径
+			if (mergeflag) {
+				response.put("reportResult", plusVersionCheckReport.getMergedReportPath());
+				commonDataCache.removeSessionData(relationId);
+				relationCounterRedisUtil.delete(Long.valueOf(relationId));
+			} else {
+				response.put("reportResult", plusVersionCheckReport.getModulePath());
+
+			}
+
+			response.put("sessionId", sessionId);
+			return response;
+		}
+	}
 
-        String relationId = request.getHeader("relationId");
-        Boolean mergeflag = resource.getMergeflag();
-        synchronized (this) {
-            ReportGenerationResult plusVersionCheckReport = csoReportService.createCsoCheckReport(
-                    resource.getData(),
-                    Long.valueOf(relationId),
-                    resource.getModuleType(),
-                    mergeflag
-            );
-            String sessionId = SESSION_MAP.get(relationId);
 
-            // 3. 构建响应
-            Map<String, Object> response = new HashMap<>();
-            // 根据是否是最后一个模块决定返回哪个路径
-            if (mergeflag) {
-                response.put("reportResult", plusVersionCheckReport.getMergedReportPath());
-                commonDataCache.removeSessionData(relationId);
-                relationCounterRedisUtil.delete(Long.valueOf(relationId));
-            } else {
-                response.put("reportResult", plusVersionCheckReport.getModulePath());
+	@PostMapping(value = "/report/create-report", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+	public ResponseEntity<byte[]> handleReport(@RequestBody byte[] fileBytes) throws Exception {
+		// 1. 直接获取PDF字节流
+		ByteArrayOutputStream pdfStream = csoReportService.saveToTempFile(fileBytes);
 
-            }
+		// 2. 转换为字节数组
+		byte[] pdfBytes = pdfStream.toByteArray();
 
-            response.put("sessionId", sessionId);
-            return response;
-        }
-    }
+		// 3. 直接返回PDF字节流
+		return ResponseEntity.ok()
+				.contentType(MediaType.APPLICATION_PDF)  // 关键:设置为PDF类型
+				.header("Content-Disposition", "attachment; filename=report.pdf")  // 设置下载文件名
+				.body(pdfBytes);
+	}
 }

+ 0 - 0
easier-report-biz/src/main/java/com/yaoyicloud/render/AntiBriberyRender.java


+ 64 - 0
easier-report-biz/src/main/java/com/yaoyicloud/service/impl/CsoReportServiceImpl.java

@@ -25,11 +25,22 @@ import com.yaoyicloud.entity.ReportGenerationResult;
 import com.yaoyicloud.render.cso.EntHeaderSectionRender;
 import com.yaoyicloud.render.cso.EntPromotionSummaryRender;
 import com.yaoyicloud.service.CsoReportService;
+import com.yaoyicloud.tools.OfficeUtil1;
 import com.yaoyicloud.tools.DocxUtil;
 
 import cn.hutool.core.util.IdUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.List;
 
 @Slf4j
 @Service
@@ -144,4 +155,57 @@ public class CsoReportServiceImpl implements CsoReportService {
 
         return new ReportGenerationResult(reportPath, mergedReportPath, mergeflag);
     }
+	@Override
+	public ByteArrayOutputStream saveToTempFile(byte[] fileBytes) throws Exception {
+		// 1. 初始化路径和字体配置
+		String imagePath = filerepoProperties.getReportImagePath();
+		List<String> fontLists = Arrays.asList(filerepoProperties.getSourceHanSansCnFontMediumPath());
+		// 2. 创建临时Word文件(自动清理)
+		File reportTempWordFile = null;
+		try {
+			reportTempWordFile = File.createTempFile("temp_word_", ".docx");
+			Files.write(reportTempWordFile.toPath(), fileBytes);
+
+			String html = OfficeUtil1.convert(reportTempWordFile.getAbsolutePath(), imagePath);
+			try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("1.html"))) {
+				writer2.write(html);
+			} catch (IOException e) {
+				System.err.println("写入 1.html 文件时发生错误: " + e.getMessage());
+				e.printStackTrace();
+			}
+			String html1 = OfficeUtil1.formatHtmlCso(html);
+			try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("2.html"))) {
+				writer2.write(html1);
+			} catch (IOException e) {
+				System.err.println("写入 2.html 文件时发生错误: " + e.getMessage());
+				e.printStackTrace();
+			}
+			OfficeUtil1.convertHtmlToPdfCso(html1, fontLists, imagePath, true);
+			String html2 = OfficeUtil1.formatHtml(html);
+
+			try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("3.html"))) {
+				writer2.write(html2);
+			} catch (IOException e) {
+				System.err.println("写入 3.html 文件时发生错误: " + e.getMessage());
+				e.printStackTrace();
+			}
+			return OfficeUtil1.convertHtmlToPdfCso(html2, fontLists, imagePath, false);
+		} catch (Exception e) {
+			log.error("文件转换失败", e);
+			throw e;
+		}
+	}
+
+
+	private void quietlyDelete(File file) {
+		if (file != null && file.exists()) {
+			try {
+				Files.delete(file.toPath());
+			} catch (IOException e) {
+				log.warn("无法删除临时文件 {}: {}", file.getAbsolutePath(), e.getMessage());
+			}
+		}
+	}
+
+
 }

+ 509 - 32
easier-report-biz/src/main/java/com/yaoyicloud/tools/OfficeUtil.java

@@ -1,6 +1,30 @@
 package com.yaoyicloud.tools;
-
+import com.lowagie.text.Image;
+import com.lowagie.text.PageSize;
+import com.lowagie.text.pdf.BaseFont;
+import com.lowagie.text.pdf.PdfContentByte;
+import com.lowagie.text.pdf.PdfReader;
+import com.lowagie.text.pdf.PdfStamper;
+import com.lowagie.text.pdf.parser.PdfTextExtractor;
+import fr.opensagres.poi.xwpf.converter.core.FileURIResolver;
+import fr.opensagres.poi.xwpf.converter.core.ImageManager;
+import fr.opensagres.poi.xwpf.converter.xhtml.XHTMLConverter;
+import fr.opensagres.poi.xwpf.converter.xhtml.XHTMLOptions;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.apache.poi.xwpf.usermodel.XWPFParagraph;
+import org.apache.poi.xwpf.usermodel.XWPFRun;
+import org.apache.poi.xwpf.usermodel.XWPFTable;
+import org.apache.poi.xwpf.usermodel.XWPFTableCell;
+import org.apache.poi.xwpf.usermodel.XWPFTableRow;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Entities;
+import org.jsoup.select.Elements;
+import org.xhtmlrenderer.pdf.ITextFontResolver;
+import org.xhtmlrenderer.pdf.ITextRenderer;
 import java.awt.Color;
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
@@ -19,39 +43,14 @@ import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import org.apache.poi.xwpf.usermodel.XWPFDocument;
-import org.apache.poi.xwpf.usermodel.XWPFParagraph;
-import org.apache.poi.xwpf.usermodel.XWPFRun;
-import org.apache.poi.xwpf.usermodel.XWPFTable;
-import org.apache.poi.xwpf.usermodel.XWPFTableCell;
-import org.apache.poi.xwpf.usermodel.XWPFTableRow;
-import org.jsoup.Jsoup;
-import org.jsoup.nodes.Document;
-import org.jsoup.nodes.Element;
-import org.jsoup.nodes.Entities;
-import org.jsoup.select.Elements;
-import org.xhtmlrenderer.pdf.ITextFontResolver;
-import org.xhtmlrenderer.pdf.ITextRenderer;
-
-import com.lowagie.text.Image;
-import com.lowagie.text.PageSize;
-import com.lowagie.text.pdf.BaseFont;
-import com.lowagie.text.pdf.PdfContentByte;
-import com.lowagie.text.pdf.PdfReader;
-import com.lowagie.text.pdf.PdfStamper;
-import com.lowagie.text.pdf.parser.PdfTextExtractor;
-
-import fr.opensagres.poi.xwpf.converter.core.FileURIResolver;
-import fr.opensagres.poi.xwpf.converter.core.ImageManager;
-import fr.opensagres.poi.xwpf.converter.xhtml.XHTMLConverter;
-import fr.opensagres.poi.xwpf.converter.xhtml.XHTMLOptions;
-
-public class OfficeUtil {
+public class OfficeUtil1 {
+    private static final org.slf4j.Logger OFFICE_UTIL_LOGGER = org.slf4j.LoggerFactory.getLogger(OfficeUtil1.class);
     private static Map<String, Integer> pageNumberMap = new LinkedHashMap<>();
 
+
     /**
      * 把word 转为html
-     * 
+     *
      * @param docxPath word文档路径
      * @param imageDir 图片文件夹路径
      * @return
@@ -477,7 +476,7 @@ public class OfficeUtil {
 
     /**
      * 将 HTML 内容转换为 PDF 文件
-     * 
+     *
      * @param html 要转换的 HTML 字符串内容
      * @param outputPdfPath 生成 PDF 文件的输出路径
      * @param fontPaths 字体文件路径列表,用于解决 PDF 中文显示等字体问题
@@ -541,7 +540,7 @@ public class OfficeUtil {
 
     /**
      * 操作已生成的pdf
-     * 
+     *
      * @param inputPdfPath 输入pdf
      * @param outputPdfPath 输出pdf
      * @param backgroundImagePath 图片文件夹位置
@@ -669,4 +668,482 @@ public class OfficeUtil {
         stamper.close();
         reader.close();
     }
+
+//    private static boolean isTableNearBottom(PdfWriter writer, PdfPTable table, float bottom) {
+//        try {
+//            // 获取当前页面的剩余高度
+//            float remainingHeight = writer.getVerticalPosition(true) - bottom;
+//
+//            // 估算当前行高度
+//            float estimatedRowHeight = 30f;
+//            float estimatedTableHeight = table.getRows().size() * estimatedRowHeight;
+//
+//            // 如果剩余空间不足以容纳整个表格,则换页
+//            return remainingHeight < estimatedTableHeight;
+//        } catch (Exception e) {
+//            e.printStackTrace();
+//            return false;
+//        }
+//    }
+public static String formatHtmlCso(String html) {
+	Document doc = Jsoup.parse(html);
+	String baseCss =
+			"@page {"
+					+ "  size: A4;"
+					+ "  @bottom-center {"
+					+ "    content: none;"  // 只显示数字页码
+					+ "  }"
+					+ "  @top-left { content: element(pageHeader); }"
+					+ "}"
+					+ "@page :first {"
+					+ "  @top-left { content: none; }"  // 第一页不显示页眉
+					+ "}"
+					+ "@page show-page-number {"
+					+ "  @bottom-center {"
+					+ "    content: counter(page);"
+					+ "    font-family: 思源黑体 Medium;"
+					+ "    font-size: 9pt;"
+					+ "    color: #000000;"
+					+ "  }"
+					+ "}"
+					+ ".start-counting {"
+					+ "  page: show-page-number;"
+					+ "}"
+					+ "#pageHeader {"
+					+ "  position: running(pageHeader);"
+					+ "  transform-origin: left top;"  // 从左上角开始缩放
+					+ "}"
+					+ " "
+					+ ".watermark {"
+					+ "  position: fixed;"
+					+ "  top: 40%;"
+					+ "  left: 15%;"
+					+ "  opacity: 1;"
+					+ "  pointer-events: none;"
+					+ "  z-index: 1000;"
+					+ "}"
+					+ "td, th { "
+					+ "  page-break-inside: avoid; "
+					+ "  -fs-table-paginate: paginate; "
+					+ "  background-clip: padding-box; "
+					+ "  -webkit-print-color-adjust: exact; "
+					+ "}";
+
+	Elements table = doc.select("table");
+	String tbaleStyle = table.attr("style");
+	tbaleStyle += "width:100%; max-width: 100%;";
+	table.attr("style", tbaleStyle);
+
+	Elements trs = doc.select("tr");
+	for (Element tr : trs) {
+		String trStyle = tr.attr("style");
+		trStyle = (trStyle == null) ? "" : trStyle;
+		trStyle += " page-break-inside: avoid !important;"; // 强制不分页
+		tr.attr("style", trStyle);
+	}
+
+	doc.head().appendElement("style").text(baseCss);
+
+
+	Elements tds = doc.select("td");
+	for (Element td : tds) {
+		Elements ps = td.select("p");
+		for (Element p : ps) {
+			String originalStyle = p.attr("style");
+			String newStyle = "margin-left: 0.5em; margin-right: 0.5em; "
+					+ "line-height: 1.2; margin-top: 6px!important; margin-bottom: 6px!important; " + originalStyle;
+			p.attr("style", newStyle);
+		}
+		if (ps.size() > 1) {
+			for (int i = 1; i < ps.size(); i++) {
+				ps.get(i).remove();
+			}
+			Element p = ps.first();
+			String pStyle = p.attr("style");
+			pStyle = removeWhiteSpacePreWrap(pStyle);
+			pStyle += " vertical-align: middle;";
+			p.attr("style", pStyle);
+		}
+
+		if (ps.size() > 0) {
+			Element p = ps.first();
+			String pStyle = p.attr("style");
+			pStyle = removeWhiteSpacePreWrap(pStyle);
+			p.attr("style", pStyle);
+
+			Elements spans = p.select("span");
+			if (!spans.isEmpty()) {
+				for (Element span : spans) {
+					String spanStyle = span.attr("style");
+					spanStyle = removeWhiteSpacePreWrap(spanStyle);
+					spanStyle = (spanStyle == null) ? "" : spanStyle;
+					span.attr("style", spanStyle);
+				}
+			} else {
+				String oriPstyle = p.attr("style");
+				oriPstyle = removeWhiteSpacePreWrap(oriPstyle);
+
+				p.attr("style", oriPstyle);
+			}
+		}
+
+		String oristyle = td.attr("style");
+		oristyle = (oristyle == null) ? "" : oristyle;
+		oristyle += " border-collapse: collapse; border: 0.75pt solid #E3EDFB;";
+		oristyle += " background-clip: padding-box; break-inside: avoid !important; page-break-inside: avoid";
+		td.attr("style", oristyle);
+	}
+	Elements divs = doc.select("div");
+	divs.attr("style", "");
+	for (int i = divs.size() - 1; i >= 1; i--) {
+		Element div = divs.get(i);
+		div.attr("style", "page-break-before: always;");
+	}
+	divs.last().addClass("start-counting").attr("style", "-fs-page-sequence:start");
+	Elements images = doc.select("img");
+
+
+	Element firstImg = images.first();
+	// 4. 删除第一个img元素
+	firstImg.parent().remove();
+	// 将所有 white-space:pre-wrap 改为 normal去除转换时的奇怪空白
+	Elements allElements = doc.getAllElements();
+
+	for (Element element : allElements) {
+		String style = element.attr("style");
+		if (style.contains("white-space:pre-wrap")) {
+			style = style.replaceAll("white-space\\s*:\\s*[^;]+;", "");
+			element.attr("style", style);
+		}
+	}
+	Element firstDiv = doc.select("div").first();
+	if (firstDiv != null) {
+		firstDiv.addClass("first-page");
+	}
+
+	addTableOfContentsCso(doc);
+	doc.body().appendElement("div")
+			.attr("class", "watermark")
+			.appendElement("img")
+			.attr("src", "file:///C:/Users/yyy/Desktop/1.png")
+			.attr("width", "500");
+
+	doc.body().prependElement("div")
+			.attr("id", "pageHeader")
+			.appendElement("img")
+			.attr("src", "file:///C:/Users/yyy/Desktop/logo.png")
+			.attr("width", "47px");
+
+	Elements paragraphs = doc.select("p.X1.X2");
+	for (Element p : paragraphs) {
+		p.attr("style", p.attr("style") + "page-break-before:always; margin-top: 0pt !important;");
+
+	}
+
+	doc.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
+	doc.outputSettings().escapeMode(Entities.EscapeMode.xhtml);
+	doc.head().prepend("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">");
+
+	return doc.html();
+}
+
+	/**
+	 * 合并表格中相同内容的单元格
+	 *
+	 * @param doc HTML文档对象
+	 */
+	public static void mergeSameContentCells(Document doc) {
+		Elements tables = doc.select("table");
+		for (Element table : tables) {
+			Elements rows = table.select("tr");
+			for (int colIndex = 0; colIndex < rows.first().select("td").size(); colIndex++) {
+				int rowspan = 1;
+				String currentCellText = "";
+				for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
+					Element currentCell = rows.get(rowIndex).select("td").get(colIndex);
+					String cellText = currentCell.text();
+					if (rowIndex == 0) {
+						currentCellText = cellText;
+					} else {
+						if (cellText.equals(currentCellText)) {
+							rowspan++;
+							currentCell.remove();
+						} else {
+							if (rowspan > 1) {
+								Element prevCell = rows.get(rowIndex - rowspan).select("td").get(colIndex);
+								prevCell.attr("rowspan", String.valueOf(rowspan));
+							}
+							rowspan = 1;
+							currentCellText = cellText;
+						}
+					}
+				}
+				if (rowspan > 1) {
+					Element lastCell = rows.get(rows.size() - rowspan).select("td").get(colIndex);
+					lastCell.attr("rowspan", String.valueOf(rowspan));
+				}
+			}
+		}
+	}
+
+	private static void addTableOfContentsCso(Document doc) {
+
+		// 目录样式
+		String tocCss = ".toc-container { margin: 20px 0; font-family: 思源黑体 Medium; }"
+				+ ".toc-title { text-align: center; font-size: 12pt; margin-bottom: 15px; color: black; }"
+				+ ".toc-list { list-style-type: none; padding: 0; width: 100%; }"
+				+ ".toc-item { margin: 5px 0;     padding-top: 2px; padding-bottom: 2px;       line-height: 2;  }"
+				+ ".toc-level-1 { padding-left: 0; }"
+				+ ".toc-level-2 { padding-left: 2em; }"
+				+ ".toc-link { "
+				+ "  display: block; "
+				+ "  position: relative; "
+				+ "  color: black !important; "
+				+ "  text-decoration: none !important; "
+				+ "  line-height: 1.5; "  // 新增:控制整体行高
+				+ "}"
+				+ ".toc-line-container { "
+				+ "  display: table; "
+				+ "  width: 100%; "
+				+ "  vertical-align: middle; "  // 关键:控制容器内垂直对齐
+				+ "}"
+				+ ".toc-text { "
+				+ "  display: table-cell; "
+				+ "  font-size: 9pt; "
+				+ "  white-space: nowrap; "
+				+ "  padding-right: 5px; "
+				+ "  vertical-align: middle; "  // 改为middle对齐
+				+ "}"
+				+ ".toc-dots { "
+				+ "  display: table-cell; "
+				+ "  width: 100%; "
+				+ "  vertical-align: middle; "  // 关键:改为middle对齐
+				+ "  border-bottom: 1px dotted #000000; "
+				+ "  height: 1em; "  // 固定高度
+				+ "  margin-top: 2px; "  // 关键:正值下移,负值上移(按需调整)
+				+ "}"
+				+ "p.X1.X2 { -fs-pdf-bookmark: level 1; }"
+				+ "p.X1.X3 { -fs-pdf-bookmark: level 2; }"
+				+ ".toc-page { "
+				+ "  display: table-cell; "
+				+ "  font-size: 9pt; "
+				+ "  white-space: nowrap; "
+				+ "  padding-left: 5px; "
+				+ "  vertical-align: middle; "  // 改为middle对齐
+				+ "}";
+		doc.head().appendElement("style").text(tocCss);
+
+		// 构建目录内容
+		Element tocList = new Element("ul").addClass("toc-list");
+		doc.select("p.X1.X2, p.X1.X3").forEach(el -> {
+			boolean isLevel1 = el.hasClass("X2");
+			String id = "sec_" + el.text().hashCode();
+			el.attr("id", id);
+			Integer pageNumber = pageNumberMap.getOrDefault(el.text(), 1);
+
+			Element li = tocList.appendElement("li")
+					.addClass("toc-item " + (isLevel1 ? "toc-level-1" : "toc-level-2"));
+
+			Element link = li.appendElement("a")
+					.attr("href", "#" + id)
+					.addClass("toc-link");
+			Element lineContainer = link.appendElement("div").addClass("toc-line-container");
+			lineContainer.appendElement("span").addClass("toc-text").text(el.text());
+			lineContainer.appendElement("span").addClass("toc-dots");
+			lineContainer.appendElement("span").addClass("toc-page").text(String.valueOf(pageNumber));
+		});
+
+		// 插入目录
+		Element secondDiv = doc.select("div").get(1);
+
+		if (secondDiv != null) {
+			secondDiv.after(
+					"<div class='toc-container' style='page-break-before: always;'>"
+							+ "<h1 class='toc-title'>目录</h1>"
+							+ tocList.outerHtml()
+							+ "</div>"
+			);
+		} else {
+			doc.body().prepend(
+					"<div class='toc-container' style='page-break-before: always;'>"
+							+ "<h1 class='toc-title'>目录</h1>"
+							+ tocList.outerHtml()
+							+ "</div>"
+			);
+		}
+
+	}
+
+
+	/**
+	 * 操作已生成的pdf
+	 *
+	 * @param backgroundImagePath    图片文件夹位置
+	 * @param onlyCollectPageNumbers 是否是遍历目录获取标题位置
+	 * @throws Exception
+	 */
+	private static void pdfReaderCso(InputStream inputStream, OutputStream outputStream,
+	                                 String backgroundImagePath, boolean onlyCollectPageNumbers)
+			throws Exception {
+		PdfReader reader = new PdfReader(inputStream);
+		PdfStamper stamper = new PdfStamper(reader, outputStream);
+		int startPage = 1;
+		if (onlyCollectPageNumbers) {
+			pageNumberMap.clear();
+			// 查找起始页
+			for (int pageNum = 1; pageNum <= reader.getNumberOfPages(); pageNum++) {
+				String pageText = new PdfTextExtractor(reader).getTextFromPage(pageNum);
+				String[] lines = pageText.split("\\r?\\n");
+				for (String line : lines) {
+					if (line.equals("1.产品介绍")) {
+						startPage = pageNum;
+						pageNumberMap.put("startPage", startPage);
+					}
+				}
+			}
+			// 收集标题和页码
+			Pattern titlePatterncso = Pattern.compile(
+					"^((\\d+)\\.|(\\d+\\.\\d+))([\\u4e00-\\u9fa5a-zA-Z0-9].*)$",
+					Pattern.MULTILINE
+			);
+
+			for (int pageNum = startPage; pageNum <= reader.getNumberOfPages(); pageNum++) {
+
+				String pageText = new PdfTextExtractor(reader).getTextFromPage(pageNum);
+				String[] lines = pageText.split("\\r?\\n");
+				for (int i = 0; i < lines.length; i++) {
+					String line = lines[i].trim();
+					if (line.isEmpty()) {
+						continue;
+					}
+					Matcher matcher = titlePatterncso.matcher(line);
+					if (matcher.matches()) {
+						pageNumberMap.put(line, pageNum - startPage + 1);
+					}
+				}
+			}
+		}
+		//删除第一页水印 避免边删边便利引入list
+		for (int pageNum = 1; pageNum <= reader.getNumberOfPages(); pageNum++) {
+			if (pageNum == 1) {
+				PdfDictionary pageDict = reader.getPageN(1);
+				PdfDictionary resources = pageDict.getAsDict(PdfName.RESOURCES);
+				PdfDictionary xobjects = resources.getAsDict(PdfName.XOBJECT);
+
+				if (xobjects != null) {
+					// 用 List 存待删除的 PdfName
+					List<PdfName> toRemove = new ArrayList<>();
+
+					for (Object nameObj : xobjects.getKeys()) {
+						PdfName name = (PdfName) nameObj;
+						PdfObject obj = xobjects.get(name);
+
+						if (obj.isIndirect()) {
+							PdfDictionary xobj = (PdfDictionary) PdfReader.getPdfObject(obj);
+							if (PdfName.IMAGE.equals(xobj.getAsName(PdfName.SUBTYPE))) {
+								// 标记需要删除的名称,不直接删
+								toRemove.add(name);
+							}
+						}
+					}
+
+					// 遍历结束后,统一删除标记的元素
+					for (PdfName name : toRemove) {
+						xobjects.remove(name);
+					}
+				}
+			}
+			if (pageNum != 1) {
+				PdfContentByte underContent = stamper.getUnderContent(pageNum);
+
+				// 固定位置参数(可根据需要调整)
+				float pageWidth = reader.getPageSize(pageNum).getWidth();
+				float pageHeight = reader.getPageSize(pageNum).getHeight();
+				float xPos = 50; // 左侧边距
+				float yPos = pageHeight - 60; // 距离顶部50单位
+
+				underContent.saveState();
+				underContent.setColorStroke(new Color(49, 137, 186)); // 浅蓝色线条
+				underContent.setLineWidth(0.5f); // 线宽
+				underContent.moveTo(xPos - 10, yPos + 35);
+				underContent.lineTo(pageWidth - xPos + 10, yPos + 35);
+				underContent.stroke();
+				underContent.restoreState();
+			}
+		}
+		//一级标题图形背景
+		Pattern firstLevelTitlePattern = Pattern.compile("^(\\d+)\\.([\\u4e00-\\u9fa5a-zA-Z].*)$");
+		Set<Integer> styledPages = new HashSet<>();
+		startPage = pageNumberMap.get("startPage");
+		for (Map.Entry<String, Integer> stringIntegerEntry : pageNumberMap.entrySet()) {
+			String key = stringIntegerEntry.getKey();
+			int value = stringIntegerEntry.getValue();
+			if (firstLevelTitlePattern.matcher(key).find()) {
+				styledPages.add(value + startPage - 1);
+			}
+		}
+
+		// 在识别出的页面添加标题样式
+		for (Integer pageNum : styledPages) {
+			if (pageNum < 1 || pageNum > reader.getNumberOfPages()) {
+				continue;
+			}
+			PdfContentByte underContent = stamper.getUnderContent(pageNum);
+
+			// 固定位置参数(可根据需要调整)
+			float pageWidth = reader.getPageSize(pageNum).getWidth();
+			float pageHeight = reader.getPageSize(pageNum).getHeight();
+			float xPos = 50; // 左侧边距
+			float yPos = pageHeight - 60; // 距离顶部50单位
+
+			// 1. 绘制浅蓝色小方块(对应示例图里的小矩形)
+			underContent.saveState();
+			underContent.setColorFill(new Color(49, 137, 186)); // 更浅的蓝色,贴近示例图小方块色调
+			// 小方块尺寸,可微调
+			underContent.roundRectangle(
+					xPos,
+					yPos,
+					10,
+					10,
+					2
+
+			);
+			underContent.fill();
+			underContent.restoreState();
+
+			// 2. 绘制深蓝色大方块(对应示例图里的主矩形)
+			underContent.saveState();
+			underContent.setColorFill(new Color(0, 82, 204)); // 深蓝色,贴近示例图主方块色调
+			// 大方块位置和尺寸,基于小方块对齐,可微调
+			underContent.roundRectangle(
+					xPos - 10,   // 相对小方块偏移,让视觉更协调
+					yPos - 10,
+					16,
+					16,
+					3
+			);
+			underContent.fill();
+			underContent.restoreState();
+
+			// 2. 绘制横线
+			underContent.saveState();
+			underContent.setColorStroke(new Color(0x16, 0x77, 0xFF)); // 浅蓝色线条
+			underContent.setLineWidth(1.5f); // 线宽
+			underContent.moveTo(xPos - 10, yPos - 20);
+			underContent.lineTo(pageWidth - xPos + 10, yPos - 20);
+			underContent.stroke();
+			underContent.restoreState();
+
+		}
+
+		//封面背景
+		PdfContentByte background = stamper.getUnderContent(1);
+		Image image = Image.getInstance(backgroundImagePath);
+		image.scaleAbsolute(PageSize.A4.getWidth(), PageSize.A4.getHeight());
+		image.setAbsolutePosition(0, 0);
+		background.addImage(image);
+
+		stamper.close();
+		reader.close();
+	}
 }

+ 1 - 0
easier-report-biz/src/main/resources/application.yml

@@ -107,3 +107,4 @@ easier:
   office:
     pdf:
       rootPath: C:/Users/yyy/dev/yyc3/easier-be
+

+ 2 - 2
easier-report-biz/src/test/java/com/yaoyicloud/render/test/TestPdf.java

@@ -54,13 +54,13 @@ public class TestPdf {
         String imageDir = "D:\\li312\\Desktop\\image";
 
         String docxHtml = convert(
-                "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\temp\\1882350890076909569_df857a4625734caaafadb6afeb67b155.docx",
+                "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\temp\\1932256473198157826_1e0c248426e94a6da07285ce36b2eaff.docx",
                 "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\file\\image");
         // String docxHtml = convert("C:\\Users\\yyy\\dev\\yyc3\\easier-be\\temp\\1922178317200191489_d60ec021eb6b497eb6360ea8d78d8245.docx",
         // "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\file\\image");
         // String docxHtml = convert("C:\\Users\\yyy\\Documents\\WXWork\\1688856358659369\\Cache\\File\\2025-04\\cso.docx",
         // "C:\\Users\\yyy\\dev\\yyc3\\easier-be\\file\\image");
-        try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("2.html"))) {
+        try (BufferedWriter writer2 = new BufferedWriter(new FileWriter("3.html"))) {
             writer2.write(docxHtml);
         } catch (IOException e) {
             System.err.println("写入 2.html 文件时发生错误: " + e.getMessage());