liujie
2026-09-15 5a95ec36565d7ecc1a5ecb261f26752c90dec120
update
4个文件已添加
1个文件已修改
356 ■■■■■ 已修改文件
admin-server/src/main/java/com/estate/expert/admin/service/impl/ReportServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/stat/report/builder/ChengduHengtaiSystemReportBuilder.java 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/stat/report/render/ChengduHengtaiPictureRenderPolicy.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/resources/templates/第三方检查报告-成都衡泰.docx 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/test/java/com/estate/expert/admin/stat/report/builder/ChengduHengtaiSystemReportBuilderTest.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
admin-server/src/main/java/com/estate/expert/admin/service/impl/ReportServiceImpl.java
@@ -15,6 +15,7 @@
import com.estate.expert.admin.service.eval.EvalFuzzyService;
import com.estate.expert.admin.service.eval.EvalScoreService;
import com.estate.expert.admin.stat.report.builder.DeviceReportBuilder;
import com.estate.expert.admin.stat.report.builder.ChengduHengtaiSystemReportBuilder;
import com.estate.expert.admin.stat.report.builder.ReportBuilderBase;
import com.estate.expert.admin.stat.report.builder.SystemReportBuilder;
import com.estate.expert.admin.stat.report.data.DeviceReportData;
@@ -52,6 +53,7 @@
    private static final String DAIGUAN_SYSTEM_TEMPLATE_FILE = "第三方评估报告new.docx";
    private static final String DAIGUAN_REMARK = "岱观科技";
    private static final String DAIGUAN_DEVICE_TEMPLATE_FILE = "设备风险评估报告new.docx";
    private static final String CHENGDU_HENGTAI_REMARK = "成都衡泰";
    @Resource
    private ReportTemplateService reportTemplateService;
