feat(report): 更新检测报告模板并优化报告生成功能
- 优化设备信息展示格式,支持多设备列表显示
- 统一检测报告和检验报告使用新版模板格式
已添加2个文件
已修改3个文件
242 ■■■■■ 文件已修改
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsReportServiceImpl.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/util/PreserveReportPicturePlaceholderHandler.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/resources/mapper/InsSampleMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/resources/static/detection-report-template-v2.docx 补丁 | 查看 | 原始文档 | blame | 历史
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -45,6 +45,7 @@
import com.ruoyi.inspect.pojo.*;
import com.ruoyi.inspect.service.*;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.PreserveReportPicturePlaceholderHandler;
import com.ruoyi.inspect.vo.InsOrderPlanTaskSwitchVo;
import com.ruoyi.inspect.vo.InsOrderPlanVO;
import com.ruoyi.inspect.vo.InsSampleUserVO;
@@ -781,12 +782,6 @@
        InsOrder order = insOrderMapper.selectOne(Wrappers.<InsOrder>lambdaQuery()
                .eq(InsOrder::getId, orderId)
                .last("FOR UPDATE"));
        InsReport existingReport = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, orderId));
        if (existingReport != null && existingReport.getReportType() != null
                && !Objects.equals(existingReport.getReportType(), reportType)) {
            throw new ErrorException("同一检验订单只能使用一种报告类型");
        }
        boolean isInspectionReport = Objects.equals(INSPECTION_REPORT, reportType);
        registerInsResults = isInspectionReport && Boolean.TRUE.equals(registerInsResults);
        // 1. åˆ¤æ–­æ˜¯å¦æœ‰é‡å¤ç¼–号, æœ‰é‡å¤ç¼–号做提醒
@@ -2497,6 +2492,11 @@
                    }
                }
            }
            // æ–°ç‰ˆæ£€æµ‹æŠ¥å‘Šç›´æŽ¥ä½¿ç”¨å›ºå®šå››åˆ—结果行,不再构造旧版可变列 TableRenderData。
            // æ£€éªŒæŠ¥å‘Šä»æ²¿ç”¨ä¸‹æ–¹åŽŸæœ‰çš„å¤§è¡¨æ ¼é€»è¾‘ã€‚
            if (!showResult) {
                return;
            }
            // æ”¶æ ·æ—¥æœŸ
            List<RowRenderData> rows = new ArrayList<>();
            List<TextRenderData> text = new ArrayList<>();
@@ -2688,7 +2688,9 @@
        for (String s : standardMethod) {
            standardMethod2.append("、").append(s);
        }
        standardMethod2.replace(0, 1, "");
        if (standardMethod2.length() > 0) {
            standardMethod2.deleteCharAt(0);
        }
        tables.forEach(table -> {
            table.put("tableSize", tables.size() + 1);
        });
