云
2026-10-27 ffca46b39d25439a06a27cafda2ec523a3fb67cf
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -5,13 +5,14 @@
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsMergeItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMergeItemMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
@@ -22,6 +23,7 @@
import cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderSourceTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
@@ -53,7 +55,7 @@
/**
 * MES 生产主计划(MPS)Service 实现类
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@@ -68,7 +70,9 @@
    @Resource
    private MesProMpsMapper mpsMapper;
    @Resource
    private MesProMpsMergeItemMapper mergeItemMapper;
    private ErpSaleOrderApi saleOrderApi;
    @Resource
    private PermissionApi permissionApi;
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
@@ -92,7 +96,7 @@
    @Transactional(rollbackFor = Exception.class)
    public Long createMps(Long saleOrderId, String saleOrderNo, Long saleOrderItemId,
                          Long mdmItemId, String productName, BigDecimal quantity, Long clientId,
                          String requestDate, String remark) {
                          String requestDate, String remark, String reelNo) {
        // 1. 根据 MDM 物料 ID 查找 MES 产品
        cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO mesItem = itemService.getItemByMdmItemId(mdmItemId);
        if (mesItem == null) {
@@ -120,6 +124,7 @@
        mps.setSaleOrderId(saleOrderId);
        mps.setSaleOrderNo(saleOrderNo);
        mps.setSaleOrderItemId(saleOrderItemId);
        mps.setReelNo(reelNo);
        mps.setProductId(mesItem.getId()); // 使用 MES 产品 ID
        mps.setQuantity(quantity);
        mps.setQuantityProduced(BigDecimal.ZERO);
@@ -193,7 +198,14 @@
    @Transactional(rollbackFor = Exception.class)
    public Long issueMps(Long id) {
        // 校验存在 + 草稿状态
        MesProMpsDO mps = validateMpsExistsAndDraft(id);
        MesProMpsDO mps = mpsMapper.selectByIdForUpdate(id);
        if (mps == null) {
            throw exception(PRO_MPS_NOT_EXISTS);
        }
        if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) {
            throw exception(PRO_MPS_NOT_DRAFT);
        }
        validateIssuePermission(mps);
        // 创建生产工单
        Long workOrderId = createWorkOrderFromMps(mps, mps.getQuantity());
@@ -209,66 +221,30 @@
        return workOrderId;
    }
    private void validateIssuePermission(MesProMpsDO mps) {
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        if (userId == null) {
            throw exception(PRO_MPS_ISSUE_FORBIDDEN);
        }
        if (permissionApi.hasAnyRoles(userId, "super_admin")) {
            return;
        }
        // 手工创建且没有销售订单的计划以计划创建人为准。
        String creator = mps.getCreator();
        if (mps.getSaleOrderId() != null) {
            var order = saleOrderApi.getSaleOrder(mps.getSaleOrderId()).getCheckedData();
            creator = order != null ? order.getCreator() : null;
        }
        if (!String.valueOf(userId).equals(creator)) {
            throw exception(PRO_MPS_ISSUE_FORBIDDEN);
        }
    }
    /** 旧合并下发入口已停用;历史合并记录仍由取消和查询逻辑处理。 */
    @Deprecated
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long mergeAndIssueMps(List<Long> mpsIds) {
        // 校验 MPS 列表
        if (CollUtil.isEmpty(mpsIds) || mpsIds.size() < 2) {
            throw exception(PRO_MPS_MERGE_AT_LEAST_TWO);
        }
        List<MesProMpsDO> mpsList = mpsMapper.selectBatchIds(mpsIds);
        if (mpsList.size() != mpsIds.size()) {
            throw exception(PRO_MPS_NOT_EXISTS);
        }
        // 校验:必须是相同产品、草稿状态
        Long productId = null;
        for (MesProMpsDO mps : mpsList) {
            if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) {
                throw exception(PRO_MPS_NOT_DRAFT);
            }
            if (productId == null) {
                productId = mps.getProductId();
            } else if (!productId.equals(mps.getProductId())) {
                throw exception(PRO_MPS_MERGE_PRODUCT_MISMATCH);
            }
        }
        // 计算合并数量
        BigDecimal totalQuantity = mpsList.stream()
                .map(MesProMpsDO::getQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // 取第一条 MPS 作为主记录,创建生产工单
        MesProMpsDO firstMps = mpsList.get(0);
        Long workOrderId = createWorkOrderFromMps(firstMps, totalQuantity);
        // 生成合并批次号
        String mergeBatchNo = generateMergeBatchNo();
        // 更新所有 MPS 状态,记录合并关系
        for (MesProMpsDO mps : mpsList) {
            mpsMapper.updateById(new MesProMpsDO()
                    .setId(mps.getId())
                    .setStatus(MesProMpsStatusEnum.ISSUED.getStatus())
                    .setWorkOrderId(workOrderId)
                    .setMergeFlag(1)
                    .setMergeBatchNo(mergeBatchNo)
                    .setIssueDate(LocalDateTime.now()));
            // 记录合并明细
            MesProMpsMergeItemDO mergeItem = new MesProMpsMergeItemDO();
            mergeItem.setMergeBatchNo(mergeBatchNo);
            mergeItem.setMpsId(mps.getId());
            mergeItem.setWorkOrderId(workOrderId);
            mergeItem.setQuantity(mps.getQuantity());
            mergeItemMapper.insert(mergeItem);
        }
        log.info("[mergeAndIssueMps] MPS 合并下发成功,mpsIds={}, workOrderId={}, totalQuantity={}",
                mpsIds, workOrderId, totalQuantity);
        return workOrderId;
        throw exception(PRO_MPS_MERGE_ISSUE_DISABLED);
    }
    @Override
