huang
13 小时以前 04914a9997afbbead6f8adbb9d9c40e05b2edbd1
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -247,17 +247,11 @@
                // 4. 大理片笼设备:启动定时器逻辑处理(不涉及PLC交互,只负责逻辑处理)
                if (isLargeGlass) {
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    ScheduledFuture<?> largeGlassTask = startLargeGlassTimer(task, step, device, context);
                    if (largeGlassTask != null) {
                        registerScheduledTask(task.getTaskId(), largeGlassTask);
                        stepSummaries.add(createStepSummary(device.getDeviceName(), true, "大理片笼定时器已启动,逻辑处理中"));
                    } else {
                        stepSummaries.add(createStepSummary(device.getDeviceName(), false, "启动定时器失败"));
                        success = false;
                        failureMessage = "大理片笼设备启动定时器失败";
                        break;
                    }
                    // 等进片大车步骤真正完成后再启动大理片笼定时器,保证执行顺序为:进片大车 -> 大理片笼
                    context.getSharedData().put("largeGlassStepId", step.getId());
                    context.getSharedData().put("largeGlassDeviceId", device.getId());
                    stepSummaries.add(createStepSummary(device.getDeviceName(), true, "已创建大理片笼步骤,等待进片大车完成后启动定时器"));
                    currentOrder++;
                    continue;
                }
