云
2 天以前 704265308b1227dbada5a36b2027b588c062ded2
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
@@ -53,6 +53,8 @@
    @Resource private MesQcSeedQualityMapper qualityMapper;
    @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper;
    @Resource private MesProBagCodeMapper bagCodeMapper;
    @Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper palletMapper;
    @Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper batchMapper;
    @Resource private MesProBatchService proBatchService;
    @Resource private MesWmBatchMapper mesWmBatchMapper;
    @Resource private MesWmMaterialStockMapper materialStockMapper;
@@ -65,22 +67,120 @@
    @Override
    public PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO) {
        if (StrUtil.isNotBlank(reqVO.getCode())) {
            MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(reqVO.getCode());
            if (bag != null) reqVO.setCode(bag.getCode());
        }
        return qualityMapper.selectPage(reqVO);
    }
    @Override
    public Map<String, Object> resolveScan(String code) {
        MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
        if (bag == null) throw new IllegalArgumentException("未解析到有效袋码,请扫描袋码");
        Map<String, Object> result = new HashMap<>();
        result.put("code", bag.getCode());
        result.put("businessCode", bag.getBusinessCode());
        result.put("batchCode", bag.getBatchCode());
        result.put("inbound", bag.getInboundEventId() != null);
        result.put("canceled", Integer.valueOf(30).equals(bag.getStatus()));
        MesQcSeedQualityDO quality = qualityMapper.selectByCode(bag.getCode());
        result.put("qualityId", quality == null ? null : quality.getId());
        result.put("result", quality == null ? null : quality.getResult());
        return result;
    }
    private record Scope(String code, String businessCode, String batchCode, boolean canceled, List<MesProBagCodeDO> bags) { }
    private Scope scope(String code, String type, boolean lock) {
        if ("BAG".equals(type)) {
            MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(code);
            if (bag == null) throw new IllegalArgumentException("未找到袋码,请核对扫码类型");
            return new Scope(bag.getCode(), bag.getBusinessCode(), bag.getBatchCode(), Integer.valueOf(30).equals(bag.getStatus()), List.of(bag));
        }
        if ("PALLET".equals(type)) {
            var pallet = lock ? palletMapper.selectByScanCodeForUpdate(code) : palletMapper.selectByScanCode(code);
            if (pallet == null) throw new IllegalArgumentException("未找到托盘码,请核对扫码类型");
            if (pallet.getBatchId() == null) throw new IllegalArgumentException("托盘尚未绑定生产批次,无法质检");
            return new Scope(StrUtil.blankToDefault(pallet.getCode(), pallet.getTempCode()), pallet.getBusinessCode(), pallet.getBatchCode(),
                    Integer.valueOf(20).equals(pallet.getStatus()), lock ? bagCodeMapper.selectListByPalletIdForUpdate(pallet.getId()) : bagCodeMapper.selectListByPalletId(pallet.getId()));
        }
        if ("BATCH".equals(type)) {
            var batch = lock ? batchMapper.selectByScanCodeForUpdate(code) : batchMapper.selectByScanCode(code);
            if (batch == null) throw new IllegalArgumentException("未找到生产批次码,请核对扫码类型");
            return new Scope(StrUtil.blankToDefault(batch.getCode(), batch.getTempCode()), batch.getBusinessCode(), batch.getCode(),
                    Integer.valueOf(40).equals(batch.getStatus()), lock ? bagCodeMapper.selectListByBatchIdForUpdate(batch.getId()) : bagCodeMapper.selectListByBatchId(batch.getId()));
        }
        throw new IllegalArgumentException("不支持的质检扫码类型");
    }
    @Override
    public Map<String, Object> resolveScan(String code, String scanType) {
        Scope scope = scope(code, scanType, false);
        List<MesProBagCodeDO> effective = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).toList();
        Set<String> inspected = qualityMapper.selectListByCodes(effective.stream().map(MesProBagCodeDO::getCode).toList()).stream()
                .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet());
        Map<String, Object> result = new HashMap<>();
        result.put("scanType", scanType); result.put("code", scope.code()); result.put("businessCode", scope.businessCode());
        result.put("batchCode", scope.batchCode()); result.put("canceled", scope.canceled());
        result.put("totalCount", effective.size());
        result.put("inbound", !effective.isEmpty() && effective.stream().allMatch(b -> b.getInboundEventId() != null));
        result.put("pendingBagIds", effective.stream().filter(b -> b.getInboundEventId() != null && !inspected.contains(b.getCode())).map(MesProBagCodeDO::getId).sorted().toList());
        result.put("completedBagIds", effective.stream().filter(b -> inspected.contains(b.getCode())).map(MesProBagCodeDO::getId).sorted().toList());
        if ("BAG".equals(scanType)) {
            MesQcSeedQualityDO quality = qualityMapper.selectByCode(scope.code());
            result.put("qualityId", quality == null ? null : quality.getId()); result.put("result", quality == null ? null : quality.getResult());
        }
        return result;
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> submitGroup(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO req) {
        Scope scope = scope(req.getCode(), req.getScanType(), true);
        if (scope.canceled()) throw new IllegalArgumentException("质检对象已作废");
        List<MesProBagCodeDO> effective = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).toList();
        if (effective.isEmpty()) throw new IllegalArgumentException("没有有效袋码");
        List<MesProBagCodeDO> locked = bagCodeMapper.selectListByIdsForUpdate(effective.stream().map(MesProBagCodeDO::getId).toList());
        if (locked.size() != effective.size() || locked.stream().anyMatch(b -> Integer.valueOf(30).equals(b.getStatus()) || b.getInboundEventId() == null))
            throw new IllegalArgumentException("袋码已变化或尚未全部入库,请重新扫描核对");
        // 在袋码锁之后重新核对归属;其他绑定操作不得改变已确认的检验范围。
        Scope current = scope(req.getCode(), req.getScanType(), false);
        Set<Long> currentIds = current.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).map(MesProBagCodeDO::getId).collect(java.util.stream.Collectors.toSet());
        if (!currentIds.equals(new HashSet<>(locked.stream().map(MesProBagCodeDO::getId).toList())))
            throw new IllegalArgumentException("袋码归属已变化,请重新扫描核对");
        Set<String> inspected = qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList()).stream()
                .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet());
        List<MesProBagCodeDO> pending = locked.stream().filter(b -> !inspected.contains(b.getCode())).sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList();
        List<Long> confirmed = req.getBagIds().stream().sorted().toList();
        if (pending.isEmpty() || !confirmed.equals(pending.stream().map(MesProBagCodeDO::getId).toList()))
            throw new IllegalArgumentException("待质检清单已变化或已全部质检,请重新扫描,禁止重复处理");
        for (MesProBagCodeDO bag : pending) {
            MesQcSeedQualitySubmitReqVO single = new MesQcSeedQualitySubmitReqVO();
            single.setCode(bag.getCode());single.setIndicators(req.getIndicators());single.setReason(req.getReason());
            single.setQualifiedWarehouseId(req.getQualifiedWarehouseId());
            // 外层事务覆盖全部袋码;任何一袋库存流转失败都会回滚整次操作。
            submit(single);
        }
        return Map.of("processedCount", pending.size());
    }
    @Override @Transactional(rollbackFor = Exception.class)
    public Long submit(MesQcSeedQualitySubmitReqVO reqVO) {
        if (reqVO.getIndicators().size() != 4) throw new IllegalArgumentException("必须提交四项指标");
        if (qualityMapper.selectByCode(reqVO.getCode()) != null) throw new IllegalArgumentException("该袋码已质检,禁止重复提交");
        // 种子质检必须以袋码(单袋)提交,物料信息由袋码自动解析,前端不再单独选择物料
        MesProBagCodeDO bag = bagCodeMapper.selectByCode(reqVO.getCode());
        MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(reqVO.getCode());
        if (bag == null) {
            throw new IllegalArgumentException("未解析到有效袋码,种子质检必须以袋码提交");
        }
        MesProBatchDO batch = resolveBatch(reqVO.getCode(), reqVO.getBatchCode());
        if (Integer.valueOf(30).equals(bag.getStatus()) || bag.getInboundEventId() == null)
            throw new IllegalArgumentException("袋码已作废或尚未入库,无法质检");
        // 双码锁定同一袋,并以追溯码记录结果,防止换码或并发重复扣减库存。
        if (qualityMapper.selectByCodeForUpdate(bag.getCode()) != null) throw new IllegalArgumentException("该袋码已质检,禁止重复提交");
        MesProBatchDO batch = bag.getBatchId() == null ? null : proBatchService.getBatch(bag.getBatchId());
        if (batch == null || batch.getItemId() == null) {
            throw new IllegalArgumentException("未根据袋码解析到物料信息,请确认扫码内容或批次号正确");
        }
        if (Integer.valueOf(40).equals(batch.getStatus())) throw new IllegalArgumentException("生产批次已作废,无法质检");
        // 袋码对应批次的库存必须处于暂存库(待质检)中,才能进行种子质检
        MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode());
        if (wmBatch == null || materialStockMapper.selectTempStockCountByBatchId(wmBatch.getId()) == 0) {
@@ -110,7 +210,7 @@
        }
        Set<String> codes = new HashSet<>();
        boolean passed = true;
        MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(reqVO.getCode()).itemId(batch.getItemId())
        MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(bag.getCode()).itemId(batch.getItemId())
                .batchCode(batch.getCode()).reason(reqVO.getReason()).result(0)
                .inspectedAt(LocalDateTime.now()).build();
        qualityMapper.insert(quality);
@@ -121,6 +221,7 @@
                    .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
        }
        if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("四项指标不完整");
        if (!passed && StrUtil.isBlank(reqVO.getReason())) throw new IllegalArgumentException("质检不合格必须填写原因");
        quality.setResult(passed ? 1 : 2);
        qualityMapper.updateById(quality);
        // 按袋码粒度流转库存:锁定该袋在暂存库中的活动明细,禁止按批次任意扣减