yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java
@@ -1,23 +1,18 @@ package cn.iocoder.yudao.module.bpm.service.message; import cn.iocoder.yudao.framework.web.config.WebProperties; import cn.iocoder.yudao.module.bpm.convert.message.BpmMessageConvert; import cn.iocoder.yudao.module.bpm.enums.message.BpmMessageEnum; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO; import cn.iocoder.yudao.module.system.api.sms.SmsSendApi; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import jakarta.annotation.Resource; import java.util.HashMap; import java.util.Map; /** * BPM 消息 Service 实现类 * * 注意:已关闭短信通知功能,相关方法仅记录日志,不再调用短信发送接口, * 以避免“短信模板不存在”等问题。 * * @author 超级管理员 */ @@ -26,54 +21,24 @@ @Slf4j public class BpmMessageServiceImpl implements BpmMessageService { @Resource private SmsSendApi smsSendApi; @Resource private WebProperties webProperties; @Override public void sendMessageWhenProcessInstanceApprove(BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO) { Map<String, Object> templateParams = new HashMap<>(); templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(), BpmMessageEnum.PROCESS_INSTANCE_APPROVE.getSmsTemplateCode(), templateParams)); log.info("[sendMessageWhenProcessInstanceApprove][已关闭短信通知,跳过发送][processInstanceId: {}]", reqDTO.getProcessInstanceId()); } @Override public void sendMessageWhenProcessInstanceReject(BpmMessageSendWhenProcessInstanceRejectReqDTO reqDTO) { Map<String, Object> templateParams = new HashMap<>(); templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); templateParams.put("reason", reqDTO.getReason()); templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(), BpmMessageEnum.PROCESS_INSTANCE_REJECT.getSmsTemplateCode(), templateParams)); log.info("[sendMessageWhenProcessInstanceReject][已关闭短信通知,跳过发送][processInstanceId: {}]", reqDTO.getProcessInstanceId()); } @Override public void sendMessageWhenTaskAssigned(BpmMessageSendWhenTaskCreatedReqDTO reqDTO) { Map<String, Object> templateParams = new HashMap<>(); templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); templateParams.put("taskName", reqDTO.getTaskName()); templateParams.put("startUserNickname", reqDTO.getStartUserNickname()); templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(), BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams)); log.info("[sendMessageWhenTaskAssigned][已关闭短信通知,跳过发送][processInstanceId: {}]", reqDTO.getProcessInstanceId()); } @Override public void sendMessageWhenTaskTimeout(BpmMessageSendWhenTaskTimeoutReqDTO reqDTO) { Map<String, Object> templateParams = new HashMap<>(); templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); templateParams.put("taskName", reqDTO.getTaskName()); templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(), BpmMessageEnum.TASK_TIMEOUT.getSmsTemplateCode(), templateParams)); } private String getProcessInstanceDetailUrl(String taskId) { return webProperties.getAdminUi().getUrl() + "/bpm/process-instance/detail?id=" + taskId; log.info("[sendMessageWhenTaskTimeout][已关闭短信通知,跳过发送][processInstanceId: {}]", reqDTO.getProcessInstanceId()); } } yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -21,6 +21,7 @@ import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi; import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsCreateReqDTO; import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO; import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import jakarta.annotation.Resource; @@ -50,6 +51,11 @@ @Validated @Slf4j public class ErpSaleOrderServiceImpl implements ErpSaleOrderService { /** * MPS 状态:已取消(对应 MES 模块 MesProMpsStatusEnum.CANCELED 的值 3) */ private static final Integer MPS_STATUS_CANCELED = 3; @Resource private ErpSaleOrderMapper saleOrderMapper; @@ -205,6 +211,13 @@ if (item.getNeedProduction() == null || item.getNeedProduction() != 1) { continue; } // 避免审批/反审批重复操作导致同一个销售订单号产生多条 MPS MesProMpsRespDTO existingMps = mpsApi.getMpsBySaleOrderItemId(item.getId()); if (existingMps != null && ObjectUtil.notEqual(existingMps.getStatus(), MPS_STATUS_CANCELED)) { log.info("[createMpsForSaleOrder] 销售订单明细({}) 已存在 MPS({}),状态={},跳过创建", item.getId(), existingMps.getId(), existingMps.getStatus()); continue; } try { Long mpsId = createMpsForSaleOrderItem(saleOrder, item); if (mpsId != null) { yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/MesPdDocumentController.java
@@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.mes.controller.admin.pd.document; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageParam; @@ -14,6 +15,7 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.pd.document.MesPdDocumentDO; import cn.iocoder.yudao.module.mes.service.pd.document.MesPdDocumentService; import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import io.swagger.v3.oas.annotations.Operation; @@ -117,10 +119,22 @@ // 1.1 批量获取关联数据(上传人) Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertSet(list, MesPdDocumentDO::getUploadUserId)); // 2. 拼接 VO return BeanUtils.toBean(list, MesPdDocumentRespVO.class, vo -> { List<MesPdDocumentRespVO> voList = BeanUtils.toBean(list, MesPdDocumentRespVO.class, vo -> { MapUtils.findAndThen(userMap, vo.getUploadUserId(), user -> vo.setUploadUserNickname(user.getNickname())); }); // 3. 拼接文件信息:优先取第一个附件的地址作为 fileUrl,并在未填文件名时自动补全 for (MesPdDocumentRespVO vo : voList) { List<StorageBlobRespDTO> attachments = storageAttachmentApi.listAttachments("mes_pd_document", vo.getId()); if (CollUtil.isNotEmpty(attachments)) { StorageBlobRespDTO first = attachments.get(0); vo.setFileUrl(first.getUrl()); if (StrUtil.isEmpty(vo.getFileName())) { vo.setFileName(first.getName()); } } } return voList; } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/vo/MesPdDocumentRespVO.java
@@ -41,6 +41,9 @@ @ExcelProperty("文件名") private String fileName; @Schema(description = "文件地址", example = "https://www.iocoder.cn/design_v1.pdf") private String fileUrl; @Schema(description = "上传人", example = "1") private Long uploadUserId; yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/document/MesPdDocumentServiceImpl.java
@@ -1,18 +1,23 @@ package cn.iocoder.yudao.module.mes.service.pd.document; import cn.hutool.core.collection.CollUtil; 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.mes.controller.admin.pd.document.vo.MesPdDocumentPageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.pd.document.vo.MesPdDocumentSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.pd.document.MesPdDocumentDO; import cn.iocoder.yudao.module.mes.dal.mysql.pd.document.MesPdDocumentMapper; import cn.iocoder.yudao.module.mes.service.pd.project.MesPdProjectService; import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.time.LocalDateTime; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -42,20 +47,40 @@ projectService.validateProjectExists(createReqVO.getProjectId()); // 插入 MesPdDocumentDO document = BeanUtils.toBean(createReqVO, MesPdDocumentDO.class); // 补充上传人、上传时间 if (document.getUploadUserId() == null) { document.setUploadUserId(SecurityFrameworkUtils.getLoginUserId()); } if (document.getUploadTime() == null) { document.setUploadTime(LocalDateTime.now()); } documentMapper.insert(document); storageAttachmentApi.bindAttachments("file", "mes_pd_document", document.getId(), createReqVO.getBlobIds()); // 未填写文件名时,从上传附件自动补全 fillFileNameIfAbsent(document); return document.getId(); } @Override public void updateDocument(MesPdDocumentSaveReqVO updateReqVO) { // 校验存在 validateDocumentExists(updateReqVO.getId()); MesPdDocumentDO existing = validateDocumentExists(updateReqVO.getId()); // 更新 MesPdDocumentDO updateObj = BeanUtils.toBean(updateReqVO, MesPdDocumentDO.class); // 保留原上传人、上传时间;历史数据为空时补齐 if (updateObj.getUploadUserId() == null) { updateObj.setUploadUserId(existing.getUploadUserId() != null ? existing.getUploadUserId() : SecurityFrameworkUtils.getLoginUserId()); } if (updateObj.getUploadTime() == null) { updateObj.setUploadTime(existing.getUploadTime() != null ? existing.getUploadTime() : LocalDateTime.now()); } documentMapper.updateById(updateObj); storageAttachmentApi.updateAttachments("file", "mes_pd_document", updateReqVO.getId(), updateReqVO.getBlobIds()); // 未填写文件名时,从上传附件自动补全 fillFileNameIfAbsent(updateObj); } @Override @@ -100,4 +125,19 @@ return document; } /** * 未填写文件名时,从上传附件自动补全文件名 * * @param document 设计资料 */ private void fillFileNameIfAbsent(MesPdDocumentDO document) { if (StrUtil.isNotEmpty(document.getFileName())) { return; } List<StorageBlobRespDTO> attachments = storageAttachmentApi.listAttachments("mes_pd_document", document.getId()); if (CollUtil.isNotEmpty(attachments)) { document.setFileName(attachments.get(0).getName()); documentMapper.updateById(document); } } } yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -22,6 +22,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; @@ -282,14 +283,26 @@ 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()); // 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"); } } } @@ -424,6 +437,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) {