liding
6 天以前 3728fa293e1faf4bf4f9bf41105b84ddb43dc82e
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.ReportResultTableRenderPolicy;
import com.ruoyi.inspect.util.PreserveReportPicturePlaceholderHandler;
import com.ruoyi.inspect.util.ReportSignaturePictureUtils;
import com.ruoyi.inspect.vo.InsOrderPlanTaskSwitchVo;
@@ -359,13 +360,16 @@
                        "模板ID " + snapshot.getTemplateId());
                if (snapshot.getDetectionTime() == null) {
                    collect.add("请选择“" + templateName + "”的检测时间");
                } else if (snapshot.getDetectionEndTime() != null
                        && snapshot.getDetectionEndTime().isBefore(snapshot.getDetectionTime())) {
                    collect.add("“" + templateName + "”的检测结束日期不能早于开始日期");
                }
                if (StringUtils.isBlank(snapshot.getDetectionPlace())) {
                    collect.add("请填写“" + templateName + "”的检测地点");
                }
            }
            map.put("errorMsg", collect);
            map.put("missingExportItems", getMissingTemplateExportValues(orderId));
            map.put("missingExportItems", getMissingTemplateExportValues(orderId, laboratory));
            map.put("unInsOrderCount", 0L);
            return map;
        }
@@ -602,11 +606,9 @@
        }
        // 添加设备记录
        threadPoolTaskExecutor.execute(() -> {
            // 添加设备使用记录
            addDeviceRecord(insSample, userId);
        });
        // 原始记录保存后,按当前模板选用的设备生成使用记录
        syncDeviceRecordsFromTemplate(saveInsContextDto.getOrderId(), saveInsContextDto.getSampleId(),
                saveInsContextDto.getCurrentTable(), userId);
    }
    /**
@@ -649,10 +651,11 @@
        JSONObject values = JSON.parseObject(JSON.toJSONString(dto.getValues() == null
                ? Collections.emptyMap() : dto.getValues()));
        // 检验期间页面展示的是标准维护中的最新模板,保存校验必须使用同一份模板,
        // 避免模板新增“导出值”批注后仍按下单时的旧快照误报未配置。
        String currentTemplateThing = getCurrentTemplateThing(snapshot);
        List<String> exportValueCoordinates = getExportValueCoordinates(currentTemplateThing).values().stream()
        // 保存时必须使用页面当前展示的模板:待检、在检阶段展示标准维护中的实时模板,
        // 复核、驳回阶段展示订单快照;否则模板在检验途中被修改后会取错导出值单元格。
        boolean useSnapshot = useSnapshotTemplate(dto.getOrderId(), getCurrentUserLaboratory(dto.getOrderId()));
        String pageTemplateThing = getPageTemplateThing(snapshot, useSnapshot);
        List<String> exportValueCoordinates = getExportValueCoordinates(pageTemplateThing).values().stream()
                .flatMap(Collection::stream)
                .distinct()
                .collect(Collectors.toList());
@@ -688,6 +691,9 @@
                .and(w -> w.isNull(InsProduct::getLastValue).or().eq(InsProduct::getLastValue, "")));
        sample.setInsState(unfinished == 0 ? 2 : 1);
        insSampleMapper.updateById(sample);
        // 原始记录保存后,按当前模板选用的设备生成使用记录
        syncDeviceRecordsFromTemplate(dto.getOrderId(), dto.getSampleId(), dto.getTemplateId(), userId);
    }
    /**
@@ -889,8 +895,8 @@
    }
    /**
     * 检验保存、提交校验读取标准维护中的实时模板;模板已被删除或内容为空时才回退订单快照。
     * 检验完成后的展示和报告仍由订单快照负责,不会随标准模板后续修改而变化。
     * 读取标准维护中的实时模板;模板已被删除或内容为空时才回退订单快照。
     * 仅用于检验室仍处于待检、在检阶段(页面展示实时模板)的保存与校验。
     */
    private String getCurrentTemplateThing(InsOrderStandardTemplate snapshot) {
        if (snapshot != null && snapshot.getTemplateId() != null) {
@@ -903,9 +909,53 @@
    }
    /**
     * 检验单页面在检验室进入复核及之后状态时展示订单快照,导出值的保存和提交校验必须与页面
     * 使用同一份模板;否则标准模板在检验途中被修改后,历史单据会按新坐标读取快照数据而误报为空。
     */
    private String getPageTemplateThing(InsOrderStandardTemplate snapshot, boolean useSnapshot) {
        if (useSnapshot && snapshot != null && StrUtil.isNotBlank(snapshot.getThing())) {
            return snapshot.getThing();
        }
        return getCurrentTemplateThing(snapshot);
    }
    /**
     * 与检验单页面 getTemplateThing 的判定保持一致:检验室状态进入复核(2)及之后时使用订单快照。
     * 指派记录里的实验室名称可能和状态行不一致(如“全部”),此时退回按订单整体状态判定。
     */
    private boolean useSnapshotTemplate(Integer orderId, String laboratory) {
        if (StringUtils.isNotBlank(laboratory)) {
            InsOrderState state = insOrderStateMapper.selectOne(Wrappers.<InsOrderState>lambdaQuery()
                    .eq(InsOrderState::getInsOrderId, orderId)
                    .eq(InsOrderState::getLaboratory, laboratory)
                    .last("limit 1"));
            if (state != null) {
                return state.getInsState() != null && state.getInsState() >= 2;
            }
        }
        return insOrderStateMapper.selectList(Wrappers.<InsOrderState>lambdaQuery()
                        .eq(InsOrderState::getInsOrderId, orderId))
                .stream()
                .anyMatch(state -> state.getInsState() != null && state.getInsState() >= 2);
    }
    /**
     * 取当前登录人在该订单上最近一条指派记录所属的检验室;未指派账号返回 null,由状态判定兜底。
     */
    private String getCurrentUserLaboratory(Integer orderId) {
        Integer userId = SecurityUtils.getUserId().intValue();
        InsSampleUser sampleUser = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery()
                .eq(InsSampleUser::getInsSampleId, orderId)
                .eq(InsSampleUser::getUserId, userId)
                .orderByDesc(InsSampleUser::getId)
                .last("limit 1"));
        return sampleUser == null ? null : sampleUser.getSonLaboratory();
    }
    /**
     * 直绑模板订单只校验每个“导出值”批注所在区域右侧紧邻单元格,不使用检验项状态或 last_value 判定。
     */
    private List<String> getMissingTemplateExportValues(Integer orderId) {
    private List<String> getMissingTemplateExportValues(Integer orderId, String laboratory) {
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
@@ -915,9 +965,10 @@
            missing.add("当前订单未找到原始记录模板快照");
            return missing;
        }
        boolean useSnapshot = useSnapshotTemplate(orderId, laboratory);
        for (InsOrderStandardTemplate snapshot : snapshots) {
            Integer templateId = snapshot.getTemplateId();
            List<String> coordinates = getExportValueCoordinates(getCurrentTemplateThing(snapshot)).values().stream()
            List<String> coordinates = getExportValueCoordinates(getPageTemplateThing(snapshot, useSnapshot)).values().stream()
                    .flatMap(Collection::stream)
                    .distinct()
                    .collect(Collectors.toList());
@@ -982,60 +1033,52 @@
        }
    }
    private synchronized void addDeviceRecord(InsSample insSample, Integer userId) {
        InsOrder order = insOrderMapper.selectById(insSample.getInsOrderId());
    /**
     * 原始记录保存后,按当前模板选用的设备生成待填写的使用记录。
     * 同一订单、样品、设备只生成一条;已经填写过的记录不会被覆盖。
     */
    private synchronized void syncDeviceRecordsFromTemplate(Integer orderId, Integer sampleId,
                                                            Integer templateId, Integer userId) {
        if (orderId == null || sampleId == null || templateId == null) {
            return;
        }
        InsOrder order = insOrderMapper.selectById(orderId);
        InsSample sample = insSampleMapper.selectById(sampleId);
        if (order == null || sample == null) {
            return;
        }
        List<Integer> deviceIds = insOrderTemplateDeviceMapper
                .selectDeviceIdsByOrderAndTemplate(orderId, templateId);
        if (CollectionUtils.isEmpty(deviceIds)) {
            return;
        }
        User user = userMapper.selectById(userId);
        // 查询设备使用记录查询该订单的使用记录
        List<InsOrderDeviceRecordDto> deviceRecordDtoList = insOrderDeviceRecordMapper.selectDeviceNumber(insSample.getInsOrderId());
        Set<String> recordCodeset = deviceRecordDtoList.stream().map(InsOrderDeviceRecordDto::getManagementNumber).collect(Collectors.toSet());
        // 获取订单设备编号
        List<InsProductResult> resultList = insProductResultMapper.selectResultByOrderId(insSample.getInsOrderId());
        Set<String> deviceCodeSet = new HashSet<>();
        for (InsProductResult result : resultList) {
            // 添加设备编号
            List<JSONObject> jsonObjects = JSON.parseArray(result.getEquipValue(), JSONObject.class);
            for (JSONObject jsonObject : jsonObjects) {
                if (!"".equals(jsonObject.get("v") + "")) {
                    List<String> v = StrUtil.split(jsonObject.get("v") + "", ",");
                    deviceCodeSet.addAll(v);
                }
        String usePerson = user == null ? "" : user.getName();
        String sampleCode = StringUtils.isNotBlank(sample.getSampleCode())
                ? sample.getSampleCode() : order.getEntrustCode();
        for (Integer deviceId : deviceIds) {
            if (deviceId == null) {
                continue;
            }
            Long count = insOrderDeviceRecordMapper.selectCount(Wrappers.<InsOrderDeviceRecord>lambdaQuery()
                    .eq(InsOrderDeviceRecord::getInsOrderId, orderId)
                    .eq(InsOrderDeviceRecord::getDeviceId, deviceId)
                    .eq(InsOrderDeviceRecord::getSampleCode, sampleCode));
            if (count != null && count > 0) {
                continue;
            }
            InsOrderDeviceRecord record = new InsOrderDeviceRecord();
            record.setInsOrderId(orderId);
            record.setDeviceId(deviceId);
            record.setSampleCode(sampleCode);
            record.setTemperature(order.getTemperature());
            record.setHumidity(order.getHumidity());
            record.setUseBefore(1);
            record.setUseAfter(1);
            record.setUsePersonId(userId);
            record.setUsePerson(usePerson);
            insOrderDeviceRecordMapper.insert(record);
        }
        // 1.判断是否有没有添加的使用记录
        Set<String> orderDeviceNumbers = getDeviceDifference(deviceCodeSet, recordCodeset);
        // 添加使用记录, 根据编号查询设备id
        if (CollectionUtils.isNotEmpty(orderDeviceNumbers)) {
            List<Integer> orderDeviceIds = insOrderDeviceRecordMapper.selectDeviceIdsByNumbers(orderDeviceNumbers);
            List<InsOrderDeviceRecord> collect = orderDeviceIds.stream().map(deviceId -> {
                InsOrderDeviceRecord insOrderDeviceRecord = new InsOrderDeviceRecord();
                insOrderDeviceRecord.setInsOrderId(insSample.getInsOrderId());
                insOrderDeviceRecord.setDeviceId(deviceId);
                insOrderDeviceRecord.setSampleCode(order.getEntrustCode());
                insOrderDeviceRecord.setUseBefore(1);
                insOrderDeviceRecord.setUseAfter(1);
                insOrderDeviceRecord.setUsePerson(user.getName());
                insOrderDeviceRecord.setUsePersonId(user.getId());
                return insOrderDeviceRecord;
            }).collect(Collectors.toList());
            insOrderDeviceRecordService.saveBatch(collect);
        }
        // 2.判断是否取消了设备使用
        Set<String> repoprNumbers = getDeviceDifference(recordCodeset, deviceCodeSet);
        if (CollectionUtils.isNotEmpty(repoprNumbers)) {
            List<Integer> reportDeviceIds = insOrderDeviceRecordMapper.selectDeviceIdsByNumbers(repoprNumbers);
            insOrderDeviceRecordMapper.delete(Wrappers.<InsOrderDeviceRecord>lambdaQuery()
                    .in(InsOrderDeviceRecord::getDeviceId, reportDeviceIds)
                    .eq(InsOrderDeviceRecord::getInsOrderId, insSample.getInsOrderId()));
        }
    }
    private static Set<String> getDeviceDifference(Set<String> number1, Set<String> number2) {
        return number1.stream().filter(s1 -> number2.stream().noneMatch(s2 -> s1.equals(s2)))
                .collect(Collectors.toSet());
    }
@@ -1273,7 +1316,7 @@
        boolean directTemplateOrder = isDirectTemplateOrder(orderId);
        List<InsProduct> insProducts;
        if (directTemplateOrder) {
            List<String> missingExportValues = getMissingTemplateExportValues(orderId);
            List<String> missingExportValues = getMissingTemplateExportValues(orderId, laboratory);
            if (!missingExportValues.isEmpty()) {
                throw new ErrorException("模板中的导出值不能为空:\n" + String.join("\n", missingExportValues));
            }
@@ -1655,6 +1698,8 @@
            refreshed.setInsOrderId(orderId);
            refreshed.setTemplateId(templateId);
            refreshed.setNumber(standardTemplate == null ? oldSnapshot.getNumber() : standardTemplate.getNumber());
            refreshed.setStandardRequirement(oldSnapshot == null
                    ? standardTemplate.getStandardRequirement() : oldSnapshot.getStandardRequirement());
            refreshed.setName(standardTemplate == null ? oldSnapshot.getName() : standardTemplate.getName());
            refreshed.setRemark(standardTemplate == null ? oldSnapshot.getRemark() : standardTemplate.getRemark());
            refreshed.setThing(standardTemplate == null ? oldSnapshot.getThing() : standardTemplate.getThing());
@@ -1665,6 +1710,7 @@
            refreshed.setHumidity(templateHumidityMap.get(templateId));
            // 刷新模板结构时必须保留检验阶段填写的业务数据,避免进入复核后回显为空。
            refreshed.setDetectionTime(oldSnapshot == null ? null : oldSnapshot.getDetectionTime());
            refreshed.setDetectionEndTime(oldSnapshot == null ? null : oldSnapshot.getDetectionEndTime());
            refreshed.setDetectionPlace(oldSnapshot == null ? null : oldSnapshot.getDetectionPlace());
            refreshed.setInsResult(oldSnapshot == null ? null : oldSnapshot.getInsResult());
            Integer templateSort = templateSortMap.get(templateId);
@@ -1810,11 +1856,13 @@
            }
            // 检验人员提交复核时不生成报告;全部试验室复核通过后生成报告。
            if (report == null || StringUtils.isBlank(report.getUrl())) {
            boolean returnedFromReport = report != null && Objects.equals(report.getIsPass(), 0);
            if (report == null || StringUtils.isBlank(report.getUrl()) || returnedFromReport) {
                this.generateReport(orderId, DETECTION_REPORT, writeUserId);
            }
            // 直绑模板订单同时生成检验报告,两份文件共用同一条审批记录。
            if (directTemplateOrder && (report == null || StringUtils.isBlank(report.getInspectionUrl()))) {
            if (directTemplateOrder && (report == null || StringUtils.isBlank(report.getInspectionUrl())
                    || returnedFromReport)) {
                this.generateReport(orderId, INSPECTION_REPORT, writeUserId);
            }
            report = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
@@ -1882,9 +1930,17 @@
        } else {
            //复核不通过将把复核的负责人去掉
            Integer id = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery().eq(InsSampleUser::getInsSampleId, orderId).orderByDesc(InsSampleUser::getId).last("limit 1")).getId();
            insSampleUserMapper.deleteById(id);
            // 复核不通过只去掉本检验室本次提交的复核负责人;按复核人状态和检验室定位,
            // 避免误删其他检验室的记录或检验人员记录。
            InsSampleUser reviewerRow = insSampleUserMapper.selectOne(Wrappers.<InsSampleUser>lambdaQuery()
                    .eq(InsSampleUser::getInsSampleId, orderId)
                    .eq(InsSampleUser::getState, 1)
                    .eq(InsSampleUser::getSonLaboratory, laboratory)
                    .orderByDesc(InsSampleUser::getId)
                    .last("limit 1"));
            if (reviewerRow != null) {
                insSampleUserMapper.deleteById(reviewerRow.getId());
            }
            // 当前审核人
            Integer currentUserId = SecurityUtils.getUserId().intValue();
@@ -2004,7 +2060,8 @@
        if (Objects.equals(report.getIsRatify(), 1)) {
            throw new ErrorException("已批准报告不能重新生成");
        }
        if (StringUtils.isNotBlank(report.getUrlS())) {
        // urlS 同时用于人工上传和审核页面保存的 Word;只有人工上传需要先手动切回系统报告。
        if (StringUtils.isNotBlank(report.getUrlS()) && !isBrowserEditedReportUrl(report.getUrlS())) {
            throw new ErrorException("当前报告已手动上传,请先还原为系统报告后再重新生成");
        }
        if (report.getInsOrderId() == null) {
@@ -2048,7 +2105,12 @@
                .eq(InsReport::getId, reportId)
                .set(InsReport::getWriteTime, report.getWriteTime())
                .set(InsReport::getIsPass, report.getIsPass())
                .set(InsReport::getUrlS, null)
                .set(InsReport::getTempUrlPdf, "/word/" + tempPdfName));
    }
    private static boolean isBrowserEditedReportUrl(String url) {
        return url != null && url.matches("(?i)^/word/(?:edited|reviewed)-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\\.docx$");
    }
    private Map<String, Object> buildExistingReportMarks(InsReport report) {
@@ -3613,7 +3675,8 @@
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId));
        List<LocalDate> detectionDates = templateSnapshots.stream()
                .map(InsOrderStandardTemplate::getDetectionTime)
                .flatMap(snapshot -> java.util.stream.Stream.of(
                        snapshot.getDetectionTime(), snapshot.getDetectionEndTime()))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        String insTime = formatReportTestDate(detectionDates);
@@ -3629,7 +3692,7 @@
        List<Map<String, String>> finalDeviceList = deviceList;
        String finalModelStr = modelStr;
        // 直绑模板订单没有 ins_product,检测项目、单位和结果均从订单模板快照中获取。
        // 直绑模板订单没有 ins_product,检测项目、标准要求值和结果均从订单模板快照中获取。
        List<Map<String, String>> detectionResultRows = directTemplateOrder
                ? buildTemplateDetectionResultRows(orderId)
                : buildDetectionResultRows(samples);
@@ -3687,7 +3750,7 @@
        }
        Configure configure = Configure.builder()
                .bind("deviceList", new HackLoopTableRenderPolicy())
                .bind("r", new HackLoopTableRenderPolicy(true))
                .bind("r", new ReportResultTableRenderPolicy())
                .setValidErrorHandler(new PreserveReportPicturePlaceholderHandler())
                .useSpringEL(true)
                .build();
@@ -3811,7 +3874,7 @@
    }
    /**
     * 新版检测报告的结果表固定为四列:序号、检测项目、单位、检测结果。
     * 新版检测、检验报告共用结果数据;标准要求值取自原始记录模板配置。
     */
    private List<Map<String, String>> buildDetectionResultRows(List<SampleProductDto> samples) {
        List<Map<String, String>> rows = new ArrayList<>();
@@ -3820,6 +3883,7 @@
        }
        boolean multipleSamples = samples.size() > 1;
        Map<Integer, String> requirementsByTemplate = new HashMap<>();
        int sequence = 1;
        for (SampleProductDto sample : samples) {
            if (CollectionUtils.isEmpty(sample.getInsProduct())) {
@@ -3840,8 +3904,15 @@
                Map<String, String> row = new LinkedHashMap<>();
                row.put("seq", String.valueOf(sequence++));
                row.put("itemName", itemName);
                row.put("itemName", formatReportItemWithUnit(itemName, product.getUnit()));
                row.put("unit", defaultReportText(product.getUnit(), "/"));
                String requirement = product.getTemplateId() == null ? null
                        : requirementsByTemplate.computeIfAbsent(product.getTemplateId(), templateId -> {
                            StandardTemplate sourceTemplate = standardTemplateService.getById(templateId);
                            return defaultReportText(sourceTemplate == null ? null
                                    : sourceTemplate.getStandardRequirement(), "/");
                        });
                row.put("standardRequirement", defaultReportText(requirement, "/"));
                row.put("result", defaultReportText(product.getLastValue(), "/"));
                row.put("inspectionConclusion", formatInspectionConclusion(product.getInsResult()));
                rows.add(row);
@@ -3867,7 +3938,8 @@
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId));
        context.setDetectionDates(snapshots.stream().map(InsOrderStandardTemplate::getDetectionTime)
        context.setDetectionDates(snapshots.stream().flatMap(snapshot -> java.util.stream.Stream.of(
                        snapshot.getDetectionTime(), snapshot.getDetectionEndTime()))
                .filter(Objects::nonNull).distinct().sorted().collect(Collectors.toList()));
        List<Integer> deviceIds = insOrderTemplateDeviceMapper.selectDeviceIdsByOrderId(orderId);
        if (CollectionUtils.isNotEmpty(deviceIds)) {
@@ -3895,6 +3967,12 @@
        List<Map<String, String>> rows = new ArrayList<>();
        int sequence = 1;
        for (InsOrderStandardTemplate snapshot : snapshots) {
            String standardRequirement = snapshot.getStandardRequirement();
            // 兼容新增该字段前下单的旧记录;有快照时始终以订单保存的值为准。
            if (StringUtils.isBlank(standardRequirement) && snapshot.getTemplateId() != null) {
                StandardTemplate sourceTemplate = standardTemplateService.getById(snapshot.getTemplateId());
                standardRequirement = sourceTemplate == null ? null : sourceTemplate.getStandardRequirement();
            }
            List<TemplateExportCell> exportCells = getTemplateExportCells(snapshot.getThing());
            JSONObject recordValues = StrUtil.isBlank(snapshot.getRecordValues())
                    ? new JSONObject() : JSON.parseObject(snapshot.getRecordValues());
@@ -3913,8 +3991,10 @@
                row.put("seq", String.valueOf(sequence++));
                String reportItemName = defaultReportText(itemName,
                        defaultReportText(snapshot.getName(), "模板ID " + snapshot.getTemplateId()));
                row.put("itemName", formatExportItemName(reportItemName));
                row.put("itemName", formatReportItemWithUnit(
                        formatExportItemName(reportItemName), unit));
                row.put("unit", defaultReportText(unit, "/"));
                row.put("standardRequirement", defaultReportText(standardRequirement, "/"));
                row.put("result", result);
                row.put("inspectionConclusion", formatInspectionConclusion(snapshot.getInsResult()));
                rows.add(row);
@@ -4090,6 +4170,14 @@
        return itemName.replaceAll("平均值\\s*[,,]?\\s*", "");
    }
    private static String formatReportItemWithUnit(String itemName, String unit) {
        if (StringUtils.isBlank(unit) || "/".equals(unit.trim())
                || itemName.contains(unit.trim())) {
            return itemName;
        }
        return itemName + "," + unit.trim();
    }
    private static String getMapText(Map<String, String> map, String... keys) {
        if (map == null) {
            return null;