云
10 天以前 bf5f52656e128f578ed42c7979c31cf4b476ce46
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -1,9 +1,10 @@
package cn.iocoder.yudao.module.mes.service.pro.feedback;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -31,6 +32,7 @@
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
import cn.iocoder.yudao.module.mes.service.wm.productissue.MesWmProductIssueService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
@@ -58,9 +60,11 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
@@ -74,6 +78,8 @@
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesFeedbackParamService feedbackParamService;
    @Resource
    private MesProWorkOrderService workOrderService;
@@ -88,6 +94,8 @@
    private MesProTaskService taskService;
    @Resource
    private MesWmItemConsumeService itemConsumeService;
    @Resource
    private MesWmProductIssueService productIssueService;
    @Resource
    private MesWmProductProduceService productProduceService;
    @Resource
@@ -104,37 +112,122 @@
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) {
        // 1. 校验
        MesProTaskDO task = validateFeedbackData(createReqVO);
        task = feedbackParamService.lockTask(task.getId());
        validateLockedTaskDimensions(createReqVO, task);
        // 2. 插入(自动填充 itemId)
        // 2. 插入(主键由数据库自增,忽略请求中可能残留的 id;自动填充 itemId)
        MesProFeedbackDO feedback = BeanUtils.toBean(createReqVO, MesProFeedbackDO.class)
                .setId(null)
                .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus())
                .setItemId(task.getItemId());
                .setItemId(task.getItemId())
                .setParamSnapshot(feedbackParamService.buildDraft(task, createReqVO.getParamValues()));
        feedbackMapper.insert(feedback);
        return feedback.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateFeedback(MesProFeedbackSaveReqVO updateReqVO) {
        // 1.1 校验存在 + 草稿状态
        validateFeedbackStatusPrepare(updateReqVO.getId());
        // 先锁报工再锁任务,与提交顺序一致。
        MesProFeedbackDO original = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, updateReqVO.getId()).last("FOR UPDATE"));
        if (original == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (!Objects.equals(original.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus()))
            throw exception(PRO_FEEDBACK_NOT_PREPARE);
        // 1.2 校验业务数据
        MesProTaskDO task = validateFeedbackData(updateReqVO);
        task = feedbackParamService.lockTask(task.getId());
        validateLockedTaskDimensions(updateReqVO, task);
        // 2. 更新(自动填充 itemId)
        MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class)
                .setItemId(task.getItemId());
        if (updateReqVO.getParamValues() != null) {
            String snapshot = feedbackParamService.buildDraft(task, updateReqVO.getParamValues());
            updateObj.setParamSnapshot(snapshot == null ? "" : snapshot);
        } else if (!Objects.equals(original.getTaskId(), task.getId())) {
            updateObj.setParamSnapshot("");
        }
        feedbackMapper.updateById(updateObj);
    }
    @Override
    public void deleteFeedback(Long id) {
        // 1. 校验存在 + 草稿状态
        validateFeedbackStatusPrepare(id);
    private void validateLockedTaskDimensions(MesProFeedbackSaveReqVO req, MesProTaskDO task) {
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        if (!Objects.equals(req.getProcessId(), task.getProcessId())
                || !Objects.equals(req.getWorkOrderId(), task.getWorkOrderId())
                || !Objects.equals(req.getRouteId(), task.getRouteId()))
            throw exception(PRO_FEEDBACK_PARAM_INVALID, "任务信息已变更,请重新选择任务");
    }
        // 2. 删除
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProFeedbackImportRespVO importFeedbackList(List<MesProFeedbackImportExcelVO> importList,
                                                         boolean updateSupport) {
        // 1. 校验非空
        if (CollUtil.isEmpty(importList)) {
            throw exception(MES_PRO_FEEDBACK_IMPORT_LIST_IS_EMPTY);
        }
        // 2. 逐行处理
        MesProFeedbackImportRespVO respVO = MesProFeedbackImportRespVO.builder()
                .createCodes(new ArrayList<>()).updateCodes(new ArrayList<>())
                .failureCodes(new LinkedHashMap<>()).build();
        AtomicInteger index = new AtomicInteger(1);
        importList.forEach(importItem -> {
            int currentIndex = index.getAndIncrement();
            String key = StrUtil.blankToDefault(importItem.getCode(), "第 " + currentIndex + " 行");
            if (StrUtil.isBlank(importItem.getCode())) {
                respVO.getFailureCodes().put(key, "报工单编号不能为空");
                return;
            }
            if (StrUtil.isBlank(importItem.getTaskCode())) {
                respVO.getFailureCodes().put(key, "生产任务编码不能为空");
                return;
            }
            // 2.1 解析生产任务
            MesProTaskDO task = taskService.getTaskByCode(importItem.getTaskCode().trim());
            if (task == null) {
                respVO.getFailureCodes().put(key, "生产任务编码[" + importItem.getTaskCode() + "]不存在");
                return;
            }
            // 2.2 构建请求并创建/更新(复用 create/update 服务,报工人/审核人取当前登录用户)
            MesProFeedbackSaveReqVO reqVO = BeanUtils.toBean(importItem, MesProFeedbackSaveReqVO.class);
            reqVO.setTaskId(task.getId());
            Long loginUserId = getLoginUserId();
            reqVO.setFeedbackUserId(ObjUtil.defaultIfNull(reqVO.getFeedbackUserId(), loginUserId));
            reqVO.setApproveUserId(ObjUtil.defaultIfNull(reqVO.getApproveUserId(), loginUserId));
            MesProFeedbackDO existFeedback = feedbackMapper.selectByCode(importItem.getCode());
            try {
                if (existFeedback == null) {
                    reqVO.setId(null);
                    createFeedback(reqVO);
                    respVO.getCreateCodes().add(importItem.getCode());
                } else if (updateSupport) {
                    reqVO.setId(existFeedback.getId());
                    updateFeedback(reqVO);
                    respVO.getUpdateCodes().add(importItem.getCode());
                } else {
                    respVO.getFailureCodes().put(key, "报工单编号已存在");
                }
            } catch (ServiceException e) {
                respVO.getFailureCodes().put(key, e.getMessage());
            }
        });
        return respVO;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteFeedback(Long id) {
        MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, id).last("FOR UPDATE"));
        if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (!Objects.equals(feedback.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus()))
            throw exception(PRO_FEEDBACK_NOT_PREPARE);
        feedbackMapper.deleteById(id);
    }
