admin-server/src/main/java/com/estate/expert/admin/domain/report/ReportTemplate.java
@@ -46,4 +46,7 @@ @Column(columnDefinition = "json") private Map<String, Integer> config; } @Column(nullable = false) private boolean active = true; } admin-server/src/main/java/com/estate/expert/admin/service/impl/ReportServiceImpl.java
@@ -50,7 +50,7 @@ private static final String DAIGUAN_TECH_REMARK = "岱观科技"; private static final String DAIGUAN_SYSTEM_TEMPLATE_FILE = "第三方评估报告new.docx"; private static final String DAIGUAN_REMARK = "daiguan"; private static final String DAIGUAN_REMARK = "岱观科技"; private static final String DAIGUAN_DEVICE_TEMPLATE_FILE = "设备风险评估报告new.docx"; @Resource admin-server/src/main/java/com/estate/expert/admin/stat/report/builder/DeviceReportBuilder.java
@@ -7,7 +7,9 @@ import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy; import com.estate.expert.admin.service.UserFileService; import com.estate.expert.admin.stat.report.render.CustomPictureRenderPolicy; import com.estate.expert.admin.stat.report.render.DeviceGroupTablePolicy; import com.estate.expert.admin.stat.report.render.DeviceTablePolicy; import com.estate.expert.admin.stat.report.render.WordNumberingSanitizer; import org.springframework.core.io.ClassPathResource; import java.io.IOException; @@ -73,7 +75,7 @@ ConfigureBuilder builder = Configure.builder(); builder.bind("data.设备信息", new LoopRowTableRenderPolicy()); builder.bind("data.设备分组信息", new LoopRowTableRenderPolicy()); builder.bind("data.设备分组信息", new DeviceGroupTablePolicy()); builder.bind("data.评估结果.设备得分情况", new LoopRowTableRenderPolicy()); builder.bind("详情", new LoopRowTableRenderPolicy()); builder.bind("data.评估结果.分类评估", new LoopRowTableRenderPolicy()); @@ -96,8 +98,9 @@ private XWPFTemplate compileAndRender(ClassPathResource res, ConfigureBuilder builder, JSONObject data) { try { return XWPFTemplate.compile(res.getInputStream(), builder.build()).render( Map.of("data", data) return WordNumberingSanitizer.sanitize( XWPFTemplate.compile(res.getInputStream(), builder.build()).render(Map.of("data", data)), res.getPath().contains("new") ); } catch (IOException e) { throw new RuntimeException(e); admin-server/src/main/java/com/estate/expert/admin/stat/report/data/DaiguanDeviceReportData.java
@@ -146,7 +146,7 @@ } JSONObject summary = new JSONObject(); summary.put("分项名称", "管理行为得分"); summary.put("分项名称", "综合得分"); summary.put("分项得分", formatFloat(totalScore / totalDevices)); details.add(summary); admin-server/src/main/java/com/estate/expert/admin/stat/report/data/ReportDataBase.java
@@ -189,7 +189,7 @@ } info.put("工程类型", project.getType().getDesc()); info.put("工程规模", StringUtils.hasText(project.getScale()) ? project.getScale() : project.getProjectCost()); info.put("工程规模", project.getScale()); if (projectInfo != null) { info.put("项目简介", projectInfo.getDetail()); admin-server/src/main/java/com/estate/expert/admin/stat/report/render/DeviceGroupTablePolicy.java
New file @@ -0,0 +1,128 @@ package com.estate.expert.admin.stat.report.render; import com.alibaba.fastjson.JSONObject; import com.deepoove.poi.policy.DynamicTableRenderPolicy; import com.deepoove.poi.util.TableTools; import org.apache.poi.xwpf.usermodel.ParagraphAlignment; 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.STBorder; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalJc; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.math.BigInteger; import java.util.List; /** * 设备分组表格渲染策略:同一设备类型内合并类型和数量,设备类型变更时重新分组。 */ public class DeviceGroupTablePolicy extends DynamicTableRenderPolicy { private static final int TYPE_COL = 0; private static final int COUNT_COL = 1; private static final int NUMBER_COL = 2; @Override public void render(XWPFTable table, Object data) { var groups = (List<Object>) data; int templateRowIndex = findTemplateRowIndex(table); if (templateRowIndex < 0) { return; } table.removeRow(templateRowIndex); if (CollectionUtils.isEmpty(groups)) { return; } int insertIndex = templateRowIndex; int groupStart = templateRowIndex; int groupSize = 0; for (Object item : groups) { if (!(item instanceof JSONObject group)) { continue; } boolean firstRowOfGroup = StringUtils.hasText(group.getString("设备类型")); XWPFTableRow row = table.insertNewTableRow(insertIndex++); renderRow(row, group, firstRowOfGroup); if (firstRowOfGroup) { mergeGroup(table, groupStart, groupSize); groupStart = insertIndex - 1; groupSize = 1; } else { groupSize++; } } mergeGroup(table, groupStart, groupSize); } private int findTemplateRowIndex(XWPFTable table) { for (int i = 0; i < table.getRows().size(); i++) { if (rowText(table.getRow(i)).contains("[设备编号]")) { return i; } } return -1; } private String rowText(XWPFTableRow row) { if (row == null) { return ""; } return row.getTableCells().stream() .map(XWPFTableCell::getText) .reduce("", String::concat); } private void renderRow(XWPFTableRow row, JSONObject group, boolean firstRowOfGroup) { renderCell(row, firstRowOfGroup ? group.getString("设备类型") : "", 2972); renderCell(row, firstRowOfGroup ? group.getString("设备数量") : "", 1531); renderCell(row, group.getString("设备编号"), 4706); } private void renderCell(XWPFTableRow row, String text, int width) { XWPFTableCell cell = row.createCell(); var tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr(); tcPr.addNewTcW().setW(BigInteger.valueOf(width)); tcPr.getTcW().setType(STTblWidth.DXA); tcPr.addNewGridSpan().setVal(BigInteger.valueOf(2)); tcPr.addNewVAlign().setVal(STVerticalJc.CENTER); var borders = tcPr.addNewTcBorders(); borders.addNewTop().setVal(STBorder.SINGLE); borders.addNewBottom().setVal(STBorder.SINGLE); borders.addNewLeft().setVal(STBorder.SINGLE); borders.addNewRight().setVal(STBorder.SINGLE); borders.getTop().setColor("000000"); borders.getBottom().setColor("000000"); borders.getLeft().setColor("000000"); borders.getRight().setColor("000000"); borders.getTop().setSz(BigInteger.valueOf(4)); borders.getBottom().setSz(BigInteger.valueOf(4)); borders.getLeft().setSz(BigInteger.valueOf(4)); borders.getRight().setSz(BigInteger.valueOf(4)); var paragraph = cell.getParagraphs().get(0); paragraph.setAlignment(ParagraphAlignment.CENTER); var run = paragraph.createRun(); run.setFontFamily("黑体"); run.setFontSize(12); run.setText(text == null ? "" : text); cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER); } private void mergeGroup(XWPFTable table, int groupStart, int groupSize) { if (groupSize <= 1) { return; } TableTools.mergeCellsVertically(table, TYPE_COL, groupStart, groupStart + groupSize - 1); TableTools.mergeCellsVertically(table, COUNT_COL, groupStart, groupStart + groupSize - 1); } } admin-server/src/main/java/com/estate/expert/admin/stat/report/render/WordNumberingSanitizer.java
New file @@ -0,0 +1,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); } } } admin-server/src/main/resources/templates/设备风险评估报告new.docxBinary files differ