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