@@ -162,19 +255,27 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitFeedback(Long id) {
        // 1. 校验存在 + 草稿状态
        validateFeedbackStatusPrepare(id);
        // 2. 更新状态为审批中(报工人和报工时间由表单保存时确定,提交不覆盖)
        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()));
        MesProFeedbackDO feedback = feedbackMapper.selectOne(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getId, id).last("FOR UPDATE"));
        if (feedback == null) throw exception(PRO_FEEDBACK_NOT_EXISTS);
        if (!Objects.equals(feedback.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus()))
            throw exception(PRO_FEEDBACK_NOT_PREPARE);
        MesProTaskDO task = feedbackParamService.lockTask(feedback.getTaskId());
        String snapshot = feedbackParamService.complete(task, feedback.getParamSnapshot());
        MesProFeedbackDO update = new MesProFeedbackDO().setId(id)
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus());
        if (snapshot != null) update.setParamSnapshot(snapshot);
        feedbackMapper.updateById(update);
    }
    @Override
    public void rejectFeedback(Long id) {
        // 1. 校验存在 + 审批中状态
        validateFeedbackStatusApproving(id);
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(id);
        // 1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
        // 2. 更新状态为草稿
        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
@@ -186,6 +287,8 @@
    public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) {
        // 1.1 校验存在 + 审批中状态
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId());
        // 1.1.1 校验当前登录人是该报工单的指定审核人
        validateCurrentUserIsApprover(feedback);
        // 1.2 校验报工数量 > 0
        if (feedback.getFeedbackQuantity() == null
                || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
@@ -209,9 +312,9 @@
        // 3. 物料消耗:倒冲工序按 BOM 自动扣料;非倒冲工序跳过(物料走手工领料/投料)
        boolean backflushFlag = routeProcess != null && Boolean.TRUE.equals(routeProcess.getBackflushFlag());
        if (backflushFlag) {
            // 3.1 校验倒冲工序必须配置 BOM 消耗,避免静默不扣料
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderId(
                    feedback.getWorkOrderId());
            // 3.1 校验当前倒冲工序必须配置投料 BOM,避免静默不扣料
            List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId(
                    feedback.getWorkOrderId(), feedback.getProcessId());
            if (CollUtil.isEmpty(boms)) {
                throw exception(PRO_FEEDBACK_BACKFLUSH_BOM_REQUIRED);
            }
@@ -219,6 +322,8 @@
            MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback);
            if (itemConsume != null) {
                itemConsumeService.finishItemConsume(itemConsume.getId());
                // 消耗单负责实际扣库;领料单仅作为倒冲业务凭证,避免重复扣减库存
                productIssueService.generateBackflushProductIssue(feedback, itemConsume);
            }
        }