@@ -723,14 +717,24 @@
                                    taskStepDetailMapper.updateById(step);
                                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                                }
                                // 继续轮询MES任务/确认状态,若MES确认完成会在内部更新步骤为COMPLETED
                                pollMesForVehicle(task, step, device, context);
                                // 如果进片大车步骤在轮询后已完成,则尝试启动大理片笼定时器
                                if (TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                                    startLargeGlassTimerIfNeeded(task, context);
                                }
                                return;
                            }
                        }
                        
                        // 如果大车已经装载过玻璃(RUNNING状态),轮询MES任务/确认状态
                        if (TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
                            // 轮询MES任务/确认状态,若MES确认完成会在内部更新步骤为COMPLETED
                            pollMesForVehicle(task, step, device, context);
                            // 如果进片大车步骤在轮询后已完成,则尝试启动大理片笼定时器
                            if (TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                                startLargeGlassTimerIfNeeded(task, context);
                            }
                        } else {
                            // 如果还没有装载过玻璃,等待卧转立输出
                            if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())) {
@@ -829,7 +833,9 @@
                        // 只有在任务已开始执行(有任务记录)时才检查MES确认
                        DevicePlcVO.OperationResult mesResult = null;
                        try {
                            mesResult = handler.execute(device, "checkMesConfirm", Collections.emptyMap());
                            Map<String, Object> confirmParams = new HashMap<>();
                            confirmParams.put("_taskContext", context);
                            mesResult = handler.execute(device, "checkMesConfirm", confirmParams);
                        } catch (Exception e) {
                            log.warn("进片大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}",
                                    task.getTaskId(), device.getId(), e.getMessage());
@@ -892,6 +898,11 @@
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                        }
                        // 当进片大车步骤真正完成后,再启动大理片笼定时器,保证执行顺序
                        if (TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                            startLargeGlassTimerIfNeeded(task, context);
                        }
                    }
                } catch (Exception e) {
                    log.error("进片大车设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
@@ -917,11 +928,10 @@
                                                         TaskExecutionContext context) {
        try {
            final long MONITOR_INTERVAL_MS = 2_000; // 2秒监控一次
            log.debug("启动出片大车设备定时器: taskId={}, deviceId={}, interval={}s",
                    task.getTaskId(), device.getId(), MONITOR_INTERVAL_MS / 1000);
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
@@ -929,183 +939,242 @@
                                task.getTaskId(), device.getId());
                        return;
                    }
                    // 出片大车设备:只有在真正开始处理时才设置为RUNNING
                    // 检查是否有已处理的玻璃信息(从大理片笼来的)
                    List<String> processedGlassIds = getProcessedGlassIds(context);
                    boolean isRunning = TaskStepDetail.Status.RUNNING.name().equals(step.getStatus());
                    // 如果没有已处理玻璃,则不应主动把步骤拉到RUNNING,只保持已运行状态
                    if (CollectionUtils.isEmpty(processedGlassIds)) {
                        if (isRunning) {
                            // 已经在运行的情况下,继续轮询MES任务/确认,避免错过确认
                            DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
                            if (handler != null) {
                                Map<String, Object> logicParams = parseLogicParams(device);
                                // 先检查MES任务(如果mesSend=1,会创建任务并开始执行)
                                DevicePlcVO.OperationResult mesTaskResult = null;
                                try {
                                    mesTaskResult = handler.execute(device, "checkMesTask", Collections.emptyMap());
                                    if (mesTaskResult != null && Boolean.TRUE.equals(mesTaskResult.getSuccess())) {
                                        log.info("出片大车设备已检查MES任务并开始执行: taskId={}, deviceId={}, message={}",
                                                task.getTaskId(), device.getId(), mesTaskResult.getMessage());
                                    }
                                } catch (Exception e) {
                                    log.warn("出片大车设备检查MES任务异常: taskId={}, deviceId={}, error={}",
                                            task.getTaskId(), device.getId(), e.getMessage());
                                }
                                // 然后检查MES确认状态(只有在任务已开始执行时才检查)
                                DevicePlcVO.OperationResult mesResult = null;
                                try {
                                    mesResult = handler.execute(device, "checkMesConfirm", Collections.emptyMap());
                                } catch (Exception e) {
                                    log.warn("出片大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}",
                                            task.getTaskId(), device.getId(), e.getMessage());
                                }
                                // 更新步骤状态(大车设备保持RUNNING,直到MES确认完成或任务取消)
                                if (mesResult != null) {
                                    updateStepStatusForVehicle(task.getTaskId(), step, mesResult);
                                    boolean opSuccess = Boolean.TRUE.equals(mesResult.getSuccess());
                                    updateTaskProgress(task, step.getStepOrder(), opSuccess);
                                    if (!opSuccess) {
                                        deviceCoordinationService.syncDeviceStatus(device,
                                                DeviceCoordinationService.DeviceStatus.FAILED, context);
                                    }
                                }
                            }
                        } else {
                            // 未运行且没有已处理玻璃,保持PENDING
                            if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())
                                    && !TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                                step.setStatus(TaskStepDetail.Status.PENDING.name());
                                step.setSuccessMessage("等待大理片笼处理完成");
                                taskStepDetailMapper.updateById(step);
                                notificationService.notifyStepUpdate(task.getTaskId(), step);
                            }
                            log.debug("出片大车设备定时器:暂无已处理的玻璃信息: taskId={}, deviceId={}",
                                    task.getTaskId(), device.getId());
                        }
                        return;
                    }
                    log.debug("出片大车设备定时器检测到已处理的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                            task.getTaskId(), device.getId(), processedGlassIds.size());
                    // 需等待大理片笼完成全部玻璃的处理后再出片
                    @SuppressWarnings("unchecked")
                    List<String> initialGlassIds = (List<String>) context.getSharedData().get("initialGlassIds");
                    if (!CollectionUtils.isEmpty(initialGlassIds)
                            && !processedGlassIds.containsAll(initialGlassIds)) {
                        // 部分玻璃尚未由大理片笼处理完成,保持等待
                    final String lastMsgKey = "outboundVehicleLastMessage:" + device.getId();
                    // 1) 总目标:大理片笼产出的“应出片玻璃列表”
                    List<String> processedGlassIdsRaw = getProcessedGlassIds(context);
                    if (CollectionUtils.isEmpty(processedGlassIdsRaw)) {
                        // 尚未有大理片笼输出,保持等待
                        deviceCoordinationService.syncDeviceStatus(device,
                                DeviceCoordinationService.DeviceStatus.WAITING, context);
                        if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())) {
                        if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())
                                && !TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                            step.setStatus(TaskStepDetail.Status.PENDING.name());
                            step.setSuccessMessage("等待大理片笼处理全部玻璃后再出片");
                            step.setSuccessMessage("等待大理片笼处理完成");
                            if (step.getStartTime() == null) {
                                step.setStartTime(new Date());
                            }
                            taskStepDetailMapper.updateById(step);
                            notificationService.notifyStepUpdate(task.getTaskId(), step);
                        }
                        log.debug("出片大车等待大理片笼完成全部玻璃: taskId={}, deviceId={}, processed={}, initial={}",
                                task.getTaskId(), device.getId(), processedGlassIds.size(), initialGlassIds.size());
                        return;
                    }
                    // 执行出片操作
                    Map<String, Object> checkParams = new HashMap<>();
                    checkParams.put("glassIds", new ArrayList<>(processedGlassIds));
                    checkParams.put("_taskContext", context);
                    // 2) 已完成累积:outboundGlassIds(任务侧维护,按mesConfirm=1累加)
                    Set<String> processedSet = new LinkedHashSet<>();
                    for (String id : processedGlassIdsRaw) {
                        if (StringUtils.hasText(id)) {
                            processedSet.add(id);
                        }
                    }
                    Set<String> outboundSet = new HashSet<>();
                    for (String id : getOutboundGlassIds(context)) {
                        if (StringUtils.hasText(id)) {
                            outboundSet.add(id);
                        }
                    }
                    List<String> remaining = new ArrayList<>();
                    for (String id : processedSet) {
                        if (!outboundSet.contains(id)) {
                            remaining.add(id);
                        }
                    }
                    int total = processedSet.size();
                    int done = total - remaining.size();
                    // 3) 若已全部完成,直接收尾步骤
                    if (total > 0 && remaining.isEmpty()) {
                        if (!TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                            step.setStatus(TaskStepDetail.Status.COMPLETED.name());
                            String lastMsg = null;
                            Object lastObj = context.getSharedData().get(lastMsgKey);
                            if (lastObj != null && StringUtils.hasText(String.valueOf(lastObj))) {
                                lastMsg = String.valueOf(lastObj);
                            }
                            step.setSuccessMessage(StringUtils.hasText(lastMsg)
                                    ? String.format("出片完成:%d/%d;%s", done, total, lastMsg)
                                    : String.format("出片完成:%d/%d", done, total));
                            step.setErrorMessage(null);
                            Date now = new Date();
                            if (step.getStartTime() == null) {
                                step.setStartTime(now);
                            }
                            if (step.getEndTime() == null) {
                                step.setEndTime(now);
                            }
                            if (step.getStartTime() != null && step.getEndTime() != null) {
                                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
                            }
                            taskStepDetailMapper.updateById(step);
                            notificationService.notifyStepUpdate(task.getTaskId(), step);
                            checkAndCompleteTaskIfDone(step.getTaskId());
                        }
                        deviceCoordinationService.syncDeviceStatus(device,
                                DeviceCoordinationService.DeviceStatus.COMPLETED, context);
                        return;
                    }
                    // 4) 进入运行态(只在真正开始出片时)
                    if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
                        step.setStatus(TaskStepDetail.Status.RUNNING.name());
                        if (step.getStartTime() == null) {
                            step.setStartTime(new Date());
                        }
                    }
                    // 更新进度信息(便于前端实时展示)
                    Date now = new Date();
                    if (step.getStartTime() != null) {
                        step.setDurationMs(now.getTime() - step.getStartTime().getTime());
                    }
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
                    if (handler != null) {
                        // 将logicParams合并到checkParams中
                        Map<String, Object> logicParams = parseLogicParams(device);
                        if (logicParams != null && !logicParams.isEmpty()) {
                            checkParams.put("_logicParams", logicParams);
                        }
                        // 第一步:写入大车出片请求
                        DevicePlcVO.OperationResult feedResult = handler.execute(device, "feedGlass", checkParams);
                        if (Boolean.TRUE.equals(feedResult.getSuccess())) {
                            // 真正开始处理,设置为RUNNING
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.RUNNING, context);
                            // 步骤状态也设置为RUNNING
                            if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
                                step.setStatus(TaskStepDetail.Status.RUNNING.name());
                                if (step.getStartTime() == null) {
                                    step.setStartTime(new Date());
                    if (handler == null) {
                        log.warn("未找到出片大车handler: deviceId={}, deviceType={}", device.getId(), device.getDeviceType());
                        return;
                    }
                    Map<String, Object> logicParams = parseLogicParams(device);
                    Map<String, Object> baseParams = new HashMap<>();
                    baseParams.put("_taskContext", context);
                    if (logicParams != null && !logicParams.isEmpty()) {
                        baseParams.put("_logicParams", logicParams);
                    }
                    String latestInteractionMsg = null;
                    // 5) 先检查MES任务(mesSend=1时会把本批次玻璃ID写入currentMesBatchGlassIds)
                    DevicePlcVO.OperationResult mesTaskResult = null;
                    try {
                        mesTaskResult = handler.execute(device, "checkMesTask", new HashMap<>(baseParams));
                    } catch (Exception e) {
                        log.warn("出片大车检查MES任务异常: taskId={}, deviceId={}, error={}",
                                task.getTaskId(), device.getId(), e.getMessage());
                    }
                    if (mesTaskResult != null && StringUtils.hasText(mesTaskResult.getMessage())) {
                        latestInteractionMsg = mesTaskResult.getMessage();
                    }
                    // 从checkMesTask返回值中读取本批次玻璃ID(不依赖设备侧写_taskContext)
                    try {
                        if (mesTaskResult != null && mesTaskResult.getData() != null) {
                            Object batchObj = mesTaskResult.getData().get("batchGlassIds");
                            if (batchObj instanceof List) {
                                @SuppressWarnings("unchecked")
                                List<Object> raw = (List<Object>) batchObj;
                                List<String> batchIds = new ArrayList<>();
                                for (Object o : raw) {
                                    if (o != null && StringUtils.hasText(String.valueOf(o))) {
                                        batchIds.add(String.valueOf(o));
                                    }
                                }
                                taskStepDetailMapper.updateById(step);
                                notificationService.notifyStepUpdate(task.getTaskId(), step);
                            }
                            log.debug("出片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}",
                                    task.getTaskId(), device.getId(), processedGlassIds.size());
                            // 清空已处理的玻璃ID列表(已处理)
                            clearProcessedGlassIds(context);
                            // feedGlass成功后,先检查MES任务(checkMesTask)来开始执行任务
                            DevicePlcVO.OperationResult mesTaskResult = null;
                            try {
                                mesTaskResult = handler.execute(device, "checkMesTask", Collections.emptyMap());
                                if (mesTaskResult != null && Boolean.TRUE.equals(mesTaskResult.getSuccess())) {
                                    log.info("出片大车设备已检查MES任务并开始执行: taskId={}, deviceId={}, message={}",
                                            task.getTaskId(), device.getId(), mesTaskResult.getMessage());
                                if (!batchIds.isEmpty()) {
                                    context.getSharedData().put("currentMesBatchGlassIds", batchIds);
                                }
                            } catch (Exception e) {
                                log.warn("出片大车设备检查MES任务异常: taskId={}, deviceId={}, error={}",
                                        task.getTaskId(), device.getId(), e.getMessage());
                            }
                        } else {
                            // 没有数据,保持WAITING状态和PENDING步骤状态
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.WAITING, context);
                            if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())) {
                                step.setStatus(TaskStepDetail.Status.PENDING.name());
                                step.setSuccessMessage("等待中");
                                taskStepDetailMapper.updateById(step);
                                notificationService.notifyStepUpdate(task.getTaskId(), step);
                            }
                            log.debug("出片大车设备定时器执行失败: taskId={}, deviceId={}, message={}",
                                    task.getTaskId(), device.getId(), feedResult.getMessage());
                        }
                        // 第二步:检查MES确认状态(如果大车处理器支持的话)
                        // 只有在任务已开始执行(有任务记录)时才检查MES确认
                        DevicePlcVO.OperationResult mesResult = null;
                    } catch (Exception ignore) {
                        // 不影响主流程
                    }
                    boolean mesSendIsZero = false;
                    if (mesTaskResult != null && mesTaskResult.getData() != null) {
                        Object reason = mesTaskResult.getData().get("waitingReason");
                        if ("mesSend=0".equals(String.valueOf(reason))) {
                            mesSendIsZero = true;
                        }
                    } else if (mesTaskResult != null && StringUtils.hasText(mesTaskResult.getMessage())
                            && mesTaskResult.getMessage().contains("mesSend=0")) {
                        mesSendIsZero = true;
                    }
                    // 6) 若MES尚未下发任务(mesSend=0),触发一次出片请求(feedGlass)
                    if (mesSendIsZero && !remaining.isEmpty()) {
                        Map<String, Object> feedParams = new HashMap<>(baseParams);
                        feedParams.put("glassIds", new ArrayList<>(remaining));
                        try {
                            mesResult = handler.execute(device, "checkMesConfirm", Collections.emptyMap());
                            DevicePlcVO.OperationResult feedResult = handler.execute(device, "feedGlass", feedParams);
                            if (feedResult != null && StringUtils.hasText(feedResult.getMessage())) {
                                latestInteractionMsg = feedResult.getMessage();
                            }
                            if (Boolean.TRUE.equals(feedResult.getSuccess())) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.RUNNING, context);
                            } else {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.WAITING, context);
                            }
                        } catch (Exception e) {
                            log.warn("出片大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}",
                            log.warn("出片大车触发feedGlass异常: taskId={}, deviceId={}, error={}",
                                    task.getTaskId(), device.getId(), e.getMessage());
                        }
                        // 更新步骤状态(大车设备保持RUNNING,直到MES确认完成或任务取消)
                        if (mesResult != null) {
                            updateStepStatusForVehicle(task.getTaskId(), step, mesResult);
                            boolean opSuccess = Boolean.TRUE.equals(mesResult.getSuccess());
                            updateTaskProgress(task, step.getStepOrder(), opSuccess);
                            if (!opSuccess) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                        } else {
                            updateStepStatusForVehicle(task.getTaskId(), step, feedResult);
                            boolean opSuccess = Boolean.TRUE.equals(feedResult.getSuccess());
                            updateTaskProgress(task, step.getStepOrder(), opSuccess);
                            if (!opSuccess) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                    }
                    // 7) 再检查MES确认(mesConfirm=1表示本次交互完成)
                    DevicePlcVO.OperationResult mesConfirmResult = null;
                    try {
                        mesConfirmResult = handler.execute(device, "checkMesConfirm", new HashMap<>(baseParams));
                    } catch (Exception e) {
                        log.warn("出片大车检查MES确认异常: taskId={}, deviceId={}, error={}",
                                task.getTaskId(), device.getId(), e.getMessage());
                    }
                    if (mesConfirmResult != null && StringUtils.hasText(mesConfirmResult.getMessage())) {
                        // 确认提示优先级最高
                        latestInteractionMsg = mesConfirmResult.getMessage();
                    }
                    boolean interactionCompleted = false;
                    if (mesConfirmResult != null && mesConfirmResult.getData() != null) {
                        Object completedFlag = mesConfirmResult.getData().get("completed");
                        if (completedFlag instanceof Boolean) {
                            interactionCompleted = (Boolean) completedFlag;
                        } else if (completedFlag != null) {
                            interactionCompleted = "true".equalsIgnoreCase(String.valueOf(completedFlag));
                        }
                    }
                    // 8) 更新“最近一次交互提示”与步骤提示文案(避免被纯进度覆盖)
                    if (StringUtils.hasText(latestInteractionMsg)) {
                        context.getSharedData().put(lastMsgKey, latestInteractionMsg);
                    }
                    String lastMsg = null;
                    Object lastObj = context.getSharedData().get(lastMsgKey);
                    if (lastObj != null && StringUtils.hasText(String.valueOf(lastObj))) {
                        lastMsg = String.valueOf(lastObj);
                    }
                    step.setSuccessMessage(StringUtils.hasText(lastMsg)
                            ? String.format("出片进行中:%d/%d;%s", done, total, lastMsg)
                            : String.format("出片进行中:%d/%d", done, total));
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    if (interactionCompleted) {
                        // 本次交互完成:将本批次(currentMesBatchGlassIds)累加到已出片(outboundGlassIds)
                        Object batchObj = context.getSharedData().get("currentMesBatchGlassIds");
                        if (batchObj instanceof List) {
                            @SuppressWarnings("unchecked")
                            List<String> batchIds = new ArrayList<>((List<String>) batchObj);
                            // 仅累加“目标集合”内的玻璃,避免脏数据
                            List<String> filtered = new ArrayList<>();
                            for (String id : batchIds) {
                                if (StringUtils.hasText(id) && processedSet.contains(id)) {
                                    filtered.add(id);
                                }
                            }
                            addOutboundGlassIds(context, filtered);
                        }
                        // 清空本批次记录,避免下一次轮询重复使用旧批次
                        context.getSharedData().put("currentMesBatchGlassIds", new ArrayList<>());
                    }
                } catch (Exception e) {
                    log.error("出片大车设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
                }
            }, 0, MONITOR_INTERVAL_MS, TimeUnit.MILLISECONDS);
            // 在串行执行模式下,设备启动定时器时先设置为 WAITING,定时器第一次执行时再设置为 RUNNING
            // 启动时保持WAITING,后续由定时器逻辑切换为RUNNING/COMPLETED
            deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.WAITING, context);
            return future;
