inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -46,6 +46,7 @@
import com.ruoyi.inspect.service.*;
import com.ruoyi.inspect.util.HackLoopTableRenderPolicy;
import com.ruoyi.inspect.util.PreserveReportPicturePlaceholderHandler;
import com.ruoyi.inspect.util.ReportSignaturePictureUtils;
import com.ruoyi.inspect.vo.InsOrderPlanTaskSwitchVo;
import com.ruoyi.inspect.vo.InsOrderPlanVO;
import com.ruoyi.inspect.vo.InsSampleUserVO;
@@ -178,8 +179,12 @@
        String userName = null;
        Integer userId = null;
        if (ObjectUtil.isNotEmpty(insOrderPlanDTO.getUserId())) {
            userId = SecurityUtils.getUserId().intValue();
            userName = userMapper.selectById(userId).getName();
            Integer currentUserId = SecurityUtils.getUserId().intValue();
            // 管理员可以查看并复核全部待复核任务,普通用户仍只查看分配给自己的任务。
            if (!SecurityUtils.isAdmin(currentUserId.longValue())) {
                userId = currentUserId;
                userName = userMapper.selectById(userId).getName();
            }
            insOrderPlanDTO.setUserId(null);
        }
        Integer isCheck = insOrderPlanDTO.getIsCheck();