@@ -194,6 +196,9 @@
        BizResultCode.BIZ_REPORT_TEMPLATE_NOT_EXIST.check(reportTemplate == null);
        switch (reportTemplate.getType()) {
            case ThirdPartyReport:
                if (isChengduHengtaiTemplate(reportTemplate)) {
                    return new ChengduHengtaiSystemReportBuilder(userFileService);
                }
                if (isDaiguanTechTemplate(reportTemplate)) {
                    return new SystemReportBuilder(userFileService, DAIGUAN_SYSTEM_TEMPLATE_FILE);
                }
@@ -479,6 +484,12 @@
        return DAIGUAN_TECH_REMARK.equals(reportTemplate.getRemark().trim());
    }
    private boolean isChengduHengtaiTemplate(ReportTemplate reportTemplate) {
        return reportTemplate != null
                && reportTemplate.getRemark() != null
                && CHENGDU_HENGTAI_REMARK.equals(reportTemplate.getRemark().trim());
    }
    private long countProjectChecks(Integer projectId) {
        if (projectId == null) {
            return 0L;
admin-server/src/main/java/com/estate/expert/admin/stat/report/builder/ChengduHengtaiSystemReportBuilder.java
New file
@@ -0,0 +1,254 @@
package com.estate.expert.admin.stat.report.builder;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import com.estate.expert.admin.service.UserFileService;
import com.estate.expert.admin.stat.report.render.ChengduHengtaiPictureRenderPolicy;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * 成都衡泰第三方检查报告构建器
 */
public class ChengduHengtaiSystemReportBuilder implements ReportBuilderBase {
    private static final String PICTURE_ROW_TITLE = "问题图片";
    private static final int EMPTY_PICTURE_ROW_HEIGHT = 240;
    private static final String LEVEL_ONE = "一级隐患";
    private static final String LEVEL_TWO = "二级隐患";
    private static final Set<String> HAZARD_LEVELS = Set.of(
            LEVEL_ONE, LEVEL_TWO, "三级隐患", "四级隐患");
    private static final String LEVEL_ONE_COLOR = "FF0000";
    private static final String LEVEL_TWO_COLOR = "FF9900";
    private static final Set<String> HAZARD_TABLE_LABELS = Set.of(
            "问题部位", "隐患等级", "问题描述", "违反条款", "建议措施", PICTURE_ROW_TITLE);
    private static final String TEMPLATE_FILE = "第三方检查报告-成都衡泰.docx";
    private final UserFileService userFileService;
    public ChengduHengtaiSystemReportBuilder(UserFileService userFileService) {
        this.userFileService = userFileService;
    }
    @Override
    public XWPFTemplate buildReport(JSONObject data, Map<String, Integer> templateConfig) {
        var modelData = buildModelData(data);
        var configure = Configure.builder()
                .bind("data.评估结果.问题清单", new LoopRowTableRenderPolicy())
                .addPlugin('%', new ChengduHengtaiPictureRenderPolicy(userFileService))
                .build();
        var resource = new ClassPathResource("templates/" + TEMPLATE_FILE);
        try {
            var template = XWPFTemplate.compile(resource.getInputStream(), configure)
                    .render(Map.of("data", modelData));
            compactEmptyPictureRows(template.getXWPFDocument());
            applyHazardLevelColors(template.getXWPFDocument());
            return template;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    static void compactEmptyPictureRows(XWPFDocument document) {
        for (var table : document.getTables()) {
            var rows = table.getRows();
            for (int index = 0; index < rows.size() - 1; index++) {
                if (isPictureTitleRow(rows.get(index)) && !containsPicture(rows.get(index + 1))) {
                    rows.get(index + 1).setHeight(EMPTY_PICTURE_ROW_HEIGHT);
                }
            }
        }
    }
    private static boolean isPictureTitleRow(XWPFTableRow row) {
        return row.getTableCells().stream()
                .flatMap(cell -> cell.getParagraphs().stream())
                .map(paragraph -> paragraph.getText().trim())
                .anyMatch(PICTURE_ROW_TITLE::equals);
    }
    private static boolean containsPicture(XWPFTableRow row) {
        return row.getTableCells().stream()
                .flatMap(cell -> cell.getParagraphs().stream())
                .flatMap(paragraph -> paragraph.getRuns().stream())
                .anyMatch(run -> !run.getEmbeddedPictures().isEmpty());
    }
    static void applyHazardLevelColors(XWPFDocument document) {
        for (var table : document.getTables()) {
            var rows = table.getRows();
            for (int startIndex = 0; startIndex < rows.size(); startIndex++) {
                var level = findHazardLevel(rows.get(startIndex));
                if (level == null) {
                    continue;
                }
                var endIndex = findNextHazardStartIndex(rows, startIndex + 1);
                var color = findHazardLevelColor(level);
                if (color != null) {
                    colorHazardRows(rows, startIndex, endIndex, color);
                }
            }
        }
    }
    private static String findHazardLevel(XWPFTableRow row) {
        for (var cell : row.getTableCells()) {
            var text = cell.getText().trim();
            if (HAZARD_LEVELS.contains(text)) {
                return text;
            }
        }
        return null;
    }
    private static String findHazardLevelColor(String level) {
        if (LEVEL_ONE.equals(level)) {
            return LEVEL_ONE_COLOR;
        }
        if (LEVEL_TWO.equals(level)) {
            return LEVEL_TWO_COLOR;
        }
        return null;
    }
    private static int findNextHazardStartIndex(java.util.List<XWPFTableRow> rows, int startIndex) {
        for (int index = startIndex; index < rows.size(); index++) {
            if (findHazardLevel(rows.get(index)) != null) {
                return index;
            }
        }
        return rows.size();
    }
    private static void colorHazardRows(java.util.List<XWPFTableRow> rows, int startIndex, int endIndex, String color) {
        for (int rowIndex = startIndex; rowIndex < endIndex; rowIndex++) {
            for (var cell : rows.get(rowIndex).getTableCells()) {
                if (HAZARD_TABLE_LABELS.contains(cell.getText().trim())) {
                    continue;
                }
                cell.getParagraphs().stream()
                        .flatMap(paragraph -> paragraph.getRuns().stream())
                        .forEach(run -> run.setColor(color));
            }
        }
    }
    private JSONObject buildModelData(JSONObject data) {
        var modelData = new JSONObject();
        modelData.putAll(data);
        modelData.put("设备概况", buildDeviceSummary(data.getJSONArray("设备分组信息")));
        var evalInfo = new JSONObject();
        evalInfo.putAll(data.getJSONObject("评估结果"));
        var checkItems = evalInfo.getJSONArray("检查项");
        evalInfo.put("项目涉及安全子项", buildSafetySubItemText(checkItems));
        evalInfo.put("问题清单", buildHazardItems(checkItems));
        modelData.put("评估结果", evalInfo);
        return modelData;
    }
    private String buildDeviceSummary(JSONArray deviceGroups) {
        if (deviceGroups == null) {
            return "";
        }
        return deviceGroups.stream()
                .map(JSONObject.class::cast)
                .filter(group -> group.getString("设备类型") != null && !group.getString("设备类型").isBlank())
                .map(group -> group.getString("设备数量") + "台" + group.getString("设备类型"))
                .collect(Collectors.joining(";"));
    }
    private String buildSafetySubItemText(JSONArray checkItems) {
        if (checkItems == null || checkItems.isEmpty()) {
            return "";
        }
        var safetyCheckItems = checkItems.stream()
                .map(JSONObject.class::cast)
                .filter(item -> {
                    var name = item.getString("名称");
                    return name != null && name.contains("安全");
                })
                .collect(Collectors.toCollection(ArrayList::new));
        if (safetyCheckItems.isEmpty()) {
            safetyCheckItems.add(checkItems.getJSONObject(0));
        }
        var subItemNames = new ArrayList<String>();
        for (var checkItem : safetyCheckItems) {
            collectSubItemNames(checkItem, subItemNames);
        }
        return String.join("、", subItemNames);
    }
    private void collectSubItemNames(JSONObject checkItem, ArrayList<String> subItemNames) {
        var categories = checkItem.getJSONArray("分项评估");
        if (categories == null) {
            return;
        }
        for (var category : categories) {
            var subItems = ((JSONObject) category).getJSONArray("分项");
            if (subItems == null) {
                continue;
            }
            for (var subItem : subItems) {
                var name = ((JSONObject) subItem).getString("类别");
                if (name != null && !subItemNames.contains(name)) {
                    subItemNames.add(name);
                }
            }
        }
    }
    private JSONArray buildHazardItems(JSONArray checkItems) {
        var hazardItems = new JSONArray();
        if (checkItems == null) {
            return hazardItems;
        }
        int sequence = 1;
        for (var item : checkItems) {
            var categories = ((JSONObject) item).getJSONArray("分项评估");
            if (categories == null) {
                continue;
            }
            for (var category : categories) {
                var subItems = ((JSONObject) category).getJSONArray("分项");
                if (subItems == null) {
                    continue;
                }
                for (var subItem : subItems) {
                    var checkInfo = (JSONObject) subItem;
                    var problems = checkInfo.getJSONArray("问题");
                    if (problems == null) {
                        continue;
                    }
                    for (var problem : problems) {
                        var hazard = new JSONObject();
                        hazard.putAll((JSONObject) problem);
                        hazard.put("序号", sequence++);
                        hazard.put("检查分项", checkInfo.getString("类别"));
                        hazardItems.add(hazard);
                    }
                }
            }
        }
        return hazardItems;
    }
}
admin-server/src/main/java/com/estate/expert/admin/stat/report/render/ChengduHengtaiPictureRenderPolicy.java
New file
@@ -0,0 +1,38 @@
package com.estate.expert.admin.stat.report.render;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.Pictures;
import com.deepoove.poi.policy.PictureRenderPolicy;
import com.estate.expert.admin.service.UserFileService;
/**
 * 成都衡泰报告图片渲染策略
 */
public class ChengduHengtaiPictureRenderPolicy extends PictureRenderPolicy {
    private static final int PICTURE_WIDTH = 265;
    private static final int PICTURE_HEIGHT = 204;
    private final UserFileService userFileService;
    public ChengduHengtaiPictureRenderPolicy(UserFileService userFileService) {
        this.userFileService = userFileService;
    }
    @Override
    public PictureRenderData cast(Object source) throws Exception {
        var file = userFileService.get((Integer) source);
        if (file == null) {
            return null;
        }
        var resource = userFileService.resolveResource(file);
        if (resource == null) {
            return null;
        }
        var data = Pictures.ofStream(resource.getInputStream())
                .size(PICTURE_WIDTH, PICTURE_HEIGHT)
                .create();
        return super.cast(data);
    }
}
admin-server/src/main/resources/templates/第三方检查报告-成都衡泰.docx
Binary files differ
admin-server/src/test/java/com/estate/expert/admin/stat/report/builder/ChengduHengtaiSystemReportBuilderTest.java
New file
@@ -0,0 +1,53 @@
package com.estate.expert.admin.stat.report.builder;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.Document;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ChengduHengtaiSystemReportBuilderTest {
    private static final int TEMPLATE_PICTURE_ROW_HEIGHT = 3308;
    private static final byte[] ONE_PIXEL_PNG = Base64.getDecoder().decode(
            "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=");
    @Test
    void shouldCompactPictureRowWhenNoPictureExists() throws Exception {
        try (var document = createPictureTable()) {
            ChengduHengtaiSystemReportBuilder.compactEmptyPictureRows(document);
            assertEquals(240, document.getTables().get(0).getRow(1).getHeight());
        }
    }
    @Test
    void shouldKeepTemplateHeightWhenPictureExists() throws Exception {
        try (var document = createPictureTable()) {
            var pictureCell = document.getTables().get(0).getRow(1).getCell(0);
            pictureCell.getParagraphs().get(0).createRun().addPicture(
                    new ByteArrayInputStream(ONE_PIXEL_PNG),
                    Document.PICTURE_TYPE_PNG,
                    "pixel.png",
                    Units.pixelToEMU(1),
                    Units.pixelToEMU(1));
            ChengduHengtaiSystemReportBuilder.compactEmptyPictureRows(document);
            assertEquals(TEMPLATE_PICTURE_ROW_HEIGHT, document.getTables().get(0).getRow(1).getHeight());
        }
    }
    private XWPFDocument createPictureTable() {
        var document = new XWPFDocument();
        var table = document.createTable(2, 1);
        table.getRow(0).getCell(0).setText("问题图片");
        table.getRow(1).setHeight(TEMPLATE_PICTURE_ROW_HEIGHT);
        return document;
    }
}