From c556e46c2a6232d74ab6dbf7566479ec955d905d Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期六, 16 五月 2026 16:00:35 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'
---
admin-server/src/main/java/com/estate/expert/admin/domain/report/ReportTemplate.java | 5
admin-server/src/main/java/com/estate/expert/admin/stat/report/data/DaiguanDeviceReportData.java | 2
admin-server/src/main/java/com/estate/expert/admin/stat/report/data/ReportDataBase.java | 2
admin-server/src/main/java/com/estate/expert/admin/stat/report/render/WordNumberingSanitizer.java | 184 ++++++++++++++++++++++++++++++
admin-server/src/main/java/com/estate/expert/admin/stat/report/render/DeviceGroupTablePolicy.java | 128 +++++++++++++++++++++
admin-server/src/main/java/com/estate/expert/admin/stat/report/builder/DeviceReportBuilder.java | 9 +
admin-server/src/main/resources/templates/设备风险评估报告new.docx | 0
admin-server/src/main/java/com/estate/expert/admin/service/impl/ReportServiceImpl.java | 2
8 files changed, 325 insertions(+), 7 deletions(-)
diff --git a/admin-server/src/main/java/com/estate/expert/admin/domain/report/ReportTemplate.java b/admin-server/src/main/java/com/estate/expert/admin/domain/report/ReportTemplate.java
index 5a5bcba..8211410 100644
--- a/admin-server/src/main/java/com/estate/expert/admin/domain/report/ReportTemplate.java
+++ b/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;
-}
\ No newline at end of file
+ @Column(nullable = false)
+ private boolean active = true;
+
+}
diff --git a/admin-server/src/main/java/com/estate/expert/admin/service/impl/ReportServiceImpl.java b/admin-server/src/main/java/com/estate/expert/admin/service/impl/ReportServiceImpl.java
index 8617e86..edd3eba 100644
--- a/admin-server/src/main/java/com/estate/expert/admin/service/impl/ReportServiceImpl.java
+++ b/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
diff --git a/admin-server/src/main/java/com/estate/expert/admin/stat/report/builder/DeviceReportBuilder.java b/admin-server/src/main/java/com/estate/expert/admin/stat/report/builder/DeviceReportBuilder.java
index 4e27d9d..b9df00b 100644
--- a/admin-server/src/main/java/com/estate/expert/admin/stat/report/builder/DeviceReportBuilder.java
+++ b/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);
diff --git a/admin-server/src/main/java/com/estate/expert/admin/stat/report/data/DaiguanDeviceReportData.java b/admin-server/src/main/java/com/estate/expert/admin/stat/report/data/DaiguanDeviceReportData.java
index 706e601..8487acd 100644
--- a/admin-server/src/main/java/com/estate/expert/admin/stat/report/data/DaiguanDeviceReportData.java
+++ b/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);
diff --git a/admin-server/src/main/java/com/estate/expert/admin/stat/report/data/ReportDataBase.java b/admin-server/src/main/java/com/estate/expert/admin/stat/report/data/ReportDataBase.java
index f97776d..eceb66d 100644
--- a/admin-server/src/main/java/com/estate/expert/admin/stat/report/data/ReportDataBase.java
+++ b/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());
diff --git a/admin-server/src/main/java/com/estate/expert/admin/stat/report/render/DeviceGroupTablePolicy.java b/admin-server/src/main/java/com/estate/expert/admin/stat/report/render/DeviceGroupTablePolicy.java
new file mode 100644
index 0000000..ecf985c
--- /dev/null
+++ b/admin-server/src/main/java/com/estate/expert/admin/stat/report/render/DeviceGroupTablePolicy.java
@@ -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);
+ }
+}
diff --git a/admin-server/src/main/java/com/estate/expert/admin/stat/report/render/WordNumberingSanitizer.java b/admin-server/src/main/java/com/estate/expert/admin/stat/report/render/WordNumberingSanitizer.java
new file mode 100644
index 0000000..980f840
--- /dev/null
+++ b/admin-server/src/main/java/com/estate/expert/admin/stat/report/render/WordNumberingSanitizer.java
@@ -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);
+ }
+ }
+}
diff --git "a/admin-server/src/main/resources/templates/\350\256\276\345\244\207\351\243\216\351\231\251\350\257\204\344\274\260\346\212\245\345\221\212new.docx" "b/admin-server/src/main/resources/templates/\350\256\276\345\244\207\351\243\216\351\231\251\350\257\204\344\274\260\346\212\245\345\221\212new.docx"
index 2305921..c0f6720 100644
--- "a/admin-server/src/main/resources/templates/\350\256\276\345\244\207\351\243\216\351\231\251\350\257\204\344\274\260\346\212\245\345\221\212new.docx"
+++ "b/admin-server/src/main/resources/templates/\350\256\276\345\244\207\351\243\216\351\231\251\350\257\204\344\274\260\346\212\245\345\221\212new.docx"
Binary files differ
--
Gitblit v1.7.1