| | |
| | | package cn.iocoder.yudao.module.mes.service.qc.ncr; |
| | | |
| | | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| | | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| | | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| | | import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi; |
| | | import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; |
| | | import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrPageReqVO; |
| | | import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ncr.MesQcNcrDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.qc.ncr.MesQcNcrMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesNcrDispositionEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesNcrStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum; |
| | | import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService; |
| | |
| | | import java.util.Collection; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | |
| | | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| | | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; |
| | |
| | | @Validated |
| | | public class MesQcNcrServiceImpl implements MesQcNcrService { |
| | | |
| | | /** |
| | | * NCR 协同审批流程分类/流程定义 key |
| | | */ |
| | | private static final String NCR_APPROVAL_PROCESS_KEY = "mes_ncr_approve"; |
| | | |
| | | /** |
| | | * 最终处置允许值:让步放行、返修、返工、报废 |
| | | * <p> |
| | | * 退货(1)、降级使用(5) 仍保留在 {@link MesNcrDispositionEnum} 与字典中,用于历史数据显示, |
| | | * 但不再作为质量部最终处置的可选项。 |
| | | */ |
| | | private static final Set<Integer> DISPOSITION_ALLOWED_TYPES = Set.of( |
| | | MesNcrDispositionEnum.SCRAP.getType(), |
| | | MesNcrDispositionEnum.REWORK.getType(), |
| | | MesNcrDispositionEnum.CONCESSION.getType(), |
| | | MesNcrDispositionEnum.REPAIR.getType()); |
| | | |
| | | @Resource |
| | | private MesQcNcrMapper ncrMapper; |
| | | |
| | | @Resource |
| | | private MdmItemApi mdmItemApi; |
| | | |
| | | @Resource |
| | | private BpmProcessInstanceApi processInstanceApi; |
| | | |
| | | @Resource |
| | | @Lazy |
| | |
| | | Integer defectLevel, String reviewOpinion) { |
| | | // 1. 校验存在 + 待评审状态 |
| | | validateNcrStatusPendingReview(id); |
| | | validateDisposition(disposition); |
| | | |
| | | // 2. 更新评审信息 |
| | | MesQcNcrDO updateObj = new MesQcNcrDO() |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void disposeNcr(Long id, Long handlerUserId, String handleResult) { |
| | | // 1. 校验存在 + 已评审状态 |
| | | public void disposeNcr(Long id, Integer disposition, Long handlerUserId, String handleResult) { |
| | | // 1. 校验存在 + 已评审(审批通过)状态 |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | if (!MesNcrStatusEnum.REVIEWED.getStatus().equals(ncr.getStatus())) { |
| | | throw exception(QC_NCR_NOT_REVIEWED); |
| | | throw exception(QC_NCR_DISPOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus())); |
| | | } |
| | | // 2. 处置方式必填,且必须是合法枚举值 |
| | | validateDisposition(disposition); |
| | | |
| | | // 2. 更新处置信息 |
| | | // 3. 更新处置信息(处置方式以本次处置选择为准并落库) |
| | | // TODO: WM 模块集成 — 处置为退货时触发供应商退货,处置为报废时触发杂项出库 |
| | | ncrMapper.updateById(new MesQcNcrDO() |
| | | .setId(id) |
| | | .setDisposition(disposition) |
| | | .setHandlerUserId(handlerUserId) |
| | | .setHandleDate(LocalDateTime.now()) |
| | | .setHandleResult(handleResult) |
| | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void submitNcrApproval(Long id) { |
| | | // 说明:BPM 的 createProcessInstance 内部通过 FlowableUtils.executeAuthenticatedUserId 执行, |
| | | // 该方法会把所有异常统一包成 RuntimeException,因此这里不能直接 catch ServiceException, |
| | | // 必须沿 cause 链判断是否为「流程定义不存在」。 |
| | | // 1. 校验存在 + 待评审(草稿/审批驳回后)状态 |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) { |
| | | throw exception(QC_NCR_SUBMIT_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus())); |
| | | } |
| | | |
| | | // 2. 全局免审批:业务校验通过后直接视为审批通过(已评审/待处置),无需流程模型 |
| | | if (processInstanceApi.isGlobalAutoApprove()) { |
| | | ncrMapper.updateById(new MesQcNcrDO() |
| | | .setId(id) |
| | | .setStatus(MesNcrStatusEnum.REVIEWED.getStatus()) |
| | | .setProcessInstanceId("")); |
| | | return; |
| | | } |
| | | |
| | | // 3. 创建 BPM 流程实例;取不到流程定义时按约定走自动通过兜底 |
| | | String processInstanceId; |
| | | try { |
| | | processInstanceId = processInstanceApi.createProcessInstance(getLoginUserId(), |
| | | new BpmProcessInstanceCreateReqDTO() |
| | | .setProcessDefinitionKey(NCR_APPROVAL_PROCESS_KEY) |
| | | .setBusinessKey(String.valueOf(id))); |
| | | } catch (RuntimeException ex) { |
| | | if (isProcessDefinitionNotExists(ex)) { |
| | | ncrMapper.updateById(new MesQcNcrDO() |
| | | .setId(id) |
| | | .setStatus(MesNcrStatusEnum.REVIEWED.getStatus()) |
| | | .setProcessInstanceId("")); |
| | | return; |
| | | } |
| | | throw ex; |
| | | } |
| | | |
| | | // 4. 更新 NCR 状态为审批中,并回写流程实例编号 |
| | | ncrMapper.updateById(new MesQcNcrDO() |
| | | .setId(id) |
| | | .setStatus(MesNcrStatusEnum.APPROVING.getStatus()) |
| | | .setProcessInstanceId(processInstanceId)); |
| | | } |
| | | |
| | | /** |
| | | * 判断异常链中是否包含「流程定义不存在」(BPM 对外会包一层 RuntimeException) |
| | | */ |
| | | private static boolean isProcessDefinitionNotExists(Throwable ex) { |
| | | Throwable current = ex; |
| | | while (current != null) { |
| | | if (current instanceof ServiceException serviceException |
| | | && cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS |
| | | .getCode().equals(serviceException.getCode())) { |
| | | return true; |
| | | } |
| | | current = current.getCause(); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateNcrAuditStatus(Long id, Integer bpmResult) { |
| | | // 1. 校验存在 + 审批中(避免并发或重复回调覆盖已处置/已关闭状态) |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | if (!MesNcrStatusEnum.APPROVING.getStatus().equals(ncr.getStatus())) { |
| | | return; |
| | | } |
| | | |
| | | // 2. 审批通过 -> 已评审(待处置);驳回/取消 -> 待评审(可重新修改并提交) |
| | | if (BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) { |
| | | ncrMapper.updateById(new MesQcNcrDO() |
| | | .setId(id) |
| | | .setStatus(MesNcrStatusEnum.REVIEWED.getStatus())); |
| | | return; |
| | | } |
| | | if (BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult) |
| | | || BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) { |
| | | // 保留 processInstanceId:驳回/取消后仍可在「查看审批」中查看审批意见, |
| | | // 重新提交时会用新流程实例编号覆盖。 |
| | | ncrMapper.updateById(new MesQcNcrDO() |
| | | .setId(id) |
| | | .setStatus(MesNcrStatusEnum.PENDING_REVIEW.getStatus())); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void closeNcr(Long id, String closeOpinion) { |
| | | // 1. 校验存在 + 已处置状态 |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | if (!MesNcrStatusEnum.DISPOSED.getStatus().equals(ncr.getStatus())) { |
| | | throw exception(QC_NCR_NOT_DISPOSED); |
| | | throw exception(QC_NCR_CLOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus())); |
| | | } |
| | | |
| | | // 2. 更新关闭信息 |
| | |
| | | public MesQcNcrDO validateNcrExists(Long id) { |
| | | MesQcNcrDO ncr = ncrMapper.selectById(id); |
| | | if (ncr == null) { |
| | | throw exception(QC_NCR_NOT_EXISTS); |
| | | throw exception(QC_NCR_NOT_EXISTS, id); |
| | | } |
| | | return ncr; |
| | | } |
| | |
| | | private MesQcNcrDO validateNcrStatusPendingReview(Long id) { |
| | | MesQcNcrDO ncr = validateNcrExists(id); |
| | | if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) { |
| | | throw exception(QC_NCR_NOT_PENDING_REVIEW); |
| | | throw exception(QC_NCR_STATUS_NOT_PENDING, ncr.getCode(), resolveStatusName(ncr.getStatus())); |
| | | } |
| | | return ncr; |
| | | } |
| | | |
| | | /** |
| | | * 校验处置方式:必填 + 必须是合法枚举值 |
| | | */ |
| | | private void validateDisposition(Integer disposition) { |
| | | if (disposition == null) { |
| | | throw exception(QC_NCR_DISPOSITION_REQUIRED); |
| | | } |
| | | if (!DISPOSITION_ALLOWED_TYPES.contains(disposition)) { |
| | | throw exception(QC_NCR_DISPOSITION_INVALID, disposition); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 状态值转名称,解析失败回退为 ID 字符串 |
| | | */ |
| | | private String resolveStatusName(Integer status) { |
| | | for (MesNcrStatusEnum value : MesNcrStatusEnum.values()) { |
| | | if (value.getStatus().equals(status)) { |
| | | return value.getName(); |
| | | } |
| | | } |
| | | return "ID=" + status; |
| | | } |
| | | |
| | | /** |
| | |
| | | private void validateNcrCodeUnique(Long id, String code) { |
| | | MesQcNcrDO existing = ncrMapper.selectByCode(code); |
| | | if (existing != null && !existing.getId().equals(id)) { |
| | | throw exception(QC_NCR_CODE_DUPLICATE); |
| | | throw exception(QC_NCR_CODE_DUPLICATE, code); |
| | | } |
| | | } |
| | | |