inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderPlanServiceImpl.java
@@ -49,6 +49,7 @@
import com.ruoyi.inspect.util.ReportSignaturePictureUtils;
import com.ruoyi.inspect.vo.InsOrderPlanTaskSwitchVo;
import com.ruoyi.inspect.vo.InsOrderPlanVO;
import com.ruoyi.inspect.vo.QualityMonitorRawContext;
import com.ruoyi.inspect.vo.InsSampleUserVO;
import com.ruoyi.performance.mapper.AuxiliaryOutputWorkingHoursMapper;
import com.ruoyi.performance.pojo.AuxiliaryOutputWorkingHours;
@@ -224,6 +225,10 @@
        if (Objects.isNull(entity)) {
            return false;
        }
        InsOrder claimOrder = insOrderMapper.selectById(entity.getInsSampleId());
        if (claimOrder != null && "QUALITY_MONITOR".equals(claimOrder.getTaskSource())) {
            throw new ErrorException("质量监控任务由计划指定检测员,不能认领");
        }
        Integer userId = SecurityUtils.getUserId().intValue();
        InsSampleUser insSampleUser = new InsSampleUser(entity.getInsSampleId().intValue(), userId, 0, entity.getSonLaboratory());
        return insSampleUserMapper.insert(insSampleUser) > 0;
@@ -360,7 +365,7 @@
                }
            }
            map.put("errorMsg", collect);
            map.put("missingExportItems", getMissingTemplateExportValues(orderId));
            map.put("missingExportItems", getMissingTemplateExportValues(orderId, laboratory));
            map.put("unInsOrderCount", 0L);
            return map;
        }
@@ -613,6 +618,16 @@
        if (dto.getOrderId() == null || dto.getSampleId() == null || dto.getTemplateId() == null) {
            throw new ErrorException("订单、样品和模板不能为空");
        }
        InsOrder sourceOrder = insOrderMapper.selectById(dto.getOrderId());
        if (sourceOrder != null && "QUALITY_MONITOR".equals(sourceOrder.getTaskSource())
                && !Objects.equals(sourceOrder.getState(), 1)) {
            throw new ErrorException("已取消的质量监控任务不能填写原始记录");
        }
        if (sourceOrder != null && "QUALITY_MONITOR".equals(sourceOrder.getTaskSource())
                && !Objects.equals(SecurityUtils.getUserId().intValue(),
                    insOrderMapper.selectQualityMonitorInspectorId(dto.getOrderId()))) {
            throw new ErrorException("只有计划指定的检测员可以填写质量监控原始记录");
        }
        InsSample sample = insSampleMapper.selectById(dto.getSampleId());
        if (sample == null || !Objects.equals(sample.getInsOrderId(), dto.getOrderId())) {
            throw new ErrorException("样品不属于当前检验单");
@@ -634,10 +649,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());
@@ -874,8 +890,8 @@
    }
    /**
     * 检验保存、提交校验读取标准维护中的实时模板;模板已被删除或内容为空时才回退订单快照。
     * 检验完成后的展示和报告仍由订单快照负责,不会随标准模板后续修改而变化。
     * 读取标准维护中的实时模板;模板已被删除或内容为空时才回退订单快照。
     * 仅用于检验室仍处于待检、在检阶段(页面展示实时模板)的保存与校验。
     */
    private String getCurrentTemplateThing(InsOrderStandardTemplate snapshot) {
        if (snapshot != null && snapshot.getTemplateId() != null) {
@@ -888,9 +904,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)
@@ -900,9 +960,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());
@@ -1027,6 +1088,10 @@
    //交接
    @Override
    public int upPlanUser(Integer userId, Integer orderId, String sonLaboratory) {
        InsOrder handoffOrder = insOrderMapper.selectById(orderId);
        if (handoffOrder != null && "QUALITY_MONITOR".equals(handoffOrder.getTaskSource())) {
            throw new ErrorException("质量监控任务检测员由计划指定,不能直接交接");
        }
        InsSampleUser insSampleUser = new InsSampleUser();
        insSampleUser.setUserId(userId);
        insSampleUser.setInsSampleId(orderId);
@@ -1211,6 +1276,23 @@
        InsOrder order = insOrderMapper.selectOne(Wrappers.<InsOrder>lambdaQuery()
                .eq(InsOrder::getId, orderId)
                .last("FOR UPDATE"));
        if (order == null) throw new ErrorException("检验任务不存在");
        if ("QUALITY_MONITOR".equals(order.getTaskSource()) && !Objects.equals(order.getState(), 1)) {
            throw new ErrorException("已取消的质量监控任务不能提交");
        }
        if ("QUALITY_MONITOR".equals(order.getTaskSource())
                && (verifyUser == null || userMapper.selectById(verifyUser) == null)) {
            throw new ErrorException("请选择质量监控任务复核人");
        }
        if ("QUALITY_MONITOR".equals(order.getTaskSource())
                && Objects.equals(verifyUser, insOrderMapper.selectQualityMonitorInspectorId(orderId))) {
            throw new ErrorException("检测员不能复核自己的质量监控原始记录");
        }
        if ("QUALITY_MONITOR".equals(order.getTaskSource())
                && !Objects.equals(SecurityUtils.getUserId().intValue(),
                    insOrderMapper.selectQualityMonitorInspectorId(orderId))) {
            throw new ErrorException("只有计划指定的检测员可以提交质量监控任务");
        }
        boolean isInspectionReport = Objects.equals(INSPECTION_REPORT, reportType);
        registerInsResults = isInspectionReport && Boolean.TRUE.equals(registerInsResults);
        // 1. 委托编号全局唯一,提交复核时再次校验,防止并发或历史数据绕过生成校验。
@@ -1237,7 +1319,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));
            }
