| | |
| | | |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.json.JSONUtil; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstockbag.MesWmMaterialStockBagDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstockbag.MesWmMaterialStockBagMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.MesProBagCodeQcStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcApproveNodeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcApprovalTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockStateEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmWarehouseTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService; |
| | | import cn.iocoder.yudao.module.mes.service.qc.approver.MesQcApproverService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.materialstockbag.MesWmMaterialStockBagService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; |
| | |
| | | import java.util.*; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_QUALIFIED_WAREHOUSE_NOT_CONFIGURED; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_NOT_EXISTS; |
| | | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.WM_WAREHOUSE_TYPE_NOT_QUALIFIED; |
| | |
| | | @Service @Validated @Slf4j |
| | | public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService { |
| | | private static final Set<String> REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture"); |
| | | private static final int WAIT_RETEST = 10, WAIT_PROCESS = 20, WAIT_INITIAL_FAIL_REVIEW = 25, WAIT_COMPANY = 30, |
| | | RELEASED = 40, WAIT_INITIAL_RECHECK = 50, WAIT_COMPANY_FAIL = 60, SCRAPPED = 70; |
| | | @Resource private MesQcSeedQualityMapper qualityMapper; |
| | | @Resource private MesQcApproverService approverService; |
| | | @Resource private MesQcSeedQualityActionMapper actionMapper; |
| | | @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper; |
| | | @Resource private MesProBagCodeMapper bagCodeMapper; |
| | | @Resource private cn.iocoder.yudao.module.mes.dal.mysql.pro.pallet.MesProPalletMapper palletMapper; |
| | |
| | | if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())) { |
| | | throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED); |
| | | } |
| | | // 解析合格库默认库区/库位(取该仓库下第一个库区、其下第一个库位,未配置则为 null) |
| | | Long qualifiedLocationId = null; |
| | | Long qualifiedAreaId = null; |
| | | List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId()); |
| | | if (CollUtil.isNotEmpty(locations)) { |
| | | qualifiedLocationId = locations.get(0).getId(); |
| | | List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId); |
| | | if (CollUtil.isNotEmpty(areas)) { |
| | | qualifiedAreaId = areas.get(0).getId(); |
| | | } |
| | | } |
| | | Set<String> codes = new HashSet<>(); |
| | | boolean passed = true; |
| | | MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(bag.getCode()).itemId(batch.getItemId()) |
| | | .batchCode(batch.getCode()).reason(reqVO.getReason()).result(0) |
| | | .batchCode(batch.getCode()).batchId(batch.getId()).reason(reqVO.getReason()).result(0) |
| | | .qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(getLoginUserId()) |
| | | .inspectedAt(LocalDateTime.now()).build(); |
| | | qualityMapper.insert(quality); |
| | | for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) { |
| | | if (item == null || item.getCode() == null || item.getValue() == null || item.getMinValue() == null || item.getMaxValue() == null) |
| | | throw new IllegalArgumentException("四项指标的实测值和本次合格范围必须完整"); |
| | | if (!REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())) throw new IllegalArgumentException("指标必须为纯度、净度、发芽率、水分且不可重复"); |
| | | if (item.getMinValue().compareTo(item.getMaxValue()) > 0 || item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false; |
| | | if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("指标下限不能大于上限"); |
| | | if (item.getValue().compareTo(BigDecimal.ZERO) < 0 || item.getValue().compareTo(new BigDecimal("100")) > 0 |
| | | || item.getMinValue().compareTo(BigDecimal.ZERO) < 0 || item.getMaxValue().compareTo(new BigDecimal("100")) > 0) |
| | | throw new IllegalArgumentException("百分比指标必须处于 0–100"); |
| | | if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false; |
| | | indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode()) |
| | | .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); |
| | | quality.setWorkflowStatus(passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW); |
| | | qualityMapper.updateById(quality); |
| | | recordAction(quality.getId(), "INITIAL", quality.getResult(), reqVO.getReason(), reqVO.getIndicators()); |
| | | // 初检仅记录结论;袋仍留在暂存库,待初检审核、复检及最终审核后再放行或报废。 |
| | | return quality.getId(); |
| | | } |
| | | |
| | | private void finalizeStock(MesQcSeedQualityDO quality, boolean passed) { |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(quality.getCode()); |
| | | if (bag == null || bag.getInboundEventId() == null || !Objects.equals(bag.getQcStatus(), MesProBagCodeQcStatusEnum.UNCHECKED.getStatus())) |
| | | throw new IllegalArgumentException("袋码状态已变化,禁止重复库存流转"); |
| | | MesProBatchDO batch = proBatchService.getBatch(bag.getBatchId()); |
| | | if (batch == null) throw new IllegalArgumentException("袋码所属生产批次不存在"); |
| | | MesWmBatchDO wmBatch = mesWmBatchMapper.selectByCode(batch.getCode()); |
| | | if (wmBatch == null) throw new IllegalArgumentException("暂存库存所属仓库批次不存在"); |
| | | MesWmWarehouseDO qualifiedWarehouse = warehouseService.getWarehouse(quality.getQualifiedWarehouseId()); |
| | | if (passed && (qualifiedWarehouse == null || Boolean.TRUE.equals(qualifiedWarehouse.getFrozen()) |
| | | || !MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType()))) |
| | | throw new IllegalArgumentException("目标合格库不存在或类型已变化"); |
| | | Long qualifiedLocationId = null; |
| | | Long qualifiedAreaId = null; |
| | | if (passed) { |
| | | List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId()); |
| | | if (CollUtil.isNotEmpty(locations)) { |
| | | qualifiedLocationId = locations.get(0).getId(); |
| | | List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(qualifiedLocationId); |
| | | if (CollUtil.isNotEmpty(areas)) qualifiedAreaId = areas.get(0).getId(); |
| | | } |
| | | } |
| | | // 按袋码粒度流转库存:锁定该袋在暂存库中的活动明细,禁止按批次任意扣减 |
| | | MesWmMaterialStockBagDO stockBag = materialStockBagMapper.selectActiveByBagCode(bag.getCode()); |
| | | MesWmMaterialStockDO tempStock = resolveTempStock(bag, wmBatch, stockBag); |
| | |
| | | .setType(MesWmTransactionTypeEnum.OUT.getType()) |
| | | .setBizType(MesBizTypeConstants.QC_SEED_QUALITY) |
| | | .setBizId(quality.getId()) |
| | | .setBizCode(reqVO.getCode()) |
| | | .setBizCode(quality.getCode()) |
| | | .setBizLineId(quality.getId()) |
| | | .setItemId(batch.getItemId()) |
| | | .setQuantity(BigDecimal.ONE.negate()) |
| | |
| | | .setType(MesWmTransactionTypeEnum.IN.getType()) |
| | | .setBizType(MesBizTypeConstants.QC_SEED_QUALITY) |
| | | .setBizId(quality.getId()) |
| | | .setBizCode(reqVO.getCode()) |
| | | .setBizCode(quality.getCode()) |
| | | .setBizLineId(quality.getId()) |
| | | .setItemId(batch.getItemId()) |
| | | .setQuantity(BigDecimal.ONE) |
| | |
| | | // B2. 同步迁移袋码明细到合格库存落点 |
| | | materialStockBagService.moveIn(qualInReq, qualifiedStock, qualInTxId); |
| | | bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.QUALIFIED.getStatus()); |
| | | log.info("[submit][质检合格,袋码 {} 由暂存库转合格库 {}]", bag.getCode(), qualifiedWarehouse.getName()); |
| | | log.info("[finalizeStock][审批放行,袋码 {} 由暂存库转合格库 {}]", bag.getCode(), qualifiedWarehouse.getName()); |
| | | } else { |
| | | // C. 质检不合格 → 该袋报废;暂存出库事务已扣减暂存库存,标记袋码报废 |
| | | materialStockBagService.scrap(tempOutReq, tempOutTxId); |
| | | bagCodeMapper.updateQcStatus(bag.getId(), MesProBagCodeQcStatusEnum.UNQUALIFIED.getStatus()); |
| | | log.info("[submit][质检不合格,袋码 {} 报废]", bag.getCode()); |
| | | log.info("[finalizeStock][审批报废,袋码 {}]", bag.getCode()); |
| | | } |
| | | return quality.getId(); |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public void retest(Long id, cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO req) { |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id); |
| | | if (quality == null || !Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus())) |
| | | throw new IllegalArgumentException("当前状态不能提交复检,请刷新后重试"); |
| | | boolean initialRecheck = Objects.equals(quality.getWorkflowStatus(), WAIT_INITIAL_RECHECK); |
| | | if (!Objects.equals(quality.getCode(), req.getCode())) throw new IllegalArgumentException("检验袋码与原质检记录不一致"); |
| | | boolean passed = validateIndicators(req.getIndicators()); |
| | | if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("检验不合格必须填写原因"); |
| | | recordAction(id, initialRecheck ? "INITIAL_RECHECK" : "RETEST", passed ? 1 : 2, req.getReason(), req.getIndicators()); |
| | | MesQcSeedQualityDO update = MesQcSeedQualityDO.builder().id(id).result(passed ? 1 : 2) |
| | | .reason(passed ? "" : req.getReason().trim()) |
| | | .inspectorId(getLoginUserId()).inspectedAt(LocalDateTime.now()) |
| | | .workflowStatus(initialRecheck ? (passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW) |
| | | : (passed ? WAIT_COMPANY : WAIT_COMPANY_FAIL)).build(); |
| | | if (!initialRecheck) { |
| | | update.setRetestResult(passed ? 1 : 2); |
| | | update.setRetestedAt(LocalDateTime.now()); |
| | | } |
| | | qualityMapper.updateById(update); |
| | | // 页面、公开追溯读取主指标表;同步为最新一次测值,历次测值在操作留痕表保留。 |
| | | Map<String, MesQcSeedQualityIndicatorDO> existing = new HashMap<>(); |
| | | for (MesQcSeedQualityIndicatorDO indicator : indicatorMapper.selectListByQualityId(id)) |
| | | existing.put(indicator.getIndicatorCode(), indicator); |
| | | for (MesQcSeedQualitySubmitReqVO.Indicator item : req.getIndicators()) { |
| | | MesQcSeedQualityIndicatorDO indicator = existing.get(item.getCode()); |
| | | if (indicator == null) throw new IllegalArgumentException("质检指标记录缺失"); |
| | | indicatorMapper.updateById(MesQcSeedQualityIndicatorDO.builder().id(indicator.getId()) |
| | | .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build()); |
| | | } |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public void retestBatch(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRetestReqVO req) { |
| | | List<Long> ids = approvalIds(req.getIds()); |
| | | String batchCode = null; |
| | | Integer workflowStatus = null; |
| | | boolean first = true; |
| | | List<MesQcSeedQualityDO> qualities = new ArrayList<>(); |
| | | for (Long id : ids) { |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id); |
| | | if (quality == null || !Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus())) |
| | | throw new IllegalArgumentException("所选记录包含不可检验的状态,请刷新后重试"); |
| | | if (StrUtil.isBlank(quality.getBatchCode())) |
| | | throw new IllegalArgumentException("记录缺少生产批次,不能批量质检"); |
| | | if (first) { |
| | | batchCode = quality.getBatchCode(); |
| | | workflowStatus = quality.getWorkflowStatus(); |
| | | first = false; |
| | | } else if (!Objects.equals(batchCode, quality.getBatchCode()) || !Objects.equals(workflowStatus, quality.getWorkflowStatus())) { |
| | | throw new IllegalArgumentException("批量质检只允许同生产批次、同流程状态的记录"); |
| | | } |
| | | qualities.add(quality); |
| | | } |
| | | for (MesQcSeedQualityDO quality : qualities) { |
| | | cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO single = |
| | | new cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO(); |
| | | single.setCode(quality.getCode()); |
| | | single.setIndicators(req.getIndicators()); |
| | | single.setReason(req.getReason()); |
| | | retest(quality.getId(), single); |
| | | } |
| | | } |
| | | |
| | | private void checkRetestScanType(String scanType) { |
| | | if (!Set.of("PALLET", "BATCH").contains(scanType)) |
| | | throw new IllegalArgumentException("批量质检只支持托盘码或生产批次码"); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> resolveRetestScope(String code, String scanType) { |
| | | checkRetestScanType(scanType); |
| | | Scope scope = scope(code, scanType, false); |
| | | if (scope.canceled()) throw new IllegalArgumentException("质检对象已作废"); |
| | | List<String> codes = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() != null) |
| | | .map(MesProBagCodeDO::getCode).toList(); |
| | | List<MesQcSeedQualityDO> qualities = codes.isEmpty() ? List.of() : qualityMapper.selectListByCodes(codes); |
| | | List<Long> retestIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_RETEST)) |
| | | .map(MesQcSeedQualityDO::getId).sorted().toList(); |
| | | List<Long> initialRecheckIds = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_RECHECK)) |
| | | .map(MesQcSeedQualityDO::getId).sorted().toList(); |
| | | long pendingProcessCount = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_PROCESS) |
| | | || Objects.equals(q.getWorkflowStatus(), WAIT_INITIAL_FAIL_REVIEW)).count(); |
| | | long pendingCompanyCount = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY) |
| | | || Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY_FAIL)).count(); |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("code", scope.code()); result.put("scanType", scanType); result.put("batchCode", scope.batchCode()); |
| | | result.put("totalCount", codes.size()); result.put("retestIds", retestIds); |
| | | result.put("initialRecheckIds", initialRecheckIds); |
| | | result.put("pendingProcessCount", pendingProcessCount); |
| | | result.put("pendingCompanyCount", pendingCompanyCount); |
| | | return result; |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public Map<String, Object> retestScope(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityScopeRetestReqVO req) { |
| | | checkRetestScanType(req.getScanType()); |
| | | if (!Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(req.getWorkflowStatus())) |
| | | throw new IllegalArgumentException("批量质检状态无效"); |
| | | List<Long> confirmed = approvalIds(req.getQualityIds()); |
| | | 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()) && b.getInboundEventId() != null).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()) && b.getInboundEventId() != null) |
| | | .map(MesProBagCodeDO::getId).collect(java.util.stream.Collectors.toSet()); |
| | | if (!currentIds.equals(new HashSet<>(locked.stream().map(MesProBagCodeDO::getId).toList()))) |
| | | throw new IllegalArgumentException("袋码归属已变化,请重新扫描"); |
| | | List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList()); |
| | | List<MesQcSeedQualityDO> pending = qualities.stream().filter(q -> Objects.equals(q.getWorkflowStatus(), req.getWorkflowStatus())) |
| | | .sorted(Comparator.comparing(MesQcSeedQualityDO::getId)).toList(); |
| | | if (pending.isEmpty() || !confirmed.equals(pending.stream().map(MesQcSeedQualityDO::getId).toList())) |
| | | throw new IllegalArgumentException("待质检清单已变化,请重新扫描核对"); |
| | | for (MesQcSeedQualityDO quality : pending) { |
| | | var single = new cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityRetestReqVO(); |
| | | single.setCode(quality.getCode()); single.setIndicators(req.getIndicators()); single.setReason(req.getReason()); |
| | | retest(quality.getId(), single); |
| | | } |
| | | return Map.of("processedCount", pending.size()); |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public void processApprove(Long id, boolean approved, String opinion) { |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id); |
| | | if (quality == null || !Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus())) |
| | | throw new IllegalArgumentException("当前状态不能进行初检审核"); |
| | | approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(), |
| | | MesQcApproveNodeEnum.PROCESS.getNode(), getLoginUserId()); |
| | | if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见"); |
| | | int next = approved ? WAIT_RETEST : WAIT_INITIAL_RECHECK; |
| | | qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next) |
| | | .processApprovedAt(LocalDateTime.now()).build()); |
| | | recordAction(id, approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), opinion, null); |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public void companyApprove(Long id, boolean approved, String opinion) { |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id); |
| | | if (quality == null || !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus())) |
| | | throw new IllegalArgumentException("当前状态不能进行复检审核"); |
| | | approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(), |
| | | MesQcApproveNodeEnum.COMPANY.getNode(), getLoginUserId()); |
| | | if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见"); |
| | | if (approved) finalizeStock(quality, quality.getWorkflowStatus() == WAIT_COMPANY); |
| | | int next = approved ? (quality.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : SCRAPPED) : WAIT_RETEST; |
| | | qualityMapper.updateById(MesQcSeedQualityDO.builder().id(id).workflowStatus(next) |
| | | .companyApprovedAt(LocalDateTime.now()).build()); |
| | | recordAction(id, approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", quality.getResult(), opinion, null); |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public void processApproveBatch(List<Long> ids, boolean approved, String opinion) { |
| | | for (Long id : approvalIds(ids)) processApprove(id, approved, opinion); |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public void companyApproveBatch(List<Long> ids, boolean approved, String opinion) { |
| | | for (Long id : approvalIds(ids)) companyApprove(id, approved, opinion); |
| | | } |
| | | |
| | | private List<Long> approvalIds(List<Long> ids) { |
| | | if (ids == null || ids.isEmpty() || ids.size() > 500 || ids.stream().anyMatch(Objects::isNull)) |
| | | throw new IllegalArgumentException("请选择 1 至 500 条质检记录"); |
| | | List<Long> sorted = ids.stream().distinct().sorted().toList(); |
| | | if (sorted.size() != ids.size()) throw new IllegalArgumentException("质检记录不能重复选择"); |
| | | return sorted; |
| | | } |
| | | |
| | | private MesQcSeedQualityDO requireStatus(Long id, int expected) { |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id); |
| | | if (quality == null || !Objects.equals(quality.getWorkflowStatus(), expected)) |
| | | throw new IllegalArgumentException("质检记录状态已变化,请刷新后重试"); |
| | | return quality; |
| | | } |
| | | |
| | | private boolean validateIndicators(List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) { |
| | | if (indicators == null || indicators.size() != REQUIRED.size()) throw new IllegalArgumentException("必须提交四项指标"); |
| | | Set<String> codes = new HashSet<>(); |
| | | boolean passed = true; |
| | | for (MesQcSeedQualitySubmitReqVO.Indicator item : indicators) { |
| | | if (item == null || !REQUIRED.contains(item.getCode()) || !codes.add(item.getCode()) |
| | | || item.getValue() == null || item.getMinValue() == null || item.getMaxValue() == null) |
| | | throw new IllegalArgumentException("指标必须为纯度、净度、发芽率、水分且不可重复"); |
| | | if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("指标下限不能大于上限"); |
| | | if (item.getValue().compareTo(BigDecimal.ZERO) < 0 || item.getValue().compareTo(new BigDecimal("100")) > 0 |
| | | || item.getMinValue().compareTo(BigDecimal.ZERO) < 0 || item.getMaxValue().compareTo(new BigDecimal("100")) > 0) |
| | | throw new IllegalArgumentException("百分比指标必须处于 0–100"); |
| | | if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false; |
| | | } |
| | | return passed; |
| | | } |
| | | |
| | | private void recordAction(Long id, String action, Integer result, String opinion, |
| | | List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) { |
| | | MesQcSeedQualityActionDO event = new MesQcSeedQualityActionDO(); |
| | | event.setQualityId(id); |
| | | event.setAction(action); |
| | | event.setResult(result); |
| | | event.setOpinion(opinion); |
| | | event.setIndicatorsJson(indicators == null ? null : JSONUtil.toJsonStr(indicators)); |
| | | event.setOperatorId(getLoginUserId()); |
| | | event.setCreateTime(LocalDateTime.now()); |
| | | actionMapper.insert(event); |
| | | } |
| | | |
| | | /** |
| | |
| | | return List.of(); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public List<MesQcSeedQualityActionDO> getActions(Long qualityId) { |
| | | return actionMapper.selectByQualityId(qualityId); |
| | | } |
| | | } |