@@ -1208,7 +1277,17 @@
                    Long processStartTime = getProcessStartTime(context);
                    if (processStartTime == null) {
                        // 第一次检测到玻璃,记录开始处理时间
                        setProcessStartTime(context, System.currentTimeMillis());
                        long now = System.currentTimeMillis();
                        setProcessStartTime(context, now);
                        // 补齐步骤的开始时间与状态,确保前端耗时正常显示
                        if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
                            step.setStatus(TaskStepDetail.Status.RUNNING.name());
                        }
                        if (step.getStartTime() == null) {
                            step.setStartTime(new Date(now));
                        }
                        taskStepDetailMapper.updateById(step);
                        notificationService.notifyStepUpdate(task.getTaskId(), step);
                        log.debug("大理片笼设备开始处理: taskId={}, deviceId={}, glassCount={}, processTime={}s",
                                task.getTaskId(), device.getId(), loadedGlassIds.size(), processTimeSeconds);
                        return;
@@ -1234,8 +1313,21 @@
                    // 更新步骤状态
                    step.setStatus(TaskStepDetail.Status.COMPLETED.name());
                    step.setErrorMessage(null);
                    // 记录结束时间与耗时
                    if (step.getEndTime() == null) {
                        step.setEndTime(new Date());
                    }
                    if (step.getStartTime() == null) {
                        // 如果开始时间缺失,用处理开始时间或结束时间兜底
                        step.setStartTime(new Date(processStartTime));
                    }
                    if (step.getStartTime() != null && step.getEndTime() != null) {
                        step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
                    }
                    step.setOutputData(toJson(Collections.singletonMap("glassIds", loadedGlassIds)));
                    taskStepDetailMapper.updateById(step);
                    // 通知前端状态更新
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    // 大理片笼完成后尝试自动收尾整个任务
                    checkAndCompleteTaskIfDone(step.getTaskId());
                    