@@ -1623,6 +1705,8 @@
            refreshed.setRemark(standardTemplate == null ? oldSnapshot.getRemark() : standardTemplate.getRemark());
            refreshed.setThing(standardTemplate == null ? oldSnapshot.getThing() : standardTemplate.getThing());
            refreshed.setRecordValues(templateRecordValuesMap.get(templateId));
            refreshed.setDeviceDefaultsInitialized(oldSnapshot == null ? 0
                    : oldSnapshot.getDeviceDefaultsInitialized());
            refreshed.setTemperature(templateTemperatureMap.get(templateId));
            refreshed.setHumidity(templateHumidityMap.get(templateId));
            // 刷新模板结构时必须保留检验阶段填写的业务数据,避免进入复核后回显为空。
@@ -1675,11 +1759,25 @@
        if (orderId == null || laboratory == null || type == null) {
            throw new ErrorException("复核参数不完整");
        }
        InsOrder verifyOrder = insOrderMapper.selectById(orderId);
        if (verifyOrder == null) throw new ErrorException("检验任务不存在");
        if ("QUALITY_MONITOR".equals(verifyOrder.getTaskSource()) && !Objects.equals(verifyOrder.getState(), 1)) {
            throw new ErrorException("已取消的质量监控任务不能复核");
        }
        ensureDirectTemplateOrderState(orderId);
        InsOrderState currentState = insOrderStateMapper.selectOne(Wrappers.<InsOrderState>lambdaQuery()
                .eq(InsOrderState::getInsOrderId, orderId)
                .eq(InsOrderState::getLaboratory, laboratory)
                .last("limit 1"));
        if ("QUALITY_MONITOR".equals(verifyOrder.getTaskSource())
                && (currentState == null || currentState.getVerifyUser() == null
                    || !Objects.equals(SecurityUtils.getUserId().intValue(), currentState.getVerifyUser()))) {
            throw new ErrorException("只有提交时指定的复核人可以复核质量监控任务");
        }
        if ("QUALITY_MONITOR".equals(verifyOrder.getTaskSource()) && Objects.equals(type, 1)
                && currentState != null && Objects.equals(currentState.getInsState(), 5)) {
            return 1;
        }
        InsReport existingReport = insReportMapper.selectOne(Wrappers.<InsReport>lambdaQuery()
                .eq(InsReport::getInsOrderId, orderId)
                .last("limit 1"));
