云
17 小时以前 ac2e2b5af3726ee64dc37c3ebf26989a0346f22a
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/MesProTaskServiceImpl.java
@@ -2,55 +2,101 @@
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.mes.service.pd.processparamtemplate.MesPdProcessParamTemplateService;
import cn.iocoder.yudao.module.mes.service.pro.feedback.dto.MesTaskParamSnapshot;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskAutoGeneratePreviewRespVO;
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.md.workstation.MesMdWorkstationWorkerDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
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.cal.team.MesCalTeamService;
import cn.iocoder.yudao.module.mes.dal.dataobject.cal.team.MesCalTeamDO;
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.md.workstation.MesMdWorkstationWorkerService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
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;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteService;
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 jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
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.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_TASK_SCHEDULE_INVALID;
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.controller.admin.pro.task.vo.MesProTaskPauseReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProTaskResumeReqVO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
/**
 * 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 MesPdProcessParamTemplateService paramTemplateService;
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesProTaskPauseRecordMapper taskPauseRecordMapper;
    @Resource
    @Lazy
@@ -66,34 +112,81 @@
    @Resource
    private MesProRouteService routeService;
    @Resource
    @Lazy
    private MesProRouteProcessService routeProcessService;
    @Resource
    @Lazy
    private MesProRouteProductService routeProductService;
    @Resource
    @Lazy
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private MesMdWorkstationMachineService workstationMachineService;
    @Resource
    @Lazy
    private MesDvMachineryService machineryService;
    @Resource
    private MesCalTeamService teamService;
    @Resource
    private MesMdWorkstationWorkerService workstationWorkerService;
    @Resource
    private AdminUserApi adminUserApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createTask(MesProTaskSaveReqVO createReqVO) {
        scheduleGuard.lock();
        // 1. 校验关联数据存在
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderExists(createReqVO.getWorkOrderId());
        workstationService.validateWorkstationExists(createReqVO.getWorkstationId());
        var station = workstationService.validateWorkstationExists(createReqVO.getWorkstationId());
        if (java.util.Objects.equals(workOrder.getType(), cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderTypeEnum.SELF.getType()) && !java.util.Objects.equals(station.getProcessId(), createReqVO.getProcessId())) {
            throw exception(PRO_TASK_SCHEDULE_INVALID, "工作站与工序不匹配");
        }
        routeService.validateRouteExists(createReqVO.getRouteId());
        processService.validateProcessExistsAndEnable(createReqVO.getProcessId());
        MesMdItemDO item = itemService.validateItemExists(createReqVO.getItemId());
        fillTeamInfo(createReqVO);
        // 2.1 构建任务 DO:自动填充客户信息
        // 2. 自动确定 itemId:优先前端传入 → 工艺路线工序的产出产品 → 工单成品
        Long itemId = createReqVO.getItemId();
        if (itemId == null) {
            MesProRouteProcessDO routeProcess = routeProcessService
                    .getRouteProcessByRouteIdAndProcessId(createReqVO.getRouteId(), createReqVO.getProcessId());
            itemId = resolveTaskItemId(routeProcess, workOrder.getProductId());
        }
        MesMdItemDO item = itemService.validateItemExists(itemId);
        // 3.1 构建任务 DO:自动填充客户信息
        List<Long> assigneeUserIds = createReqVO.getAssigneeUserIds();
        if (CollUtil.isEmpty(assigneeUserIds)) {
            // 未指定报工人时默认取工作站配置的人员,避免一键排产生成的任务无人可报
            assigneeUserIds = getWorkstationWorkerUserIds(createReqVO.getWorkstationId());
        }
        MesProTaskDO task = BeanUtils.toBean(createReqVO, MesProTaskDO.class)
                .setItemId(itemId) // 确保使用计算后的 itemId
                .setName(buildTaskName(item, createReqVO.getQuantity()))
                .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_TASK_CODE.getCode()))
                .setAssigneeUserIds(joinUserIds(assigneeUserIds))
                .setStatus(MesProTaskStatusEnum.PREPARE.getStatus());
        if (task.getClientId() == null && workOrder.getClientId() != null) {
            task.setClientId(workOrder.getClientId());
        }
        // 2.2 插入
        scheduleGuard.validate(workOrder, task, null);
        freezeTaskParamConfig(task);
        taskMapper.insert(task);
        synchronizeScheduledQuantity(workOrder, task.getRouteId());
        return task.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateTask(MesProTaskSaveReqVO updateReqVO) {
        // 1.1 校验存在
        MesProTaskDO task = validateTaskExists(updateReqVO.getId());
        scheduleGuard.lock();
        MesProTaskDO task = taskMapper.selectOne(new LambdaQueryWrapperX<MesProTaskDO>()
                .eq(MesProTaskDO::getId, updateReqVO.getId()).last("FOR UPDATE"));
        if (task == null) throw exception(PRO_TASK_NOT_EXISTS);
        // 1.2 校验关联数据存在
        if (updateReqVO.getWorkOrderId() != null) {
            workOrderService.validateWorkOrderExists(updateReqVO.getWorkOrderId());
@@ -107,6 +200,7 @@
        if (updateReqVO.getProcessId() != null) {
            processService.validateProcessExistsAndEnable(updateReqVO.getProcessId());
        }
        fillTeamInfo(updateReqVO);
        MesMdItemDO item;
        if (updateReqVO.getItemId() != null) {
            item = itemService.validateItemExists(updateReqVO.getItemId());
@@ -115,10 +209,121 @@
        }
        BigDecimal quantity = updateReqVO.getQuantity() != null ? updateReqVO.getQuantity() : task.getQuantity();
        MesProTaskDO candidate = BeanUtils.toBean(task, MesProTaskDO.class)
                .setWorkOrderId(updateReqVO.getWorkOrderId() != null ? updateReqVO.getWorkOrderId() : task.getWorkOrderId())
                .setRouteId(updateReqVO.getRouteId() != null ? updateReqVO.getRouteId() : task.getRouteId())
                .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.validateWorkstationExists(candidate.getWorkstationId());
        if (java.util.Objects.equals(workOrderService.validateWorkOrderExists(candidate.getWorkOrderId()).getType(), cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderTypeEnum.SELF.getType()) && !java.util.Objects.equals(station.getProcessId(), candidate.getProcessId())) {
            throw exception(PRO_TASK_SCHEDULE_INVALID, "工作站与工序不匹配");
        }
        scheduleGuard.validate(workOrderService.validateWorkOrderExists(candidate.getWorkOrderId()), candidate, task.getId());
        // 2. 更新
        MesProTaskDO updateObj = BeanUtils.toBean(updateReqVO, MesProTaskDO.class)
                .setName(buildTaskName(item, quantity));
        // 报工人为 null 表示不修改;空集合表示清空限制(所有人可报工)
        if (updateReqVO.getAssigneeUserIds() != null) {
            updateObj.setAssigneeUserIds(joinUserIds(updateReqVO.getAssigneeUserIds()));
        }
        if (updateReqVO.getProcessId() != null && !updateReqVO.getProcessId().equals(task.getProcessId())) {
            if (feedbackMapper.selectCountByTaskId(task.getId()) > 0) throw exception(PRO_TASK_HAS_FEEDBACK);
            freezeTaskParamConfig(updateObj);
        }
        taskMapper.updateById(updateObj);
        synchronizeScheduledQuantity(workOrderService.validateWorkOrderExists(candidate.getWorkOrderId()), candidate.getRouteId());
        if (!java.util.Objects.equals(task.getWorkOrderId(), candidate.getWorkOrderId())) synchronizeScheduledQuantity(workOrderService.validateWorkOrderExists(task.getWorkOrderId()), task.getRouteId());
    }
    private void synchronizeScheduledQuantity(MesProWorkOrderDO order, Long routeId) {
        if (!java.util.Objects.equals(order.getType(), cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderTypeEnum.SELF.getType()) || order.getQuantity() == null || routeId == null) return;
        List<MesProRouteProcessDO> required = buildRouteProcessList(routeId, order.getId());
        List<MesProTaskDO> tasks = taskMapper.selectListByWorkOrderId(order.getId());
        BigDecimal scheduled = required.isEmpty() ? BigDecimal.ZERO : 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 freezeTaskParamConfig(MesProTaskDO task) {
        var process = processService.getProcess(task.getProcessId());
        if (process == null) throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_PROCESS_NOT_EXISTS);
        int mode = process.getParamCollectMode() == null ? 0 : process.getParamCollectMode();
        task.setParamCollectMode(mode);
        task.setParamTemplateSnapshot("");
        task.setParamSharedSnapshot("");
        if (mode == 0) return;
        if ((mode != 1 && mode != 2) || process.getParamTemplateId() == null)
            throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_PROCESS_PARAM_CONFIG_INVALID);
        var template = paramTemplateService.validateProcessParamTemplateExists(process.getParamTemplateId());
        var details = paramTemplateService.getProcessParamTemplateDetailListByTemplateId(template.getId());
        if (CollUtil.isEmpty(details)) throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_WORK_ORDER_PARAM_RECORD_TEMPLATE_NO_DETAIL);
        MesTaskParamSnapshot snapshot = new MesTaskParamSnapshot();
        snapshot.setTemplateId(template.getId());
        snapshot.setTemplateCode(template.getTemplateCode());
        snapshot.setTemplateName(template.getTemplateName());
        List<MesTaskParamSnapshot.Detail> frozenDetails = BeanUtils.toBean(details, MesTaskParamSnapshot.Detail.class);
        for (MesTaskParamSnapshot.Detail detail : frozenDetails) {
            if (detail.getUnitMeasureId() != null) {
                var unit = unitMeasureService.getUnitMeasure(detail.getUnitMeasureId());
                if (unit != null) detail.setUnitMeasureName(unit.getName());
            }
        }
        snapshot.setDetails(frozenDetails);
        task.setParamTemplateSnapshot(JsonUtils.toJsonString(snapshot));
    }
    /**
     * 校验班组存在并回填班组名称
     */
    private void fillTeamInfo(MesProTaskSaveReqVO reqVO) {
        if (reqVO.getTeamId() == null) {
            return;
        }
        MesCalTeamDO team = teamService.validateTeamExists(reqVO.getTeamId());
        if (team != null) {
            reqVO.setTeamName(team.getName());
        }
    }
    /**
     * 获得工作站配置岗位对应的员工编号
     */
    private List<Long> getWorkstationWorkerUserIds(Long workstationId) {
        if (workstationId == null) {
            return Collections.emptyList();
        }
        List<MesMdWorkstationWorkerDO> workers = workstationWorkerService
                .getWorkstationWorkerListByWorkstationId(workstationId);
        if (CollUtil.isEmpty(workers)) {
            return Collections.emptyList();
        }
        Set<Long> postIds = convertSet(workers, MesMdWorkstationWorkerDO::getPostId);
        if (CollUtil.isEmpty(postIds)) {
            return Collections.emptyList();
        }
        return convertList(adminUserApi.getUserListByPostIds(postIds), AdminUserRespDTO::getId);
    }
    /**
     * 将报工人用户编号列表拼接为逗号分隔字符串
     *
     * @return null 表示不修改/不限制;空字符串表示清空限制
     */
    private String joinUserIds(List<Long> userIds) {
        if (userIds == null) {
            return null;
        }
        return userIds.stream().filter(java.util.Objects::nonNull).distinct()
                .map(String::valueOf).collect(java.util.stream.Collectors.joining(","));
    }
    /**
@@ -135,21 +340,100 @@
        return item.getName() + "【" + quantity + "】" + unitName;
    }
    /**
     * 确定排产使用的工艺路线 ID:优先请求指定 → 工单关联 → 产品关联
     */
    private Long resolveRouteId(Long reqRouteId, MesProWorkOrderDO workOrder) {
        if (reqRouteId != null) {
            return reqRouteId;
        }
        if (workOrder.getRouteId() != null) {
            return workOrder.getRouteId();
        }
        // 从产品关联的工艺路线获取
        MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(workOrder.getProductId());
        if (routeProduct == null) {
            throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_ROUTE_NOT_EXISTS);
        }
        return routeProduct.getRouteId();
    }
    /**
     * 构建工序列表:优先使用工单已保存的工序 → 回退到工艺路线模板工序
     */
    private List<MesProRouteProcessDO> buildRouteProcessList(Long routeId, Long workOrderId) {
        // 1. 优先使用工单已保存的工序
        List<MesProWorkOrderProcessDO> woProcesses = workOrderProcessService
                .getWorkOrderProcessListByWorkOrderId(workOrderId);
        if (CollUtil.isNotEmpty(woProcesses)) {
            List<MesProRouteProcessDO> result = new java.util.ArrayList<>();
            for (MesProWorkOrderProcessDO wp : woProcesses) {
                result.add(new MesProRouteProcessDO()
                        .setRouteId(routeId)
                        .setProcessId(wp.getProcessId())
                        .setSort(wp.getSort())
                        .setKeyFlag(wp.getKeyFlag())
                        .setCheckFlag(wp.getCheckFlag())
                        .setBackflushFlag(wp.getBackflushFlag())
                        .setOutputItemId(wp.getOutputItemId())
                        .setColorCode(wp.getColorCode()));
            }
            return result;
        }
        // 2. 回退到工艺路线模板工序
        List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByRouteId(routeId);
        if (CollUtil.isEmpty(routeProcesses)) {
            throw exception(cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_ROUTE_ENABLE_NO_PROCESS);
        }
        return routeProcesses;
    }
    /**
     * 任务产出物料:优先取工序在当前产品下的产出(按归属过滤),没有产出时取工单成品
     */
    private Long resolveTaskItemId(MesProRouteProcessDO routeProcess, Long productId) {
        if (routeProcess == null) {
            return productId;
        }
        if (routeProcess.getId() != null) {
            // 工艺路线模板:按产出归属过滤出当前产品的产出
            List<Long> outputIds = routeProcessService
                    .getOutputItemIdsByRouteProcessId(routeProcess.getId(), productId);
            if (outputIds != null) {
                // 有归属配置:该产品无产出时取工单成品;null 表示存量无归属数据,继续走聚合兜底
                return CollUtil.isNotEmpty(outputIds) ? outputIds.get(0) : productId;
            }
        }
        // 工单工序快照 / 存量无归属数据:产出已按产品过滤,取第一个
        String outputItemId = routeProcess.getOutputItemId();
        if (outputItemId != null && !outputItemId.isBlank()) {
            return Long.valueOf(outputItemId.split(",")[0].trim());
        }
        return productId;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteTask(Long id) {
        // 1. 校验存在且非终态
        validateTaskNotFinished(id);
        scheduleGuard.lock();
        MesProTaskDO task = validateTaskNotFinished(id);
        // 2. 校验任务下无报工记录,避免删除后报工追溯断链
        if (feedbackMapper.selectCountByTaskId(id) > 0) {
            throw exception(PRO_TASK_HAS_FEEDBACK);
        }
        // 3. 删除
        taskMapper.deleteById(id);
        synchronizeScheduledQuantity(workOrderService.validateWorkOrderExists(task.getWorkOrderId()), task.getRouteId());
    }
    @Override
    public MesProTaskDO getTask(Long id) {
        return taskMapper.selectById(id);
    }
    @Override
    public MesProTaskDO getTaskByCode(String code) {
        return taskMapper.selectByCode(code);
    }
    @Override
@@ -160,6 +444,20 @@
    @Override
    public PageResult<MesProTaskDO> getPendingFeedbackPage(MesProTaskPageReqVO pageReqVO) {
        return taskMapper.selectPendingFeedbackPage(pageReqVO);
    }
    @Override
    public Map<Long, Long> getPendingFeedbackCountByWorkstation(Long assigneeUserId) {
        List<Map<String, Object>> rows = taskMapper.selectPendingFeedbackCountByWorkstation(assigneeUserId);
        Map<Long, Long> result = new HashMap<>(rows.size());
        for (Map<String, Object> row : rows) {
            Object workstationId = row.get("workstation_id");
            Object count = row.get("pending_count");
            if (workstationId instanceof Number && count instanceof Number) {
                result.put(((Number) workstationId).longValue(), ((Number) count).longValue());
            }
        }
        return result;
    }
    @Override
@@ -237,4 +535,160 @@
        taskMapper.updateProducedQuantity(id, incrProducedQuantity, incrQualifyQuantity, incrUnqualifyQuantity);
    }
    @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
    public MesProTaskAutoGeneratePreviewRespVO previewAutoGenerate(MesProTaskAutoGenerateReqVO reqVO) {
        // 1. 校验工单已确认
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        // 2. 确定工艺路线:优先请求指定 → 工单关联 → 产品关联
        Long routeId = resolveRouteId(reqVO.getRouteId(), workOrder);
        MesProRouteDO route = routeService.validateRouteExists(routeId);
        // 3. 获取工序列表:优先工单工序 → 工艺路线工序
        List<MesProRouteProcessDO> routeProcesses = buildRouteProcessList(routeId, reqVO.getWorkOrderId());
        // 按序号排序
        routeProcesses.sort(java.util.Comparator.comparing(MesProRouteProcessDO::getSort, java.util.Comparator.nullsLast(java.util.Comparator.naturalOrder())));
        // 4. 查询工序名称
        java.util.Set<Long> processIds = routeProcesses.stream().map(MesProRouteProcessDO::getProcessId).collect(java.util.stream.Collectors.toSet());
        java.util.Map<Long, cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO> procMap = processService.getProcessMap(processIds);
        // 5. 组装预览
        MesProTaskAutoGeneratePreviewRespVO vo = new MesProTaskAutoGeneratePreviewRespVO()
                .setWorkOrderId(workOrder.getId())
                .setRouteId(routeId)
                .setRouteName(route.getName());
        java.util.List<MesProTaskAutoGeneratePreviewRespVO.TaskPreview> taskList = new java.util.ArrayList<>();
        for (MesProRouteProcessDO rp : routeProcesses) {
            cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO proc = procMap.get(rp.getProcessId());
            taskList.add(new MesProTaskAutoGeneratePreviewRespVO.TaskPreview()
                    .setProcessId(rp.getProcessId())
                    .setProcessName(proc != null ? proc.getName() : null)
                    .setSort(rp.getSort())
                    .setQuantity(workOrder.getQuantity())
                    .setKeyFlag(rp.getKeyFlag()));
        }
        vo.setTaskList(taskList);
        return vo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<Long> autoGenerateTasks(MesProTaskAutoGenerateReqVO reqVO) {
        scheduleGuard.lock();
        // 1. 校验工单已确认
        MesProWorkOrderDO workOrder = workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId());
        // 2. 确定工艺路线:优先请求指定 → 工单关联 → 产品关联
        Long routeId = resolveRouteId(reqVO.getRouteId(), workOrder);
        routeService.validateRouteExists(routeId);
        // 3. 校验排产数量
        workOrderService.validateScheduleQuantity(reqVO.getWorkOrderId(), workOrder.getQuantity());
        // 4. 获取工序列表:优先工单工序 → 工艺路线工序
        List<MesProRouteProcessDO> routeProcesses = buildRouteProcessList(routeId, reqVO.getWorkOrderId());
        routeProcesses.sort(java.util.Comparator.comparing(MesProRouteProcessDO::getSort, java.util.Comparator.nullsLast(java.util.Comparator.naturalOrder())));
        // 5. 查询工序信息
        java.util.Set<Long> processIds = routeProcesses.stream().map(MesProRouteProcessDO::getProcessId).collect(java.util.stream.Collectors.toSet());
        java.util.Map<Long, cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO> procMap = processService.getProcessMap(processIds);
        // 6. 为每个工序创建任务
        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 = resolveTaskItemId(rp, workOrder.getProductId());
            cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO item = itemService.validateItemExists(itemId);
            MesProTaskDO task = new MesProTaskDO()
                    .setWorkOrderId(workOrder.getId())
                    .setRouteId(routeId)
                    .setProcessId(rp.getProcessId())
                    .setItemId(itemId)
                    .setQuantity(workOrder.getQuantity())
                    .setName(buildTaskName(item, workOrder.getQuantity()))
                    .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_TASK_CODE.getCode()))
                    .setStatus(MesProTaskStatusEnum.PREPARE.getStatus())
                    .setClientId(workOrder.getClientId())
                    .setColorCode(rp.getColorCode());
            scheduleGuard.validate(workOrder, task, null);
            freezeTaskParamConfig(task);
            taskMapper.insert(task);
            taskIds.add(task.getId());
        }
        // 7. 更新工单已排产数量
        workOrderService.updateScheduledQuantity(workOrder.getId(), workOrder.getQuantity());
        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.PRODUCING.getStatus(),
                MesProTaskStatusEnum.PARTIAL_FINISHED.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 = task.getProducedQuantity() != null
                && task.getProducedQuantity().signum() > 0
                ? MesProTaskStatusEnum.PARTIAL_FINISHED.getStatus()
                : MesProTaskStatusEnum.PRODUCING.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);
        }
    }
}