huang
2025-12-01 dad0263459b30dbfa75f06dff062a0c85183517b
添加卧转立扫码设备交互逻辑,任务流转
19个文件已修改
2个文件已删除
2300 ■■■■ 已修改文件
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGroupRelationMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DeviceGroupVO.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/BaseDeviceLogicHandler.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandlerFactory.java 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/GlassStorageLogicHandler.java 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java 256 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/dto/TaskParameters.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/entity/TaskStepDetail.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java 1232 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/TaskStatusNotificationServiceImpl.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceEditDialog.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java
@@ -35,7 +35,7 @@
    @TableField("device_code")
    private String deviceCode;
    @ApiModelProperty(value = "设备类型", example = "大车设备/大理片笼/卧式缓存")
    @ApiModelProperty(value = "设备类型", example = "大车设备/大理片笼")
    @TableField("device_type")
    private String deviceType;
@@ -110,9 +110,8 @@
    public static final class DeviceType {
        public static final String LOAD_VEHICLE = "大车设备";      // 大车设备
        public static final String LARGE_GLASS = "大理片笼";      // 大理片笼
        public static final String GLASS_STORAGE = "卧式缓存";   // 卧式缓存
        public static final String WORKSTATION_SCANNER = "卧转立扫码"; // 卧转立扫码设备
        public static final String WORKSTATION_TRANSFER = "卧转立";    // 卧转立设备
        public static final String WORKSTATION_SCANNER = "卧转立扫码设备"; // 卧转立扫码设备
        public static final String WORKSTATION_TRANSFER = "卧转立设备";    // 卧转立设备
    }
    // PLC类型常量
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGroupRelationMapper.java
@@ -58,7 +58,7 @@
     */
    @Select("SELECT d.id, d.device_name as deviceName, d.device_code as deviceCode, " +
            "d.device_type as deviceType, d.plc_ip as plcIp, dgr.role, d.status, " +
            "ds.last_heartbeat as lastHeartbeat, " +
            "ds.last_heartbeat as lastHeartbeat, d.enabled, " +
            "CASE WHEN ds.status = 'ONLINE' THEN TRUE ELSE FALSE END as isOnline " +
            "FROM device_config d " +
            "INNER JOIN device_group_relation dgr ON d.id = dgr.device_id " +
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java
@@ -60,5 +60,15 @@
     * @return 是否成功
     */
    boolean batchSaveOrUpdateGlassInfo(List<GlassInfo> glassInfos);
    /**
     * 查询最近扫码的玻璃ID列表
     *
     * @param minutesAgo 查询最近多少分钟内的记录,默认2分钟
     * @param maxCount 最大返回数量,默认20
     * @param workLine 工作线号(可选,用于过滤)
     * @return 玻璃ID列表
     */
    List<String> getRecentScannedGlassIds(Integer minutesAgo, Integer maxCount, String workLine);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java
@@ -152,14 +152,8 @@
            if (CollectionUtils.isEmpty(loadedGlassIds)) {
                missingDependencies.add("大车设备未完成,缺少玻璃ID列表");
            }
        } else if (DeviceConfig.DeviceType.GLASS_STORAGE.equals(deviceType)) {
            // 玻璃存储设备需要大理片设备先完成(优先),或大车设备完成
            List<String> processedGlassIds = context.getSafeProcessedGlassIds();
            List<String> loadedGlassIds = context.getSafeLoadedGlassIds();
            if (CollectionUtils.isEmpty(processedGlassIds) && CollectionUtils.isEmpty(loadedGlassIds)) {
                missingDependencies.add("前置设备未完成,缺少玻璃ID列表");
            }
        }
        // 其他设备类型暂不需要依赖检查
        // 检查设备配置中的依赖关系(从extraParams中读取)
        Map<String, Object> deviceDependencies = getDeviceDependencies(device);
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java
@@ -9,6 +9,7 @@
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -124,5 +125,40 @@
            return false;
        }
    }
    @Override
    public List<String> getRecentScannedGlassIds(Integer minutesAgo, Integer maxCount, String workLine) {
        try {
            // 默认查询最近2分钟内的记录,最多返回20条
            int minutes = minutesAgo != null && minutesAgo > 0 ? minutesAgo : 2;
            int limit = maxCount != null && maxCount > 0 ? maxCount : 20;
            Date timeThreshold = new Date(System.currentTimeMillis() - minutes * 60 * 1000L);
            LambdaQueryWrapper<GlassInfo> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(GlassInfo::getStatus, GlassInfo.Status.ACTIVE)
                   .ge(GlassInfo::getCreatedTime, timeThreshold)
                   .orderByDesc(GlassInfo::getCreatedTime)
                   .last("LIMIT " + limit);
            // 如果指定了workLine,则过滤description
            if (workLine != null && !workLine.trim().isEmpty()) {
                wrapper.like(GlassInfo::getDescription, "workLine=" + workLine);
            }
            List<GlassInfo> recentGlasses = baseMapper.selectList(wrapper);
            // 提取玻璃ID列表
            return recentGlasses.stream()
                    .map(GlassInfo::getGlassId)
                    .filter(id -> id != null && !id.trim().isEmpty())
                    .collect(Collectors.toList());
        } catch (Exception e) {
            log.error("查询最近扫码的玻璃ID失败, minutesAgo={}, maxCount={}, workLine={}",
                    minutesAgo, maxCount, workLine, e);
            return Collections.emptyList();
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DeviceGroupVO.java
@@ -31,6 +31,7 @@
        private String status;
        private Date lastHeartbeat;
        private Boolean isOnline;
        private Boolean enabled;
    }
    /**
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/BaseDeviceLogicHandler.java
@@ -28,6 +28,15 @@
    @Override
    public DevicePlcVO.OperationResult execute(DeviceConfig deviceConfig, String operation, Map<String, Object> params) {
        try {
            // 记录参数信息(用于调试)
            if (params != null) {
                log.debug("BaseDeviceLogicHandler.execute接收参数: deviceId={}, operation={}, paramsKeys={}, params={}",
                        deviceConfig.getId(), operation, params.keySet(), params);
            } else {
                log.warn("BaseDeviceLogicHandler.execute接收参数为null: deviceId={}, operation={}",
                        deviceConfig.getId(), operation);
            }
            // 验证设备配置
            String validationError = validateLogicParams(deviceConfig);
            if (validationError != null) {
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandlerFactory.java
@@ -3,6 +3,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import java.util.HashMap;
@@ -33,8 +34,12 @@
        if (handlers != null) {
            for (DeviceLogicHandler handler : handlers) {
                String deviceType = handler.getDeviceType();
                if (deviceType != null && !deviceType.isEmpty()) {
                    handlerMap.put(deviceType, handler);
                if (StringUtils.hasText(deviceType)) {
                    String normalized = normalizeDeviceType(deviceType);
                    handlerMap.put(normalized, handler);
                    if (!normalized.equals(deviceType)) {
                        handlerMap.put(deviceType, handler);
                    }
                    log.info("注册设备逻辑处理器: {} -> {}", deviceType, handler.getClass().getSimpleName());
                }
            }
@@ -49,10 +54,18 @@
     * @return 设备逻辑处理器,如果未找到返回null
     */
    public DeviceLogicHandler getHandler(String deviceType) {
        if (deviceType == null || deviceType.isEmpty()) {
        if (!StringUtils.hasText(deviceType)) {
            return null;
        }
        return handlerMap.get(deviceType);
        DeviceLogicHandler handler = handlerMap.get(deviceType);
        if (handler != null) {
            return handler;
        }
        String normalized = normalizeDeviceType(deviceType);
        if (!normalized.equals(deviceType)) {
            handler = handlerMap.get(normalized);
        }
        return handler;
    }
    /**
@@ -73,5 +86,16 @@
    public java.util.Set<String> getSupportedDeviceTypes() {
        return handlerMap.keySet();
    }
    private String normalizeDeviceType(String deviceType) {
        if (!StringUtils.hasText(deviceType)) {
            return deviceType;
        }
        String trimmed = deviceType.trim();
        if (trimmed.endsWith("设备")) {
            return trimmed.substring(0, trimmed.length() - 2);
        }
        return trimmed;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/GlassStorageLogicHandler.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java
@@ -16,6 +16,8 @@
import com.mes.s7.enhanced.EnhancedS7Serializer;
import com.mes.s7.provider.S7SerializerProvider;
import com.mes.service.PlcDynamicDataService;
import com.mes.task.model.TaskExecutionContext;
import com.mes.interaction.workstation.scanner.handler.HorizontalScannerLogicHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -256,7 +258,23 @@
        }
        
        // 从配置中获取速度(如果有)
        Double speed = getLogicParam(logicParams, "vehicleSpeed", null);
        Object speedObj = logicParams != null ? logicParams.get("vehicleSpeed") : null;
        Double speed = null;
        if (speedObj != null) {
            if (speedObj instanceof Double) {
                speed = (Double) speedObj;
            } else if (speedObj instanceof Integer) {
                speed = ((Integer) speedObj).doubleValue();
            } else if (speedObj instanceof Number) {
                speed = ((Number) speedObj).doubleValue();
            } else {
                try {
                    speed = Double.parseDouble(String.valueOf(speedObj));
                } catch (NumberFormatException e) {
                    log.warn("无法解析vehicleSpeed: {}", speedObj);
                }
            }
        }
        if (speed != null) {
            task.setSpeed(speed);
            task.calculateEstimatedEndTime();
@@ -284,7 +302,23 @@
        // 从逻辑参数中获取配置(从 extraParams.deviceLogic 读取)
        Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", 6000);
        Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", 1000);
        // 优先使用运行时参数中的glassIntervalMs(从任务参数传入),如果没有则使用设备配置的
        Integer glassIntervalMs = null;
        if (params.containsKey("glassIntervalMs") && params.get("glassIntervalMs") != null) {
            Object intervalObj = params.get("glassIntervalMs");
            if (intervalObj instanceof Number) {
                glassIntervalMs = ((Number) intervalObj).intValue();
            } else if (intervalObj instanceof String) {
                try {
                    glassIntervalMs = Integer.parseInt((String) intervalObj);
                } catch (NumberFormatException e) {
                    // 忽略
                }
            }
        }
        if (glassIntervalMs == null) {
            glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", 1000);
        }
        Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true);
        Integer maxRetryCount = getLogicParam(logicParams, "maxRetryCount", 5);
@@ -311,11 +345,20 @@
                    .build();
        }
        if (plannedGlasses.isEmpty()) {
            // 装不下,通知卧转立扫码设备暂停
            notifyScannerPause(params, true);
            log.warn("大车设备装不下,已通知卧转立扫码暂停: deviceId={}, glassCount={}, vehicleCapacity={}",
                    deviceConfig.getId(), glassInfos.size(), vehicleCapacity);
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("当前玻璃尺寸超出车辆容量,无法装载")
                    .message("当前玻璃尺寸超出车辆容量,无法装载,已通知卧转立扫码暂停")
                    .build();
        }
        // 装得下,确保卧转立扫码继续运行
        notifyScannerPause(params, false);
        // 继续执行原有逻辑
        // 构建写入数据
        Map<String, Object> payload = new HashMap<>();
@@ -354,16 +397,13 @@
        log.info("大车设备玻璃上料: deviceId={}, glassCount={}, position={}, plannedGlassIds={}",
                deviceConfig.getId(), plcSlots, positionCode, plannedGlasses);
        if (glassIntervalMs != null && glassIntervalMs > 0) {
            try {
                Thread.sleep(glassIntervalMs);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        // 写入PLC,让大车开始装玻璃
        DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
            deviceConfig.getId(), payload, operationName);
        // 注意:glassIntervalMs 的等待应该在批次之间(在TaskExecutionEngine中处理),
        // 而不是在这里等待,因为这里等待会阻塞大车的正常装玻璃流程
        // 如果需要在写入后等待,应该在批次之间等待,让大车有时间处理当前批次的玻璃
        
        // 如果执行成功,更新位置信息到状态,并启动状态监控
        if (Boolean.TRUE.equals(result.getSuccess())) {
@@ -1754,5 +1794,25 @@
            Thread.currentThread().interrupt();
        }
    }
    /**
     * 通知卧转立扫码设备暂停或继续
     * @param params 参数,包含_taskContext引用
     * @param pause true=暂停,false=继续
     */
    private void notifyScannerPause(Map<String, Object> params, boolean pause) {
        if (params == null) {
            return;
        }
        Object contextObj = params.get("_taskContext");
        if (contextObj instanceof TaskExecutionContext) {
            TaskExecutionContext context = (TaskExecutionContext) contextObj;
            HorizontalScannerLogicHandler.setPauseFlag(context, pause);
            log.info("已通知卧转立扫码设备{}: pause={}", pause ? "暂停" : "继续", pause);
        } else {
            log.debug("未找到TaskExecutionContext,无法通知卧转立扫码设备暂停");
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
@@ -10,12 +10,15 @@
import com.mes.s7.enhanced.EnhancedS7Serializer;
import com.mes.s7.provider.S7SerializerProvider;
import com.mes.service.PlcDynamicDataService;
import com.mes.task.model.TaskExecutionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -53,60 +56,222 @@
                                                    String operation,
                                                    Map<String, Object> params,
                                                    Map<String, Object> logicParams) {
        WorkstationLogicConfig config = parseWorkstationConfig(logicParams);
        EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
        if (serializer == null) {
            return buildResult(deviceConfig, operation, false, "获取PLC序列化器失败");
            return buildResult(deviceConfig, operation, false, "获取PLC序列化器失败", null);
        }
        if ("clearPlc".equalsIgnoreCase(operation) || "reset".equalsIgnoreCase(operation)) {
            return clearPlc(deviceConfig, serializer);
        }
        WorkstationLogicConfig config = parseWorkstationConfig(logicParams);
        return executeScan(deviceConfig, config, serializer, params);
    }
    private DevicePlcVO.OperationResult executeScan(DeviceConfig deviceConfig,
                                                    WorkstationLogicConfig config,
                                                    EnhancedS7Serializer serializer,
                                                    Map<String, Object> params) {
        try {
            log.debug("卧转立扫码读取MES写区: deviceId={}, scanInterval={}ms",
                    deviceConfig.getId(), config.getScanIntervalMs());
            Map<String, Object> mesData = plcDynamicDataService.readPlcData(deviceConfig, MES_FIELDS, serializer);
            if (mesData == null || mesData.isEmpty()) {
                return buildResult(deviceConfig, operation, false, "读取MES写区失败");
            // 从参数中获取玻璃ID(定时器每次只处理一个)
            String inputGlassId = null;
            if (params != null) {
                Object glassIdObj = params.get("glassId");
                if (glassIdObj != null) {
                    inputGlassId = String.valueOf(glassIdObj).trim();
                }
            }
            Integer mesSend = parseInteger(mesData.get("mesSend"));
            if (mesSend == null || mesSend == 0) {
                return buildResult(deviceConfig, operation, true, "暂无待处理的玻璃信息");
            }
            String glassId = parseString(mesData.get("mesGlassId"));
            if (!StringUtils.hasText(glassId)) {
                return buildResult(deviceConfig, operation, false, "MES写区未提供玻璃ID");
            }
            Integer longSide = convertDimension(parseInteger(mesData.get("mesWidth")));
            Integer shortSide = convertDimension(parseInteger(mesData.get("mesHeight")));
            Integer workLine = parseInteger(mesData.get("workLine"));
            GlassInfo glassInfo = buildGlassInfo(glassId, longSide, shortSide, workLine);
            boolean saved = glassInfoService.saveOrUpdateGlassInfo(glassInfo);
            if (!saved) {
                return buildResult(deviceConfig, operation, false, "保存玻璃信息失败: " + glassId);
            }
            String msg = String.format("玻璃[%s] 尺寸[%s x %s] 已接收并入库,workLine=%s",
                    glassId,
                    longSide != null ? longSide + "mm" : "-",
                    shortSide != null ? shortSide + "mm" : "-",
                    workLine != null ? workLine : "-");
            return buildResult(deviceConfig, operation, true, msg);
            // 执行单次扫描(定时器会循环调用此方法)
            return executeSingleScan(deviceConfig, config, serializer, inputGlassId, params);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.warn("卧转立扫码等待MES数据被中断, deviceId={}", deviceConfig.getId(), e);
            return buildResult(deviceConfig, "scanOnce", false, "等待MES数据被中断", null);
        } catch (Exception e) {
            log.error("卧转立扫码处理异常, deviceId={}", deviceConfig.getId(), e);
            return buildResult(deviceConfig, operation, false, "处理异常: " + e.getMessage());
            return buildResult(deviceConfig, "scanOnce", false, "处理异常: " + e.getMessage(), null);
        }
    }
    /**
     * 执行单次扫描
     */
    private DevicePlcVO.OperationResult executeSingleScan(DeviceConfig deviceConfig,
                                                          WorkstationLogicConfig config,
                                                          EnhancedS7Serializer serializer,
                                                          String inputGlassId,
                                                          Map<String, Object> params) throws InterruptedException {
        // 1. 写入plcRequest=1和plcGlassId(如果提供了玻璃ID)
        triggerScanRequest(deviceConfig, serializer, inputGlassId);
        // 2. 等待MES回写mesSend=1以及玻璃信息
        Map<String, Object> mesData = waitForMesData(deviceConfig, serializer, config);
        if (mesData == null || mesData.isEmpty()) {
            log.error("等待MES写入玻璃信息超时: deviceId={}, timeout={}ms",
                    deviceConfig.getId(), config.getScanIntervalMs());
            return buildResult(deviceConfig, "scanOnce", false,
                    String.format("等待MES写入玻璃信息超时(%dms)", config.getScanIntervalMs()), null);
        }
        // 3. 读取MES回写的玻璃信息
        String glassId = parseString(mesData.get("mesGlassId"));
        if (!StringUtils.hasText(glassId)) {
            return buildResult(deviceConfig, "scanOnce", false, "MES写区未提供玻璃ID", null);
        }
        // 读取MES尺寸数据:mesWidth=表宽,mesHeight=长
        Integer rawWidth = parseInteger(mesData.get("mesWidth"));
        Integer rawHeight = parseInteger(mesData.get("mesHeight"));
        Integer workLine = parseInteger(mesData.get("workLine"));
        // 4. 清空plcRequest和plcGlassId(只清除PLC字段)
        clearPlcRequestFields(deviceConfig, serializer);
            // 5. 保存玻璃信息到数据库
            GlassInfo glassInfo = buildGlassInfo(glassId, rawWidth, rawHeight, workLine);
            boolean saved = glassInfoService.saveOrUpdateGlassInfo(glassInfo);
            if (!saved) {
                return buildResult(deviceConfig, "scanOnce", false, "保存玻璃信息失败: " + glassId, null);
            }
            // 6. 将扫描到的玻璃ID保存到共享数据中(供大车设备定时器读取)
            saveScannedGlassId(params, glassId);
            String msg = String.format("玻璃[%s] 尺寸[表宽:%s x 长:%s] 已接收并入库,workLine=%s",
                    glassId,
                    rawWidth != null ? rawWidth + "mm" : "-",
                    rawHeight != null ? rawHeight + "mm" : "-",
                    workLine != null ? workLine : "-");
            Map<String, Object> resultData = new HashMap<>();
            resultData.put("glassIds", Collections.singletonList(glassId));
            if (workLine != null) {
                resultData.put("workLine", workLine);
            }
            return buildResult(deviceConfig, "scanOnce", true, msg, resultData);
    }
    /**
     * 设置暂停标志(供大车设备调用)
     */
    public static void setPauseFlag(TaskExecutionContext context, boolean pause) {
        if (context != null) {
            context.getSharedData().put("scannerPause", pause);
        }
    }
    /**
     * 保存扫描到的玻璃ID到共享数据中
     */
    @SuppressWarnings("unchecked")
    private void saveScannedGlassId(Map<String, Object> params, String glassId) {
        if (params == null || !StringUtils.hasText(glassId)) {
            return;
        }
        Object contextObj = params.get("_taskContext");
        if (contextObj instanceof TaskExecutionContext) {
            TaskExecutionContext context = (TaskExecutionContext) contextObj;
            List<String> scannedGlassIds = (List<String>) context.getSharedData()
                    .computeIfAbsent("scannedGlassIds", k -> new java.util.ArrayList<>());
            if (!scannedGlassIds.contains(glassId)) {
                scannedGlassIds.add(glassId);
                log.debug("已保存扫描到的玻璃ID到共享数据: glassId={}", glassId);
            }
        }
    }
    private GlassInfo buildGlassInfo(String glassId, Integer longSide, Integer shortSide, Integer workLine) {
    private DevicePlcVO.OperationResult clearPlc(DeviceConfig deviceConfig,
                                                 EnhancedS7Serializer serializer) {
        try {
            // 只清空PLC操作区字段(plcRequest、plcGlassId),不清空MES写区字段
            Map<String, Object> resetFields = new HashMap<>();
            resetFields.put("plcRequest", 0);
            resetFields.put("plcGlassId", "");
            plcDynamicDataService.writePlcData(deviceConfig, resetFields, serializer);
            return buildResult(deviceConfig, "clearPlc", true, "已清空PLC操作区字段(保留MES写区字段)", null);
        } catch (Exception e) {
            log.error("卧转立扫码清空PLC失败, deviceId={}", deviceConfig.getId(), e);
            return buildResult(deviceConfig, "clearPlc", false, "清空PLC失败: " + e.getMessage(), null);
        }
    }
    /**
     * 触发MES请求:写入plcRequest=1和plcGlassId(如果提供了玻璃ID)
     */
    private void triggerScanRequest(DeviceConfig deviceConfig, EnhancedS7Serializer serializer, String glassId) {
        Map<String, Object> writeFields = new HashMap<>();
        writeFields.put("plcRequest", 1);
        if (StringUtils.hasText(glassId)) {
            writeFields.put("plcGlassId", glassId);
        }
        plcDynamicDataService.writePlcData(deviceConfig, writeFields, serializer);
    }
    /**
     * 清空PLC请求字段:只清除plcRequest和plcGlassId(不清除MES写区字段)
     */
    private void clearPlcRequestFields(DeviceConfig deviceConfig, EnhancedS7Serializer serializer) {
        try {
            Map<String, Object> clearFields = new HashMap<>();
            clearFields.put("plcRequest", 0);
            clearFields.put("plcGlassId", "");
            plcDynamicDataService.writePlcData(deviceConfig, clearFields, serializer);
        } catch (Exception e) {
            log.error("清空PLC请求字段失败: deviceId={}", deviceConfig.getId(), e);
            // 不清空不影响主流程,只记录错误
        }
    }
    private Map<String, Object> waitForMesData(DeviceConfig deviceConfig,
                                               EnhancedS7Serializer serializer,
                                               WorkstationLogicConfig config) throws InterruptedException {
        long timeoutMs = Math.max(config.getScanIntervalMs(), 3_000);
        long deadline = System.currentTimeMillis() + timeoutMs;
        int pollInterval = Math.max(200, Math.min(config.getScanIntervalMs() / 5, 1_000));
        while (System.currentTimeMillis() < deadline) {
            Map<String, Object> mesData = plcDynamicDataService.readPlcData(deviceConfig, MES_FIELDS, serializer);
            if (mesData != null && !mesData.isEmpty()) {
                Integer mesSend = parseInteger(mesData.get("mesSend"));
                if (mesSend != null && mesSend == 1) {
                    log.info("检测到MES已写入数据: deviceId={}, mesSend=1, mesGlassId={}, mesWidth={}, mesHeight={}, workLine={}",
                            deviceConfig.getId(),
                            mesData.get("mesGlassId"),
                            mesData.get("mesWidth"),
                            mesData.get("mesHeight"),
                            mesData.get("workLine"));
                    return mesData;
                }
            }
            Thread.sleep(pollInterval);
        }
        // 超时前最后一次尝试读取
        log.warn("等待MES数据超时: deviceId={}, timeout={}ms", deviceConfig.getId(), timeoutMs);
        Map<String, Object> lastMesData = plcDynamicDataService.readPlcData(deviceConfig, MES_FIELDS, serializer);
        if (lastMesData != null && !lastMesData.isEmpty()) {
            log.warn("超时前最后一次读取到的数据: deviceId={}, mesData={}",
                    deviceConfig.getId(), lastMesData);
        }
        return Collections.emptyMap();
    }
    private GlassInfo buildGlassInfo(String glassId, Integer width, Integer height, Integer workLine) {
        GlassInfo glassInfo = new GlassInfo();
        glassInfo.setGlassId(glassId.trim());
        if (longSide != null) {
            glassInfo.setGlassLength(longSide);
        // mesWidth=表宽 -> glassWidth, mesHeight=长 -> glassLength
        if (width != null) {
            glassInfo.setGlassWidth(width);  // 表宽
        }
        if (shortSide != null) {
            glassInfo.setGlassWidth(shortSide);
        if (height != null) {
            glassInfo.setGlassLength(height); // 长
        }
        glassInfo.setStatus(GlassInfo.Status.ACTIVE);
        if (workLine != null) {
@@ -133,17 +298,11 @@
        return value == null ? null : String.valueOf(value).trim();
    }
    private Integer convertDimension(Integer raw) {
        if (raw == null) {
            return null;
        }
        return raw / 10;
    }
    private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig,
                                                    String operation,
                                                    boolean success,
                                                    String message) {
                                                    String message,
                                                    Map<String, Object> data) {
        return DevicePlcVO.OperationResult.builder()
                .deviceId(deviceConfig.getId())
                .deviceName(deviceConfig.getDeviceName())
@@ -153,6 +312,7 @@
                .success(success)
                .message(message)
                .timestamp(LocalDateTime.now())
                .data(data)
                .build();
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/dto/TaskParameters.java
@@ -7,7 +7,6 @@
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@@ -22,8 +21,7 @@
@ApiModel(value = "TaskParameters", description = "多设备任务执行参数")
public class TaskParameters implements Serializable {
    @ApiModelProperty(value = "玻璃ID列表(保持执行顺序)", required = true)
    @NotEmpty(message = "玻璃ID列表不能为空")
    @ApiModelProperty(value = "玻璃ID列表(保持执行顺序),可为空表示由后台自动获取最近扫码的玻璃ID")
    private List<String> glassIds;
    @ApiModelProperty(value = "上大车位置编码")
@@ -41,6 +39,18 @@
    @ApiModelProperty(value = "执行间隔(毫秒)")
    private Integer executionInterval;
    @ApiModelProperty(value = "单片间隔(毫秒),多个玻璃ID时每个玻璃ID之间的间隔时间,0表示一次性全部传递")
    private Integer glassIntervalMs;
    @ApiModelProperty(value = "是否在任务开始前先触发请求(plcRequest),仅在未提供玻璃ID时生效,默认true")
    private Boolean triggerRequestFirst;
    @ApiModelProperty(value = "任务超时时间(分钟),默认30分钟")
    private Integer timeoutMinutes;
    @ApiModelProperty(value = "重试次数,默认3次")
    private Integer retryCount;
    @ApiModelProperty(value = "设备级别参数覆盖,key可以是设备类型或设备编码")
    private Map<String, Map<String, Object>> deviceOverrides;
mes-processes/mes-plcSend/src/main/java/com/mes/task/entity/TaskStepDetail.java
@@ -70,6 +70,10 @@
    @ApiModelProperty("错误信息")
    private String errorMessage;
    @TableField("success_message")
    @ApiModelProperty("成功信息")
    private String successMessage;
    @TableField("retry_count")
    @ApiModelProperty("重试次数")
    private Integer retryCount;
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -7,14 +7,16 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.DeviceGroupConfig;
import com.mes.device.service.DeviceCoordinationService;
import com.mes.device.service.DeviceInteractionService;
import com.mes.device.service.GlassInfoService;
import com.mes.device.vo.DevicePlcVO;
import com.mes.interaction.DeviceInteraction;
import com.mes.interaction.DeviceInteractionRegistry;
import com.mes.interaction.DeviceLogicHandler;
import com.mes.interaction.DeviceLogicHandlerFactory;
import com.mes.interaction.base.InteractionContext;
import com.mes.interaction.base.InteractionResult;
import com.mes.device.service.DeviceCoordinationService;
import com.mes.device.service.DeviceInteractionService;
import com.mes.task.dto.TaskParameters;
import com.mes.task.entity.MultiDeviceTask;
import com.mes.task.entity.TaskStepDetail;
@@ -23,16 +25,16 @@
import com.mes.task.model.RetryPolicy;
import com.mes.task.model.TaskExecutionContext;
import com.mes.task.model.TaskExecutionResult;
import com.mes.task.service.TaskStatusNotificationService;
import com.mes.device.vo.DevicePlcVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * 多设备任务执行引擎
@@ -45,6 +47,8 @@
    private static final Map<String, String> DEFAULT_OPERATIONS = new HashMap<>();
    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<Map<String, Object>>() {};
    private static final int SCANNER_LOOKBACK_MINUTES = 2;
    private static final int SCANNER_LOOKBACK_LIMIT = 20;
    // 执行模式常量
    private static final String EXECUTION_MODE_SERIAL = "SERIAL";
@@ -53,7 +57,8 @@
    static {
        DEFAULT_OPERATIONS.put(DeviceConfig.DeviceType.LOAD_VEHICLE, "feedGlass");
        DEFAULT_OPERATIONS.put(DeviceConfig.DeviceType.LARGE_GLASS, "processGlass");
        DEFAULT_OPERATIONS.put(DeviceConfig.DeviceType.GLASS_STORAGE, "storeGlass");
        DEFAULT_OPERATIONS.put(DeviceConfig.DeviceType.WORKSTATION_SCANNER, "scanOnce");
        DEFAULT_OPERATIONS.put(DeviceConfig.DeviceType.WORKSTATION_TRANSFER, "checkAndProcess");
    }
    private final TaskStepDetailMapper taskStepDetailMapper;
@@ -64,6 +69,8 @@
    private final DeviceCoordinationService deviceCoordinationService;
    private final TaskStatusNotificationService notificationService;
    private final ObjectMapper objectMapper;
    @Qualifier("deviceGlassInfoService")
    private final GlassInfoService glassInfoService;
    // 线程池用于并行执行
    private final ExecutorService executorService = Executors.newCachedThreadPool(r -> {
@@ -71,6 +78,18 @@
        t.setDaemon(true);
        return t;
    });
    // 定时器线程池:用于设备定时扫描
    private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(10, r -> {
        Thread t = new Thread(r, "TaskExecutionEngine-Scheduled");
        t.setDaemon(true);
        return t;
    });
    // 存储每个任务的定时器任务:taskId -> List<ScheduledFuture>
    private final Map<String, List<ScheduledFuture<?>>> taskScheduledTasks = new ConcurrentHashMap<>();
    // 记录正在运行任务的上下文,便于取消任务时访问
    private final Map<String, TaskExecutionContext> runningTaskContexts = new ConcurrentHashMap<>();
    public TaskExecutionResult execute(MultiDeviceTask task,
                                       DeviceGroupConfig groupConfig,
@@ -82,16 +101,8 @@
        }
        TaskExecutionContext context = new TaskExecutionContext(parameters);
        runningTaskContexts.put(task.getTaskId(), context);
        
        // 设备协调:检查依赖关系和执行条件
        DeviceCoordinationService.CoordinationResult coordinationResult =
            deviceCoordinationService.coordinateExecution(groupConfig, devices, context);
        if (!coordinationResult.canExecute()) {
            log.warn("设备协调失败: {}", coordinationResult.getMessage());
            return TaskExecutionResult.failure(coordinationResult.getMessage(), Collections.emptyMap());
        }
        log.info("设备协调成功: {}", coordinationResult.getMessage());
        task.setTotalSteps(devices.size());
        task.setStatus(MultiDeviceTask.Status.RUNNING.name());
        multiDeviceTaskMapper.updateById(task);
@@ -120,10 +131,133 @@
            stepSummaries = new ArrayList<>();
            success = true;
            failureMessage = null;
            for (int i = 0; i < devices.size(); i++) {
                DeviceConfig device = devices.get(i);
                int order = i + 1;
                TaskStepDetail step = createStepRecord(task, device, order);
            TaskParameters params = context.getParameters();
            boolean hasGlassIds = !CollectionUtils.isEmpty(params.getGlassIds());
            boolean triggerFirst = Boolean.TRUE.equals(params.getTriggerRequestFirst());
            int currentOrder = 1;
            // 统计大车设备数量,用于区分进片大车和出片大车
            int loadVehicleCount = 0;
            for (DeviceConfig device : devices) {
                if (DeviceConfig.DeviceType.LOAD_VEHICLE.equals(device.getDeviceType())) {
                    loadVehicleCount++;
                }
            }
            int currentLoadVehicleIndex = 0;
            for (DeviceConfig device : devices) {
                String deviceType = device.getDeviceType();
                log.info("处理设备: deviceId={}, deviceType={}, deviceName={}, WORKSTATION_SCANNER常量={}, equals={}",
                        device.getId(), deviceType, device.getDeviceName(),
                        DeviceConfig.DeviceType.WORKSTATION_SCANNER,
                        DeviceConfig.DeviceType.WORKSTATION_SCANNER.equals(deviceType));
                boolean isLoadVehicle = DeviceConfig.DeviceType.LOAD_VEHICLE.equals(deviceType);
                boolean isScanner = DeviceConfig.DeviceType.WORKSTATION_SCANNER.equals(deviceType)
                        || (deviceType != null && (deviceType.contains("扫码") || deviceType.contains("SCANNER")));
                boolean isLargeGlass = DeviceConfig.DeviceType.LARGE_GLASS.equals(deviceType);
                boolean isTransfer = DeviceConfig.DeviceType.WORKSTATION_TRANSFER.equals(deviceType);
                log.info("设备类型判断: deviceId={}, isLoadVehicle={}, isScanner={}, isLargeGlass={}, isTransfer={}",
                        device.getId(), isLoadVehicle, isScanner, isLargeGlass, isTransfer);
                // 1. 卧转立扫码设备:启动定时器扫描(每10秒处理一个玻璃ID)
                if (isScanner) {
                    log.info("检测到扫码设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}",
                            device.getId(), device.getDeviceType(), device.getDeviceName());
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    ScheduledFuture<?> scannerTask = startScannerTimer(task, step, device, context);
                    if (scannerTask != null) {
                        registerScheduledTask(task.getTaskId(), scannerTask);
                        stepSummaries.add(createStepSummary(device.getDeviceName(), true, "定时器已启动,每10秒扫描一次"));
                        log.info("扫码设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId());
                    } else {
                        log.warn("扫码设备定时器启动失败,glassIds可能为空: deviceId={}, taskId={}, contextParams={}",
                                device.getId(), task.getTaskId(), context.getParameters());
                        stepSummaries.add(createStepSummary(device.getDeviceName(), false, "启动定时器失败"));
                        success = false;
                        failureMessage = "卧转立扫码设备启动定时器失败";
                        break;
                    }
                    currentOrder++;
                    continue;
                }
                // 2. 卧转立设备:启动定时器定期检查并处理(中转设备)
                if (isTransfer) {
                    log.info("检测到卧转立设备,准备启动定时器: deviceId={}, deviceType={}, deviceName={}",
                            device.getId(), device.getDeviceType(), device.getDeviceName());
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    ScheduledFuture<?> transferTask = startTransferTimer(task, step, device, context);
                    if (transferTask != null) {
                        registerScheduledTask(task.getTaskId(), transferTask);
                        stepSummaries.add(createStepSummary(device.getDeviceName(), true, "定时器已启动,定期检查并处理玻璃批次"));
                        log.info("卧转立设备定时器启动成功: deviceId={}, taskId={}", device.getId(), task.getTaskId());
                    } else {
                        log.warn("卧转立设备定时器启动失败: deviceId={}, taskId={}", device.getId(), task.getTaskId());
                        stepSummaries.add(createStepSummary(device.getDeviceName(), false, "启动定时器失败"));
                        success = false;
                        failureMessage = "卧转立设备启动定时器失败";
                        break;
                    }
                    currentOrder++;
                    continue;
                }
                // 3. 进片大车设备:启动定时器持续监控容量(第一个大车设备)
                if (isLoadVehicle) {
                    currentLoadVehicleIndex++;
                    boolean isInboundVehicle = currentLoadVehicleIndex == 1; // 第一个大车是进片大车
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    ScheduledFuture<?> vehicleTask;
                    if (isInboundVehicle) {
                        // 进片大车:监控容量,动态判断
                        vehicleTask = startInboundVehicleTimer(task, step, device, context);
                        if (vehicleTask != null) {
                            registerScheduledTask(task.getTaskId(), vehicleTask);
                            stepSummaries.add(createStepSummary(device.getDeviceName(), true, "进片大车定时器已启动,持续监控容量"));
                        } else {
                            stepSummaries.add(createStepSummary(device.getDeviceName(), false, "启动定时器失败"));
                            success = false;
                            failureMessage = "进片大车设备启动定时器失败";
                            break;
                        }
                    } else {
                        // 出片大车:启动定时器监控出片任务
                        vehicleTask = startOutboundVehicleTimer(task, step, device, context);
                        if (vehicleTask != null) {
                            registerScheduledTask(task.getTaskId(), vehicleTask);
                            stepSummaries.add(createStepSummary(device.getDeviceName(), true, "出片大车定时器已启动,持续监控出片任务"));
                        } else {
                            stepSummaries.add(createStepSummary(device.getDeviceName(), false, "启动定时器失败"));
                            success = false;
                            failureMessage = "出片大车设备启动定时器失败";
                            break;
                        }
                    }
                    currentOrder++;
                    continue;
                }
                // 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;
                    }
                    currentOrder++;
                    continue;
                }
                // 其他设备:正常执行
                TaskStepDetail step = createStepRecord(task, device, currentOrder);
                StepResult stepResult = executeStep(task, step, device, context);
                stepSummaries.add(stepResult.toSummary());
                if (!stepResult.isSuccess()) {
@@ -131,7 +265,35 @@
                    failureMessage = stepResult.getMessage();
                    break;
                }
                currentOrder++;
            }
            // 如果所有设备都是定时器模式,任务保持运行状态,不等待完成
            // 定时器会在后台持续运行,直到手动停止或超时
            boolean hasScheduledTasks = !CollectionUtils.isEmpty(taskScheduledTasks.get(task.getTaskId()));
            if (hasScheduledTasks) {
                log.info("任务已启动所有定时器,保持运行状态: taskId={}, scheduledTasksCount={}",
                        task.getTaskId(), taskScheduledTasks.get(task.getTaskId()).size());
                // 任务保持 RUNNING 状态,定时器在后台运行
                // 不更新任务状态为 COMPLETED,让任务持续运行
                Map<String, Object> payload = new HashMap<>();
                payload.put("steps", stepSummaries);
                payload.put("groupId", groupConfig.getId());
                payload.put("deviceCount", devices.size());
                payload.put("executionMode", executionMode);
                payload.put("message", "任务已启动,定时器在后台运行中");
                // 通知任务状态(保持 RUNNING)
                notificationService.notifyTaskStatus(task);
                if (success) {
                    return TaskExecutionResult.success(payload);
                }
                return TaskExecutionResult.failure(failureMessage != null ? failureMessage : "任务执行失败", payload);
            }
            // 如果没有定时器任务,等待所有步骤完成
            // 这种情况通常不会发生,因为所有设备都是定时器模式
        }
        Map<String, Object> payload = new HashMap<>();
@@ -140,8 +302,15 @@
        payload.put("deviceCount", devices.size());
        payload.put("executionMode", executionMode);
        // 停止所有定时器任务
        stopScheduledTasks(task.getTaskId());
        boolean cancelled = isTaskCancelled(context);
        // 更新任务最终状态
        if (success) {
        if (cancelled) {
            task.setStatus(MultiDeviceTask.Status.CANCELLED.name());
            task.setErrorMessage("任务已取消");
        } else if (success) {
            task.setStatus(MultiDeviceTask.Status.COMPLETED.name());
        } else {
            task.setStatus(MultiDeviceTask.Status.FAILED.name());
@@ -157,6 +326,744 @@
            return TaskExecutionResult.success(payload);
        }
        return TaskExecutionResult.failure(failureMessage != null ? failureMessage : "任务执行失败", payload);
    }
    /**
     * 请求取消任务:停止所有定时器并标记上下文
     */
    public void requestTaskCancellation(String taskId) {
        TaskExecutionContext context = runningTaskContexts.get(taskId);
        if (context != null) {
            context.getSharedData().put("taskCancelled", true);
            log.warn("已标记任务取消: taskId={}", taskId);
        } else {
            log.warn("请求取消任务但未找到上下文: taskId={}", taskId);
        }
        stopScheduledTasks(taskId);
    }
    /**
     * 启动卧转立扫码设备定时器:每10秒处理一个玻璃ID
     */
    private ScheduledFuture<?> startScannerTimer(MultiDeviceTask task,
                                                 TaskStepDetail step,
                                                 DeviceConfig device,
                                                 TaskExecutionContext context) {
        try {
            TaskParameters params = context.getParameters();
            List<String> glassIds = params.getGlassIds();
            log.info("卧转立扫码定时器初始化: taskId={}, deviceId={}, glassIds={}, glassIdsSize={}, isEmpty={}",
                    task.getTaskId(), device.getId(), glassIds,
                    glassIds != null ? glassIds.size() : 0,
                    CollectionUtils.isEmpty(glassIds));
            if (CollectionUtils.isEmpty(glassIds)) {
                log.warn("卧转立扫码设备没有玻璃ID,定时器不启动: deviceId={}", device.getId());
                return null;
            }
            // 创建待处理玻璃ID队列
            Queue<String> glassIdQueue = new ConcurrentLinkedQueue<>(glassIds);
            AtomicInteger processedCount = new AtomicInteger(0);
            AtomicInteger successCount = new AtomicInteger(0);
            AtomicInteger failCount = new AtomicInteger(0);
            final long CYCLE_INTERVAL_MS = 10_000; // 10秒间隔
            log.info("启动卧转立扫码定时器: taskId={}, deviceId={}, glassCount={}, interval={}s, glassIds={}",
                    task.getTaskId(), device.getId(), glassIds.size(), CYCLE_INTERVAL_MS / 1000, glassIds);
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止卧转立扫码定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    // 检查是否需要暂停
                    if (shouldPauseScanner(context)) {
                        log.debug("卧转立扫码定时器暂停: taskId={}, deviceId={}", task.getTaskId(), device.getId());
                        return;
                    }
                    // 检查是否还有待处理的玻璃ID
                    String glassId = glassIdQueue.poll();
                    if (glassId == null) {
                        log.info("卧转立扫码定时器完成: taskId={}, deviceId={}, processed={}/{}, success={}, fail={}",
                                task.getTaskId(), device.getId(), processedCount.get(), glassIds.size(),
                                successCount.get(), failCount.get());
                        deviceCoordinationService.syncDeviceStatus(device,
                                DeviceCoordinationService.DeviceStatus.COMPLETED, context);
                        return;
                    }
                    int currentIndex = processedCount.incrementAndGet();
                    log.info("卧转立扫码定时器处理第{}/{}个玻璃: taskId={}, deviceId={}, glassId={}",
                            currentIndex, glassIds.size(), task.getTaskId(), device.getId(), glassId);
                    // 执行单次扫描
                    Map<String, Object> scanParams = new HashMap<>();
                    scanParams.put("glassId", glassId);
                    scanParams.put("_taskContext", context);
                    log.info("卧转立扫码定时器准备执行: taskId={}, deviceId={}, glassId={}, scanParams={}",
                            task.getTaskId(), device.getId(), glassId, scanParams);
                    DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
                    if (handler != null) {
                        // 将logicParams合并到scanParams中
                        Map<String, Object> logicParams = parseLogicParams(device);
                        if (logicParams != null && !logicParams.isEmpty()) {
                            scanParams.put("_logicParams", logicParams);
                        }
                        log.info("卧转立扫码定时器调用handler.execute: taskId={}, deviceId={}, glassId={}, operation=scanOnce, scanParamsKeys={}, scanParams={}",
                                task.getTaskId(), device.getId(), glassId, scanParams.keySet(), scanParams);
                        DevicePlcVO.OperationResult result = handler.execute(device, "scanOnce", scanParams);
                        log.info("卧转立扫码定时器handler.execute返回: taskId={}, deviceId={}, glassId={}, success={}",
                                task.getTaskId(), device.getId(), glassId, result.getSuccess());
                        if (Boolean.TRUE.equals(result.getSuccess())) {
                            successCount.incrementAndGet();
                            log.info("卧转立扫码定时器处理成功: taskId={}, deviceId={}, glassId={}",
                                    task.getTaskId(), device.getId(), glassId);
                        } else {
                            failCount.incrementAndGet();
                            log.warn("卧转立扫码定时器处理失败: taskId={}, deviceId={}, glassId={}, error={}",
                                    task.getTaskId(), device.getId(), glassId, result.getMessage());
                        }
                        // 更新步骤状态
                        updateStepStatus(step, result);
                        // 通知步骤更新(让前端实时看到步骤状态)
                        notificationService.notifyStepUpdate(task.getTaskId(), step);
                        boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
                        updateTaskProgress(task, step.getStepOrder(), opSuccess);
                        if (!opSuccess) {
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.FAILED, context);
                        }
                    }
                } catch (Exception e) {
                    log.error("卧转立扫码定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
                    failCount.incrementAndGet();
                }
            }, 0, CYCLE_INTERVAL_MS, TimeUnit.MILLISECONDS);
            deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.RUNNING, context);
            return future;
        } catch (Exception e) {
            log.error("启动卧转立扫码定时器失败: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
            return null;
        }
    }
    /**
     * 启动卧转立设备定时器:定期检查并处理玻璃批次
     */
    private ScheduledFuture<?> startTransferTimer(MultiDeviceTask task,
                                                  TaskStepDetail step,
                                                  DeviceConfig device,
                                                  TaskExecutionContext context) {
        try {
            // 从设备配置中获取监控间隔,默认5秒
            Map<String, Object> logicParams = parseLogicParams(device);
            Integer monitorIntervalMs = getLogicParam(logicParams, "monitorIntervalMs", 5_000);
            log.info("启动卧转立设备定时器: taskId={}, deviceId={}, interval={}ms",
                    task.getTaskId(), device.getId(), monitorIntervalMs);
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止卧转立设备定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    // 构建参数
                    Map<String, Object> params = new HashMap<>();
                    params.put("_taskContext", context);
                    if (logicParams != null && !logicParams.isEmpty()) {
                        params.put("_logicParams", logicParams);
                    }
                    // 调用handler执行checkAndProcess
                    DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
                    if (handler != null) {
                        DevicePlcVO.OperationResult result = handler.execute(device, "checkAndProcess", params);
                        // 更新步骤状态
                        updateStepStatus(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());
                        } else {
                            log.warn("卧转立设备定时器执行失败: taskId={}, deviceId={}, message={}",
                                    task.getTaskId(), device.getId(), result.getMessage());
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.FAILED, context);
                        }
                    }
                } catch (Exception e) {
                    log.error("卧转立设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
                }
            }, 0, monitorIntervalMs, TimeUnit.MILLISECONDS);
            deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.RUNNING, context);
            return future;
        } catch (Exception e) {
            log.error("启动卧转立设备定时器失败: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
            return null;
        }
    }
    /**
     * 启动进片大车设备定时器:持续监控容量,动态判断
     */
    private ScheduledFuture<?> startInboundVehicleTimer(MultiDeviceTask task,
                                                        TaskStepDetail step,
                                                        DeviceConfig device,
                                                        TaskExecutionContext context) {
        try {
            final long MONITOR_INTERVAL_MS = 2_000; // 2秒监控一次
            final AtomicInteger lastProcessedCount = new AtomicInteger(0);
            log.info("启动进片大车设备定时器: taskId={}, deviceId={}, interval={}s",
                    task.getTaskId(), device.getId(), MONITOR_INTERVAL_MS / 1000);
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止进片大车定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    // 检查是否有已扫描的玻璃信息
                    List<String> scannedGlassIds = getScannedGlassIds(context);
                    if (CollectionUtils.isEmpty(scannedGlassIds)) {
                        // 没有已扫描的玻璃,确保卧转立扫码继续运行
                        setScannerPause(context, false);
                        return;
                    }
                    // 如果玻璃ID数量没有变化,说明没有新的玻璃,继续等待
                    int currentCount = scannedGlassIds.size();
                    if (currentCount == lastProcessedCount.get()) {
                        log.debug("大车设备定时器:玻璃ID数量未变化,继续等待: taskId={}, deviceId={}, count={}",
                                task.getTaskId(), device.getId(), currentCount);
                        return;
                    }
                    log.info("进片大车设备定时器检测到新的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                            task.getTaskId(), device.getId(), currentCount);
                    // 检查容量
                    Map<String, Object> checkParams = new HashMap<>();
                    checkParams.put("glassIds", new ArrayList<>(scannedGlassIds));
                    checkParams.put("_taskContext", context);
                    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 result = handler.execute(device, "feedGlass", checkParams);
                        if (Boolean.TRUE.equals(result.getSuccess())) {
                            log.info("进片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}",
                                    task.getTaskId(), device.getId(), scannedGlassIds.size());
                            // 将已装载的玻璃ID保存到共享数据中(供大理片笼使用)
                            setLoadedGlassIds(context, new ArrayList<>(scannedGlassIds));
                            // 清空已扫描的玻璃ID列表(已处理)
                            clearScannedGlassIds(context);
                            lastProcessedCount.set(0);
                            // 确保卧转立扫码继续运行
                            setScannerPause(context, false);
                        } else {
                            // 装不下,通知卧转立扫码暂停
                            log.warn("进片大车设备定时器容量不足: taskId={}, deviceId={}, message={}, 已通知卧转立扫码暂停",
                                    task.getTaskId(), device.getId(), result.getMessage());
                            setScannerPause(context, true);
                            lastProcessedCount.set(currentCount); // 记录当前数量,避免重复检查
                        }
                        // 更新步骤状态
                        updateStepStatus(step, result);
                        boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
                        updateTaskProgress(task, step.getStepOrder(), opSuccess);
                        if (!opSuccess) {
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.FAILED, context);
                        }
                    }
                } catch (Exception e) {
                    log.error("进片大车设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
                }
            }, 0, MONITOR_INTERVAL_MS, TimeUnit.MILLISECONDS);
            deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.RUNNING, context);
            return future;
        } catch (Exception e) {
            log.error("启动进片大车设备定时器失败: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
            return null;
        }
    }
    /**
     * 启动出片大车设备定时器:持续监控出片任务
     */
    private ScheduledFuture<?> startOutboundVehicleTimer(MultiDeviceTask task,
                                                         TaskStepDetail step,
                                                         DeviceConfig device,
                                                         TaskExecutionContext context) {
        try {
            final long MONITOR_INTERVAL_MS = 2_000; // 2秒监控一次
            log.info("启动出片大车设备定时器: taskId={}, deviceId={}, interval={}s",
                    task.getTaskId(), device.getId(), MONITOR_INTERVAL_MS / 1000);
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止出片大车定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    // 检查是否有已处理的玻璃信息(从大理片笼来的)
                    List<String> processedGlassIds = getProcessedGlassIds(context);
                    if (CollectionUtils.isEmpty(processedGlassIds)) {
                        log.debug("出片大车设备定时器:暂无已处理的玻璃信息: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    log.info("出片大车设备定时器检测到已处理的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                            task.getTaskId(), device.getId(), processedGlassIds.size());
                    // 执行出片操作
                    Map<String, Object> checkParams = new HashMap<>();
                    checkParams.put("glassIds", new ArrayList<>(processedGlassIds));
                    checkParams.put("_taskContext", context);
                    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 result = handler.execute(device, "feedGlass", checkParams);
                        if (Boolean.TRUE.equals(result.getSuccess())) {
                            log.info("出片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}",
                                    task.getTaskId(), device.getId(), processedGlassIds.size());
                            // 清空已处理的玻璃ID列表(已处理)
                            clearProcessedGlassIds(context);
                        } else {
                            log.debug("出片大车设备定时器执行失败: taskId={}, deviceId={}, message={}",
                                    task.getTaskId(), device.getId(), result.getMessage());
                        }
                        // 更新步骤状态
                        updateStepStatus(step, result);
                        boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
                        updateTaskProgress(task, step.getStepOrder(), opSuccess);
                        if (!opSuccess) {
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.FAILED, context);
                        }
                    }
                } catch (Exception e) {
                    log.error("出片大车设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
                }
            }, 0, MONITOR_INTERVAL_MS, TimeUnit.MILLISECONDS);
            deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.RUNNING, context);
            return future;
        } catch (Exception e) {
            log.error("启动出片大车设备定时器失败: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
            return null;
        }
    }
    /**
     * 启动大理片笼设备定时器:逻辑处理(不涉及PLC交互,只负责逻辑处理,比如多久给任务汇报)
     */
    private ScheduledFuture<?> startLargeGlassTimer(MultiDeviceTask task,
                                                    TaskStepDetail step,
                                                    DeviceConfig device,
                                                    TaskExecutionContext context) {
        try {
            // 从设备配置中获取处理时间(默认30秒)
            Map<String, Object> logicParams = parseLogicParams(device);
            Integer processTimeSeconds = getLogicParam(logicParams, "processTimeSeconds", 30);
            final long PROCESS_TIME_MS = processTimeSeconds * 1000;
            log.info("启动大理片笼设备定时器: taskId={}, deviceId={}, processTime={}s",
                    task.getTaskId(), device.getId(), processTimeSeconds);
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
                        log.info("任务已取消,停止大理片笼定时器: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    // 检查是否有已装载的玻璃信息(从进片大车来的)
                    List<String> loadedGlassIds = getLoadedGlassIds(context);
                    if (CollectionUtils.isEmpty(loadedGlassIds)) {
                        log.debug("大理片笼设备定时器:暂无已装载的玻璃信息: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                        return;
                    }
                    // 检查玻璃是否已经处理完成(通过处理时间判断)
                    Long processStartTime = getProcessStartTime(context);
                    if (processStartTime == null) {
                        // 第一次检测到玻璃,记录开始处理时间
                        setProcessStartTime(context, System.currentTimeMillis());
                        log.info("大理片笼设备开始处理: taskId={}, deviceId={}, glassCount={}, processTime={}s",
                                task.getTaskId(), device.getId(), loadedGlassIds.size(), processTimeSeconds);
                        return;
                    }
                    long elapsed = System.currentTimeMillis() - processStartTime;
                    if (elapsed < PROCESS_TIME_MS) {
                        // 处理时间未到,继续等待
                        log.debug("大理片笼设备处理中: taskId={}, deviceId={}, elapsed={}s, remaining={}s",
                                task.getTaskId(), device.getId(), elapsed / 1000, (PROCESS_TIME_MS - elapsed) / 1000);
                        return;
                    }
                    // 处理时间已到,完成任务汇报
                    log.info("大理片笼设备处理完成: taskId={}, deviceId={}, glassCount={}, processTime={}s",
                            task.getTaskId(), device.getId(), loadedGlassIds.size(), processTimeSeconds);
                    // 将已处理的玻璃ID转移到已处理列表(供出片大车使用)
                    setProcessedGlassIds(context, new ArrayList<>(loadedGlassIds));
                    clearLoadedGlassIds(context);
                    clearProcessStartTime(context);
                    // 更新步骤状态
                    step.setStatus(TaskStepDetail.Status.COMPLETED.name());
                    step.setErrorMessage(null);
                    step.setOutputData(toJson(Collections.singletonMap("glassIds", loadedGlassIds)));
                    taskStepDetailMapper.updateById(step);
                } catch (Exception e) {
                    log.error("大理片笼设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
                }
            }, 0, 1_000, TimeUnit.MILLISECONDS); // 每秒检查一次
            return future;
        } catch (Exception e) {
            log.error("启动大理片笼设备定时器失败: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
            return null;
        }
    }
    /**
     * 获取逻辑参数
     */
    @SuppressWarnings("unchecked")
    private <T> T getLogicParam(Map<String, Object> logicParams, String key, T defaultValue) {
        if (logicParams == null) {
            return defaultValue;
        }
        Object value = logicParams.get(key);
        if (value == null) {
            return defaultValue;
        }
        try {
            return (T) value;
        } catch (ClassCastException e) {
            return defaultValue;
        }
    }
    /**
     * 获取已装载的玻璃ID列表
     */
    @SuppressWarnings("unchecked")
    private List<String> getLoadedGlassIds(TaskExecutionContext context) {
        if (context == null) {
            return Collections.emptyList();
        }
        Object glassIds = context.getSharedData().get("loadedGlassIds");
        if (glassIds instanceof List) {
            return new ArrayList<>((List<String>) glassIds);
        }
        return Collections.emptyList();
    }
    /**
     * 设置已装载的玻璃ID列表
     */
    private void setLoadedGlassIds(TaskExecutionContext context, List<String> glassIds) {
        if (context != null) {
            context.getSharedData().put("loadedGlassIds", new ArrayList<>(glassIds));
        }
    }
    /**
     * 清空已装载的玻璃ID列表
     */
    private void clearLoadedGlassIds(TaskExecutionContext context) {
        if (context != null) {
            context.getSharedData().put("loadedGlassIds", new ArrayList<>());
        }
    }
    /**
     * 获取已处理的玻璃ID列表
     */
    @SuppressWarnings("unchecked")
    private List<String> getProcessedGlassIds(TaskExecutionContext context) {
        if (context == null) {
            return Collections.emptyList();
        }
        Object glassIds = context.getSharedData().get("processedGlassIds");
        if (glassIds instanceof List) {
            return new ArrayList<>((List<String>) glassIds);
        }
        return Collections.emptyList();
    }
    /**
     * 设置已处理的玻璃ID列表
     */
    private void setProcessedGlassIds(TaskExecutionContext context, List<String> glassIds) {
        if (context != null) {
            context.getSharedData().put("processedGlassIds", new ArrayList<>(glassIds));
        }
    }
    /**
     * 清空已处理的玻璃ID列表
     */
    private void clearProcessedGlassIds(TaskExecutionContext context) {
        if (context != null) {
            context.getSharedData().put("processedGlassIds", new ArrayList<>());
        }
    }
    /**
     * 获取处理开始时间
     */
    private Long getProcessStartTime(TaskExecutionContext context) {
        if (context == null) {
            return null;
        }
        Object time = context.getSharedData().get("processStartTime");
        if (time instanceof Number) {
            return ((Number) time).longValue();
        }
        return null;
    }
    /**
     * 设置处理开始时间
     */
    private void setProcessStartTime(TaskExecutionContext context, long time) {
        if (context != null) {
            context.getSharedData().put("processStartTime", time);
        }
    }
    /**
     * 清空处理开始时间
     */
    private void clearProcessStartTime(TaskExecutionContext context) {
        if (context != null) {
            context.getSharedData().remove("processStartTime");
        }
    }
    /**
     * 设置卧转立扫码暂停标志
     */
    private void setScannerPause(TaskExecutionContext context, boolean pause) {
        if (context != null) {
            context.getSharedData().put("scannerPause", pause);
        }
    }
    private boolean isTaskCancelled(TaskExecutionContext context) {
        if (context == null) {
            return false;
        }
        Object cancelled = context.getSharedData().get("taskCancelled");
        return cancelled instanceof Boolean && (Boolean) cancelled;
    }
    /**
     * 检查是否需要暂停卧转立扫码
     */
    private boolean shouldPauseScanner(TaskExecutionContext context) {
        if (context == null) {
            return false;
        }
        Object pauseFlag = context.getSharedData().get("scannerPause");
        return pauseFlag instanceof Boolean && (Boolean) pauseFlag;
    }
    /**
     * 获取已扫描的玻璃ID列表
     */
    @SuppressWarnings("unchecked")
    private List<String> getScannedGlassIds(TaskExecutionContext context) {
        if (context == null) {
            return Collections.emptyList();
        }
        Object glassIds = context.getSharedData().get("scannedGlassIds");
        if (glassIds instanceof List) {
            return new ArrayList<>((List<String>) glassIds);
        }
        return Collections.emptyList();
    }
    /**
     * 清空已扫描的玻璃ID列表
     */
    private void clearScannedGlassIds(TaskExecutionContext context) {
        if (context != null) {
            context.getSharedData().put("scannedGlassIds", new ArrayList<>());
        }
    }
    /**
     * 注册定时器任务
     */
    private void registerScheduledTask(String taskId, ScheduledFuture<?> future) {
        taskScheduledTasks.computeIfAbsent(taskId, k -> new ArrayList<>()).add(future);
    }
    /**
     * 停止所有定时器任务
     */
    private void stopScheduledTasks(String taskId) {
        List<ScheduledFuture<?>> futures = taskScheduledTasks.remove(taskId);
        if (futures != null) {
            for (ScheduledFuture<?> future : futures) {
                if (future != null && !future.isCancelled()) {
                    future.cancel(false);
                }
            }
            log.info("已停止任务的所有定时器: taskId={}, count={}", taskId, futures.size());
        }
        runningTaskContexts.remove(taskId);
    }
    /**
     * 等待定时器任务完成(带超时)
     */
    private void waitForScheduledTasks(String taskId, TaskExecutionContext context) {
        // 获取任务超时时间(默认30分钟)
        TaskParameters params = context.getParameters();
        long timeoutMinutes = params != null && params.getTimeoutMinutes() != null
                ? params.getTimeoutMinutes() : 30;
        long timeoutMs = timeoutMinutes * 60 * 1000;
        long deadline = System.currentTimeMillis() + timeoutMs;
        log.info("等待定时器任务完成: taskId={}, timeout={}分钟", taskId, timeoutMinutes);
        while (System.currentTimeMillis() < deadline) {
            List<ScheduledFuture<?>> futures = taskScheduledTasks.get(taskId);
            if (futures == null || futures.isEmpty()) {
                break;
            }
            // 检查是否所有任务都已完成
            boolean allDone = true;
            for (ScheduledFuture<?> future : futures) {
                if (future != null && !future.isDone()) {
                    allDone = false;
                    break;
                }
            }
            if (allDone) {
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        log.info("定时器任务等待完成: taskId={}", taskId);
    }
    /**
     * 更新步骤状态
     */
    private void updateStepStatus(TaskStepDetail step, DevicePlcVO.OperationResult result) {
        if (step == null || result == null) {
            return;
        }
        boolean success = Boolean.TRUE.equals(result.getSuccess());
        step.setStatus(success
                ? TaskStepDetail.Status.COMPLETED.name()
                : TaskStepDetail.Status.FAILED.name());
        // 设置消息:成功时如果有消息也保存,失败时保存错误消息
        String message = result.getMessage();
        if (success) {
            // 成功时,如果有消息则保存(用于提示信息),否则清空
            step.setSuccessMessage(StringUtils.hasText(message) ? message : null);
        } else {
            // 失败时保存错误消息
            step.setErrorMessage(message);
        }
        step.setOutputData(toJson(result));
        taskStepDetailMapper.updateById(step);
    }
    /**
     * 创建步骤摘要
     */
    private Map<String, Object> createStepSummary(String deviceName, boolean success, String message) {
        Map<String, Object> summary = new HashMap<>();
        summary.put("deviceName", deviceName);
        summary.put("success", success);
        summary.put("message", message);
        return summary;
    }
    /**
     * 解析设备逻辑参数
     */
    @SuppressWarnings("unchecked")
    private Map<String, Object> parseLogicParams(DeviceConfig device) {
        String extraParams = device.getExtraParams();
        if (!StringUtils.hasText(extraParams)) {
            return Collections.emptyMap();
        }
        try {
            Map<String, Object> extraParamsMap = objectMapper.readValue(extraParams, MAP_TYPE);
            Object deviceLogic = extraParamsMap.get("deviceLogic");
            if (deviceLogic instanceof Map) {
                return (Map<String, Object>) deviceLogic;
            }
            return Collections.emptyMap();
        } catch (Exception e) {
            log.warn("解析设备逻辑参数失败: deviceId={}", device.getId(), e);
            return Collections.emptyMap();
        }
    }
    /**
@@ -346,6 +1253,74 @@
        }
    }
    /**
     * 分批执行大车设备玻璃上料(当玻璃ID数量超过6个且设置了单片间隔时)
     */
    private StepResult executeLoadVehicleWithBatches(MultiDeviceTask task,
                                                      DeviceConfig device,
                                                      int order,
                                                      TaskExecutionContext context,
                                                      List<Map<String, Object>> stepSummaries) {
        List<String> allGlassIds = context.getParameters().getGlassIds();
        Integer glassIntervalMs = context.getParameters().getGlassIntervalMs();
        int batchSize = 6; // 每批最多6个玻璃ID
        // 分批处理
        int totalBatches = (allGlassIds.size() + batchSize - 1) / batchSize;
        log.info("大车设备分批上料: deviceId={}, totalGlassIds={}, batchSize={}, totalBatches={}, glassIntervalMs={}",
                device.getId(), allGlassIds.size(), batchSize, totalBatches, glassIntervalMs);
        for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
            int startIndex = batchIndex * batchSize;
            int endIndex = Math.min(startIndex + batchSize, allGlassIds.size());
            List<String> batchGlassIds = allGlassIds.subList(startIndex, endIndex);
            // 创建临时参数,只包含当前批次的玻璃ID
            TaskParameters batchParams = new TaskParameters();
            batchParams.setGlassIds(new ArrayList<>(batchGlassIds));
            batchParams.setGlassIntervalMs(glassIntervalMs);
            batchParams.setPositionCode(context.getParameters().getPositionCode());
            batchParams.setPositionValue(context.getParameters().getPositionValue());
            // 创建临时上下文
            TaskExecutionContext batchContext = new TaskExecutionContext(batchParams);
            // 创建步骤记录
            TaskStepDetail step = createStepRecord(task, device, order);
            step.setStepName(step.getStepName() + String.format(" (批次 %d/%d)", batchIndex + 1, totalBatches));
            // 执行当前批次
            StepResult stepResult = executeStep(task, step, device, batchContext);
            stepSummaries.add(stepResult.toSummary());
            if (!stepResult.isSuccess()) {
                log.error("大车设备分批上料失败: deviceId={}, batchIndex={}/{}, error={}",
                        device.getId(), batchIndex + 1, totalBatches, stepResult.getMessage());
                return stepResult;
            }
            log.info("大车设备分批上料成功: deviceId={}, batchIndex={}/{}, glassIds={}",
                    device.getId(), batchIndex + 1, totalBatches, batchGlassIds);
            // 如果不是最后一批,等待间隔(模拟玻璃每片运动的时间)
            // 这个等待让大车有时间处理当前批次的玻璃,然后再传递下一批
            if (batchIndex < totalBatches - 1 && glassIntervalMs != null && glassIntervalMs > 0) {
                try {
                    log.info("等待单片间隔(模拟玻璃运动时间): glassIntervalMs={}ms, 大车可在此期间继续装玻璃", glassIntervalMs);
                    Thread.sleep(glassIntervalMs);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return StepResult.failure(device.getDeviceName(), "等待单片间隔时被中断");
                }
            }
        }
        // 更新上下文中的已加载玻璃ID
        context.setLoadedGlassIds(new ArrayList<>(allGlassIds));
        return StepResult.success(device.getDeviceName(), "分批上料完成,共" + totalBatches + "批");
    }
    private TaskStepDetail createStepRecord(MultiDeviceTask task, DeviceConfig device, int order) {
        TaskStepDetail step = new TaskStepDetail();
        step.setTaskId(task.getTaskId());
@@ -362,6 +1337,18 @@
                                   TaskStepDetail step,
                                   DeviceConfig device,
                                   TaskExecutionContext context) {
        DeviceCoordinationService.DependencyCheckResult dependencyResult =
                deviceCoordinationService.checkDependencies(device, context);
        if (!dependencyResult.isSatisfied()) {
            log.warn("设备依赖未满足: deviceId={}, message={}", device.getId(), dependencyResult.getMessage());
            step.setStatus(TaskStepDetail.Status.FAILED.name());
            step.setErrorMessage(dependencyResult.getMessage());
            step.setStartTime(new Date());
            step.setEndTime(new Date());
            taskStepDetailMapper.updateById(step);
            updateTaskProgress(task, step.getStepOrder(), false);
            return StepResult.failure(device.getDeviceName(), dependencyResult.getMessage());
        }
        return executeStepWithRetry(task, step, device, context, getRetryPolicy(device));
    }
@@ -384,6 +1371,10 @@
        }
        Map<String, Object> params = buildOperationParams(device, context);
        // 将context引用放入params,供设备处理器使用(用于设备协调)
        params.put("_taskContext", context);
        log.info("executeStepWithRetry构建参数: deviceId={}, deviceType={}, operation={}, paramsKeys={}, params={}",
                device.getId(), device.getDeviceType(), determineOperation(device, params), params.keySet(), params);
        step.setInputData(toJson(params));
        taskStepDetailMapper.updateById(step);
@@ -424,7 +1415,7 @@
                notificationService.notifyStepUpdate(task.getTaskId(), step);
                if (opSuccess) {
                    updateContextAfterSuccess(device, context, params);
                    updateContextAfterSuccess(device, context, params, result);
                    
                    // 同步设备状态
                    deviceCoordinationService.syncDeviceStatus(device, 
@@ -511,6 +1502,69 @@
        
        return StepResult.failure(device.getDeviceName(), 
            String.format("执行失败(已重试%d次)", retryAttempt));
    }
    /**
     * 执行一次简单的设备操作步骤(不走交互引擎),用于触发请求等场景
     */
    private StepResult executeDirectOperationStep(MultiDeviceTask task,
                                                  TaskStepDetail step,
                                                  DeviceConfig device,
                                                  TaskExecutionContext context,
                                                  String operation,
                                                  Map<String, Object> params) {
        Date startTime = new Date();
        step.setStartTime(startTime);
        step.setStatus(TaskStepDetail.Status.RUNNING.name());
        step.setRetryCount(0);
        step.setInputData(toJson(params));
        taskStepDetailMapper.updateById(step);
        try {
            DeviceCoordinationService.DependencyCheckResult dependencyResult =
                    deviceCoordinationService.checkDependencies(device, context);
            if (!dependencyResult.isSatisfied()) {
                log.warn("直接操作依赖未满足: deviceId={}, message={}", device.getId(), dependencyResult.getMessage());
                step.setStatus(TaskStepDetail.Status.FAILED.name());
                step.setErrorMessage(dependencyResult.getMessage());
                step.setEndTime(new Date());
                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
                taskStepDetailMapper.updateById(step);
                updateTaskProgress(task, step.getStepOrder(), false);
                return StepResult.failure(device.getDeviceName(), dependencyResult.getMessage());
            }
            DevicePlcVO.OperationResult result = deviceInteractionService.executeOperation(
                    device.getId(), operation, params);
            boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
            updateStepAfterOperation(step, result, opSuccess);
            updateTaskProgress(task, step.getStepOrder(), opSuccess);
            if (opSuccess) {
                updateContextAfterSuccess(device, context, params, result);
                // 简单同步设备状态为已完成
                deviceCoordinationService.syncDeviceStatus(device,
                        DeviceCoordinationService.DeviceStatus.COMPLETED, context);
                return StepResult.success(device.getDeviceName(), result.getMessage());
            } else {
                deviceCoordinationService.syncDeviceStatus(device,
                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                return StepResult.failure(device.getDeviceName(), result.getMessage());
            }
        } catch (Exception e) {
            log.error("直接设备操作异常, deviceId={}, operation={}", device.getId(), operation, e);
            step.setStatus(TaskStepDetail.Status.FAILED.name());
            step.setErrorMessage(e.getMessage());
            step.setEndTime(new Date());
            step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
            taskStepDetailMapper.updateById(step);
            updateTaskProgress(task, step.getStepOrder(), false);
            deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.FAILED, context);
            return StepResult.failure(device.getDeviceName(), e.getMessage());
        }
    }
    /**
@@ -652,7 +1706,15 @@
            step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
        }
        step.setStatus(success ? TaskStepDetail.Status.COMPLETED.name() : TaskStepDetail.Status.FAILED.name());
        step.setErrorMessage(success ? null : result.getMessage());
        // 设置消息:成功时如果有消息也保存,失败时保存错误消息
        String message = result != null ? result.getMessage() : null;
        if (success) {
            // 成功时,如果有消息则保存(用于提示信息),否则清空
            step.setErrorMessage(StringUtils.hasText(message) ? message : null);
        } else {
            // 失败时保存错误消息
            step.setErrorMessage(message);
        }
        step.setOutputData(toJson(result));
        taskStepDetailMapper.updateById(step);
    }
@@ -671,20 +1733,48 @@
    }
    private void updateTaskProgress(MultiDeviceTask task, int currentStep, boolean success) {
        task.setCurrentStep(currentStep);
        if (!success) {
            task.setStatus(MultiDeviceTask.Status.FAILED.name());
        }
        // 计算已完成的步骤数(用于进度显示)
        int completedSteps = countCompletedSteps(task.getTaskId());
        int progressStep = success
                ? completedSteps
                : Math.max(completedSteps, currentStep); // 失败时至少显示当前步骤
        LambdaUpdateWrapper<MultiDeviceTask> update = Wrappers.<MultiDeviceTask>lambdaUpdate()
                .eq(MultiDeviceTask::getId, task.getId())
                .set(MultiDeviceTask::getCurrentStep, currentStep);
                .set(MultiDeviceTask::getCurrentStep, progressStep);
        if (!success) {
            update.set(MultiDeviceTask::getStatus, MultiDeviceTask.Status.FAILED.name());
        }
        multiDeviceTaskMapper.update(null, update);
        
        // 通知任务状态更新
        // 更新任务对象的进度,用于通知
        task.setCurrentStep(progressStep);
        // 通知任务状态更新(包含进度信息)
        notificationService.notifyTaskStatus(task);
    }
    /**
     * 统计已完成的步骤数
     */
    private int countCompletedSteps(String taskId) {
        if (taskId == null) {
            return 0;
        }
        try {
            return taskStepDetailMapper.selectCount(
                Wrappers.<TaskStepDetail>lambdaQuery()
                    .eq(TaskStepDetail::getTaskId, taskId)
                    .eq(TaskStepDetail::getStatus, TaskStepDetail.Status.COMPLETED.name())
            ).intValue();
        } catch (Exception e) {
            log.warn("统计已完成步骤数失败: taskId={}", taskId, e);
            return 0;
        }
    }
    private String determineOperation(DeviceConfig device, Map<String, Object> params) {
@@ -710,6 +1800,10 @@
                if (taskParams.getPositionValue() != null) {
                    params.put("positionValue", taskParams.getPositionValue());
                }
                // 传递单片间隔配置,如果任务参数中有设置,优先使用任务参数的,否则使用设备配置的
                if (taskParams.getGlassIntervalMs() != null) {
                    params.put("glassIntervalMs", taskParams.getGlassIntervalMs());
                }
                params.put("triggerRequest", true);
                break;
            case DeviceConfig.DeviceType.LARGE_GLASS:
@@ -724,19 +1818,21 @@
                params.put("processType", taskParams.getProcessType() != null ? taskParams.getProcessType() : 1);
                params.put("triggerRequest", true);
                break;
            case DeviceConfig.DeviceType.GLASS_STORAGE:
                List<String> processed = context.getSafeProcessedGlassIds();
                if (CollectionUtils.isEmpty(processed)) {
                    processed = context.getSafeLoadedGlassIds();
            case DeviceConfig.DeviceType.WORKSTATION_SCANNER:
                // 卧转立扫码设备:从任务参数中获取玻璃ID列表,取第一个作为当前要测试的玻璃ID
                // 注意:扫码设备通常通过定时器执行,但如果通过executeStep执行,也需要传递glassId
                log.info("buildOperationParams处理扫码设备: deviceId={}, taskParams.glassIds={}, isEmpty={}",
                        device.getId(), taskParams.getGlassIds(),
                        CollectionUtils.isEmpty(taskParams.getGlassIds()));
                if (!CollectionUtils.isEmpty(taskParams.getGlassIds())) {
                    params.put("glassId", taskParams.getGlassIds().get(0));
                    params.put("glassIds", new ArrayList<>(taskParams.getGlassIds()));
                    log.info("buildOperationParams为扫码设备添加glassId: deviceId={}, glassId={}, glassIdsSize={}",
                            device.getId(), taskParams.getGlassIds().get(0), taskParams.getGlassIds().size());
                } else {
                    log.warn("buildOperationParams扫码设备glassIds为空: deviceId={}, taskParams.glassIds={}, taskParams={}",
                            device.getId(), taskParams.getGlassIds(), taskParams);
                }
                if (!CollectionUtils.isEmpty(processed)) {
                    params.put("glassId", processed.get(0));
                    params.put("glassIds", new ArrayList<>(processed));
                }
                if (taskParams.getStoragePosition() != null) {
                    params.put("storagePosition", taskParams.getStoragePosition());
                }
                params.put("triggerRequest", true);
                break;
            default:
                if (!CollectionUtils.isEmpty(taskParams.getExtra())) {
@@ -763,10 +1859,14 @@
    private void updateContextAfterSuccess(DeviceConfig device,
                                           TaskExecutionContext context,
                                           Map<String, Object> params) {
                                           Map<String, Object> params,
                                           DevicePlcVO.OperationResult result) {
        List<String> glassIds = extractGlassIds(params);
        switch (device.getDeviceType()) {
            case DeviceConfig.DeviceType.WORKSTATION_SCANNER:
                handleScannerSuccess(context, result);
                break;
            case DeviceConfig.DeviceType.LOAD_VEHICLE:
                context.setLoadedGlassIds(glassIds);
                // 数据传递:大车设备 -> 下一个设备
@@ -792,6 +1892,23 @@
        }
    }
    private void handleScannerSuccess(TaskExecutionContext context,
                                      DevicePlcVO.OperationResult result) {
        List<String> scannerGlassIds = extractGlassIdsFromResult(result);
        if (CollectionUtils.isEmpty(scannerGlassIds)) {
            String workLine = resolveWorkLineFromResult(result, context.getParameters());
            scannerGlassIds = glassInfoService.getRecentScannedGlassIds(
                    SCANNER_LOOKBACK_MINUTES, SCANNER_LOOKBACK_LIMIT, workLine);
        }
        if (!CollectionUtils.isEmpty(scannerGlassIds)) {
            context.getParameters().setGlassIds(new ArrayList<>(scannerGlassIds));
            context.setLoadedGlassIds(new ArrayList<>(scannerGlassIds));
            log.info("卧转立扫码获取到玻璃ID: {}", scannerGlassIds);
        } else {
            log.warn("卧转立扫码未获取到玻璃ID,后续设备可能无法执行");
        }
    }
    private List<String> extractGlassIds(Map<String, Object> params) {
        if (params == null) {
            return Collections.emptyList();
@@ -809,6 +1926,45 @@
        return Collections.emptyList();
    }
    @SuppressWarnings("unchecked")
    private List<String> extractGlassIdsFromResult(DevicePlcVO.OperationResult result) {
        if (result == null || result.getData() == null) {
            return Collections.emptyList();
        }
        Object data = result.getData().get("glassIds");
        if (data instanceof List) {
            List<Object> raw = (List<Object>) data;
            List<String> converted = new ArrayList<>();
            for (Object item : raw) {
                if (item != null) {
                    converted.add(String.valueOf(item));
                }
            }
            return converted;
        }
        if (data instanceof String && StringUtils.hasText((String) data)) {
            return Collections.singletonList((String) data);
        }
        return Collections.emptyList();
    }
    private String resolveWorkLineFromResult(DevicePlcVO.OperationResult result,
                                             TaskParameters parameters) {
        if (result != null && result.getData() != null) {
            Object workLine = result.getData().get("workLine");
            if (workLine != null && StringUtils.hasText(String.valueOf(workLine))) {
                return String.valueOf(workLine);
            }
        }
        if (parameters != null && !CollectionUtils.isEmpty(parameters.getExtra())) {
            Object extraWorkLine = parameters.getExtra().get("workLine");
            if (extraWorkLine != null) {
                return String.valueOf(extraWorkLine);
            }
        }
        return null;
    }
    private String toJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java
@@ -20,7 +20,6 @@
import com.mes.task.service.MultiDeviceTaskService;
import com.mes.task.service.TaskExecutionEngine;
import com.mes.task.service.TaskStatusNotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@@ -32,13 +31,13 @@
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
 * 多设备任务服务实现
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class MultiDeviceTaskServiceImpl extends ServiceImpl<MultiDeviceTaskMapper, MultiDeviceTask>
        implements MultiDeviceTaskService {
@@ -48,6 +47,21 @@
    private final TaskExecutionEngine taskExecutionEngine;
    private final TaskStatusNotificationService notificationService;
    private final ObjectMapper objectMapper;
    public MultiDeviceTaskServiceImpl(
            DeviceGroupConfigService deviceGroupConfigService,
            DeviceGroupRelationMapper deviceGroupRelationMapper,
            TaskStepDetailMapper taskStepDetailMapper,
            TaskExecutionEngine taskExecutionEngine,
            TaskStatusNotificationService notificationService,
            ObjectMapper objectMapper) {
        this.deviceGroupConfigService = deviceGroupConfigService;
        this.deviceGroupRelationMapper = deviceGroupRelationMapper;
        this.taskStepDetailMapper = taskStepDetailMapper;
        this.taskExecutionEngine = taskExecutionEngine;
        this.notificationService = notificationService;
        this.objectMapper = objectMapper;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -66,8 +80,15 @@
        }
        TaskParameters parameters = request.getParameters();
        if (parameters == null || CollectionUtils.isEmpty(parameters.getGlassIds())) {
            throw new IllegalArgumentException("至少需要配置一条玻璃ID");
        if (parameters == null) {
            parameters = new TaskParameters();
        }
        // 默认允许卧转立扫码设备在任务执行阶段获取玻璃信息
        boolean hasGlassIds = !CollectionUtils.isEmpty(parameters.getGlassIds());
        if (!hasGlassIds) {
            log.info("测试任务未提供玻璃ID,将在设备组流程中由卧转立扫码设备采集玻璃信息: groupId={}",
                    groupConfig.getId());
        }
        // 创建任务记录
@@ -113,11 +134,26 @@
            // 执行任务
            TaskExecutionResult result = taskExecutionEngine.execute(task, groupConfig, devices, parameters);
            
            // 更新任务结果
            // 检查任务数据中是否包含持续运行的标记
            Map<String, Object> resultData = result.getData();
            boolean isContinuousTask = resultData != null && "任务已启动,定时器在后台运行中".equals(resultData.get("message"));
            // 如果是持续运行的任务(定时器模式),保持 RUNNING 状态,不更新为 COMPLETED
            if (isContinuousTask && result.isSuccess()) {
                log.info("任务已启动定时器,保持运行状态: taskId={}, message={}",
                    task.getTaskId(), resultData.get("message"));
                task.setResultData(writeJson(resultData));
                updateById(task);
                // 通知任务状态(保持 RUNNING)
                notificationService.notifyTaskStatus(task);
                return;
            }
            // 更新任务结果(非持续运行的任务)
            task.setStatus(result.isSuccess() ? MultiDeviceTask.Status.COMPLETED.name() : MultiDeviceTask.Status.FAILED.name());
            task.setErrorMessage(result.isSuccess() ? null : result.getMessage());
            task.setEndTime(new Date());
            task.setResultData(writeJson(result.getData()));
            task.setResultData(writeJson(resultData));
            updateById(task);
            
            // 通知任务完成
@@ -164,9 +200,14 @@
        if (!MultiDeviceTask.Status.RUNNING.name().equals(task.getStatus())) {
            return false;
        }
        taskExecutionEngine.requestTaskCancellation(taskId);
        task.setStatus(MultiDeviceTask.Status.CANCELLED.name());
        task.setEndTime(new Date());
        return updateById(task);
        boolean updated = updateById(task);
        if (updated) {
            notificationService.notifyTaskStatus(task);
        }
        return updated;
    }
    @Override
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/TaskStatusNotificationServiceImpl.java
@@ -204,7 +204,12 @@
                    .name(eventName)
                    .data(createMessage("", data)));
            } catch (IOException e) {
                log.warn("发送SSE消息失败: taskId={}, event={}", taskId, eventName, e);
                // 客户端断开连接是正常情况,使用DEBUG级别
                if (e instanceof org.apache.catalina.connector.ClientAbortException) {
                    log.debug("客户端断开SSE连接: taskId={}, event={}", taskId, eventName);
                } else {
                    log.warn("发送SSE消息失败: taskId={}, event={}", taskId, eventName, e);
                }
                toRemove.add(emitter);
            }
        }
@@ -228,7 +233,12 @@
                    .name(eventName)
                    .data(createMessage("", data)));
            } catch (IOException e) {
                log.warn("发送SSE消息失败: event={}", eventName, e);
                // 客户端断开连接是正常情况,使用DEBUG级别
                if (e instanceof org.apache.catalina.connector.ClientAbortException) {
                    log.debug("客户端断开SSE连接: event={}", eventName);
                } else {
                    log.warn("发送SSE消息失败: event={}", eventName, e);
                }
                toRemove.add(emitter);
            }
        }
mes-web/src/views/device/DeviceEditDialog.vue
@@ -354,7 +354,7 @@
const deviceTypesLoading = ref(false)
// 设备逻辑参数(根据设备类型动态显示)
const deviceLogicParams = reactive({})
const deviceLogicParams = ref({})
const S7_PLC_TYPES = ['S1200', 'S1500']
const MODBUS_PLC_TYPES = ['MODBUS']
@@ -683,15 +683,11 @@
}
// 加载设备逻辑参数
const loadDeviceLogicParams = (deviceLogic, deviceType) => {
  // 清空现有参数
  Object.keys(deviceLogicParams).forEach(key => {
    delete deviceLogicParams[key]
  })
  // 根据设备类型加载对应的参数
const loadDeviceLogicParams = (deviceLogic) => {
  if (deviceLogic && Object.keys(deviceLogic).length > 0) {
    Object.assign(deviceLogicParams, deviceLogic)
    deviceLogicParams.value = { ...deviceLogic }
  } else {
    deviceLogicParams.value = {}
  }
}
@@ -701,9 +697,7 @@
  deviceFormRef.value?.clearValidate()
  
  // 重置设备逻辑参数
  Object.keys(deviceLogicParams).forEach(key => {
    delete deviceLogicParams[key]
  })
  deviceLogicParams.value = {}
}
const addConfigParam = () => {
@@ -778,8 +772,10 @@
  }
    // 保存设备逻辑参数(直接使用deviceLogicParams,由各个配置组件管理)
    if (deviceLogicParams && Object.keys(deviceLogicParams).length > 0) {
      extraObj.deviceLogic = { ...deviceLogicParams }
    if (deviceLogicParams.value && Object.keys(deviceLogicParams.value).length > 0) {
      extraObj.deviceLogic = { ...deviceLogicParams.value }
    } else {
      delete extraObj.deviceLogic
    }
    // 构建 configJson:将 configParams 数组转换为 JSON 字符串
mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue
@@ -9,6 +9,7 @@
            :max="10000"
            :step="100"
            style="width: 100%;"
            @change="emitConfigUpdate"
          />
          <span class="form-tip">车辆最大容量</span>
        </el-form-item>
@@ -22,6 +23,7 @@
            :step="0.1"
            :precision="1"
            style="width: 100%;"
            @change="emitConfigUpdate"
          />
          <span class="form-tip">车辆运动速度,默认1格/秒</span>
        </el-form-item>
@@ -50,6 +52,7 @@
            :max="10000"
            :step="100"
            style="width: 100%;"
            @change="emitConfigUpdate"
          />
          <span class="form-tip">当玻璃未提供长度时使用的默认值</span>
        </el-form-item>
@@ -65,6 +68,7 @@
            :max="1000"
            :step="1"
            style="width: 100%;"
            @change="emitConfigUpdate"
          />
          <span class="form-tip">车辆初始位置(格子)</span>
        </el-form-item>
@@ -78,6 +82,7 @@
            :step="1"
            style="width: 40%;"
            placeholder="最小"
            @change="emitConfigUpdate"
          />
          <span style="margin: 0 2%;">~</span>
          <el-input-number
@@ -87,6 +92,7 @@
            :step="1"
            style="width: 40%;"
            placeholder="最大"
            @change="emitConfigUpdate"
          />
          <span class="form-tip">运动距离范围(格子)</span>
        </el-form-item>
@@ -137,7 +143,7 @@
      </el-col>
      <el-col :span="12">
        <el-form-item label="自动上料">
          <el-switch v-model="config.autoFeed" />
          <el-switch v-model="config.autoFeed" @change="emitConfigUpdate" />
          <span class="form-tip">是否自动触发上料请求</span>
        </el-form-item>
      </el-col>
@@ -152,6 +158,7 @@
            :max="10"
            :step="1"
            style="width: 100%;"
            @change="emitConfigUpdate"
          />
        </el-form-item>
      </el-col>
@@ -160,30 +167,31 @@
    <el-form-item label="位置映射">
      <div class="position-mapping">
        <div
          v-for="(value, key, index) in config.positionMapping"
          :key="index"
          v-for="(item, index) in positionList"
          :key="item.id"
          class="mapping-item"
        >
          <el-input
            v-model="mappingKeys[index]"
            v-model="item.key"
            placeholder="位置代码(如900/901)"
            size="small"
            style="width: 150px; margin-right: 10px;"
            @input="updatePositionMapping(index, $event, value)"
            @change="handlePositionKeyChange(index)"
          />
          <el-input-number
            v-model="config.positionMapping[mappingKeys[index] || key]"
            v-model="item.value"
            :min="0"
            :max="1000"
            :step="1"
            size="small"
            style="width: 120px; margin-right: 10px;"
            placeholder="位置值(格)"
            @change="handlePositionValueChange(index)"
          />
          <el-button
            type="danger"
            size="small"
            @click="removePositionMapping(key)"
            @click="removePositionMapping(index)"
          >
            删除
          </el-button>
@@ -226,8 +234,40 @@
  positionMapping: {}
})
// 位置映射的键数组
const mappingKeys = ref([])
// 位置映射编辑列表
const positionList = ref([])
let suppressPositionSync = false
let suppressEmit = false
const emitConfigUpdate = () => {
  if (suppressEmit) return
  emit('update:modelValue', { ...config.value })
}
const syncPositionListFromConfig = () => {
  suppressPositionSync = true
  const entries = Object.entries(config.value.positionMapping || {})
  positionList.value = entries.map(([key, value], idx) => ({
    id: `${key}_${idx}_${Date.now()}`,
    key,
    value: value ?? 1
  }))
  suppressPositionSync = false
}
const applyPositionListToConfig = () => {
  if (suppressPositionSync) return
  const mapping = {}
  positionList.value.forEach((item) => {
    if (item.key && item.key.trim()) {
      mapping[item.key.trim()] = item.value ?? 1
    }
  })
  config.value.positionMapping = mapping
  emitConfigUpdate()
}
syncPositionListFromConfig()
// 时间字段(秒)- 用于前端显示和输入
const glassIntervalSeconds = ref(1.0)
@@ -238,6 +278,7 @@
// 监听props变化
watch(() => props.modelValue, (newVal) => {
  if (newVal && Object.keys(newVal).length > 0) {
    suppressEmit = true
    config.value = {
      vehicleCapacity: newVal.vehicleCapacity ?? 6000,
      vehicleSpeed: newVal.vehicleSpeed ?? 1.0,
@@ -258,67 +299,54 @@
    idleMonitorIntervalSeconds.value = (config.value.idleMonitorIntervalMs ?? 2000) / 1000
    taskMonitorIntervalSeconds.value = (config.value.taskMonitorIntervalMs ?? 1000) / 1000
    mesConfirmTimeoutSeconds.value = (config.value.mesConfirmTimeoutMs ?? 30000) / 1000
    mappingKeys.value = Object.keys(config.value.positionMapping)
    syncPositionListFromConfig()
    suppressEmit = false
  }
}, { immediate: true, deep: true })
// 监听秒字段变化,转换为毫秒并更新config
watch(glassIntervalSeconds, (val) => {
  config.value.glassIntervalMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
  emitConfigUpdate()
})
watch(idleMonitorIntervalSeconds, (val) => {
  config.value.idleMonitorIntervalMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
  emitConfigUpdate()
})
watch(taskMonitorIntervalSeconds, (val) => {
  config.value.taskMonitorIntervalMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
  emitConfigUpdate()
})
watch(mesConfirmTimeoutSeconds, (val) => {
  config.value.mesConfirmTimeoutMs = Math.round(val * 1000)
  emit('update:modelValue', { ...config.value })
  emitConfigUpdate()
})
// 监听config其他字段变化,同步到父组件
watch(() => [
  config.value.vehicleCapacity,
  config.value.vehicleSpeed,
  config.value.defaultGlassLength,
  config.value.homePosition,
  config.value.minRange,
  config.value.maxRange,
  config.value.autoFeed,
  config.value.maxRetryCount,
  config.value.positionMapping
], () => {
  emit('update:modelValue', { ...config.value })
}, { deep: true })
// 位置映射相关方法
const addPositionMapping = () => {
  const newKey = `POS${Object.keys(config.value.positionMapping).length + 1}`
  config.value.positionMapping[newKey] = 1
  mappingKeys.value.push(newKey)
  const nextIndex = positionList.value.length + 1
  positionList.value.push({
    id: `POS_${Date.now()}_${nextIndex}`,
    key: '',
    value: 1
  })
}
const removePositionMapping = (key) => {
  delete config.value.positionMapping[key]
  mappingKeys.value = mappingKeys.value.filter(k => k !== key)
const removePositionMapping = (idx) => {
  positionList.value.splice(idx, 1)
  applyPositionListToConfig()
}
const updatePositionMapping = (index, newKey, oldValue) => {
  const oldKey = mappingKeys.value[index]
  if (oldKey && oldKey !== newKey) {
    delete config.value.positionMapping[oldKey]
  }
  mappingKeys.value[index] = newKey
  if (newKey) {
    config.value.positionMapping[newKey] = oldValue || 1
  }
const handlePositionKeyChange = () => {
  applyPositionListToConfig()
}
const handlePositionValueChange = () => {
  applyPositionListToConfig()
}
</script>
mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue
@@ -105,8 +105,8 @@
          {{ selectedDevice.moduleName }}
        </el-descriptions-item>
        <el-descriptions-item label="是否启用">
          <el-tag :type="selectedDevice.enabled ? 'success' : 'info'">
            {{ selectedDevice.enabled ? '启用' : '停用' }}
          <el-tag :type="getEnabledType(selectedDevice.enabled)">
            {{ getEnabledLabel(selectedDevice.enabled) }}
          </el-tag>
        </el-descriptions-item>
      </el-descriptions>
@@ -229,6 +229,22 @@
  return String(status)
}
const getEnabledType = (enabled) => {
  // 支持数字 1/0、布尔值 true/false、字符串 '1'/'0'
  if (enabled === 1 || enabled === true || enabled === '1' || String(enabled).toUpperCase() === 'TRUE') {
    return 'success'
  }
  return 'info'
}
const getEnabledLabel = (enabled) => {
  // 支持数字 1/0、布尔值 true/false、字符串 '1'/'0'
  if (enabled === 1 || enabled === true || enabled === '1' || String(enabled).toUpperCase() === 'TRUE') {
    return '启用'
  }
  return '停用'
}
watch(
  () => props.group,
  () => {
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -26,21 +26,34 @@
    </div>
    <el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
      <el-form-item label="玻璃ID列表" prop="glassIds" required>
      <el-form-item label="玻璃ID列表" prop="glassIds">
        <el-input
          v-model="glassIdsInput"
          type="textarea"
          :rows="4"
          placeholder="请输入玻璃条码,支持多行或逗号分隔,每行一个或逗号分隔"
          placeholder="可选:如果输入玻璃ID,将使用输入的ID进行测试(代替卧转立扫码);如果不输入,将从数据库读取最近扫码的玻璃ID进行测试"
          show-word-limit
          :maxlength="5000"
        />
        <div class="form-tip">
          已输入 {{ glassIds.length }} 个玻璃ID
          <span v-if="glassIds.length > 0">已输入 {{ glassIds.length }} 个玻璃ID(测试模式:使用输入的ID)</span>
          <span v-else>未输入玻璃ID(正常模式:将从数据库读取最近扫码的玻璃ID)</span>
        </div>
      </el-form-item>
      
      <el-divider content-position="left">执行配置</el-divider>
      <el-form-item label="单片间隔 (秒)">
        <el-input-number
          v-model="form.glassIntervalSeconds"
          :min="0"
          :max="60"
          :step="0.1"
          :precision="1"
          placeholder="每个玻璃ID之间的间隔时间"
        />
        <div class="form-tip">多个玻璃ID时,每个玻璃ID传递之间的间隔时间(秒),用于模拟玻璃每片运动的时间。0表示一次性全部传递</div>
      </el-form-item>
      
      <el-form-item label="执行间隔 (ms)">
        <el-input-number
@@ -93,8 +106,9 @@
})
const emit = defineEmits(['task-started'])
//配置默认值
const form = reactive({
  glassIntervalSeconds: 10, // 单片间隔,默认10秒
  executionInterval: 1000,
  timeoutMinutes: 30,
  retryCount: 3
@@ -106,8 +120,10 @@
  glassIds: [
    {
      validator: (rule, value, callback) => {
        // 如果输入了玻璃ID,则进行验证;如果没有输入,则允许(将从数据库读取)
        if (glassIds.value.length === 0) {
          callback(new Error('请至少输入一个玻璃ID'))
          // 允许为空,将从数据库读取最近扫码的玻璃ID
          callback()
        } else if (glassIds.value.length > 100) {
          callback(new Error('玻璃ID数量不能超过100个'))
        } else {
@@ -151,6 +167,8 @@
    .filter((item) => item.length > 0)
})
const normalizeType = (type) => (type || '').trim().toUpperCase()
const fetchLoadDevice = async () => {
  loadDeviceId.value = null
  loadDeviceName.value = ''
@@ -172,9 +190,15 @@
      : Array.isArray(rawList?.data)
      ? rawList.data
      : []
    const targetDevice =
      deviceList.find((item) => (item.deviceType || '').toUpperCase() === 'LOAD_VEHICLE') ||
      deviceList[0]
    const scannerDevice = deviceList.find((item) => {
      const type = normalizeType(item.deviceType)
      return type.includes('SCANNER') || type.includes('扫码')
    })
    const loadVehicleDevice = deviceList.find((item) => {
      const type = normalizeType(item.deviceType)
      return type.includes('LOAD_VEHICLE') || type.includes('大车')
    })
    const targetDevice = scannerDevice || loadVehicleDevice || deviceList[0]
    if (targetDevice && targetDevice.id) {
      loadDeviceId.value = targetDevice.id
      loadDeviceName.value = targetDevice.deviceName || targetDevice.deviceCode || `ID: ${targetDevice.id}`
@@ -202,17 +226,18 @@
    return
  }
  
  if (glassIds.value.length === 0) {
    ElMessage.warning('请至少输入一个玻璃ID')
    return
  }
  try {
    loading.value = true
    
    // 构建任务参数
    // 如果输入了玻璃ID,使用输入的;如果没有输入,glassIds为空数组,后端会从数据库读取
    // 将秒转换为毫秒传给后端
    const glassIntervalMs = form.glassIntervalSeconds != null && form.glassIntervalSeconds !== undefined
      ? Math.round(form.glassIntervalSeconds * 1000)
      : 1000
    const parameters = {
      glassIds: glassIds.value,
      glassIds: glassIds.value.length > 0 ? glassIds.value : [],
      glassIntervalMs: glassIntervalMs,
      executionInterval: form.executionInterval || 1000
    }
    
@@ -261,14 +286,14 @@
    return
  }
  if (!loadDeviceId.value) {
    ElMessage.warning('未找到上大车设备,无法清空PLC')
    ElMessage.warning('未找到对应设备,无法清空PLC')
    return
  }
  try {
    clearLoading.value = true
    const response = await deviceInteractionApi.executeOperation({
      deviceId: loadDeviceId.value,
      operation: 'clearGlass',
      operation: 'clearPlc',
      params: {}
    })
    if (response?.code !== 200) {