src/main/java/com/zy/asrs/controller/ConsoleController.java
@@ -13,6 +13,7 @@ import com.zy.asrs.domain.param.SystemSwitchParam; import com.zy.asrs.domain.vo.CrnDetailVo; import com.zy.asrs.domain.vo.CrnLatestDataVo; import com.zy.asrs.domain.vo.FakeTaskTraceVo; import com.zy.asrs.domain.vo.StationLatestDataVo; import com.zy.asrs.domain.vo.RgvLatestDataVo; import com.zy.asrs.entity.*; @@ -30,6 +31,7 @@ import com.zy.core.thread.StationThread; import com.zy.core.thread.RgvThread; import com.zy.core.model.protocol.RgvProtocol; import com.zy.core.network.fake.FakeTaskTraceRegistry; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -61,6 +63,8 @@ private BasMapService basMapService; @Autowired private StationCycleCapacityService stationCycleCapacityService; @Autowired private FakeTaskTraceRegistry fakeTaskTraceRegistry; @PostMapping("/system/running/status") @ManagerAuth(memo = "系统运行状态") @@ -274,6 +278,13 @@ return R.ok().add(stationCycleCapacityService.getLatestSnapshot()); } @PostMapping("/latest/data/fake/trace") @ManagerAuth(memo = "仿真任务轨迹实时数据") public R fakeTaskTraceLatestData() { List<FakeTaskTraceVo> traceList = fakeTaskTraceRegistry.listActiveTraces(); return R.ok().add(traceList); } // @PostMapping("/latest/data/barcode") // @ManagerAuth(memo = "条码扫描仪实时数据") // public R barcodeLatestData(){ src/main/java/com/zy/asrs/domain/vo/FakeTaskTraceEventVo.java
New file @@ -0,0 +1,23 @@ package com.zy.asrs.domain.vo; import lombok.Data; import java.util.Map; @Data public class FakeTaskTraceEventVo { private Long timestamp; private String eventType; private String message; private String status; private Integer currentStationId; private Integer targetStationId; private Map<String, Object> details; } src/main/java/com/zy/asrs/domain/vo/FakeTaskTraceVo.java
New file @@ -0,0 +1,35 @@ package com.zy.asrs.domain.vo; import lombok.Data; import java.util.List; @Data public class FakeTaskTraceVo { private Integer taskNo; private String threadImpl; private String status; private Integer startStationId; private Integer currentStationId; private Integer finalTargetStationId; private Integer blockedStationId; private List<Integer> stitchedPathStationIds; private List<Integer> passedStationIds; private List<Integer> pendingStationIds; private List<Integer> latestAppendedPath; private Long updatedAt; private List<FakeTaskTraceEventVo> events; } src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java
@@ -106,6 +106,9 @@ } else if ("/console/latest/data/station/cycle/capacity".equals(url)) { ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class); resObj = consoleController.stationCycleCapacity(); } else if ("/console/latest/data/fake/trace".equals(url)) { ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class); resObj = consoleController.fakeTaskTraceLatestData(); } else if ("/crn/table/crn/state".equals(url)) { resObj = SpringUtils.getBean(CrnController.class).crnStateTable(); } else if ("/rgv/table/rgv/state".equals(url)) { src/main/java/com/zy/core/network/ZyStationConnectDriver.java
@@ -9,7 +9,6 @@ import com.zy.core.network.api.ZyStationConnectApi; import com.zy.core.network.entity.ZyStationStatusEntity; import java.util.List; import com.zy.core.network.fake.ZyStationFakeConnect; import com.zy.core.network.fake.ZyStationFakeSegConnect; import com.zy.core.network.fake.ZyStationV4FakeSegConnect; import com.zy.core.network.real.ZyStationRealConnect; @@ -28,7 +27,6 @@ @Slf4j public class ZyStationConnectDriver implements ThreadHandler { private static final ZyStationFakeConnect zyStationFakeConnect = new ZyStationFakeConnect(); private static final ZyStationFakeSegConnect zyStationFakeSegConnect = new ZyStationFakeSegConnect(); private static final ZyStationV4FakeSegConnect zyStationV4FakeSegConnect = new ZyStationV4FakeSegConnect(); @@ -38,6 +36,7 @@ private RedisUtil redisUtil; private volatile ZyStationConnectApi zyStationConnectApi; private volatile boolean closed = false; private volatile boolean fakeConfigUnsupported = false; private ScheduledExecutorService executor; private final Object connectLock = new Object(); @@ -55,6 +54,9 @@ public boolean connect() { synchronized (connectLock) { if (closed) { return false; } if (fakeConfigUnsupported) { return false; } if (connected && zyStationConnectApi != null) { @@ -80,8 +82,11 @@ zyStationV4FakeSegConnect.addFakeConnect(deviceConfig, redisUtil); connectApi = zyStationV4FakeSegConnect; } else { zyStationFakeConnect.addFakeConnect(deviceConfig, redisUtil); connectApi = zyStationFakeConnect; fakeConfigUnsupported = true; zyStationConnectApi = null; log.error("旧版输送站 fake 已移除,deviceNo={}, threadImpl={}, 请切换到 ZyStationV3Thread 或 ZyStationV4Thread", deviceConfig.getDeviceNo(), deviceConfig.getThreadImpl()); return false; } } src/main/java/com/zy/core/network/fake/FakeTaskTraceRegistry.java
New file @@ -0,0 +1,176 @@ package com.zy.core.network.fake; import com.zy.asrs.domain.vo.FakeTaskTraceEventVo; import com.zy.asrs.domain.vo.FakeTaskTraceVo; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Component public class FakeTaskTraceRegistry { private static final int MAX_EVENT_COUNT = 200; private static final long TERMINAL_KEEP_MS = 3000L; private final Map<Integer, TraceTaskState> taskStateMap = new ConcurrentHashMap<Integer, TraceTaskState>(); public void record(Integer taskNo, String threadImpl, String status, Integer startStationId, Integer currentStationId, Integer finalTargetStationId, Integer blockedStationId, List<Integer> stitchedPathStationIds, List<Integer> passedStationIds, List<Integer> pendingStationIds, List<Integer> latestAppendedPath, String eventType, String message, Map<String, Object> details, boolean terminal) { if (taskNo == null || taskNo <= 0) { return; } cleanupExpired(); TraceTaskState taskState = taskStateMap.computeIfAbsent(taskNo, TraceTaskState::new); taskState.apply(threadImpl, status, startStationId, currentStationId, finalTargetStationId, blockedStationId, stitchedPathStationIds, passedStationIds, pendingStationIds, latestAppendedPath, eventType, message, details, terminal); } public List<FakeTaskTraceVo> listActiveTraces() { cleanupExpired(); List<FakeTaskTraceVo> result = new ArrayList<FakeTaskTraceVo>(); for (TraceTaskState taskState : taskStateMap.values()) { result.add(taskState.toVo()); } Collections.sort(result, new Comparator<FakeTaskTraceVo>() { @Override public int compare(FakeTaskTraceVo o1, FakeTaskTraceVo o2) { long v1 = o1.getUpdatedAt() == null ? 0L : o1.getUpdatedAt(); long v2 = o2.getUpdatedAt() == null ? 0L : o2.getUpdatedAt(); return Long.compare(v2, v1); } }); return result; } private void cleanupExpired() { long now = System.currentTimeMillis(); for (Map.Entry<Integer, TraceTaskState> entry : taskStateMap.entrySet()) { TraceTaskState state = entry.getValue(); if (state != null && state.shouldRemove(now)) { taskStateMap.remove(entry.getKey(), state); } } } private static List<Integer> copyIntegerList(List<Integer> source) { List<Integer> result = new ArrayList<Integer>(); if (source == null) { return result; } for (Integer item : source) { if (item != null) { result.add(item); } } return result; } private static Map<String, Object> copyDetails(Map<String, Object> source) { Map<String, Object> result = new LinkedHashMap<String, Object>(); if (source == null || source.isEmpty()) { return result; } for (Map.Entry<String, Object> entry : source.entrySet()) { Object value = entry.getValue(); if (value instanceof List) { result.put(entry.getKey(), new ArrayList<Object>((List<?>) value)); } else if (value instanceof Map) { result.put(entry.getKey(), new LinkedHashMap<Object, Object>((Map<?, ?>) value)); } else { result.put(entry.getKey(), value); } } return result; } private static class TraceTaskState { private final Integer taskNo; private String threadImpl; private String status = "WAITING"; private Integer startStationId; private Integer currentStationId; private Integer finalTargetStationId; private Integer blockedStationId; private List<Integer> stitchedPathStationIds = new ArrayList<Integer>(); private List<Integer> passedStationIds = new ArrayList<Integer>(); private List<Integer> pendingStationIds = new ArrayList<Integer>(); private List<Integer> latestAppendedPath = new ArrayList<Integer>(); private final List<FakeTaskTraceEventVo> events = new ArrayList<FakeTaskTraceEventVo>(); private Long updatedAt = System.currentTimeMillis(); private Long terminalExpireAt; private TraceTaskState(Integer taskNo) { this.taskNo = taskNo; } private synchronized void apply(String threadImpl, String status, Integer startStationId, Integer currentStationId, Integer finalTargetStationId, Integer blockedStationId, List<Integer> stitchedPathStationIds, List<Integer> passedStationIds, List<Integer> pendingStationIds, List<Integer> latestAppendedPath, String eventType, String message, Map<String, Object> details, boolean terminal) { this.threadImpl = threadImpl; this.status = status == null ? "WAITING" : status; this.startStationId = startStationId; this.currentStationId = currentStationId; this.finalTargetStationId = finalTargetStationId; this.blockedStationId = blockedStationId; this.stitchedPathStationIds = copyIntegerList(stitchedPathStationIds); this.passedStationIds = copyIntegerList(passedStationIds); this.pendingStationIds = copyIntegerList(pendingStationIds); this.latestAppendedPath = copyIntegerList(latestAppendedPath); long now = System.currentTimeMillis(); this.updatedAt = now; if (eventType != null) { FakeTaskTraceEventVo event = new FakeTaskTraceEventVo(); event.setTimestamp(now); event.setEventType(eventType); event.setMessage(message); event.setStatus(this.status); event.setCurrentStationId(this.currentStationId); event.setTargetStationId(this.finalTargetStationId); event.setDetails(copyDetails(details)); this.events.add(event); if (this.events.size() > MAX_EVENT_COUNT) { this.events.remove(0); } } this.terminalExpireAt = terminal ? now + TERMINAL_KEEP_MS : null; } private synchronized boolean shouldRemove(long now) { return terminalExpireAt != null && terminalExpireAt <= now; } private synchronized FakeTaskTraceVo toVo() { FakeTaskTraceVo vo = new FakeTaskTraceVo(); vo.setTaskNo(taskNo); vo.setThreadImpl(threadImpl); vo.setStatus(status); vo.setStartStationId(startStationId); vo.setCurrentStationId(currentStationId); vo.setFinalTargetStationId(finalTargetStationId); vo.setBlockedStationId(blockedStationId); vo.setStitchedPathStationIds(copyIntegerList(stitchedPathStationIds)); vo.setPassedStationIds(copyIntegerList(passedStationIds)); vo.setPendingStationIds(copyIntegerList(pendingStationIds)); vo.setLatestAppendedPath(copyIntegerList(latestAppendedPath)); vo.setUpdatedAt(updatedAt); vo.setEvents(new ArrayList<FakeTaskTraceEventVo>(events)); return vo; } } } src/main/java/com/zy/core/network/fake/ZyStationFakeConnect.java
File was deleted src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java
@@ -2,6 +2,7 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.core.common.SpringUtils; import com.zy.asrs.entity.DeviceConfig; import com.zy.common.utils.RedisUtil; import com.zy.core.News; @@ -11,42 +12,51 @@ import com.zy.core.model.command.StationCommand; import com.zy.core.network.api.ZyStationConnectApi; import com.zy.core.network.entity.ZyStationStatusEntity; import java.util.ArrayList; import java.util.HashMap; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.Map; import java.util.Arrays; public class ZyStationFakeSegConnect implements ZyStationConnectApi { private static final long DEFAULT_FAKE_RUN_BLOCK_TIMEOUT_MS = 10000L; // 站点级锁:每个站点独立一把锁,提升并发性能 private final Map<Integer, ReentrantLock> stationLocks = new ConcurrentHashMap<>(); private HashMap<Integer, List<ZyStationStatusEntity>> deviceStatusMap = new HashMap<>(); private HashMap<Integer, DeviceConfig> deviceConfigMap = new HashMap<>(); private RedisUtil redisUtil; private final Map<Integer, BlockingQueue<StationCommand>> taskQueues = new ConcurrentHashMap<>(); private final Map<Integer, Long> taskLastUpdateTime = new ConcurrentHashMap<>(); private final Map<Integer, Boolean> taskRunning = new ConcurrentHashMap<>(); private static final long DEFAULT_FAKE_RUN_BLOCK_TIMEOUT_MS = 10000L; private static final long WAIT_SEGMENT_TIMEOUT_MS = 30000L; private static final String STATUS_WAITING = "WAITING"; private static final String STATUS_RUNNING = "RUNNING"; private static final String STATUS_BLOCKED = "BLOCKED"; private static final String STATUS_CANCELLED = "CANCELLED"; private static final String STATUS_TIMEOUT = "TIMEOUT"; private static final String STATUS_FINISHED = "FINISHED"; private final Map<Integer, ReentrantLock> stationLocks = new ConcurrentHashMap<Integer, ReentrantLock>(); private final Map<Integer, List<ZyStationStatusEntity>> deviceStatusMap = new ConcurrentHashMap<Integer, List<ZyStationStatusEntity>>(); private final Map<Integer, DeviceConfig> deviceConfigMap = new ConcurrentHashMap<Integer, DeviceConfig>(); private final Map<Integer, BlockingQueue<StationCommand>> taskQueues = new ConcurrentHashMap<Integer, BlockingQueue<StationCommand>>(); private final Map<Integer, Long> taskLastUpdateTime = new ConcurrentHashMap<Integer, Long>(); private final Map<Integer, Boolean> taskRunning = new ConcurrentHashMap<Integer, Boolean>(); private final ExecutorService executor = Executors.newCachedThreadPool(); private RedisUtil redisUtil; public void addFakeConnect(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.redisUtil = redisUtil; if (deviceConfigMap.containsKey(deviceConfig.getDeviceNo())) { return; } deviceConfigMap.put(deviceConfig.getDeviceNo(), deviceConfig); deviceStatusMap.put(deviceConfig.getDeviceNo(), new CopyOnWriteArrayList<>()); deviceStatusMap.put(deviceConfig.getDeviceNo(), new CopyOnWriteArrayList<ZyStationStatusEntity>()); } @Override @@ -64,10 +74,11 @@ public List<ZyStationStatusEntity> getStatus(Integer deviceNo) { List<ZyStationStatusEntity> statusList = deviceStatusMap.get(deviceNo); if (statusList == null) { return new ArrayList<>(); return new ArrayList<ZyStationStatusEntity>(); } DeviceConfig deviceConfig = deviceConfigMap.get(deviceNo); if (statusList.isEmpty()) { if (statusList.isEmpty() && deviceConfig != null) { List<ZyStationStatusEntity> init = JSON.parseArray(deviceConfig.getFakeInitStatus(), ZyStationStatusEntity.class); if (init != null) { @@ -97,44 +108,92 @@ return new CommandResponse(false, "任务号为空"); } // 处理非移动命令 if (command.getCommandType() != StationCommandType.MOVE) { handleCommand(deviceNo, command); } else { // 将移动命令追加到任务队列(支持分段下发) taskQueues.computeIfAbsent(taskNo, k -> new LinkedBlockingQueue<>()).offer(command); taskLastUpdateTime.put(taskNo, System.currentTimeMillis()); return new CommandResponse(true, "命令已受理(异步执行)"); } // 只有任务未启动时才启动执行器,后续分段命令仅追加到队列 if (taskRunning.putIfAbsent(taskNo, true) == null) { executor.submit(() -> runTaskLoop(deviceNo, taskNo)); } // 后续分段命令不再返回错误,正常追加到队列 if (isDirectMoveCommand(command)) { handleDirectMoveCommand(deviceNo, command); return new CommandResponse(true, "命令已受理(异步执行)"); } taskQueues.computeIfAbsent(taskNo, key -> new LinkedBlockingQueue<StationCommand>()).offer(command); taskLastUpdateTime.put(taskNo, System.currentTimeMillis()); if (taskRunning.putIfAbsent(taskNo, true) == null) { executor.submit(new Runnable() { @Override public void run() { runTaskLoop(deviceNo, taskNo); } }); } return new CommandResponse(true, "命令已受理(异步执行)"); } private void runTaskLoop(Integer deviceNo, Integer taskNo) { try { // 待执行的路径队列(存储站点ID序列) LinkedBlockingQueue<Integer> pendingPathQueue = new LinkedBlockingQueue<>(); // 当前所在站点ID Integer currentStationId = null; // 最终目标站点ID Integer finalTargetStationId = null; // 是否需要生成条码 boolean generateBarcode = false; // 是否已初始化起点 boolean initialized = false; // 上一步执行时间(用于堵塞检测) long stepExecuteTime = System.currentTimeMillis(); long runBlockTimeoutMs = getFakeRunBlockTimeoutMs(); // 仅在每次到达目标时执行一次到位处理,避免重复生成条码 boolean arrivalHandled = false; @Override public CommandResponse sendOriginCommand(String address, short[] data) { return new CommandResponse(true, "原始命令已受理(异步执行)"); } @Override public byte[] readOriginCommand(String address, int length) { return new byte[0]; } private boolean isDirectMoveCommand(StationCommand command) { if (command == null || command.getCommandType() != StationCommandType.MOVE) { return false; } List<Integer> path = command.getNavigatePath(); return (path == null || path.isEmpty()) && command.getStationId() != null && command.getStationId().equals(command.getTargetStaNo()); } private void handleDirectMoveCommand(Integer deviceNo, StationCommand command) { Integer taskNo = command.getTaskNo(); Integer stationId = command.getStationId(); Integer targetStationId = command.getTargetStaNo(); if (taskNo != null && taskNo > 0 && taskNo != 9999 && taskNo != 9998 && stationId != null && stationId.equals(targetStationId)) { generateStationData(deviceNo, taskNo, stationId, targetStationId); } TaskRuntimeContext context = new TaskRuntimeContext(taskNo, getThreadImpl(deviceNo)); context.startStationId = stationId; context.currentStationId = stationId; context.finalTargetStationId = targetStationId; context.initialized = true; context.status = STATUS_RUNNING; context.appendStitchedPath(Arrays.asList(stationId)); context.addPassedStation(stationId); context.generateBarcode = checkTaskNoInArea(taskNo); traceEvent(deviceNo, context, "MOVE_INIT", "同站点任务直接到位", buildDetails("stationId", stationId), false); if (context.generateBarcode) { generateStationBarcode(taskNo, stationId, deviceNo); } traceEvent(deviceNo, context, "ARRIVED", "任务已在起点站点完成", buildDetails("barcodeGenerated", context.generateBarcode, "stationId", stationId), false); context.status = STATUS_FINISHED; traceEvent(deviceNo, context, "TASK_END", "任务执行完成", buildDetails("reason", STATUS_FINISHED), true); } private void runTaskLoop(Integer deviceNo, Integer taskNo) { TaskRuntimeContext context = new TaskRuntimeContext(taskNo, getThreadImpl(deviceNo)); try { while (true) { if (Thread.currentThread().isInterrupted()) { if (!isTerminalStatus(context.status)) { context.status = STATUS_CANCELLED; } break; } if (hasTaskReset(taskNo)) { context.status = STATUS_CANCELLED; break; } @@ -143,172 +202,278 @@ break; } // 尝试获取新的分段命令 StationCommand command = commandQueue.poll(100, TimeUnit.MILLISECONDS); if (command != null) { taskLastUpdateTime.put(taskNo, System.currentTimeMillis()); List<Integer> newPath = command.getNavigatePath(); Integer lastInQueue = getLastInQueue(pendingPathQueue); int startIndex = getPathAppendStartIndex(newPath, currentStationId, lastInQueue); if (newPath != null && !newPath.isEmpty() && startIndex < 0) { News.info("[WCS Debug] 任务{}忽略无法衔接的旧路径段: {}, 当前位置: {}, 队列尾: {}", taskNo, newPath, currentStationId, lastInQueue); continue; } // 每次接收命令都刷新目标,避免沿用旧目标导致状态抖动 Integer commandTargetStationId = command.getTargetStaNo(); if (commandTargetStationId != null) { if (!commandTargetStationId.equals(finalTargetStationId)) { arrivalHandled = false; News.info("[WCS Debug] 任务{}切换目标: {} -> {}", taskNo, finalTargetStationId, commandTargetStationId); } finalTargetStationId = commandTargetStationId; // 当前站点先同步最新目标,避免上层在窗口期重复下发同一路径 syncCurrentStationTarget(taskNo, currentStationId, finalTargetStationId); } if (!generateBarcode && checkTaskNoInArea(taskNo)) { generateBarcode = true; } // 将新路径追加到待执行队列 if (newPath != null && !newPath.isEmpty()) { for (int i = startIndex; i < newPath.size(); i++) { pendingPathQueue.offer(newPath.get(i)); } News.info("[WCS Debug] 任务{}追加路径段: {} -> 队列大小: {}", taskNo, newPath, pendingPathQueue.size()); } context.lastCommandAt = System.currentTimeMillis(); handleIncomingSegment(deviceNo, context, command); } // 执行移动逻辑 if (!pendingPathQueue.isEmpty()) { Integer nextStationId = pendingPathQueue.peek(); // 如果尚未初始化起点 if (!initialized && currentStationId == null) { // 优先查找托盘当前实际位置(支持堵塞后重路由场景) Integer actualCurrentStationId = findCurrentStationIdByTask(taskNo); if (actualCurrentStationId != null) { // 找到了当前托盘位置,使用实际位置作为起点 currentStationId = actualCurrentStationId; initialized = true; // 清除该站点的 runBlock 标记(堵塞恢复) Integer deviceId = getDeviceNoByStationId(currentStationId); if (deviceId != null) { clearRunBlock(currentStationId, deviceId); } // 如果路径起点与当前位置相同,移除起点避免重复 if (nextStationId.equals(currentStationId)) { pendingPathQueue.poll(); } stepExecuteTime = System.currentTimeMillis(); News.info("[WCS Debug] 任务{}恢复执行,当前位置: {}", taskNo, currentStationId); continue; } // 未找到当前位置(首次执行),首个站点就是起点 currentStationId = nextStationId; Integer deviceId = getDeviceNoByStationId(currentStationId); if (deviceId != null) { boolean result = initStationMove(taskNo, currentStationId, deviceId, taskNo, finalTargetStationId, true, null); if (result) { initialized = true; pendingPathQueue.poll(); // 移除起点 stepExecuteTime = System.currentTimeMillis(); News.info("[WCS Debug] 任务{}初始化起点: {}", taskNo, currentStationId); } } sleep(500); if (!context.pendingPathQueue.isEmpty()) { if (!context.initialized || context.currentStationId == null) { initializeTaskPosition(deviceNo, context); continue; } // 执行从当前站点到下一站点的移动 Integer currentDeviceNo = getDeviceNoByStationId(currentStationId); Integer nextDeviceNo = getDeviceNoByStationId(nextStationId); if (currentDeviceNo != null && nextDeviceNo != null) { boolean moveSuccess = stationMoveToNext(taskNo, currentStationId, currentDeviceNo, nextStationId, nextDeviceNo, taskNo, finalTargetStationId); if (moveSuccess) { currentStationId = nextStationId; pendingPathQueue.poll(); stepExecuteTime = System.currentTimeMillis(); arrivalHandled = false; News.info("[WCS Debug] 任务{}移动到站点: {}, 剩余队列: {}", taskNo, currentStationId, pendingPathQueue.size()); sleep(1000); // 模拟移动耗时 } else { // 移动失败,检查是否堵塞 if (!checkTaskNoInArea(taskNo)) { boolean fakeAllowCheckBlock = getFakeAllowCheckBlock(); if (fakeAllowCheckBlock && System.currentTimeMillis() - stepExecuteTime > runBlockTimeoutMs) { // 认定堵塞 boolean result = runBlockStation(taskNo, currentStationId, currentDeviceNo, taskNo, currentStationId); if (result) { News.info("[WCS Debug] 任务{}在站点{}被标记为堵塞", taskNo, currentStationId); pendingPathQueue.clear(); break; } } } sleep(500); // 失败重试等待 } } else { // 无法获取设备号,跳过该站点 pendingPathQueue.poll(); } } else { // 路径队列为空,等待新的分段命令 if (currentStationId != null && finalTargetStationId != null && currentStationId.equals(finalTargetStationId)) { // 已到达当前目标后继续等待下一条分段命令,避免排序/绕圈场景吞掉后续命令 if (!arrivalHandled) { if (generateBarcode) { Integer targetDeviceNo = getDeviceNoByStationId(finalTargetStationId); if (targetDeviceNo != null) { generateStationBarcode(taskNo, finalTargetStationId, targetDeviceNo); News.info("[WCS Debug] 任务{}到达目标{}并生成条码", taskNo, finalTargetStationId); } } arrivalHandled = true; } } // 继续等待新的分段命令 Long lastTime = taskLastUpdateTime.get(taskNo); if (lastTime != null && System.currentTimeMillis() - lastTime > 30000) { // 超时:30秒内没有收到新分段命令 News.info("[WCS Debug] 任务{}等待分段超时,当前位置: {}, 目标: {}", taskNo, currentStationId, finalTargetStationId); if (!executeNextMove(deviceNo, context)) { break; } // 继续等待新分段命令(不做任何事情,下一轮循环会尝试获取新命令) continue; } if (handleIdleState(deviceNo, context)) { break; } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); if (!isTerminalStatus(context.status)) { context.status = STATUS_CANCELLED; } } finally { taskQueues.remove(taskNo); taskLastUpdateTime.remove(taskNo); taskRunning.remove(taskNo); News.info("[WCS Debug] 任务{}执行结束并清理资源", taskNo); if (!isTerminalStatus(context.status)) { context.status = STATUS_FINISHED; } traceEvent(deviceNo, context, "TASK_END", "任务执行结束并清理资源", buildDetails("reason", context.status), true); News.info("[WCS Debug] 任务{}执行结束并清理资源,状态={}", taskNo, context.status); } } /** * 获取队列中最后一个元素(不移除) */ private void handleIncomingSegment(Integer deviceNo, TaskRuntimeContext context, StationCommand command) { List<Integer> newPath = normalizePath(command.getNavigatePath()); Integer lastInQueue = getLastInQueue(context.pendingPathQueue); int startIndex = getPathAppendStartIndex(newPath, context.currentStationId, lastInQueue); context.setStartStationIdIfAbsent(command.getStationId()); if (!context.generateBarcode && checkTaskNoInArea(context.taskNo)) { context.generateBarcode = true; } traceEvent(deviceNo, context, "SEGMENT_RECEIVED", "收到新的路径分段命令", buildDetails("segmentPath", newPath, "appendStartIndex", startIndex, "currentStationId", context.currentStationId, "queueTailStationId", lastInQueue, "commandStationId", command.getStationId(), "commandTargetStationId", command.getTargetStaNo()), false); Integer commandTargetStationId = command.getTargetStaNo(); if (commandTargetStationId != null) { if (!commandTargetStationId.equals(context.finalTargetStationId)) { traceEvent(deviceNo, context, "TARGET_SWITCHED", "任务目标站发生切换: " + context.finalTargetStationId + " -> " + commandTargetStationId, buildDetails("fromTargetStationId", context.finalTargetStationId, "toTargetStationId", commandTargetStationId), false); context.arrivalHandled = false; } context.finalTargetStationId = commandTargetStationId; syncCurrentStationTarget(context.taskNo, context.currentStationId, context.finalTargetStationId); } if (!newPath.isEmpty() && startIndex < 0) { traceEvent(deviceNo, context, "SEGMENT_IGNORED", "路径分段无法与当前运行上下文衔接,已忽略", buildDetails("segmentPath", newPath, "currentStationId", context.currentStationId, "queueTailStationId", lastInQueue, "ignoreReason", "PATH_NOT_CONNECTED"), false); context.latestAppendedPath.clear(); return; } List<Integer> appendedPath = new ArrayList<Integer>(); for (int i = startIndex; i < newPath.size(); i++) { Integer stationId = newPath.get(i); context.pendingPathQueue.offer(stationId); appendedPath.add(stationId); } context.appendStitchedPath(appendedPath); if (!appendedPath.isEmpty()) { traceEvent(deviceNo, context, "SEGMENT_APPENDED", "路径分段已追加到待执行队列,队列长度=" + context.pendingPathQueue.size(), buildDetails("segmentPath", newPath, "appendedPath", appendedPath, "appendStartIndex", startIndex, "queueSize", context.pendingPathQueue.size()), false); } } private void initializeTaskPosition(Integer deviceNo, TaskRuntimeContext context) { Integer nextStationId = context.pendingPathQueue.peek(); if (nextStationId == null) { return; } if (context.currentStationId == null) { Integer actualCurrentStationId = findCurrentStationIdByTask(context.taskNo); if (actualCurrentStationId != null) { context.currentStationId = actualCurrentStationId; context.initialized = true; context.status = STATUS_RUNNING; context.blockedStationId = null; Integer actualDeviceNo = getDeviceNoByStationId(actualCurrentStationId); if (actualDeviceNo != null) { clearRunBlock(actualCurrentStationId, actualDeviceNo); } trimPendingPathToCurrent(context.pendingPathQueue, actualCurrentStationId); if (actualCurrentStationId.equals(context.pendingPathQueue.peek())) { context.pendingPathQueue.poll(); } context.addPassedStation(actualCurrentStationId); context.lastStepAt = System.currentTimeMillis(); traceEvent(deviceNo, context, "MOVE_INIT", "任务从当前实际站点恢复执行", buildDetails("stationId", actualCurrentStationId, "recovered", true), false); return; } } context.currentStationId = nextStationId; Integer currentDeviceNo = getDeviceNoByStationId(context.currentStationId); if (currentDeviceNo == null) { context.pendingPathQueue.poll(); return; } boolean result = initStationMove(context.taskNo, context.currentStationId, currentDeviceNo, context.taskNo, context.finalTargetStationId, true, null); if (!result) { sleep(200); return; } context.initialized = true; context.status = STATUS_RUNNING; context.pendingPathQueue.poll(); context.addPassedStation(context.currentStationId); context.lastStepAt = System.currentTimeMillis(); traceEvent(deviceNo, context, "MOVE_INIT", "任务初始化起点站点", buildDetails("stationId", context.currentStationId, "recovered", false), false); sleep(500); } private boolean executeNextMove(Integer deviceNo, TaskRuntimeContext context) { Integer nextStationId = context.pendingPathQueue.peek(); if (nextStationId == null || context.currentStationId == null) { return true; } Integer currentDeviceNo = getDeviceNoByStationId(context.currentStationId); Integer nextDeviceNo = getDeviceNoByStationId(nextStationId); if (currentDeviceNo == null || nextDeviceNo == null) { context.pendingPathQueue.poll(); return true; } boolean moveSuccess = stationMoveToNext(context.taskNo, context.currentStationId, currentDeviceNo, nextStationId, nextDeviceNo, context.taskNo, context.finalTargetStationId); if (moveSuccess) { Integer previousStationId = context.currentStationId; context.currentStationId = nextStationId; context.pendingPathQueue.poll(); context.addPassedStation(nextStationId); context.arrivalHandled = false; context.blockedStationId = null; context.status = STATUS_RUNNING; context.lastStepAt = System.currentTimeMillis(); traceEvent(deviceNo, context, "MOVE_STEP_OK", "任务完成一步站点移动", buildDetails("fromStationId", previousStationId, "toStationId", nextStationId, "remainingPendingPath", context.getPendingStationIds()), false); sleep(1000); return true; } if (!checkTaskNoInArea(context.taskNo) && getFakeAllowCheckBlock() && System.currentTimeMillis() - context.lastStepAt > getFakeRunBlockTimeoutMs()) { boolean blocked = runBlockStation(context.taskNo, context.currentStationId, currentDeviceNo, context.taskNo, context.currentStationId); if (blocked) { context.blockedStationId = context.currentStationId; context.status = STATUS_BLOCKED; context.pendingPathQueue.clear(); traceEvent(deviceNo, context, "RUN_BLOCKED", "任务在当前站点被标记为堵塞", buildDetails("blockedStationId", context.currentStationId), false); return false; } } sleep(500); return true; } private boolean handleIdleState(Integer deviceNo, TaskRuntimeContext context) { if (context.currentStationId != null && context.finalTargetStationId != null && context.currentStationId.equals(context.finalTargetStationId)) { if (!context.arrivalHandled) { boolean barcodeGenerated = false; if (context.generateBarcode) { Integer targetDeviceNo = getDeviceNoByStationId(context.finalTargetStationId); if (targetDeviceNo != null) { barcodeGenerated = generateStationBarcode(context.taskNo, context.finalTargetStationId, targetDeviceNo); } } context.arrivalHandled = true; traceEvent(deviceNo, context, "ARRIVED", "任务到达最终目标站点", buildDetails("stationId", context.currentStationId, "barcodeGenerated", barcodeGenerated), false); } context.status = STATUS_FINISHED; return true; } Long lastTime = taskLastUpdateTime.get(context.taskNo); if (lastTime != null && System.currentTimeMillis() - lastTime > WAIT_SEGMENT_TIMEOUT_MS) { context.status = STATUS_TIMEOUT; traceEvent(deviceNo, context, "WAIT_TIMEOUT", "等待新的路径分段超时", buildDetails("timeoutMs", WAIT_SEGMENT_TIMEOUT_MS, "currentStationId", context.currentStationId, "targetStationId", context.finalTargetStationId), false); return true; } return false; } private List<Integer> normalizePath(List<Integer> path) { List<Integer> result = new ArrayList<Integer>(); if (path == null) { return result; } for (Integer stationId : path) { if (stationId != null) { result.add(stationId); } } return result; } private void trimPendingPathToCurrent(LinkedBlockingQueue<Integer> queue, Integer currentStationId) { if (queue == null || currentStationId == null || queue.isEmpty()) { return; } List<Integer> snapshot = new ArrayList<Integer>(queue); int index = snapshot.indexOf(currentStationId); if (index <= 0) { return; } for (int i = 0; i < index; i++) { queue.poll(); } } private boolean hasTaskReset(Integer taskNo) { if (redisUtil == null || taskNo == null) { return false; } Object cancel = redisUtil.get(RedisKeyType.DEVICE_STATION_MOVE_RESET.key + taskNo); return cancel != null; } private Integer getLastInQueue(LinkedBlockingQueue<Integer> queue) { Integer last = null; for (Integer item : queue) { @@ -317,9 +482,6 @@ return last; } /** * 计算新路径在队列中的追加起点,避免重复下发导致路径来回跳 */ private int getPathAppendStartIndex(List<Integer> newPath, Integer currentStationId, Integer lastInQueue) { if (newPath == null || newPath.isEmpty()) { return 0; @@ -347,9 +509,6 @@ return 0; } /** * 命令刚到达时同步当前站点目标,降低上层重复发同一路径的概率 */ private void syncCurrentStationTarget(Integer taskNo, Integer currentStationId, Integer targetStationId) { if (currentStationId == null || targetStationId == null) { return; @@ -383,16 +542,14 @@ } } /** * 获取是否允许检查堵塞的配置 */ @SuppressWarnings("unchecked") private boolean getFakeAllowCheckBlock() { boolean fakeAllowCheckBlock = true; Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key); if (systemConfigMapObj != null) { HashMap<String, String> systemConfigMap = (HashMap<String, String>) systemConfigMapObj; Object systemConfigMapObj = redisUtil == null ? null : redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key); if (systemConfigMapObj instanceof Map) { Map<String, String> systemConfigMap = (Map<String, String>) systemConfigMapObj; String value = systemConfigMap.get("fakeAllowCheckBlock"); if (value != null && !value.equals("Y")) { if (value != null && !"Y".equals(value)) { fakeAllowCheckBlock = false; } } @@ -401,7 +558,7 @@ private long getFakeRunBlockTimeoutMs() { long timeoutMs = DEFAULT_FAKE_RUN_BLOCK_TIMEOUT_MS; Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key); Object systemConfigMapObj = redisUtil == null ? null : redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key); if (systemConfigMapObj instanceof Map) { Map<?, ?> systemConfigMap = (Map<?, ?>) systemConfigMapObj; Object value = systemConfigMap.get("fakeRunBlockTimeoutMs"); @@ -418,30 +575,15 @@ return timeoutMs; } @Override public CommandResponse sendOriginCommand(String address, short[] data) { return new CommandResponse(true, "原始命令已受理(异步执行)"); } @Override public byte[] readOriginCommand(String address, int length) { return new byte[0]; } private void handleCommand(Integer deviceNo, StationCommand command) { News.info("[WCS Debug] 站点仿真模拟(V3)已启动,命令数据={}", JSON.toJSONString(command)); Integer taskNo = command.getTaskNo(); Integer stationId = command.getStationId(); Integer targetStationId = command.getTargetStaNo(); boolean generateBarcode = false; if (command.getCommandType() == StationCommandType.RESET) { resetStation(deviceNo, stationId); return; } if (checkTaskNoInArea(taskNo)) { generateBarcode = true; } if (command.getCommandType() == StationCommandType.WRITE_INFO) { @@ -450,26 +592,27 @@ return; } if (taskNo == 9998 && targetStationId == 0) { // 生成出库站点仿真数据 generateFakeOutStationData(deviceNo, stationId); return; } } if (taskNo > 0 && taskNo != 9999 && taskNo != 9998 && stationId == targetStationId) { if (taskNo != null && taskNo > 0 && taskNo != 9999 && taskNo != 9998 && stationId != null && stationId.equals(targetStationId)) { generateStationData(deviceNo, taskNo, stationId, targetStationId); } // 注意:MOVE 类型的命令现已在 sendCommand 中处理,handleCommand 仅处理非 MOVE 命令 } private void generateFakeOutStationData(Integer deviceNo, Integer stationId) { List<ZyStationStatusEntity> statusList = deviceStatusMap.get(deviceNo); if (statusList == null) { return; } ZyStationStatusEntity status = statusList.stream() .filter(item -> item.getStationId().equals(stationId)).findFirst().orElse(null); if (status == null) { return; } synchronized (status) { status.setLoading(true); } @@ -477,12 +620,14 @@ private void generateStationData(Integer deviceNo, Integer taskNo, Integer stationId, Integer targetStationId) { List<ZyStationStatusEntity> statusList = deviceStatusMap.get(deviceNo); if (statusList == null) { return; } ZyStationStatusEntity status = statusList.stream() .filter(item -> item.getStationId().equals(stationId)).findFirst().orElse(null); if (status == null) { return; } synchronized (status) { status.setTaskNo(taskNo); status.setTargetStaNo(targetStationId); @@ -491,12 +636,14 @@ private void resetStation(Integer deviceNo, Integer stationId) { List<ZyStationStatusEntity> statusList = deviceStatusMap.get(deviceNo); if (statusList == null) { return; } ZyStationStatusEntity status = statusList.stream() .filter(item -> item.getStationId().equals(stationId)).findFirst().orElse(null); if (status == null) { return; } synchronized (status) { status.setTaskNo(0); status.setLoading(false); @@ -509,29 +656,25 @@ if (statusList == null) { return; } ZyStationStatusEntity status = statusList.stream() .filter(item -> item.getStationId().equals(stationId)).findFirst().orElse(null); if (status == null) { return; } synchronized (status) { status.setBarcode(barcode); } } // segmentedPathCommand 方法已删除,功能已整合到 runTaskLoop private Integer getDeviceNoByStationId(Integer stationId) { for (Integer devNo : deviceStatusMap.keySet()) { List<ZyStationStatusEntity> list = deviceStatusMap.get(devNo); for (Map.Entry<Integer, List<ZyStationStatusEntity>> entry : deviceStatusMap.entrySet()) { List<ZyStationStatusEntity> list = entry.getValue(); if (list == null) { continue; } for (ZyStationStatusEntity e : list) { if (e.getStationId() != null && e.getStationId().equals(stationId)) { return devNo; for (ZyStationStatusEntity entity : list) { if (entity.getStationId() != null && entity.getStationId().equals(stationId)) { return entry.getKey(); } } } @@ -539,21 +682,18 @@ } private Integer findCurrentStationIdByTask(Integer taskNo) { for (Integer devNo : deviceStatusMap.keySet()) { List<ZyStationStatusEntity> list = deviceStatusMap.get(devNo); for (List<ZyStationStatusEntity> list : deviceStatusMap.values()) { if (list == null) { continue; } for (ZyStationStatusEntity e : list) { if (e.getTaskNo() != null && e.getTaskNo().equals(taskNo) && e.isLoading()) { return e.getStationId(); for (ZyStationStatusEntity entity : list) { if (entity.getTaskNo() != null && entity.getTaskNo().equals(taskNo) && entity.isLoading()) { return entity.getStationId(); } } } return null; } // stationMoveByPathIds 方法已删除,功能已整合到 runTaskLoop private void sleep(long ms) { try { @@ -563,16 +703,10 @@ } } /** * 获取站点锁,如果不存在则创建 */ private ReentrantLock getStationLock(Integer stationId) { return stationLocks.computeIfAbsent(stationId, k -> new ReentrantLock()); return stationLocks.computeIfAbsent(stationId, key -> new ReentrantLock()); } /** * 按顺序锁定多个站点(避免死锁) */ private void lockStations(Integer... stationIds) { Integer[] sorted = Arrays.copyOf(stationIds, stationIds.length); Arrays.sort(sorted); @@ -581,9 +715,6 @@ } } /** * 按逆序解锁多个站点 */ private void unlockStations(Integer... stationIds) { Integer[] sorted = Arrays.copyOf(stationIds, stationIds.length); Arrays.sort(sorted); @@ -592,19 +723,14 @@ } } /** * 更新站点数据(调用前必须已持有该站点的锁) */ private boolean updateStationDataInternal(Integer stationId, Integer deviceNo, Integer taskNo, Integer targetStaNo, Boolean isLoading, String barcode, Boolean runBlock) { List<ZyStationStatusEntity> statusList = deviceStatusMap.get(deviceNo); if (statusList == null) { return false; } ZyStationStatusEntity currentStatus = statusList.stream() .filter(item -> item.getStationId().equals(stationId)).findFirst().orElse(null); if (currentStatus == null) { return false; } @@ -612,28 +738,21 @@ if (taskNo != null) { currentStatus.setTaskNo(taskNo); } if (targetStaNo != null) { currentStatus.setTargetStaNo(targetStaNo); } if (isLoading != null) { currentStatus.setLoading(isLoading); } if (barcode != null) { currentStatus.setBarcode(barcode); } if (runBlock != null) { currentStatus.setRunBlock(runBlock); } return true; } /** * 初始化站点移动(使用站点级锁) */ public boolean initStationMove(Integer lockTaskNo, Integer currentStationId, Integer currentStationDeviceNo, Integer taskNo, Integer targetStationId, Boolean isLoading, String barcode) { lockStations(currentStationId); @@ -642,18 +761,14 @@ if (statusList == null) { return false; } ZyStationStatusEntity currentStatus = statusList.stream() .filter(item -> item.getStationId().equals(currentStationId)).findFirst().orElse(null); if (currentStatus == null) { return false; } if (currentStatus.getTaskNo() > 0) { if (!currentStatus.getTaskNo().equals(taskNo) && currentStatus.isLoading()) { return false; } if (currentStatus.getTaskNo() != null && currentStatus.getTaskNo() > 0 && !currentStatus.getTaskNo().equals(taskNo) && currentStatus.isLoading()) { return false; } return updateStationDataInternal(currentStationId, currentStationDeviceNo, taskNo, targetStationId, @@ -663,35 +778,24 @@ } } /** * 站点移动到下一个位置(使用站点级锁,按ID顺序获取锁避免死锁) */ public boolean stationMoveToNext(Integer lockTaskNo, Integer currentStationId, Integer currentStationDeviceNo, Integer nextStationId, Integer nextStationDeviceNo, Integer taskNo, Integer targetStaNo) { // 同时锁定当前站点和下一个站点(按ID顺序,避免死锁) lockStations(currentStationId, nextStationId); try { List<ZyStationStatusEntity> statusList = deviceStatusMap.get(currentStationDeviceNo); if (statusList == null) { return false; } List<ZyStationStatusEntity> nextStatusList = deviceStatusMap.get(nextStationDeviceNo); if (nextStatusList == null) { if (statusList == null || nextStatusList == null) { return false; } ZyStationStatusEntity currentStatus = statusList.stream() .filter(item -> item.getStationId().equals(currentStationId)).findFirst().orElse(null); ZyStationStatusEntity nextStatus = nextStatusList.stream() .filter(item -> item.getStationId().equals(nextStationId)).findFirst().orElse(null); if (currentStatus == null || nextStatus == null) { return false; } if (nextStatus.getTaskNo() > 0 || nextStatus.isLoading()) { if (nextStatus.getTaskNo() != null && nextStatus.getTaskNo() > 0 || nextStatus.isLoading()) { return false; } @@ -700,53 +804,35 @@ if (!result) { return false; } boolean result2 = updateStationDataInternal(currentStationId, currentStationDeviceNo, 0, 0, false, "", false); if (!result2) { return false; } return true; return updateStationDataInternal(currentStationId, currentStationDeviceNo, 0, 0, false, "", false); } finally { unlockStations(currentStationId, nextStationId); } } /** * 生成站点条码(使用站点级锁) */ public boolean generateStationBarcode(Integer lockTaskNo, Integer currentStationId, Integer currentStationDeviceNo) { public boolean generateStationBarcode(Integer lockTaskNo, Integer currentStationId, Integer currentStationDeviceNo) { lockStations(currentStationId); try { List<ZyStationStatusEntity> statusList = deviceStatusMap.get(currentStationDeviceNo); if (statusList == null) { return false; } ZyStationStatusEntity currentStatus = statusList.stream() .filter(item -> item.getStationId().equals(currentStationId)).findFirst().orElse(null); if (currentStatus == null) { return false; } Random random = new Random(); String barcodeTime = String.valueOf(System.currentTimeMillis()); String barcode = String.valueOf(random.nextInt(10)) + String.valueOf(random.nextInt(10)) + barcodeTime.substring(7); return updateStationDataInternal(currentStationId, currentStationDeviceNo, null, null, null, barcode, null); } finally { unlockStations(currentStationId); } } /** * 清除站点数据(使用站点级锁) */ public boolean clearStation(Integer deviceNo, Integer lockTaskNo, Integer currentStationId) { lockStations(currentStationId); try { @@ -754,23 +840,17 @@ if (statusList == null) { return false; } ZyStationStatusEntity currentStatus = statusList.stream() .filter(item -> item.getStationId().equals(currentStationId)).findFirst().orElse(null); if (currentStatus == null) { return false; } return updateStationDataInternal(currentStationId, deviceNo, 0, 0, false, "", false); } finally { unlockStations(currentStationId); } } /** * 标记站点堵塞(使用站点级锁) */ public boolean runBlockStation(Integer lockTaskNo, Integer currentStationId, Integer currentStationDeviceNo, Integer taskNo, Integer blockStationId) { lockStations(currentStationId); @@ -779,14 +859,11 @@ if (statusList == null) { return false; } ZyStationStatusEntity currentStatus = statusList.stream() .filter(item -> item.getStationId().equals(currentStationId)).findFirst().orElse(null); if (currentStatus == null) { return false; } return updateStationDataInternal(currentStationId, currentStationDeviceNo, taskNo, blockStationId, true, "", true); } finally { @@ -794,9 +871,6 @@ } } /** * 清除站点堵塞标记(堵塞恢复时使用) */ public void clearRunBlock(Integer stationId, Integer deviceNo) { lockStations(stationId); try { @@ -804,14 +878,11 @@ if (statusList == null) { return; } ZyStationStatusEntity currentStatus = statusList.stream() .filter(item -> item.getStationId().equals(stationId)).findFirst().orElse(null); if (currentStatus == null) { return; } if (currentStatus.isRunBlock()) { currentStatus.setRunBlock(false); News.info("[WCS Debug] 站点{}堵塞标记已清除", stationId); @@ -822,6 +893,10 @@ } private boolean checkTaskNoInArea(Integer taskNo) { if (taskNo == null || redisUtil == null) { return false; } Object fakeTaskNoAreaObj = redisUtil.get(RedisKeyType.FAKE_TASK_NO_AREA.key); if (fakeTaskNoAreaObj == null) { return false; @@ -830,11 +905,127 @@ JSONObject data = JSON.parseObject(String.valueOf(fakeTaskNoAreaObj)); Integer start = data.getInteger("start"); Integer end = data.getInteger("end"); if (start == null || end == null) { return false; } return taskNo >= start && taskNo <= end; } if (taskNo >= start && taskNo <= end) { return true; private Map<String, Object> buildDetails(Object... keyValues) { Map<String, Object> details = new LinkedHashMap<String, Object>(); if (keyValues == null) { return details; } for (int i = 0; i + 1 < keyValues.length; i += 2) { Object key = keyValues[i]; if (key != null) { details.put(String.valueOf(key), keyValues[i + 1]); } } return details; } private void traceEvent(Integer deviceNo, TaskRuntimeContext context, String eventType, String message, Map<String, Object> details, boolean terminal) { if (context == null || context.taskNo == null) { return; } try { FakeTaskTraceRegistry registry = SpringUtils.getBean(FakeTaskTraceRegistry.class); if (registry == null) { return; } registry.record(context.taskNo, context.threadImpl != null ? context.threadImpl : getThreadImpl(deviceNo), context.status, context.startStationId, context.currentStationId, context.finalTargetStationId, context.blockedStationId, context.getStitchedPathStationIds(), context.getPassedStationIds(), context.getPendingStationIds(), context.getLatestAppendedPath(), eventType, message, details, terminal); } catch (Exception ignore) { } } private String getThreadImpl(Integer deviceNo) { DeviceConfig deviceConfig = deviceConfigMap.get(deviceNo); return deviceConfig == null ? "" : deviceConfig.getThreadImpl(); } private boolean isTerminalStatus(String status) { return STATUS_BLOCKED.equals(status) || STATUS_CANCELLED.equals(status) || STATUS_TIMEOUT.equals(status) || STATUS_FINISHED.equals(status); } private static class TaskRuntimeContext { private final Integer taskNo; private final String threadImpl; private final LinkedBlockingQueue<Integer> pendingPathQueue = new LinkedBlockingQueue<Integer>(); private final List<Integer> stitchedPathStationIds = new ArrayList<Integer>(); private final List<Integer> passedStationIds = new ArrayList<Integer>(); private final List<Integer> latestAppendedPath = new ArrayList<Integer>(); private Integer startStationId; private Integer currentStationId; private Integer finalTargetStationId; private Integer blockedStationId; private boolean generateBarcode; private boolean initialized; private boolean arrivalHandled; private long lastStepAt = System.currentTimeMillis(); private long lastCommandAt = System.currentTimeMillis(); private String status = STATUS_WAITING; private TaskRuntimeContext(Integer taskNo, String threadImpl) { this.taskNo = taskNo; this.threadImpl = threadImpl; } return false; private void setStartStationIdIfAbsent(Integer stationId) { if (startStationId == null && stationId != null) { startStationId = stationId; } } private void appendStitchedPath(List<Integer> path) { latestAppendedPath.clear(); if (path == null) { return; } for (Integer stationId : path) { if (stationId == null) { continue; } latestAppendedPath.add(stationId); if (stitchedPathStationIds.isEmpty() || !stationId.equals(stitchedPathStationIds.get(stitchedPathStationIds.size() - 1))) { stitchedPathStationIds.add(stationId); } } } private void addPassedStation(Integer stationId) { if (stationId == null) { return; } if (passedStationIds.isEmpty() || !stationId.equals(passedStationIds.get(passedStationIds.size() - 1))) { passedStationIds.add(stationId); } } private List<Integer> getPendingStationIds() { return new ArrayList<Integer>(pendingPathQueue); } private List<Integer> getPassedStationIds() { return new ArrayList<Integer>(passedStationIds); } private List<Integer> getStitchedPathStationIds() { return new ArrayList<Integer>(stitchedPathStationIds); } private List<Integer> getLatestAppendedPath() { return new ArrayList<Integer>(latestAppendedPath); } } } src/main/webapp/components/DevpCard.js
@@ -32,6 +32,7 @@ <div class="mc-action-row"> <button type="button" class="mc-btn" @click="controlCommand">下发</button> <button type="button" class="mc-btn mc-btn-soft" @click="resetCommand">复位</button> <button v-if="showFakeTraceEntry" type="button" class="mc-btn mc-btn-ghost" @click="openFakeTracePage">仿真轨迹</button> </div> </div> </div> @@ -113,6 +114,7 @@ targetStationId: "" }, barcodePreviewCache: {}, showFakeTraceEntry: false, pageSize: this.readOnly ? 24 : 12, currentPage: 1, timer: null @@ -155,6 +157,7 @@ }, created: function () { MonitorCardKit.ensureStyles(); this.loadFakeProcessStatus(); if (this.autoRefresh) { this.timer = setInterval(this.getDevpStateInfo, 1000); } @@ -223,9 +226,32 @@ this.afterDataRefresh(); } }, loadFakeProcessStatus: function () { if (this.readOnly || !window.$ || typeof baseUrl === "undefined") { this.showFakeTraceEntry = false; return; } $.ajax({ url: baseUrl + "/openapi/getFakeSystemRunStatus", method: "get", success: function (res) { var data = res && res.data ? res.data : null; this.showFakeTraceEntry = !!(data && data.isFake); }.bind(this), error: function () { this.showFakeTraceEntry = false; }.bind(this) }); }, openControl: function () { this.showControl = !this.showControl; }, openFakeTracePage: function () { if (!this.showFakeTraceEntry) { return; } window.open(baseUrl + "/views/watch/fakeTrace.html", "_blank"); }, buildDetailEntries: function (item) { return [ { label: "编号", value: this.orDash(item.stationId) }, src/main/webapp/components/MapCanvas.js
@@ -49,7 +49,7 @@ </div> </div> `, props: ['lev', 'levList', 'crnParam', 'rgvParam', 'devpParam', 'stationTaskRange', 'highlightOnParamChange', 'viewportPadding', 'hudPadding'], props: ['lev', 'levList', 'crnParam', 'rgvParam', 'devpParam', 'stationTaskRange', 'highlightOnParamChange', 'viewportPadding', 'hudPadding', 'traceOverlay'], data() { return { map: [], @@ -103,6 +103,8 @@ hoverRaf: null, objectsContainer: null, objectsContainer2: null, traceOverlayContainer: null, tracePulseTween: null, tracksContainer: null, tracksGraphics: null, shelvesContainer: null, @@ -176,6 +178,7 @@ if (this.shelfCullRaf) { cancelAnimationFrame(this.shelfCullRaf); this.shelfCullRaf = null; } if (this.resizeDebounceTimer) { clearTimeout(this.resizeDebounceTimer); this.resizeDebounceTimer = null; } if (window.gsap && this.pixiApp && this.pixiApp.stage) { window.gsap.killTweensOf(this.pixiApp.stage.position); } this.clearTraceOverlay(); if (this.pixiApp) { this.pixiApp.destroy(true, { children: true }); } if (this.containerResizeObserver) { this.containerResizeObserver.disconnect(); this.containerResizeObserver = null; } window.removeEventListener('resize', this.scheduleResizeToContainer); @@ -234,6 +237,12 @@ window.gsap.fromTo(sprite, { alpha: 1 }, { alpha: 0.2, yoyo: true, repeat: 6, duration: 0.15 }); } } } }, traceOverlay: { deep: true, handler() { this.renderTraceOverlay(); } } }, @@ -384,6 +393,7 @@ this.graphicsRgvTrack = this.createTrackTexture(25, 25, 10); this.objectsContainer = new PIXI.Container(); this.objectsContainer2 = new PIXI.Container(); this.traceOverlayContainer = new PIXI.Container(); this.tracksContainer = new PIXI.ParticleContainer(10000, { scale: true, position: true, rotation: false, uvs: false, alpha: false }); this.tracksGraphics = new PIXI.Graphics(); this.shelvesContainer = new PIXI.Container(); @@ -395,6 +405,7 @@ this.mapRoot.addChild(this.shelvesContainer); this.mapRoot.addChild(this.objectsContainer); this.mapRoot.addChild(this.objectsContainer2); this.mapRoot.addChild(this.traceOverlayContainer); this.pixiApp.renderer.roundPixels = true; this.hoveredShelfCell = null; this.hoverPointer = { x: 0, y: 0 }; @@ -594,6 +605,7 @@ changeFloor(lev) { this.currentLev = lev; this.clearLoopStationHighlight(); this.clearTraceOverlay(); this.isSwitchingFloor = true; this.hideShelfTooltip(); this.hoveredShelfCell = null; @@ -619,6 +631,7 @@ }, createMapData(map) { this.clearLoopStationHighlight(); this.clearTraceOverlay(); this.hideShelfTooltip(); this.hoveredShelfCell = null; this.mapRowOffsets = []; @@ -886,6 +899,7 @@ this.applyMapTransform(true); this.map = map; this.isSwitchingFloor = false; this.renderTraceOverlay(); }, initWidth(map) { let maxRow = map.length; @@ -1816,6 +1830,129 @@ this.hoverLoopNo = null; this.hoverLoopStationIdSet = new Set(); }, clearTraceOverlay() { if (window.gsap && this.tracePulseTween) { try { this.tracePulseTween.kill(); } catch (e) {} } this.tracePulseTween = null; if (!this.traceOverlayContainer) { return; } const children = this.traceOverlayContainer.removeChildren(); children.forEach((child) => { if (child && typeof child.destroy === 'function') { child.destroy({ children: true, texture: false, baseTexture: false }); } }); }, normalizeTraceOverlay(trace) { if (!trace) { return null; } const taskNo = parseInt(trace.taskNo, 10); return { taskNo: isNaN(taskNo) ? null : taskNo, status: trace.status || '', currentStationId: this.parseStationTaskNo(trace.currentStationId), finalTargetStationId: this.parseStationTaskNo(trace.finalTargetStationId), blockedStationId: this.parseStationTaskNo(trace.blockedStationId), passedStationIds: this.normalizeTraceStationIds(trace.passedStationIds), pendingStationIds: this.normalizeTraceStationIds(trace.pendingStationIds), latestAppendedPath: this.normalizeTraceStationIds(trace.latestAppendedPath) }; }, normalizeTraceStationIds(list) { if (!Array.isArray(list)) { return []; } const result = []; list.forEach((item) => { const stationId = parseInt(item, 10); if (!isNaN(stationId)) { result.push(stationId); } }); return result; }, getStationCenter(stationId) { if (stationId == null || !this.pixiStaMap) { return null; } const sprite = this.pixiStaMap.get(parseInt(stationId, 10)); if (!sprite) { return null; } return { x: sprite.x + sprite.width / 2, y: sprite.y + sprite.height / 2 }; }, drawTracePairs(graphics, stationIds, color, width, alpha) { if (!graphics || !Array.isArray(stationIds) || stationIds.length < 2) { return; } graphics.lineStyle({ width: width, color: color, alpha: alpha, cap: PIXI.LINE_CAP.ROUND, join: PIXI.LINE_JOIN.ROUND }); for (let i = 1; i < stationIds.length; i++) { const prev = this.getStationCenter(stationIds[i - 1]); const curr = this.getStationCenter(stationIds[i]); if (!prev || !curr) { continue; } graphics.moveTo(prev.x, prev.y); graphics.lineTo(curr.x, curr.y); } }, drawTraceMarker(container, stationId, options) { const point = this.getStationCenter(stationId); if (!container || !point) { return null; } const marker = new PIXI.Container(); const ring = new PIXI.Graphics(); const fill = new PIXI.Graphics(); const radius = options && options.radius ? options.radius : 18; const color = options && options.color != null ? options.color : 0x1d4ed8; ring.lineStyle(3, color, 0.95); ring.drawCircle(0, 0, radius); fill.beginFill(color, 0.18); fill.drawCircle(0, 0, Math.max(6, radius * 0.42)); fill.endFill(); marker.addChild(ring); marker.addChild(fill); marker.position.set(point.x, point.y); container.addChild(marker); return marker; }, buildPendingTraceSequence(trace) { const pending = this.normalizeTraceStationIds(trace && trace.pendingStationIds); const currentStationId = this.parseStationTaskNo(trace && trace.currentStationId); if (pending.length === 0) { return currentStationId > 0 ? [currentStationId] : []; } if (currentStationId > 0 && pending[0] !== currentStationId) { return [currentStationId].concat(pending); } return pending; }, renderTraceOverlay() { if (!this.traceOverlayContainer) { return; } this.clearTraceOverlay(); const trace = this.normalizeTraceOverlay(this.traceOverlay); if (!trace || !this.pixiStaMap || this.pixiStaMap.size === 0) { return; } const graphics = new PIXI.Graphics(); this.drawTracePairs(graphics, trace.passedStationIds, 0x2563eb, 5, 0.95); this.drawTracePairs(graphics, this.buildPendingTraceSequence(trace), 0xf97316, 4, 0.9); this.drawTracePairs(graphics, trace.latestAppendedPath, 0xfacc15, 7, 0.88); this.traceOverlayContainer.addChild(graphics); const currentMarker = this.drawTraceMarker(this.traceOverlayContainer, trace.currentStationId, { color: 0x2563eb, radius: 20 }); if (currentMarker && window.gsap) { this.tracePulseTween = window.gsap.to(currentMarker.scale, { x: 1.18, y: 1.18, duration: 0.55, repeat: -1, yoyo: true, ease: 'sine.inOut' }); } if (trace.blockedStationId > 0) { const blockedMarker = this.drawTraceMarker(this.traceOverlayContainer, trace.blockedStationId, { color: 0xdc2626, radius: 22 }); if (blockedMarker) { blockedMarker.alpha = 0.95; } } }, handleLoopCardEnter(loopItem) { if (!loopItem) { return; } this.hoverLoopNo = loopItem.loopNo; @@ -2657,9 +2794,6 @@ } } }); src/main/webapp/views/watch/console.html
@@ -553,7 +553,7 @@ <script src="../../components/WatchDualCrnCard.js"></script> <script src="../../components/DevpCard.js"></script> <script src="../../components/WatchRgvCard.js"></script> <script src="../../components/MapCanvas.js?v=20260311_resize_debounce1"></script> <script src="../../components/MapCanvas.js?v=20260319_fake_trace_overlay1"></script> <script> let ws; var app = new Vue({ src/main/webapp/views/watch/fakeTrace.html
New file @@ -0,0 +1,885 @@ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>仿真任务轨迹调试</title> <link rel="stylesheet" href="../../static/css/watch/console_vue.css"> <link rel="stylesheet" href="../../static/vue/element/element.css"> <style> html, body, #app { width: 100%; height: 100%; margin: 0; overflow: hidden; } body { background: linear-gradient(180deg, #eef4f8 0%, #e7edf4 100%); color: #27425c; font-family: "PingFang SC", "Microsoft YaHei", sans-serif; } #app { display: flex; flex-direction: column; } .trace-page { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 18px; box-sizing: border-box; gap: 14px; } .trace-topbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 18px; border-radius: 18px; border: 1px solid rgba(255, 255, 255, 0.4); background: rgba(248, 251, 253, 0.92); box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08); } .trace-topbar-main { min-width: 0; } .trace-title { font-size: 18px; font-weight: 700; line-height: 1.2; } .trace-subtitle { margin-top: 4px; font-size: 12px; color: #718399; } .trace-topbar-actions { display: flex; align-items: center; gap: 10px; flex-shrink: 0; } .trace-search { width: 180px; height: 34px; padding: 0 12px; border-radius: 999px; border: 1px solid rgba(210, 221, 232, 0.98); background: rgba(255, 255, 255, 0.9); color: #31485f; outline: none; box-sizing: border-box; } .trace-status-pill { padding: 6px 10px; border-radius: 999px; font-size: 11px; font-weight: 700; background: rgba(111, 149, 189, 0.12); color: #42617f; } .trace-main { flex: 1; min-height: 0; display: grid; grid-template-columns: 300px minmax(0, 1fr) 360px; gap: 14px; } .trace-card { min-height: 0; display: flex; flex-direction: column; border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.42); background: rgba(248, 251, 253, 0.94); box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08); overflow: hidden; } .trace-card-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 14px 16px 10px; border-bottom: 1px solid rgba(226, 232, 240, 0.72); background: rgba(255, 255, 255, 0.24); } .trace-card-title { font-size: 14px; font-weight: 700; color: #27425c; } .trace-card-body { flex: 1; min-height: 0; display: flex; flex-direction: column; } .trace-task-list { flex: 1; min-height: 0; padding: 10px; overflow: auto; } .trace-task-item { width: 100%; display: flex; flex-direction: column; gap: 7px; margin-bottom: 10px; padding: 12px; border: 1px solid transparent; border-radius: 14px; background: rgba(247, 250, 252, 0.82); text-align: left; color: inherit; cursor: pointer; box-sizing: border-box; } .trace-task-item:last-child { margin-bottom: 0; } .trace-task-item.is-active { border-color: rgba(111, 149, 189, 0.34); background: rgba(235, 243, 251, 0.94); } .trace-task-line { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .trace-task-title { font-size: 13px; font-weight: 700; color: #27425c; } .trace-task-meta { font-size: 11px; color: #748397; line-height: 1.5; word-break: break-all; } .trace-badge { padding: 3px 8px; border-radius: 999px; font-size: 10px; font-weight: 700; white-space: nowrap; } .trace-badge.is-running { background: rgba(59, 130, 246, 0.14); color: #245baf; } .trace-badge.is-waiting { background: rgba(148, 163, 184, 0.16); color: #64748b; } .trace-badge.is-blocked { background: rgba(239, 68, 68, 0.14); color: #b42318; } .trace-badge.is-timeout { background: rgba(249, 115, 22, 0.16); color: #b45309; } .trace-badge.is-finished { background: rgba(34, 197, 94, 0.14); color: #15803d; } .trace-badge.is-cancelled { background: rgba(148, 163, 184, 0.18); color: #64748b; } .trace-map-card { padding: 0; } .trace-map-card .trace-card-body { gap: 10px; padding: 12px; box-sizing: border-box; } .trace-summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; } .trace-summary-item { padding: 10px 12px; border-radius: 12px; background: rgba(247, 250, 252, 0.88); border: 1px solid rgba(233, 239, 244, 0.96); } .trace-summary-label { font-size: 11px; color: #8090a2; } .trace-summary-value { margin-top: 5px; font-size: 14px; font-weight: 700; color: #31485f; word-break: break-all; } .trace-path-board { display: grid; grid-template-columns: 1fr; gap: 8px; } .trace-path-row { padding: 10px 12px; border-radius: 12px; background: rgba(247, 250, 252, 0.88); border: 1px solid rgba(233, 239, 244, 0.96); } .trace-path-label { font-size: 11px; font-weight: 700; color: #6f8194; } .trace-path-value { margin-top: 6px; font-size: 12px; line-height: 1.5; color: #31485f; word-break: break-all; } .trace-map-shell { flex: 1; min-height: 0; border-radius: 16px; overflow: hidden; border: 1px solid rgba(224, 232, 239, 0.92); background: rgba(255, 255, 255, 0.62); } .trace-map-shell map-canvas { display: block; width: 100%; height: 100%; } .trace-timeline { flex: 1; min-height: 0; padding: 10px; overflow: auto; } .trace-event { position: relative; padding: 0 0 14px 18px; margin-bottom: 14px; border-left: 2px solid rgba(210, 221, 232, 0.96); } .trace-event:last-child { margin-bottom: 0; padding-bottom: 0; } .trace-event::before { content: ""; position: absolute; left: -6px; top: 2px; width: 10px; height: 10px; border-radius: 50%; background: #6f95bd; box-shadow: 0 0 0 3px rgba(111, 149, 189, 0.12); } .trace-event-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; } .trace-event-title { font-size: 12px; font-weight: 700; color: #27425c; } .trace-event-time { font-size: 11px; color: #8090a2; white-space: nowrap; } .trace-event-message { margin-top: 4px; font-size: 12px; line-height: 1.5; color: #4a627a; } .trace-event-detail { margin-top: 6px; font-size: 11px; line-height: 1.6; color: #6f8194; word-break: break-all; } .trace-empty { flex: 1; display: flex; align-items: center; justify-content: center; padding: 20px; text-align: center; color: #8b9aad; font-size: 12px; } @media (max-width: 1440px) { .trace-main { grid-template-columns: 280px minmax(0, 1fr) 320px; } .trace-summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } </style> <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> <script type="text/javascript" src="../../static/js/common.js"></script> <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> <script type="text/javascript" src="../../static/vue/element/element.js"></script> <script src="../../static/js/gsap.min.js"></script> <script src="../../static/js/pixi-legacy.min.js"></script> </head> <body> <div id="app"> <div class="trace-page"> <div class="trace-topbar"> <div class="trace-topbar-main"> <div class="trace-title">仿真任务轨迹调试</div> <div class="trace-subtitle">按任务号查看路径动态拼接、当前位置、堵塞站点和执行时间线</div> </div> <div class="trace-topbar-actions"> <div class="trace-status-pill">{{ wsStatusText }}</div> <input class="trace-search" v-model.trim="searchTaskNo" placeholder="筛选任务号" /> </div> </div> <div class="trace-main"> <div class="trace-card"> <div class="trace-card-header"> <div class="trace-card-title">活跃任务</div> <div class="trace-status-pill">{{ filteredTraces.length }} 个</div> </div> <div class="trace-card-body"> <div v-if="filteredTraces.length === 0" class="trace-empty">当前没有活跃仿真任务</div> <div v-else class="trace-task-list"> <button v-for="item in filteredTraces" :key="item.taskNo" type="button" class="trace-task-item" :class="{ 'is-active': selectedTaskNo === item.taskNo }" @click="selectTrace(item.taskNo)"> <div class="trace-task-line"> <div class="trace-task-title">任务 {{ item.taskNo }}</div> <span class="trace-badge" :class="'is-' + statusTone(item.status)">{{ item.status || '--' }}</span> </div> <div class="trace-task-meta"> 当前站: {{ orDash(item.currentStationId) }}<br> 目标站: {{ orDash(item.finalTargetStationId) }}<br> 更新时间: {{ formatTime(item.updatedAt) }} </div> </button> </div> </div> </div> <div class="trace-card trace-map-card"> <div class="trace-card-header"> <div class="trace-card-title">地图与任务摘要</div> <div v-if="selectedTrace" class="trace-status-pill">楼层 {{ currentLev }}F</div> </div> <div class="trace-card-body"> <template v-if="selectedTrace"> <div class="trace-summary-grid"> <div class="trace-summary-item"> <div class="trace-summary-label">任务号</div> <div class="trace-summary-value">{{ selectedTrace.taskNo }}</div> </div> <div class="trace-summary-item"> <div class="trace-summary-label">当前站点</div> <div class="trace-summary-value">{{ orDash(selectedTrace.currentStationId) }}</div> </div> <div class="trace-summary-item"> <div class="trace-summary-label">最终目标</div> <div class="trace-summary-value">{{ orDash(selectedTrace.finalTargetStationId) }}</div> </div> <div class="trace-summary-item"> <div class="trace-summary-label">线程实现</div> <div class="trace-summary-value">{{ orDash(selectedTrace.threadImpl) }}</div> </div> </div> <div class="trace-path-board"> <div class="trace-path-row"> <div class="trace-path-label">已走路径</div> <div class="trace-path-value">{{ formatPath(selectedTrace.passedStationIds) }}</div> </div> <div class="trace-path-row"> <div class="trace-path-label">待走路径</div> <div class="trace-path-value">{{ formatPath(selectedTrace.pendingStationIds) }}</div> </div> <div class="trace-path-row"> <div class="trace-path-label">最新拼接段</div> <div class="trace-path-value">{{ formatPath(selectedTrace.latestAppendedPath) }}</div> </div> </div> </template> <div class="trace-map-shell"> <map-canvas :lev="currentLev" :lev-list="levList" :crn-param="crnParam" :rgv-param="rgvParam" :devp-param="devpParam" :station-task-range="stationTaskRange" :trace-overlay="selectedTrace" @switch-lev="switchLev"> </map-canvas> </div> </div> </div> <div class="trace-card"> <div class="trace-card-header"> <div class="trace-card-title">执行时间线</div> <div v-if="selectedTrace" class="trace-status-pill">{{ selectedTraceEvents.length }} 条</div> </div> <div class="trace-card-body"> <div v-if="!selectedTrace" class="trace-empty">请选择一个任务查看明细</div> <div v-else-if="selectedTraceEvents.length === 0" class="trace-empty">当前任务还没有轨迹事件</div> <div v-else class="trace-timeline"> <div v-for="(event, index) in selectedTraceEvents" :key="event.eventType + '-' + event.timestamp + '-' + index" class="trace-event"> <div class="trace-event-head"> <div class="trace-event-title">{{ event.eventType }}</div> <div class="trace-event-time">{{ formatTime(event.timestamp) }}</div> </div> <div class="trace-event-message">{{ event.message || '--' }}</div> <div v-for="detail in renderEventDetails(event)" :key="detail" class="trace-event-detail">{{ detail }}</div> </div> </div> </div> </div> </div> </div> </div> <script src="../../components/MapCanvas.js?v=20260319_fake_trace_overlay1"></script> <script> var fakeTraceWs = null; new Vue({ el: '#app', data: { traces: [], selectedTaskNo: null, searchTaskNo: '', currentLev: 1, levList: [], stationTaskRange: { inbound: null, outbound: null }, crnParam: { crnNo: 0 }, rgvParam: { rgvNo: 0 }, devpParam: { stationId: 0 }, wsReconnectTimer: null, wsReconnectAttempts: 0, wsReconnectBaseDelay: 1000, wsReconnectMaxDelay: 15000, tracePollTimer: null, wsStatus: 'connecting' }, computed: { filteredTraces: function () { var keyword = String(this.searchTaskNo || '').trim(); if (!keyword) { return this.traces; } return this.traces.filter(function (item) { return String(item.taskNo || '').indexOf(keyword) >= 0; }); }, selectedTrace: function () { if (!this.selectedTaskNo) { return null; } for (var i = 0; i < this.traces.length; i++) { if (this.traces[i] && this.traces[i].taskNo === this.selectedTaskNo) { return this.traces[i]; } } return null; }, selectedTraceEvents: function () { var trace = this.selectedTrace; if (!trace || !Array.isArray(trace.events)) { return []; } return trace.events.slice().sort(function (a, b) { var va = a && a.timestamp ? a.timestamp : 0; var vb = b && b.timestamp ? b.timestamp : 0; return vb - va; }); }, wsStatusText: function () { if (this.wsStatus === 'open') { return '已连接'; } if (this.wsStatus === 'closed') { return '连接断开'; } if (this.wsStatus === 'error') { return '连接异常'; } return '连接中'; } }, watch: { selectedTrace: { deep: true, immediate: true, handler: function (trace) { if (!trace) { this.devpParam.stationId = 0; return; } this.devpParam.stationId = this.resolveFocusStationId(trace); this.applySelectedTraceFloor(trace); } } }, created: function () { this.init(); }, beforeDestroy: function () { if (this.tracePollTimer) { clearInterval(this.tracePollTimer); this.tracePollTimer = null; } if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; } if (fakeTraceWs && (fakeTraceWs.readyState === WebSocket.OPEN || fakeTraceWs.readyState === WebSocket.CONNECTING)) { try { fakeTraceWs.close(); } catch (e) {} } }, methods: { init: function () { this.connectWs(); this.getLevList(); this.getStationTaskRange(); }, connectWs: function () { if (fakeTraceWs && (fakeTraceWs.readyState === WebSocket.OPEN || fakeTraceWs.readyState === WebSocket.CONNECTING)) { return; } this.wsStatus = 'connecting'; fakeTraceWs = new WebSocket('ws://' + window.location.host + baseUrl + '/console/websocket'); fakeTraceWs.onopen = this.webSocketOnOpen; fakeTraceWs.onerror = this.webSocketOnError; fakeTraceWs.onmessage = this.webSocketOnMessage; fakeTraceWs.onclose = this.webSocketOnClose; }, webSocketOnOpen: function () { this.wsStatus = 'open'; this.wsReconnectAttempts = 0; if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; } this.refreshTrace(); if (!this.tracePollTimer) { this.tracePollTimer = setInterval(function () { this.refreshTrace(); }.bind(this), 1000); } }, webSocketOnError: function () { this.wsStatus = 'error'; this.scheduleReconnect(); }, webSocketOnClose: function () { this.wsStatus = 'closed'; this.scheduleReconnect(); }, webSocketOnMessage: function (event) { var result = JSON.parse(event.data); if (result.url === '/console/latest/data/fake/trace') { this.setTraceList(JSON.parse(result.data)); } }, scheduleReconnect: function () { if (this.wsReconnectTimer) { return; } var attempt = this.wsReconnectAttempts + 1; var jitter = Math.floor(Math.random() * 300); var delay = Math.min(this.wsReconnectMaxDelay, this.wsReconnectBaseDelay * Math.pow(2, this.wsReconnectAttempts)) + jitter; this.wsReconnectTimer = setTimeout(function () { this.wsReconnectTimer = null; this.wsReconnectAttempts = attempt; this.connectWs(); }.bind(this), delay); }, sendWs: function (payload) { if (fakeTraceWs && fakeTraceWs.readyState === WebSocket.OPEN) { fakeTraceWs.send(payload); } }, refreshTrace: function () { this.sendWs(JSON.stringify({ url: '/console/latest/data/fake/trace', data: {} })); }, setTraceList: function (res) { if (!res) { return; } if (res.code === 403) { parent.location.href = baseUrl + '/login'; return; } if (res.code !== 200) { return; } this.traces = Array.isArray(res.data) ? res.data : []; if (this.selectedTaskNo != null) { var matched = this.traces.some(function (item) { return item && item.taskNo === this.selectedTaskNo; }.bind(this)); if (!matched) { this.selectedTaskNo = this.traces.length > 0 ? this.traces[0].taskNo : null; } } else if (this.traces.length > 0) { this.selectedTaskNo = this.traces[0].taskNo; } }, selectTrace: function (taskNo) { this.selectedTaskNo = taskNo; }, switchLev: function (lev) { this.currentLev = lev; }, getLevList: function () { $.ajax({ url: baseUrl + '/basMap/getLevList', headers: { token: localStorage.getItem('token') }, method: 'get', success: function (res) { if (!res || res.code !== 200) { return; } this.levList = res.data || []; if ((!this.currentLev || this.currentLev === 0) && this.levList.length > 0) { this.currentLev = this.levList[0]; } }.bind(this) }); }, getStationTaskRange: function () { this.fetchWrkLastnoRange(1, 'inbound'); this.fetchWrkLastnoRange(101, 'outbound'); }, fetchWrkLastnoRange: function (id, key) { $.ajax({ url: baseUrl + '/wrkLastno/' + id + '/auth', headers: { token: localStorage.getItem('token') }, method: 'get', success: function (res) { if (!res || res.code !== 200 || !res.data) { return; } var data = res.data; var nextRange = Object.assign({}, this.stationTaskRange); nextRange[key] = { start: data.sNo, end: data.eNo }; this.stationTaskRange = nextRange; }.bind(this) }); }, resolveFocusStationId: function (trace) { if (!trace) { return 0; } return trace.currentStationId || trace.blockedStationId || trace.startStationId || 0; }, applySelectedTraceFloor: function (trace) { var floor = this.resolveTraceFloor(trace); if (floor > 0 && floor !== this.currentLev) { this.currentLev = floor; } }, resolveTraceFloor: function (trace) { if (!trace) { return this.currentLev || 1; } var status = String(trace.status || '').toUpperCase(); var stationId = trace.currentStationId || trace.blockedStationId; if (!stationId && status === 'WAITING') { stationId = trace.startStationId; } if (!stationId) { return this.currentLev || 1; } var text = String(stationId); var floor = parseInt(text.charAt(0), 10); if (isNaN(floor) || floor <= 0) { return this.currentLev || 1; } return floor; }, statusTone: function (status) { var value = String(status || '').toUpperCase(); if (value === 'RUNNING') { return 'running'; } if (value === 'BLOCKED') { return 'blocked'; } if (value === 'TIMEOUT') { return 'timeout'; } if (value === 'FINISHED') { return 'finished'; } if (value === 'CANCELLED') { return 'cancelled'; } return 'waiting'; }, renderEventDetails: function (event) { if (!event || !event.details) { return []; } var labelMap = { segmentPath: '原始分段', appendedPath: '实际追加', appendStartIndex: '追加起点索引', currentStationId: '当前站点', queueTailStationId: '队列尾站点', ignoreReason: '忽略原因', fromTargetStationId: '原目标站', toTargetStationId: '新目标站', fromStationId: '起始站', toStationId: '到达站', remainingPendingPath: '剩余路径', blockedStationId: '堵塞站点', timeoutMs: '超时时间', targetStationId: '目标站', reason: '结束原因', barcodeGenerated: '生成条码', commandStationId: '命令起点', commandTargetStationId: '命令目标', stationId: '站点' }; var result = []; Object.keys(event.details).forEach(function (key) { var value = event.details[key]; if (value == null || value === '') { return; } var text; if (Array.isArray(value)) { text = value.join(' -> '); } else if (typeof value === 'boolean') { text = value ? '是' : '否'; } else { text = String(value); } result.push((labelMap[key] || key) + ': ' + text); }); return result; }, formatPath: function (path) { if (!Array.isArray(path) || path.length === 0) { return '--'; } return path.join(' -> '); }, formatTime: function (timestamp) { if (!timestamp) { return '--'; } var date = new Date(timestamp); var pad = function (value) { return value < 10 ? '0' + value : '' + value; }; return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); }, orDash: function (value) { return value == null || value === '' ? '--' : value; } } }); </script> </body> </html>