“mitao”
2026-05-16 c556e46c2a6232d74ab6dbf7566479ec955d905d
Merge remote-tracking branch 'origin/master'

# Conflicts:
# admin-server/src/main/java/com/estate/expert/admin/stat/report/data/ReportDataBase.java
2个文件已添加
6个文件已修改
330 ■■■■■ 已修改文件
admin-server/src/main/java/com/estate/expert/admin/domain/report/ReportTemplate.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/service/impl/ReportServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/stat/report/builder/DeviceReportBuilder.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/stat/report/data/DaiguanDeviceReportData.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/stat/report/data/ReportDataBase.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/stat/report/render/DeviceGroupTablePolicy.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/stat/report/render/WordNumberingSanitizer.java 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/resources/templates/设备风险评估报告new.docx 补丁 | 查看 | 原始文档 | blame | 历史
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.docx
Binary files differ