Junjie
8 小时以前 be5c87afd82e50b6ef58a24e06a7a6cb36fb5007
#仿真优化
1个文件已删除
4个文件已添加
7个文件已修改
2883 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/ConsoleController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/vo/FakeTaskTraceEventVo.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/vo/FakeTaskTraceVo.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/ZyStationConnectDriver.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/fake/FakeTaskTraceRegistry.java 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/fake/ZyStationFakeConnect.java 734 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java 833 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/DevpCard.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/MapCanvas.js 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/watch/console.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/watch/fakeTrace.html 885 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>