@@ -745,18 +750,9 @@
        } catch (Exception ignored) {
            thing = compressedThing;
        }
        JSONArray cellData = JSON.parseObject(thing).getJSONArray("data").getJSONObject(0).getJSONArray("celldata");
        JSONObject sheet = JSON.parseObject(thing).getJSONArray("data").getJSONObject(0);
        JSONArray cellData = sheet.getJSONArray("celldata");
        NavigableMap<Integer, List<String>> result = new TreeMap<>();
        Integer inspectionResultColumn = null;
        for (Object item : cellData) {
            JSONObject cell = JSON.parseObject(JSON.toJSONString(item));
            Integer column = cell.getInteger("c");
            JSONObject value = cell.getJSONObject("v");
            String displayValue = value == null ? null : value.getString("v");
            if (inspectionResultColumn == null && "检验结果".equals(StringUtils.trim(displayValue))) {
                inspectionResultColumn = column;
            }
        }
        for (Object item : cellData) {
            JSONObject cell = JSON.parseObject(JSON.toJSONString(item));
            JSONObject value = cell.getJSONObject("v");
@@ -767,16 +763,119 @@
            Integer row = cell.getInteger("r");
            Integer column = cell.getInteger("c");
            if (row != null && column != null) {
                // “导出值”标在检测项目标题单元格时,真正导出的值是同一行“检验结果”列的单元格。
                String coordinate = row + "-" + column;
                String resultCoordinate = row + "-" + inspectionResultColumn;
                if (inspectionResultColumn != null && !Objects.equals(column, inspectionResultColumn)) {
                    coordinate = resultCoordinate;
                }
                // 批注单元格可能横向合并,右侧结果单元格也可能纵向合并;最终读取结果区域的左上角存储坐标。
                String coordinate = getRightAdjacentCoordinate(sheet, row, column);
                result.computeIfAbsent(row, ignored -> new ArrayList<>()).add(coordinate);
            }
        }
        return result;
    }
    /**
     * 返回指定单元格(或其所在合并区域)右侧紧邻列的下标。
     * Luckysheet 的合并信息通常保存在 config.merge;部分历史模板只在单元格 v.mc 中保留,
     * 因此两种结构都要兼容。
     */
    private int getRightAdjacentColumn(JSONObject sheet, int row, int column) {
        int rightEdge = column;
        JSONObject config = sheet == null ? null : sheet.getJSONObject("config");
        JSONObject merges = config == null ? null : config.getJSONObject("merge");
        if (merges != null) {
            for (Object rawMerge : merges.values()) {
                rightEdge = extendMergedRightEdge(rightEdge, row, column, rawMerge);
            }
        }
        JSONArray cellData = sheet == null ? null : sheet.getJSONArray("celldata");
        if (cellData != null) {
            for (Object rawCell : cellData) {
                JSONObject cell = JSON.parseObject(JSON.toJSONString(rawCell));
                JSONObject value = cell == null ? null : cell.getJSONObject("v");
                JSONObject merge = value == null ? null : value.getJSONObject("mc");
                rightEdge = extendMergedRightEdge(rightEdge, row, column, merge);
            }
        }
        return rightEdge + 1;
    }
    private String getRightAdjacentCoordinate(JSONObject sheet, int row, int column) {
        int adjacentColumn = getRightAdjacentColumn(sheet, row, column);
        int[] anchor = findMergedAnchor(sheet, row, adjacentColumn);
        return anchor[0] + "-" + anchor[1];
    }
    /**
     * Luckysheet 只在合并区域左上角保存实际值。若目标单元格位于合并区域内部,
     * 将其转换为该区域的左上角坐标;未合并则返回原坐标。
     */
    private int[] findMergedAnchor(JSONObject sheet, int row, int column) {
        JSONObject config = sheet == null ? null : sheet.getJSONObject("config");
        JSONObject merges = config == null ? null : config.getJSONObject("merge");
        if (merges != null) {
            for (Object rawMerge : merges.values()) {
                int[] anchor = findMergedAnchor(row, column, rawMerge);
                if (anchor != null) {
                    return anchor;
                }
            }
        }
        JSONArray cellData = sheet == null ? null : sheet.getJSONArray("celldata");
        if (cellData != null) {
            for (Object rawCell : cellData) {
                JSONObject cell = JSON.parseObject(JSON.toJSONString(rawCell));
                JSONObject value = cell == null ? null : cell.getJSONObject("v");
                int[] anchor = findMergedAnchor(row, column,
                        value == null ? null : value.getJSONObject("mc"));
                if (anchor != null) {
                    return anchor;
                }
            }
        }
        return new int[]{row, column};
    }
    private int[] findMergedAnchor(int row, int column, Object rawMerge) {
        if (rawMerge == null) {
            return null;
        }
        JSONObject merge = rawMerge instanceof JSONObject
                ? (JSONObject) rawMerge : JSON.parseObject(JSON.toJSONString(rawMerge));
        Integer startRow = merge.getInteger("r");
        Integer startColumn = merge.getInteger("c");
        Integer rowSpan = merge.getInteger("rs");
        Integer columnSpan = merge.getInteger("cs");
        if (startRow == null || startColumn == null || rowSpan == null || columnSpan == null
                || rowSpan < 1 || columnSpan < 1) {
            return null;
        }
        if (row >= startRow && row < startRow + rowSpan
                && column >= startColumn && column < startColumn + columnSpan) {
            return new int[]{startRow, startColumn};
        }
        return null;
    }
    private int extendMergedRightEdge(int currentRightEdge, int row, int column, Object rawMerge) {
        if (rawMerge == null) {
            return currentRightEdge;
        }
        JSONObject merge = rawMerge instanceof JSONObject
                ? (JSONObject) rawMerge : JSON.parseObject(JSON.toJSONString(rawMerge));
        Integer startRow = merge.getInteger("r");
        Integer startColumn = merge.getInteger("c");
        Integer rowSpan = merge.getInteger("rs");
        Integer columnSpan = merge.getInteger("cs");
        if (startRow == null || startColumn == null || rowSpan == null || columnSpan == null
                || rowSpan < 1 || columnSpan < 1) {
            return currentRightEdge;
        }
        int endRow = startRow + rowSpan - 1;
        int endColumn = startColumn + columnSpan - 1;
        if (row >= startRow && row <= endRow && column >= startColumn && column <= endColumn) {
            return Math.max(currentRightEdge, endColumn);
        }
        return currentRightEdge;
    }
    /**
@@ -794,7 +893,7 @@
    }
    /**
     * 直绑模板订单只校验模板中标记为“导出值”的单元格,不使用检验项状态或 last_value 判定。
     * 直绑模板订单只校验每个“导出值”批注所在区域右侧紧邻单元格,不使用检验项状态或 last_value 判定。
     */
    private List<String> getMissingTemplateExportValues(Integer orderId) {
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
@@ -820,12 +919,12 @@
            JSONObject recordValues = StrUtil.isBlank(snapshot.getRecordValues())
                    ? new JSONObject() : JSON.parseObject(snapshot.getRecordValues());
            // 保存时同样只取模板中的第一个导出值,提交校验与保存口径保持一致。
            String coordinate = coordinates.get(0);
            JSONObject cell = recordValues.getJSONObject(coordinate);
            Object value = cell == null ? null : cell.get("v");
            if (value == null || StringUtils.isBlank(String.valueOf(value))) {
                missing.add(templateName + "(" + coordinate + ")");
            for (String coordinate : coordinates) {
                JSONObject cell = recordValues.getJSONObject(coordinate);
                Object value = cell == null ? null : cell.get("v");
                if (value == null || StringUtils.isBlank(String.valueOf(value))) {
                    missing.add(templateName + "(" + coordinate + ")");
                }
            }
        }
        return missing;
@@ -1119,13 +1218,12 @@
                .last("FOR UPDATE"));
        boolean isInspectionReport = Objects.equals(INSPECTION_REPORT, reportType);
        registerInsResults = isInspectionReport && Boolean.TRUE.equals(registerInsResults);
        // 1. 判断是否有重复编号, 有重复编号做提醒
        // 1. 委托编号全局唯一,提交复核时再次校验,防止并发或历史数据绕过生成校验。
        Long codeCount = insOrderMapper.selectCount(Wrappers.<InsOrder>lambdaQuery()
                .ne(InsOrder::getState, -1)
                .ne(InsOrder::getIfsInventoryId, order.getIfsInventoryId())
                .ne(InsOrder::getId, orderId)
                .eq(InsOrder::getEntrustCode, order.getEntrustCode()));
        if (codeCount > 0) {
            throw new ErrorException("当前编号有重复, 请先去修改重复编号");
            throw new ErrorException("样品/委托单已存在,请确认样品编号");
        }
        // 2. 判断该订单是否是第一次生产(后续报告生成只取第一次提交时间)
