package com.mes.interaction.workstation.transfer.handler; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.mes.device.entity.DeviceConfig; import com.mes.device.entity.GlassInfo; import com.mes.device.mapper.DeviceGlassInfoMapper; import com.mes.device.service.DevicePlcOperationService; import com.mes.device.service.GlassInfoService; import com.mes.device.vo.DevicePlcVO; import com.mes.interaction.workstation.base.WorkstationBaseHandler; import com.mes.interaction.workstation.config.WorkstationLogicConfig; import com.mes.s7.enhanced.EnhancedS7Serializer; import com.mes.s7.provider.S7SerializerProvider; import com.mes.service.PlcDynamicDataService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import javax.annotation.PreDestroy; import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; /** * 卧转立主体设备逻辑处理器 * 负责玻璃缓冲、容量校验、批次组装、PLC写入等逻辑 */ @Slf4j @Component public class HorizontalTransferLogicHandler extends WorkstationBaseHandler { private final PlcDynamicDataService plcDynamicDataService; private final GlassInfoService glassInfoService; private final S7SerializerProvider s7SerializerProvider; @Autowired(required = false) private DeviceGlassInfoMapper glassInfoMapper; // 玻璃缓冲队列:deviceId -> 玻璃信息列表 private final Map> glassBuffer = new ConcurrentHashMap<>(); // 最后扫码时间:deviceId -> 最后扫码时间戳 private final Map lastScanTime = new ConcurrentHashMap<>(); // 监控任务:deviceId -> 监控任务 private final Map> monitorTasks = new ConcurrentHashMap<>(); // 监控线程池 private final ScheduledExecutorService monitorExecutor = Executors.newScheduledThreadPool(5, r -> { Thread t = new Thread(r, "HorizontalTransferMonitor"); t.setDaemon(true); return t; }); @Autowired public HorizontalTransferLogicHandler(DevicePlcOperationService devicePlcOperationService, PlcDynamicDataService plcDynamicDataService, @Qualifier("deviceGlassInfoService") GlassInfoService glassInfoService, S7SerializerProvider s7SerializerProvider) { super(devicePlcOperationService); this.plcDynamicDataService = plcDynamicDataService; this.glassInfoService = glassInfoService; this.s7SerializerProvider = s7SerializerProvider; } @Override public String getDeviceType() { return DeviceConfig.DeviceType.WORKSTATION_TRANSFER; } @Override protected DevicePlcVO.OperationResult doExecute(DeviceConfig deviceConfig, String operation, Map params, Map logicParams) { WorkstationLogicConfig config = parseWorkstationConfig(logicParams); try { switch (operation) { case "checkAndProcess": case "process": // 这里必须把 params 传进去,以便在多设备任务流程中 // 能够通过 _taskContext 将卧转立输出的玻璃ID写入任务上下文 return handleCheckAndProcess(deviceConfig, config, logicParams, params); case "startMonitor": return handleStartMonitor(deviceConfig, config, logicParams); case "stopMonitor": return handleStopMonitor(deviceConfig); case "clearBuffer": return handleClearBuffer(deviceConfig); case "clearPlc": return handleClearPlc(deviceConfig); default: return buildResult(deviceConfig, operation, false, "不支持的操作: " + operation); } } catch (Exception e) { log.error("卧转立主体处理异常: deviceId={}, operation={}", deviceConfig.getId(), operation, e); return buildResult(deviceConfig, operation, false, "处理异常: " + e.getMessage()); } } /** * 检查并处理玻璃批次 * 从数据库读取最近扫码的玻璃,进行容量判断,组装批次,写入PLC */ private DevicePlcVO.OperationResult handleCheckAndProcess( DeviceConfig deviceConfig, WorkstationLogicConfig config, Map logicParams, Map params) { String deviceId = deviceConfig.getDeviceId(); EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig); if (serializer == null) { return buildResult(deviceConfig, "checkAndProcess", false, "获取PLC序列化器失败"); } try { // 1. 从数据库查询最近扫码的玻璃信息(最近1分钟内的记录) List recentGlasses = queryRecentScannedGlasses(deviceConfig, logicParams); 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); } // 3. 检查缓冲队列(即使查询不到新玻璃,缓冲中可能还有待处理的玻璃) List buffer = glassBuffer.get(deviceId); if (buffer == null || buffer.isEmpty()) { // 缓冲为空且无新玻璃,返回空状态 return buildResult(deviceConfig, "checkAndProcess", true, "缓冲队列为空,无待处理玻璃"); } // 4. 判断是否满足处理条件 boolean shouldProcess = shouldProcessBatch(deviceId, buffer, config); if (!shouldProcess) { // 未满足处理条件:构造带有等待进度的提示信息,便于前端展示 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. 容量判断和批次组装(考虑玻璃间隙) Integer glassGap = getLogicParam(logicParams, "glassGap", 200); // 玻璃之间的物理间隔(mm),默认200mm List batch = assembleBatch(buffer, config.getVehicleCapacity(), glassGap); if (batch.isEmpty()) { return buildResult(deviceConfig, "checkAndProcess", false, "无法组装有效批次(容量不足)"); } // 6. 写入PLC(尝试从任务参数中获取卧转立编号) DevicePlcVO.OperationResult writeResult = writeBatchToPlc( deviceConfig, batch, serializer, logicParams, params); if (!Boolean.TRUE.equals(writeResult.getSuccess())) { return writeResult; } // 卧转立批次已成功写入PLC,将本批次玻璃ID写入任务上下文,供大车进片使用 try { if (params != null) { Object ctxObj = params.get("_taskContext"); if (ctxObj instanceof com.mes.task.model.TaskExecutionContext) { com.mes.task.model.TaskExecutionContext ctx = (com.mes.task.model.TaskExecutionContext) ctxObj; List batchGlassIds = batch.stream() .map(GlassInfo::getGlassId) .filter(Objects::nonNull) .collect(Collectors.toList()); if (!batchGlassIds.isEmpty()) { ctx.getSharedData().put("transferReadyGlassIds", new java.util.ArrayList<>(batchGlassIds)); log.info("卧转立已输出批次玻璃到任务上下文: deviceId={}, glassIds={}", deviceConfig.getId(), batchGlassIds); } } } } catch (Exception e) { log.warn("卧转立写入任务上下文transferReadyGlassIds失败: deviceId={}", deviceConfig.getId(), e); } // 7. 从缓冲队列中移除已处理的玻璃并更新状态 removeProcessedGlasses(deviceId, batch); glassInfoService.updateGlassStatus( batch.stream().map(GlassInfo::getGlassId).collect(Collectors.toList()), GlassInfo.Status.PROCESSED); // 8. 检查缓冲是否为空,如果为空且无新玻璃,标记为完成 List 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) { log.error("检查并处理玻璃批次异常: deviceId={}", deviceId, e); return buildResult(deviceConfig, "checkAndProcess", false, "处理异常: " + e.getMessage()); } } /** * 查询最近扫码的玻璃信息 */ private List queryRecentScannedGlasses( DeviceConfig deviceConfig, Map logicParams) { if (glassInfoMapper == null) { log.warn("GlassInfoMapper未注入,无法查询最近扫码的玻璃"); return Collections.emptyList(); } try { // 从配置中获取workLine,用于过滤(配置中是Integer类型) Integer workLine = getLogicParam(logicParams, "workLine", null); // 查询state=1的玻璃记录(已扫码交互完成,等待卧转立处理) LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.in(GlassInfo::getStatus, GlassInfo.Status.PENDING, GlassInfo.Status.ACTIVE) .eq(GlassInfo::getState, 1) // 只查询state=1的玻璃(已扫码完成) .orderByDesc(GlassInfo::getCreatedTime) .last("LIMIT 20"); // 限制查询数量,避免过多 // 如果配置了workLine,则过滤work_line字段 if (workLine != null) { wrapper.eq(GlassInfo::getWorkLine, workLine); } List recentGlasses = glassInfoMapper.selectList(wrapper); log.debug("查询到最近扫码的玻璃: deviceId={}, workLine={}, count={}", deviceConfig.getId(), workLine, recentGlasses.size()); return recentGlasses; } catch (Exception e) { log.error("查询最近扫码的玻璃信息异常: deviceId={}", deviceConfig.getId(), e); return Collections.emptyList(); } } /** * 更新缓冲队列 * @return 是否有新的玻璃被加入缓冲(用于判断是否刷新 lastScanTime) */ private boolean updateBuffer(String deviceId, List newGlasses) { List buffer = glassBuffer.computeIfAbsent( deviceId, k -> new CopyOnWriteArrayList<>()); Set existingIds = buffer.stream() .map(item -> item.glassInfo.getGlassId()) .collect(Collectors.toSet()); boolean hasNewGlass = false; for (GlassInfo glass : newGlasses) { if (!existingIds.contains(glass.getGlassId())) { buffer.add(new GlassBufferItem(glass, System.currentTimeMillis())); hasNewGlass = true; log.debug("添加玻璃到缓冲队列: deviceId={}, glassId={}", deviceId, glass.getGlassId()); } } return hasNewGlass; } /** * 判断是否应该处理批次 * 注意:这里只做粗略判断,精确的容量计算(含间隙)在assembleBatch中完成 */ private boolean shouldProcessBatch(String deviceId, List buffer, WorkstationLogicConfig config) { // 条件1:缓冲队列已满(达到容量限制) // 粗略计算:所有玻璃长度之和(不考虑间隙,因为间隙是动态的) int totalLength = buffer.stream() .mapToInt(item -> item.glassInfo.getGlassLength() != null ? item.glassInfo.getGlassLength() : 0) .sum(); // 粗略判断:如果总长度接近容量(留一些余量给间隙),就触发处理 // 精确判断会在assembleBatch中完成 if (totalLength >= config.getVehicleCapacity() * 0.8) { // 80%阈值,留余量给间隙 log.info("缓冲队列容量接近满载,触发批次处理: deviceId={}, totalLength={}, capacity={}", deviceId, totalLength, config.getVehicleCapacity()); return true; } // 条件2:30s内无新玻璃扫码 AtomicLong lastTime = lastScanTime.get(deviceId); if (lastTime != null) { long elapsed = System.currentTimeMillis() - lastTime.get(); if (elapsed >= config.getTransferDelayMs()) { log.info("30s内无新玻璃扫码,触发批次处理: deviceId={}, elapsed={}ms", deviceId, elapsed); return true; } } return false; } /** * 组装批次(容量判断,考虑玻璃间隙) * @param buffer 缓冲队列 * @param vehicleCapacity 车辆容量(mm) * @param glassGap 玻璃之间的物理间隔(mm),默认200mm * @return 组装好的批次列表 */ private List assembleBatch(List buffer, int vehicleCapacity, int glassGap) { List 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 (glassLength <= 0) { continue; // 跳过无效长度的玻璃 } if (batch.isEmpty()) { // 第一块玻璃,不需要间隙 if (glassLength <= vehicleCapacity && batch.size() < 6) { batch.add(glass); usedLength = glassLength; } else { break; // 第一块就装不下 } } else { // 后续玻璃需要考虑间隙:玻璃长度 + 间隙 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; } /** * 写入批次到PLC */ private DevicePlcVO.OperationResult writeBatchToPlc( DeviceConfig deviceConfig, List batch, EnhancedS7Serializer serializer, Map logicParams, Map params) { Map payload = new HashMap<>(); // 写入玻璃ID(最多6个) int count = Math.min(batch.size(), 6); for (int i = 0; i < count; i++) { String fieldName = "plcGlassId" + (i + 1); payload.put(fieldName, batch.get(i).getGlassId()); } // 写入玻璃数量 payload.put("plcGlassCount", count); // 写入卧转立编号(优先从任务参数获取,其次从设备配置获取,直接写入编号,不进行位置映射) Object 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; inPosition = ctx.getParameters().getExtra() != null ? ctx.getParameters().getExtra().get("inPosition") : null; } } 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()); } // 写入请求字(触发大车) payload.put("plcRequest", 1); try { plcDynamicDataService.writePlcData(deviceConfig, payload, serializer); log.info("批次已写入PLC: deviceId={}, glassCount={}, inPosition={}", deviceConfig.getId(), count, inPosition); return buildResult(deviceConfig, "writeBatchToPlc", true, "批次写入成功"); } catch (Exception e) { log.error("写入批次到PLC失败: deviceId={}", deviceConfig.getId(), e); return buildResult(deviceConfig, "writeBatchToPlc", false, "写入失败: " + e.getMessage()); } } /** * 从缓冲队列移除已处理的玻璃 */ private void removeProcessedGlasses(String deviceId, List processed) { List buffer = glassBuffer.get(deviceId); if (buffer == null) { return; } Set processedIds = processed.stream() .map(GlassInfo::getGlassId) .collect(Collectors.toSet()); buffer.removeIf(item -> processedIds.contains(item.glassInfo.getGlassId())); } /** * 启动监控任务(定期检查并处理) */ private DevicePlcVO.OperationResult handleStartMonitor( DeviceConfig deviceConfig, WorkstationLogicConfig config, Map logicParams) { String deviceId = deviceConfig.getDeviceId(); // 停止旧的监控任务 handleStopMonitor(deviceConfig); // 获取监控间隔 Integer monitorIntervalMs = getLogicParam(logicParams, "monitorIntervalMs", config.getScanIntervalMs()); // 启动监控任务 ScheduledFuture future = monitorExecutor.scheduleWithFixedDelay(() -> { try { // 监控任务不在多设备任务上下文中运行,这里不需要传入 params/_taskContext handleCheckAndProcess(deviceConfig, config, logicParams, null); } catch (Exception e) { log.error("监控任务执行异常: deviceId={}", deviceId, e); } }, monitorIntervalMs, monitorIntervalMs, TimeUnit.MILLISECONDS); monitorTasks.put(deviceId, future); log.info("已启动卧转立监控任务: deviceId={}, interval={}ms", deviceId, monitorIntervalMs); return buildResult(deviceConfig, "startMonitor", true, "监控任务已启动"); } /** * 停止监控任务 */ private DevicePlcVO.OperationResult handleStopMonitor(DeviceConfig deviceConfig) { String deviceId = deviceConfig.getDeviceId(); ScheduledFuture future = monitorTasks.remove(deviceId); if (future != null && !future.isCancelled()) { future.cancel(false); log.info("已停止卧转立监控任务: deviceId={}", deviceId); } return buildResult(deviceConfig, "stopMonitor", true, "监控任务已停止"); } /** * 清空缓冲队列 */ private DevicePlcVO.OperationResult handleClearBuffer(DeviceConfig deviceConfig) { String deviceId = deviceConfig.getDeviceId(); glassBuffer.remove(deviceId); lastScanTime.remove(deviceId); 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 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()); } } /** * 构建操作结果 */ private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig, String operation, boolean success, String message) { return DevicePlcVO.OperationResult.builder() .deviceId(deviceConfig.getId()) .deviceName(deviceConfig.getDeviceName()) .deviceCode(deviceConfig.getDeviceCode()) .projectId(deviceConfig.getProjectId() != null ? String.valueOf(deviceConfig.getProjectId()) : null) .operation(operation) .success(success) .message(message) .timestamp(LocalDateTime.now()) .build(); } /** * 应用关闭时清理资源 */ @PreDestroy public void destroy() { log.info("正在关闭卧转立监控线程池..."); // 停止所有监控任务 for (String deviceId : new ArrayList<>(monitorTasks.keySet())) { ScheduledFuture future = monitorTasks.remove(deviceId); if (future != null && !future.isCancelled()) { future.cancel(false); } } // 关闭线程池 monitorExecutor.shutdown(); try { if (!monitorExecutor.awaitTermination(5, TimeUnit.SECONDS)) { monitorExecutor.shutdownNow(); if (!monitorExecutor.awaitTermination(5, TimeUnit.SECONDS)) { log.warn("卧转立监控线程池未能正常关闭"); } } } catch (InterruptedException e) { monitorExecutor.shutdownNow(); Thread.currentThread().interrupt(); } log.info("卧转立监控线程池已关闭"); } /** * 玻璃缓冲项 */ private static class GlassBufferItem { final GlassInfo glassInfo; final long timestamp; GlassBufferItem(GlassInfo glassInfo, long timestamp) { this.glassInfo = glassInfo; this.timestamp = timestamp; } } }