yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
@@ -4,6 +4,7 @@
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
@@ -16,6 +17,7 @@
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.service.qc.imports.MesQcImportHelper;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
@@ -23,6 +25,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
@@ -40,6 +43,7 @@
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateService;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO;
import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
@@ -76,16 +80,19 @@
    /**
     * IPQC 保存数据校验上下文
     *
     * @param workstation 工位
     * @param workstation 工位(可选,未指定 workstationId 时为空)
     * @param task        生产任务(可选,指定 taskId 时非空)
     * @param workOrder   生产工单(可选,未指定 workOrderId 时为空)
     */
    private record SaveDataContext(MesMdWorkstationDO workstation, MesProTaskDO task) {}
    private record SaveDataContext(MesMdWorkstationDO workstation, MesProTaskDO task, MesProWorkOrderDO workOrder) {}
    @Resource
    private MesQcIpqcMapper ipqcMapper;
    @Resource
    private MesQcTemplateItemService templateItemService;
    @Resource
    private MesQcTemplateService templateService;
    @Resource
    private MesQcIpqcLineService ipqcLineService;
    @Resource
@@ -132,36 +139,21 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createIpqc(MesQcIpqcSaveReqVO createReqVO) {
        // 1.1 校验数据
        // 1.1 校验数据(生产订单、工位均非必填)
        SaveDataContext ctx = validateIpqcSaveData(createReqVO);
        // 1.2 校验工单存在
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderExists(createReqVO.getWorkOrderId());
        // 1.3 确定检验物料:有任务时取任务的物料,无任务时取工单的产品
        Long itemId = (ctx.task() != null) ? ctx.task().getItemId() : workOrder.getProductId();
        // 1.4 根据物料 + IPQC 类型查找可用质检方案(支持前端显式指定)
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                itemId, MesQcTypeEnum.IPQC.getType(), null, null);
        if (CollUtil.isEmpty(templateItems)) {
            throw exception(QC_NO_TEMPLATE);
        }
        MesQcTemplateItemDO templateItem = templateItems.get(0);
        if (createReqVO.getTemplateId() != null) {
            templateItem = templateItems.stream()
                    .filter(available -> Objects.equals(available.getTemplateId(), createReqVO.getTemplateId()))
                    .findFirst().orElse(null);
            if (templateItem == null) {
                throw exception(QC_NO_TEMPLATE); // 指定了方案但不在匹配列表中,拒绝
            }
        }
        Long templateId = templateItem.getTemplateId();
        // 1.5 获取来源单据编号
        // 1.2 确定检验物料:有任务时取任务的物料,有工单时取工单的产品,都没有时取请求指定的产品物料
        Long itemId = resolveItemId(createReqVO, ctx);
        // 1.3 确定质检方案:显式指定时校验方案本身可用,未指定时按产品物料已绑定方案兜底
        Long templateId = resolveTemplateId(itemId, createReqVO.getTemplateId());
        // 1.4 获取来源单据编号
        String sourceDocCode = validateAndGetSourceDocCode(
                createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId());
        // 2. 插入主表
        MesQcIpqcDO ipqc = BeanUtils.toBean(createReqVO, MesQcIpqcDO.class)
                .setItemId(itemId)
                .setProcessId(getProcessId(itemId, ctx.workstation().getProcessId()))
                .setProcessId(getProcessId(itemId,
                        ctx.workstation() != null ? ctx.workstation().getProcessId() : null))
                .setTemplateId(templateId)
                .setSourceDocCode(sourceDocCode)
                .setStatus(MesQcStatusEnum.DRAFT.getStatus());
