gaoluyang
2 天以前 8c65f898ed6e5b0c7a94c792c4562f6ec831dccd
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ncr/MesQcNcrServiceImpl.java
@@ -1,11 +1,16 @@
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;
@@ -23,6 +28,7 @@
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;
@@ -38,11 +44,31 @@
@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
@@ -105,6 +131,7 @@
                           Integer defectLevel, String reviewOpinion) {
        // 1. 校验存在 + 待评审状态
        validateNcrStatusPendingReview(id);
        validateDisposition(disposition);
        // 2. 更新评审信息
        MesQcNcrDO updateObj = new MesQcNcrDO()
@@ -123,17 +150,20 @@
    @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)
@@ -142,11 +172,99 @@
    @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. 更新关闭信息
@@ -164,7 +282,7 @@
    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;
    }
@@ -175,9 +293,33 @@
    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;
    }
    /**
@@ -186,7 +328,7 @@
    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);
        }
    }