@@ -282,14 +258,32 @@
            throw exception(PRO_MPS_NOT_ISSUED);
        }
        // 级联取消关联的生产工单
        // 级联取消关联的生产工单(跟随工单当前状态,且不误伤合并下发的共享工单)
        if (mps.getWorkOrderId() != null) {
            try {
                workOrderService.cancelWorkOrder(mps.getWorkOrderId());
                log.info("[cancelMps] 级联取消生产工单,workOrderId={}", mps.getWorkOrderId());
            } catch (Exception e) {
                // 工单可能已完成或已取消,记录日志但不阻止 MPS 取消
                log.warn("[cancelMps] 取消生产工单失败,workOrderId={},原因:{}", mps.getWorkOrderId(), e.getMessage());
            // 新增逻辑:若该工单有报工记录,则不允许取消 MPS
            Long feedbackCount = feedbackMapper.selectCount(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId());
            if (feedbackCount != null && feedbackCount > 0) {
                throw exception(PRO_MPS_WORK_ORDER_HAS_FEEDBACK);
            }
            // 1. 统计该工单被多少条 MPS 引用:合并下发的多条 MPS 共享同一个工单
            Long refCount = mpsMapper.selectCount(MesProMpsDO::getWorkOrderId, mps.getWorkOrderId());
            if (refCount != null && refCount > 1) {
                // 共享工单:只取消当前 MPS,不动工单
                log.info("[cancelMps] 生产工单({}) 被 {} 条 MPS 共用,跳过级联取消工单",
                        mps.getWorkOrderId(), refCount);
            } else {
                MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(mps.getWorkOrderId());
                if (workOrder != null
                        && MesProWorkOrderStatusEnum.CONFIRMED.getStatus().equals(workOrder.getStatus())) {
                    // 只有"已确认"状态的工单才需要级联取消
                    workOrderService.cancelWorkOrder(mps.getWorkOrderId());
                    log.info("[cancelMps] 级联取消生产工单,workOrderId={}", mps.getWorkOrderId());
                } else {
                    // 工单已是草稿/已完成/已取消,无需级联取消,直接取消 MPS
                    log.info("[cancelMps] 生产工单({}) 状态为 {},无需级联取消",
                            mps.getWorkOrderId(), workOrder != null ? workOrder.getStatus() : "null");
                }
            }
        }
@@ -329,9 +323,7 @@
    @Override
    public PageResult<MesProMpsDO> getMpsPage(MesProMpsPageReqVO pageReqVO) {
        // 获取用户有权限的客户 ID 列表
        List<Long> permittedClientIds = customerApi.getPermittedCustomerIds();
        return mpsMapper.selectPage(pageReqVO, permittedClientIds);
        return mpsMapper.selectPage(pageReqVO, (List<Long>) null);
    }
    @Override
@@ -424,6 +416,10 @@
     * 判断单个 MPS 是否完成
     */
    private boolean isMpsFinished(MesProMpsDO mps) {
        // 已取消的 MPS 视为终态,不再阻塞生产完成判断(与 checkCanCreateSalesNotice 的口径保持一致)
        if (MesProMpsStatusEnum.CANCELED.getStatus().equals(mps.getStatus())) {
            return true;
        }
        // 1. 已生产数量 >= 计划数量
        BigDecimal quantityProduced = ObjUtil.defaultIfNull(mps.getQuantityProduced(), BigDecimal.ZERO);
        if (quantityProduced.compareTo(mps.getQuantity()) < 0) {
@@ -431,12 +427,13 @@
                    mps.getId(), quantityProduced, mps.getQuantity());
            return false;
        }
        // 2. 检查工单下所有报工是否完成(确保质检完成)
        // 2. 已审批待检的报工已计入生产进度,不以质检状态阻塞生产完成。
        if (mps.getWorkOrderId() != null) {
            Long unfinishedCount = feedbackMapper.selectCount(
                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProFeedbackDO>()
                            .eq(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId())
                            .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus()));
                            .notIn(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus(),
                                    MesProFeedbackStatusEnum.UNCHECK.getStatus()));
            log.debug("[isMpsFinished] MPS {} unfinished feedback count: {}", mps.getId(), unfinishedCount);
            if (unfinishedCount > 0) {
                return false;
@@ -460,9 +457,8 @@
        }
    }
    /**
     * 生成合并批次号
     */
    /** 已停用:旧合并下发批次号生成逻辑,仅供识别历史实现,不再调用。 */
    @Deprecated
    private String generateMergeBatchNo() {
        return "MERGE" + System.currentTimeMillis();
    }