@@ -186,19 +178,23 @@
        updateObj.setTemplateId(null); // 不允许修改模板
        updateObj.setItemId(null); // 不允许修改物料
        updateObj.setWorkOrderId(null); // 不允许修改工单
        updateObj.setProcessId(getProcessId(existIpqc.getItemId(), ctx.workstation().getProcessId()));
        updateObj.setProcessId(getProcessId(existIpqc.getItemId(),
                ctx.workstation() != null ? ctx.workstation().getProcessId() : null));
        ipqcMapper.updateById(updateObj);
    }
    private SaveDataContext validateIpqcSaveData(MesQcIpqcSaveReqVO reqVO) {
        // 校验编号唯一
        validateIpqcCodeUnique(reqVO.getId(), reqVO.getCode());
        // 校验工位、检测人员存在
        MesMdWorkstationDO workstation = workstationService.validateWorkstationExists(reqVO.getWorkstationId());
        // 校验工位存在(工位非必填,未选择时跳过)
        MesMdWorkstationDO workstation = reqVO.getWorkstationId() != null
                ? workstationService.validateWorkstationExists(reqVO.getWorkstationId()) : null;
        adminUserApi.validateUser(reqVO.getInspectorUserId());
        // 校验工单存在
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        if (reqVO.getProcessId() != null && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) {
        // 校验工单存在且已确认(生产订单非必填,未选择时跳过)
        MesProWorkOrderDO workOrder = reqVO.getWorkOrderId() != null
                ? workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId()) : null;
        if (workstation != null && reqVO.getProcessId() != null
                && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) {
            throw exception(PRO_WORKSTATION_PROCESS_MISMATCH);
        }
        // 校验任务关系(如果指定了任务)
@@ -206,24 +202,103 @@
        if (reqVO.getTaskId() != null) {
            task = taskService.validateTaskNotFinished(reqVO.getTaskId());
            validateTaskRelation(task, workstation, workOrder, reqVO);
        } else {
        } else if (workOrder != null && reqVO.getItemId() != null
                && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) {
            // 无任务时,itemId 仍需与工单产品一致
            if (reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) {
                throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH);
            }
        return new SaveDataContext(workstation, task, workOrder);
        }
        return new SaveDataContext(workstation, task);
    /**
     * 确定检验物料:任务优先,其次工单产品,都没有时取请求指定的产品物料(都未指定则为空)
     */
    private Long resolveItemId(MesQcIpqcSaveReqVO reqVO, SaveDataContext ctx) {
        if (ctx.task() != null) {
            return ctx.task().getItemId();
        }
        if (ctx.workOrder() != null) {
            return ctx.workOrder().getProductId();
        }
        return reqVO.getItemId();
    }
    /**
     * 确定质检方案
     *
     * <p>产品物料已绑定 IPQC 方案时沿用原逻辑:未显式指定取绑定列表首个,显式指定则必须命中绑定列表;
     * <p>产品物料未匹配到任何方案(未选生产订单/生产任务/产品物料)时,显式指定的方案改为校验方案本身:
     * 存在、已启用、检测种类包含过程检验,不再要求与产品物料绑定。
     */
    private Long resolveTemplateId(Long itemId, Long explicitTemplateId) {
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                itemId, MesQcTypeEnum.IPQC.getType(), null, null);
        if (CollUtil.isNotEmpty(templateItems)) {
            if (explicitTemplateId == null) {
                return templateItems.get(0).getTemplateId();
            }
            return templateItems.stream()
                    .filter(available -> Objects.equals(available.getTemplateId(), explicitTemplateId))
                    .findFirst()
                    .orElseThrow(() -> exception(QC_NO_TEMPLATE)) // 指定了方案但不在匹配列表中,拒绝
                    .getTemplateId();
        }
        // 产品物料未匹配到任何 IPQC 方案
        if (explicitTemplateId != null) {
            return validateTemplateUsable(explicitTemplateId);
        }
        throw new ServiceException(QC_IPQC_NO_TEMPLATE.getCode(), buildNoTemplateMessage(itemId));
    }
    /**
     * 校验质检方案本身可用:存在、已启用、检测种类包含过程检验,返回方案编号
     */
    private Long validateTemplateUsable(Long templateId) {
        MesQcTemplateDO template = templateService.getTemplate(templateId);
        if (template == null) {
            throw new ServiceException(QC_TEMPLATE_NOT_EXISTS.getCode(), StrUtil.format(
                    "所选质检方案不存在(ID={}),可能已被删除,请刷新后重新选择质检方案", templateId));
        }
        if (!CommonStatusEnum.isEnable(template.getStatus())) {
            throw exception(QC_IPQC_TEMPLATE_IS_DISABLE, template.getName());
        }
        if (!CollUtil.contains(template.getTypes(), MesQcTypeEnum.IPQC.getType())) {
            throw exception(QC_IPQC_TEMPLATE_TYPE_NOT_MATCH, template.getName());
        }
        return template.getId();
    }
    /**
     * 构建「无可用质检方案」的提示:区分「有产品物料但未绑定方案」与「未选工单/任务/物料」
     */
    private String buildNoTemplateMessage(Long itemId) {
        if (itemId == null) {
            return "未选择生产订单/生产任务/产品物料,无法自动匹配质检方案:"
                    + "请直接选择「质检方案」后重试,或先选择生产订单/生产任务以自动带出该产品已绑定的方案。";
        }
        return StrUtil.format("产品物料[{}]未绑定「过程检验」质检方案:请在【质量管理-质检方案】中为该物料"
                + "配置检测种类包含过程检验的方案,或在本单手动选择质检方案。", describeItem(itemId));
    }
    /**
     * 描述产品物料,用于异常提示:优先给出「编码 名称」,查不到时回退为物料编号
     */
    private String describeItem(Long itemId) {
        MesMdItemDO item = itemService.getItem(itemId);
        return item != null ? item.getCode() + " " + item.getName() : "物料编号 ID=" + itemId;
    }
    private void validateTaskRelation(MesProTaskDO task, MesMdWorkstationDO workstation,
                                      MesProWorkOrderDO workOrder, MesQcIpqcSaveReqVO reqVO) {
        if (ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) {
        // 生产订单、工位均非必填,未选择时跳过对应比对
        if (workOrder != null && ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) {
            throw exception(PRO_TASK_WORK_ORDER_MISMATCH);
        }
        if (ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) {
        if (workstation != null && ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) {
            throw exception(PRO_TASK_WORKSTATION_MISMATCH);
        }
        Long expectedProcessId = reqVO.getProcessId() != null ? reqVO.getProcessId() : workstation.getProcessId();
        Long expectedProcessId = reqVO.getProcessId() != null ? reqVO.getProcessId()
                : (workstation != null ? workstation.getProcessId() : null);
        if (expectedProcessId != null && ObjUtil.notEqual(task.getProcessId(), expectedProcessId)) {
            throw exception(PRO_TASK_ROUTE_PROCESS_MISMATCH);
        }
@@ -528,7 +603,7 @@
                    + "。请修改模板中的「检验单编号」,或先在系统中删除该编号的检验单后重新导入");
        }
        reqVO.setCode(code);
        // 2. 名称、检验类型、工单、工位、质检方案
        // 2. 名称、检验类型、工单、工位、质检方案(生产工单号、工位名称留空即不关联,不再拦截)
        if (StrUtil.isBlank(row.getName())) {
            throw new IllegalArgumentException("检验单名称不能为空:请填写模板中该行的「检验单名称」列");
        }