@@ -1290,17 +1382,16 @@
    
    /**
     * 设置已装载的玻璃ID列表
     * 对于分批出片场景,每次设置应替换为当前批次的玻璃ID
     */
    private void setLoadedGlassIds(TaskExecutionContext context, List<String> glassIds) {
        if (context != null) {
            // 累加记录,避免后续 containsAll 判断因覆盖丢失历史玻璃而回退为等待
            List<String> merged = new ArrayList<>(getLoadedGlassIds(context)); // 确保可变
            // 替换为当前批次的玻璃ID
            List<String> currentBatch = new ArrayList<>();
            if (glassIds != null) {
                merged.addAll(glassIds);
                currentBatch.addAll(glassIds);
            }
            // 去重
            List<String> distinct = merged.stream().distinct().collect(java.util.stream.Collectors.toList());
            context.getSharedData().put("loadedGlassIds", distinct);
            context.getSharedData().put("loadedGlassIds", currentBatch);
        }
    }
    
@@ -1343,6 +1434,33 @@
    private void clearProcessedGlassIds(TaskExecutionContext context) {
        if (context != null) {
            context.getSharedData().put("processedGlassIds", new ArrayList<>());
        }
    }
    /**
     * 获取已出片的玻璃ID列表
     */
    @SuppressWarnings("unchecked")
    private List<String> getOutboundGlassIds(TaskExecutionContext context) {
        if (context == null) {
            return Collections.emptyList();
        }
        Object glassIds = context.getSharedData().get("outboundGlassIds");
        if (glassIds instanceof List) {
            return new ArrayList<>((List<String>) glassIds);
        }
        return Collections.emptyList();
    }
    /**
     * 添加已出片的玻璃ID列表
     */
    private void addOutboundGlassIds(TaskExecutionContext context, List<String> glassIds) {
        if (context != null && glassIds != null && !glassIds.isEmpty()) {
            List<String> existing = getOutboundGlassIds(context);
            Set<String> allOutbound = new HashSet<>(existing);
            allOutbound.addAll(glassIds);
            context.getSharedData().put("outboundGlassIds", new ArrayList<>(allOutbound));
        }
    }
    
