huang
2025-12-02 628aa6a42e587e9f337e213f87f922fc2ab2af02
修改卧转立扫码到卧转立任务流转,卧转立判断玻璃超时时间
10个文件已修改
307 ■■■■ 已修改文件
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/GlassInfo.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java 152 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/MultiDeviceWorkbench.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/GlassInfo.java
@@ -86,8 +86,10 @@
    // 状态常量
    public static final class Status {
        public static final String ACTIVE = "ACTIVE";      // 活跃
        public static final String ACTIVE = "ACTIVE";      // 兼容旧数据
        public static final String ARCHIVED = "ARCHIVED";  // 已归档
        public static final String PENDING = "PENDING";    // 待卧转立处理
        public static final String PROCESSED = "PROCESSED"; // 已处理
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java
@@ -70,5 +70,10 @@
     * @return 玻璃ID列表
     */
    List<String> getRecentScannedGlassIds(Integer minutesAgo, Integer maxCount, String workLine);
    /**
     * 批量更新玻璃状态
     */
    boolean updateGlassStatus(List<String> glassIds, String status);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java
@@ -1,12 +1,14 @@
package com.mes.device.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mes.device.entity.GlassInfo;
import com.mes.device.mapper.DeviceGlassInfoMapper;
import com.mes.device.service.GlassInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.Date;
@@ -100,8 +102,35 @@
            GlassInfo existing = baseMapper.selectByGlassId(glassInfo.getGlassId());
            if (existing != null) {
                glassInfo.setId(existing.getId());
                // 保留原始创建信息
                if (glassInfo.getCreatedTime() == null) {
                    glassInfo.setCreatedTime(existing.getCreatedTime());
                }
                if (glassInfo.getCreatedBy() == null) {
                    glassInfo.setCreatedBy(existing.getCreatedBy());
                }
                // 更新为当前时间
                if (glassInfo.getUpdatedTime() == null) {
                    glassInfo.setUpdatedTime(new Date());
                }
                if (glassInfo.getUpdatedBy() == null) {
                    glassInfo.setUpdatedBy("system");
                }
                return updateById(glassInfo);
            } else {
                Date now = new Date();
                if (glassInfo.getCreatedTime() == null) {
                    glassInfo.setCreatedTime(now);
                }
                if (glassInfo.getUpdatedTime() == null) {
                    glassInfo.setUpdatedTime(now);
                }
                if (glassInfo.getCreatedBy() == null) {
                    glassInfo.setCreatedBy("system");
                }
                if (glassInfo.getUpdatedBy() == null) {
                    glassInfo.setUpdatedBy("system");
                }
                return save(glassInfo);
            }
        } catch (Exception e) {
@@ -136,7 +165,7 @@
            Date timeThreshold = new Date(System.currentTimeMillis() - minutes * 60 * 1000L);
            
            LambdaQueryWrapper<GlassInfo> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(GlassInfo::getStatus, GlassInfo.Status.ACTIVE)
            wrapper.eq(GlassInfo::getStatus, GlassInfo.Status.PENDING)
                   .ge(GlassInfo::getCreatedTime, timeThreshold)
                   .orderByDesc(GlassInfo::getCreatedTime)
                   .last("LIMIT " + limit);
@@ -160,5 +189,24 @@
            return Collections.emptyList();
        }
    }
    @Override
    public boolean updateGlassStatus(List<String> glassIds, String status) {
        if (CollectionUtils.isEmpty(glassIds) || status == null) {
            return true;
        }
        try {
            LambdaUpdateWrapper<GlassInfo> wrapper = new LambdaUpdateWrapper<>();
            wrapper.in(GlassInfo::getGlassId, glassIds);
            GlassInfo update = new GlassInfo();
            update.setStatus(status);
            update.setUpdatedTime(new Date());
            update.setUpdatedBy("system");
            return this.update(update, wrapper);
        } catch (Exception e) {
            log.error("批量更新玻璃状态失败, glassIds={}, status={}", glassIds, status, e);
            return false;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
@@ -18,6 +18,7 @@
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -273,10 +274,15 @@
        if (height != null) {
            glassInfo.setGlassLength(height); // 长
        }
        glassInfo.setStatus(GlassInfo.Status.ACTIVE);
        glassInfo.setStatus(GlassInfo.Status.PENDING);
        if (workLine != null) {
            glassInfo.setDescription("workLine=" + workLine);
        }
        Date now = new Date();
        glassInfo.setCreatedTime(now);
        glassInfo.setUpdatedTime(now);
        glassInfo.setCreatedBy("system");
        glassInfo.setUpdatedBy("system");
        return glassInfo;
    }
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java
@@ -83,7 +83,9 @@
            switch (operation) {
                case "checkAndProcess":
                case "process":
                    return handleCheckAndProcess(deviceConfig, config, logicParams);
                    // 这里必须把 params 传进去,以便在多设备任务流程中
                    // 能够通过 _taskContext 将卧转立输出的玻璃ID写入任务上下文
                    return handleCheckAndProcess(deviceConfig, config, logicParams, params);
                case "startMonitor":
                    return handleStartMonitor(deviceConfig, config, logicParams);
                case "stopMonitor":
@@ -109,7 +111,8 @@
    private DevicePlcVO.OperationResult handleCheckAndProcess(
            DeviceConfig deviceConfig,
            WorkstationLogicConfig config,
            Map<String, Object> logicParams) {
            Map<String, Object> logicParams,
            Map<String, Object> params) {
        
        String deviceId = deviceConfig.getDeviceId();
        EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
@@ -129,9 +132,13 @@
            log.info("查询到最近扫码的玻璃: deviceId={}, count={}", 
                    deviceId, recentGlasses.size());
            // 2. 更新缓冲队列和最后扫码时间
            updateBuffer(deviceId, recentGlasses);
            lastScanTime.put(deviceId, new AtomicLong(System.currentTimeMillis()));
            // 2. 更新缓冲队列;仅在有“新玻璃”加入缓冲时才更新最后扫码时间
            boolean hasNewGlass = updateBuffer(deviceId, recentGlasses);
            if (hasNewGlass) {
                lastScanTime
                        .computeIfAbsent(deviceId, k -> new AtomicLong())
                        .set(System.currentTimeMillis());
            }
            // 3. 检查是否需要立即处理(容量已满或30s内无新玻璃)
            List<GlassBufferItem> buffer = glassBuffer.get(deviceId);
@@ -162,8 +169,34 @@
                return writeResult;
            }
            // 7. 从缓冲队列中移除已处理的玻璃
            // 卧转立批次已成功写入PLC,将本批次玻璃ID写入任务上下文,供大车进片使用
            try {
                if (params != null) {
                    Object ctxObj = params.get("_taskContext");
                    if (ctxObj instanceof com.mes.task.model.TaskExecutionContext) {
                        com.mes.task.model.TaskExecutionContext ctx =
                                (com.mes.task.model.TaskExecutionContext) ctxObj;
                        List<String> batchGlassIds = batch.stream()
                                .map(GlassInfo::getGlassId)
                                .filter(Objects::nonNull)
                                .collect(Collectors.toList());
                        if (!batchGlassIds.isEmpty()) {
                            ctx.getSharedData().put("transferReadyGlassIds",
                                    new java.util.ArrayList<>(batchGlassIds));
                            log.info("卧转立已输出批次玻璃到任务上下文: deviceId={}, glassIds={}",
                                    deviceConfig.getId(), batchGlassIds);
                        }
                    }
                }
            } catch (Exception e) {
                log.warn("卧转立写入任务上下文transferReadyGlassIds失败: deviceId={}", deviceConfig.getId(), e);
            }
            // 7. 从缓冲队列中移除已处理的玻璃并更新状态
            removeProcessedGlasses(deviceId, batch);
            glassInfoService.updateGlassStatus(
                    batch.stream().map(GlassInfo::getGlassId).collect(Collectors.toList()),
                    GlassInfo.Status.PROCESSED);
            String msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s", 
                    batch.size(), 
@@ -197,7 +230,7 @@
            Date twoMinutesAgo = new Date(System.currentTimeMillis() - 120000);
            
            LambdaQueryWrapper<GlassInfo> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(GlassInfo::getStatus, GlassInfo.Status.ACTIVE)
            wrapper.in(GlassInfo::getStatus, GlassInfo.Status.PENDING, GlassInfo.Status.ACTIVE)
                   .ge(GlassInfo::getCreatedTime, twoMinutesAgo)
                   .orderByDesc(GlassInfo::getCreatedTime)
                   .last("LIMIT 20"); // 限制查询数量,避免过多
@@ -223,8 +256,9 @@
    /**
     * 更新缓冲队列
     * @return 是否有新的玻璃被加入缓冲(用于判断是否刷新 lastScanTime)
     */
    private void updateBuffer(String deviceId, List<GlassInfo> newGlasses) {
    private boolean updateBuffer(String deviceId, List<GlassInfo> newGlasses) {
        List<GlassBufferItem> buffer = glassBuffer.computeIfAbsent(
                deviceId, k -> new CopyOnWriteArrayList<>());
        
@@ -232,13 +266,16 @@
                .map(item -> item.glassInfo.getGlassId())
                .collect(Collectors.toSet());
        
        boolean hasNewGlass = false;
        for (GlassInfo glass : newGlasses) {
            if (!existingIds.contains(glass.getGlassId())) {
                buffer.add(new GlassBufferItem(glass, System.currentTimeMillis()));
                hasNewGlass = true;
                log.debug("添加玻璃到缓冲队列: deviceId={}, glassId={}", 
                        deviceId, glass.getGlassId());
            }
        }
        return hasNewGlass;
    }
    /**
@@ -375,7 +412,8 @@
        // 启动监控任务
        ScheduledFuture<?> future = monitorExecutor.scheduleWithFixedDelay(() -> {
            try {
                handleCheckAndProcess(deviceConfig, config, logicParams);
                // 监控任务不在多设备任务上下文中运行,这里不需要传入 params/_taskContext
                handleCheckAndProcess(deviceConfig, config, logicParams, null);
            } catch (Exception e) {
                log.error("监控任务执行异常: deviceId={}", deviceId, e);
            }
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -165,6 +165,12 @@
                    log.info("检测到扫码设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}", 
                            device.getId(), device.getDeviceType(), device.getDeviceName());
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    // 设置步骤为运行状态,并设置开始时间
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    ScheduledFuture<?> scannerTask = startScannerTimer(task, step, device, context);
                    if (scannerTask != null) {
                        registerScheduledTask(task.getTaskId(), scannerTask);
@@ -187,6 +193,12 @@
                    log.info("检测到卧转立设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}", 
                            device.getId(), device.getDeviceType(), device.getDeviceName());
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    // 设置步骤为运行状态,并设置开始时间
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    ScheduledFuture<?> transferTask = startTransferTimer(task, step, device, context);
                    if (transferTask != null) {
                        registerScheduledTask(task.getTaskId(), transferTask);
@@ -209,6 +221,12 @@
                    boolean isInboundVehicle = currentLoadVehicleIndex == 1; // 第一个大车是进片大车
                    
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    // 设置步骤为运行状态,并设置开始时间
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    ScheduledFuture<?> vehicleTask;
                    if (isInboundVehicle) {
                        // 进片大车:监控容量,动态判断
@@ -242,6 +260,12 @@
                // 4. 大理片笼设备:启动定时器逻辑处理(不涉及PLC交互,只负责逻辑处理)
                if (isLargeGlass) {
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    // 设置步骤为运行状态,并设置开始时间
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    ScheduledFuture<?> largeGlassTask = startLargeGlassTimer(task, step, device, context);
                    if (largeGlassTask != null) {
                        registerScheduledTask(task.getTaskId(), largeGlassTask);
@@ -392,6 +416,17 @@
                        log.info("卧转立扫码定时器完成: taskId={}, deviceId={}, processed={}/{}, success={}, fail={}",
                                task.getTaskId(), device.getId(), processedCount.get(), glassIds.size(),
                                successCount.get(), failCount.get());
                        // 若之前未出现失败,再将状态置为完成
                        boolean alreadyFailed = TaskStepDetail.Status.FAILED.name().equals(step.getStatus());
                        if (!alreadyFailed) {
                            step.setStatus(TaskStepDetail.Status.COMPLETED.name());
                            step.setSuccessMessage(String.format("已完成扫描: 成功=%d, 失败=%d", successCount.get(), failCount.get()));
                            if (step.getEndTime() == null) {
                                step.setEndTime(new Date());
                            }
                            taskStepDetailMapper.updateById(step);
                            notificationService.notifyStepUpdate(task.getTaskId(), step);
                        }
                        deviceCoordinationService.syncDeviceStatus(device,
                                DeviceCoordinationService.DeviceStatus.COMPLETED, context);
                        return;
@@ -492,15 +527,21 @@
                    if (handler != null) {
                        DevicePlcVO.OperationResult result = handler.execute(device, "checkAndProcess", params);
                        
                        // 更新步骤状态
                        updateStepStatus(step, result);
                        // 更新步骤状态(区分等待中和真正完成)
                        updateStepStatusForTransfer(step, result);
                        // 通知步骤更新(让前端实时看到步骤状态)
                        notificationService.notifyStepUpdate(task.getTaskId(), step);
                        boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
                        updateTaskProgress(task, step.getStepOrder(), opSuccess);
                        if (opSuccess) {
                            log.debug("卧转立设备定时器执行成功: taskId={}, deviceId={}, message={}",
                                    task.getTaskId(), device.getId(), result.getMessage());
                            String message = result.getMessage();
                            if (message != null && message.contains("批次已写入PLC")) {
                                log.info("卧转立设备定时器执行成功(已写入PLC): taskId={}, deviceId={}, message={}",
                                        task.getTaskId(), device.getId(), message);
                            } else {
                                log.debug("卧转立设备定时器等待中: taskId={}, deviceId={}, message={}",
                                        task.getTaskId(), device.getId(), message);
                            }
                        } else {
                            log.warn("卧转立设备定时器执行失败: taskId={}, deviceId={}, message={}",
                                    task.getTaskId(), device.getId(), result.getMessage());
@@ -544,28 +585,27 @@
                                task.getTaskId(), device.getId());
                        return;
                    }
                    // 检查是否有已扫描的玻璃信息
                    List<String> scannedGlassIds = getScannedGlassIds(context);
                    if (CollectionUtils.isEmpty(scannedGlassIds)) {
                        // 没有已扫描的玻璃,确保卧转立扫码继续运行
                        setScannerPause(context, false);
                    // 检查是否有卧转立主体已输出、准备上大车的玻璃信息
                    List<String> readyGlassIds = getTransferReadyGlassIds(context);
                    if (CollectionUtils.isEmpty(readyGlassIds)) {
                        // 没有卧转立输出的玻璃,继续等待
                        return;
                    }
                    
                    // 如果玻璃ID数量没有变化,说明没有新的玻璃,继续等待
                    int currentCount = scannedGlassIds.size();
                    int currentCount = readyGlassIds.size();
                    if (currentCount == lastProcessedCount.get()) {
                        log.debug("大车设备定时器:玻璃ID数量未变化,继续等待: taskId={}, deviceId={}, count={}",
                                task.getTaskId(), device.getId(), currentCount);
                        return;
                    }
                    
                    log.info("进片大车设备定时器检测到新的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                    log.info("进片大车设备定时器检测到卧转立输出的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                            task.getTaskId(), device.getId(), currentCount);
                    
                    // 检查容量
                    Map<String, Object> checkParams = new HashMap<>();
                    checkParams.put("glassIds", new ArrayList<>(scannedGlassIds));
                    checkParams.put("glassIds", new ArrayList<>(readyGlassIds));
                    checkParams.put("_taskContext", context);
                    
                    DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
@@ -579,19 +619,18 @@
                        
                        if (Boolean.TRUE.equals(result.getSuccess())) {
                            log.info("进片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}",
                                    task.getTaskId(), device.getId(), scannedGlassIds.size());
                                    task.getTaskId(), device.getId(), readyGlassIds.size());
                            // 将已装载的玻璃ID保存到共享数据中(供大理片笼使用)
                            setLoadedGlassIds(context, new ArrayList<>(scannedGlassIds));
                            // 清空已扫描的玻璃ID列表(已处理)
                            clearScannedGlassIds(context);
                            setLoadedGlassIds(context, new ArrayList<>(readyGlassIds));
                            // 清空卧转立输出的玻璃ID列表(已处理)
                            clearTransferReadyGlassIds(context);
                            lastProcessedCount.set(0);
                            // 确保卧转立扫码继续运行
                            setScannerPause(context, false);
                        } else {
                            // 装不下,通知卧转立扫码暂停
                            log.warn("进片大车设备定时器容量不足: taskId={}, deviceId={}, message={}, 已通知卧转立扫码暂停",
                            // 装不下,记录容量不足(是否需要影响扫码由工艺再决定)
                            log.warn("进片大车设备定时器容量不足: taskId={}, deviceId={}, message={}",
                                    task.getTaskId(), device.getId(), result.getMessage());
                            setScannerPause(context, true);
                            lastProcessedCount.set(currentCount); // 记录当前数量,避免重复检查
                        }
                        
@@ -942,6 +981,30 @@
            context.getSharedData().put("scannedGlassIds", new ArrayList<>());
        }
    }
    /**
     * 获取卧转立主体已输出、准备上大车的玻璃ID列表
     */
    @SuppressWarnings("unchecked")
    private List<String> getTransferReadyGlassIds(TaskExecutionContext context) {
        if (context == null) {
            return Collections.emptyList();
        }
        Object glassIds = context.getSharedData().get("transferReadyGlassIds");
        if (glassIds instanceof List) {
            return new ArrayList<>((List<String>) glassIds);
        }
        return Collections.emptyList();
    }
    /**
     * 清空卧转立主体已输出的玻璃ID列表
     */
    private void clearTransferReadyGlassIds(TaskExecutionContext context) {
        if (context != null) {
            context.getSharedData().put("transferReadyGlassIds", new ArrayList<>());
        }
    }
    
    /**
     * 注册定时器任务
@@ -1025,15 +1088,66 @@
        if (success) {
            // 成功时,如果有消息则保存(用于提示信息),否则清空
            step.setSuccessMessage(StringUtils.hasText(message) ? message : null);
            // 如果状态变为完成,设置结束时间
            if (TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus()) && step.getEndTime() == null) {
                step.setEndTime(new Date());
            }
        } else {
            // 失败时保存错误消息
            step.setErrorMessage(message);
            // 如果状态变为失败,设置结束时间
            if (TaskStepDetail.Status.FAILED.name().equals(step.getStatus()) && step.getEndTime() == null) {
                step.setEndTime(new Date());
            }
        }
        step.setOutputData(toJson(result));
        taskStepDetailMapper.updateById(step);
    }
    
    /**
     * 更新卧转立设备步骤状态(区分等待中和真正完成)
     */
    private void updateStepStatusForTransfer(TaskStepDetail step, DevicePlcVO.OperationResult result) {
        if (step == null || result == null) {
            return;
        }
        boolean success = Boolean.TRUE.equals(result.getSuccess());
        String message = result.getMessage();
        // 判断是否真正完成(只有写入PLC才算完成)
        boolean isRealCompleted = success && message != null && message.contains("批次已写入PLC");
        if (isRealCompleted) {
            // 真正完成:设置为完成状态,并设置结束时间
            step.setStatus(TaskStepDetail.Status.COMPLETED.name());
            step.setSuccessMessage(message);
            if (step.getEndTime() == null) {
                step.setEndTime(new Date());
            }
        } else if (success) {
            // 等待中:保持运行状态,只更新消息
            if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
                step.setStatus(TaskStepDetail.Status.RUNNING.name());
            }
            step.setSuccessMessage(message);
            // 确保开始时间已设置
            if (step.getStartTime() == null) {
                step.setStartTime(new Date());
            }
        } else {
            // 失败:设置为失败状态,并设置结束时间
            step.setStatus(TaskStepDetail.Status.FAILED.name());
            step.setErrorMessage(message);
            if (step.getEndTime() == null) {
                step.setEndTime(new Date());
            }
        }
        step.setOutputData(toJson(result));
        taskStepDetailMapper.updateById(step);
    }
    /**
     * 创建步骤摘要
     */
    private Map<String, Object> createStepSummary(String deviceName, boolean success, String message) {
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java
@@ -197,9 +197,14 @@
        if (task == null) {
            return false;
        }
        if (!MultiDeviceTask.Status.RUNNING.name().equals(task.getStatus())) {
        // 允许在 RUNNING 或 FAILED 状态下执行取消操作
        String status = task.getStatus();
        boolean cancellable = MultiDeviceTask.Status.RUNNING.name().equals(status)
                || MultiDeviceTask.Status.FAILED.name().equals(status);
        if (!cancellable) {
            return false;
        }
        // 标记任务取消并停止所有定时器
        taskExecutionEngine.requestTaskCancellation(taskId);
        task.setStatus(MultiDeviceTask.Status.CANCELLED.name());
        task.setEndTime(new Date());
mes-web/src/views/plcTest/MultiDeviceWorkbench.vue
@@ -80,6 +80,10 @@
  // 如果传入了任务信息,可以自动选中
  if (task && task.taskId) {
    selectedTaskId.value = task.taskId
    // 稍后自动打开该任务的步骤详情抽屉
    setTimeout(() => {
      monitorRef.value?.openTaskDrawer?.(task.taskId)
    }, 600)
  }
}
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
@@ -86,7 +86,7 @@
            查看详情
          </el-button>
          <el-button
            v-if="row.status === 'RUNNING'"
            v-if="row.status === 'RUNNING' || row.status === 'FAILED'"
            link
            type="danger"
            size="small"
@@ -421,6 +421,20 @@
  }
}
// 根据taskId打开任务详情抽屉(供父组件调用)
const openTaskDrawer = async (taskId) => {
  if (!taskId) return
  // 如果任务列表为空,先加载一次
  if (!tasks.value || tasks.value.length === 0) {
    await fetchTasks()
  }
  const task = tasks.value.find(t => t.taskId === taskId)
  if (!task) {
    return
  }
  await handleRowClick(task)
}
const statusType = (status) => {
  switch ((status || '').toUpperCase()) {
    case 'COMPLETED':
@@ -562,7 +576,8 @@
defineExpose({
  fetchTasks,
  connectSSE,
  disconnectSSE
  disconnectSSE,
  openTaskDrawer
})
</script>
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -110,7 +110,7 @@
const form = reactive({
  glassIntervalSeconds: 10, // 单片间隔,默认10秒
  executionInterval: 1000,
  timeoutMinutes: 30,
  timeoutMinutes: 1,
  retryCount: 3
})