@@ -1532,6 +1630,10 @@
            refreshed.setRecordValues(templateRecordValuesMap.get(templateId));
            refreshed.setTemperature(templateTemperatureMap.get(templateId));
            refreshed.setHumidity(templateHumidityMap.get(templateId));
            // 刷新模板结构时必须保留检验阶段填写的业务数据,避免进入复核后回显为空。
            refreshed.setDetectionTime(oldSnapshot == null ? null : oldSnapshot.getDetectionTime());
            refreshed.setDetectionPlace(oldSnapshot == null ? null : oldSnapshot.getDetectionPlace());
            refreshed.setInsResult(oldSnapshot == null ? null : oldSnapshot.getInsResult());
            Integer templateSort = templateSortMap.get(templateId);
            refreshed.setSort(templateSort == null ? nextTemplateSort++ : templateSort);
            refreshedSnapshots.add(refreshed);
@@ -1586,12 +1688,13 @@
        InsReport existingReport = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, orderId)
                .last("limit 1"));
        boolean directTemplateOrder = isDirectTemplateOrder(orderId);
        if (currentState != null && Objects.equals(currentState.getInsState(), 5)
                && existingReport != null && StringUtils.isNotBlank(existingReport.getUrl())) {
                && existingReport != null && StringUtils.isNotBlank(existingReport.getUrl())
                && (!directTemplateOrder || StringUtils.isNotBlank(existingReport.getInspectionUrl()))) {
            // 防止复核页面重复点击或网络重试重复生成报告、重复发起报告审批。
            return 1;
        }
        boolean directTemplateOrder = isDirectTemplateOrder(orderId);
        if (directTemplateOrder && Objects.equals(type, 1)
                && !Objects.equals(dto.getInsResult(), 0) && !Objects.equals(dto.getInsResult(), 1)) {
            throw new ErrorException("请选择检验单结论");
@@ -1640,15 +1743,22 @@
                    .eq(InsOrder::getId, orderId)
                    .set(InsOrder::getInsState, 5));
            // 检验人员提交复核时不再生成报告;仅在全部试验室复核通过后生成检测报告。
            // 检验人员提交复核时不生成报告;全部试验室复核通过后生成报告。
            if (report == null || StringUtils.isBlank(report.getUrl())) {
                this.generateReport(orderId, DETECTION_REPORT, writeUserId);
                report = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                        .eq(InsReport::getInsOrderId, orderId)
                        .last("limit 1"));
            }
            // 直绑模板订单同时生成检验报告,两份文件共用同一条审批记录。
            if (directTemplateOrder && (report == null || StringUtils.isBlank(report.getInspectionUrl()))) {
                this.generateReport(orderId, INSPECTION_REPORT, writeUserId);
            }
            report = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                    .eq(InsReport::getInsOrderId, orderId)
                    .last("limit 1"));
            if (report == null || StringUtils.isBlank(report.getUrl())) {
                throw new ErrorException("检测报告生成失败");
            }
            if (directTemplateOrder && StringUtils.isBlank(report.getInspectionUrl())) {
                throw new ErrorException("检验报告生成失败");
            }
            if (report.getTempUrlPdf() == null) {
                String tempUrlPdf = this.wordToPdfTemp(report.getUrl().replace("/word", wordUrl));
@@ -1798,6 +1908,101 @@
            throw new ErrorException("找不到当前检验任务的检验人,无法生成检测报告");
        }
        return inspector.getUserId();
    }
    /**
     * 重新生成尚未批准的系统报告,并恢复已存在的编制、审核签名。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void regenerateReport(Integer reportId, Integer createOrderUser) {
        if (reportId == null) {
            throw new ErrorException("报告不能为空");
        }
        InsReport accessibleReport = insReportMapper.selectDownloadableReport(reportId, createOrderUser);
        if (accessibleReport == null) {
            throw new ErrorException("报告不存在、尚未生成或无权操作");
        }
        // 数据库行锁避免同一报告被多人同时重新生成并覆盖。
        InsReport report = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getId, reportId)
                .last("FOR UPDATE"));
        if (report == null) {
            throw new ErrorException("报告不存在");
        }
        if (Objects.equals(report.getIsRatify(), 1)) {
            throw new ErrorException("已批准报告不能重新生成");
        }
        if (StringUtils.isNotBlank(report.getUrlS())) {
            throw new ErrorException("当前报告已手动上传,请先还原为系统报告后再重新生成");
        }
        if (report.getInsOrderId() == null) {
            throw new ErrorException("报告未关联检验单,无法重新生成");
        }
        Integer reportType = report.getReportType() == null ? DETECTION_REPORT : report.getReportType();
        Integer writeUserId = report.getWriteUserId() == null
                ? SecurityUtils.getUserId().intValue() : report.getWriteUserId();
        boolean directTemplateOrder = isDirectTemplateOrder(report.getInsOrderId());
        if (directTemplateOrder) {
            generateReport(report.getInsOrderId(), DETECTION_REPORT, writeUserId);
            generateReport(report.getInsOrderId(), INSPECTION_REPORT, writeUserId);
        } else {
            generateReport(report.getInsOrderId(), reportType, writeUserId);
        }
        InsReport regenerated = insReportMapper.selectById(reportId);
        if (regenerated == null || StringUtils.isBlank(regenerated.getUrl())) {
            throw new ErrorException("报告重新生成失败");
        }
        if (directTemplateOrder && StringUtils.isBlank(regenerated.getInspectionUrl())) {
            throw new ErrorException("检验报告重新生成失败");
        }
        String reportPath = regenerated.getUrl().replace("/word", wordUrl);
        Map<String, Object> marks = buildExistingReportMarks(report);
        if (!marks.isEmpty()) {
            insReportService.wordInsertUrl(marks, reportPath);
            if (StringUtils.isNotBlank(regenerated.getInspectionUrl())) {
                insReportService.wordInsertUrl(marks,
                        regenerated.getInspectionUrl().replace("/word", wordUrl));
            }
        }
        String tempPdfName = wordToPdfTemp(reportPath);
        if (StringUtils.isBlank(tempPdfName)) {
            throw new ErrorException("报告预览文件生成失败");
        }
        // generateReport 会写入当前时间;重新生成不应改变原审批记录时间。
        insReportMapper.update(null, Wrappers.<InsReport>lambdaUpdate()
                .eq(InsReport::getId, reportId)
                .set(InsReport::getWriteTime, report.getWriteTime())
                .set(InsReport::getTempUrlPdf, "/word/" + tempPdfName));
    }
    private Map<String, Object> buildExistingReportMarks(InsReport report) {
        Map<String, Object> marks = new HashMap<>();
        boolean submitted = Objects.equals(report.getState(), 1)
                || report.getIsExamine() != null
                || report.getExamineTime() != null;
        if (submitted) {
            User writer = getReportSigner(report.getWriteUserId(), "编制人");
            marks.put("writeUrl", ReportSignaturePictureUtils.create(imgUrl, writer.getSignatureUrl()));
            marks.put("writeDateUrl", Pictures.ofStream(DateImageUtil.createDateImage(report.getWriteTime())).create());
            marks.put("insUrl", ReportSignaturePictureUtils.create(imgUrl, writer.getSignatureUrl()));
        }
        if (Objects.equals(report.getIsExamine(), 1)) {
            User examiner = getReportSigner(report.getExamineUserId(), "审核人");
            marks.put("examineUrl", ReportSignaturePictureUtils.create(imgUrl, examiner.getSignatureUrl()));
            marks.put("examineDateUrl", Pictures.ofStream(DateImageUtil.createDateImage(report.getExamineTime())).create());
        }
        return marks;
    }
    private User getReportSigner(Integer userId, String roleName) {
        User user = userId == null ? null : userMapper.selectById(userId);
        if (user == null || StringUtils.isBlank(user.getSignatureUrl())) {
            throw new ErrorException("找不到" + roleName + "的签名");
        }
        return user;
    }
    /**
@@ -2331,13 +2536,18 @@
        enterFactoryReport.setLotBatchNo(ifsInventoryQuantity.getUpdateBatchNo());
        // 检测依据
        Set<String> standardMethod = new HashSet<>();
        Set<String> standardMethod = new LinkedHashSet<>();
        StringBuilder standardMethod2 = new StringBuilder();
        standardMethod.add(baseMapper.getStandardMethodCode(insSample.getStandardMethodListId()));
        String reportStandardMethod = baseMapper.getStandardMethodReportText(insSample.getStandardMethodListId());
        if (StringUtils.isNotBlank(reportStandardMethod)) {
            standardMethod.add(reportStandardMethod);
        }
        for (String s : standardMethod) {
            standardMethod2.append("、").append(s);
        }
        standardMethod2.replace(0, 1, "");
        if (standardMethod2.length() > 0) {
            standardMethod2.deleteCharAt(0);
        }
        // 样品类型
        String orderType = iSysDictTypeService.selectLabelByDict(DictDataConstants.CHECK_TYPE, insOrder.getOrderType());
@@ -3009,7 +3219,7 @@
        insReport.setReportType(reportType);
        boolean showResult = Objects.equals(reportType, INSPECTION_REPORT);
        List<Map<String, Object>> tables = new ArrayList<>();
        Set<String> standardMethod = new HashSet<>();
        Set<String> standardMethod = new LinkedHashSet<>();
        Set<String> deviceSet = new HashSet<>();
        Set<String> models = new HashSet<>();
        // 查询检验项数量
@@ -3022,11 +3232,13 @@
        /*基础报告(根据绘制的原始记录模版形成)*/
        samples.forEach(a -> {
            models.add(a.getModel());
            String standardMethodCode = baseMapper.getStandardMethodCode(a.getStandardMethodListId());
            if (StrUtil.isNotBlank(a.getSpecialStandardMethod())) {
                standardMethodCode = standardMethodCode + "+" + a.getSpecialStandardMethod();
            String standardMethodCode = baseMapper.getStandardMethodReportText(a.getStandardMethodListId());
            if (StrUtil.isNotBlank(standardMethodCode)) {
                if (StrUtil.isNotBlank(a.getSpecialStandardMethod())) {
                    standardMethodCode = standardMethodCode + "+" + a.getSpecialStandardMethod();
                }
                standardMethod.add(standardMethodCode);
            }
            standardMethod.add(standardMethodCode);
            for (InsProduct b : a.getInsProduct()) {
                if (b.getInsProductResult() != null) {
                    List<JSONObject> jsonObjects = JSON.parseArray(b.getInsProductResult().getEquipValue(), JSONObject.class);
@@ -3038,9 +3250,9 @@
                    }
                }
            }
            // 新版检测报告直接使用固定四列结果行,不再构造旧版可变列 TableRenderData。
            // 检验报告仍沿用下方原有的大表格逻辑。
            if (!showResult) {
            // 直绑模板的检测报告、检验报告都使用新版固定结果行,不再进入旧版检验项表格逻辑。
            // 直绑模板中的承载记录没有旧检验项字段,进入旧逻辑会在格式化时产生空指针。
            if (!showResult || directTemplateOrder) {
                return;
            }
            // 收样日期
@@ -3372,9 +3584,14 @@
        if (firstInspectDate == null) {
            firstInspectDate = insOrder.getSendTime();
        }
        if (firstInspectDate == null) {
            firstInspectDate = submitTime;
        }
        String insTime = firstInspectDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")) + "-"
                + submitTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
        String testStartDate = firstInspectDate.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
        String testEndDate = submitTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
        String insTimeEn = monthNames[firstInspectDate.getMonthValue() - 1] + " " + firstInspectDate.format(DateTimeFormatter.ofPattern("dd, yyyy")) + "-"
                + monthNames[submitTime.getMonthValue() - 1] + " " + submitTime.format(DateTimeFormatter.ofPattern("dd, yyyy"));
@@ -3431,11 +3648,8 @@
                productionDate = insOrder.getProductionDate().format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
            }
            batchNo = defaultReportText(insOrder.getProductionBatch(), "/");
            standardMethodText = templateSnapshots.stream()
                    .map(InsOrderStandardTemplate::getRemark)
                    .filter(StringUtils::isNotBlank)
                    .distinct()
                    .collect(Collectors.joining(";"));
            standardMethodText = defaultReportText(
                    baseMapper.getStandardMethodReportTextByCode(insOrder.getSample()), insOrder.getSample());
        }
        boolean containsOrganicChlorine = detectionResultRows.stream()
                .map(row -> row.get("itemName"))
@@ -3445,8 +3659,9 @@
                ? "微库仑法,有机氯含量小于0.005%时,以未检出报出。"
                : defaultReportText(insOrder.getRemark(), "/");
        // 检测报告和检验报告统一使用新版报告模板
        String templateName = "/static/detection-report-template-v2.docx";
        String templateName = showResult
                ? "/static/inspection-report-template-v2.docx"
                : "/static/detection-report-template-v2.docx";
        InputStream inputStream = this.getClass().getResourceAsStream(templateName);
        if (inputStream == null) {
            throw new RuntimeException("找不到报告模板: " + templateName);
@@ -3523,6 +3738,8 @@
                    put("batch", finalBatchNo);
                    put("phone", defaultReportText(insOrder.getPhone(), "/"));
                    put("testDate", insTime);
                    put("testStartDate", testStartDate);
                    put("testEndDate", testEndDate);
                    put("entrustNo", defaultReportText(insOrder.getEntrustCode(), "/"));
                    put("sampling", "/");
                    put("standard", finalStandardMethodText);
@@ -3533,7 +3750,7 @@
                    put("resultNote", finalResultRemark);
                }});
        try {
            String name = insReport.getCode().replace("/", "") + (showResult ? ".docx" : "-C.docx");
            String name = insReport.getCode().replace("/", "") + (showResult ? "-J.docx" : "-C.docx");
            Files.createDirectories(Paths.get(wordUrl));
            template.writeAndClose(Files.newOutputStream(Paths.get(wordUrl, name)));
            insReport.setUrl("/word/" + name);
@@ -3550,6 +3767,22 @@
        InsReport existing = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, report.getInsOrderId())
                .last("limit 1"));
        if (Objects.equals(report.getReportType(), INSPECTION_REPORT)) {
            String inspectionUrl = report.getUrl();
            if (existing == null) {
                report.setReportType(DETECTION_REPORT);
                report.setUrl(null);
                report.setInspectionUrl(inspectionUrl);
                insReportMapper.insert(report);
            } else {
                insReportMapper.update(null, Wrappers.<InsReport>lambdaUpdate()
                        .eq(InsReport::getId, existing.getId())
                        .set(InsReport::getInspectionUrl, inspectionUrl)
                        .set(InsReport::getWriteUserId, report.getWriteUserId())
                        .set(InsReport::getWriteTime, report.getWriteTime()));
            }
            return;
        }
        if (existing == null) {
            insReportMapper.insert(report);
            return;
@@ -3591,6 +3824,7 @@
                row.put("itemName", itemName);
                row.put("unit", defaultReportText(product.getUnit(), "/"));
                row.put("result", defaultReportText(product.getLastValue(), "/"));
                row.put("inspectionConclusion", formatInspectionConclusion(product.getInsResult()));
                rows.add(row);
            }
        }
