| | |
| | | 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.bpm.api.task.BpmProcessInstanceApi; |
| | | import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityBatchRespVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedReturnQualitySubmitReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; |
| | | import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.*; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesLineDO; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnSalesStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; |
| | |
| | | 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.returnsales.MesWmReturnSalesLineService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO; |
| | | import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; |
| | |
| | | import org.springframework.jdbc.BadSqlGrammarException; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.transaction.support.TransactionSynchronization; |
| | | import org.springframework.transaction.support.TransactionSynchronizationManager; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import java.math.BigDecimal; |
| | | import java.sql.Timestamp; |
| | | import java.time.LocalDateTime; |
| | | import java.util.*; |
| | | import java.util.function.Function; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
| | |
| | | |
| | | @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 MAX_BATCH_SIZE = 500, WRITE_BATCH_SIZE = 100; |
| | | 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; |
| | | /** 质检线:1 加工部;2 销售部 */ |
| | | private static final int QC_LINE_PROCESS = 1, QC_LINE_SALES = 2; |
| | | /** 业务类型:1 生产入库质检;2 经销商退货质检 */ |
| | | private static final int BIZ_TYPE_PRODUCTION = 1, BIZ_TYPE_RETURN = 2; |
| | | /** 本线领导审批流程定义标识 */ |
| | | private static final String LINE_PROCESS_KEY = "mes_seed_quality_line_approve"; |
| | | /** 公司质检审批流程定义标识 */ |
| | | private static final String COMPANY_PROCESS_KEY = "mes_seed_quality_company_approve"; |
| | | /** 经销商退货质检审批流程定义标识 */ |
| | | private static final String RETURN_PROCESS_KEY = "mes_seed_return_approve"; |
| | | @Resource private MesQcSeedQualityMapper qualityMapper; |
| | | @Resource private BpmProcessInstanceApi processInstanceApi; |
| | | @Resource private MesWmReturnSalesService returnSalesService; |
| | | @Resource private MesWmReturnSalesLineService returnSalesLineService; |
| | | @Resource private MesQcApproverService approverService; |
| | | @Resource private MesQcSeedQualityActionMapper actionMapper; |
| | | @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper; |
| | |
| | | @Resource private MesWmWarehouseService warehouseService; |
| | | @Resource private MesWmWarehouseLocationService warehouseLocationService; |
| | | @Resource private MesWmWarehouseAreaService warehouseAreaService; |
| | | @Resource private MesMdmItemService mdmItemService; |
| | | @Resource private MesQcSeedIndicatorRangeService rangeService; |
| | | |
| | | @Override |
| | | public PageResult<MesQcSeedQualityDO> getQualityPage(MesQcSeedQualityPageReqVO reqVO) { |
| | | resolveBagScanCode(reqVO); |
| | | return qualityMapper.selectPage(reqVO); |
| | | } |
| | | |
| | | @Override |
| | | public PageResult<MesQcSeedQualityBatchRespVO> getQualityBatchPage(MesQcSeedQualityPageReqVO reqVO) { |
| | | resolveBagScanCode(reqVO); |
| | | int pageNo = reqVO.getPageNo() == null || reqVO.getPageNo() < 1 ? 1 : reqVO.getPageNo(); |
| | | int pageSize = reqVO.getPageSize() == null || reqVO.getPageSize() < 1 ? 10 : Math.min(reqVO.getPageSize(), 100); |
| | | long total = qualityMapper.countBatchGroups(reqVO); |
| | | if (total == 0) { |
| | | return new PageResult<>(Collections.emptyList(), 0L); |
| | | } |
| | | List<MesQcSeedQualityBatchRespVO> list = qualityMapper.selectBatchGroups(reqVO, (long) (pageNo - 1) * pageSize, pageSize) |
| | | .stream().map(this::toBatchRespVO).collect(Collectors.toList()); |
| | | // 回填批次与品种信息 |
| | | Set<Long> batchIds = list.stream().map(MesQcSeedQualityBatchRespVO::getBatchId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | Map<Long, MesProBatchDO> batchMap = batchIds.isEmpty() ? Collections.emptyMap() |
| | | : batchMapper.selectBatchIds(batchIds).stream() |
| | | .collect(Collectors.toMap(MesProBatchDO::getId, Function.identity(), (first, ignored) -> first)); |
| | | Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(list.stream() |
| | | .map(MesQcSeedQualityBatchRespVO::getItemId).filter(Objects::nonNull).collect(Collectors.toSet())); |
| | | for (MesQcSeedQualityBatchRespVO vo : list) { |
| | | vo.setBatchLabel(StrUtil.blankToDefault(vo.getBatchCode(), "未绑定批次")); |
| | | MesProBatchDO batch = batchMap.get(vo.getBatchId()); |
| | | if (batch != null) { |
| | | vo.setLineName(batch.getLineName()); |
| | | vo.setProduceDate(batch.getProduceDate()); |
| | | } |
| | | MesMdmItemDO item = itemMap.get(vo.getItemId()); |
| | | if (item != null) { |
| | | vo.setItemCode(item.getCode()); |
| | | vo.setItemName(item.getName()); |
| | | } |
| | | } |
| | | return new PageResult<>(list, total); |
| | | } |
| | | |
| | | private void resolveBagScanCode(MesQcSeedQualityPageReqVO reqVO) { |
| | | if (StrUtil.isNotBlank(reqVO.getCode())) { |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByScanCode(reqVO.getCode()); |
| | | if (bag != null) reqVO.setCode(bag.getCode()); |
| | | if (bag != null) { |
| | | reqVO.setCode(bag.getCode()); |
| | | } |
| | | } |
| | | return qualityMapper.selectPage(reqVO); |
| | | } |
| | | |
| | | private MesQcSeedQualityBatchRespVO toBatchRespVO(Map<String, Object> row) { |
| | | MesQcSeedQualityBatchRespVO vo = new MesQcSeedQualityBatchRespVO(); |
| | | vo.setBatchCode((String) row.get("batchCode")); |
| | | vo.setBatchId(toLong(row.get("batchId"))); |
| | | vo.setItemId(toLong(row.get("itemId"))); |
| | | vo.setTotalCount(toInt(row.get("totalCount"))); |
| | | vo.setPassCount(toInt(row.get("passCount"))); |
| | | vo.setFailCount(toInt(row.get("failCount"))); |
| | | vo.setProcessingCount(toInt(row.get("processingCount"))); |
| | | vo.setPendingRetestCount(toInt(row.get("pendingRetestCount"))); |
| | | vo.setPendingProcessReviewCount(toInt(row.get("pendingProcessReviewCount"))); |
| | | vo.setPendingCompanyReviewCount(toInt(row.get("pendingCompanyReviewCount"))); |
| | | vo.setReleasedCount(toInt(row.get("releasedCount"))); |
| | | vo.setScrappedCount(toInt(row.get("scrappedCount"))); |
| | | vo.setLastInspectedAt(toLocalDateTime(row.get("lastInspectedAt"))); |
| | | return vo; |
| | | } |
| | | |
| | | private static Long toLong(Object value) { |
| | | return value instanceof Number number ? number.longValue() : null; |
| | | } |
| | | |
| | | private static Integer toInt(Object value) { |
| | | return value instanceof Number number ? number.intValue() : 0; |
| | | } |
| | | |
| | | private static LocalDateTime toLocalDateTime(Object value) { |
| | | if (value instanceof LocalDateTime dateTime) { |
| | | return dateTime; |
| | | } |
| | | if (value instanceof Timestamp timestamp) { |
| | | return timestamp.toLocalDateTime(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | |
| | | 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()); |
| | | Long itemId = bag.getItemId(); |
| | | MesMdmItemDO item = itemId == null ? null : mdmItemService.getItem(itemId); |
| | | result.put("itemId", itemId); |
| | | result.put("itemCode", item == null ? null : item.getCode()); |
| | | result.put("itemName", item == null ? null : item.getName()); |
| | | result.put("indicators", toRangePayload(itemId)); |
| | | List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByCode(bag.getCode()); |
| | | MesQcSeedQualityDO quality = qualities.stream() |
| | | .filter(q -> !isReturnQuality(q)).findFirst().orElse(null); |
| | | result.put("qualityId", quality == null ? null : quality.getId()); |
| | | result.put("result", quality == null ? null : quality.getResult()); |
| | | result.put("qualityIds", qualities.stream().map(MesQcSeedQualityDO::getId).toList()); |
| | | result.put("qualityLines", qualities.stream() |
| | | .filter(q -> !isReturnQuality(q)) |
| | | .map(q -> { |
| | | Map<String, Object> line = new HashMap<>(); |
| | | line.put("id", q.getId()); |
| | | line.put("qcLine", q.getQcLine()); |
| | | line.put("workflowStatus", q.getWorkflowStatus()); |
| | | return line; |
| | | }) |
| | | .toList()); |
| | | return result; |
| | | } |
| | | |
| | |
| | | |
| | | @Override |
| | | public Map<String, Object> resolveScan(String code, String scanType) { |
| | | return resolveScan(code, scanType, QC_LINE_PROCESS); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> resolveScan(String code, String scanType, Integer qcLine) { |
| | | requireQcLine(qcLine); |
| | | 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() |
| | | .filter(q -> !isReturnQuality(q)) |
| | | .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("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()); |
| | | Long itemId = resolveItemId(scope); |
| | | MesMdmItemDO item = itemId == null ? null : mdmItemService.getItem(itemId); |
| | | result.put("itemId", itemId); |
| | | result.put("itemCode", item == null ? null : item.getCode()); |
| | | result.put("itemName", item == null ? null : item.getName()); |
| | | result.put("indicators", toRangePayload(itemId)); |
| | | 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()); |
| | | List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByCode(scope.code()); |
| | | MesQcSeedQualityDO quality = qualities.stream() |
| | | .filter(q -> !isReturnQuality(q)).findFirst().orElse(null); |
| | | result.put("qualityId", quality == null ? null : quality.getId()); |
| | | result.put("result", quality == null ? null : quality.getResult()); |
| | | result.put("qualityIds", qualities.stream().map(MesQcSeedQualityDO::getId).toList()); |
| | | } |
| | | return result; |
| | | } |
| | |
| | | if (scope.canceled()) throw invalidParamException("质检对象已作废"); |
| | | List<MesProBagCodeDO> effective = scope.bags().stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus())).toList(); |
| | | if (effective.isEmpty()) throw new IllegalArgumentException("没有有效袋码"); |
| | | // 一次提交同时生成两条质检线记录,任一已存在即视为该袋已质检。 |
| | | Set<String> inspected = qualityMapper.selectListByCodes(effective.stream().map(MesProBagCodeDO::getCode).toList()).stream() |
| | | .filter(q -> !isReturnQuality(q)) |
| | | .map(MesQcSeedQualityDO::getCode).collect(java.util.stream.Collectors.toSet()); |
| | | List<MesProBagCodeDO> pendingPreview = effective.stream().filter(b -> b.getInboundEventId() != null && !inspected.contains(b.getCode())) |
| | | .sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList(); |
| | |
| | | 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.containsAll(confirmed)) |
| | | throw new IllegalArgumentException("袋码归属已变化,请重新扫描核对"); |
| | | if (!qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList()).isEmpty()) |
| | | if (qualityMapper.selectListByCodesForUpdate(locked.stream().map(MesProBagCodeDO::getCode).toList()).stream() |
| | | .anyMatch(q -> !isReturnQuality(q))) |
| | | throw new IllegalArgumentException("待质检清单已变化或已全部质检,请重新扫描,禁止重复处理"); |
| | | List<MesProBagCodeDO> pending = locked.stream().sorted(Comparator.comparing(MesProBagCodeDO::getId)).toList(); |
| | | submitInitialBatch(pending, req); |
| | |
| | | |
| | | private void submitInitialBatch(List<MesProBagCodeDO> bags, |
| | | cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityGroupSubmitReqVO req) { |
| | | boolean passed = validateIndicators(req.getIndicators()); |
| | | if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("质检不合格必须填写原因"); |
| | | Long batchId = bags.get(0).getBatchId(); |
| | | if (batchId == null || bags.stream().anyMatch(bag -> !Objects.equals(batchId, bag.getBatchId()))) |
| | | throw new IllegalArgumentException("批量初检只允许同一生产批次的袋码"); |
| | | MesProBatchDO batch = proBatchService.getBatch(batchId); |
| | | if (batch == null || batch.getItemId() == null) throw new IllegalArgumentException("未根据袋码解析到物料信息"); |
| | | Map<String, IndicatorMeta> expected = requireEffectiveIndicators(batch.getItemId()); |
| | | boolean passed = validateIndicators(req.getIndicators(), expected); |
| | | if (!passed && StrUtil.isBlank(req.getReason())) 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) |
| | |
| | | if (qualifiedWarehouse == null) throw exception(WM_WAREHOUSE_NOT_EXISTS); |
| | | if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())) |
| | | throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED); |
| | | // 提前校验合格库已配置库区/库位,避免复检审核放行时才失败 |
| | | resolveQualifiedLocation(qualifiedWarehouse); |
| | | |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | Long operatorId = getLoginUserId(); |
| | | int result = passed ? 1 : 2; |
| | | int status = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW; |
| | | List<MesQcSeedQualityDO> qualities = bags.stream().map(bag -> MesQcSeedQualityDO.builder() |
| | | .code(bag.getCode()).itemId(batch.getItemId()).batchId(batch.getId()).batchCode(batch.getCode()) |
| | | .result(result).reason(passed ? "" : req.getReason().trim()).workflowStatus(status) |
| | | .qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(operatorId).inspectedAt(now).build()).toList(); |
| | | int status = passed ? WAIT_PROCESS : WAIT_RETEST; |
| | | // 一次提交同时生成加工部与销售部两条记录,指标与结论完全一致。 |
| | | List<MesQcSeedQualityDO> qualities = new ArrayList<>(bags.size() * 2); |
| | | for (MesProBagCodeDO bag : bags) { |
| | | for (int qcLine : new int[]{QC_LINE_PROCESS, QC_LINE_SALES}) { |
| | | qualities.add(MesQcSeedQualityDO.builder() |
| | | .code(bag.getCode()).qcLine(qcLine).bizType(BIZ_TYPE_PRODUCTION) |
| | | .itemId(batch.getItemId()).batchId(batch.getId()).batchCode(batch.getCode()) |
| | | .result(result).reason(passed ? "" : req.getReason().trim()).workflowStatus(status) |
| | | .qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(operatorId).inspectedAt(now).build()); |
| | | } |
| | | } |
| | | qualityMapper.insertBatch(qualities, WRITE_BATCH_SIZE); |
| | | if (qualities.stream().anyMatch(quality -> quality.getId() == null)) |
| | | throw new IllegalStateException("批量初检记录编号生成失败"); |
| | | |
| | | List<MesQcSeedQualityIndicatorDO> indicators = new ArrayList<>(qualities.size() * REQUIRED.size()); |
| | | List<MesQcSeedQualityIndicatorDO> indicators = new ArrayList<>(qualities.size() * expected.size()); |
| | | List<MesQcSeedQualityActionDO> actions = new ArrayList<>(qualities.size()); |
| | | for (MesQcSeedQualityDO quality : qualities) { |
| | | for (MesQcSeedQualitySubmitReqVO.Indicator item : req.getIndicators()) { |
| | | IndicatorMeta meta = expected.get(item.getCode()); |
| | | indicators.add(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode()) |
| | | .indicatorName(meta.name()).unit(meta.unit()) |
| | | .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build()); |
| | | } |
| | | actions.add(newAction(quality.getId(), "INITIAL", result, req.getReason(), req.getIndicators(), operatorId, now)); |
| | | } |
| | | indicatorMapper.insertBatch(indicators, WRITE_BATCH_SIZE * REQUIRED.size()); |
| | | indicatorMapper.insertBatch(indicators, WRITE_BATCH_SIZE * expected.size()); |
| | | actionMapper.insertBatch(actions, WRITE_BATCH_SIZE); |
| | | String batchCode = batch.getCode(); |
| | | if (passed) { |
| | | afterCommit(() -> startLineProcess(QC_LINE_PROCESS, batchCode, operatorId)); |
| | | afterCommit(() -> startLineProcess(QC_LINE_SALES, batchCode, operatorId)); |
| | | } |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public Long submit(MesQcSeedQualitySubmitReqVO reqVO) { |
| | | if (reqVO.getIndicators().size() != 4) throw new IllegalArgumentException("必须提交四项指标"); |
| | | // 种子质检必须以袋码(单袋)提交,物料信息由袋码自动解析,前端不再单独选择物料 |
| | | MesProBagCodeDO bag = bagCodeMapper.selectByScanCodeForUpdate(reqVO.getCode()); |
| | | if (bag == null) { |
| | |
| | | if (Integer.valueOf(30).equals(bag.getStatus()) || bag.getInboundEventId() == null) |
| | | throw new IllegalArgumentException("袋码已作废或尚未入库,无法质检"); |
| | | // 双码锁定同一袋,并以追溯码记录结果,防止换码或并发重复扣减库存。 |
| | | if (qualityMapper.selectByCodeForUpdate(bag.getCode()) != null) throw new IllegalArgumentException("该袋码已质检,禁止重复提交"); |
| | | if (qualityMapper.selectListByCodeForUpdate(bag.getCode()).stream() |
| | | .anyMatch(q -> !isReturnQuality(q))) |
| | | throw new IllegalArgumentException("该袋码已质检,禁止重复提交"); |
| | | MesProBatchDO batch = bag.getBatchId() == null ? null : proBatchService.getBatch(bag.getBatchId()); |
| | | if (batch == null || batch.getItemId() == null) { |
| | | throw new IllegalArgumentException("未根据袋码解析到物料信息,请确认扫码内容或批次号正确"); |
| | |
| | | if (!MesWmWarehouseTypeEnum.QUALIFIED.getType().equals(qualifiedWarehouse.getType())) { |
| | | throw exception(WM_WAREHOUSE_TYPE_NOT_QUALIFIED); |
| | | } |
| | | // 提前校验合格库已配置库区/库位,避免复检审核放行时才失败 |
| | | resolveQualifiedLocation(qualifiedWarehouse); |
| | | Set<String> codes = new HashSet<>(); |
| | | boolean passed = true; |
| | | MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(bag.getCode()).itemId(batch.getItemId()) |
| | | .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) 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("四项指标不完整"); |
| | | Map<String, IndicatorMeta> expected = requireEffectiveIndicators(batch.getItemId()); |
| | | boolean passed = validateIndicators(reqVO.getIndicators(), expected); |
| | | 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()); |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | Long operatorId = getLoginUserId(); |
| | | int result = passed ? 1 : 2; |
| | | int status = passed ? WAIT_PROCESS : WAIT_RETEST; |
| | | // 一次提交同时生成加工部与销售部两条记录,指标与结论完全一致。 |
| | | Long firstQualityId = null; |
| | | for (int qcLine : new int[]{QC_LINE_PROCESS, QC_LINE_SALES}) { |
| | | MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(bag.getCode()) |
| | | .qcLine(qcLine).bizType(BIZ_TYPE_PRODUCTION) |
| | | .itemId(batch.getItemId()).batchCode(batch.getCode()).batchId(batch.getId()) |
| | | .reason(passed ? "" : reqVO.getReason().trim()).result(result).workflowStatus(status) |
| | | .qualifiedWarehouseId(qualifiedWarehouse.getId()).inspectorId(operatorId) |
| | | .inspectedAt(now).build(); |
| | | qualityMapper.insert(quality); |
| | | for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) { |
| | | IndicatorMeta meta = expected.get(item.getCode()); |
| | | indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode()) |
| | | .indicatorName(meta.name()).unit(meta.unit()) |
| | | .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build()); |
| | | } |
| | | recordAction(quality.getId(), "INITIAL", result, reqVO.getReason(), reqVO.getIndicators()); |
| | | if (firstQualityId == null) firstQualityId = quality.getId(); |
| | | } |
| | | // 初检仅记录结论;袋仍留在暂存库,待初检审核、复检及最终审核后再放行或报废。 |
| | | return quality.getId(); |
| | | String batchCode = batch.getCode(); |
| | | if (passed) { |
| | | afterCommit(() -> startLineProcess(QC_LINE_PROCESS, batchCode, operatorId)); |
| | | afterCommit(() -> startLineProcess(QC_LINE_SALES, batchCode, operatorId)); |
| | | } |
| | | return firstQualityId; |
| | | } |
| | | |
| | | private void finalizeStock(MesQcSeedQualityDO quality, boolean passed) { |
| | |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id); |
| | | if (quality == null || !Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus())) |
| | | throw new IllegalArgumentException("当前状态不能提交复检,请刷新后重试"); |
| | | boolean returnQuality = Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN); |
| | | if (returnQuality && !Objects.equals(quality.getWorkflowStatus(), WAIT_RETEST)) |
| | | 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); |
| | | boolean passed = validateIndicators(req.getIndicators(), toMetaMap(existing.values(), quality.getItemId())); |
| | | if (!passed && StrUtil.isBlank(req.getReason())) throw new IllegalArgumentException("检验不合格必须填写原因"); |
| | | recordAction(id, initialRecheck ? "INITIAL_RECHECK" : "RETEST", passed ? 1 : 2, req.getReason(), req.getIndicators()); |
| | | Long operatorId = getLoginUserId(); |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | int nextStatus; |
| | | if (returnQuality) { |
| | | // 复检结论统一交由销售部领导审批,不合格时审批后不予退货。 |
| | | nextStatus = WAIT_PROCESS; |
| | | } else if (initialRecheck) { |
| | | nextStatus = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW; |
| | | } else { |
| | | nextStatus = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW; |
| | | } |
| | | MesQcSeedQualityDO update = MesQcSeedQualityDO.builder().id(id).result(passed ? 1 : 2) |
| | | .reason(passed ? "" : req.getReason().trim()) |
| | | .inspectorId(operatorId).inspectedAt(now) |
| | | .workflowStatus(nextStatus).build(); |
| | | if (!initialRecheck) { |
| | | update.setRetestResult(passed ? 1 : 2); |
| | | update.setRetestedAt(now); |
| | | } |
| | | qualityMapper.updateById(update); |
| | | if (returnQuality) { |
| | | afterCommit(() -> startReturnProcess(quality, operatorId)); |
| | | } else if (initialRecheck) { |
| | | afterCommit(() -> startLineProcess(quality.getQcLine(), quality.getBatchCode(), operatorId)); |
| | | } else if (!initialRecheck) { |
| | | // 初检不合格的复检完成后,先交本线部门审批。 |
| | | afterCommit(() -> startLineProcess(quality.getQcLine(), quality.getBatchCode(), operatorId)); |
| | | } |
| | | // 页面、公开追溯读取主指标表;同步为最新一次测值,历次测值在操作留痕表保留。 |
| | | for (MesQcSeedQualitySubmitReqVO.Indicator item : req.getIndicators()) { |
| | | MesQcSeedQualityIndicatorDO indicator = existing.get(item.getCode()); |
| | | if (indicator == null) throw new IllegalArgumentException("质检指标记录缺失"); |
| | |
| | | validateLockedIds(ids, qualities); |
| | | String batchCode = null; |
| | | Integer workflowStatus = null; |
| | | Integer batchQcLine = null; |
| | | boolean first = true; |
| | | for (MesQcSeedQualityDO quality : qualities) { |
| | | if (!Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(quality.getWorkflowStatus())) |
| | |
| | | if (first) { |
| | | batchCode = quality.getBatchCode(); |
| | | workflowStatus = quality.getWorkflowStatus(); |
| | | batchQcLine = quality.getQcLine(); |
| | | first = false; |
| | | } else if (!Objects.equals(batchCode, quality.getBatchCode()) || !Objects.equals(workflowStatus, quality.getWorkflowStatus())) { |
| | | throw new IllegalArgumentException("批量质检只允许同生产批次、同流程状态的记录"); |
| | | } else if (!Objects.equals(batchCode, quality.getBatchCode()) |
| | | || !Objects.equals(workflowStatus, quality.getWorkflowStatus()) |
| | | || !Objects.equals(batchQcLine, quality.getQcLine())) { |
| | | throw new IllegalArgumentException("批量质检只允许同生产批次、同质检线和同流程状态的记录"); |
| | | } |
| | | } |
| | | applyRetestBatch(qualities, req.getIndicators(), req.getReason()); |
| | |
| | | |
| | | @Override |
| | | public Map<String, Object> resolveRetestScope(String code, String scanType) { |
| | | return resolveRetestScope(code, scanType, QC_LINE_PROCESS); |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> resolveRetestScope(String code, String scanType, Integer qcLine) { |
| | | requireQcLine(qcLine); |
| | | checkRetestScanType(scanType); |
| | | Scope scope = scope(code, scanType, false); |
| | | if (scope.canceled()) throw invalidParamException("质检对象已作废"); |
| | | 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<MesQcSeedQualityDO> qualities = codes.isEmpty() ? List.of() : qualityMapper.selectListByCodes(codes).stream() |
| | | .filter(q -> Objects.equals(q.getQcLine(), qcLine) && !isReturnQuality(q)).toList(); |
| | | 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)) |
| | |
| | | result.put("pendingCompanyIds", pendingCompanyIds); result.put("pendingCompanyFailIds", pendingCompanyFailIds); |
| | | result.put("pendingProcessCount", pendingProcessIds.size() + pendingInitialFailIds.size()); |
| | | result.put("pendingCompanyCount", pendingCompanyIds.size() + pendingCompanyFailIds.size()); |
| | | Long itemId = qualities.stream().map(MesQcSeedQualityDO::getItemId).filter(Objects::nonNull).findFirst().orElse(null); |
| | | result.put("itemId", itemId); |
| | | MesQcSeedQualityDO actionable = qualities.stream() |
| | | .filter(q -> Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(q.getWorkflowStatus())) |
| | | .findFirst().orElse(null); |
| | | result.put("indicators", actionable == null ? List.of() |
| | | : toIndicatorPayload(indicatorMapper.selectListByQualityId(actionable.getId()), itemId)); |
| | | return result; |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | | public Map<String, Object> retestScope(cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualityScopeRetestReqVO req) { |
| | | requireQcLine(req.getQcLine()); |
| | | checkRetestScanType(req.getScanType()); |
| | | if (!Set.of(WAIT_RETEST, WAIT_INITIAL_RECHECK).contains(req.getWorkflowStatus())) |
| | | throw new IllegalArgumentException("批量质检状态无效"); |
| | |
| | | if (effective.isEmpty()) throw new IllegalArgumentException("当前范围没有已入库的有效袋码"); |
| | | Set<String> effectiveCodes = effective.stream().map(MesProBagCodeDO::getCode).collect(java.util.stream.Collectors.toSet()); |
| | | List<MesQcSeedQualityDO> pendingPreview = qualityMapper.selectListByCodes(effectiveCodes).stream() |
| | | .filter(q -> Objects.equals(q.getWorkflowStatus(), req.getWorkflowStatus())) |
| | | .filter(q -> Objects.equals(q.getQcLine(), req.getQcLine()) |
| | | && !isReturnQuality(q) && Objects.equals(q.getWorkflowStatus(), req.getWorkflowStatus())) |
| | | .sorted(Comparator.comparing(MesQcSeedQualityDO::getId)).toList(); |
| | | List<Long> expectedChunk = pendingPreview.stream().limit(MAX_BATCH_SIZE).map(MesQcSeedQualityDO::getId).toList(); |
| | | if (expectedChunk.isEmpty() || !confirmed.equals(expectedChunk)) |
| | |
| | | List<MesQcSeedQualityDO> pending = qualityMapper.selectListByIdsForUpdate(confirmed); |
| | | validateLockedIds(confirmed, pending); |
| | | if (pending.stream().anyMatch(quality -> !Objects.equals(quality.getWorkflowStatus(), req.getWorkflowStatus()) |
| | | || !Objects.equals(quality.getQcLine(), req.getQcLine()) || isReturnQuality(quality) |
| | | || !effectiveCodes.contains(quality.getCode()))) |
| | | throw new IllegalArgumentException("待质检记录状态或归属已变化,请重新扫描核对"); |
| | | applyRetestBatch(pending, req.getIndicators(), req.getReason()); |
| | |
| | | |
| | | private void applyRetestBatch(List<MesQcSeedQualityDO> qualities, |
| | | List<MesQcSeedQualitySubmitReqVO.Indicator> submittedIndicators, String reason) { |
| | | boolean passed = validateIndicators(submittedIndicators); |
| | | if (!passed && StrUtil.isBlank(reason)) throw new IllegalArgumentException("检验不合格必须填写原因"); |
| | | List<Long> ids = qualities.stream().map(MesQcSeedQualityDO::getId).toList(); |
| | | Map<Long, Map<String, MesQcSeedQualityIndicatorDO>> existingByQuality = new HashMap<>(); |
| | | for (MesQcSeedQualityIndicatorDO indicator : indicatorMapper.selectListByQualityIds(ids)) { |
| | | existingByQuality.computeIfAbsent(indicator.getQualityId(), ignored -> new HashMap<>()) |
| | | .put(indicator.getIndicatorCode(), indicator); |
| | | } |
| | | Map<String, MesQcSeedQualityIndicatorDO> firstIndicators = existingByQuality.get(qualities.get(0).getId()); |
| | | Map<String, IndicatorMeta> expected = toMetaMap( |
| | | firstIndicators == null ? Collections.<MesQcSeedQualityIndicatorDO>emptyList() : firstIndicators.values(), |
| | | qualities.get(0).getItemId()); |
| | | boolean passed = validateIndicators(submittedIndicators, expected); |
| | | if (!passed && StrUtil.isBlank(reason)) throw new IllegalArgumentException("检验不合格必须填写原因"); |
| | | |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | Long operatorId = getLoginUserId(); |
| | | int result = passed ? 1 : 2; |
| | | List<MesQcSeedQualityDO> qualityUpdates = new ArrayList<>(qualities.size()); |
| | | List<MesQcSeedQualityIndicatorDO> indicatorUpdates = new ArrayList<>(qualities.size() * REQUIRED.size()); |
| | | List<MesQcSeedQualityIndicatorDO> indicatorUpdates = new ArrayList<>(qualities.size() * submittedIndicators.size()); |
| | | List<MesQcSeedQualityActionDO> actions = new ArrayList<>(qualities.size()); |
| | | Set<String> lineProcessTargets = new HashSet<>(); |
| | | List<MesQcSeedQualityDO> returnReady = new ArrayList<>(); |
| | | for (MesQcSeedQualityDO quality : qualities) { |
| | | boolean returnQuality = Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN); |
| | | boolean initialRecheck = Objects.equals(quality.getWorkflowStatus(), WAIT_INITIAL_RECHECK); |
| | | if (!initialRecheck && !Objects.equals(quality.getWorkflowStatus(), WAIT_RETEST)) |
| | | if (returnQuality && !Objects.equals(quality.getWorkflowStatus(), WAIT_RETEST)) |
| | | throw new IllegalArgumentException("所选记录包含不可检验的状态,请刷新后重试"); |
| | | if (!returnQuality && !initialRecheck && !Objects.equals(quality.getWorkflowStatus(), WAIT_RETEST)) |
| | | throw new IllegalArgumentException("所选记录包含不可检验的状态,请刷新后重试"); |
| | | int nextStatus; |
| | | if (returnQuality) { |
| | | nextStatus = WAIT_PROCESS; |
| | | returnReady.add(quality); |
| | | } else if (initialRecheck) { |
| | | nextStatus = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW; |
| | | lineProcessTargets.add(quality.getQcLine() + "#" + quality.getBatchCode()); |
| | | } else { |
| | | nextStatus = passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW; |
| | | lineProcessTargets.add(quality.getQcLine() + "#" + quality.getBatchCode()); |
| | | } |
| | | MesQcSeedQualityDO update = MesQcSeedQualityDO.builder().id(quality.getId()).result(result) |
| | | .reason(passed ? "" : reason.trim()).inspectorId(operatorId).inspectedAt(now) |
| | | .workflowStatus(initialRecheck ? (passed ? WAIT_PROCESS : WAIT_INITIAL_FAIL_REVIEW) |
| | | : (passed ? WAIT_COMPANY : WAIT_COMPANY_FAIL)).build(); |
| | | .workflowStatus(nextStatus).build(); |
| | | if (!initialRecheck) { |
| | | update.setRetestResult(result); |
| | | update.setRetestedAt(now); |
| | |
| | | reason, submittedIndicators, operatorId, now)); |
| | | } |
| | | qualityMapper.updateBatch(qualityUpdates, WRITE_BATCH_SIZE); |
| | | indicatorMapper.updateBatch(indicatorUpdates, WRITE_BATCH_SIZE * REQUIRED.size()); |
| | | indicatorMapper.updateBatch(indicatorUpdates, WRITE_BATCH_SIZE * submittedIndicators.size()); |
| | | actionMapper.insertBatch(actions, WRITE_BATCH_SIZE); |
| | | // 复检按记录所属质检线分别发起本线审批,避免提前触发另一条线。 |
| | | lineProcessTargets.forEach(target -> { |
| | | String[] parts = target.split("#", 2); |
| | | afterCommit(() -> startLineProcess(Integer.valueOf(parts[0]), parts[1], operatorId)); |
| | | }); |
| | | returnReady.forEach(quality -> afterCommit(() -> startReturnProcess(quality, operatorId))); |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | |
| | | 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()); |
| | | requireLegacyApproval(quality); |
| | | applyProcessApproval(quality, approved, opinion, true); |
| | | } |
| | | |
| | | /** |
| | | * 本线领导审批:生产质检通过后进入公司质检审批,驳回后重新初检;经销商退货质检则直接终审。 |
| | | */ |
| | | private void applyProcessApproval(MesQcSeedQualityDO quality, boolean approved, String opinion, boolean validateApprover) { |
| | | if (validateApprover) { |
| | | 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); |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | Long operatorId = getLoginUserId(); |
| | | if (Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)) { |
| | | if (!Objects.equals(quality.getWorkflowStatus(), WAIT_PROCESS)) |
| | | throw new IllegalArgumentException("经销商退货质检当前状态不能进行销售部审批"); |
| | | applyReturnApproval(quality, approved, opinion, operatorId, now); |
| | | return; |
| | | } |
| | | int next = approved ? (Objects.equals(quality.getQcLine(), 0) ? WAIT_RETEST |
| | | : Objects.equals(quality.getResult(), 1) ? WAIT_COMPANY : WAIT_COMPANY_FAIL) |
| | | : WAIT_INITIAL_RECHECK; |
| | | qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId()).workflowStatus(next) |
| | | .processApprovedAt(now).build()); |
| | | if (!approved) qualityMapper.clearLineProcessInstance(quality.getId()); |
| | | actionMapper.insert(newAction(quality.getId(), approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", |
| | | quality.getResult(), opinion, null, operatorId, now)); |
| | | if (approved && !Objects.equals(quality.getQcLine(), 0)) |
| | | afterCommit(() -> maybeStartCompanyProcess(quality.getBatchCode(), operatorId)); |
| | | } |
| | | |
| | | @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())) |
| | | if (quality == null) throw new IllegalArgumentException("质检记录不存在"); |
| | | if (Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)) |
| | | throw new IllegalArgumentException("经销商退货质检请使用销售部审批"); |
| | | if (!Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus())) |
| | | throw new IllegalArgumentException("当前状态不能进行复检审核"); |
| | | requireLegacyApproval(quality); |
| | | approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(), |
| | | MesQcApproveNodeEnum.COMPANY.getNode(), getLoginUserId()); |
| | | applyCompanyApproval(quality.getCode(), approved, opinion, getLoginUserId()); |
| | | } |
| | | |
| | | /** |
| | | * 公司质检审批:以「袋」为单位成对审批,两条质检线都走完才允许审批。 |
| | | * 两条线均合格才放行入库,任一条线不合格则整袋报废;驳回则两条线全部退回复检。 |
| | | */ |
| | | private void applyCompanyApproval(String code, boolean approved, String opinion, Long operatorId) { |
| | | 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); |
| | | List<MesQcSeedQualityDO> pair = qualityMapper.selectListByCodeForUpdate(code).stream() |
| | | .filter(q -> !isReturnQuality(q)).toList(); |
| | | if (pair.size() != 2 || pair.stream().anyMatch(q -> !Objects.equals(q.getQcLine(), QC_LINE_PROCESS) |
| | | && !Objects.equals(q.getQcLine(), QC_LINE_SALES))) |
| | | throw new IllegalArgumentException("袋码 " + code + " 的双线质检记录不完整,无法公司审批"); |
| | | if (pair.stream().anyMatch(q -> !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(q.getWorkflowStatus()))) |
| | | throw new IllegalArgumentException("袋码 " + code + " 的双线质检尚未全部完成,不能进行公司质检审批"); |
| | | boolean passed = pair.stream().allMatch(q -> Objects.equals(q.getWorkflowStatus(), WAIT_COMPANY)); |
| | | MesQcSeedQualityDO representative = pair.stream() |
| | | .filter(q -> Objects.equals(q.getQcLine(), QC_LINE_PROCESS)).findFirst().orElse(pair.get(0)); |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | if (approved) { |
| | | finalizeStock(representative, passed); |
| | | qualityMapper.updateBatch(pair.stream().map(quality -> MesQcSeedQualityDO.builder() |
| | | .id(quality.getId()).workflowStatus(passed ? RELEASED : SCRAPPED) |
| | | .companyApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE); |
| | | } else { |
| | | qualityMapper.clearCompanyProcessInstance(code); |
| | | pair.forEach(quality -> qualityMapper.clearLineProcessInstance(quality.getId())); |
| | | qualityMapper.updateBatch(pair.stream().map(quality -> MesQcSeedQualityDO.builder() |
| | | .id(quality.getId()).workflowStatus(WAIT_RETEST).companyApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE); |
| | | } |
| | | actionMapper.insertBatch(pair.stream().map(quality -> newAction(quality.getId(), |
| | | approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", quality.getResult(), opinion, null, operatorId, now)).toList(), |
| | | WRITE_BATCH_SIZE); |
| | | } |
| | | |
| | | @Override @Transactional(rollbackFor = Exception.class) |
| | |
| | | List<Long> confirmed = approvalIds(ids); |
| | | List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByIdsForUpdate(confirmed); |
| | | validateLockedIds(confirmed, qualities); |
| | | qualities.forEach(MesQcSeedQualityServiceImpl::requireLegacyApproval); |
| | | if (qualities.stream().anyMatch(quality -> !Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus()))) |
| | | throw new IllegalArgumentException("所选记录包含不能进行初检审核的状态,请刷新后重试"); |
| | | approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(), |
| | |
| | | if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见"); |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | Long operatorId = getLoginUserId(); |
| | | List<MesQcSeedQualityDO> production = qualities.stream() |
| | | .filter(quality -> !Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)).toList(); |
| | | qualities.stream().filter(quality -> Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)) |
| | | .forEach(quality -> applyProcessApproval(quality, approved, opinion, false)); |
| | | if (production.isEmpty()) return; |
| | | int next = approved ? WAIT_RETEST : WAIT_INITIAL_RECHECK; |
| | | qualityMapper.updateBatch(qualities.stream().map(quality -> MesQcSeedQualityDO.builder().id(quality.getId()) |
| | | .workflowStatus(next).processApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE); |
| | | actionMapper.insertBatch(qualities.stream().map(quality -> newAction(quality.getId(), |
| | | qualityMapper.updateBatch(production.stream().map(quality -> MesQcSeedQualityDO.builder().id(quality.getId()) |
| | | .workflowStatus(next) |
| | | .processApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE); |
| | | if (!approved) production.forEach(quality -> qualityMapper.clearLineProcessInstance(quality.getId())); |
| | | actionMapper.insertBatch(production.stream().map(quality -> newAction(quality.getId(), |
| | | approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), opinion, null, operatorId, now)).toList(), |
| | | WRITE_BATCH_SIZE); |
| | | } |
| | |
| | | List<Long> confirmed = approvalIds(ids); |
| | | List<MesQcSeedQualityDO> qualities = qualityMapper.selectListByIdsForUpdate(confirmed); |
| | | validateLockedIds(confirmed, qualities); |
| | | if (qualities.stream().anyMatch(quality -> !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus()))) |
| | | throw new IllegalArgumentException("所选记录包含不能进行复检审核的状态,请刷新后重试"); |
| | | qualities.forEach(MesQcSeedQualityServiceImpl::requireLegacyApproval); |
| | | if (qualities.stream().anyMatch(quality -> Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN))) |
| | | throw new IllegalArgumentException("经销商退货质检请使用销售部审批"); |
| | | approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(), |
| | | MesQcApproveNodeEnum.COMPANY.getNode(), getLoginUserId()); |
| | | if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见"); |
| | | if (approved) { |
| | | for (MesQcSeedQualityDO quality : qualities) finalizeStock(quality, quality.getWorkflowStatus() == WAIT_COMPANY); |
| | | } |
| | | Long operatorId = getLoginUserId(); |
| | | // 公司审批以袋为单位成对处理,同一袋的两条线记录一次审批 |
| | | qualities.stream().map(MesQcSeedQualityDO::getCode).distinct().sorted() |
| | | .forEach(code -> applyCompanyApproval(code, approved, opinion, operatorId)); |
| | | } |
| | | |
| | | // ==================== 经销商退货质检 ==================== |
| | | |
| | | @Override |
| | | public Map<String, Object> resolveReturnQuality(Long returnSalesId) { |
| | | MesWmReturnSalesDO returnSales = returnSalesService.getReturnSales(returnSalesId); |
| | | if (returnSales == null) throw new IllegalArgumentException("销售退货单不存在"); |
| | | List<MesWmReturnSalesLineDO> lines = returnSalesLineService.getReturnSalesLineListByReturnId(returnSalesId); |
| | | MesWmReturnSalesLineDO first = lines.stream().filter(line -> line.getItemId() != null).findFirst().orElse(null); |
| | | Long itemId = first == null ? null : first.getItemId(); |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByReturnSalesId(returnSalesId); |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("returnSalesId", returnSales.getId()); |
| | | result.put("returnSalesCode", returnSales.getCode()); |
| | | result.put("returnSalesStatus", returnSales.getStatus()); |
| | | result.put("itemId", itemId); |
| | | result.put("batchId", first == null ? null : first.getBatchId()); |
| | | result.put("batchCode", first == null ? null : first.getBatchCode()); |
| | | result.put("indicators", toRangePayload(itemId)); |
| | | result.put("qualityId", quality == null ? null : quality.getId()); |
| | | result.put("result", quality == null ? null : quality.getResult()); |
| | | result.put("workflowStatus", quality == null ? null : quality.getWorkflowStatus()); |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long submitReturnQuality(MesQcSeedReturnQualitySubmitReqVO reqVO) { |
| | | MesWmReturnSalesDO returnSales = returnSalesService.getReturnSales(reqVO.getReturnSalesId()); |
| | | if (returnSales == null) throw new IllegalArgumentException("销售退货单不存在"); |
| | | if (qualityMapper.selectByReturnSalesId(returnSales.getId()) != null) |
| | | throw new IllegalArgumentException("该销售退货单已完成退货质检,禁止重复提交"); |
| | | if (!Set.of(MesWmReturnSalesStatusEnum.PREPARE.getStatus(), |
| | | MesWmReturnSalesStatusEnum.CONFIRMED.getStatus()).contains(returnSales.getStatus())) |
| | | throw new IllegalArgumentException("当前销售退货单状态不能进行退货质检"); |
| | | List<MesWmReturnSalesLineDO> lines = returnSalesLineService.getReturnSalesLineListByReturnId(returnSales.getId()); |
| | | if (CollUtil.isEmpty(lines)) throw new IllegalArgumentException("销售退货单没有明细,无法质检"); |
| | | MesWmReturnSalesLineDO first = lines.stream().filter(line -> line.getItemId() != null).findFirst() |
| | | .orElseThrow(() -> new IllegalArgumentException("销售退货单明细缺少物料,无法质检")); |
| | | Map<String, IndicatorMeta> expected = requireEffectiveIndicators(first.getItemId()); |
| | | boolean passed = validateIndicators(reqVO.getIndicators(), expected); |
| | | if (!passed && StrUtil.isBlank(reqVO.getReason())) throw new IllegalArgumentException("质检不合格必须填写原因"); |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | Long operatorId = getLoginUserId(); |
| | | List<MesQcSeedQualityDO> updates = qualities.stream().map(quality -> MesQcSeedQualityDO.builder().id(quality.getId()) |
| | | .workflowStatus(approved ? (quality.getWorkflowStatus() == WAIT_COMPANY ? RELEASED : SCRAPPED) : WAIT_RETEST) |
| | | .companyApprovedAt(now).build()).toList(); |
| | | qualityMapper.updateBatch(updates, WRITE_BATCH_SIZE); |
| | | actionMapper.insertBatch(qualities.stream().map(quality -> newAction(quality.getId(), |
| | | approved ? "COMPANY_APPROVE" : "COMPANY_REJECT", quality.getResult(), opinion, null, operatorId, now)).toList(), |
| | | int result = passed ? 1 : 2; |
| | | MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder() |
| | | .code(returnSales.getCode()).qcLine(QC_LINE_SALES).bizType(BIZ_TYPE_RETURN) |
| | | .itemId(first.getItemId()).batchId(first.getBatchId()).batchCode(first.getBatchCode()) |
| | | .result(result).reason(passed ? "" : reqVO.getReason().trim()) |
| | | // 初检合格进入销售部审批;不合格先复检。 |
| | | .workflowStatus(passed ? WAIT_PROCESS : WAIT_RETEST) |
| | | .returnSalesId(returnSales.getId()).returnSalesCode(returnSales.getCode()) |
| | | .inspectorId(operatorId).inspectedAt(now).build(); |
| | | qualityMapper.insert(quality); |
| | | for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) { |
| | | IndicatorMeta meta = expected.get(item.getCode()); |
| | | indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()) |
| | | .indicatorCode(item.getCode()).indicatorName(meta.name()).unit(meta.unit()) |
| | | .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build()); |
| | | } |
| | | recordAction(quality.getId(), "INITIAL", result, reqVO.getReason(), reqVO.getIndicators()); |
| | | returnSalesService.updateReturnSalesStatus(returnSales.getId(), MesWmReturnSalesStatusEnum.CONFIRMED.getStatus()); |
| | | if (passed) afterCommit(() -> startReturnProcess(quality, operatorId)); |
| | | return quality.getId(); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void returnApprove(Long id, boolean approved, String opinion) { |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByIdForUpdate(id); |
| | | if (quality == null || !Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)) |
| | | throw new IllegalArgumentException("质检记录不存在或不是经销商退货质检"); |
| | | requireLegacyApproval(quality); |
| | | if (!Objects.equals(quality.getWorkflowStatus(), WAIT_PROCESS)) |
| | | throw new IllegalArgumentException("当前状态不能进行销售部审批"); |
| | | approverService.validateApprover(MesQcApprovalTypeEnum.SEED_QUALITY.getType(), |
| | | MesQcApproveNodeEnum.PROCESS.getNode(), getLoginUserId()); |
| | | applyReturnApproval(quality, approved, opinion, getLoginUserId(), LocalDateTime.now()); |
| | | } |
| | | |
| | | /** |
| | | * 销售部领导审批:通过则退货单进入执行;不通过则不予退货并取消退货单,质检记录留存。 |
| | | */ |
| | | private void applyReturnApproval(MesQcSeedQualityDO quality, boolean approved, String opinion, |
| | | Long operatorId, LocalDateTime now) { |
| | | if (!approved && StrUtil.isBlank(opinion)) throw new IllegalArgumentException("驳回时请填写审批意见"); |
| | | if (approved && Objects.equals(quality.getResult(), 1)) { |
| | | returnSalesService.updateReturnSalesStatus(quality.getReturnSalesId(), |
| | | MesWmReturnSalesStatusEnum.APPROVING.getStatus()); |
| | | qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId()) |
| | | .workflowStatus(RELEASED).processApprovedAt(now).build()); |
| | | } else { |
| | | cancelReturnSalesQuietly(quality.getReturnSalesId()); |
| | | qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId()) |
| | | .workflowStatus(SCRAPPED).processApprovedAt(now).build()); |
| | | } |
| | | actionMapper.insert(newAction(quality.getId(), approved ? "RETURN_APPROVE" : "RETURN_REJECT", |
| | | quality.getResult(), opinion, null, operatorId, now)); |
| | | } |
| | | |
| | | // ==================== BPM 流程回调 ==================== |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void handleLineProcessResult(String processInstanceId, Integer qcLine, String batchCode, boolean approved, String reason) { |
| | | if (StrUtil.isBlank(processInstanceId) || qcLine == null || StrUtil.isBlank(batchCode)) return; |
| | | List<MesQcSeedQualityDO> pending = qualityMapper.selectListByProcessInstanceId(processInstanceId).stream() |
| | | .filter(quality -> Objects.equals(quality.getQcLine(), qcLine) |
| | | && Objects.equals(quality.getBatchCode(), batchCode) |
| | | && !isReturnQuality(quality) |
| | | && Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus())) |
| | | .toList(); |
| | | if (pending.isEmpty()) { |
| | | log.info("[handleLineProcessResult][批次 {} 质检线 {} 无待审批记录,忽略 BPM 事件]", batchCode, qcLine); |
| | | return; |
| | | } |
| | | List<Long> ids = pending.stream().map(MesQcSeedQualityDO::getId).sorted().toList(); |
| | | List<MesQcSeedQualityDO> locked = qualityMapper.selectListByIdsForUpdate(ids); |
| | | validateLockedIds(ids, locked); |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | int next = approved ? WAIT_COMPANY : WAIT_INITIAL_RECHECK; |
| | | String finalReason = StrUtil.blankToDefault(reason, approved ? "审批通过" : "审批驳回"); |
| | | qualityMapper.updateBatch(locked.stream().map(quality -> MesQcSeedQualityDO.builder().id(quality.getId()) |
| | | .workflowStatus(approved && Objects.equals(quality.getResult(), 2) ? WAIT_COMPANY_FAIL : next) |
| | | .processApprovedAt(now).build()).toList(), WRITE_BATCH_SIZE); |
| | | if (!approved) locked.forEach(quality -> qualityMapper.clearLineProcessInstance(quality.getId())); |
| | | actionMapper.insertBatch(locked.stream().map(quality -> newAction(quality.getId(), |
| | | approved ? "PROCESS_APPROVE" : "PROCESS_REJECT", quality.getResult(), finalReason, null, null, now)).toList(), |
| | | WRITE_BATCH_SIZE); |
| | | if (approved) { |
| | | Long inspectorId = locked.stream().map(MesQcSeedQualityDO::getInspectorId) |
| | | .filter(Objects::nonNull).findFirst().orElse(null); |
| | | afterCommit(() -> maybeStartCompanyProcess(batchCode, inspectorId)); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void handleCompanyProcessResult(String processInstanceId, String businessKey, boolean approved, String reason) { |
| | | if (StrUtil.isBlank(processInstanceId)) return; |
| | | // 公司审批按生产批次发起,一个流程实例覆盖该批次的所有袋; |
| | | // 按流程实例编号回查可同时兼容历史按袋码发起的审批。 |
| | | List<MesQcSeedQualityDO> pending = qualityMapper.selectListByCompanyProcessInstanceId(processInstanceId).stream() |
| | | .filter(quality -> !isReturnQuality(quality) |
| | | && Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus())) |
| | | .toList(); |
| | | if (pending.isEmpty()) { |
| | | log.info("[handleCompanyProcessResult][流程 {} 无待公司审批记录,忽略 BPM 事件]", processInstanceId); |
| | | return; |
| | | } |
| | | String finalReason = StrUtil.blankToDefault(reason, approved ? "公司质检审批通过" : "公司质检审批驳回"); |
| | | // 放行/报废以袋为单位落库,同一批次的多个袋依次处理 |
| | | pending.stream().map(MesQcSeedQualityDO::getCode).distinct().sorted() |
| | | .forEach(code -> applyCompanyApproval(code, approved, finalReason, null)); |
| | | log.info("[handleCompanyProcessResult][公司质检审批 {} 完成,批次/袋码 {} 共处理 {} 条记录]", |
| | | processInstanceId, businessKey, pending.size()); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void handleReturnProcessResult(String processInstanceId, Long returnSalesId, boolean approved, String reason) { |
| | | if (StrUtil.isBlank(processInstanceId) || returnSalesId == null) return; |
| | | MesQcSeedQualityDO quality = qualityMapper.selectByReturnSalesIdForUpdate(returnSalesId); |
| | | if (quality == null || !Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN) |
| | | || !Objects.equals(quality.getProcessInstanceId(), processInstanceId) |
| | | || !Objects.equals(quality.getWorkflowStatus(), WAIT_PROCESS)) { |
| | | log.info("[handleReturnProcessResult][销售退货单 {} 无待审批退货质检,忽略 BPM 事件]", returnSalesId); |
| | | return; |
| | | } |
| | | applyReturnApproval(quality, approved, StrUtil.blankToDefault(reason, approved ? "审批通过" : "审批驳回"), |
| | | null, LocalDateTime.now()); |
| | | } |
| | | |
| | | private void startLineProcess(Integer qcLine, String batchCode, Long operatorId) { |
| | | if (qcLine == null || StrUtil.isBlank(batchCode) || operatorId == null) return; |
| | | List<MesQcSeedQualityDO> pending = qualityMapper.selectListByBatchCode(batchCode).stream() |
| | | .filter(quality -> Objects.equals(quality.getQcLine(), qcLine) |
| | | && !isReturnQuality(quality) |
| | | && StrUtil.isBlank(quality.getProcessInstanceId()) |
| | | && Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus())) |
| | | .toList(); |
| | | if (pending.isEmpty()) return; |
| | | Map<String, Object> variables = new HashMap<>(); |
| | | variables.put("qcLine", qcLine); |
| | | variables.put("batchCode", batchCode); |
| | | String processInstanceId = processInstanceApi.createProcessInstance(operatorId, |
| | | new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey(LINE_PROCESS_KEY) |
| | | .setBusinessKey(qcLine + "#" + batchCode) |
| | | .setVariables(variables)); |
| | | for (MesQcSeedQualityDO quality : pending) { |
| | | qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId()) |
| | | .processInstanceId(processInstanceId).build()); |
| | | } |
| | | log.info("[startLineProcess][批次 {} 质检线 {} 已发起本线领导审批 {}", batchCode, qcLine, processInstanceId); |
| | | } |
| | | |
| | | /** |
| | | * 双线部门审批全部完成后,按生产批次统一发起一次公司质检审批。 |
| | | * 同一批次的多袋共用同一个流程实例,避免每袋各发一条审批。 |
| | | */ |
| | | private void maybeStartCompanyProcess(String batchCode, Long operatorId) { |
| | | if (StrUtil.isBlank(batchCode) || operatorId == null) return; |
| | | List<MesQcSeedQualityDO> records = qualityMapper.selectListByBatchCode(batchCode).stream() |
| | | .filter(quality -> !isReturnQuality(quality)) |
| | | // 已放行/已报废的记录属于已终结的批次,不再参与新的公司审批 |
| | | .filter(quality -> !Set.of(RELEASED, SCRAPPED).contains(quality.getWorkflowStatus())) |
| | | .toList(); |
| | | if (records.isEmpty()) return; |
| | | if (records.stream().anyMatch(quality -> !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL) |
| | | .contains(quality.getWorkflowStatus()))) return; |
| | | if (records.stream().anyMatch(quality -> StrUtil.isNotBlank(quality.getCompanyProcessInstanceId()))) return; |
| | | Map<String, Object> variables = new HashMap<>(); |
| | | variables.put("batchCode", batchCode); |
| | | // 兼容流程条件中引用的 code 变量 |
| | | variables.put("code", batchCode); |
| | | variables.put("passed", records.stream().allMatch(quality -> Objects.equals(quality.getWorkflowStatus(), WAIT_COMPANY))); |
| | | String processInstanceId = processInstanceApi.createProcessInstance(operatorId, |
| | | new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey(COMPANY_PROCESS_KEY) |
| | | .setBusinessKey(batchCode) |
| | | .setVariables(variables)); |
| | | for (MesQcSeedQualityDO quality : records) { |
| | | qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId()) |
| | | .companyProcessInstanceId(processInstanceId).build()); |
| | | } |
| | | log.info("[maybeStartCompanyProcess][批次 {} 双线质检完成,已发起公司质检审批 {},覆盖 {} 条记录]", |
| | | batchCode, processInstanceId, records.size()); |
| | | } |
| | | |
| | | private void startReturnProcess(MesQcSeedQualityDO quality, Long operatorId) { |
| | | if (quality == null || quality.getReturnSalesId() == null || StrUtil.isNotBlank(quality.getProcessInstanceId())) |
| | | return; |
| | | Map<String, Object> variables = new HashMap<>(); |
| | | variables.put("returnSalesId", quality.getReturnSalesId()); |
| | | variables.put("returnSalesCode", quality.getReturnSalesCode()); |
| | | variables.put("code", quality.getCode()); |
| | | String processInstanceId = processInstanceApi.createProcessInstance(operatorId, |
| | | new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey(RETURN_PROCESS_KEY) |
| | | .setBusinessKey(String.valueOf(quality.getReturnSalesId())) |
| | | .setVariables(variables)); |
| | | qualityMapper.updateById(MesQcSeedQualityDO.builder().id(quality.getId()) |
| | | .processInstanceId(processInstanceId).build()); |
| | | log.info("[startReturnProcess][销售退货单 {} 已发起销售部领导审批 {}]", quality.getReturnSalesId(), processInstanceId); |
| | | } |
| | | |
| | | @Override |
| | | public void retryBpmProcess(Long id) { |
| | | MesQcSeedQualityDO quality = qualityMapper.selectById(id); |
| | | if (quality == null) throw invalidParamException("质检记录不存在"); |
| | | Long operatorId = getLoginUserId(); |
| | | if (Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN)) { |
| | | if (!Objects.equals(quality.getWorkflowStatus(), WAIT_PROCESS)) |
| | | throw invalidParamException("当前退货质检不需要发起审批"); |
| | | if (StrUtil.isNotBlank(quality.getProcessInstanceId())) |
| | | throw invalidParamException("退货质检审批流程已经发起"); |
| | | startReturnProcess(quality, operatorId); |
| | | } else if (Set.of(WAIT_PROCESS, WAIT_INITIAL_FAIL_REVIEW).contains(quality.getWorkflowStatus())) { |
| | | if (StrUtil.isNotBlank(quality.getProcessInstanceId())) |
| | | throw invalidParamException("部门审批流程已经发起"); |
| | | startLineProcess(quality.getQcLine(), quality.getBatchCode(), operatorId); |
| | | } else if (Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL).contains(quality.getWorkflowStatus())) { |
| | | String batchCode = quality.getBatchCode(); |
| | | if (StrUtil.isBlank(batchCode)) |
| | | throw invalidParamException("质检记录未绑定生产批次,不能发起公司审批"); |
| | | List<MesQcSeedQualityDO> batchRecords = qualityMapper.selectListByBatchCode(batchCode).stream() |
| | | .filter(q -> !isReturnQuality(q)) |
| | | .filter(q -> !Set.of(RELEASED, SCRAPPED).contains(q.getWorkflowStatus())) |
| | | .toList(); |
| | | if (batchRecords.isEmpty() || batchRecords.stream().anyMatch(q -> !Set.of(WAIT_COMPANY, WAIT_COMPANY_FAIL) |
| | | .contains(q.getWorkflowStatus()))) |
| | | throw invalidParamException("同批次双线部门审批尚未全部完成,不能发起公司审批"); |
| | | if (batchRecords.stream().anyMatch(q -> StrUtil.isNotBlank(q.getCompanyProcessInstanceId()))) |
| | | throw invalidParamException("公司质检审批流程已经发起"); |
| | | maybeStartCompanyProcess(batchCode, operatorId); |
| | | } else { |
| | | throw invalidParamException("当前质检状态不需要发起审批"); |
| | | } |
| | | } |
| | | |
| | | private void cancelReturnSalesQuietly(Long returnSalesId) { |
| | | if (returnSalesId == null) return; |
| | | try { |
| | | returnSalesService.cancelReturnSales(returnSalesId); |
| | | } catch (Exception ex) { |
| | | log.warn("[cancelReturnSalesQuietly][销售退货单 {} 取消失败: {}]", returnSalesId, ex.getMessage()); |
| | | } |
| | | } |
| | | |
| | | private void afterCommit(Runnable action) { |
| | | TransactionSynchronization synchronization = new TransactionSynchronization() { |
| | | @Override |
| | | public void afterCommit() { |
| | | try { |
| | | action.run(); |
| | | } catch (Exception ex) { |
| | | log.error("[afterCommit][BPM 流程启动失败,请修复流程配置并在质检记录中重新发起审批]", ex); |
| | | } |
| | | } |
| | | }; |
| | | if (TransactionSynchronizationManager.isSynchronizationActive()) { |
| | | TransactionSynchronizationManager.registerSynchronization(synchronization); |
| | | } else { |
| | | synchronization.afterCommit(); |
| | | } |
| | | } |
| | | |
| | | /** 历史数据 bizType 为空,按生产入库质检处理 */ |
| | | private static void requireQcLine(Integer qcLine) { |
| | | if (!Objects.equals(qcLine, QC_LINE_PROCESS) && !Objects.equals(qcLine, QC_LINE_SALES)) |
| | | throw new IllegalArgumentException("质检线必须为加工部或销售部"); |
| | | } |
| | | |
| | | /** 生产双线记录只能走 BPM 审批;经销商退货质检不拆分质检线,仍可使用人工审批接口。 */ |
| | | private static void requireLegacyApproval(MesQcSeedQualityDO quality) { |
| | | if (!isReturnQuality(quality) && (Objects.equals(quality.getQcLine(), QC_LINE_PROCESS) |
| | | || Objects.equals(quality.getQcLine(), QC_LINE_SALES))) |
| | | throw new IllegalArgumentException("该质检记录由 BPM 审批,请在流程待办中处理"); |
| | | } |
| | | |
| | | private static boolean isReturnQuality(MesQcSeedQualityDO quality) { |
| | | return Objects.equals(quality.getBizType(), BIZ_TYPE_RETURN); |
| | | } |
| | | |
| | | private List<Long> approvalIds(List<Long> ids) { |
| | |
| | | return quality; |
| | | } |
| | | |
| | | private boolean validateIndicators(List<MesQcSeedQualitySubmitReqVO.Indicator> indicators) { |
| | | if (indicators == null || indicators.size() != REQUIRED.size()) throw new IllegalArgumentException("必须提交四项指标"); |
| | | private record IndicatorMeta(String code, String name, String unit) { } |
| | | |
| | | private Map<String, IndicatorMeta> requireEffectiveIndicators(Long itemId) { |
| | | Map<String, IndicatorMeta> result = new LinkedHashMap<>(); |
| | | for (MesQcSeedIndicatorRangeDO range : rangeService.getEffectiveRanges(itemId)) { |
| | | result.put(range.getIndicatorCode(), new IndicatorMeta(range.getIndicatorCode(), |
| | | StrUtil.blankToDefault(range.getIndicatorName(), range.getIndicatorCode()), |
| | | StrUtil.blankToDefault(range.getUnit(), "%"))); |
| | | } |
| | | if (result.isEmpty()) |
| | | throw new IllegalArgumentException("该产品尚未配置质检项,请先在「质检项合格范围配置」中维护"); |
| | | return result; |
| | | } |
| | | |
| | | private Map<String, IndicatorMeta> toMetaMap(Collection<MesQcSeedQualityIndicatorDO> indicators, Long itemId) { |
| | | Map<String, MesQcSeedIndicatorRangeDO> config = rangeService.getEffectiveMap(itemId); |
| | | Map<String, IndicatorMeta> result = new LinkedHashMap<>(); |
| | | for (MesQcSeedQualityIndicatorDO indicator : indicators) { |
| | | MesQcSeedIndicatorRangeDO range = config.get(indicator.getIndicatorCode()); |
| | | String name = StrUtil.blankToDefault(indicator.getIndicatorName(), |
| | | range == null ? indicator.getIndicatorCode() : range.getIndicatorName()); |
| | | String unit = StrUtil.blankToDefault(indicator.getUnit(), range == null ? "%" : range.getUnit()); |
| | | result.put(indicator.getIndicatorCode(), new IndicatorMeta(indicator.getIndicatorCode(), name, unit)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private boolean validateIndicators(List<MesQcSeedQualitySubmitReqVO.Indicator> indicators, |
| | | Map<String, IndicatorMeta> expected) { |
| | | if (indicators == null || indicators.isEmpty()) throw new IllegalArgumentException("请至少提交一项质检指标"); |
| | | if (expected.isEmpty()) throw new IllegalArgumentException("该产品尚未配置质检项"); |
| | | if (indicators.size() != expected.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()) |
| | | if (item == null || StrUtil.isBlank(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"); |
| | | throw new IllegalArgumentException("质检项的实测值和合格范围必须完整且不可重复"); |
| | | if (!expected.containsKey(item.getCode())) |
| | | throw new IllegalArgumentException("质检项未在产品配置中启用:" + item.getCode()); |
| | | if (item.getMinValue().compareTo(item.getMaxValue()) > 0) throw new IllegalArgumentException("质检项下限不能大于上限"); |
| | | if (item.getValue().compareTo(BigDecimal.ZERO) < 0 || item.getMinValue().compareTo(BigDecimal.ZERO) < 0 |
| | | || item.getMaxValue().compareTo(BigDecimal.ZERO) < 0) |
| | | throw new IllegalArgumentException("质检项数值不能为负数"); |
| | | if (item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false; |
| | | } |
| | | if (!codes.equals(expected.keySet())) throw new IllegalArgumentException("产品质检项不完整,请刷新后重新录入"); |
| | | return passed; |
| | | } |
| | | |
| | | private Long resolveItemId(Scope scope) { |
| | | Long itemId = scope.bags().stream().map(MesProBagCodeDO::getItemId).filter(Objects::nonNull).findFirst().orElse(null); |
| | | if (itemId != null || scope.bags().isEmpty()) return itemId; |
| | | Long batchId = scope.bags().get(0).getBatchId(); |
| | | if (batchId == null) return null; |
| | | MesProBatchDO batch = proBatchService.getBatch(batchId); |
| | | return batch == null ? null : batch.getItemId(); |
| | | } |
| | | |
| | | private List<Map<String, Object>> toRangePayload(Long itemId) { |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | for (MesQcSeedIndicatorRangeDO range : rangeService.getEffectiveRanges(itemId)) { |
| | | Map<String, Object> item = new HashMap<>(); |
| | | item.put("code", range.getIndicatorCode()); |
| | | item.put("name", StrUtil.blankToDefault(range.getIndicatorName(), range.getIndicatorCode())); |
| | | item.put("unit", StrUtil.blankToDefault(range.getUnit(), "%")); |
| | | item.put("minValue", range.getMinValue()); |
| | | item.put("maxValue", range.getMaxValue()); |
| | | result.add(item); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private List<Map<String, Object>> toIndicatorPayload(List<MesQcSeedQualityIndicatorDO> indicators, Long itemId) { |
| | | Map<String, MesQcSeedIndicatorRangeDO> config = rangeService.getEffectiveMap(itemId); |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | for (MesQcSeedQualityIndicatorDO indicator : indicators) { |
| | | MesQcSeedIndicatorRangeDO range = config.get(indicator.getIndicatorCode()); |
| | | Map<String, Object> item = new HashMap<>(); |
| | | item.put("code", indicator.getIndicatorCode()); |
| | | item.put("name", StrUtil.blankToDefault(indicator.getIndicatorName(), |
| | | range == null ? indicator.getIndicatorCode() : range.getIndicatorName())); |
| | | item.put("unit", StrUtil.blankToDefault(indicator.getUnit(), range == null ? "%" : range.getUnit())); |
| | | item.put("minValue", indicator.getMinValue()); |
| | | item.put("maxValue", indicator.getMaxValue()); |
| | | result.add(item); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private void recordAction(Long id, String action, Integer result, String opinion, |
| | |
| | | /** |
| | | * 解析合格库的默认入库库区与库位:取第一个同时配置了库位的非冻结库区。 |
| | | * |
| | | * 质检按仓库维度选择入库目标,库区/库位由系统自动落点;合格库缺少可用库区或库位时, |
| | | * 库存事务无法写入,直接提示管理员完善仓库配置。 |
| | | * 库区/库位为可选项,仓库未配置时按仓库层级直接入库,返回 null。 |
| | | */ |
| | | private Long[] resolveQualifiedLocation(MesWmWarehouseDO qualifiedWarehouse) { |
| | | List<MesWmWarehouseLocationDO> locations = warehouseLocationService.getWarehouseLocationList(qualifiedWarehouse.getId()) |
| | | .stream().filter(location -> !Boolean.TRUE.equals(location.getFrozen())).toList(); |
| | | if (CollUtil.isEmpty(locations)) |
| | | throw new IllegalArgumentException("合格库「" + qualifiedWarehouse.getName() + "」未配置可用库区,请先在仓库管理中完善库区与库位"); |
| | | if (CollUtil.isEmpty(locations)) { |
| | | return new Long[]{null, null}; |
| | | } |
| | | for (MesWmWarehouseLocationDO location : locations) { |
| | | List<MesWmWarehouseAreaDO> areas = warehouseAreaService.getWarehouseAreaList(location.getId()) |
| | | .stream().filter(area -> !Boolean.TRUE.equals(area.getFrozen())).toList(); |
| | | if (CollUtil.isNotEmpty(areas)) return new Long[]{location.getId(), areas.get(0).getId()}; |
| | | } |
| | | throw new IllegalArgumentException("合格库「" + qualifiedWarehouse.getName() + "」未配置可用库位,请先在仓库管理中完善库区与库位"); |
| | | return new Long[]{null, null}; |
| | | } |
| | | |
| | | private MesWmMaterialStockDO getOrCreateQualifiedStock(MesWmMaterialStockDO tempStock, Long qualifiedWarehouseId, |