mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java
@@ -16,9 +16,8 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PreDestroy;
import javax.annotation.PreDestroy;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
@@ -92,6 +91,8 @@
                    return handleStopMonitor(deviceConfig);
                case "clearBuffer":
                    return handleClearBuffer(deviceConfig);
                case "clearPlc":
                    return handleClearPlc(deviceConfig);
                default:
                    return buildResult(deviceConfig, operation, false, 
                            "不支持的操作: " + operation);
@@ -124,46 +125,66 @@
        try {
            // 1. 从数据库查询最近扫码的玻璃信息(最近1分钟内的记录)
            List<GlassInfo> recentGlasses = queryRecentScannedGlasses(deviceConfig, logicParams);
            if (recentGlasses.isEmpty()) {
                return buildResult(deviceConfig, "checkAndProcess", true,
                        "暂无待处理的玻璃信息");
            boolean hasNewGlass = false;
            if (!recentGlasses.isEmpty()) {
                log.info("查询到最近扫码的玻璃: deviceId={}, count={}",
                        deviceId, recentGlasses.size());
                // 2. 更新缓冲队列;仅在有“新玻璃”加入缓冲时才更新最后扫码时间
                hasNewGlass = updateBuffer(deviceId, recentGlasses);
                if (hasNewGlass) {
                    lastScanTime
                            .computeIfAbsent(deviceId, k -> new AtomicLong())
                            .set(System.currentTimeMillis());
                }
            } else {
                log.debug("未查询到最近扫码的玻璃: deviceId={}", deviceId);
            }
            log.info("查询到最近扫码的玻璃: deviceId={}, count={}",
                    deviceId, recentGlasses.size());
            // 2. 更新缓冲队列;仅在有“新玻璃”加入缓冲时才更新最后扫码时间
            boolean hasNewGlass = updateBuffer(deviceId, recentGlasses);
            if (hasNewGlass) {
                lastScanTime
                        .computeIfAbsent(deviceId, k -> new AtomicLong())
                        .set(System.currentTimeMillis());
            }
            // 3. 检查是否需要立即处理(容量已满或30s内无新玻璃)
            // 3. 检查缓冲队列(即使查询不到新玻璃,缓冲中可能还有待处理的玻璃)
            List<GlassBufferItem> buffer = glassBuffer.get(deviceId);
            if (buffer == null || buffer.isEmpty()) {
                // 缓冲为空且无新玻璃,返回空状态
                return buildResult(deviceConfig, "checkAndProcess", true, 
                        "缓冲队列为空");
                        "缓冲队列为空,无待处理玻璃");
            }
            // 4. 判断是否满足处理条件
            boolean shouldProcess = shouldProcessBatch(deviceId, buffer, config);
            if (!shouldProcess) {
                return buildResult(deviceConfig, "checkAndProcess", true,
                        "等待更多玻璃或30s超时");
                // 未满足处理条件:构造带有等待进度的提示信息,便于前端展示
                String waitMessage;
                AtomicLong lastTime = lastScanTime.get(deviceId);
                Integer delayMs = config.getTransferDelayMs();
                if (lastTime != null && delayMs != null && delayMs > 0) {
                    long elapsedMs = System.currentTimeMillis() - lastTime.get();
                    if (elapsedMs < 0) {
                        elapsedMs = 0;
                    }
                    long totalMs = delayMs;
                    long elapsedSec = elapsedMs / 1000;
                    long totalSec = totalMs / 1000;
                    waitMessage = String.format("等待更多玻璃或超时触发批次处理 (已等待 %d/%d 秒)",
                            elapsedSec, totalSec);
                } else {
                    // 没有有效的最后扫码时间或配置,退回到固定提示
                    waitMessage = "等待更多玻璃或30s超时";
                }
                return buildResult(deviceConfig, "checkAndProcess", true, waitMessage);
            }
            // 5. 容量判断和批次组装
            List<GlassInfo> batch = assembleBatch(buffer, config.getVehicleCapacity());
            // 5. 容量判断和批次组装(考虑玻璃间隙)
            Integer glassGap = getLogicParam(logicParams, "glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm
            List<GlassInfo> batch = assembleBatch(buffer, config.getVehicleCapacity(), glassGap);
            if (batch.isEmpty()) {
                return buildResult(deviceConfig, "checkAndProcess", false, 
                        "无法组装有效批次(容量不足)");
            }
            // 6. 写入PLC
            // 6. 写入PLC(尝试从任务参数中获取卧转立编号)
            DevicePlcVO.OperationResult writeResult = writeBatchToPlc(
                    deviceConfig, batch, serializer, logicParams);
                    deviceConfig, batch, serializer, logicParams, params);
            
            if (!Boolean.TRUE.equals(writeResult.getSuccess())) {
                return writeResult;
@@ -198,9 +219,23 @@
                    batch.stream().map(GlassInfo::getGlassId).collect(Collectors.toList()),
                    GlassInfo.Status.PROCESSED);
            String msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s",
                    batch.size(),
                    batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(",")));
            // 8. 检查缓冲是否为空,如果为空且无新玻璃,标记为完成
            List<GlassBufferItem> remainingBuffer = glassBuffer.get(deviceId);
            boolean bufferEmpty = remainingBuffer == null || remainingBuffer.isEmpty();
            boolean noNewGlass = !hasNewGlass;
            String msg;
            if (bufferEmpty && noNewGlass) {
                // 缓冲已清空且无新玻璃,任务完成
                msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s, 缓冲已清空,任务完成",
                        batch.size(),
                        batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(",")));
            } else {
                // 缓冲还有玻璃或可能有新玻璃,继续运行
                msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s",
                        batch.size(),
                        batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(",")));
            }
            return buildResult(deviceConfig, "checkAndProcess", true, msg);
        } catch (Exception e) {
@@ -223,8 +258,8 @@
        }
        
        try {
            // 从配置中获取workLine,用于过滤
            String workLine = getLogicParam(logicParams, "workLine", null);
            // 从配置中获取workLine,用于过滤(配置中是Integer类型)
            Integer workLine = getLogicParam(logicParams, "workLine", null);
            
            // 查询最近2分钟内的玻璃记录(扩大时间窗口,确保不遗漏)
            Date twoMinutesAgo = new Date(System.currentTimeMillis() - 120000);
@@ -235,9 +270,9 @@
                   .orderByDesc(GlassInfo::getCreatedTime)
                   .last("LIMIT 20"); // 限制查询数量,避免过多
            
            // 如果配置了workLine,则过滤description
            if (workLine != null && !workLine.isEmpty()) {
                wrapper.like(GlassInfo::getDescription, "workLine=" + workLine);
            // 如果配置了workLine,则过滤work_line字段
            if (workLine != null) {
                wrapper.eq(GlassInfo::getWorkLine, workLine);
            }
            
            List<GlassInfo> recentGlasses = glassInfoMapper.selectList(wrapper);
@@ -280,17 +315,21 @@
    /**
     * 判断是否应该处理批次
     * 注意:这里只做粗略判断,精确的容量计算(含间隙)在assembleBatch中完成
     */
    private boolean shouldProcessBatch(String deviceId, 
                                      List<GlassBufferItem> buffer,
                                      WorkstationLogicConfig config) {
        // 条件1:缓冲队列已满(达到容量限制)
        // 粗略计算:所有玻璃长度之和(不考虑间隙,因为间隙是动态的)
        int totalLength = buffer.stream()
                .mapToInt(item -> item.glassInfo.getGlassLength() != null ? 
                        item.glassInfo.getGlassLength() : 0)
                .sum();
        if (totalLength >= config.getVehicleCapacity()) {
            log.info("缓冲队列容量已满,触发批次处理: deviceId={}, totalLength={}, capacity={}",
        // 粗略判断:如果总长度接近容量(留一些余量给间隙),就触发处理
        // 精确判断会在assembleBatch中完成
        if (totalLength >= config.getVehicleCapacity() * 0.8) { // 80%阈值,留余量给间隙
            log.info("缓冲队列容量接近满载,触发批次处理: deviceId={}, totalLength={}, capacity={}",
                    deviceId, totalLength, config.getVehicleCapacity());
            return true;
        }
@@ -310,25 +349,50 @@
    }
    /**
     * 组装批次(容量判断)
     * 组装批次(容量判断,考虑玻璃间隙)
     * @param buffer 缓冲队列
     * @param vehicleCapacity 车辆容量(mm)
     * @param glassGap 玻璃之间的物理间隔(mm),默认200mm
     * @return 组装好的批次列表
     */
    private List<GlassInfo> assembleBatch(List<GlassBufferItem> buffer, 
                                          int vehicleCapacity) {
                                          int vehicleCapacity,
                                          int glassGap) {
        List<GlassInfo> batch = new ArrayList<>();
        int usedLength = 0;
        int gap = Math.max(glassGap, 0); // 确保间隔不为负数
        
        for (GlassBufferItem item : buffer) {
            GlassInfo glass = item.glassInfo;
            int glassLength = glass.getGlassLength() != null ? 
                    glass.getGlassLength() : 0;
            
            if (usedLength + glassLength <= vehicleCapacity && batch.size() < 6) {
                batch.add(glass);
                usedLength += glassLength;
            if (glassLength <= 0) {
                continue; // 跳过无效长度的玻璃
            }
            if (batch.isEmpty()) {
                // 第一块玻璃,不需要间隙
                if (glassLength <= vehicleCapacity && batch.size() < 6) {
                    batch.add(glass);
                    usedLength = glassLength;
                } else {
                    break; // 第一块就装不下
                }
            } else {
                break;
                // 后续玻璃需要考虑间隙:玻璃长度 + 间隙
                int requiredLength = glassLength + gap;
                if (usedLength + requiredLength <= vehicleCapacity && batch.size() < 6) {
                    batch.add(glass);
                    usedLength += requiredLength; // 包含间隙
                } else {
                    break; // 装不下了
                }
            }
        }
        log.debug("批次组装完成: batchSize={}, usedLength={}, capacity={}, glassGap={}",
                batch.size(), usedLength, vehicleCapacity, gap);
        
        return batch;
    }
@@ -340,7 +404,8 @@
            DeviceConfig deviceConfig,
            List<GlassInfo> batch,
            EnhancedS7Serializer serializer,
            Map<String, Object> logicParams) {
            Map<String, Object> logicParams,
            Map<String, Object> params) {
        
        Map<String, Object> payload = new HashMap<>();
        
@@ -354,10 +419,33 @@
        // 写入玻璃数量
        payload.put("plcGlassCount", count);
        
        // 写入位置信息(如果有配置)
        Integer inPosition = getLogicParam(logicParams, "inPosition", null);
        // 写入卧转立编号(优先从任务参数获取,其次从设备配置获取)
        Integer inPosition = null;
        if (params != null) {
            try {
                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;
                    Object positionObj = ctx.getParameters().getExtra() != null
                            ? ctx.getParameters().getExtra().get("inPosition") : null;
                    if (positionObj instanceof Number) {
                        inPosition = ((Number) positionObj).intValue();
                    }
                }
            } catch (Exception e) {
                log.debug("从任务参数获取卧转立编号失败: deviceId={}", deviceConfig.getId(), e);
            }
        }
        // 如果任务参数中没有,从设备配置中获取
        if (inPosition == null) {
            inPosition = getLogicParam(logicParams, "inPosition", null);
        }
        if (inPosition != null) {
            payload.put("inPosition", inPosition);
            log.info("写入卧转立编号: deviceId={}, inPosition={}", deviceConfig.getId(), inPosition);
        } else {
            log.debug("未配置卧转立编号,跳过写入: deviceId={}", deviceConfig.getId());
        }
        
        // 写入请求字(触发大车)
@@ -365,8 +453,8 @@
        
        try {
            plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
            log.info("批次已写入PLC: deviceId={}, glassCount={}",
                    deviceConfig.getId(), count);
            log.info("批次已写入PLC: deviceId={}, glassCount={}, inPosition={}",
                    deviceConfig.getId(), count, inPosition);
            return buildResult(deviceConfig, "writeBatchToPlc", true, 
                    "批次写入成功");
        } catch (Exception e) {
@@ -450,6 +538,34 @@
        log.info("已清空缓冲队列: deviceId={}", deviceId);
        return buildResult(deviceConfig, "clearBuffer", true, "缓冲队列已清空");
    }
    /**
     * 清空PLC相关字段(供测试页面一键清空使用)
     */
    private DevicePlcVO.OperationResult handleClearPlc(DeviceConfig deviceConfig) {
        try {
            EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
            if (serializer == null) {
                return buildResult(deviceConfig, "clearPlc", false, "获取PLC序列化器失败");
            }
            Map<String, Object> payload = new HashMap<>();
            // 根据卧转立主体写入的字段进行清空
            for (int i = 1; i <= 6; i++) {
                payload.put("plcGlassId" + i, "");
            }
            payload.put("plcGlassCount", 0);
            payload.put("plcRequest", 0);
            payload.put("inPosition", 0);
            plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
            log.info("卧转立主体清空PLC字段完成: deviceId={}", deviceConfig.getId());
            return buildResult(deviceConfig, "clearPlc", true, "已清空卧转立主体PLC字段");
        } catch (Exception e) {
            log.error("卧转立主体清空PLC失败: deviceId={}", deviceConfig.getId(), e);
            return buildResult(deviceConfig, "clearPlc", false, "清空PLC失败: " + e.getMessage());
        }
    }
    /**
     * 构建操作结果