云
5 小时以前 e09f103259fa602382bb5ce2a03361a6397af46a
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -7,19 +7,25 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskAutoGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationMachineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessParamDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
import cn.iocoder.yudao.module.mes.service.dv.machinery.MesDvMachineryService;
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.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationMachineService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessParamService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
@@ -29,6 +35,7 @@
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderParamRecordService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -40,27 +47,51 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_ALREADY_FINISHED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_CANNOT_PAUSE;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_HAS_FEEDBACK;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_NOT_EXISTS;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_NOT_PAUSED;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskPauseRecordDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskPauseRecordMapper;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskAssigneeDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskAssigneeMapper;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskPauseReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskResumeReqVO;
/**
 * MES 生产任务 Service 实现类
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
public class MesProTaskServiceImpl implements MesProTaskService {
    @Resource
    private cn.iocoder.yudao.module.mes.service.pro.task.schedule.MesProScheduleGuardService scheduleGuard;
    @Resource
    private cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper scheduleWorkOrderMapper;
    @Resource
    private MesProTaskMapper taskMapper;
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesProTaskPauseRecordMapper taskPauseRecordMapper;
    @Resource
    private MesProTaskAssigneeMapper taskAssigneeMapper;
    @Resource
    @Lazy
@@ -85,13 +116,29 @@
    @Lazy
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    @Lazy
    private MesProWorkOrderParamRecordService workOrderParamRecordService;
    @Resource
    @Lazy
    private MesProProcessParamService processParamService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private MesMdWorkstationMachineService workstationMachineService;
    @Resource
    @Lazy
    private MesDvMachineryService machineryService;
    @Override
    @Transactional(rollbackFor = Exception.class, isolation = org.springframework.transaction.annotation.Isolation.SERIALIZABLE)
    public Long createTask(MesProTaskSaveReqVO createReqVO) {
        scheduleGuard.lock();
        // 1. 校验关联数据存在
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderExists(createReqVO.getWorkOrderId());
        workstationService.validateWorkstationExists(createReqVO.getWorkstationId());
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(createReqVO.getWorkOrderId());
        var station = workstationService.validateWorkstationExistsAndEnable(createReqVO.getWorkstationId());
        if (!java.util.Objects.equals(station.getProcessId(), createReqVO.getProcessId())) {
            throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_SCHEDULE_INVALID, "工作站与工序不匹配");
        }
        routeService.validateRouteExists(createReqVO.getRouteId());
        processService.validateProcessExistsAndEnable(createReqVO.getProcessId());
@@ -101,7 +148,7 @@
            MesProRouteProcessDO routeProcess = routeProcessService
                    .getRouteProcessByRouteIdAndProcessId(createReqVO.getRouteId(), createReqVO.getProcessId());
            if (routeProcess != null && routeProcess.getOutputItemId() != null) {
                itemId = routeProcess.getOutputItemId();
                itemId = Long.valueOf(routeProcess.getOutputItemId().split(",")[0].trim());
            } else {
                itemId = workOrder.getProductId();
            }
@@ -117,15 +164,40 @@
        if (task.getClientId() == null && workOrder.getClientId() != null) {
            task.setClientId(workOrder.getClientId());
        }
        scheduleGuard.validate(workOrder, task, null);
        // 2.2 插入
        taskMapper.insert(task);
        // 2.2.1 参数快照:优先使用模板;无模板时回退到工序主数据绑定的工艺参数
        createTaskParamRecordWithFallback(task);
        // 2.3 保存指定报工人员
        updateTaskAssignees(task.getId(), createReqVO.getAssigneeUserIds());
        synchronizeScheduledQuantity(workOrder, task.getRouteId());
        return task.getId();
    }
    /**
     * 为任务固化参数快照:优先使用路线/工单工序绑定的参数模板;无模板时回退到工序主数据绑定的工艺参数
     */
    private void createTaskParamRecordWithFallback(MesProTaskDO task) {
        if (task.getParamTemplateId() != null) {
            workOrderParamRecordService.createTaskParamRecord(task.getId(), task.getParamTemplateId());
            return;
        }
        // 无模板:从工序主数据解析绑定的工艺参数
        List<MesProProcessParamDO> processParams = processParamService.getProcessParamListByProcessId(task.getProcessId());
        if (CollUtil.isNotEmpty(processParams)) {
            workOrderParamRecordService.createTaskParamRecordFromProcessParams(task.getId(), processParams);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class, isolation = org.springframework.transaction.annotation.Isolation.SERIALIZABLE)
    public void updateTask(MesProTaskSaveReqVO updateReqVO) {
        scheduleGuard.lock();
        // 1.1 校验存在
        MesProTaskDO task = validateTaskExists(updateReqVO.getId());
        MesProTaskDO task = validateTaskNotFinished(updateReqVO.getId());
        // 1.2 校验关联数据存在
        if (updateReqVO.getWorkOrderId() != null) {
            workOrderService.validateWorkOrderExists(updateReqVO.getWorkOrderId());
@@ -147,10 +219,71 @@
        }
        BigDecimal quantity = updateReqVO.getQuantity() != null ? updateReqVO.getQuantity() : task.getQuantity();
        MesProTaskDO candidate = BeanUtils.toBean(task, MesProTaskDO.class)
                .setWorkOrderId(updateReqVO.getWorkOrderId() != null ? updateReqVO.getWorkOrderId() : task.getWorkOrderId())
                .setProcessId(updateReqVO.getProcessId() != null ? updateReqVO.getProcessId() : task.getProcessId())
                .setWorkstationId(updateReqVO.getWorkstationId() != null ? updateReqVO.getWorkstationId() : task.getWorkstationId())
                .setQuantity(quantity)
                .setStartTime(updateReqVO.getStartTime() != null ? updateReqVO.getStartTime() : task.getStartTime())
                .setEndTime(updateReqVO.getEndTime() != null ? updateReqVO.getEndTime() : task.getEndTime());
        var station = workstationService.validateWorkstationExistsAndEnable(candidate.getWorkstationId());
        if (!java.util.Objects.equals(station.getProcessId(), candidate.getProcessId())) {
            throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_SCHEDULE_INVALID, "工作站与工序不匹配");
        }
        scheduleGuard.validate(workOrderService.validateWorkOrderConfirmed(candidate.getWorkOrderId()), candidate, task.getId());
        // 2. 更新
        MesProTaskDO updateObj = BeanUtils.toBean(updateReqVO, MesProTaskDO.class)
                .setName(buildTaskName(item, quantity));
        taskMapper.updateById(updateObj);
        // 3. 更新指定报工人员(比对更新)
        updateTaskAssignees(task.getId(), updateReqVO.getAssigneeUserIds());
        synchronizeScheduledQuantity(workOrderService.validateWorkOrderExists(candidate.getWorkOrderId()),
                updateReqVO.getRouteId() != null ? updateReqVO.getRouteId() : task.getRouteId());
        if (!java.util.Objects.equals(task.getWorkOrderId(), candidate.getWorkOrderId())) {
            synchronizeScheduledQuantity(workOrderService.validateWorkOrderExists(task.getWorkOrderId()), task.getRouteId());
        }
    }
    /** 工单排产量按各工序累计排产量的最小值计算,任务修改/删除后同步,避免无法补排。 */
    private void synchronizeScheduledQuantity(MesProWorkOrderDO order, Long routeId) {
        List<MesProRouteProcessDO> required = buildRouteProcessList(routeId, order.getId());
        List<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(order.getId());
        BigDecimal scheduled = order.getQuantity();
        for (var process : required) {
            BigDecimal amount = tasks.stream().filter(task -> java.util.Objects.equals(task.getProcessId(), process.getProcessId())
                            && !java.util.Objects.equals(task.getStatus(), MesProTaskStatusEnum.CANCELED.getStatus()))
                    .map(task -> task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity())
                    .reduce(BigDecimal.ZERO, BigDecimal::add);
            scheduled = scheduled.min(amount);
        }
        scheduleWorkOrderMapper.updateById(new MesProWorkOrderDO().setId(order.getId()).setQuantityScheduled(scheduled));
    }
    private void updateTaskAssignees(Long taskId, List<Long> assigneeUserIds) {
        if (assigneeUserIds == null) {
            assigneeUserIds = Collections.emptyList();
        }
        // 1. 查询数据库中原有的
        List<MesProTaskAssigneeDO> oldAssignees = taskAssigneeMapper.selectListByTaskId(taskId);
        Set<Long> oldUserIds = convertSet(oldAssignees, MesProTaskAssigneeDO::getUserId);
        Set<Long> newUserIds = new java.util.HashSet<>(assigneeUserIds);
        // 2. 计算需要新增的和需要删除的
        Set<Long> toAddUserIds = newUserIds.stream().filter(id -> !oldUserIds.contains(id)).collect(java.util.stream.Collectors.toSet());
        Set<Long> toRemoveUserIds = oldUserIds.stream().filter(id -> !newUserIds.contains(id)).collect(java.util.stream.Collectors.toSet());
        // 3. 新增
        if (CollUtil.isNotEmpty(toAddUserIds)) {
            List<MesProTaskAssigneeDO> addList = convertList(toAddUserIds, userId -> new MesProTaskAssigneeDO().setTaskId(taskId).setUserId(userId));
            taskAssigneeMapper.insertBatch(addList);
        }
        // 4. 删除
        if (CollUtil.isNotEmpty(toRemoveUserIds)) {
            taskAssigneeMapper.deleteByTaskIdAndUserIds(taskId, toRemoveUserIds);
        }
    }
    /**
@@ -198,6 +331,7 @@
                result.add(new MesProRouteProcessDO()
                        .setRouteId(routeId)
                        .setProcessId(wp.getProcessId())
                        .setParamTemplateId(wp.getParamTemplateId())
                        .setSort(wp.getSort())
                        .setKeyFlag(wp.getKeyFlag())
                        .setCheckFlag(wp.getCheckFlag())
@@ -216,15 +350,20 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteTask(Long id) {
        scheduleGuard.lock();
        // 1. 校验存在且非终态
        validateTaskNotFinished(id);
        MesProTaskDO task = validateTaskNotFinished(id);
        // 2. 校验任务下无报工记录,避免删除后报工追溯断链
        if (feedbackMapper.selectCountByTaskId(id) > 0) {
            throw exception(PRO_TASK_HAS_FEEDBACK);
        }
        // 3. 删除
        // 3. 删除任务主表
        taskMapper.deleteById(id);
        // 4. 删除关联的人员
        taskAssigneeMapper.deleteByTaskId(id);
        synchronizeScheduledQuantity(workOrderService.validateWorkOrderExists(task.getWorkOrderId()), task.getRouteId());
    }
    @Override
@@ -248,6 +387,14 @@
            return Collections.emptyList();
        }
        return taskMapper.selectListByWorkOrderIds(workOrderIds);
    }
    @Override
    public List<MesProTaskAssigneeDO> getTaskAssigneeListByTaskIds(Collection<Long> taskIds) {
        if (CollUtil.isEmpty(taskIds)) {
            return Collections.emptyList();
        }
        return taskAssigneeMapper.selectListByTaskIds(taskIds);
    }
    @Override
@@ -315,6 +462,42 @@
        validateTaskExists(id);
        // 更新数量
        taskMapper.updateProducedQuantity(id, incrProducedQuantity, incrQualifyQuantity, incrUnqualifyQuantity);
        MesProTaskDO task = taskMapper.selectById(id);
        if (task != null) {
            LocalDateTime now = LocalDateTime.now();
            MesProTaskDO timeUpdate = new MesProTaskDO().setId(id).setLastFeedbackTime(now);
            if (task.getFirstFeedbackTime() == null) {
                timeUpdate.setFirstFeedbackTime(now);
            }
            taskMapper.updateById(timeUpdate);
        }
    }
    @Override
    public Integer calculateDuration(Long workstationId, BigDecimal quantity) {
        if (quantity == null || quantity.compareTo(BigDecimal.ZERO) <= 0) {
            return 0;
        }
        // 1. 查询工作站绑定的设备
        List<MesMdWorkstationMachineDO> machines = workstationMachineService
                .getWorkstationMachineListByWorkstationId(workstationId);
        if (CollUtil.isEmpty(machines)) {
            return 0;
        }
        // 2. 获取设备详情,计算总效率
        Set<Long> machineryIds = convertSet(machines, MesMdWorkstationMachineDO::getMachineryId);
        List<MesDvMachineryDO> machineryList = machineryService.getMachineryList(machineryIds);
        BigDecimal totalEfficiency = BigDecimal.ZERO;
        for (MesDvMachineryDO m : machineryList) {
            BigDecimal efficiency = m.getProductionEfficiency() != null ? m.getProductionEfficiency() : BigDecimal.ZERO;
            Integer belt = m.getConveyorBelt() != null && m.getConveyorBelt() > 0 ? m.getConveyorBelt() : 1;
            totalEfficiency = totalEfficiency.add(efficiency.multiply(BigDecimal.valueOf(belt)));
        }
        if (totalEfficiency.compareTo(BigDecimal.ZERO) <= 0) {
            return 0;
        }
        // 3. 生产时长(小时) = ceil(排产数量 / 总效率),效率单位为每小时产量
        return quantity.divide(totalEfficiency, 0, java.math.RoundingMode.CEILING).intValue();
    }
    @Override
@@ -353,6 +536,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<Long> autoGenerateTasks(MesProTaskAutoGenerateReqVO reqVO) {
        scheduleGuard.lock();
        // 1. 校验工单已确认
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        // 2. 确定工艺路线:优先请求指定 → 工单关联 → 产品关联
@@ -370,7 +554,7 @@
        List<Long> taskIds = new java.util.ArrayList<>();
        for (MesProRouteProcessDO rp : routeProcesses) {
            cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO proc = procMap.get(rp.getProcessId());
            Long itemId = rp.getOutputItemId() != null ? rp.getOutputItemId() : workOrder.getProductId();
            Long itemId = rp.getOutputItemId() != null ? Long.valueOf(rp.getOutputItemId().split(",")[0].trim()) : workOrder.getProductId();
            cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO item = itemService.validateItemExists(itemId);
            MesProTaskDO task = new MesProTaskDO()
                    .setWorkOrderId(workOrder.getId())
@@ -381,9 +565,13 @@
                    .setName(buildTaskName(item, workOrder.getQuantity()))
                    .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_TASK_CODE.getCode()))
                    .setStatus(MesProTaskStatusEnum.PREPARE.getStatus())
                    .setParamTemplateId(rp.getParamTemplateId())
                    .setClientId(workOrder.getClientId())
                    .setColorCode(rp.getColorCode());
            scheduleGuard.validate(workOrder, task, null);
            taskMapper.insert(task);
            // 参数快照:优先使用模板;无模板时回退到工序主数据绑定的工艺参数
            createTaskParamRecordWithFallback(task);
            taskIds.add(task.getId());
        }
        // 7. 更新工单已排产数量
@@ -391,4 +579,52 @@
        return taskIds;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void pauseTask(MesProTaskPauseReqVO pauseReqVO) {
        // 1. 校验任务存在
        MesProTaskDO task = validateTaskExists(pauseReqVO.getId());
        // 2. 校验状态是否允许停工(如必须是 生产中 或 部分完成)
        if (!ObjectUtils.equalsAny(task.getStatus(), MesProTaskStatusEnum.PREPARE.getStatus())) {
            throw exception(PRO_TASK_CANNOT_PAUSE);
        }
        // 3. 修改任务状态为已停工
        taskMapper.updateById(new MesProTaskDO().setId(task.getId()).setStatus(MesProTaskStatusEnum.PAUSED.getStatus()));
        // 4. 创建停工记录
        MesProTaskPauseRecordDO pauseRecord = new MesProTaskPauseRecordDO()
                .setTaskId(task.getId())
                .setWorkOrderId(task.getWorkOrderId())
                .setWorkstationId(task.getWorkstationId())
                .setReason(pauseReqVO.getReason())
                .setRemark(pauseReqVO.getRemark())
                .setPauseTime(LocalDateTime.now())
                .setPauseUserId(SecurityFrameworkUtils.getLoginUserId());
        taskPauseRecordMapper.insert(pauseRecord);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void resumeTask(MesProTaskResumeReqVO resumeReqVO) {
        // 1. 校验任务存在
        MesProTaskDO task = validateTaskExists(resumeReqVO.getId());
        // 2. 校验状态必须是已停工
        if (!ObjectUtils.equalsAny(task.getStatus(), MesProTaskStatusEnum.PAUSED.getStatus())) {
            throw exception(PRO_TASK_NOT_PAUSED);
        }
        // 3. 恢复任务状态
        Integer resumeStatus = MesProTaskStatusEnum.PREPARE.getStatus();
        taskMapper.updateById(new MesProTaskDO().setId(task.getId()).setStatus(resumeStatus));
        // 4. 更新最新的一条停工记录
        MesProTaskPauseRecordDO pauseRecord = taskPauseRecordMapper.selectLastUnresumedByTaskId(task.getId());
        if (pauseRecord != null) {
            pauseRecord.setResumeTime(LocalDateTime.now());
            pauseRecord.setResumeUserId(SecurityFrameworkUtils.getLoginUserId());
            taskPauseRecordMapper.updateById(pauseRecord);
        }
    }
}