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