“mitao”
2026-05-16 c556e46c2a6232d74ab6dbf7566479ec955d905d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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<XWPFParagraph> 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);
        }
    }
}