package com.estate.expert.admin.stat.report.render; import com.deepoove.poi.XWPFTemplate; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFNumbering; 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.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumLvl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumPr; import java.math.BigInteger; import java.util.List; /** * 修复模板中缺失层级的编号段落,避免 POI 转换时读取 ilvl 空指针。 */ public final class WordNumberingSanitizer { private WordNumberingSanitizer() { } public static XWPFTemplate sanitize(XWPFTemplate template) { return sanitize(template, false); } public static XWPFTemplate sanitize(XWPFTemplate template, boolean normalizeDangerDetail) { if (template == null) { return null; } sanitizeDocument(template.getXWPFDocument(), normalizeDangerDetail); return template; } private static void sanitizeDocument(XWPFDocument document, boolean normalizeDangerDetail) { if (document == null) { return; } document.getParagraphs().forEach(WordNumberingSanitizer::sanitizeParagraph); if (normalizeDangerDetail) { normalizeDangerDetailHeadingNumbering(document, document.getParagraphs()); } document.getTables().forEach(table -> table.getRows().forEach(row -> row.getTableCells().forEach(cell -> sanitizeCell(document, cell, normalizeDangerDetail)))); if (normalizeDangerDetail) { boldSummaryRows(document, "综合得分"); } document.getHeaderList().forEach(header -> header.getParagraphs().forEach(WordNumberingSanitizer::sanitizeParagraph)); document.getFooterList().forEach(footer -> footer.getParagraphs().forEach(WordNumberingSanitizer::sanitizeParagraph)); } private static void sanitizeCell(XWPFDocument document, XWPFTableCell cell, boolean normalizeDangerDetail) { if (cell == null) { return; } cell.getParagraphs().forEach(WordNumberingSanitizer::sanitizeParagraph); if (normalizeDangerDetail) { normalizeDangerDetailHeadingNumbering(document, cell.getParagraphs()); } cell.getTables().forEach(table -> table.getRows().forEach(row -> row.getTableCells().forEach(innerCell -> sanitizeCell(document, innerCell, normalizeDangerDetail)))); } private static void boldSummaryRows(XWPFDocument document, String markerText) { if (document == null || markerText == null || markerText.isBlank()) { return; } for (XWPFTable table : document.getTables()) { for (XWPFTableRow row : table.getRows()) { boolean summaryRow = row.getTableCells().stream() .map(XWPFTableCell::getText) .anyMatch(text -> text != null && text.contains(markerText)); if (!summaryRow) { continue; } for (XWPFTableCell cell : row.getTableCells()) { for (XWPFParagraph paragraph : cell.getParagraphs()) { for (XWPFRun run : paragraph.getRuns()) { run.setBold(true); } } } } } } private static void normalizeDangerDetailHeadingNumbering(XWPFDocument document, List paragraphs) { boolean inDangerDetail = false; boolean pendingNewGroup = false; BigInteger currentProblemNumId = null; for (XWPFParagraph paragraph : paragraphs) { String styleId = styleId(paragraph); if (!inDangerDetail) { if (isDangerDetailTitle(styleId, paragraph.getText())) { inDangerDetail = true; } continue; } if (isNextSectionTitle(styleId, paragraph.getText())) { return; } if ("4".equals(styleId) && hasVisibleText(paragraph.getText())) { pendingNewGroup = true; currentProblemNumId = null; continue; } if ("5".equals(styleId) && hasVisibleText(paragraph.getText())) { if (pendingNewGroup || currentProblemNumId == null) { currentProblemNumId = createRestartNumber(document, paragraph); pendingNewGroup = false; } paragraph.setNumID(currentProblemNumId); paragraph.setNumILvl(BigInteger.ZERO); } } } private static boolean isDangerDetailTitle(String styleId, String text) { return "2".equals(styleId) && text != null && text.contains("隐患") && text.contains("详单"); } private static boolean isNextSectionTitle(String styleId, String text) { return ("1".equals(styleId) || "2".equals(styleId)) && hasVisibleText(text) && !(text.contains("隐患") && text.contains("详单")); } private static boolean hasVisibleText(String text) { return text != null && !text.trim().isEmpty() && !text.trim().startsWith("{{"); } private static String styleId(XWPFParagraph paragraph) { String styleId = paragraph.getStyleID(); return styleId != null ? styleId : paragraph.getStyle(); } private static BigInteger createRestartNumber(XWPFDocument document, XWPFParagraph paragraph) { XWPFNumbering numbering = document.getNumbering(); if (numbering == null) { numbering = document.createNumbering(); } BigInteger abstractNumId = paragraph.getNumID() == null ? BigInteger.ONE : numbering.getAbstractNumID(paragraph.getNumID()); if (abstractNumId == null) { abstractNumId = BigInteger.ONE; } BigInteger numId = numbering.addNum(abstractNumId); CTNumLvl override = numbering.getNum(numId).getCTNum().addNewLvlOverride(); override.setIlvl(BigInteger.ZERO); override.addNewStartOverride().setVal(BigInteger.ONE); return numId; } private static void sanitizeParagraph(XWPFParagraph paragraph) { if (paragraph == null || !paragraph.getCTP().isSetPPr()) { return; } var paragraphProperties = paragraph.getCTP().getPPr(); if (!paragraphProperties.isSetNumPr()) { return; } CTNumPr numPr = paragraphProperties.getNumPr(); if (numPr.getNumId() != null && numPr.getIlvl() == null) { CTDecimalNumber ilvl = numPr.addNewIlvl(); ilvl.setVal(BigInteger.ZERO); } } }