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 cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
|
import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
|
import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcService;
|
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
|
import jakarta.annotation.Resource;
|
import org.springframework.context.annotation.Lazy;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.time.LocalDateTime;
|
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;
|
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
|
|
/**
|
* MES 不合格品处理单(NCR) Service 实现类
|
*
|
* @author 超级管理员
|
*/
|
@Service
|
@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
|
private MesQcIqcService iqcService;
|
@Resource
|
@Lazy
|
private MesQcIpqcService ipqcService;
|
@Resource
|
@Lazy
|
private MesQcOqcService oqcService;
|
@Resource
|
@Lazy
|
private MesQcRqcService rqcService;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createNcr(MesQcNcrSaveReqVO createReqVO) {
|
// 1. 校验数据
|
validateNcrCodeUnique(null, createReqVO.getCode());
|
validateItemExists(createReqVO.getItemId());
|
|
// 2. 插入主表
|
MesQcNcrDO ncr = BeanUtils.toBean(createReqVO, MesQcNcrDO.class)
|
.setStatus(MesNcrStatusEnum.PENDING_REVIEW.getStatus());
|
// 如果传入了 qcId 和 qcType,则查询来源检验单编号并冗余存储
|
if (createReqVO.getQcType() != null && createReqVO.getQcId() != null) {
|
ncr.setQcCode(lookupQcCode(createReqVO.getQcType(), createReqVO.getQcId()));
|
}
|
ncrMapper.insert(ncr);
|
return ncr.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateNcr(MesQcNcrSaveReqVO updateReqVO) {
|
// 1. 校验存在 + 只允许待评审状态编辑
|
MesQcNcrDO ncr = validateNcrStatusPendingReview(updateReqVO.getId());
|
validateNcrCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
|
validateItemExists(updateReqVO.getItemId());
|
|
// 2. 更新主表(仅允许更新部分字段)
|
MesQcNcrDO updateObj = BeanUtils.toBean(updateReqVO, MesQcNcrDO.class);
|
updateObj.setQcType(null); // 不允许修改检验类型
|
updateObj.setQcId(null); // 不允许修改检验单
|
updateObj.setStatus(null); // 不允许修改状态
|
ncrMapper.updateById(updateObj);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteNcr(Long id) {
|
// 校验存在 + 只允许删除待评审状态
|
validateNcrStatusPendingReview(id);
|
ncrMapper.deleteById(id);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void reviewNcr(Long id, Integer disposition, Long responsibleDeptId,
|
Integer defectLevel, String reviewOpinion) {
|
// 1. 校验存在 + 待评审状态
|
validateNcrStatusPendingReview(id);
|
validateDisposition(disposition);
|
|
// 2. 更新评审信息
|
MesQcNcrDO updateObj = new MesQcNcrDO()
|
.setId(id)
|
.setDisposition(disposition)
|
.setResponsibleDeptId(responsibleDeptId)
|
.setReviewOpinion(reviewOpinion)
|
.setReviewerUserId(getLoginUserId())
|
.setReviewDate(LocalDateTime.now())
|
.setStatus(MesNcrStatusEnum.REVIEWED.getStatus());
|
if (defectLevel != null) {
|
updateObj.setDefectLevel(defectLevel);
|
}
|
ncrMapper.updateById(updateObj);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
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_DISPOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
|
}
|
// 2. 处置方式必填,且必须是合法枚举值
|
validateDisposition(disposition);
|
|
// 3. 更新处置信息(处置方式以本次处置选择为准并落库)
|
// TODO: WM 模块集成 — 处置为退货时触发供应商退货,处置为报废时触发杂项出库
|
ncrMapper.updateById(new MesQcNcrDO()
|
.setId(id)
|
.setDisposition(disposition)
|
.setHandlerUserId(handlerUserId)
|
.setHandleDate(LocalDateTime.now())
|
.setHandleResult(handleResult)
|
.setStatus(MesNcrStatusEnum.DISPOSED.getStatus()));
|
}
|
|
@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_CLOSE_STATUS_INVALID, ncr.getCode(), resolveStatusName(ncr.getStatus()));
|
}
|
|
// 2. 更新关闭信息
|
ncrMapper.updateById(new MesQcNcrDO()
|
.setId(id)
|
.setCloseUserId(getLoginUserId())
|
.setCloseDate(LocalDateTime.now())
|
.setCloseOpinion(closeOpinion)
|
.setStatus(MesNcrStatusEnum.CLOSED.getStatus()));
|
}
|
|
// ==================== 校验方法 ====================
|
|
@Override
|
public MesQcNcrDO validateNcrExists(Long id) {
|
MesQcNcrDO ncr = ncrMapper.selectById(id);
|
if (ncr == null) {
|
throw exception(QC_NCR_NOT_EXISTS, id);
|
}
|
return ncr;
|
}
|
|
/**
|
* 校验 NCR 存在且为待评审状态
|
*/
|
private MesQcNcrDO validateNcrStatusPendingReview(Long id) {
|
MesQcNcrDO ncr = validateNcrExists(id);
|
if (!MesNcrStatusEnum.PENDING_REVIEW.getStatus().equals(ncr.getStatus())) {
|
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;
|
}
|
|
/**
|
* 校验 NCR 编号唯一
|
*/
|
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, code);
|
}
|
}
|
|
/**
|
* 校验物料存在
|
*/
|
private void validateItemExists(Long itemId) {
|
if (itemId != null) {
|
mdmItemApi.getItem(itemId).getCheckedData();
|
}
|
}
|
|
/**
|
* 根据检验类型和检验单 ID 查询来源检验单编号
|
*/
|
private String lookupQcCode(Integer qcType, Long qcId) {
|
if (Objects.equals(qcType, MesQcTypeEnum.IQC.getType())) {
|
return iqcService.getIqc(qcId).getCode();
|
} else if (Objects.equals(qcType, MesQcTypeEnum.IPQC.getType())) {
|
return ipqcService.getIpqc(qcId).getCode();
|
} else if (Objects.equals(qcType, MesQcTypeEnum.OQC.getType())) {
|
return oqcService.getOqc(qcId).getCode();
|
} else if (Objects.equals(qcType, MesQcTypeEnum.RQC.getType())) {
|
return rqcService.getRqc(qcId).getCode();
|
}
|
return null;
|
}
|
|
// ==================== 查询方法 ====================
|
|
@Override
|
public MesQcNcrDO getNcr(Long id) {
|
return ncrMapper.selectById(id);
|
}
|
|
@Override
|
public PageResult<MesQcNcrDO> getNcrPage(MesQcNcrPageReqVO pageReqVO) {
|
return ncrMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
public Map<Long, MesQcNcrDO> getNcrMap(Collection<Long> ids) {
|
return convertMap(ncrMapper.selectBatchIds(ids), MesQcNcrDO::getId);
|
}
|
|
}
|