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