@@ -519,8 +515,9 @@
        }
        Long routeId = routeProduct.getRouteId();
        // 2. 校验工艺路线是否有工序
        List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByRouteId(routeId);
        // 2. 校验工艺路线是否有工序(按产品过滤其它产品的产出物料)
        List<MesProRouteProcessDO> routeProcesses = routeProcessService
                .getRouteProcessListByProductId(mps.getProductId());
        if (CollUtil.isEmpty(routeProcesses)) {
            throw exception(PRO_ROUTE_ENABLE_NO_PROCESS);
        }
@@ -533,6 +530,7 @@
        workOrderReq.setOrderSourceType(MesProWorkOrderSourceTypeEnum.ORDER.getType());
        workOrderReq.setOrderSourceCode(mps.getSaleOrderNo());
        workOrderReq.setProductId(mps.getProductId());
        workOrderReq.setReelNo(mps.getReelNo());
        workOrderReq.setQuantity(quantity);
        workOrderReq.setClientId(mps.getClientId());
        workOrderReq.setRequestDate(mps.getRequestDate() != null ? mps.getRequestDate() : LocalDateTime.now());
@@ -560,6 +558,10 @@
                        .map(Long::valueOf).collect(Collectors.toList()));
            }
            process.setColorCode(rp.getColorCode());
            // 带入工艺路线产品配置的报工仓库
            process.setWarehouseId(routeProduct.getWarehouseId());
            process.setLocationId(routeProduct.getLocationId());
            process.setAreaId(routeProduct.getAreaId());
            // 带入工序投料BOM
            List<MesProRouteProductBomDO> routeBomItems = bomMap != null ? bomMap.get(rp.getProcessId()) : null;
            if (CollUtil.isNotEmpty(routeBomItems)) {
@@ -595,4 +597,4 @@
        }
    }
}
}