huang
9 小时以前 04914a9997afbbead6f8adbb9d9c40e05b2edbd1
修复调用导入工程失败 重复保存;修复分批出片逻辑
10个文件已修改
953 ■■■■■ 已修改文件
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/GlassInfoImportController.java 93 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/EngineeringSequenceService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/EngineeringSequenceServiceImpl.java 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/config/LargeGlassConfig.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java 145 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java 576 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/GlassInfoImportController.java
@@ -42,6 +42,11 @@
     */
    @PostMapping("/importExcel")
    public ResponseEntity<?> importEngineer(@RequestBody Map<String, Object> body) {
        // 初始化返回结果和关键标记
        Map<String, Object> errorResponse = new HashMap<>();
        boolean mesSuccess = false;
        String engineeringId = null;
        // 1. 校验
        Object rowsObj = body.get("excelRows");
        if (!(rowsObj instanceof List)) {
            return ResponseEntity.badRequest().body("excelRows 必须是数组");
@@ -52,71 +57,75 @@
            return ResponseEntity.badRequest().body("excelRows 不能为空");
        }
        Map<String, Object> payload = glassInfoService.buildEngineerImportPayload(excelRows);
        log.info("构建的 MES 导入数据: {}", payload);
         try {
            // 2. 构建MES导入数据
            Map<String, Object> payload = glassInfoService.buildEngineerImportPayload(excelRows);
            log.info("构建的 MES 导入数据: {}", payload);
        // 从payload中提取工程号(payload中使用的是engineerId)
        String engineeringId = (String) payload.get("engineerId");
        if (engineeringId == null || engineeringId.isEmpty()) {
            // 如果payload中没有engineerId,尝试从glassInfolList中获取
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> glassInfoList = (List<Map<String, Object>>) payload.get("glassInfolList");
            if (glassInfoList != null && !glassInfoList.isEmpty()) {
                Object firstEngineerId = glassInfoList.get(0).get("engineerId");
                if (firstEngineerId != null) {
                    engineeringId = firstEngineerId.toString();
            // 3. 提取工程号
            engineeringId = (String) payload.get("engineerId");
            if (engineeringId == null || engineeringId.isEmpty()) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> glassInfoList = (List<Map<String, Object>>) payload.get("glassInfolList");
                if (glassInfoList != null && !glassInfoList.isEmpty()) {
                    Object firstEngineerId = glassInfoList.get(0).get("engineerId");
                    if (firstEngineerId != null) {
                        engineeringId = firstEngineerId.toString();
                    }
                }
            }
        }
        String mesEngineeringImportUrl = glassInfoService.getMesEngineeringImportUrl();
        try {
          // 4. 调用MES接口
            String mesEngineeringImportUrl = glassInfoService.getMesEngineeringImportUrl();
            ResponseEntity<Map> mesResp = restTemplate.postForEntity(mesEngineeringImportUrl, payload, Map.class);
            Map<String, Object> mesBody = mesResp.getBody();
            // 检查MES响应是否真正成功(不仅检查HTTP状态码,还要检查响应体中的code字段)
            boolean mesSuccess = false;
            // 5. 检查MES响应是否真正成功(优化后的判断逻辑,增加异常捕获)
            if (mesResp.getStatusCode().is2xxSuccessful() && mesBody != null) {
                Object codeObj = mesBody.get("code");
                if (codeObj != null) {
                    int code = codeObj instanceof Number ? ((Number) codeObj).intValue() :
                              Integer.parseInt(String.valueOf(codeObj));
                    // MES成功通常返回code=200或0
                    mesSuccess = (code == 200 || code == 0);
                    try {
                        // 安全转换code为整数,避免类型转换异常
                        int code = codeObj instanceof Number ? ((Number) codeObj).intValue() :
                                Integer.parseInt(String.valueOf(codeObj).trim());
                        // MES成功通常返回code=200或0
                        mesSuccess = (code == 200 || code == 0);
                    } catch (NumberFormatException e) {
                        log.warn("MES响应code字段不是有效数字:{}", codeObj, e);
                    }
                } else {
                    // 如果没有code字段,认为HTTP 2xx就是成功
                    // 没有code字段,认为HTTP 2xx就是成功
                    mesSuccess = true;
                }
            }
            // 只有MES导入真正成功时,才保存玻璃信息到本地数据库,并关联engineering_id
            if (mesSuccess && engineeringId != null) {
                try {
                    glassInfoService.saveGlassInfosFromExcel(excelRows, engineeringId);
                    log.info("MES导入成功,已保存玻璃信息到本地数据库,工程号: {}", engineeringId);
                } catch (Exception e) {
                    log.error("MES导入成功,但保存玻璃信息到本地数据库失败: engineeringId={}", engineeringId, e);
                    // 即使保存失败,也返回MES的成功响应,但记录错误日志
                }
            } else {
                log.warn("MES导入未成功,不保存玻璃信息到本地数据库: engineeringId={}, mesSuccess={}", engineeringId, mesSuccess);
                // HTTP状态码不是2xx或响应体为空,设为失败
                mesSuccess = false;
                log.warn("MES接口返回非2xx状态码或空响应体,状态码:{}", mesResp.getStatusCode());
            }
            // 直接返回 MES 的响应,让前端根据响应体中的 code 字段判断是否成功
            // 6. 只有MES导入真正成功且工程号不为空时,才保存到本地数据库
            if (mesSuccess && engineeringId != null && !engineeringId.isEmpty()) {
                // 先保存工程号
                engineeringSequenceService.saveEngineeringId(new Date(), engineeringId);
                // 再保存玻璃信息
                glassInfoService.saveGlassInfosFromExcel(excelRows, engineeringId);
                log.info("MES导入成功,已保存工程号和玻璃信息到本地数据库,工程号: {}", engineeringId);
            } else {
                log.warn("MES导入未成功,不保存工程号和玻璃信息到本地数据库: engineeringId={}, mesSuccess={}", engineeringId, mesSuccess);
            }
            // 7. 返回MES的响应
            return ResponseEntity.status(mesResp.getStatusCode()).body(mesBody);
        } catch (org.springframework.web.client.ResourceAccessException e) {
            // 连接超时或无法连接
            log.error("转发 MES 导入接口失败(连接问题) url={}, error={}", mesEngineeringImportUrl, e.getMessage(), e);
            Map<String, Object> errorResponse = new java.util.HashMap<>();
            log.error("转发 MES 导入接口失败(连接问题) url={}, error={}", glassInfoService.getMesEngineeringImportUrl(), e.getMessage(), e);
            errorResponse.put("code", 500);
            errorResponse.put("message", "无法连接到 MES 接口,请检查网络连接或联系管理员");
            errorResponse.put("data", false);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
        } catch (Exception e) {
            // 其他异常
            log.error("转发 MES 导入接口失败 url={}, error={}", mesEngineeringImportUrl, e.getMessage(), e);
            Map<String, Object> errorResponse = new java.util.HashMap<>();
            log.error("转发 MES 导入接口失败 url={}, error={}", glassInfoService.getMesEngineeringImportUrl(), e.getMessage(), e);
            errorResponse.put("code", 500);
            errorResponse.put("message", "转发 MES 失败: " + e.getMessage());
            errorResponse.put("data", false);
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/EngineeringSequenceService.java
@@ -21,5 +21,23 @@
     * @return 生成的工程号(格式:P + yyMMdd + 两位序号)
     */
    String generateAndSaveEngineeringId(Date date);
    /**
     * 生成工程号但不保存
     * 根据日期获取最大序号,然后自增1生成新的工程号,但不保存到数据库
     *
     * @param date 日期
     * @return 生成的工程号(格式:P + yyMMdd + 两位序号)
     */
    String generateEngineeringId(Date date);
    /**
     * 保存工程号
     *
     * @param date 日期
     * @param engineeringId 工程号
     * @return 保存是否成功
     */
    boolean saveEngineeringId(Date date, String engineeringId);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/EngineeringSequenceServiceImpl.java
@@ -34,36 +34,61 @@
    private static final int RETRY_INTERVAL_MAX = 200;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String generateAndSaveEngineeringId(Date date) {
        try {
            Integer maxSequence = baseMapper.selectMaxSequenceByDate(date);
            maxSequence = (maxSequence == null) ? 0 : maxSequence;
            int newSequence = maxSequence + 1;
    public String generateEngineeringId(Date date) {
        Integer maxSequence = baseMapper.selectMaxSequenceByDate(date);
        maxSequence = (maxSequence == null) ? 0 : maxSequence;
        int newSequence = maxSequence + 1;
        LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        String dateStr = DATE_FORMATTER_THREAD_LOCAL.get().format(localDate);
        String engineeringId = "P" + dateStr + String.format("%02d", newSequence);
        log.info("生成工程号(未保存): engineeringId={}, date={}, sequence={}", engineeringId, date, newSequence);
        return engineeringId;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean saveEngineeringId(Date date, String engineeringId) {
        try {
            // 解析工程号获取序号
            LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            String dateStr = DATE_FORMATTER_THREAD_LOCAL.get().format(localDate);
            String engineeringId = "P" + dateStr + String.format("%02d", newSequence);
            String sequenceStr = engineeringId.substring(engineeringId.length() - 2);
            int sequence = Integer.parseInt(sequenceStr);
            EngineeringSequence engineeringSequence = new EngineeringSequence();
            engineeringSequence.setEngineeringId(engineeringId);
            engineeringSequence.setDate(date);
            engineeringSequence.setSequence(newSequence);
            engineeringSequence.setSequence(sequence);
            engineeringSequence.setCreatedTime(new Date());
            engineeringSequence.setUpdatedTime(new Date());
            engineeringSequence.setCreatedBy("system");
            engineeringSequence.setUpdatedBy("system");
            save(engineeringSequence);
            boolean result = save(engineeringSequence);
            log.info("生成工程号成功: engineeringId={}, date={}, sequence={}", engineeringId, date, newSequence);
            return engineeringId;
            if (result) {
                log.info("保存工程号成功: engineeringId={}, date={}, sequence={}", engineeringId, date, sequence);
            } else {
                log.error("保存工程号失败: engineeringId={}, date={}, sequence={}", engineeringId, date, sequence);
            }
            return result;
        } catch (DuplicateKeyException dup) {
            log.error("生成工程号唯一键冲突: date={}", date, dup);
            throw new RuntimeException("生成工程号失败", dup);
            log.error("保存工程号唯一键冲突: date={}, engineeringId={}", date, engineeringId, dup);
            throw new RuntimeException("保存工程号失败", dup);
        } catch (Exception e) {
            log.error("生成工程号失败, date={}", date, e);
            throw new RuntimeException("生成工程号失败", e);
            log.error("保存工程号失败, date={}, engineeringId={}", date, engineeringId, e);
            throw new RuntimeException("保存工程号失败", e);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String generateAndSaveEngineeringId(Date date) {
        String engineeringId = generateEngineeringId(date);
        saveEngineeringId(date, engineeringId);
        return engineeringId;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java
@@ -232,8 +232,8 @@
            return result;
        }
        // 工程号生成:每次导入都生成新的工程号(使用数据库自增序号,避免重复)
        final String engineerId = engineeringSequenceService.generateAndSaveEngineeringId(new Date());
        // 工程号生成:每次导入都生成新的工程号(先只生成,不保存到数据库,等到MES调用成功后再保存)
        final String engineerId = engineeringSequenceService.generateEngineeringId(new Date());
        final String filmsIdDefault = firstValue(excelRows, "filmsId", "白玻");
        final double thicknessDefault = parseDouble(firstValue(excelRows, "thickness"), 0d);
@@ -254,7 +254,7 @@
        Map<String, Integer> rawSequenceMap = new HashMap<>();
        for (Map<String, Object> row : excelRows) {
            double width = parseDouble(row.get("width"), 0d);
            double height = parseDouble(row.get("height"), 0d);
            double height = parseDouble(row.get("length"), 0d);
            double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
            String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal);
            String key = width + "_" + height + "_" + thickness + "_" + filmsId;
@@ -279,7 +279,7 @@
                    String productName = str(row.get("productName"));
                    String customerName = str(row.get("customerName"));
                    double width = parseDouble(row.get("width"), 0d);
                    double height = parseDouble(row.get("height"), 0d);
                    double height = parseDouble(row.get("length"), 0d);
                    double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
                    
                    // 计算 rawSequence
@@ -352,7 +352,7 @@
        Map<String, Map<String, Object>> rawGlassMap = new HashMap<>();
        for (Map<String, Object> row : excelRows) {
            double width = parseDouble(row.get("width"), 0d);
            double height = parseDouble(row.get("height"), 0d);
            double height = parseDouble(row.get("length"), 0d);
            double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
            String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal);
            String key = width + "_" + height + "_" + thickness + "_" + filmsId;
@@ -389,7 +389,7 @@
            Object qtyObj = row.getOrDefault("quantity", 1);
            int qty = parseDouble(qtyObj, 1) > 0 ? (int) parseDouble(qtyObj, 1) : 1;
            double width = parseDouble(row.get("width"), 0d);
            double height = parseDouble(row.get("height"), 0d);
            double height = parseDouble(row.get("length"), 0d);
            double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
            String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal);
            String productName = str(row.get("productName"));
@@ -590,7 +590,7 @@
            if (qty <= 0) qty = 1;
            double width = parseDouble(row.get("width"), 0d);
            double height = parseDouble(row.get("height"), 0d);
            double length = parseDouble(row.get("length"), 0d);
            double thickness = parseDouble(row.get("thickness"), 0d);
            // 与导入规则保持一致:glassId 前加工程号前缀,数量>1时追加序号
@@ -601,7 +601,7 @@
                GlassInfo glassInfo = new GlassInfo();
                glassInfo.setGlassId(finalGlassId);
                glassInfo.setEngineeringId(engineeringId.trim());
                glassInfo.setGlassLength((int) Math.round(height));
                glassInfo.setGlassLength((int) Math.round(length));
                glassInfo.setGlassWidth((int) Math.round(width));
                glassInfo.setGlassThickness(BigDecimal.valueOf(thickness));
                glassInfo.setStatus(GlassInfo.Status.ACTIVE);
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/config/LargeGlassConfig.java
@@ -33,5 +33,11 @@
     * 每格厚度(mm)
     */
    private Integer gridThickness = 5;
    /**
     * 处理时间(秒)
     * 大理片笼处理玻璃的时间,默认30秒
     */
    private Integer processTimeSeconds = 30;
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java
@@ -1331,10 +1331,22 @@
            MesTaskInfo existingTask = currentTasks.get(deviceId);
            if (existingTask != null) {
                log.debug("设备已有任务在执行中,跳过检查MES任务: deviceId={}", deviceId);
                // 仍然返回当前任务的玻璃列表,供任务引擎记录/对账本批次
                List<String> batchIds = new ArrayList<>();
                if (existingTask.glasses != null) {
                    for (GlassTaskInfo g : existingTask.glasses) {
                        if (g != null && g.glassId != null && !g.glassId.isEmpty()) {
                            batchIds.add(g.glassId);
                        }
                    }
                }
                return DevicePlcVO.OperationResult.builder()
                        .success(true)
                        .message("任务执行中,无需重复检查MES任务")
                        .data(Collections.singletonMap("waiting", false))
                        .data(new HashMap<String, Object>() {{
                            put("waiting", false);
                            put("batchGlassIds", batchIds);
                        }})
                        .build();
            }
            
@@ -1357,6 +1369,7 @@
                waitData.put("completed", false);
                waitData.put("waiting", true);
                waitData.put("waitingReason", "mesSend=0");
                waitData.put("batchGlassIds", new ArrayList<>());
                return DevicePlcVO.OperationResult.builder()
                        .success(true)
                        .message("等待MES发送请求(mesSend=0)")
@@ -1500,6 +1513,39 @@
            }
            
            currentTasks.put(deviceId, taskInfo);
            // 如果有多设备任务上下文,则记录本次MES下发的玻璃ID列表到上下文,供分批校验使用
            if (params != null) {
                Object ctxObj = params.get("_taskContext");
                if (ctxObj instanceof com.mes.task.model.TaskExecutionContext) {
                    com.mes.task.model.TaskExecutionContext ctx = (com.mes.task.model.TaskExecutionContext) ctxObj;
                    List<String> batchIds = new ArrayList<>();
                    for (GlassTaskInfo g : glasses) {
                        if (g != null && g.glassId != null && !g.glassId.isEmpty()) {
                            batchIds.add(g.glassId);
                        }
                    }
                    // 1. 记录当前批次的玻璃ID
                    ctx.getSharedData().put("currentMesBatchGlassIds", batchIds);
                    log.info("记录本次MES批次玻璃列表: deviceId={}, batchIds={}", deviceId, batchIds);
                    // 2. 初始化总待出片玻璃列表(仅第一次初始化,从任务参数获取)
                    if (!ctx.getSharedData().containsKey("initialGlassIds")) {
                        // 从任务参数中获取总待出片玻璃ID(核心:总列表来自任务参数,而非MES批次)
                        List<String> taskGlassIds = ctx.getParameters().getGlassIds();
                        if (taskGlassIds != null && !taskGlassIds.isEmpty()) {
                            ctx.getSharedData().put("initialGlassIds", new ArrayList<>(taskGlassIds));
                            // 初始化已出片列表为空
                            if (!ctx.getSharedData().containsKey("outboundGlassIds")) {
                                ctx.getSharedData().put("outboundGlassIds", new ArrayList<>());
                            }
                            log.info("初始化总待出片玻璃列表: deviceId={}, taskGlassIds={}", deviceId, taskGlassIds);
                        } else {
                            log.warn("任务参数中未找到总待出片玻璃ID列表: deviceId={}", deviceId);
                        }
                    }
                }
            }
            
            // 清空plcRequest(表示已接收任务)
            Map<String, Object> payload = new HashMap<>();
@@ -1537,6 +1583,14 @@
            Map<String, Object> successData = new HashMap<>();
            successData.put("waiting", false);
            successData.put("taskStarted", true);
            // 将本次MES下发的玻璃ID列表通过返回值带回(任务引擎不再依赖_taskContext写入)
            List<String> batchIdsForReturn = new ArrayList<>();
            for (GlassTaskInfo g : glasses) {
                if (g != null && g.glassId != null && !g.glassId.isEmpty()) {
                    batchIdsForReturn.add(g.glassId);
                }
            }
            successData.put("batchGlassIds", batchIdsForReturn);
            
            return DevicePlcVO.OperationResult.builder()
                    .success(true)
@@ -2063,8 +2117,11 @@
        if (taskInfo == null) {
            log.info("检查MES确认时未找到任务记录,尝试补偿检查MES任务: deviceId={}", deviceId);
            try {
                // 关键:补偿检查时也要透传params(包含_taskContext),
                // 否则handleCheckMesTask无法把本批次玻璃ID写入currentMesBatchGlassIds,任务引擎无法累加完成进度
                Map<String, Object> checkParams = params != null ? params : Collections.emptyMap();
                DevicePlcVO.OperationResult checkResult =
                        handleCheckMesTask(deviceConfig, Collections.emptyMap(), logicParams);
                        handleCheckMesTask(deviceConfig, checkParams, logicParams);
                if (Boolean.TRUE.equals(checkResult.getSuccess())) {
                    taskInfo = currentTasks.get(deviceId);
                    if (taskInfo != null) {
@@ -2150,84 +2207,14 @@
            data.put("completed", completed);
            if (completed) {
                // MES已确认,检查是否还有未出片的玻璃(仅对出片任务)
                boolean hasMoreGlass = false;
                int completedCount = 0;
                int totalCount = 0;
                if (taskInfo.isOutbound && params != null) {
                    // 从TaskExecutionContext中获取已出片的玻璃ID列表和初始玻璃ID列表
                    Object contextObj = params.get("_taskContext");
                    if (contextObj instanceof com.mes.task.model.TaskExecutionContext) {
                        com.mes.task.model.TaskExecutionContext context =
                                (com.mes.task.model.TaskExecutionContext) contextObj;
                        @SuppressWarnings("unchecked")
                        List<String> initialGlassIds = (List<String>) context.getSharedData().get("initialGlassIds");
                        @SuppressWarnings("unchecked")
                        List<String> outboundGlassIds = (List<String>) context.getSharedData().get("outboundGlassIds");
                        if (initialGlassIds != null && !initialGlassIds.isEmpty()) {
                            totalCount = initialGlassIds.size();
                            completedCount = (outboundGlassIds != null) ? outboundGlassIds.size() : 0;
                            // 检查是否所有玻璃都已出片
                            if (outboundGlassIds == null || !outboundGlassIds.containsAll(initialGlassIds)) {
                                hasMoreGlass = true;
                            }
                        }
                    }
                }
                // 如果还有未出片的玻璃,保持plcRequest=1,清理本次任务状态,等待下次交互
                // 这样第二次交互时,checkMesTask可以检测到mesSend=1,创建新任务,完整地走一遍逻辑
                if (hasMoreGlass) {
                    // 清空state和汇报字(本次交互已完成)
                    clearTaskStates(deviceConfig, serializer);
                    // 注意:不记录lastCompletedMesRecords,因为还有未出片的玻璃,任务未真正完成
                    // 这样第二次交互时,即使MES发送新任务(新的玻璃ID),也不会被误判为旧任务
                    // 任务完成,恢复为空闲状态(本次交互已完成)
                    statusManager.updateVehicleStatus(
                            deviceConfig.getDeviceId(), VehicleState.IDLE);
                    statusManager.clearVehicleTask(deviceConfig.getDeviceId());
                    // 移除任务记录(本次交互已完成,等待下次交互时创建新任务)
                    currentTasks.remove(deviceConfig.getDeviceId());
                    // 停止任务监控(本次交互已完成)
                    handleStopTaskMonitor(deviceConfig);
                    // 保持plcRequest=1(可以接收下次任务)
                    Map<String, Object> payload = new HashMap<>();
                    payload.put("plcRequest", 1);
                    plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
                    log.info("出片任务本次交互完成,还有未出片的玻璃,等待下次交互: deviceId={}, completedCount={}, totalCount={}",
                            deviceConfig.getDeviceId(), completedCount, totalCount);
                    String progressMessage = String.format("目前完成出片玻璃数量%d/%d,等待下次交互任务", completedCount, totalCount);
                    data.put("completed", false); // 标记为未完成,因为还有未出片的玻璃
                    data.put("waiting", true);
                    data.put("waitingReason", "moreGlassToOutbound");
                    data.put("completedCount", completedCount);
                    data.put("totalCount", totalCount);
                    return DevicePlcVO.OperationResult.builder()
                            .success(true)
                            .message(String.format("出片任务本次交互完成:MES已确认(mesConfirm=1),已清空state和汇报字。%s。大车空闲(plcRequest=1),等待MES发送下次任务", progressMessage))
                            .data(data)
                            .build();
                }
                // 所有玻璃都已出片,正常完成流程
                // MES已确认,清空state和汇报字
                // MES已确认:本次交互完成(不在设备侧判断“是否还有更多玻璃”,由任务引擎统一编排)
                clearTaskStates(deviceConfig, serializer);
                // 记录已完成的任务签名,避免MES未复位时被重复拉起
                lastCompletedMesRecords.put(deviceId,
                        new CompletedMesRecord(taskInfo.mesSignature, System.currentTimeMillis()));
                if (taskInfo != null && taskInfo.mesSignature != null) {
                    lastCompletedMesRecords.put(deviceId,
                            new CompletedMesRecord(taskInfo.mesSignature, System.currentTimeMillis()));
                }
                // 任务完成,恢复为空闲状态
                statusManager.updateVehicleStatus(
@@ -2246,10 +2233,10 @@
                plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
                log.info("MES任务已确认完成: deviceId={}", deviceConfig.getDeviceId());
                String taskType = taskInfo.isOutbound ? "出片" : "进片";
                String taskType = (taskInfo != null && taskInfo.isOutbound) ? "出片" : "进片";
                return DevicePlcVO.OperationResult.builder()
                        .success(true)
                        .message(String.format("%s任务完成:MES已确认(mesConfirm=1),已清空state和汇报字,大车空闲(plcRequest=1),可以等待下次任务", taskType))
                        .message(String.format("%s任务交互完成:MES已确认(mesConfirm=1),已清空state和汇报字,大车空闲(plcRequest=1)", taskType))
                        .data(data)
                        .build();
            }
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
@@ -144,12 +144,11 @@
        saveScannedGlassId(params, glassId);
        Integer intervalMs = config != null ? config.getScanIntervalMs() : null;
        String msg = String.format("玻璃[%s] 尺寸[宽:%s x 长:%s] 已接收,workLine=%s,扫描间隔=%s",
        String msg = String.format("玻璃[%s] 尺寸[宽:%s x 长:%s] 已接收,workLine=%s",
                glassId,
                rawWidth != null ? rawWidth + "mm" : "-",
                rawHeight != null ? rawHeight + "mm" : "-",
                workLine != null ? workLine : "-",
                intervalMs != null ? intervalMs + "ms" : "-");
                workLine != null ? workLine : "-");
        Map<String, Object> resultData = new HashMap<>();
        resultData.put("glassIds", Collections.singletonList(glassId));
        if (workLine != null) {
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -247,17 +247,11 @@
                // 4. 大理片笼设备:启动定时器逻辑处理(不涉及PLC交互,只负责逻辑处理)
                if (isLargeGlass) {
                    TaskStepDetail step = createStepRecord(task, device, currentOrder);
                    ScheduledFuture<?> largeGlassTask = startLargeGlassTimer(task, step, device, context);
                    if (largeGlassTask != null) {
                        registerScheduledTask(task.getTaskId(), largeGlassTask);
                        stepSummaries.add(createStepSummary(device.getDeviceName(), true, "大理片笼定时器已启动,逻辑处理中"));
                    } else {
                        stepSummaries.add(createStepSummary(device.getDeviceName(), false, "启动定时器失败"));
                        success = false;
                        failureMessage = "大理片笼设备启动定时器失败";
                        break;
                    }
                    // 等进片大车步骤真正完成后再启动大理片笼定时器,保证执行顺序为:进片大车 -> 大理片笼
                    context.getSharedData().put("largeGlassStepId", step.getId());
                    context.getSharedData().put("largeGlassDeviceId", device.getId());
                    stepSummaries.add(createStepSummary(device.getDeviceName(), true, "已创建大理片笼步骤,等待进片大车完成后启动定时器"));
                    currentOrder++;
                    continue;
                }
@@ -723,14 +717,24 @@
                                    taskStepDetailMapper.updateById(step);
                                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                                }
                                // 继续轮询MES任务/确认状态,若MES确认完成会在内部更新步骤为COMPLETED
                                pollMesForVehicle(task, step, device, context);
                                // 如果进片大车步骤在轮询后已完成,则尝试启动大理片笼定时器
                                if (TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                                    startLargeGlassTimerIfNeeded(task, context);
                                }
                                return;
                            }
                        }
                        
                        // 如果大车已经装载过玻璃(RUNNING状态),轮询MES任务/确认状态
                        if (TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
                            // 轮询MES任务/确认状态,若MES确认完成会在内部更新步骤为COMPLETED
                            pollMesForVehicle(task, step, device, context);
                            // 如果进片大车步骤在轮询后已完成,则尝试启动大理片笼定时器
                            if (TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                                startLargeGlassTimerIfNeeded(task, context);
                            }
                        } else {
                            // 如果还没有装载过玻璃,等待卧转立输出
                            if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())) {
@@ -894,6 +898,11 @@
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                        }
                        // 当进片大车步骤真正完成后,再启动大理片笼定时器,保证执行顺序
                        if (TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                            startLargeGlassTimerIfNeeded(task, context);
                        }
                    }
                } catch (Exception e) {
                    log.error("进片大车设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
@@ -919,11 +928,10 @@
                                                         TaskExecutionContext context) {
        try {
            final long MONITOR_INTERVAL_MS = 2_000; // 2秒监控一次
            log.debug("启动出片大车设备定时器: taskId={}, deviceId={}, interval={}s",
                    task.getTaskId(), device.getId(), MONITOR_INTERVAL_MS / 1000);
            // 启动定时任务
            ScheduledFuture<?> future = scheduledExecutor.scheduleWithFixedDelay(() -> {
                try {
                    if (isTaskCancelled(context)) {
@@ -931,271 +939,242 @@
                                task.getTaskId(), device.getId());
                        return;
                    }
                    // 出片大车设备:只有在真正开始处理时才设置为RUNNING
                    // 检查是否有已处理的玻璃信息(从大理片笼来的)
                    List<String> processedGlassIds = getProcessedGlassIds(context);
                    boolean isRunning = TaskStepDetail.Status.RUNNING.name().equals(step.getStatus());
                    boolean isCompleted = TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus());
                    // 获取已出片的玻璃ID列表(在方法开始处声明,避免重复定义)
                    List<String> outboundGlassIds = getOutboundGlassIds(context);
                    // 如果步骤已完成,检查是否所有初始玻璃都已出片
                    if (isCompleted) {
                        @SuppressWarnings("unchecked")
                        List<String> initialGlassIds = (List<String>) context.getSharedData().get("initialGlassIds");
                        // 如果还有未出片的玻璃,重置步骤状态为RUNNING,继续等待
                        if (initialGlassIds != null && !initialGlassIds.isEmpty()
                                && (outboundGlassIds == null || !outboundGlassIds.containsAll(initialGlassIds))) {
                            log.info("出片大车步骤已完成,但还有未出片的玻璃,重置为RUNNING继续等待: taskId={}, deviceId={}, initialCount={}, outboundCount={}",
                                    task.getTaskId(), device.getId(),
                                    initialGlassIds.size(),
                                    outboundGlassIds != null ? outboundGlassIds.size() : 0);
                            step.setStatus(TaskStepDetail.Status.RUNNING.name());
                            step.setEndTime(null); // 清除结束时间
                            step.setSuccessMessage("等待剩余玻璃出片");
                    final String lastMsgKey = "outboundVehicleLastMessage:" + device.getId();
                    // 1) 总目标:大理片笼产出的“应出片玻璃列表”
                    List<String> processedGlassIdsRaw = getProcessedGlassIds(context);
                    if (CollectionUtils.isEmpty(processedGlassIdsRaw)) {
                        // 尚未有大理片笼输出,保持等待
                        deviceCoordinationService.syncDeviceStatus(device,
                                DeviceCoordinationService.DeviceStatus.WAITING, context);
                        if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())
                                && !TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                            step.setStatus(TaskStepDetail.Status.PENDING.name());
                            step.setSuccessMessage("等待大理片笼处理完成");
                            if (step.getStartTime() == null) {
                                step.setStartTime(new Date());
                            }
                            taskStepDetailMapper.updateById(step);
                            notificationService.notifyStepUpdate(task.getTaskId(), step);
                            // 继续执行后续逻辑,检查是否有新的已处理玻璃
                        } else {
                            // 所有玻璃都已出片,保持完成状态
                            log.debug("出片大车所有玻璃都已出片: taskId={}, deviceId={}, initialCount={}, outboundCount={}",
                                    task.getTaskId(), device.getId(),
                                    initialGlassIds != null ? initialGlassIds.size() : 0,
                                    outboundGlassIds != null ? outboundGlassIds.size() : 0);
                            return;
                        }
                    }
                    // 如果没有已处理玻璃,则不应主动把步骤拉到RUNNING,只保持已运行状态
                    if (CollectionUtils.isEmpty(processedGlassIds)) {
                        if (isRunning || isCompleted) {
                            // 已经在运行的情况下,继续轮询MES任务/确认,避免错过确认
                            DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
                            if (handler != null) {
                                Map<String, Object> logicParams = parseLogicParams(device);
                                // 先检查MES任务(如果mesSend=1,会创建任务并开始执行)
                                DevicePlcVO.OperationResult mesTaskResult = null;
                                try {
                                    mesTaskResult = handler.execute(device, "checkMesTask", Collections.emptyMap());
                                    if (mesTaskResult != null) {
                                        if (Boolean.TRUE.equals(mesTaskResult.getSuccess())) {
                                            log.info("出片大车设备已检查MES任务并开始执行: taskId={}, deviceId={}, message={}",
                                                    task.getTaskId(), device.getId(), mesTaskResult.getMessage());
                                        } else {
                                            log.debug("出片大车设备检查MES任务,等待中: taskId={}, deviceId={}, message={}",
                                                    task.getTaskId(), device.getId(), mesTaskResult.getMessage());
                                        }
                                    }
                                } catch (Exception e) {
                                    log.warn("出片大车设备检查MES任务异常: taskId={}, deviceId={}, error={}",
                                            task.getTaskId(), device.getId(), e.getMessage());
                                }
                                // 然后检查MES确认状态(只有在任务已开始执行时才检查)
                                DevicePlcVO.OperationResult mesResult = null;
                                try {
                                    Map<String, Object> checkParams = new HashMap<>();
                                    checkParams.put("_taskContext", context);
                                    mesResult = handler.execute(device, "checkMesConfirm", checkParams);
                                } catch (Exception e) {
                                    log.warn("出片大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}",
                                            task.getTaskId(), device.getId(), e.getMessage());
                                }
                                // 更新步骤状态(大车设备保持RUNNING,直到MES确认完成或任务取消)
                                if (mesResult != null) {
                                    updateStepStatusForVehicle(task.getTaskId(), step, mesResult);
                                    boolean opSuccess = Boolean.TRUE.equals(mesResult.getSuccess());
                                    updateTaskProgress(task, step.getStepOrder(), opSuccess);
                                    if (!opSuccess) {
                                        deviceCoordinationService.syncDeviceStatus(device,
                                                DeviceCoordinationService.DeviceStatus.FAILED, context);
                                    }
                                }
                            }
                        } else {
                            // 未运行且没有已处理玻璃,保持PENDING
                            if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())
                                    && !TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                                step.setStatus(TaskStepDetail.Status.PENDING.name());
                                step.setSuccessMessage("等待大理片笼处理完成");
                                taskStepDetailMapper.updateById(step);
                                notificationService.notifyStepUpdate(task.getTaskId(), step);
                            }
                            log.debug("出片大车设备定时器:暂无已处理的玻璃信息: taskId={}, deviceId={}",
                                    task.getTaskId(), device.getId());
                        }
                        return;
                    }
                    log.debug("出片大车设备定时器检测到已处理的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                            task.getTaskId(), device.getId(), processedGlassIds.size());
                    // 过滤出还未出片的玻璃(支持分批出片)
                    // 重新获取已出片的玻璃ID列表(可能在上面的逻辑中已更新)
                    outboundGlassIds = getOutboundGlassIds(context);
                    List<String> glassIdsToOutbound = new ArrayList<>();
                    for (String glassId : processedGlassIds) {
                        if (outboundGlassIds == null || !outboundGlassIds.contains(glassId)) {
                            glassIdsToOutbound.add(glassId);
                    // 2) 已完成累积:outboundGlassIds(任务侧维护,按mesConfirm=1累加)
                    Set<String> processedSet = new LinkedHashSet<>();
                    for (String id : processedGlassIdsRaw) {
                        if (StringUtils.hasText(id)) {
                            processedSet.add(id);
                        }
                    }
                    // 如果没有需要出片的玻璃(都已经出片过了),继续等待新的已处理玻璃
                    if (glassIdsToOutbound.isEmpty()) {
                        log.debug("出片大车已处理的玻璃都已出片,等待新的已处理玻璃: taskId={}, deviceId={}",
                                task.getTaskId(), device.getId());
                    Set<String> outboundSet = new HashSet<>();
                    for (String id : getOutboundGlassIds(context)) {
                        if (StringUtils.hasText(id)) {
                            outboundSet.add(id);
                        }
                    }
                    List<String> remaining = new ArrayList<>();
                    for (String id : processedSet) {
                        if (!outboundSet.contains(id)) {
                            remaining.add(id);
                        }
                    }
                    int total = processedSet.size();
                    int done = total - remaining.size();
                    // 3) 若已全部完成,直接收尾步骤
                    if (total > 0 && remaining.isEmpty()) {
                        if (!TaskStepDetail.Status.COMPLETED.name().equals(step.getStatus())) {
                            step.setStatus(TaskStepDetail.Status.COMPLETED.name());
                            String lastMsg = null;
                            Object lastObj = context.getSharedData().get(lastMsgKey);
                            if (lastObj != null && StringUtils.hasText(String.valueOf(lastObj))) {
                                lastMsg = String.valueOf(lastObj);
                            }
                            step.setSuccessMessage(StringUtils.hasText(lastMsg)
                                    ? String.format("出片完成:%d/%d;%s", done, total, lastMsg)
                                    : String.format("出片完成:%d/%d", done, total));
                            step.setErrorMessage(null);
                            Date now = new Date();
                            if (step.getStartTime() == null) {
                                step.setStartTime(now);
                            }
                            if (step.getEndTime() == null) {
                                step.setEndTime(now);
                            }
                            if (step.getStartTime() != null && step.getEndTime() != null) {
                                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
                            }
                            taskStepDetailMapper.updateById(step);
                            notificationService.notifyStepUpdate(task.getTaskId(), step);
                            checkAndCompleteTaskIfDone(step.getTaskId());
                        }
                        deviceCoordinationService.syncDeviceStatus(device,
                                DeviceCoordinationService.DeviceStatus.COMPLETED, context);
                        return;
                    }
                    log.debug("出片大车准备出片: taskId={}, deviceId={}, 待出片数量={}, 已出片数量={}",
                            task.getTaskId(), device.getId(), glassIdsToOutbound.size(),
                            outboundGlassIds != null ? outboundGlassIds.size() : 0);
                    // 执行出片操作
                    Map<String, Object> checkParams = new HashMap<>();
                    checkParams.put("glassIds", glassIdsToOutbound);
                    checkParams.put("_taskContext", context);
                    // 4) 进入运行态(只在真正开始出片时)
                    if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
                        step.setStatus(TaskStepDetail.Status.RUNNING.name());
                        if (step.getStartTime() == null) {
                            step.setStartTime(new Date());
                        }
                    }
                    // 更新进度信息(便于前端实时展示)
                    Date now = new Date();
                    if (step.getStartTime() != null) {
                        step.setDurationMs(now.getTime() - step.getStartTime().getTime());
                    }
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
                    if (handler != null) {
                        // 将logicParams合并到checkParams中
                        Map<String, Object> logicParams = parseLogicParams(device);
                        if (logicParams != null && !logicParams.isEmpty()) {
                            checkParams.put("_logicParams", logicParams);
                        }
                        // 第一步:写入大车出片请求
                        DevicePlcVO.OperationResult feedResult = handler.execute(device, "feedGlass", checkParams);
                        if (Boolean.TRUE.equals(feedResult.getSuccess())) {
                            // 真正开始处理,设置为RUNNING
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.RUNNING, context);
                            // 步骤状态也设置为RUNNING
                            if (!TaskStepDetail.Status.RUNNING.name().equals(step.getStatus())) {
                                step.setStatus(TaskStepDetail.Status.RUNNING.name());
                                if (step.getStartTime() == null) {
                                    step.setStartTime(new Date());
                    if (handler == null) {
                        log.warn("未找到出片大车handler: deviceId={}, deviceType={}", device.getId(), device.getDeviceType());
                        return;
                    }
                    Map<String, Object> logicParams = parseLogicParams(device);
                    Map<String, Object> baseParams = new HashMap<>();
                    baseParams.put("_taskContext", context);
                    if (logicParams != null && !logicParams.isEmpty()) {
                        baseParams.put("_logicParams", logicParams);
                    }
                    String latestInteractionMsg = null;
                    // 5) 先检查MES任务(mesSend=1时会把本批次玻璃ID写入currentMesBatchGlassIds)
                    DevicePlcVO.OperationResult mesTaskResult = null;
                    try {
                        mesTaskResult = handler.execute(device, "checkMesTask", new HashMap<>(baseParams));
                    } catch (Exception e) {
                        log.warn("出片大车检查MES任务异常: taskId={}, deviceId={}, error={}",
                                task.getTaskId(), device.getId(), e.getMessage());
                    }
                    if (mesTaskResult != null && StringUtils.hasText(mesTaskResult.getMessage())) {
                        latestInteractionMsg = mesTaskResult.getMessage();
                    }
                    // 从checkMesTask返回值中读取本批次玻璃ID(不依赖设备侧写_taskContext)
                    try {
                        if (mesTaskResult != null && mesTaskResult.getData() != null) {
                            Object batchObj = mesTaskResult.getData().get("batchGlassIds");
                            if (batchObj instanceof List) {
                                @SuppressWarnings("unchecked")
                                List<Object> raw = (List<Object>) batchObj;
                                List<String> batchIds = new ArrayList<>();
                                for (Object o : raw) {
                                    if (o != null && StringUtils.hasText(String.valueOf(o))) {
                                        batchIds.add(String.valueOf(o));
                                    }
                                }
                                taskStepDetailMapper.updateById(step);
                                notificationService.notifyStepUpdate(task.getTaskId(), step);
                            }
                            log.debug("出片大车设备定时器执行成功: taskId={}, deviceId={}, glassCount={}",
                                    task.getTaskId(), device.getId(), glassIdsToOutbound.size());
                            // 记录已出片的玻璃ID(只记录本次出片的玻璃)
                            addOutboundGlassIds(context, glassIdsToOutbound);
                            // 从processedGlassIds中移除已出片的玻璃,保留未出片的玻璃
                            processedGlassIds.removeAll(glassIdsToOutbound);
                            // 如果还有未出片的玻璃,不清空processedGlassIds;如果全部出片了,清空
                            if (processedGlassIds.isEmpty()) {
                                clearProcessedGlassIds(context);
                            } else {
                                setProcessedGlassIds(context, processedGlassIds);
                            }
                            // feedGlass成功后,先检查MES任务(checkMesTask)来开始执行任务
                            DevicePlcVO.OperationResult mesTaskResult = null;
                            try {
                                mesTaskResult = handler.execute(device, "checkMesTask", Collections.emptyMap());
                                if (mesTaskResult != null && Boolean.TRUE.equals(mesTaskResult.getSuccess())) {
                                    log.info("出片大车设备已检查MES任务并开始执行: taskId={}, deviceId={}, message={}",
                                            task.getTaskId(), device.getId(), mesTaskResult.getMessage());
                                if (!batchIds.isEmpty()) {
                                    context.getSharedData().put("currentMesBatchGlassIds", batchIds);
                                }
                            } catch (Exception e) {
                                log.warn("出片大车设备检查MES任务异常: taskId={}, deviceId={}, error={}",
                                        task.getTaskId(), device.getId(), e.getMessage());
                            }
                        } else {
                            // 没有数据,保持WAITING状态和PENDING步骤状态
                            deviceCoordinationService.syncDeviceStatus(device,
                                    DeviceCoordinationService.DeviceStatus.WAITING, context);
                            if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())) {
                                step.setStatus(TaskStepDetail.Status.PENDING.name());
                                step.setSuccessMessage("等待中");
                                taskStepDetailMapper.updateById(step);
                                notificationService.notifyStepUpdate(task.getTaskId(), step);
                            }
                            log.debug("出片大车设备定时器执行失败: taskId={}, deviceId={}, message={}",
                                    task.getTaskId(), device.getId(), feedResult.getMessage());
                        }
                        // 第二步:检查MES确认状态(如果大车处理器支持的话)
                        // 只有在任务已开始执行(有任务记录)时才检查MES确认
                        DevicePlcVO.OperationResult mesResult = null;
                    } catch (Exception ignore) {
                        // 不影响主流程
                    }
                    boolean mesSendIsZero = false;
                    if (mesTaskResult != null && mesTaskResult.getData() != null) {
                        Object reason = mesTaskResult.getData().get("waitingReason");
                        if ("mesSend=0".equals(String.valueOf(reason))) {
                            mesSendIsZero = true;
                        }
                    } else if (mesTaskResult != null && StringUtils.hasText(mesTaskResult.getMessage())
                            && mesTaskResult.getMessage().contains("mesSend=0")) {
                        mesSendIsZero = true;
                    }
                    // 6) 若MES尚未下发任务(mesSend=0),触发一次出片请求(feedGlass)
                    if (mesSendIsZero && !remaining.isEmpty()) {
                        Map<String, Object> feedParams = new HashMap<>(baseParams);
                        feedParams.put("glassIds", new ArrayList<>(remaining));
                        try {
                            Map<String, Object> confirmParams = new HashMap<>();
                            confirmParams.put("_taskContext", context);
                            mesResult = handler.execute(device, "checkMesConfirm", confirmParams);
                            DevicePlcVO.OperationResult feedResult = handler.execute(device, "feedGlass", feedParams);
                            if (feedResult != null && StringUtils.hasText(feedResult.getMessage())) {
                                latestInteractionMsg = feedResult.getMessage();
                            }
                            if (Boolean.TRUE.equals(feedResult.getSuccess())) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.RUNNING, context);
                            } else {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.WAITING, context);
                            }
                        } catch (Exception e) {
                            log.warn("出片大车设备检查MES确认状态异常: taskId={}, deviceId={}, error={}",
                            log.warn("出片大车触发feedGlass异常: taskId={}, deviceId={}, error={}",
                                    task.getTaskId(), device.getId(), e.getMessage());
                        }
                        // 对于出片大车,需要检查是否所有初始玻璃都已出片
                        // 如果MES返回completed=true,但还有未出片的玻璃,则不应标记为完成
                        if (mesResult != null && mesResult.getData() != null) {
                            Object completedFlag = mesResult.getData().get("completed");
                            boolean mesCompleted = false;
                            if (completedFlag instanceof Boolean) {
                                mesCompleted = (Boolean) completedFlag;
                            } else if (completedFlag != null) {
                                mesCompleted = "true".equalsIgnoreCase(String.valueOf(completedFlag));
                            }
                            // 如果MES返回completed=true,检查是否所有初始玻璃都已出片
                            if (mesCompleted) {
                                @SuppressWarnings("unchecked")
                                List<String> initialGlassIds = (List<String>) context.getSharedData().get("initialGlassIds");
                                // 重新获取已出片的玻璃ID列表(可能在上面的逻辑中已更新)
                                outboundGlassIds = getOutboundGlassIds(context);
                                // 如果还有未出片的玻璃,修改mesResult,将completed设为false
                                if (initialGlassIds != null && !initialGlassIds.isEmpty()
                                        && (outboundGlassIds == null || !outboundGlassIds.containsAll(initialGlassIds))) {
                                    log.debug("出片大车MES返回completed=true,但还有未出片的玻璃: taskId={}, deviceId={}, initialCount={}, outboundCount={}",
                                            task.getTaskId(), device.getId(),
                                            initialGlassIds.size(),
                                            outboundGlassIds != null ? outboundGlassIds.size() : 0);
                                    // 修改mesResult,将completed设为false,保持RUNNING状态
                                    Map<String, Object> modifiedData = new HashMap<>(mesResult.getData());
                                    modifiedData.put("completed", false);
                                    DevicePlcVO.OperationResult modifiedResult = new DevicePlcVO.OperationResult();
                                    modifiedResult.setSuccess(mesResult.getSuccess());
                                    modifiedResult.setMessage(mesResult.getMessage());
                                    modifiedResult.setData(modifiedData);
                                    mesResult = modifiedResult;
                    }
                    // 7) 再检查MES确认(mesConfirm=1表示本次交互完成)
                    DevicePlcVO.OperationResult mesConfirmResult = null;
                    try {
                        mesConfirmResult = handler.execute(device, "checkMesConfirm", new HashMap<>(baseParams));
                    } catch (Exception e) {
                        log.warn("出片大车检查MES确认异常: taskId={}, deviceId={}, error={}",
                                task.getTaskId(), device.getId(), e.getMessage());
                    }
                    if (mesConfirmResult != null && StringUtils.hasText(mesConfirmResult.getMessage())) {
                        // 确认提示优先级最高
                        latestInteractionMsg = mesConfirmResult.getMessage();
                    }
                    boolean interactionCompleted = false;
                    if (mesConfirmResult != null && mesConfirmResult.getData() != null) {
                        Object completedFlag = mesConfirmResult.getData().get("completed");
                        if (completedFlag instanceof Boolean) {
                            interactionCompleted = (Boolean) completedFlag;
                        } else if (completedFlag != null) {
                            interactionCompleted = "true".equalsIgnoreCase(String.valueOf(completedFlag));
                        }
                    }
                    // 8) 更新“最近一次交互提示”与步骤提示文案(避免被纯进度覆盖)
                    if (StringUtils.hasText(latestInteractionMsg)) {
                        context.getSharedData().put(lastMsgKey, latestInteractionMsg);
                    }
                    String lastMsg = null;
                    Object lastObj = context.getSharedData().get(lastMsgKey);
                    if (lastObj != null && StringUtils.hasText(String.valueOf(lastObj))) {
                        lastMsg = String.valueOf(lastObj);
                    }
                    step.setSuccessMessage(StringUtils.hasText(lastMsg)
                            ? String.format("出片进行中:%d/%d;%s", done, total, lastMsg)
                            : String.format("出片进行中:%d/%d", done, total));
                    taskStepDetailMapper.updateById(step);
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    if (interactionCompleted) {
                        // 本次交互完成:将本批次(currentMesBatchGlassIds)累加到已出片(outboundGlassIds)
                        Object batchObj = context.getSharedData().get("currentMesBatchGlassIds");
                        if (batchObj instanceof List) {
                            @SuppressWarnings("unchecked")
                            List<String> batchIds = new ArrayList<>((List<String>) batchObj);
                            // 仅累加“目标集合”内的玻璃,避免脏数据
                            List<String> filtered = new ArrayList<>();
                            for (String id : batchIds) {
                                if (StringUtils.hasText(id) && processedSet.contains(id)) {
                                    filtered.add(id);
                                }
                            }
                            addOutboundGlassIds(context, filtered);
                        }
                        // 更新步骤状态(大车设备保持RUNNING,直到MES确认完成或任务取消)
                        if (mesResult != null) {
                            updateStepStatusForVehicle(task.getTaskId(), step, mesResult);
                            boolean opSuccess = Boolean.TRUE.equals(mesResult.getSuccess());
                            updateTaskProgress(task, step.getStepOrder(), opSuccess);
                            if (!opSuccess) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                        } else {
                            updateStepStatusForVehicle(task.getTaskId(), step, feedResult);
                            boolean opSuccess = Boolean.TRUE.equals(feedResult.getSuccess());
                            updateTaskProgress(task, step.getStepOrder(), opSuccess);
                            if (!opSuccess) {
                                deviceCoordinationService.syncDeviceStatus(device,
                                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                            }
                        }
                        // 清空本批次记录,避免下一次轮询重复使用旧批次
                        context.getSharedData().put("currentMesBatchGlassIds", new ArrayList<>());
                    }
                } catch (Exception e) {
                    log.error("出片大车设备定时器执行异常: taskId={}, deviceId={}", task.getTaskId(), device.getId(), e);
                }
            }, 0, MONITOR_INTERVAL_MS, TimeUnit.MILLISECONDS);
            // 在串行执行模式下,设备启动定时器时先设置为 WAITING,定时器第一次执行时再设置为 RUNNING
            // 启动时保持WAITING,后续由定时器逻辑切换为RUNNING/COMPLETED
            deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.WAITING, context);
            return future;
@@ -1347,6 +1326,8 @@
                    }
                    step.setOutputData(toJson(Collections.singletonMap("glassIds", loadedGlassIds)));
                    taskStepDetailMapper.updateById(step);
                    // 通知前端状态更新
                    notificationService.notifyStepUpdate(task.getTaskId(), step);
                    // 大理片笼完成后尝试自动收尾整个任务
                    checkAndCompleteTaskIfDone(step.getTaskId());
                    
@@ -1401,17 +1382,16 @@
    
    /**
     * 设置已装载的玻璃ID列表
     * 对于分批出片场景,每次设置应替换为当前批次的玻璃ID
     */
    private void setLoadedGlassIds(TaskExecutionContext context, List<String> glassIds) {
        if (context != null) {
            // 累加记录,避免后续 containsAll 判断因覆盖丢失历史玻璃而回退为等待
            List<String> merged = new ArrayList<>(getLoadedGlassIds(context)); // 确保可变
            // 替换为当前批次的玻璃ID
            List<String> currentBatch = new ArrayList<>();
            if (glassIds != null) {
                merged.addAll(glassIds);
                currentBatch.addAll(glassIds);
            }
            // 去重
            List<String> distinct = merged.stream().distinct().collect(java.util.stream.Collectors.toList());
            context.getSharedData().put("loadedGlassIds", distinct);
            context.getSharedData().put("loadedGlassIds", currentBatch);
        }
    }
    
@@ -1690,6 +1670,8 @@
            
            // 所有步骤都已完成,收尾任务
            task.setStatus(MultiDeviceTask.Status.COMPLETED.name());
            // 关键:同步进度到最终值,避免前端仍显示“3/5”这类旧进度
            task.setCurrentStep(totalSteps);
            task.setEndTime(new Date());
            multiDeviceTaskMapper.updateById(task);
            
@@ -1922,7 +1904,9 @@
            // 先检查MES任务(如果mesSend=1,会创建任务并开始执行)
            DevicePlcVO.OperationResult mesTaskResult = null;
            try {
                mesTaskResult = handler.execute(device, "checkMesTask", Collections.emptyMap());
                Map<String, Object> taskParams = new HashMap<>();
                taskParams.put("_taskContext", context);
                mesTaskResult = handler.execute(device, "checkMesTask", taskParams);
                if (mesTaskResult != null && Boolean.TRUE.equals(mesTaskResult.getSuccess())) {
                    log.info("大车设备已检查MES任务并开始执行: taskId={}, deviceId={}, message={}",
                            task.getTaskId(), device.getId(), mesTaskResult.getMessage());
@@ -2809,6 +2793,80 @@
    }
    /**
     * 当进片大车步骤完成后,如果存在大理片笼设备且尚未启动其定时器,则启动大理片笼定时器
     * 保证执行顺序为:进片大车 -> 大理片笼
     */
    private void startLargeGlassTimerIfNeeded(MultiDeviceTask task, TaskExecutionContext context) {
        if (task == null || context == null) {
            return;
        }
        // 防止重复启动
        Object startedFlag = context.getSharedData().get("largeGlassTimerStarted");
        if (startedFlag instanceof Boolean && (Boolean) startedFlag) {
            return;
        }
        try {
            // 从上下文中获取设备列表
            @SuppressWarnings("unchecked")
            List<DeviceConfig> devices = (List<DeviceConfig>) context.getSharedData().get("devices");
            if (devices == null || devices.isEmpty()) {
                return;
            }
            DeviceConfig largeGlassDevice = null;
            for (DeviceConfig device : devices) {
                if (DeviceConfig.DeviceType.LARGE_GLASS.equals(device.getDeviceType())) {
                    largeGlassDevice = device;
                    break;
                }
            }
            if (largeGlassDevice == null) {
                log.warn("未在设备列表中找到大理片笼设备: taskId={}", task.getTaskId());
                return;
            }
            // 重新加载大理片笼步骤,确保拿到最新状态(取该设备最新的一条步骤记录)
            List<TaskStepDetail> largeGlassSteps = taskStepDetailMapper.selectList(
                    Wrappers.<TaskStepDetail>lambdaQuery()
                            .eq(TaskStepDetail::getTaskId, task.getTaskId())
                            .eq(TaskStepDetail::getDeviceId, largeGlassDevice.getId())
                            .orderByDesc(TaskStepDetail::getStepOrder)
                            .last("LIMIT 1")
            );
            TaskStepDetail largeGlassStep = (largeGlassSteps != null && !largeGlassSteps.isEmpty())
                    ? largeGlassSteps.get(0)
                    : null;
            if (largeGlassStep == null) {
                log.warn("未找到大理片笼步骤记录: taskId={}, deviceId={}", task.getTaskId(), largeGlassDevice.getId());
                return;
            }
            // 如果步骤已经完成或失败,则不再启动定时器
            String status = largeGlassStep.getStatus();
            if (TaskStepDetail.Status.COMPLETED.name().equals(status)
                    || TaskStepDetail.Status.FAILED.name().equals(status)) {
                log.debug("大理片笼步骤已结束,不再启动定时器: taskId={}, stepId={}, status={}", task.getTaskId(), largeGlassStep.getId(), status);
                context.getSharedData().put("largeGlassTimerStarted", true);
                return;
            }
            ScheduledFuture<?> largeGlassTask = startLargeGlassTimer(task, largeGlassStep, largeGlassDevice, context);
            if (largeGlassTask != null) {
                registerScheduledTask(task.getTaskId(), largeGlassTask);
                context.getSharedData().put("largeGlassTimerStarted", true);
                log.info("已在进片大车完成后启动大理片笼定时器: taskId={}, largeGlassDeviceId={}, largeGlassStepId={}",
                        task.getTaskId(), largeGlassDevice.getId(), largeGlassStep.getId());
            } else {
                log.warn("在进片大车完成后启动大理片笼定时器失败: taskId={}, largeGlassDeviceId={}, largeGlassStepId={}",
                        task.getTaskId(), largeGlassDevice.getId(), largeGlassStep.getId());
            }
        } catch (Exception e) {
            log.warn("在进片大车完成后启动大理片笼定时器异常: taskId={}", task.getTaskId(), e);
        }
    }
    /**
     * 检查卧转立设备是否已完成
     * 返回true表示卧转立已完成(COMPLETED),可以判断大车是否完成
     * 返回false表示卧转立还在运行中(RUNNING)或等待中(PENDING),不应该标记大车为完成
mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue
@@ -79,6 +79,21 @@
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="8">
        <el-form-item label="处理时间(秒)">
          <el-input-number
            v-model="config.processTimeSeconds"
            :min="1"
            :max="3600"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">大理片笼处理玻璃的时间(秒),默认30秒</span>
        </el-form-item>
      </el-col>
    </el-row>
  </div>
</template>
@@ -102,7 +117,8 @@
  ],
  gridLength: 2000,
  gridWidth: 1500,
  gridThickness: 5
  gridThickness: 5,
  processTimeSeconds: 30
})
// 监听props变化
@@ -122,7 +138,8 @@
      gridRanges: gridRanges,
      gridLength: newVal.gridLength ?? 2000,
      gridWidth: newVal.gridWidth ?? 1500,
      gridThickness: newVal.gridThickness ?? 5
      gridThickness: newVal.gridThickness ?? 5,
      processTimeSeconds: newVal.processTimeSeconds ?? 30
    }
  }
}, { immediate: true, deep: true })
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -531,10 +531,10 @@
      headerStr === 'w' || headerStr === '宽度') {
      headerMap.width = index
    }
    // 高度
    else if (headerStr.includes('高') || headerStr.includes('height') ||
      headerStr === 'h' || headerStr === '高度') {
      headerMap.height = index
    // 长度
    else if (headerStr.includes('长') || headerStr.includes('length') ||
      headerStr === 'l' || headerStr === '长度') {
      headerMap.length = index
    }
    // 厚度
    else if (headerStr.includes('厚') || headerStr.includes('thickness') ||
@@ -570,10 +570,10 @@
  // 如果没有找到表头,尝试使用第一行作为表头(索引方式)
  if (Object.keys(headerMap).length === 0 && jsonData.length > 1) {
    // 默认格式:玻璃ID, 宽, 高, 厚, 数量(按列顺序)
    // 默认格式:玻璃ID, 宽, 长, 厚, 数量(按列顺序)
    headerMap.glassId = 0
    headerMap.width = 1
    headerMap.height = 2
    headerMap.length = 2
    headerMap.thickness = 3
    headerMap.quantity = 4
  }
@@ -586,7 +586,7 @@
    const glassId = row[headerMap.glassId] ? String(row[headerMap.glassId]).trim() : ''
    const width = row[headerMap.width] ? String(row[headerMap.width]).trim() : ''
    const height = row[headerMap.height] ? String(row[headerMap.height]).trim() : ''
    const length = row[headerMap.length] ? String(row[headerMap.length]).trim() : ''
    const thickness = row[headerMap.thickness] ? String(row[headerMap.thickness]).trim() : ''
    const quantity = row[headerMap.quantity] ? String(row[headerMap.quantity]).trim() : ''
    const filmsId = row[headerMap.filmsId] ? String(row[headerMap.filmsId]).trim() : ''
@@ -595,7 +595,7 @@
    const customerName = row[headerMap.customerName] ? String(row[headerMap.customerName]).trim() : ''
    // 跳过空行
    if (!glassId && !width && !height && !thickness && !quantity) {
    if (!glassId && !width && !length && !thickness && !quantity) {
      continue
    }
@@ -621,7 +621,7 @@
      result.push({
        glassId: finalGlassId,
        width: parseNumber(width),
        height: parseNumber(height),
        length: parseNumber(length),
        thickness: parseNumber(thickness),
        quantity: '1', // 每条记录数量为1
        filmsId: filmsId,