inspect-server/src/main/java/com/ruoyi/inspect/service/impl/InsOrderServiceImpl.java
@@ -55,6 +55,7 @@
import com.ruoyi.inspect.vo.TemplateConditionVo;
import com.ruoyi.system.mapper.UserMapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.context.ApplicationEventPublisher;
@@ -80,6 +81,7 @@
 * @description 针对表【ins_order(检验下单)】的数据库操作Service实现
 * @createDate 2024-03-12 16:17:55
 */
@Slf4j
@Service
@AllArgsConstructor
public class InsOrderServiceImpl extends ServiceImpl<InsOrderMapper, InsOrder>
@@ -300,6 +302,9 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addInsOrder(List<SampleProductDto> list, InsOrder insOrder) {
        if (insOrder == null || insOrder.getSampleDate() == null) {
            throw new ErrorException("送样日期不能为空");
        }
        // todo: 下单判断抽样计划的唯一性
        if (insOrder.getQuarterItemId() != null) {
            Long quarterItemCount = insOrderMapper.selectCount(Wrappers.<InsOrder>lambdaQuery()
@@ -502,6 +507,64 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addNewJuInsOrder(InsPlaceOrderDto dto) {
        return createNewJuOrder(dto, null, false);
    }
    /**
     * 批量下单:公共委托信息只填一次,后台按样品逐条拆成独立委托单并共享批次号。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int addBatchNewJuInsOrder(InsBatchPlaceOrderDto dto) {
        if (dto == null || dto.getInsOrder() == null) {
            throw new ErrorException("下单信息不能为空");
        }
        List<InsBatchSampleDto> rows = dto.getSampleList();
        if (CollectionUtils.isEmpty(rows)) {
            throw new ErrorException("请至少添加一个样品");
        }
        // 只有一个样品时按普通单样品下单处理,不产生批次
        if (rows.size() == 1) {
            return createNewJuOrder(buildBatchRowOrder(dto.getInsOrder(), rows.get(0)), null, false);
        }
        String batchNo = "PC" + DateUtil.format(new Date(), "yyyyMMddHHmmssSSS");
        int firstOrderId = 0;
        for (InsBatchSampleDto row : rows) {
            int orderId = createNewJuOrder(buildBatchRowOrder(dto.getInsOrder(), row), batchNo, true);
            if (firstOrderId == 0) {
                firstOrderId = orderId;
            }
        }
        return firstOrderId;
    }
    private InsPlaceOrderDto buildBatchRowOrder(InsOrder common, InsBatchSampleDto row) {
        if (common == null || row == null) {
            throw new ErrorException("下单信息不能为空");
        }
        if (row.getStandardTreeId() == null) {
            throw new ErrorException("请为每个样品选择检测依据");
        }
        if (StringUtils.isBlank(row.getSampleCode()) || row.getSampleCode().trim().length() < 2) {
            throw new ErrorException("样品编号不能为空且不能少于2位");
        }
        InsOrder order = new InsOrder();
        BeanUtils.copyProperties(common, order);
        order.setId(null);
        order.setEntrustCode(null);
        order.setBatchNo(null);
        order.setSampleView(row.getSampleView());
        order.setTestQuantity(row.getTestQuantity());
        order.setSampleStatus(row.getSampleStatus());
        InsPlaceOrderDto single = new InsPlaceOrderDto();
        single.setInsOrder(order);
        single.setStandardTreeId(row.getStandardTreeId());
        single.setSampleCode(row.getSampleCode());
        single.setTemplateList(row.getTemplateList());
        return single;
    }
    private int createNewJuOrder(InsPlaceOrderDto dto, String batchNo, boolean batchEntrustFallback) {
        if (dto == null || dto.getInsOrder() == null || dto.getStandardTreeId() == null) {
            throw new ErrorException("下单信息或标准不能为空");
        }
@@ -593,6 +656,7 @@
        InsOrder order = dto.getInsOrder();
        order.setId(null);
        order.setBatchNo(StringUtils.isBlank(batchNo) ? null : batchNo);
        // 新聚下单免审核:直接受理,下单即流转到样品接收
        order.setState(1);
        order.setExamineTime(LocalDateTime.now());
@@ -653,6 +717,10 @@
        // 下单时生成委托编号并校验唯一性,重复则提示,避免留到审核才报错。
        String entrustCode = buildEntrustCode(order, Collections.singletonList(sample));
        if (batchEntrustFallback && isEntrustCodeUsed(entrustCode)) {
            // 批量下单时不同样品编号末两位可能相同,改用当天顺延编号兜底
            entrustCode = buildNextAvailableEntrustCode(order);
        }
        assertEntrustCodeUnique(entrustCode, order.getId());
        insOrderMapper.update(null, Wrappers.<InsOrder>lambdaUpdate()
                .eq(InsOrder::getId, order.getId())
@@ -844,6 +912,7 @@
            snapshot.setTemplateId(template.getId());
            snapshot.setName(template.getName());
            snapshot.setNumber(template.getNumber());
            snapshot.setStandardRequirement(template.getStandardRequirement());
            snapshot.setRemark(template.getRemark());
            snapshot.setThing(template.getThing());
            snapshot.setSort(selection.getSort());
@@ -1044,6 +1113,23 @@
        }
    }
    private boolean isEntrustCodeUsed(String entrustCode) {
        return StringUtils.isNotBlank(entrustCode) && insOrderMapper.selectCount(Wrappers.<InsOrder>lambdaQuery()
                .eq(InsOrder::getEntrustCode, entrustCode)) > 0;
    }
    private String buildNextAvailableEntrustCode(InsOrder order) {
        LocalDate entrustDate = order.getSampleDate() == null ? LocalDate.now() : order.getSampleDate();
        String prefix = "LMSY-" + entrustDate.format(DateTimeFormatter.BASIC_ISO_DATE) + "-";
        for (int seq = 1; seq <= 99; seq++) {
            String candidate = prefix + String.format("%02d", seq);
            if (!isEntrustCodeUsed(candidate)) {
                return candidate;
            }
        }
        throw new ErrorException("当天委托编号已用尽,请调整样品编号");
    }
    /**
     * 生成委托编号:新聚直绑模板订单末两位取样品编号末两位,其余按送样日期生成序号。
     */
@@ -1067,7 +1153,11 @@
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> getInsOrderAndSample(Integer id, String laboratory) {
        Map<String, Object> map = new HashMap<>();
        initializeTemplateDevicesOnFirstOpen(id);
        try {
            initializeTemplateDevicesOnFirstOpen(id, laboratory);
        } catch (RuntimeException e) {
            log.warn("检验任务默认设备同步失败,orderId={}", id, e);
        }
//        InsOrder insOrder = insOrderMapper.selectById(id);
        IfsOrderVO ifsOrderVO = insOrderMapper.selectOrderInfoById(id);
        Map<Integer, List<Integer>> templateDeviceIds = insOrderTemplateDeviceMapper.selectList(
@@ -1085,6 +1175,7 @@
                                .select(InsOrderStandardTemplate::getTemplateId,
                                        InsOrderStandardTemplate::getTemperature,
                                        InsOrderStandardTemplate::getDetectionTime,
                                        InsOrderStandardTemplate::getDetectionEndTime,
                                        InsOrderStandardTemplate::getDetectionPlace,
                                        InsOrderStandardTemplate::getInsResult)
                                .orderByAsc(InsOrderStandardTemplate::getSort,
@@ -1094,6 +1185,8 @@
                    condition.setTemplateId(snapshot.getTemplateId());
                    condition.setTemperature(snapshot.getTemperature());
                    condition.setDetectionTime(snapshot.getDetectionTime());
                    condition.setDetectionEndTime(snapshot.getDetectionEndTime() == null
                            ? snapshot.getDetectionTime() : snapshot.getDetectionEndTime());
                    condition.setDetectionPlace(snapshot.getDetectionPlace());
                    condition.setInsResult(snapshot.getInsResult());
                    condition.setDeviceIds(templateDeviceIds.getOrDefault(
@@ -1110,18 +1203,34 @@
        return map;
    }
    /** 首次打开检验任务时,将仍为空的模板设备从对应标准补入订单。 */
    private void initializeTemplateDevicesOnFirstOpen(Integer orderId) {
    /**
     * 打开检验任务时,把尚未被人工改动过的模板设备同步为标准库当前绑定的设备;
     * 检验员手动增删过的模板以人工为准,不再自动填充。
     */
    private void initializeTemplateDevicesOnFirstOpen(Integer orderId, String laboratory) {
        InsOrder order = insOrderMapper.selectById(orderId);
        if (order == null || !DIRECT_TEMPLATE_LABORATORY.equals(order.getLaboratory())
                || !Objects.equals(order.getTypeSource(), -1)
        if (order == null || !Objects.equals(order.getTypeSource(), -1)
                || !Objects.equals(order.getState(), 1)
                || "QUALITY_MONITOR".equals(order.getTaskSource())) {
            return;
        }
        if (StringUtils.isNotBlank(laboratory)) {
            InsOrderState orderState = insOrderStateService.getOne(Wrappers.<InsOrderState>lambdaQuery()
                    .eq(InsOrderState::getInsOrderId, orderId)
                    .eq(InsOrderState::getLaboratory, laboratory)
                    .last("limit 1"));
            // 仅待检、在检阶段同步;进入复核等后续状态后以保存下来的设备为准。
            if (orderState != null && orderState.getInsState() != null
                    && orderState.getInsState() != 0 && orderState.getInsState() != 1) {
                return;
            }
        }
        List<InsOrderStandardTemplate> snapshots = insOrderStandardTemplateMapper.selectList(
                Wrappers.<InsOrderStandardTemplate>lambdaQuery()
                        .eq(InsOrderStandardTemplate::getInsOrderId, orderId)
                        .select(InsOrderStandardTemplate::getId,
                                InsOrderStandardTemplate::getTemplateId,
                                InsOrderStandardTemplate::getDeviceDefaultsInitialized)
                        .orderByAsc(InsOrderStandardTemplate::getSort, InsOrderStandardTemplate::getId)
                        .last("FOR UPDATE"));
        if (snapshots.isEmpty() || snapshots.stream().allMatch(
@@ -1129,41 +1238,57 @@
            return;
        }
        Set<Integer> existingTemplateIds = insOrderTemplateDeviceMapper.selectList(
                Wrappers.<InsOrderTemplateDevice>lambdaQuery()
                        .eq(InsOrderTemplateDevice::getInsOrderId, orderId))
                .stream().map(InsOrderTemplateDevice::getTemplateId)
                .filter(Objects::nonNull).collect(Collectors.toSet());
        boolean inspectionStarted = order.getFirstInspectDate() != null
                || (order.getInsState() != null && order.getInsState() > 0)
                || snapshots.stream().anyMatch(snapshot -> StringUtils.isNotBlank(snapshot.getRecordValues())
                        || snapshot.getDetectionTime() != null || snapshot.getTemperature() != null
                        || snapshot.getDetectionPlace() != null);
        Map<Integer, List<Integer>> defaults = inspectionStarted ? Collections.emptyMap()
                : findStandardDeviceDefaults(order, snapshots);
        Map<Integer, List<InsOrderTemplateDevice>> existingDevices = insOrderTemplateDeviceMapper.selectList(
                        Wrappers.<InsOrderTemplateDevice>lambdaQuery()
                                .eq(InsOrderTemplateDevice::getInsOrderId, orderId)
                                .orderByAsc(InsOrderTemplateDevice::getSort, InsOrderTemplateDevice::getId))
                .stream().filter(relation -> relation.getTemplateId() != null)
                .collect(Collectors.groupingBy(InsOrderTemplateDevice::getTemplateId));
        Map<Integer, List<Integer>> defaults = findStandardDeviceDefaults(order, snapshots);
        for (InsOrderStandardTemplate snapshot : snapshots) {
            // 检验员手动选择或清空过的模板以人工为准,不再套用标准库默认值。
            if (Objects.equals(snapshot.getDeviceDefaultsInitialized(), 1)) {
                continue;
            }
            Integer templateId = snapshot.getTemplateId();
            if (!inspectionStarted && !existingTemplateIds.contains(templateId)
                    && defaults.containsKey(templateId)) {
                List<Integer> deviceIds = defaults.get(templateId);
                for (int index = 0; index < deviceIds.size(); index++) {
                    InsOrderTemplateDevice relation = new InsOrderTemplateDevice();
                    relation.setInsOrderId(orderId);
                    relation.setTemplateId(templateId);
                    relation.setDeviceId(deviceIds.get(index));
                    relation.setSort(index);
                    insOrderTemplateDeviceMapper.insert(relation);
                }
            if (templateId == null) {
                continue;
            }
            // 无法唯一确定来源时也只尝试这一次,避免以后标准库调整时意外改动订单。
            insOrderStandardTemplateMapper.update(null,
                    Wrappers.<InsOrderStandardTemplate>lambdaUpdate()
                            .eq(InsOrderStandardTemplate::getId, snapshot.getId())
                            .set(InsOrderStandardTemplate::getDeviceDefaultsInitialized, 1));
            List<InsOrderTemplateDevice> currentDevices = existingDevices.getOrDefault(
                    templateId, Collections.emptyList());
            // 旧数据没有初始化标记,已有设备视为人工选择,标记后不再自动覆盖。
            if (snapshot.getDeviceDefaultsInitialized() == null && !currentDevices.isEmpty()) {
                insOrderStandardTemplateMapper.update(null,
                        Wrappers.<InsOrderStandardTemplate>lambdaUpdate()
                                .eq(InsOrderStandardTemplate::getId, snapshot.getId())
                                .set(InsOrderStandardTemplate::getDeviceDefaultsInitialized, 1));
                continue;
            }
            if (!defaults.containsKey(templateId)) {
                continue;
            }
            List<Integer> standardDeviceIds = defaults.get(templateId).stream()
                    .filter(Objects::nonNull).distinct().collect(Collectors.toList());
            List<Integer> currentDeviceIds = currentDevices.stream()
                    .map(InsOrderTemplateDevice::getDeviceId)
                    .filter(Objects::nonNull).distinct().collect(Collectors.toList());
            if (currentDeviceIds.equals(standardDeviceIds)) {
                continue;
            }
            // 未初始化过的模板完全对齐标准库绑定:绑定几个就显示几个。
            insOrderTemplateDeviceMapper.delete(
                    Wrappers.<InsOrderTemplateDevice>lambdaQuery()
                            .eq(InsOrderTemplateDevice::getInsOrderId, orderId)
                            .eq(InsOrderTemplateDevice::getTemplateId, templateId));
            for (int index = 0; index < standardDeviceIds.size(); index++) {
                InsOrderTemplateDevice relation = new InsOrderTemplateDevice();
                relation.setInsOrderId(orderId);
                relation.setTemplateId(templateId);
                relation.setDeviceId(standardDeviceIds.get(index));
                relation.setSort(index);
                insOrderTemplateDeviceMapper.insert(relation);
            }
        }
    }
@@ -1184,23 +1309,29 @@
                        .isNotNull(StandardTree::getSample)
                        .isNull(StandardTree::getSampleType)
                        .isNull(StandardTree::getModel));
                List<StandardTree> matches = candidates.stream()
                        .filter(tree -> StringUtils.isNotBlank(tree.getSample()))
                        .filter(tree -> {
                            Set<Integer> boundIds = standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                    .map(StandardTreeTemplateBindingVo::getTemplateId).collect(Collectors.toSet());
                            return boundIds.containsAll(templateIds);
                        }).collect(Collectors.toList());
                if (matches.size() == 1) {
                    standardTreeId = matches.get(0).getId();
                } else {
                    List<StandardTree> exact = matches.stream().filter(tree ->
                            standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                    .map(StandardTreeTemplateBindingVo::getTemplateId)
                                    .collect(Collectors.toSet()).equals(templateIds))
                            .collect(Collectors.toList());
                    if (exact.size() == 1) {
                        standardTreeId = exact.get(0).getId();
                if (candidates.size() == 1) {
                    // 同一标准编号、同一实验室只有一个二级节点时直接认定,
                    // 避免标准库调整过模板后旧订单匹配不上。
                    standardTreeId = candidates.get(0).getId();
                } else if (!candidates.isEmpty()) {
                    List<StandardTree> matches = candidates.stream()
                            .filter(tree -> StringUtils.isNotBlank(tree.getSample()))
                            .filter(tree -> {
                                Set<Integer> boundIds = standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                        .map(StandardTreeTemplateBindingVo::getTemplateId).collect(Collectors.toSet());
                                return boundIds.containsAll(templateIds);
                            }).collect(Collectors.toList());
                    if (matches.size() == 1) {
                        standardTreeId = matches.get(0).getId();
                    } else if (!matches.isEmpty()) {
                        List<StandardTree> exact = matches.stream().filter(tree ->
                                standardTreeService.selectTemplateBindings(tree.getId()).stream()
                                        .map(StandardTreeTemplateBindingVo::getTemplateId)
                                        .collect(Collectors.toSet()).equals(templateIds))
                                .collect(Collectors.toList());
                        if (exact.size() == 1) {
                            standardTreeId = exact.get(0).getId();
                        }
                    }
                }
            }
@@ -1755,7 +1886,7 @@
                List<String> unqualifiedList = insProductMapper.selectUnqualifiedList(orderDto.getId());
                orderDto.setUnqualifiedItem(CollUtil.join(unqualifiedList, ","));
            }
            orderDto.setCreateTimeString(orderDto.getCreateTime() == null ? "" : orderDto.getCreateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            orderDto.setSampleDateString(orderDto.getSampleDate() == null ? "" : orderDto.getSampleDate().toString());
        }