已修改6个文件
154 ■■■■■ 文件已修改
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/MesPdDocumentController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/vo/MesPdDocumentRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/document/MesPdDocumentServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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) {