@@ -2792,10 +2794,13 @@
        // æŠ½æ ·æ—¥æœŸ
        // æˆå“æ˜¯æŠ½æ ·æ—¶é—´
        LocalDateTime sendTime = insOrder.getSendTime();
        if (insOrder.getTypeSource() != null && insOrder.getTypeSource().equals(1)) {
        IfsInventoryQuantity reportInventory = null;
        if (insOrder.getIfsInventoryId() != null) {
            reportInventory = ifsInventoryQuantityMapper.selectById(insOrder.getIfsInventoryId());
        }
        if (insOrder.getTypeSource() != null && insOrder.getTypeSource().equals(1) && reportInventory != null) {
            // åŽŸææ–™æ”¶æ ·æ—¥æœŸæ˜¯æŠ¥æ£€æ—¥æœŸ
            IfsInventoryQuantity ifsInventoryQuantity = ifsInventoryQuantityMapper.selectById(insOrder.getIfsInventoryId());
            sendTime = ifsInventoryQuantity.getDeclareDate();
            sendTime = reportInventory.getDeclareDate();
        }
        // æ£€éªŒæ—¶é—´  æŠ½æ ·æ—¶é—´-提交时间
@@ -2824,12 +2829,60 @@
        List<Map<String, String>> finalDeviceList = deviceList;
        String finalModelStr = modelStr;
        InputStream inputStream = this.getClass().getResourceAsStream(showResult ? "/static/report-template.docx" : "/static/detection-report-template.docx");
        List<Map<String, String>> detectionResultRows = buildDetectionResultRows(samples);
        String productName = defaultReportText(insOrder.getSampleView(), insOrder.getSample());
        String customerName = defaultReportText(custom == null ? null : custom.getCompany(), insOrder.getCompany());
        String detectionCategory = defaultReportText(finalOrderType, "委托检测");
        String sampleCode = samples.stream()
                .map(SampleProductDto::getSampleCode)
                .filter(StringUtils::isNotBlank)
                .distinct()
                .collect(Collectors.joining("、"));
        String productAndModel = productName;
        if (StringUtils.isNotBlank(finalModelStr) && !Objects.equals(productName, finalModelStr)) {
            productAndModel = productName + " " + finalModelStr;
        }
        String standardMethodText = "null".equals(standardMethod2.toString()) ? "" : standardMethod2.toString();
        String productionDate = "/";
        String batchNo = "/";
        if (reportInventory != null) {
            if (reportInventory.getProductDate() != null) {
                productionDate = reportInventory.getProductDate().format(DateTimeFormatter.ofPattern("yyyyå¹´MM月dd日"));
            }
            batchNo = defaultReportText(reportInventory.getUpdateBatchNo(), reportInventory.getLotBatchNo());
        }
        boolean containsOrganicChlorine = detectionResultRows.stream()
                .map(row -> row.get("itemName"))
                .filter(Objects::nonNull)
                .anyMatch(name -> name.contains("有机氯"));
        String resultRemark = containsOrganicChlorine
                ? "微库仑法,有机氯含量小于0.005%时,以未检出报出。"
                : defaultReportText(insOrder.getRemark(), "/");
        // æ£€æµ‹æŠ¥å‘Šå’Œæ£€éªŒæŠ¥å‘Šç»Ÿä¸€ä½¿ç”¨æ–°ç‰ˆæŠ¥å‘Šæ¨¡æ¿
        String templateName = "/static/detection-report-template-v2.docx";
        InputStream inputStream = this.getClass().getResourceAsStream(templateName);
        if (inputStream == null) {
            throw new RuntimeException("找不到报告模板: " + templateName);
        }
        Configure configure = Configure.builder()
                .bind("deviceList", new HackLoopTableRenderPolicy())
                .bind("r", new HackLoopTableRenderPolicy(true))
                .setValidErrorHandler(new PreserveReportPicturePlaceholderHandler())
                .useSpringEL(true)
                .build();
        String finalProductAndModel = productAndModel;
        String finalProductName = productName;
        String finalCustomerName = customerName;
        String finalDetectionCategory = detectionCategory;
        String finalSampleCode = defaultReportText(sampleCode, "/");
        String finalStandardMethodText = defaultReportText(standardMethodText, "/");
        String finalProductionDate = productionDate;
        String finalBatchNo = batchNo;
        String finalResultRemark = resultRemark;
        String equipmentText = buildEquipmentText(finalDeviceList);
        String issueDate = now.format(DateTimeFormatter.ofPattern("yyyyå¹´MM月dd日"));
        XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                new HashMap<String, Object>() {{
                    put("order", insOrder);
@@ -2839,7 +2892,7 @@
                    put("sampleSize", insOrder.getTestQuantity());
                    put("tables", tables);
                    put("tableSize", tables.size() + 1);
                    put("standardMethod", (standardMethod2.toString().equals("null") ? "" : standardMethod2));
                    put("standardMethod", finalStandardMethodText);
                    put("deviceList", finalDeviceList);
                    put("twoCode", null);
                    put("models", finalModelStr);
@@ -2863,10 +2916,35 @@
                    put("resultCh", finalResultCh);
                    put("resultEn", finalResultEn);
                    put("sampleStatus", sampleStatus);
                    // æ–°ç‰ˆæ£€æµ‹æŠ¥å‘Šæ¨¡æ¿ï¼ˆå‚照《有机氯检测报告》)使用的扁平字段。
                    put("no", insReport.getCode());
                    put("cover", finalProductAndModel);
                    put("product", finalProductName);
                    put("model", defaultReportText(finalModelStr, "/"));
                    put("customer", finalCustomerName);
                    put("category", finalDetectionCategory);
                    put("date", issueDate);
                    put("maker", defaultReportText(insOrder.getProduction(), "/"));
                    put("env", finalEnvironment);
                    put("qty", defaultReportText(insOrder.getTestQuantity(), "/"));
                    put("receive", finalSendTime.format(DateTimeFormatter.ofPattern("yyyyå¹´MM月dd日")));
                    put("sampleNo", finalSampleCode);
                    put("appearance", defaultReportText(sampleStatus, "/"));
                    put("sender", defaultReportText(insOrder.getPrepareUser(), "/"));
                    put("produced", finalProductionDate);
                    put("batch", finalBatchNo);
                    put("phone", defaultReportText(insOrder.getPhone(), "/"));
                    put("testDate", insTime);
                    put("entrustNo", defaultReportText(insOrder.getEntrustCode(), "/"));
                    put("sampling", "/");
                    put("standard", finalStandardMethodText);
                    put("equipment", equipmentText);
                    put("conclusion", "样品按委托方要求进行检测,数据见检测结果。");
                    put("remark", "本报告检测结果仅对当日所接收样品负责。");
                    put("r", detectionResultRows);
                    put("resultNote", finalResultRemark);
                }});
        try {
            // ä¿®æ”¹æ¢è¡Œå’Œåˆå¹¶é—®é¢˜
            updaeMerge(template.getXWPFDocument(), false);
            String name = insReport.getCode().replace("/", "") + (showResult ? ".docx" : "-C.docx");
            Files.createDirectories(Paths.get(wordUrl));
            template.writeAndClose(Files.newOutputStream(Paths.get(wordUrl, name)));
@@ -2884,6 +2962,89 @@
    }
    /**
     * æ–°ç‰ˆæ£€æµ‹æŠ¥å‘Šçš„结果表固定为四列:序号、检测项目、单位、检测结果。
     */
    private static List<Map<String, String>> buildDetectionResultRows(List<SampleProductDto> samples) {
        List<Map<String, String>> rows = new ArrayList<>();
        if (CollectionUtils.isEmpty(samples)) {
            return rows;
        }
        boolean multipleSamples = samples.size() > 1;
        int sequence = 1;
        for (SampleProductDto sample : samples) {
            if (CollectionUtils.isEmpty(sample.getInsProduct())) {
                continue;
            }
            for (InsProduct product : sample.getInsProduct()) {
                // ç»‘定项目仅参与计算,不应出现在报告中。
                if (product == null || Objects.equals(product.getIsBinding(), 1)) {
                    continue;
                }
                String itemName = defaultReportText(
                        product.getInspectionItemSubclass(),
                        defaultReportText(product.getInspectionItem(), product.getInspectionItemClass()));
                if (multipleSamples && StringUtils.isNotBlank(sample.getSampleCode())) {
                    itemName = sample.getSampleCode() + " - " + itemName;
                }
                Map<String, String> row = new LinkedHashMap<>();
                row.put("seq", String.valueOf(sequence++));
                row.put("itemName", itemName);
                row.put("unit", defaultReportText(product.getUnit(), "/"));
                row.put("result", defaultReportText(product.getLastValue(), "/"));
                rows.add(row);
            }
        }
        return rows;
    }
    private static String buildEquipmentText(List<Map<String, String>> deviceList) {
        if (CollectionUtils.isEmpty(deviceList)) {
            return "/";
        }
        Set<String> lines = new LinkedHashSet<>();
        int sequence = 1;
        for (Map<String, String> device : deviceList) {
            String name = getMapText(device, "deviceName", "device_name");
            String managementNumber = getMapText(device, "managementNumber", "management_number");
            String line = sequence++ + ". " + defaultReportText(name, "/");
            if (StringUtils.isNotBlank(managementNumber)) {
                line += ":" + managementNumber;
            }
            lines.add(line);
        }
        return String.join("\n", lines);
    }
    private static String getMapText(Map<String, String> map, String... keys) {
        if (map == null) {
            return null;
        }
        for (String key : keys) {
            String value = map.get(key);
            if (StringUtils.isNotBlank(value)) {
                return value;
            }
            for (Map.Entry<String, String> entry : map.entrySet()) {
                if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(key)
                        && StringUtils.isNotBlank(entry.getValue())) {
                    return entry.getValue();
                }
            }
        }
        return null;
    }
    private static String defaultReportText(String value, String fallback) {
        if (StringUtils.isNotBlank(value)) {
            return value;
        }
        return StringUtils.isNotBlank(fallback) ? fallback : "/";
    }
    /**
     * è®¡ç®—表格列宽度
     * @param max æœ€å¤§æ£€éªŒä¸ªæ•°
     * @return
inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsReportServiceImpl.java
@@ -17,6 +17,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.data.Pictures;
import com.itextpdf.text.BadElementException;
import com.itextpdf.text.DocumentException;
@@ -44,6 +45,7 @@
import com.ruoyi.inspect.service.InsOrderService;
import com.ruoyi.inspect.service.InsReportService;
import com.ruoyi.inspect.service.RawMaterialOrderService;
import com.ruoyi.inspect.util.PreserveReportPicturePlaceholderHandler;
import com.ruoyi.system.mapper.UserMapper;
import com.ruoyi.system.service.InformationNotificationService;
import lombok.extern.slf4j.Slf4j;
@@ -523,7 +525,10 @@
    @Override
    public int wordInsertUrl(Map<String, Object> map, String url) {
        XWPFTemplate template = XWPFTemplate.compile(url).render(map);
        Configure configure = Configure.builder()
                .setValidErrorHandler(new PreserveReportPicturePlaceholderHandler())
                .build();
        XWPFTemplate template = XWPFTemplate.compile(url, configure).render(map);
        try {
            template.writeAndClose(Files.newOutputStream(Paths.get(url)));
        } catch (IOException e) {
inspect-server/src/main/java/com/ruoyi/inspect/util/PreserveReportPicturePlaceholderHandler.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package com.ruoyi.inspect.util;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.render.RenderContext;
import com.deepoove.poi.template.ElementTemplate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
 * æŠ¥å‘Šä¸­çš„签名和印章会在编制、审核、批准等不同阶段分别写入。
 * ä¿ç•™åŽç»­é˜¶æ®µéœ€è¦ä½¿ç”¨çš„图片占位符,避免在前面的渲染阶段被提前清除。
 */