@@ -1738,6 +1836,24 @@
            insOrderMapper.update(null, Wrappers.<InsOrder>lambdaUpdate()
                    .eq(InsOrder::getId, orderId)
                    .set(InsOrder::getInsState, 5));
            // 质量监控任务只保留原始记录,后续由质量监控评价完成归档。
            if ("QUALITY_MONITOR".equals(order.getTaskSource())) {
                Integer evaluatorId = insOrderMapper.selectQualityMonitorEvaluatorId(orderId);
                if (evaluatorId != null) {
                    InformationNotification notice = new InformationNotification();
                    notice.setCreateUser(insProductMapper.selectUserById(SecurityUtils.getUserId().intValue()).get("name"));
                    notice.setSenderId(SecurityUtils.getUserId().intValue());
                    notice.setConsigneeId(evaluatorId);
                    notice.setMessageType("2");
                    notice.setTheme("质量监控评价通知");
                    notice.setContent("质量监控任务 " + order.getEntrustCode() + " 的原始记录已复核,请填写评价记录");
                    notice.setViewStatus(false);
                    notice.setJumpPath(MenuJumpPathConstants.QUALITY_MONITOR);
                    informationNotificationService.addInformationNotification(notice);
                }
                return 1;
            }
            // 检验人员提交复核时不生成报告;全部试验室复核通过后生成报告。
            if (report == null || StringUtils.isBlank(report.getUrl())) {
@@ -1812,9 +1928,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();
@@ -1877,6 +2001,11 @@
     * 兼容修复上线前已经下发、但因没有 ins_product 而缺少任务状态的直绑模板订单。
     */
    private void ensureDirectTemplateOrderState(Integer orderId) {
        InsOrder order = insOrderMapper.selectById(orderId);
        // 未下发的单还没进入检验任务,查看原始记录不能提前创建任务状态。
        if (order == null || order.getSendTime() == null) {
            return;
        }
        if (!isDirectTemplateOrder(orderId)) {
            return;
        }
@@ -2009,6 +2138,10 @@
     * @param orderId
     */
    private void generateReport(Integer orderId, Integer reportType, Integer writeUserId) {
        InsOrder sourceOrder = insOrderMapper.selectById(orderId);
        if (sourceOrder != null && "QUALITY_MONITOR".equals(sourceOrder.getTaskSource())) {
            throw new ErrorException("质量监控检验任务不生成报告");
        }
        /*样品下的项目只要有一个项目不合格则检验结果为0,否则为1*/
        //这里的insSamples是订单下的所有样品包括("/")
        List<InsSample> insSamples = insSampleMapper.selectList(Wrappers.<InsSample>lambdaQuery().eq(InsSample::getInsOrderId, orderId));
@@ -3774,6 +3907,40 @@
    /**
     * 新聚直绑模板订单:模板中每一个“导出值”批注都对应报告中的一条检测项目。
     */
    @Override
    public List<Map<String, String>> getQualityMonitorRawResults(Integer orderId) {
        requireReviewedQualityMonitorOrder(orderId);
        return buildTemplateDetectionResultRows(orderId);
    }
    @Override
    public QualityMonitorRawContext getQualityMonitorRawContext(Integer orderId) {
        requireReviewedQualityMonitorOrder(orderId);
        QualityMonitorRawContext context = new QualityMonitorRawContext();
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId));
        context.setDetectionDates(snapshots.stream().map(InsOrderStandardTemplate::getDetectionTime)
                .filter(Objects::nonNull).distinct().sorted().collect(Collectors.toList()));
        List<Integer> deviceIds = insOrderTemplateDeviceMapper.selectDeviceIdsByOrderId(orderId);
        if (CollectionUtils.isNotEmpty(deviceIds)) {
            context.setDevices(insOrderMapper.selectDeviceListByIds(new LinkedHashSet<>(deviceIds)));
        }
        return context;
    }
    private void requireReviewedQualityMonitorOrder(Integer orderId) {
        InsOrder order = insOrderMapper.selectById(orderId);
        if (order == null || !"QUALITY_MONITOR".equals(order.getTaskSource())
                || !Objects.equals(order.getInsState(), 5)) {
            throw new ErrorException("质控任务原始记录复核通过后才能读取检测数据");
        }
        Long pendingCount = insOrderStateMapper.selectCount(Wrappers.<InsOrderState>lambdaQuery()
                .eq(InsOrderState::getInsOrderId, orderId).ne(InsOrderState::getInsState, 5));
        if (pendingCount > 0) throw new ErrorException("质控任务仍有未复核的原始记录");
    }
    private List<Map<String, String>> buildTemplateDetectionResultRows(Integer orderId) {
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateService.list(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()