package cn.iocoder.yudao.module.mes.service.pro.mps; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; 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.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.dataobject.pro.feedback.MesProFeedbackDO; 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.service.md.autocode.MesMdAutoCodeRecordService; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.Collections; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** * MES 生产主计划(MPS)Service 实现类 * * @author 芋道源码 */ @Service @Validated @Slf4j public class MesProMpsServiceImpl implements MesProMpsService { /** * MPS 编码规则 */ private static final String MPS_CODE_RULE = "MPS"; @Resource private MesProMpsMapper mpsMapper; @Resource private MesProMpsMergeItemMapper mergeItemMapper; @Resource private MesProWorkOrderService workOrderService; @Resource private MesMdItemService itemService; @Resource private MesMdAutoCodeRecordService autoCodeRecordService; @Resource private CrmCustomerApi customerApi; @Resource private MesProFeedbackMapper feedbackMapper; @Override @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) { // 1. 根据 MDM 物料 ID 查找 MES 产品 cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO mesItem = itemService.getItemByMdmItemId(mdmItemId); if (mesItem == null) { // MES 产品不存在,抛出异常并包含产品名称提示 throw exception(PRO_MPS_MES_PRODUCT_NOT_EXISTS, productName != null ? productName : mdmItemId); } // 2. 生成 MPS 编码 String code = generateMpsCode(); // 3. 解析需求日期 LocalDateTime requestDateTime = null; if (StrUtil.isNotBlank(requestDate)) { try { requestDateTime = LocalDateTime.parse(requestDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } catch (Exception e) { requestDateTime = LocalDateTime.parse(requestDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")); } } // 4. 创建 MPS(使用 MES 产品 ID) MesProMpsDO mps = new MesProMpsDO(); mps.setCode(code); mps.setStatus(MesProMpsStatusEnum.DRAFT.getStatus()); mps.setSaleOrderId(saleOrderId); mps.setSaleOrderNo(saleOrderNo); mps.setSaleOrderItemId(saleOrderItemId); mps.setProductId(mesItem.getId()); // 使用 MES 产品 ID mps.setQuantity(quantity); mps.setQuantityProduced(BigDecimal.ZERO); mps.setUnitMeasureId(mesItem.getUnitMeasureId()); // 从 MES 产品获取计量单位 mps.setClientId(clientId); mps.setRequestDate(requestDateTime); mps.setMergeFlag(0); mps.setRemark(remark); mpsMapper.insert(mps); log.info("[createMps] 创建 MPS 成功,id={}, code={}, saleOrderItemId={}, mdmItemId={}, productId={}", mps.getId(), mps.getCode(), mps.getSaleOrderItemId(), mdmItemId, mesItem.getId()); return mps.getId(); } @Override @Transactional(rollbackFor = Exception.class) public Long createMps(MesProMpsSaveReqVO createReqVO) { // 校验产品存在 itemService.validateItemExists(createReqVO.getProductId()); // 如果未填写编码,自动生成 if (StrUtil.isBlank(createReqVO.getCode())) { createReqVO.setCode(generateMpsCode()); } else { // 校验编码唯一 validateMpsCodeUnique(null, createReqVO.getCode()); } // 插入 MesProMpsDO mps = BeanUtils.toBean(createReqVO, MesProMpsDO.class); mps.setStatus(MesProMpsStatusEnum.DRAFT.getStatus()); mps.setQuantityProduced(BigDecimal.ZERO); mps.setMergeFlag(0); mpsMapper.insert(mps); log.info("[createMps] 手动创建 MPS 成功,id={}, code={}", mps.getId(), mps.getCode()); return mps.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void updateMps(MesProMpsSaveReqVO updateReqVO) { // 校验存在 + 草稿状态 validateMpsExistsAndDraft(updateReqVO.getId()); // 校验编码唯一 validateMpsCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); // 更新 MesProMpsDO updateObj = BeanUtils.toBean(updateReqVO, MesProMpsDO.class); mpsMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) public void deleteMps(Long id) { // 校验存在 + 草稿状态 MesProMpsDO mps = validateMpsExistsAndDraft(id); // 校验是否已关联工单 if (mps.getWorkOrderId() != null) { throw exception(PRO_MPS_HAS_WORK_ORDER); } // 删除 mpsMapper.deleteById(id); } @Override @Transactional(rollbackFor = Exception.class) public Long issueMps(Long id) { // 校验存在 + 草稿状态 MesProMpsDO mps = validateMpsExistsAndDraft(id); // 创建生产工单 Long workOrderId = createWorkOrderFromMps(mps, mps.getQuantity()); // 更新 MPS 状态 mpsMapper.updateById(new MesProMpsDO() .setId(id) .setStatus(MesProMpsStatusEnum.ISSUED.getStatus()) .setWorkOrderId(workOrderId) .setIssueDate(LocalDateTime.now())); log.info("[issueMps] MPS 下发成功,mpsId={}, workOrderId={}", id, workOrderId); return workOrderId; } @Override @Transactional(rollbackFor = Exception.class) public Long mergeAndIssueMps(List mpsIds) { // 校验 MPS 列表 if (CollUtil.isEmpty(mpsIds) || mpsIds.size() < 2) { throw exception(PRO_MPS_MERGE_AT_LEAST_TWO); } List 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; } @Override @Transactional(rollbackFor = Exception.class) public void cancelMps(Long id) { // 校验存在 MesProMpsDO mps = validateMpsExists(id); // 校验状态:只有已下发状态才能取消 if (!MesProMpsStatusEnum.ISSUED.getStatus().equals(mps.getStatus())) { 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 状态 mpsMapper.updateById(new MesProMpsDO() .setId(id) .setStatus(MesProMpsStatusEnum.CANCELED.getStatus())); } @Override @Transactional(rollbackFor = Exception.class) public void finishMps(Long id) { // 校验存在 MesProMpsDO mps = validateMpsExists(id); // 校验状态:只有已下发状态才能完成 if (!MesProMpsStatusEnum.ISSUED.getStatus().equals(mps.getStatus())) { throw exception(PRO_MPS_NOT_ISSUED); } // 更新状态 mpsMapper.updateById(new MesProMpsDO() .setId(id) .setStatus(MesProMpsStatusEnum.FINISHED.getStatus()) .setFinishDate(LocalDateTime.now())); } @Override public MesProMpsDO getMpsBySaleOrderItemId(Long saleOrderItemId) { return mpsMapper.selectBySaleOrderItemId(saleOrderItemId); } @Override public MesProMpsDO getMps(Long id) { return mpsMapper.selectById(id); } @Override public PageResult getMpsPage(MesProMpsPageReqVO pageReqVO) { // 获取用户有权限的客户 ID 列表 List permittedClientIds = customerApi.getPermittedCustomerIds(); return mpsMapper.selectPage(pageReqVO, permittedClientIds); } @Override public List getMpsList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return mpsMapper.selectBatchIds(ids); } @Override public List getMpsListBySaleOrderItemIds(Collection saleOrderItemIds) { return mpsMapper.selectListBySaleOrderItemIds(saleOrderItemIds); } @Override public List getMergeableMpsList(Long productId) { return mpsMapper.selectMergeableList(productId); } @Override public void updateProducedQuantity(Long id, BigDecimal incrQuantity) { mpsMapper.updateProducedQuantity(id, incrQuantity); } @Override public Boolean checkSaleOrderProductionFinished(Long saleOrderId) { if (saleOrderId == null) { return false; } // 1. 查询该销售订单所有 MPS(通过 saleOrderId 关联) List mpsList = mpsMapper.selectList( MesProMpsDO::getSaleOrderId, saleOrderId); if (CollUtil.isEmpty(mpsList)) { // 无 MPS 关联,视为完成 return true; } // 2. 检查每个 MPS 是否完成 for (MesProMpsDO mps : mpsList) { if (!isMpsFinished(mps)) { return false; } } return true; } @Override public MesProMpsDO getMpsByWorkOrderId(Long workOrderId) { if (workOrderId == null) { return null; } return mpsMapper.selectOne(MesProMpsDO::getWorkOrderId, workOrderId); } /** * 判断单个 MPS 是否完成 */ private boolean isMpsFinished(MesProMpsDO mps) { // 1. 已生产数量 >= 计划数量 BigDecimal quantityProduced = ObjUtil.defaultIfNull(mps.getQuantityProduced(), BigDecimal.ZERO); if (quantityProduced.compareTo(mps.getQuantity()) < 0) { return false; } // 2. 检查工单下所有报工是否完成(确保质检完成) if (mps.getWorkOrderId() != null) { Long unfinishedCount = feedbackMapper.selectCount( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId()) .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus())); if (unfinishedCount > 0) { return false; } } return true; } // ========== 私有方法 ========== /** * 生成 MPS 编码 */ private String generateMpsCode() { try { return autoCodeRecordService.generateAutoCode(MPS_CODE_RULE); } catch (Exception e) { // 如果编码规则不存在,使用时间戳生成 log.warn("[generateMpsCode] 编码规则 {} 不存在,使用时间戳生成", MPS_CODE_RULE); return "MPS" + System.currentTimeMillis(); } } /** * 生成合并批次号 */ private String generateMergeBatchNo() { return "MERGE" + System.currentTimeMillis(); } /** * 校验 MPS 存在 */ private MesProMpsDO validateMpsExists(Long id) { MesProMpsDO mps = mpsMapper.selectById(id); if (mps == null) { throw exception(PRO_MPS_NOT_EXISTS); } return mps; } /** * 校验 MPS 存在 + 草稿状态 */ private MesProMpsDO validateMpsExistsAndDraft(Long id) { MesProMpsDO mps = validateMpsExists(id); if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) { throw exception(PRO_MPS_NOT_DRAFT); } return mps; } /** * 校验 MPS 编码唯一 */ private void validateMpsCodeUnique(Long id, String code) { if (StrUtil.isBlank(code)) { return; } MesProMpsDO mps = mpsMapper.selectByCode(code); if (mps == null) { return; } if (id == null) { throw exception(PRO_MPS_CODE_DUPLICATE); } if (!id.equals(mps.getId())) { throw exception(PRO_MPS_CODE_DUPLICATE); } } /** * 根据 MPS 创建生产工单 */ private Long createWorkOrderFromMps(MesProMpsDO mps, BigDecimal quantity) { MesProWorkOrderSaveReqVO workOrderReq = new MesProWorkOrderSaveReqVO(); workOrderReq.setCode(generateWorkOrderCode()); workOrderReq.setName("生产工单-" + mps.getCode()); workOrderReq.setType(1); // 自行生产 workOrderReq.setOrderSourceType(MesProWorkOrderSourceTypeEnum.ORDER.getType()); workOrderReq.setOrderSourceCode(mps.getSaleOrderNo()); workOrderReq.setProductId(mps.getProductId()); workOrderReq.setQuantity(quantity); workOrderReq.setClientId(mps.getClientId()); workOrderReq.setRequestDate(mps.getRequestDate() != null ? mps.getRequestDate() : LocalDateTime.now()); workOrderReq.setRemark("来自 MPS: " + mps.getCode()); Long workOrderId = workOrderService.createWorkOrder(workOrderReq); // 工单直接确认为已确认状态(跳过草稿) workOrderService.confirmWorkOrder(workOrderId); return workOrderId; } /** * 生成生产工单编码 */ private String generateWorkOrderCode() { try { return autoCodeRecordService.generateAutoCode("WO"); } catch (Exception e) { log.warn("[generateWorkOrderCode] 编码规则 WO 不存在,使用时间戳生成"); return "WO" + System.currentTimeMillis(); } } }