public class PreserveReportPicturePlaceholderHandler implements Configure.ValidErrorHandler {
    private static final Set<String> DEFERRED_PICTURE_TAGS = new HashSet<>(Arrays.asList(
            "writeUrl",
            "writeDateUrl",
            "insUrl",
            "examineUrl",
            "examineDateUrl",
            "ratifyUrl",
            "ratifyDateUrl",
            "seal1",
            "seal2"
    ));
    private final Configure.ClearHandler clearHandler = new Configure.ClearHandler();
    @Override
    public void handler(RenderContext<?> context) {
        ElementTemplate template = context.getEleTemplate();
        if (template != null
                && Character.valueOf('@').equals(template.getSign())
                && DEFERRED_PICTURE_TAGS.contains(template.getTagName())) {
            return;
        }
        clearHandler.handler(context);
    }
}
inspect-server/src/main/resources/mapper/InsSampleMapper.xml
@@ -61,6 +61,7 @@
        <result property="tell" column="tell" jdbcType="VARCHAR"/>
        <result property="lastValue" column="last_value" jdbcType="VARCHAR"/>
        <result property="insResult" column="ip_ins_result" jdbcType="INTEGER"/>
        <result property="isBinding" column="ip_is_binding" jdbcType="INTEGER"/>
        <result property="state" column="state" jdbcType="INTEGER"/>
        <result property="insBushId" column="ins_bush_id" jdbcType="INTEGER"/>
        <result property="insSampleId" column="ins_sample_id" jdbcType="INTEGER"/>
@@ -143,6 +144,7 @@
               ask,
               `last_value`,
               ip.ins_result  ip_ins_result,
                ip.is_binding  ip_is_binding,
               state,
               ins_sample_id,
               ip.create_user ip_create_user,
@@ -357,6 +359,7 @@
               tell,
               `last_value`,
               ip.ins_result  ip_ins_result,
               ip.is_binding  ip_is_binding,
               state,
               ins_sample_id,
               ip.create_user ip_create_user,
@@ -416,6 +419,7 @@
               ask,
               `last_value`,
               ip.ins_result  ip_ins_result,
               ip.is_binding  ip_is_binding,
               state,
               ins_sample_id,
               ip.create_user ip_create_user,
@@ -506,6 +510,7 @@
               tell,
               `last_value`,
               ip.ins_result  ip_ins_result,
               ip.is_binding  ip_is_binding,
               state,
               ins_sample_id,
               ip.create_user ip_create_user,
inspect-server/src/main/resources/static/detection-report-template-v2.docx
Binary files differ