@@ -351,6 +456,23 @@
    }
    /**
     * 校验当前登录用户是否为报工单指定的审核人
     *
     * 报工单创建时必须指定 approveUserId,审批通过/驳回只能由该审核人操作,
     * 避免任意拥有菜单权限的用户越权审批。
     *
     * @param feedback 报工单
     */
    private void validateCurrentUserIsApprover(MesProFeedbackDO feedback) {
        if (feedback.getApproveUserId() == null) {
            throw exception(PRO_FEEDBACK_APPROVER_NOT_CONFIGURED);
        }
        if (!feedback.getApproveUserId().equals(getLoginUserId())) {
            throw exception(PRO_FEEDBACK_NOT_APPROVER);
        }
    }
    /**
     * 校验报工单的业务数据(创建 & 修改共用)
     *
     * <p>以生产任务为核心:前端只需传 taskId,workOrderId、routeId、processId、itemId、workstationId
@@ -449,6 +571,14 @@
                .setFeedbackTime(LocalDateTime.now())
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
                .setRemark(reqVO.getRemark());
        MesProTaskDO lockedTask = feedbackParamService.lockTask(task.getId());
        if (!MesProTaskStatusEnum.canFeedback(lockedTask.getStatus())) throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        if (!Objects.equals(task.getProcessId(), lockedTask.getProcessId())
                || !Objects.equals(task.getWorkOrderId(), lockedTask.getWorkOrderId())
                || !Objects.equals(task.getRouteId(), lockedTask.getRouteId()))
            throw exception(PRO_FEEDBACK_PARAM_INVALID, "任务信息已变更,请重新报工");
        String draftParam = feedbackParamService.buildDraft(lockedTask, reqVO.getParamValues());
        feedback.setParamSnapshot(feedbackParamService.complete(lockedTask, draftParam));
        feedbackMapper.insert(feedback);
        // 5. 返回结果
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(task.getWorkOrderId());
@@ -466,6 +596,11 @@
        int failCount = 0;
        for (Long taskId : reqVO.getTaskIds()) {
            try {
                MesProTaskDO batchTask = feedbackParamService.lockTask(taskId);
                if (Objects.equals(batchTask.getParamCollectMode(), 1)
                        || (Objects.equals(batchTask.getParamCollectMode(), 2) && StrUtil.isBlank(batchTask.getParamSharedSnapshot()))) {
                    throw exception(PRO_FEEDBACK_PARAM_INVALID, "该任务需要单独填写参数,请使用单笔报工");
                }
                MesProFeedbackQuickCreateReqVO singleReq = new MesProFeedbackQuickCreateReqVO();
                singleReq.setTaskId(taskId);
                singleReq.setFeedbackQuantity(reqVO.getFeedbackQuantity());