@@ -1552,6 +1670,8 @@
            
            // 所有步骤都已完成,收尾任务
            task.setStatus(MultiDeviceTask.Status.COMPLETED.name());
            // 关键:同步进度到最终值,避免前端仍显示“3/5”这类旧进度
            task.setCurrentStep(totalSteps);
            task.setEndTime(new Date());
            multiDeviceTaskMapper.updateById(task);
            
@@ -1784,7 +1904,9 @@
            // 先检查MES任务(如果mesSend=1,会创建任务并开始执行)
            DevicePlcVO.OperationResult mesTaskResult = null;
            try {
                mesTaskResult = handler.execute(device, "checkMesTask", Collections.emptyMap());
                Map<String, Object> taskParams = new HashMap<>();
                taskParams.put("_taskContext", context);
                mesTaskResult = handler.execute(device, "checkMesTask", taskParams);
                if (mesTaskResult != null && Boolean.TRUE.equals(mesTaskResult.getSuccess())) {
                    log.info("大车设备已检查MES任务并开始执行: taskId={}, deviceId={}, message={}",
                            task.getTaskId(), device.getId(), mesTaskResult.getMessage());
@@ -1797,7 +1919,9 @@
            // 然后检查MES确认状态
            DevicePlcVO.OperationResult mesResult = null;
            try {
                mesResult = handler.execute(device, "checkMesConfirm", Collections.emptyMap());
                Map<String, Object> checkParams = new HashMap<>();
                checkParams.put("_taskContext", context);
                mesResult = handler.execute(device, "checkMesConfirm", checkParams);
            } catch (Exception e) {
                log.warn("大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}",
                        task.getTaskId(), device.getId(), e.getMessage());
@@ -2669,6 +2793,80 @@
    }
    /**
     * 当进片大车步骤完成后,如果存在大理片笼设备且尚未启动其定时器,则启动大理片笼定时器
     * 保证执行顺序为:进片大车 -> 大理片笼
     */
    private void startLargeGlassTimerIfNeeded(MultiDeviceTask task, TaskExecutionContext context) {
        if (task == null || context == null) {
            return;
        }
        // 防止重复启动
        Object startedFlag = context.getSharedData().get("largeGlassTimerStarted");
        if (startedFlag instanceof Boolean && (Boolean) startedFlag) {
            return;
        }
        try {
            // 从上下文中获取设备列表
            @SuppressWarnings("unchecked")
            List<DeviceConfig> devices = (List<DeviceConfig>) context.getSharedData().get("devices");
            if (devices == null || devices.isEmpty()) {
                return;
            }
            DeviceConfig largeGlassDevice = null;
            for (DeviceConfig device : devices) {
                if (DeviceConfig.DeviceType.LARGE_GLASS.equals(device.getDeviceType())) {
                    largeGlassDevice = device;
                    break;
                }
            }
            if (largeGlassDevice == null) {
                log.warn("未在设备列表中找到大理片笼设备: taskId={}", task.getTaskId());
                return;
            }
            // 重新加载大理片笼步骤,确保拿到最新状态(取该设备最新的一条步骤记录)
            List<TaskStepDetail> largeGlassSteps = taskStepDetailMapper.selectList(
                    Wrappers.<TaskStepDetail>lambdaQuery()
                            .eq(TaskStepDetail::getTaskId, task.getTaskId())
                            .eq(TaskStepDetail::getDeviceId, largeGlassDevice.getId())
                            .orderByDesc(TaskStepDetail::getStepOrder)
                            .last("LIMIT 1")
            );
            TaskStepDetail largeGlassStep = (largeGlassSteps != null && !largeGlassSteps.isEmpty())
                    ? largeGlassSteps.get(0)
                    : null;
            if (largeGlassStep == null) {
                log.warn("未找到大理片笼步骤记录: taskId={}, deviceId={}", task.getTaskId(), largeGlassDevice.getId());
                return;
            }
            // 如果步骤已经完成或失败,则不再启动定时器
            String status = largeGlassStep.getStatus();
            if (TaskStepDetail.Status.COMPLETED.name().equals(status)
                    || TaskStepDetail.Status.FAILED.name().equals(status)) {
                log.debug("大理片笼步骤已结束,不再启动定时器: taskId={}, stepId={}, status={}", task.getTaskId(), largeGlassStep.getId(), status);
                context.getSharedData().put("largeGlassTimerStarted", true);
                return;
            }
            ScheduledFuture<?> largeGlassTask = startLargeGlassTimer(task, largeGlassStep, largeGlassDevice, context);
            if (largeGlassTask != null) {
                registerScheduledTask(task.getTaskId(), largeGlassTask);
                context.getSharedData().put("largeGlassTimerStarted", true);
                log.info("已在进片大车完成后启动大理片笼定时器: taskId={}, largeGlassDeviceId={}, largeGlassStepId={}",
                        task.getTaskId(), largeGlassDevice.getId(), largeGlassStep.getId());
            } else {
                log.warn("在进片大车完成后启动大理片笼定时器失败: taskId={}, largeGlassDeviceId={}, largeGlassStepId={}",
                        task.getTaskId(), largeGlassDevice.getId(), largeGlassStep.getId());
            }
        } catch (Exception e) {
            log.warn("在进片大车完成后启动大理片笼定时器异常: taskId={}", task.getTaskId(), e);
        }
    }
    /**
     * 检查卧转立设备是否已完成
     * 返回true表示卧转立已完成(COMPLETED),可以判断大车是否完成
     * 返回false表示卧转立还在运行中(RUNNING)或等待中(PENDING),不应该标记大车为完成