@@ -3598,7 +3832,7 @@
    }
    /**
     * 新聚直绑模板订单:一个模板页签在报告中只对应一条检测项目。
     * 新聚直绑模板订单:模板中每一个“导出值”批注都对应报告中的一条检测项目。
     */
    private List<Map<String, String>> buildTemplateDetectionResultRows(Integer orderId) {
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
@@ -3608,27 +3842,30 @@
        List<Map<String, String>> rows = new ArrayList<>();
        int sequence = 1;
        for (InsOrderStandardTemplate snapshot : snapshots) {
            TemplateExportCell exportCell = getFirstTemplateExportCell(snapshot.getThing());
            List<TemplateExportCell> exportCells = getTemplateExportCells(snapshot.getThing());
            JSONObject recordValues = StrUtil.isBlank(snapshot.getRecordValues())
                    ? new JSONObject() : JSON.parseObject(snapshot.getRecordValues());
            String result = readTemplateCellText(recordValues, exportCell.resultCoordinate, exportCell.templateValues);
            if (StringUtils.isBlank(result)) {
                throw new ErrorException("模板“" + defaultReportText(snapshot.getName(), "模板ID " + snapshot.getTemplateId())
                        + "”的导出值不能为空");
            }
            String unit = exportCell.unitCoordinate == null ? null
                    : readTemplateCellText(recordValues, exportCell.unitCoordinate, exportCell.templateValues);
            String itemName = exportCell.itemCoordinate == null ? null
                    : readTemplateCellText(recordValues, exportCell.itemCoordinate, exportCell.templateValues);
            for (TemplateExportCell exportCell : exportCells) {
                String result = readTemplateCellText(recordValues, exportCell.resultCoordinate, exportCell.templateValues);
                if (StringUtils.isBlank(result)) {
                    throw new ErrorException("模板“" + defaultReportText(snapshot.getName(), "模板ID " + snapshot.getTemplateId())
                            + "”的导出值不能为空(" + exportCell.resultCoordinate + ")");
                }
                String unit = exportCell.unitCoordinate == null ? null
                        : readTemplateCellText(recordValues, exportCell.unitCoordinate, exportCell.templateValues);
                String itemName = exportCell.itemCoordinate == null ? null
                        : readTemplateCellText(recordValues, exportCell.itemCoordinate, exportCell.templateValues);
            Map<String, String> row = new LinkedHashMap<>();
            row.put("seq", String.valueOf(sequence++));
            String reportItemName = defaultReportText(itemName,
                    defaultReportText(snapshot.getName(), "模板ID " + snapshot.getTemplateId()));
            row.put("itemName", formatExportItemName(reportItemName));
            row.put("unit", defaultReportText(unit, "/"));
            row.put("result", result);
            rows.add(row);
                Map<String, String> row = new LinkedHashMap<>();
                row.put("seq", String.valueOf(sequence++));
                String reportItemName = defaultReportText(itemName,
                        defaultReportText(snapshot.getName(), "模板ID " + snapshot.getTemplateId()));
                row.put("itemName", formatExportItemName(reportItemName));
                row.put("unit", defaultReportText(unit, "/"));
                row.put("result", result);
                row.put("inspectionConclusion", formatInspectionConclusion(snapshot.getInsResult()));
                rows.add(row);
            }
        }
        if (rows.isEmpty()) {
            throw new ErrorException("当前订单未找到可生成报告的原始记录模板");
@@ -3636,7 +3873,17 @@
        return rows;
    }
    private TemplateExportCell getFirstTemplateExportCell(String compressedThing) {
    private static String formatInspectionConclusion(Integer insResult) {
        if (Objects.equals(insResult, 1)) {
            return "合格";
        }
        if (Objects.equals(insResult, 0)) {
            return "不合格";
        }
        return "/";
    }
    private List<TemplateExportCell> getTemplateExportCells(String compressedThing) {
        if (StrUtil.isBlank(compressedThing)) {
            throw new ErrorException("原始记录模板为空");
        }
@@ -3646,9 +3893,8 @@
        } catch (Exception ignored) {
            thing = compressedThing;
        }
        JSONArray cellData = JSON.parseObject(thing).getJSONArray("data").getJSONObject(0).getJSONArray("celldata");
        Integer resultColumn = null;
        Integer itemColumn = null;
        JSONObject sheet = JSON.parseObject(thing).getJSONArray("data").getJSONObject(0);
        JSONArray cellData = sheet.getJSONArray("celldata");
        Integer unitColumn = null;
        List<int[]> exportCells = new ArrayList<>();
        Map<String, String> templateValues = new HashMap<>();
@@ -3662,13 +3908,6 @@
            }
            String displayValue = value.get("v") == null ? null : String.valueOf(value.get("v"));
            templateValues.put(row + "-" + column, displayValue);
            if (resultColumn == null && "检验结果".equals(StringUtils.trim(displayValue))) {
                resultColumn = column;
            }
            if (itemColumn == null && ("检测项目".equals(StringUtils.trim(displayValue))
                    || "检验项目".equals(StringUtils.trim(displayValue)))) {
                itemColumn = column;
            }
            if (unitColumn == null && "单位".equals(StringUtils.trim(displayValue))) {
                unitColumn = column;
            }
@@ -3681,17 +3920,16 @@
            throw new ErrorException("模板必须至少配置一个导出值批注");
        }
        exportCells.sort(Comparator.comparingInt((int[] cell) -> cell[0]).thenComparingInt(cell -> cell[1]));
        int[] firstExport = exportCells.get(0);
        int resultColumnForRow = resultColumn == null ? firstExport[1] : resultColumn;
        // 通常“导出值”批注配置在项目名称单元格;若配置在结果单元格,则回到“检测项目”列取名称。
        Integer itemColumnForRow = !Objects.equals(firstExport[1], resultColumnForRow)
                ? firstExport[1] : itemColumn;
        TemplateExportCell result = new TemplateExportCell();
        result.resultCoordinate = firstExport[0] + "-" + resultColumnForRow;
        result.itemCoordinate = itemColumnForRow == null ? null : firstExport[0] + "-" + itemColumnForRow;
        result.unitCoordinate = unitColumn == null ? null : firstExport[0] + "-" + unitColumn;
        result.templateValues = templateValues;
        return result;
        List<TemplateExportCell> results = new ArrayList<>();
        for (int[] exportCell : exportCells) {
            TemplateExportCell result = new TemplateExportCell();
            result.resultCoordinate = getRightAdjacentCoordinate(sheet, exportCell[0], exportCell[1]);
            result.itemCoordinate = exportCell[0] + "-" + exportCell[1];
            result.unitCoordinate = unitColumn == null ? null : exportCell[0] + "-" + unitColumn;
            result.templateValues = templateValues;
            results.add(result);
        }
        return results;
    }
    private String readTemplateCellText(JSONObject recordValues, String coordinate, Map<String, String> templateValues) {
@@ -5134,7 +5372,7 @@
                }
            }
            // 判断是否是数字类型
            if (sampleProductDto2.getInspectionValueType().equals("1")) {
            if ("1".equals(sampleProductDto2.getInspectionValueType())) {
                // 把检验内容如果正硫化点和焦烧时间把  .  切割成  :
                String lastValue = sampleProductDto2.getLastValue();
                if (sampleProductDto2.getInspectionItem().contains("正硫化点") || sampleProductDto2.getInspectionItem().contains("焦烧时间")) {