#
Junjie
2 天以前 90402710d962aa357062ecb94649e0f277c1dfb3
#
2个文件已删除
28个文件已添加
12个文件已修改
9286 ■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/StationController.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/cache/MessageQueue.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/dispatch/StationCommandDispatchResult.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/dispatch/StationCommandDispatcher.java 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/FakeProcess.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/GslProcess.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/NormalProcess.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/XiaosongProcess.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/ZyStationV5Thread.java 971 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/v5/StationV5RunBlockReroutePlanner.java 375 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutionPlan.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentPlanner.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/v5/StationV5StatusReader.java 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/StationOperateProcessUtils.java 2998 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/StationDispatchLoadSupport.java 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/StationDispatchRuntimeStateSupport.java 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/StationOutboundDecisionSupport.java 615 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/StationOutboundDispatchProcessor.java 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/StationRegularDispatchProcessor.java 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/StationRerouteProcessor.java 734 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/StationTaskIdleTrack.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/CircleTargetCandidate.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/DispatchLimitConfig.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/LoadGuardState.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/LoopHitResult.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/OutOrderDispatchDecision.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/RerouteCommandPlan.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/RerouteContext.java 185 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/RerouteDecision.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/RerouteExecutionResult.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/station/model/RerouteSceneType.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/asrs/controller/StationControllerTest.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/core/dispatch/StationCommandDispatcherTest.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/core/thread/impl/ZyStationV5ThreadTest.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/core/thread/impl/v5/StationV5RunBlockReroutePlannerTest.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/core/utils/StationOperateProcessUtilsReroutePipelineTest.java 382 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/core/utils/StationRerouteProcessorTest.java 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/core/utils/station/StationDispatchRuntimeStateSupportTest.java 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/core/utils/station/StationOutboundDispatchProcessorTest.java 202 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/core/utils/station/StationRegularDispatchProcessorTest.java 240 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/core/utils/station/model/StationModelTypePlacementTest.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/StationController.java
@@ -7,6 +7,8 @@
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.DeviceConfigService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.dispatch.StationCommandDispatchResult;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.model.StationObjModel;
@@ -22,10 +24,8 @@
import com.core.common.Cools;
import com.core.common.R;
import com.zy.asrs.domain.param.StationCommandMoveParam;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.SlaveType;
import com.zy.core.model.Task;
import com.zy.core.model.command.StationCommand;
import com.zy.core.thread.StationThread;
@@ -45,6 +45,8 @@
    private ConfigService configService;
    @Autowired
    private DeviceConfigService deviceConfigService;
    @Autowired
    private StationCommandDispatcher stationCommandDispatcher;
    @PostMapping("/command/move")
    public R commandMove(@RequestBody StationCommandMoveParam param) {
@@ -73,7 +75,11 @@
        if (command == null) {
            return R.error("生成输送命令失败,路径为空或不可达");
        }
        MessageQueue.offer(SlaveType.Devp, devpNo, new Task(2, command));
        StationCommandDispatchResult dispatchResult = stationCommandDispatcher
                .dispatch(devpNo, command, "station-controller", "manual-move");
        if (!dispatchResult.isAccepted()) {
            return R.error("输送命令下发失败:" + dispatchResult.getReason());
        }
        return R.ok();
    }
@@ -118,7 +124,11 @@
        StationCommand command = stationThread.getCommand(StationCommandType.WRITE_INFO, 9997, stationId, stationId, 0);
        command.setBarcode(barcode.trim());
        MessageQueue.offer(SlaveType.Devp, devpNo, new Task(2, command));
        StationCommandDispatchResult dispatchResult = stationCommandDispatcher
                .dispatch(devpNo, command, "station-controller", "manual-barcode");
        if (!dispatchResult.isAccepted()) {
            return R.error("条码命令下发失败:" + dispatchResult.getReason());
        }
        return R.ok();
    }
@@ -172,5 +182,4 @@
        }
        return null;
    }
}
src/main/java/com/zy/core/cache/MessageQueue.java
@@ -4,6 +4,7 @@
import com.zy.core.model.Task;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
@@ -65,21 +66,19 @@
     * 如果发现队列已满无法添加的话,会直接返回false。
     */
    public static boolean offer(SlaveType type, Integer id, Task task) {
        Queue<Task> queue = resolveQueue(type, id);
        if (queue == null) {
            return false;
        }
        switch (type) {
            case Crn:
                return CRN_EXCHANGE.get(id).offer(task);
            case DualCrn:
                return DUAL_CRN_EXCHANGE.get(id).offer(task);
            case Rgv:
                return RGV_EXCHANGE.get(id).offer(task);
            case Devp:
                return DEVP_EXCHANGE.get(id).offer(task);
            case Barcode:
                return BARCODE_EXCHANGE.get(id).offer(task);
            case Led:
                return LED_EXCHANGE.get(id).offer(task);
            case Scale:
                return SCALE_EXCHANGE.get(id).offer(task);
                return queue.offer(task);
            default:
                return false;
        }
@@ -90,21 +89,19 @@
     * 若队列为空,返回null。
     */
    public static Task poll(SlaveType type, Integer id) {
        Queue<Task> queue = resolveQueue(type, id);
        if (queue == null) {
            return null;
        }
        switch (type) {
            case Crn:
                return CRN_EXCHANGE.get(id).poll();
            case DualCrn:
                return DUAL_CRN_EXCHANGE.get(id).poll();
            case Rgv:
                return RGV_EXCHANGE.get(id).poll();
            case Devp:
                return DEVP_EXCHANGE.get(id).poll();
            case Barcode:
                return BARCODE_EXCHANGE.get(id).poll();
            case Led:
                return LED_EXCHANGE.get(id).poll();
            case Scale:
                return SCALE_EXCHANGE.get(id).poll();
                return queue.poll();
            default:
                return null;
        }
@@ -114,52 +111,75 @@
     * 取出元素,并不删除.
     */
    public static Task peek(SlaveType type, Integer id) {
        Queue<Task> queue = resolveQueue(type, id);
        if (queue == null) {
            return null;
        }
        switch (type) {
            case Crn:
                return CRN_EXCHANGE.get(id).peek();
            case DualCrn:
                return DUAL_CRN_EXCHANGE.get(id).peek();
            case Rgv:
                return RGV_EXCHANGE.get(id).peek();
            case Devp:
                return DEVP_EXCHANGE.get(id).peek();
            case Barcode:
                return BARCODE_EXCHANGE.get(id).peek();
            case Led:
                return LED_EXCHANGE.get(id).peek();
            case Scale:
                return SCALE_EXCHANGE.get(id).peek();
                return queue.peek();
            default:
                return null;
        }
    }
    public static void clear(SlaveType type, Integer id){
        Queue<Task> queue = resolveQueue(type, id);
        if (queue == null) {
            return;
        }
        switch (type) {
            case Crn:
                CRN_EXCHANGE.get(id).clear();
                break;
            case DualCrn:
                DUAL_CRN_EXCHANGE.get(id).clear();
                break;
            case Rgv:
                RGV_EXCHANGE.get(id).clear();
                break;
            case Devp:
                DEVP_EXCHANGE.get(id).clear();
                break;
            case Barcode:
                BARCODE_EXCHANGE.get(id).clear();
                break;
            case Led:
                LED_EXCHANGE.get(id).clear();
                break;
            case Scale:
                SCALE_EXCHANGE.get(id).clear();
                queue.clear();
                break;
            default:
                break;
        }
    }
    public static boolean hasExchange(SlaveType type, Integer id) {
        return resolveQueue(type, id) != null;
    }
    public static int size(SlaveType type, Integer id) {
        Queue<Task> queue = resolveQueue(type, id);
        return queue == null ? 0 : queue.size();
    }
    private static Queue<Task> resolveQueue(SlaveType type, Integer id) {
        if (type == null || id == null) {
            return null;
        }
        switch (type) {
            case Crn:
                return CRN_EXCHANGE.get(id);
            case DualCrn:
                return DUAL_CRN_EXCHANGE.get(id);
            case Rgv:
                return RGV_EXCHANGE.get(id);
            case Devp:
                return DEVP_EXCHANGE.get(id);
            case Barcode:
                return BARCODE_EXCHANGE.get(id);
            case Led:
                return LED_EXCHANGE.get(id);
            case Scale:
                return SCALE_EXCHANGE.get(id);
            default:
                return null;
        }
    }
}
src/main/java/com/zy/core/dispatch/StationCommandDispatchResult.java
New file
@@ -0,0 +1,27 @@
package com.zy.core.dispatch;
import lombok.Data;
@Data
public class StationCommandDispatchResult {
    private final boolean accepted;
    private final String reason;
    private final int queueDepth;
    private final String source;
    private final String scene;
    public static StationCommandDispatchResult accepted(String reason,
                                                        int queueDepth,
                                                        String source,
                                                        String scene) {
        return new StationCommandDispatchResult(true, reason, queueDepth, source, scene);
    }
    public static StationCommandDispatchResult rejected(String reason,
                                                        int queueDepth,
                                                        String source,
                                                        String scene) {
        return new StationCommandDispatchResult(false, reason, queueDepth, source, scene);
    }
}
src/main/java/com/zy/core/dispatch/StationCommandDispatcher.java
New file
@@ -0,0 +1,121 @@
package com.zy.core.dispatch;
import com.alibaba.fastjson.JSON;
import com.core.common.Cools;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.MessageQueue;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.model.Task;
import com.zy.core.model.command.StationCommand;
import com.zy.core.move.StationMoveCoordinator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class StationCommandDispatcher {
    private static final int STATION_COMMAND_DISPATCH_DEDUP_SECONDS = 10;
    @Autowired(required = false)
    private RedisUtil redisUtil;
    @Autowired(required = false)
    private StationMoveCoordinator stationMoveCoordinator;
    public StationCommandDispatcher() {
    }
    public StationCommandDispatcher(RedisUtil redisUtil, StationMoveCoordinator stationMoveCoordinator) {
        this.redisUtil = redisUtil;
        this.stationMoveCoordinator = stationMoveCoordinator;
    }
    public StationCommandDispatchResult dispatch(Integer deviceNo,
                                                 StationCommand command,
                                                 String source,
                                                 String scene) {
        String normalizedSource = Cools.isEmpty(source) ? "unknown" : source;
        String normalizedScene = Cools.isEmpty(scene) ? "default" : scene;
        if (deviceNo == null || command == null) {
            return reject("invalid-argument", 0, normalizedSource, normalizedScene, null);
        }
        if (!MessageQueue.hasExchange(SlaveType.Devp, deviceNo)) {
            return reject("queue-not-initialized", 0, normalizedSource, normalizedScene, command);
        }
        String dedupKey = buildDedupKey(deviceNo, command);
        if (!Cools.isEmpty(dedupKey) && redisUtil != null && redisUtil.get(dedupKey) != null) {
            return reject("dedup-suppressed",
                    MessageQueue.size(SlaveType.Devp, deviceNo),
                    normalizedSource,
                    normalizedScene,
                    command);
        }
        if (!Cools.isEmpty(dedupKey) && redisUtil != null) {
            redisUtil.set(dedupKey, "lock", STATION_COMMAND_DISPATCH_DEDUP_SECONDS);
        }
        boolean offered = MessageQueue.offer(SlaveType.Devp, deviceNo, new Task(2, command));
        int queueDepth = MessageQueue.size(SlaveType.Devp, deviceNo);
        if (!offered) {
            if (!Cools.isEmpty(dedupKey) && redisUtil != null) {
                redisUtil.del(dedupKey);
            }
            return reject("queue-offer-failed", queueDepth, normalizedSource, normalizedScene, command);
        }
        News.info("输送站点命令入队成功。source={},scene={},deviceNo={},taskNo={},stationId={},targetStaNo={},commandType={},queueDepth={}",
                normalizedSource,
                normalizedScene,
                deviceNo,
                command.getTaskNo(),
                command.getStationId(),
                command.getTargetStaNo(),
                command.getCommandType(),
                queueDepth);
        return StationCommandDispatchResult.accepted("accepted", queueDepth, normalizedSource, normalizedScene);
    }
    private StationCommandDispatchResult reject(String reason,
                                                int queueDepth,
                                                String source,
                                                String scene,
                                                StationCommand command) {
        News.warn("输送站点命令入队失败。reason={},source={},scene={},deviceNo?=N/A,taskNo={},stationId={},targetStaNo={},commandType={},queueDepth={}",
                reason,
                source,
                scene,
                command == null ? null : command.getTaskNo(),
                command == null ? null : command.getStationId(),
                command == null ? null : command.getTargetStaNo(),
                command == null ? null : command.getCommandType(),
                queueDepth);
        return StationCommandDispatchResult.rejected(reason, queueDepth, source, scene);
    }
    private String buildDedupKey(Integer deviceNo, StationCommand command) {
        if (deviceNo == null || command == null) {
            return "";
        }
        return RedisKeyType.STATION_COMMAND_DISPATCH_DEDUP_.key
                + deviceNo + "_"
                + command.getTaskNo() + "_"
                + command.getStationId() + "_"
                + buildCommandSignatureHash(command);
    }
    private String buildCommandSignatureHash(StationCommand command) {
        if (command == null) {
            return "";
        }
        if (stationMoveCoordinator != null && command.getCommandType() != null
                && command.getCommandType().name().startsWith("MOVE")) {
            String pathSignatureHash = stationMoveCoordinator.buildPathSignatureHash(command);
            if (!Cools.isEmpty(pathSignatureHash)) {
                return pathSignatureHash;
            }
        }
        return Integer.toHexString(JSON.toJSONString(command).hashCode());
    }
}
src/main/java/com/zy/core/plugin/FakeProcess.java
@@ -16,6 +16,7 @@
import com.zy.core.News;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.*;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.Task;
@@ -93,6 +94,8 @@
    private DualCrnOperateProcessUtils dualCrnOperateProcessUtils;
    @Autowired
    private StoreInTaskGenerationService storeInTaskGenerationService;
    @Autowired
    private StationCommandDispatcher stationCommandDispatcher;
    /**
     * 带超时保护执行方法
@@ -254,7 +257,7 @@
                    StationCommand command = stationThread.getCommand(StationCommandType.MOVE,
                            commonService.getWorkNo(WrkIoType.FAKE_TASK_NO.id), stationId,
                            entity.getBarcodeStation().getStationId(), 0);
                    MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command));
                    stationCommandDispatcher.dispatch(basDevp.getDevpNo(), command, "fake-process", "fake-enable-in");
                    redisUtil.set(RedisKeyType.GENERATE_FAKE_IN_STATION_DATA_LIMIT.key + stationId, "lock", 5);
                }
            }
@@ -349,7 +352,7 @@
                        News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
                        continue;
                    }
                    MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command));
                    stationCommandDispatcher.dispatch(basDevp.getDevpNo(), command, "fake-process", "fake-in-task");
                    redisUtil.set(RedisKeyType.GENERATE_FAKE_IN_TASK_LIMIT.key + stationId, "lock", 5);
                }
            }
@@ -459,7 +462,7 @@
            News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
            return;
        }
        MessageQueue.offer(SlaveType.Devp, context.getBasDevp().getDevpNo(), new Task(2, command));
        stationCommandDispatcher.dispatch(context.getBasDevp().getDevpNo(), command, "fake-process", "write-info");
    }
    // 计算所有站点停留时间
@@ -521,7 +524,7 @@
                        continue;
                    }
                    MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                    stationCommandDispatcher.dispatch(stationObjModel.getDeviceNo(), command, "fake-process", "out-station-reset");
                    redisUtil.set(
                            RedisKeyType.CHECK_OUT_STATION_STAY_TIME_OUT_LIMIT.key + stationObjModel.getStationId(),
                            "lock", 10);
@@ -584,7 +587,7 @@
                WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
                if (wrkMast == null) {
                    MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                    stationCommandDispatcher.dispatch(stationObjModel.getDeviceNo(), command, "fake-process", "in-station-reset-task-over");
                    redisUtil.set(
                            RedisKeyType.CHECK_IN_STATION_STAY_TIME_OUT_LIMIT.key + stationObjModel.getStationId(),
                            "lock", 10);
@@ -605,7 +608,7 @@
                                continue;
                            }
                            MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                            stationCommandDispatcher.dispatch(stationObjModel.getDeviceNo(), command, "fake-process", "in-station-reset-crn-fetch");
                            redisUtil.set(RedisKeyType.CHECK_IN_STATION_STAY_TIME_OUT_LIMIT.key
                                    + stationObjModel.getStationId(), "lock", 10);
                            News.info("输送站点重置命令下发成功(crn_fetch),站点号={},命令数据={}", stationObjModel.getStationId(),
@@ -632,7 +635,7 @@
                                continue;
                            }
                            MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                            stationCommandDispatcher.dispatch(stationObjModel.getDeviceNo(), command, "fake-process", "in-station-reset-dual-crn-fetch");
                            redisUtil.set(RedisKeyType.CHECK_IN_STATION_STAY_TIME_OUT_LIMIT.key
                                    + stationObjModel.getStationId(), "lock", 10);
                            News.info("输送站点重置命令下发成功(crn_fetch),站点号={},命令数据={}", stationObjModel.getStationId(),
@@ -699,7 +702,7 @@
                        // 生成仿真站点数据
                        StationCommand command = stationThread.getCommand(StationCommandType.WRITE_INFO, 9998,
                                wrkMast.getSourceStaNo(), 0, 0);
                        MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                        stationCommandDispatcher.dispatch(stationObjModel.getDeviceNo(), command, "fake-process", "crn-out-complete-write-info");
                        redisUtil.set(RedisKeyType.CRN_OUT_TASK_COMPLETE_STATION_INFO.key + wrkMast.getWrkNo(), JSON.toJSONString(stationObjModel, SerializerFeature.DisableCircularReferenceDetect), 60 * 60 * 24);
                    }
                } else if (wrkMast.getWrkSts() == WrkStsType.LOC_MOVE_RUN.sts) {
src/main/java/com/zy/core/plugin/GslProcess.java
@@ -8,14 +8,13 @@
import com.zy.common.service.CommonService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.enums.WrkIoType;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.Task;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.plugin.api.MainProcessPluginApi;
@@ -49,6 +48,8 @@
    private RedisUtil redisUtil;
    @Autowired
    private StoreInTaskGenerationService storeInTaskGenerationService;
    @Autowired
    private StationCommandDispatcher stationCommandDispatcher;
    @Override
    public void run() {
@@ -115,7 +116,7 @@
            News.taskInfo(stationProtocol.getTaskNo(), "{}工作,获取输送线命令失败", stationProtocol.getTaskNo());
            return false;
        }
        MessageQueue.offer(SlaveType.Devp, context.getBasDevp().getDevpNo(), new Task(2, command));
        stationCommandDispatcher.dispatch(context.getBasDevp().getDevpNo(), command, "gsl-process", "station-back");
        News.taskInfo(stationProtocol.getTaskNo(), "{}扫码异常,已退回至{}", backStation.getStationId());
        redisUtil.set(RedisKeyType.GENERATE_STATION_BACK_LIMIT.key + stationProtocol.getStationId(), "lock", 10);
        return true;
@@ -163,7 +164,7 @@
                        && stationProtocol.isEnableIn()
                ) {
                    StationCommand command = stationThread.getCommand(StationCommandType.MOVE, commonService.getWorkNo(WrkIoType.ENABLE_IN.id), stationId, entity.getBarcodeStation().getStationId(), 0);
                    MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command));
                    stationCommandDispatcher.dispatch(basDevp.getDevpNo(), command, "gsl-process", "enable-in");
                    redisUtil.set(RedisKeyType.GENERATE_ENABLE_IN_STATION_DATA_LIMIT.key + stationId, "lock", 15);
                    News.info("{}站点启动入库成功,数据包:{}", stationId, JSON.toJSONString(command));
                }
src/main/java/com/zy/core/plugin/NormalProcess.java
@@ -13,14 +13,13 @@
import com.zy.common.service.CommonService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.enums.WrkIoType;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.Task;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.plugin.api.MainProcessPluginApi;
@@ -59,6 +58,8 @@
    private WmsOperateUtils wmsOperateUtils;
    @Autowired
    private StoreInTaskGenerationService storeInTaskGenerationService;
    @Autowired
    private StationCommandDispatcher stationCommandDispatcher;
    @Override
    public void run() {
@@ -135,7 +136,7 @@
                        && stationProtocol.isEnableIn()
                ) {
                    StationCommand command = stationThread.getCommand(StationCommandType.MOVE, commonService.getWorkNo(WrkIoType.ENABLE_IN.id), stationId, entity.getBarcodeStation().getStationId(), 0);
                    MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command));
                    stationCommandDispatcher.dispatch(basDevp.getDevpNo(), command, "normal-process", "enable-in");
                    redisUtil.set(RedisKeyType.GENERATE_ENABLE_IN_STATION_DATA_LIMIT.key + stationId, "lock", 15);
                    News.info("{}站点启动入库成功,数据包:{}", stationId, JSON.toJSONString(command));
                }
src/main/java/com/zy/core/plugin/XiaosongProcess.java
@@ -13,14 +13,13 @@
import com.zy.common.service.CommonService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.enums.WrkIoType;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.Task;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.plugin.api.MainProcessPluginApi;
@@ -62,6 +61,8 @@
    private DualCrnOperateProcessUtils dualCrnOperateProcessUtils;
    @Autowired
    private StoreInTaskGenerationService storeInTaskGenerationService;
    @Autowired
    private StationCommandDispatcher stationCommandDispatcher;
    @Override
    public void run() {
@@ -130,7 +131,7 @@
            News.taskInfo(stationProtocol.getTaskNo(), "{}工作,获取输送线命令失败", stationProtocol.getTaskNo());
            return false;
        }
        MessageQueue.offer(SlaveType.Devp, context.getBasDevp().getDevpNo(), new Task(2, command));
        stationCommandDispatcher.dispatch(context.getBasDevp().getDevpNo(), command, "xiaosong-process", "station-back");
        News.taskInfo(stationProtocol.getTaskNo(), "{}扫码异常,已退回至{}", backStation.getStationId());
        redisUtil.set(RedisKeyType.GENERATE_STATION_BACK_LIMIT.key + stationProtocol.getTaskNo(), "lock", 10);
        return true;
@@ -170,7 +171,7 @@
                        && stationProtocol.isEnableIn()
                ) {
                    StationCommand command = stationThread.getCommand(StationCommandType.MOVE, commonService.getWorkNo(WrkIoType.ENABLE_IN.id), stationId, entity.getBarcodeStation().getStationId(), 0);
                    MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command));
                    stationCommandDispatcher.dispatch(basDevp.getDevpNo(), command, "xiaosong-process", "enable-in");
                    redisUtil.set(RedisKeyType.GENERATE_ENABLE_IN_STATION_DATA_LIMIT.key + stationId, "lock", 15);
                    News.info("{}站点启动入库成功,数据包:{}", stationId, JSON.toJSONString(command));
                }
src/main/java/com/zy/core/thread/impl/ZyStationV5Thread.java
@@ -2,26 +2,14 @@
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.Cools;
import com.core.common.DateUtils;
import com.core.common.SpringUtils;
import com.zy.asrs.domain.vo.StationCycleCapacityVo;
import com.zy.asrs.domain.vo.StationCycleLoopVo;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.BasStationOpt;
import com.zy.asrs.entity.DeviceConfig;
import com.zy.asrs.entity.DeviceDataLog;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.BasStationOptService;
import com.zy.asrs.service.StationCycleCapacityService;
import com.zy.asrs.utils.Utils;
import com.zy.common.model.NavigateNode;
import com.zy.common.utils.NavigateUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.OutputQueue;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.model.CommandResponse;
@@ -33,26 +21,19 @@
import com.zy.core.network.ZyStationConnectDriver;
import com.zy.core.network.entity.ZyStationStatusEntity;
import com.zy.core.service.StationTaskLoopService;
import com.zy.core.thread.impl.v5.StationV5RunBlockReroutePlanner;
import com.zy.core.thread.impl.v5.StationV5SegmentExecutor;
import com.zy.core.thread.impl.v5.StationV5StatusReader;
import com.zy.core.thread.support.RecentStationArrivalTracker;
import com.zy.core.trace.StationTaskTraceRegistry;
import com.zy.core.utils.DeviceLogRedisKeyBuilder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Date;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -60,42 +41,35 @@
@Slf4j
public class ZyStationV5Thread implements Runnable, com.zy.core.thread.StationThread {
    private static final int RUN_BLOCK_REROUTE_STATE_EXPIRE_SECONDS = 60 * 60 * 24;
    private static final int SHORT_PATH_REPEAT_AVOID_THRESHOLD = 2;
    private static final int LOOP_REPEAT_TRIGGER_COUNT = 3;
    private static final int LOCAL_LOOP_NEIGHBOR_HOP = 3;
    private static final int SEGMENT_EXECUTOR_POOL_SIZE = 64;
    private List<StationProtocol> statusList = new ArrayList<>();
    private DeviceConfig deviceConfig;
    private RedisUtil redisUtil;
    private ZyStationConnectDriver zyStationConnectDriver;
    private int deviceLogCollectTime = 200;
    private boolean initStatus = false;
    private long deviceDataLogTime = System.currentTimeMillis();
    private ExecutorService executor = Executors.newFixedThreadPool(9999);
    private final ExecutorService executor = Executors.newFixedThreadPool(SEGMENT_EXECUTOR_POOL_SIZE);
    private StationV5SegmentExecutor segmentExecutor;
    private final RecentStationArrivalTracker recentArrivalTracker;
    private final StationV5StatusReader statusReader;
    private final StationV5RunBlockReroutePlanner runBlockReroutePlanner;
    public ZyStationV5Thread(DeviceConfig deviceConfig, RedisUtil redisUtil) {
        this.deviceConfig = deviceConfig;
        this.redisUtil = redisUtil;
        this.recentArrivalTracker = new RecentStationArrivalTracker(redisUtil);
        this.segmentExecutor = new StationV5SegmentExecutor(deviceConfig, redisUtil, this::sendCommand);
        this.statusReader = new StationV5StatusReader(deviceConfig, redisUtil, recentArrivalTracker);
        this.runBlockReroutePlanner = new StationV5RunBlockReroutePlanner(redisUtil);
    }
    @Override
    @SuppressWarnings("InfiniteLoopStatement")
    public void run() {
        this.connect();
        deviceLogCollectTime = Utils.getDeviceLogCollectTime();
        Thread readThread = new Thread(() -> {
            while (true) {
                try {
                    if (initStatus) {
                        deviceLogCollectTime = Utils.getDeviceLogCollectTime();
                    }
                    readStatus();
                    statusReader.readStatus(zyStationConnectDriver);
                    Thread.sleep(100);
                } catch (Exception e) {
                    log.error("StationV5Thread Fail", e);
@@ -107,15 +81,7 @@
        Thread processThread = new Thread(() -> {
            while (true) {
                try {
                    int step = 1;
                    Task task = MessageQueue.poll(SlaveType.Devp, deviceConfig.getDeviceNo());
                    if (task != null) {
                        step = task.getStep();
                    }
                    if (step == 2) {
                        StationCommand cmd = (StationCommand) task.getData();
                        executor.submit(() -> segmentExecutor.execute(cmd));
                    }
                    pollAndDispatchQueuedCommand();
                    Thread.sleep(100);
                } catch (Exception e) {
                    log.error("StationV5Process Fail", e);
@@ -123,84 +89,6 @@
            }
        }, "DevpProcess-" + deviceConfig.getDeviceNo());
        processThread.start();
    }
    private void readStatus() {
        if (zyStationConnectDriver == null) {
            return;
        }
        if (statusList.isEmpty()) {
            BasDevpService basDevpService = null;
            try {
                basDevpService = SpringUtils.getBean(BasDevpService.class);
            } catch (Exception ignore) {
            }
            if (basDevpService == null) {
                return;
            }
            BasDevp basDevp = basDevpService
                    .getOne(new QueryWrapper<BasDevp>().eq("devp_no", deviceConfig.getDeviceNo()));
            if (basDevp == null) {
                return;
            }
            List<ZyStationStatusEntity> list = JSONObject.parseArray(basDevp.getStationList(), ZyStationStatusEntity.class);
            for (ZyStationStatusEntity entity : list) {
                StationProtocol stationProtocol = new StationProtocol();
                stationProtocol.setStationId(entity.getStationId());
                statusList.add(stationProtocol);
            }
            initStatus = true;
        }
        List<ZyStationStatusEntity> zyStationStatusEntities = zyStationConnectDriver.getStatus();
        for (ZyStationStatusEntity statusEntity : zyStationStatusEntities) {
            for (StationProtocol stationProtocol : statusList) {
                if (stationProtocol.getStationId().equals(statusEntity.getStationId())) {
                    stationProtocol.setTaskNo(statusEntity.getTaskNo());
                    stationProtocol.setTargetStaNo(statusEntity.getTargetStaNo());
                    stationProtocol.setAutoing(statusEntity.isAutoing());
                    stationProtocol.setLoading(statusEntity.isLoading());
                    stationProtocol.setInEnable(statusEntity.isInEnable());
                    stationProtocol.setOutEnable(statusEntity.isOutEnable());
                    stationProtocol.setEmptyMk(statusEntity.isEmptyMk());
                    stationProtocol.setFullPlt(statusEntity.isFullPlt());
                    stationProtocol.setPalletHeight(statusEntity.getPalletHeight());
                    stationProtocol.setError(statusEntity.getError());
                    stationProtocol.setErrorMsg(statusEntity.getErrorMsg());
                    stationProtocol.setBarcode(statusEntity.getBarcode());
                    stationProtocol.setRunBlock(statusEntity.isRunBlock());
                    stationProtocol.setEnableIn(statusEntity.isEnableIn());
                    stationProtocol.setWeight(statusEntity.getWeight());
                    stationProtocol.setTaskWriteIdx(statusEntity.getTaskWriteIdx());
                    stationProtocol.setTaskBufferItems(statusEntity.getTaskBufferItems());
                    recentArrivalTracker.observe(statusEntity.getStationId(), statusEntity.getTaskNo(), statusEntity.isLoading());
                }
                if (!Cools.isEmpty(stationProtocol.getSystemWarning())) {
                    if (stationProtocol.isAutoing() && !stationProtocol.isLoading()) {
                        stationProtocol.setSystemWarning("");
                    }
                }
            }
        }
        OutputQueue.DEVP.offer(MessageFormat.format("【{0}】[id:{1}] <<<<< 实时数据更新成功",
                DateUtils.convert(new Date()), deviceConfig.getDeviceNo()));
        if (System.currentTimeMillis() - deviceDataLogTime > deviceLogCollectTime) {
            DeviceDataLog deviceDataLog = new DeviceDataLog();
            deviceDataLog.setOriginData(JSON.toJSONString(zyStationStatusEntities));
            deviceDataLog.setWcsData(JSON.toJSONString(statusList));
            deviceDataLog.setType(String.valueOf(SlaveType.Devp));
            deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo());
            deviceDataLog.setCreateTime(new Date());
            redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24);
            deviceDataLogTime = System.currentTimeMillis();
        }
    }
    @Override
@@ -226,16 +114,31 @@
    @Override
    public List<StationProtocol> getStatus() {
        return statusList;
        return statusReader.getStatusList();
    }
    @Override
    public Map<Integer, StationProtocol> getStatusMap() {
        Map<Integer, StationProtocol> map = new HashMap<>();
        for (StationProtocol stationProtocol : statusList) {
        for (StationProtocol stationProtocol : statusReader.getStatusList()) {
            map.put(stationProtocol.getStationId(), stationProtocol);
        }
        return map;
    }
    private void pollAndDispatchQueuedCommand() {
        Task task = MessageQueue.poll(SlaveType.Devp, deviceConfig.getDeviceNo());
        if (task == null || task.getStep() == null || task.getStep() != 2) {
            return;
        }
        submitSegmentCommand((StationCommand) task.getData());
    }
    private void submitSegmentCommand(StationCommand command) {
        if (command == null || executor == null || segmentExecutor == null) {
            return;
        }
        executor.submit(() -> segmentExecutor.execute(command));
    }
    @Override
@@ -294,7 +197,6 @@
            return getCommand(StationCommandType.MOVE, taskNo, stationId, targetStationId, palletSize, pathLenFactor);
        }
        RunBlockRerouteState rerouteState = loadRunBlockRerouteState(taskNo, stationId);
        StationTaskLoopService taskLoopService = loadStationTaskLoopService();
        StationTaskLoopService.LoopEvaluation loopEvaluation = taskLoopService == null
                ? new StationTaskLoopService.LoopEvaluation(taskNo, stationId, StationTaskLoopService.LoopIdentitySnapshot.empty(), 0, 0, false)
@@ -305,61 +207,44 @@
                loopEvaluation.getLoopIdentity().getScopeType(),
                loopEvaluation.getLoopIdentity().getLocalStationCount(),
                loopEvaluation.getLoopIdentity().getSourceLoopStationCount());
        rerouteState.setTaskNo(taskNo);
        rerouteState.setBlockStationId(stationId);
        rerouteState.setLastTargetStationId(targetStationId);
        rerouteState.setPlanCount((rerouteState.getPlanCount() == null ? 0 : rerouteState.getPlanCount()) + 1);
        rerouteState.setLastPlanTime(System.currentTimeMillis());
        List<List<NavigateNode>> candidatePathList = calcCandidatePathNavigateNodes(taskNo, stationId, targetStationId, pathLenFactor);
        if (candidatePathList.isEmpty()) {
            saveRunBlockRerouteState(rerouteState);
        List<StationCommand> candidateCommandList = new ArrayList<>();
        for (List<NavigateNode> candidatePath : candidatePathList) {
            StationCommand rerouteCommand = buildMoveCommand(taskNo, stationId, targetStationId, palletSize, candidatePath);
            if (rerouteCommand == null || rerouteCommand.getNavigatePath() == null || rerouteCommand.getNavigatePath().isEmpty()) {
                continue;
            }
            candidateCommandList.add(rerouteCommand);
        }
        StationV5RunBlockReroutePlanner.PlanResult planResult = runBlockReroutePlanner.plan(
                taskNo,
                stationId,
                loopEvaluation,
                candidateCommandList
        );
        if (candidateCommandList.isEmpty()) {
            log.warn("输送线堵塞重规划失败,候选路径为空,taskNo={}, planCount={}, stationId={}, targetStationId={}",
                    taskNo, rerouteState.getPlanCount(), stationId, targetStationId);
                    taskNo, planResult.getPlanCount(), stationId, targetStationId);
            return null;
        }
        StationCommand rerouteCommand = selectAvailableRerouteCommand(
                rerouteState,
                loopEvaluation,
                candidatePathList,
                taskNo,
                stationId,
                targetStationId,
                palletSize
        );
        if (rerouteCommand == null) {
            log.info("输送线堵塞重规划候选路线已全部试过,重置路线历史后重新开始,taskNo={}, planCount={}, stationId={}, targetStationId={}",
                    taskNo, rerouteState.getPlanCount(), stationId, targetStationId);
            rerouteState.resetIssuedRoutes();
            rerouteCommand = selectAvailableRerouteCommand(
                    rerouteState,
                    loopEvaluation,
                    candidatePathList,
                    taskNo,
                    stationId,
                    targetStationId,
                    palletSize
            );
        }
        StationCommand rerouteCommand = planResult.getCommand();
        if (rerouteCommand != null) {
            saveRunBlockRerouteState(rerouteState);
            if (taskLoopService != null) {
                taskLoopService.recordLoopIssue(loopEvaluation, "RUN_BLOCK_REROUTE");
            }
            log.info("输送线堵塞重规划选中候选路线,taskNo={}, planCount={}, stationId={}, targetStationId={}, route={}",
                    taskNo, rerouteState.getPlanCount(), stationId, targetStationId, JSON.toJSONString(rerouteCommand.getNavigatePath()));
                    taskNo, planResult.getPlanCount(), stationId, targetStationId, JSON.toJSONString(rerouteCommand.getNavigatePath()));
            return rerouteCommand;
        }
        saveRunBlockRerouteState(rerouteState);
        log.warn("输送线堵塞重规划未找到可下发路线,taskNo={}, planCount={}, stationId={}, targetStationId={}, triedRoutes={}",
                taskNo,
                rerouteState.getPlanCount(),
                planResult.getPlanCount(),
                stationId,
                targetStationId,
                JSON.toJSONString(rerouteState.getIssuedRoutePathList()));
                JSON.toJSONString(planResult.getIssuedRoutePathList()));
        return null;
    }
@@ -395,6 +280,8 @@
                            stationId, item.getSlotIdx(), item.getTaskNo());
                    continue;
                }else {
                    item.setTaskNo(0);
                    item.setTargetStaNo(0);
                    success = true;
                    log.warn("输送站缓存区残留路径清理成功。stationId={}, slotIdx={}, taskNo={}",
                            stationId, item.getSlotIdx(), item.getTaskNo());
@@ -529,769 +416,11 @@
        return stationCommand;
    }
    private StationCommand selectAvailableRerouteCommand(RunBlockRerouteState rerouteState,
                                                         StationTaskLoopService.LoopEvaluation loopEvaluation,
                                                         List<List<NavigateNode>> candidatePathList,
                                                         Integer taskNo,
                                                         Integer stationId,
                                                         Integer targetStationId,
                                                         Integer palletSize) {
        if (rerouteState == null || candidatePathList == null || candidatePathList.isEmpty()) {
            return null;
        }
        Set<String> issuedRouteSignatureSet = rerouteState.getIssuedRouteSignatureSet();
        List<RerouteCandidateCommand> candidateCommandList = new ArrayList<>();
        for (List<NavigateNode> candidatePath : candidatePathList) {
            StationCommand rerouteCommand = buildMoveCommand(taskNo, stationId, targetStationId, palletSize, candidatePath);
            if (rerouteCommand == null || rerouteCommand.getNavigatePath() == null || rerouteCommand.getNavigatePath().isEmpty()) {
                continue;
            }
            String routeSignature = buildPathSignature(rerouteCommand.getNavigatePath());
            if (Cools.isEmpty(routeSignature)) {
                continue;
            }
            RerouteCandidateCommand candidateCommand = new RerouteCandidateCommand();
            candidateCommand.setCommand(rerouteCommand);
            candidateCommand.setRouteSignature(routeSignature);
            candidateCommand.setPathLength(rerouteCommand.getNavigatePath().size());
            candidateCommand.setIssuedCount(rerouteState.getRouteIssueCountMap().getOrDefault(routeSignature, 0));
            candidateCommand.setLoopFingerprint(loopEvaluation.getLoopIdentity().getLoopFingerprint());
            candidateCommand.setLoopIssuedCount(loopEvaluation.getExpectedLoopIssueCount());
            candidateCommand.setLoopTriggered(loopEvaluation.isLargeLoopTriggered());
            candidateCommand.setCurrentLoopHitCount(countCurrentLoopStationHit(
                    rerouteCommand.getNavigatePath(),
                    loopEvaluation.getLoopIdentity().getStationIdSet()
            ));
            candidateCommandList.add(candidateCommand);
        }
        if (candidateCommandList.isEmpty()) {
            return null;
        }
        List<RerouteCandidateCommand> orderedCandidateCommandList = reorderCandidateCommandsForLoopRelease(candidateCommandList);
        for (RerouteCandidateCommand candidateCommand : orderedCandidateCommandList) {
            if (candidateCommand == null || candidateCommand.getCommand() == null) {
                continue;
            }
            if (issuedRouteSignatureSet.contains(candidateCommand.getRouteSignature())) {
                continue;
            }
            StationCommand rerouteCommand = candidateCommand.getCommand();
            issuedRouteSignatureSet.add(candidateCommand.getRouteSignature());
            rerouteState.getIssuedRoutePathList().add(new ArrayList<>(rerouteCommand.getNavigatePath()));
            rerouteState.setLastSelectedRoute(new ArrayList<>(rerouteCommand.getNavigatePath()));
            rerouteState.getRouteIssueCountMap().put(
                    candidateCommand.getRouteSignature(),
                    rerouteState.getRouteIssueCountMap().getOrDefault(candidateCommand.getRouteSignature(), 0) + 1
            );
            return rerouteCommand;
        }
        return null;
    }
    private List<RerouteCandidateCommand> reorderCandidateCommandsForLoopRelease(List<RerouteCandidateCommand> candidateCommandList) {
        if (candidateCommandList == null || candidateCommandList.isEmpty()) {
            return new ArrayList<>();
        }
        int shortestPathLength = Integer.MAX_VALUE;
        int shortestPathLoopHitCount = Integer.MAX_VALUE;
        boolean shortestPathOverused = false;
        boolean currentLoopOverused = false;
        boolean hasLongerCandidate = false;
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand == null || candidateCommand.getPathLength() == null || candidateCommand.getPathLength() <= 0) {
                continue;
            }
            shortestPathLength = Math.min(shortestPathLength, candidateCommand.getPathLength());
        }
        if (shortestPathLength == Integer.MAX_VALUE) {
            return candidateCommandList;
        }
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand == null || candidateCommand.getPathLength() == null || candidateCommand.getPathLength() <= 0) {
                continue;
            }
            if (candidateCommand.getPathLength() == shortestPathLength) {
                shortestPathLoopHitCount = Math.min(shortestPathLoopHitCount, safeInt(candidateCommand.getCurrentLoopHitCount()));
            }
            if (candidateCommand.getPathLength() > shortestPathLength) {
                hasLongerCandidate = true;
            }
            if (candidateCommand.getPathLength() == shortestPathLength
                    && candidateCommand.getIssuedCount() != null
                    && candidateCommand.getIssuedCount() >= SHORT_PATH_REPEAT_AVOID_THRESHOLD) {
                shortestPathOverused = true;
            }
            if (!Cools.isEmpty(candidateCommand.getLoopFingerprint())
                    && Boolean.TRUE.equals(candidateCommand.getLoopTriggered())) {
                currentLoopOverused = true;
            }
        }
        if (!shortestPathOverused && !currentLoopOverused) {
            return candidateCommandList;
        }
        if (shortestPathLoopHitCount == Integer.MAX_VALUE) {
            shortestPathLoopHitCount = 0;
        }
        boolean hasLoopExitCandidate = false;
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand == null) {
                continue;
            }
            if (safeInt(candidateCommand.getCurrentLoopHitCount()) < shortestPathLoopHitCount) {
                hasLoopExitCandidate = true;
                break;
            }
        }
        if (!hasLongerCandidate && !hasLoopExitCandidate) {
            return candidateCommandList;
        }
        List<RerouteCandidateCommand> reorderedList = new ArrayList<>();
        if (currentLoopOverused && hasLoopExitCandidate) {
            for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
                if (candidateCommand == null) {
                    continue;
                }
                if (safeInt(candidateCommand.getCurrentLoopHitCount()) < shortestPathLoopHitCount) {
                    appendCandidateIfAbsent(reorderedList, candidateCommand);
                }
            }
        }
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand != null
                    && candidateCommand.getPathLength() != null
                    && candidateCommand.getPathLength() > shortestPathLength) {
                appendCandidateIfAbsent(reorderedList, candidateCommand);
            }
        }
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand == null || candidateCommand.getPathLength() == null) {
                continue;
            }
            appendCandidateIfAbsent(reorderedList, candidateCommand);
        }
        return reorderedList;
    }
    private void appendCandidateIfAbsent(List<RerouteCandidateCommand> reorderedList,
                                         RerouteCandidateCommand candidateCommand) {
        if (reorderedList == null || candidateCommand == null) {
            return;
        }
        if (!reorderedList.contains(candidateCommand)) {
            reorderedList.add(candidateCommand);
        }
    }
    private RunBlockRerouteState loadRunBlockRerouteState(Integer taskNo, Integer blockStationId) {
        if (redisUtil == null || taskNo == null || taskNo <= 0 || blockStationId == null || blockStationId <= 0) {
            return new RunBlockRerouteState();
        }
        Object stateObj = redisUtil.get(buildRunBlockRerouteStateKey(taskNo, blockStationId));
        if (stateObj == null) {
            return new RunBlockRerouteState();
        }
        try {
            RunBlockRerouteState state = JSON.parseObject(String.valueOf(stateObj), RunBlockRerouteState.class);
            return state == null ? new RunBlockRerouteState() : state.normalize();
        } catch (Exception ignore) {
            return new RunBlockRerouteState();
        }
    }
    private void saveRunBlockRerouteState(RunBlockRerouteState rerouteState) {
        if (redisUtil == null
                || rerouteState == null
                || rerouteState.getTaskNo() == null
                || rerouteState.getTaskNo() <= 0
                || rerouteState.getBlockStationId() == null
                || rerouteState.getBlockStationId() <= 0) {
            return;
        }
        rerouteState.normalize();
        redisUtil.set(
                buildRunBlockRerouteStateKey(rerouteState.getTaskNo(), rerouteState.getBlockStationId()),
                JSON.toJSONString(rerouteState),
                RUN_BLOCK_REROUTE_STATE_EXPIRE_SECONDS
        );
    }
    private TaskLoopRerouteState loadTaskLoopRerouteState(Integer taskNo) {
        if (redisUtil == null || taskNo == null || taskNo <= 0) {
            return new TaskLoopRerouteState();
        }
        Object stateObj = redisUtil.get(RedisKeyType.STATION_RUN_BLOCK_TASK_LOOP_STATE_.key + taskNo);
        if (stateObj == null) {
            return new TaskLoopRerouteState();
        }
        try {
            TaskLoopRerouteState state = JSON.parseObject(String.valueOf(stateObj), TaskLoopRerouteState.class);
            return state == null ? new TaskLoopRerouteState() : state.normalize();
        } catch (Exception ignore) {
            return new TaskLoopRerouteState();
        }
    }
    private void saveTaskLoopRerouteState(TaskLoopRerouteState taskLoopRerouteState) {
        if (redisUtil == null
                || taskLoopRerouteState == null
                || taskLoopRerouteState.getTaskNo() == null
                || taskLoopRerouteState.getTaskNo() <= 0) {
            return;
        }
        taskLoopRerouteState.normalize();
        redisUtil.set(
                RedisKeyType.STATION_RUN_BLOCK_TASK_LOOP_STATE_.key + taskLoopRerouteState.getTaskNo(),
                JSON.toJSONString(taskLoopRerouteState),
                RUN_BLOCK_REROUTE_STATE_EXPIRE_SECONDS
        );
    }
    private void touchTaskLoopRerouteState(TaskLoopRerouteState taskLoopRerouteState,
                                           LoopIdentity currentLoopIdentity) {
        if (taskLoopRerouteState == null || currentLoopIdentity == null || Cools.isEmpty(currentLoopIdentity.getLoopFingerprint())) {
            return;
        }
        taskLoopRerouteState.getLoopIssueCountMap().put(
                currentLoopIdentity.getLoopFingerprint(),
                taskLoopRerouteState.getLoopIssueCountMap().getOrDefault(currentLoopIdentity.getLoopFingerprint(), 0) + 1
        );
        taskLoopRerouteState.setLastLoopFingerprint(currentLoopIdentity.getLoopFingerprint());
        taskLoopRerouteState.setLastIssueTime(System.currentTimeMillis());
    }
    private int resolveCurrentLoopIssuedCount(TaskLoopRerouteState taskLoopRerouteState,
                                              LoopIdentity currentLoopIdentity) {
        if (taskLoopRerouteState == null || currentLoopIdentity == null || Cools.isEmpty(currentLoopIdentity.getLoopFingerprint())) {
            return 0;
        }
        return taskLoopRerouteState.getLoopIssueCountMap().getOrDefault(currentLoopIdentity.getLoopFingerprint(), 0);
    }
    private int resolveExpectedLoopIssuedCount(TaskLoopRerouteState taskLoopRerouteState,
                                               LoopIdentity currentLoopIdentity) {
        if (currentLoopIdentity == null || Cools.isEmpty(currentLoopIdentity.getLoopFingerprint())) {
            return 0;
        }
        return resolveCurrentLoopIssuedCount(taskLoopRerouteState, currentLoopIdentity) + 1;
    }
    private boolean isLoopRepeatTriggered(Integer loopIssuedCount) {
        return loopIssuedCount != null && loopIssuedCount >= LOOP_REPEAT_TRIGGER_COUNT;
    }
    private void syncTaskTraceLoopAlert(Integer taskNo,
                                        Integer blockedStationId,
                                        LoopIdentity currentLoopIdentity,
                                        int loopIssuedCount) {
        StationTaskTraceRegistry traceRegistry;
        try {
            traceRegistry = SpringUtils.getBean(StationTaskTraceRegistry.class);
        } catch (Exception ignore) {
            return;
        }
        if (traceRegistry == null || taskNo == null || taskNo <= 0) {
            return;
        }
        boolean active = currentLoopIdentity != null
                && !Cools.isEmpty(currentLoopIdentity.getLoopFingerprint())
                && isLoopRepeatTriggered(loopIssuedCount);
        Map<String, Object> details = new HashMap<>();
        details.put("blockedStationId", blockedStationId);
        details.put("loopScopeType", currentLoopIdentity == null ? "" : currentLoopIdentity.getScopeType());
        details.put("loopStationCount", currentLoopIdentity == null ? 0 : currentLoopIdentity.getLocalStationCount());
        details.put("sourceLoopStationCount", currentLoopIdentity == null ? 0 : currentLoopIdentity.getSourceLoopStationCount());
        details.put("loopRepeatCount", loopIssuedCount);
        String loopAlertType = resolveLoopAlertType(currentLoopIdentity);
        String loopAlertText = buildLoopAlertText(loopAlertType, currentLoopIdentity, loopIssuedCount);
        traceRegistry.updateLoopHint(taskNo, active, loopAlertType, loopAlertText, loopIssuedCount, details);
    }
    private String resolveLoopAlertType(LoopIdentity currentLoopIdentity) {
        if (currentLoopIdentity == null || Cools.isEmpty(currentLoopIdentity.getLoopFingerprint())) {
            return "";
        }
        return "wholeLoop".equals(currentLoopIdentity.getScopeType()) ? "LARGE_LOOP" : "SMALL_LOOP";
    }
    private String buildLoopAlertText(String loopAlertType,
                                      LoopIdentity currentLoopIdentity,
                                      int loopIssuedCount) {
        if (Cools.isEmpty(loopAlertType) || currentLoopIdentity == null || !isLoopRepeatTriggered(loopIssuedCount)) {
            return "";
        }
        String typeLabel = "LARGE_LOOP".equals(loopAlertType) ? "大环线" : "小环线";
        return typeLabel + "绕圈预警,累计重规划" + loopIssuedCount + "次,当前识别范围"
                + currentLoopIdentity.getLocalStationCount() + "站";
    }
    private int countCurrentLoopStationHit(List<Integer> path, Set<Integer> currentLoopStationIdSet) {
        if (path == null || path.isEmpty() || currentLoopStationIdSet == null || currentLoopStationIdSet.isEmpty()) {
            return 0;
        }
        int hitCount = 0;
        for (Integer stationId : path) {
            if (stationId != null && currentLoopStationIdSet.contains(stationId)) {
                hitCount++;
            }
        }
        return hitCount;
    }
    private String buildPathSignature(List<Integer> path) {
        if (path == null || path.isEmpty()) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        for (Integer stationNo : path) {
            if (stationNo == null) {
                continue;
            }
            if (builder.length() > 0) {
                builder.append("->");
            }
            builder.append(stationNo);
        }
        return builder.toString();
    }
    private StationTaskLoopService loadStationTaskLoopService() {
        try {
            return SpringUtils.getBean(StationTaskLoopService.class);
        } catch (Exception ignore) {
            return null;
        }
    }
    private String buildRunBlockRerouteStateKey(Integer taskNo, Integer blockStationId) {
        return RedisKeyType.STATION_RUN_BLOCK_REROUTE_STATE_.key + taskNo + "_" + blockStationId;
    }
    private LoopIdentity resolveStationLoopIdentity(Integer stationId) {
        if (stationId == null || stationId <= 0) {
            return LoopIdentity.empty();
        }
        try {
            StationCycleCapacityService stationCycleCapacityService = SpringUtils.getBean(StationCycleCapacityService.class);
            if (stationCycleCapacityService == null) {
                return LoopIdentity.empty();
            }
            StationCycleCapacityVo capacityVo = stationCycleCapacityService.getLatestSnapshot();
            if (capacityVo == null || capacityVo.getLoopList() == null || capacityVo.getLoopList().isEmpty()) {
                return LoopIdentity.empty();
            }
            for (StationCycleLoopVo loopVo : capacityVo.getLoopList()) {
                List<Integer> loopStationIdList = normalizeLoopStationIdList(loopVo == null ? null : loopVo.getStationIdList());
                if (loopStationIdList.isEmpty() || !loopStationIdList.contains(stationId)) {
                    continue;
                }
                Set<Integer> loopStationIdSet = new HashSet<>(loopStationIdList);
                Map<Integer, Set<Integer>> stationGraph = loadUndirectedStationGraph();
                List<Integer> localCycleStationIdList = resolveLocalCycleStationIdList(stationId, loopStationIdSet, stationGraph);
                if (localCycleStationIdList.size() >= 3) {
                    return buildLoopIdentity(localCycleStationIdList, loopStationIdList.size(), "localCycle");
                }
                List<Integer> localNeighborhoodStationIdList = resolveLocalNeighborhoodStationIdList(stationId, loopStationIdSet, stationGraph);
                if (localNeighborhoodStationIdList.size() >= 3 && localNeighborhoodStationIdList.size() < loopStationIdList.size()) {
                    return buildLoopIdentity(localNeighborhoodStationIdList, loopStationIdList.size(), "localNeighborhood");
                }
                return buildLoopIdentity(loopStationIdList, loopStationIdList.size(), "wholeLoop");
            }
        } catch (Exception ignore) {
        }
        return LoopIdentity.empty();
    }
    private LoopIdentity buildLoopIdentity(List<Integer> stationIdList,
                                           int sourceLoopStationCount,
                                           String scopeType) {
        List<Integer> normalizedStationIdList = normalizeLoopStationIdList(stationIdList);
        if (normalizedStationIdList.isEmpty()) {
            return LoopIdentity.empty();
        }
        return new LoopIdentity(
                buildLoopFingerprint(normalizedStationIdList),
                new HashSet<>(normalizedStationIdList),
                sourceLoopStationCount,
                normalizedStationIdList.size(),
                scopeType
        );
    }
    private List<Integer> resolveLocalCycleStationIdList(Integer stationId,
                                                         Set<Integer> loopStationIdSet,
                                                         Map<Integer, Set<Integer>> stationGraph) {
        if (stationId == null
                || stationId <= 0
                || loopStationIdSet == null
                || loopStationIdSet.isEmpty()
                || stationGraph == null
                || stationGraph.isEmpty()) {
            return new ArrayList<>();
        }
        Set<Integer> localNeighborhoodStationIdSet = collectLoopNeighborhoodStationIdSet(
                stationId,
                loopStationIdSet,
                stationGraph,
                LOCAL_LOOP_NEIGHBOR_HOP
        );
        if (localNeighborhoodStationIdSet.size() < 3) {
            return new ArrayList<>();
        }
        Set<Integer> directNeighborStationIdSet = filterLoopNeighborStationIdSet(
                stationGraph.getOrDefault(stationId, Collections.emptySet()),
                localNeighborhoodStationIdSet,
                stationId
        );
        if (directNeighborStationIdSet.size() < 2) {
            return new ArrayList<>();
        }
        List<Integer> bestCycleStationIdList = new ArrayList<>();
        List<Integer> neighborStationIdList = new ArrayList<>(directNeighborStationIdSet);
        for (int i = 0; i < neighborStationIdList.size(); i++) {
            Integer leftNeighborStationId = neighborStationIdList.get(i);
            if (leftNeighborStationId == null) {
                continue;
            }
            for (int j = i + 1; j < neighborStationIdList.size(); j++) {
                Integer rightNeighborStationId = neighborStationIdList.get(j);
                if (rightNeighborStationId == null) {
                    continue;
                }
                List<Integer> pathBetweenNeighbors = findShortestScopePath(
                        leftNeighborStationId,
                        rightNeighborStationId,
                        stationId,
                        localNeighborhoodStationIdSet,
                        stationGraph
                );
                if (pathBetweenNeighbors.isEmpty()) {
                    continue;
                }
                List<Integer> cycleStationIdList = new ArrayList<>();
                cycleStationIdList.add(stationId);
                cycleStationIdList.addAll(pathBetweenNeighbors);
                cycleStationIdList = normalizeLoopStationIdList(cycleStationIdList);
                if (cycleStationIdList.size() < 3) {
                    continue;
                }
                if (bestCycleStationIdList.isEmpty() || cycleStationIdList.size() < bestCycleStationIdList.size()) {
                    bestCycleStationIdList = cycleStationIdList;
                }
            }
        }
        return bestCycleStationIdList;
    }
    private List<Integer> resolveLocalNeighborhoodStationIdList(Integer stationId,
                                                                Set<Integer> loopStationIdSet,
                                                                Map<Integer, Set<Integer>> stationGraph) {
        return normalizeLoopStationIdList(new ArrayList<>(collectLoopNeighborhoodStationIdSet(
                stationId,
                loopStationIdSet,
                stationGraph,
                LOCAL_LOOP_NEIGHBOR_HOP
        )));
    }
    private Set<Integer> collectLoopNeighborhoodStationIdSet(Integer stationId,
                                                             Set<Integer> loopStationIdSet,
                                                             Map<Integer, Set<Integer>> stationGraph,
                                                             int maxHop) {
        Set<Integer> neighborhoodStationIdSet = new LinkedHashSet<>();
        if (stationId == null
                || stationId <= 0
                || loopStationIdSet == null
                || loopStationIdSet.isEmpty()
                || !loopStationIdSet.contains(stationId)
                || stationGraph == null
                || stationGraph.isEmpty()
                || maxHop < 0) {
            return neighborhoodStationIdSet;
        }
        Deque<StationHopNode> queue = new ArrayDeque<>();
        queue.offer(new StationHopNode(stationId, 0));
        neighborhoodStationIdSet.add(stationId);
        while (!queue.isEmpty()) {
            StationHopNode current = queue.poll();
            if (current == null || current.getHop() >= maxHop) {
                continue;
            }
            Set<Integer> neighborStationIdSet = filterLoopNeighborStationIdSet(
                    stationGraph.getOrDefault(current.getStationId(), Collections.emptySet()),
                    loopStationIdSet,
                    null
            );
            for (Integer neighborStationId : neighborStationIdSet) {
                if (neighborStationId == null || !neighborhoodStationIdSet.add(neighborStationId)) {
                    continue;
                }
                queue.offer(new StationHopNode(neighborStationId, current.getHop() + 1));
            }
        }
        return neighborhoodStationIdSet;
    }
    private Set<Integer> filterLoopNeighborStationIdSet(Set<Integer> candidateNeighborStationIdSet,
                                                        Set<Integer> allowedStationIdSet,
                                                        Integer excludedStationId) {
        Set<Integer> result = new LinkedHashSet<>();
        if (candidateNeighborStationIdSet == null || candidateNeighborStationIdSet.isEmpty()
                || allowedStationIdSet == null || allowedStationIdSet.isEmpty()) {
            return result;
        }
        for (Integer stationId : candidateNeighborStationIdSet) {
            if (stationId == null
                    || !allowedStationIdSet.contains(stationId)
                    || (excludedStationId != null && excludedStationId.equals(stationId))) {
                continue;
            }
            result.add(stationId);
        }
        return result;
    }
    private List<Integer> findShortestScopePath(Integer startStationId,
                                                Integer endStationId,
                                                Integer excludedStationId,
                                                Set<Integer> allowedStationIdSet,
                                                Map<Integer, Set<Integer>> stationGraph) {
        if (startStationId == null
                || endStationId == null
                || Objects.equals(startStationId, excludedStationId)
                || Objects.equals(endStationId, excludedStationId)
                || allowedStationIdSet == null
                || !allowedStationIdSet.contains(startStationId)
                || !allowedStationIdSet.contains(endStationId)
                || stationGraph == null
                || stationGraph.isEmpty()) {
            return new ArrayList<>();
        }
        Deque<Integer> queue = new ArrayDeque<>();
        Map<Integer, Integer> parentMap = new HashMap<>();
        Set<Integer> visitedStationIdSet = new HashSet<>();
        queue.offer(startStationId);
        visitedStationIdSet.add(startStationId);
        while (!queue.isEmpty()) {
            Integer currentStationId = queue.poll();
            if (currentStationId == null) {
                continue;
            }
            if (Objects.equals(currentStationId, endStationId)) {
                break;
            }
            Set<Integer> neighborStationIdSet = filterLoopNeighborStationIdSet(
                    stationGraph.getOrDefault(currentStationId, Collections.emptySet()),
                    allowedStationIdSet,
                    excludedStationId
            );
            for (Integer neighborStationId : neighborStationIdSet) {
                if (neighborStationId == null || !visitedStationIdSet.add(neighborStationId)) {
                    continue;
                }
                parentMap.put(neighborStationId, currentStationId);
                queue.offer(neighborStationId);
            }
        }
        if (!visitedStationIdSet.contains(endStationId)) {
            return new ArrayList<>();
        }
        List<Integer> pathStationIdList = new ArrayList<>();
        Integer cursorStationId = endStationId;
        while (cursorStationId != null) {
            pathStationIdList.add(cursorStationId);
            if (Objects.equals(cursorStationId, startStationId)) {
                break;
            }
            cursorStationId = parentMap.get(cursorStationId);
        }
        if (pathStationIdList.isEmpty()
                || !Objects.equals(pathStationIdList.get(pathStationIdList.size() - 1), startStationId)) {
            return new ArrayList<>();
        }
        Collections.reverse(pathStationIdList);
        return pathStationIdList;
    }
    private Map<Integer, Set<Integer>> loadUndirectedStationGraph() {
        NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class);
        if (navigateUtils == null) {
            return new HashMap<>();
        }
        Map<Integer, Set<Integer>> stationGraph = navigateUtils.loadUndirectedStationGraphSnapshot();
        return stationGraph == null ? new HashMap<>() : stationGraph;
    }
    private List<Integer> normalizeLoopStationIdList(List<Integer> stationIdList) {
        if (stationIdList == null || stationIdList.isEmpty()) {
            return new ArrayList<>();
        }
        List<Integer> normalizedList = new ArrayList<>();
        Set<Integer> seenStationIdSet = new HashSet<>();
        for (Integer stationId : stationIdList) {
            if (stationId == null || stationId <= 0 || !seenStationIdSet.add(stationId)) {
                continue;
            }
            normalizedList.add(stationId);
        }
        Collections.sort(normalizedList);
        return normalizedList;
    }
    private String buildLoopFingerprint(List<Integer> stationIdList) {
        if (stationIdList == null || stationIdList.isEmpty()) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        for (Integer stationId : stationIdList) {
            if (stationId == null) {
                continue;
            }
            if (builder.length() > 0) {
                builder.append("|");
            }
            builder.append(stationId);
        }
        return builder.toString();
    }
    private int safeInt(Integer value) {
        return value == null ? 0 : value;
    }
    @Data
    private static class RunBlockRerouteState {
        private Integer taskNo;
        private Integer blockStationId;
        private Integer planCount = 0;
        private Integer lastTargetStationId;
        private Long lastPlanTime;
        private List<List<Integer>> issuedRoutePathList = new ArrayList<>();
        private List<Integer> lastSelectedRoute = new ArrayList<>();
        private Set<String> issuedRouteSignatureSet = new LinkedHashSet<>();
        private Map<String, Integer> routeIssueCountMap = new HashMap<>();
        private RunBlockRerouteState normalize() {
            if (planCount == null || planCount < 0) {
                planCount = 0;
            }
            if (issuedRoutePathList == null) {
                issuedRoutePathList = new ArrayList<>();
            }
            if (lastSelectedRoute == null) {
                lastSelectedRoute = new ArrayList<>();
            }
            if (issuedRouteSignatureSet == null) {
                issuedRouteSignatureSet = new LinkedHashSet<>();
            }
            if (routeIssueCountMap == null) {
                routeIssueCountMap = new HashMap<>();
            }
            for (List<Integer> routePath : issuedRoutePathList) {
                if (routePath == null || routePath.isEmpty()) {
                    continue;
                }
                String pathSignature = buildPathSignatureText(routePath);
                if (!Cools.isEmpty(pathSignature)) {
                    issuedRouteSignatureSet.add(pathSignature);
                    routeIssueCountMap.putIfAbsent(pathSignature, 1);
                }
            }
            return this;
        }
        private void resetIssuedRoutes() {
            this.issuedRoutePathList = new ArrayList<>();
            this.lastSelectedRoute = new ArrayList<>();
            this.issuedRouteSignatureSet = new LinkedHashSet<>();
        }
        private static String buildPathSignatureText(List<Integer> routePath) {
            if (routePath == null || routePath.isEmpty()) {
                return "";
            }
            StringBuilder builder = new StringBuilder();
            for (Integer stationId : routePath) {
                if (stationId == null) {
                    continue;
                }
                if (builder.length() > 0) {
                    builder.append("->");
                }
                builder.append(stationId);
            }
            return builder.toString();
        }
    }
    @Data
    private static class RerouteCandidateCommand {
        private StationCommand command;
        private String routeSignature;
        private Integer pathLength;
        private Integer issuedCount;
        private String loopFingerprint;
        private Integer loopIssuedCount;
        private Boolean loopTriggered;
        private Integer currentLoopHitCount;
    }
    @Data
    private static class TaskLoopRerouteState {
        private Integer taskNo;
        private String lastLoopFingerprint;
        private Long lastIssueTime;
        private Map<String, Integer> loopIssueCountMap = new HashMap<>();
        private TaskLoopRerouteState normalize() {
            if (loopIssueCountMap == null) {
                loopIssueCountMap = new HashMap<>();
            }
            return this;
        }
    }
    @Data
    private static class LoopIdentity {
        private String loopFingerprint;
        private Set<Integer> stationIdSet = new HashSet<>();
        private int sourceLoopStationCount;
        private int localStationCount;
        private String scopeType;
        private LoopIdentity(String loopFingerprint,
                             Set<Integer> stationIdSet,
                             int sourceLoopStationCount,
                             int localStationCount,
                             String scopeType) {
            this.loopFingerprint = loopFingerprint;
            this.stationIdSet = stationIdSet == null ? new HashSet<>() : stationIdSet;
            this.sourceLoopStationCount = sourceLoopStationCount;
            this.localStationCount = localStationCount;
            this.scopeType = scopeType == null ? "" : scopeType;
        }
        private static LoopIdentity empty() {
            return new LoopIdentity("", new HashSet<>(), 0, 0, "none");
        }
    }
    @Data
    private static class StationHopNode {
        private final Integer stationId;
        private final int hop;
    }
}
src/main/java/com/zy/core/thread/impl/v5/StationV5RunBlockReroutePlanner.java
New file
@@ -0,0 +1,375 @@
package com.zy.core.thread.impl.v5;
import com.alibaba.fastjson.JSON;
import com.core.common.Cools;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.model.command.StationCommand;
import com.zy.core.service.StationTaskLoopService;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class StationV5RunBlockReroutePlanner {
    private static final int RUN_BLOCK_REROUTE_STATE_EXPIRE_SECONDS = 60 * 60 * 24;
    private static final int SHORT_PATH_REPEAT_AVOID_THRESHOLD = 2;
    private final RedisUtil redisUtil;
    public StationV5RunBlockReroutePlanner(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }
    public PlanResult plan(Integer taskNo,
                           Integer blockStationId,
                           StationTaskLoopService.LoopEvaluation loopEvaluation,
                           List<StationCommand> candidateCommands) {
        RunBlockRerouteState rerouteState = loadRunBlockRerouteState(taskNo, blockStationId);
        rerouteState.setTaskNo(taskNo);
        rerouteState.setBlockStationId(blockStationId);
        rerouteState.setPlanCount((rerouteState.getPlanCount() == null ? 0 : rerouteState.getPlanCount()) + 1);
        StationCommand rerouteCommand = selectAvailableRerouteCommand(rerouteState, loopEvaluation, candidateCommands);
        if (rerouteCommand == null && candidateCommands != null && !candidateCommands.isEmpty()) {
            rerouteState.resetIssuedRoutes();
            rerouteCommand = selectAvailableRerouteCommand(rerouteState, loopEvaluation, candidateCommands);
        }
        saveRunBlockRerouteState(rerouteState);
        return new PlanResult(
                rerouteCommand,
                rerouteState.getPlanCount() == null ? 0 : rerouteState.getPlanCount(),
                copyRoutePathList(rerouteState.getIssuedRoutePathList())
        );
    }
    private StationCommand selectAvailableRerouteCommand(RunBlockRerouteState rerouteState,
                                                         StationTaskLoopService.LoopEvaluation loopEvaluation,
                                                         List<StationCommand> candidateCommands) {
        if (rerouteState == null || candidateCommands == null || candidateCommands.isEmpty()) {
            return null;
        }
        Set<String> issuedRouteSignatureSet = rerouteState.getIssuedRouteSignatureSet();
        StationTaskLoopService.LoopIdentitySnapshot loopIdentity = loopEvaluation == null
                ? StationTaskLoopService.LoopIdentitySnapshot.empty()
                : loopEvaluation.getLoopIdentity();
        if (loopIdentity == null) {
            loopIdentity = StationTaskLoopService.LoopIdentitySnapshot.empty();
        }
        List<RerouteCandidateCommand> candidateCommandList = new ArrayList<>();
        for (StationCommand candidateCommand : candidateCommands) {
            if (candidateCommand == null || candidateCommand.getNavigatePath() == null || candidateCommand.getNavigatePath().isEmpty()) {
                continue;
            }
            String routeSignature = buildPathSignature(candidateCommand.getNavigatePath());
            if (Cools.isEmpty(routeSignature)) {
                continue;
            }
            RerouteCandidateCommand rerouteCandidateCommand = new RerouteCandidateCommand();
            rerouteCandidateCommand.setCommand(candidateCommand);
            rerouteCandidateCommand.setRouteSignature(routeSignature);
            rerouteCandidateCommand.setPathLength(candidateCommand.getNavigatePath().size());
            rerouteCandidateCommand.setIssuedCount(rerouteState.getRouteIssueCountMap().getOrDefault(routeSignature, 0));
            rerouteCandidateCommand.setLoopFingerprint(loopIdentity.getLoopFingerprint());
            rerouteCandidateCommand.setLoopTriggered(loopEvaluation != null && loopEvaluation.isLargeLoopTriggered());
            rerouteCandidateCommand.setCurrentLoopHitCount(countCurrentLoopStationHit(
                    candidateCommand.getNavigatePath(),
                    loopIdentity.getStationIdSet()
            ));
            candidateCommandList.add(rerouteCandidateCommand);
        }
        if (candidateCommandList.isEmpty()) {
            return null;
        }
        List<RerouteCandidateCommand> orderedCandidateCommandList = reorderCandidateCommandsForLoopRelease(candidateCommandList);
        for (RerouteCandidateCommand candidateCommand : orderedCandidateCommandList) {
            if (candidateCommand == null || candidateCommand.getCommand() == null) {
                continue;
            }
            if (issuedRouteSignatureSet.contains(candidateCommand.getRouteSignature())) {
                continue;
            }
            StationCommand rerouteCommand = candidateCommand.getCommand();
            issuedRouteSignatureSet.add(candidateCommand.getRouteSignature());
            rerouteState.getIssuedRoutePathList().add(new ArrayList<>(rerouteCommand.getNavigatePath()));
            rerouteState.setLastSelectedRoute(new ArrayList<>(rerouteCommand.getNavigatePath()));
            rerouteState.getRouteIssueCountMap().put(
                    candidateCommand.getRouteSignature(),
                    rerouteState.getRouteIssueCountMap().getOrDefault(candidateCommand.getRouteSignature(), 0) + 1
            );
            return rerouteCommand;
        }
        return null;
    }
    private List<RerouteCandidateCommand> reorderCandidateCommandsForLoopRelease(List<RerouteCandidateCommand> candidateCommandList) {
        if (candidateCommandList == null || candidateCommandList.isEmpty()) {
            return new ArrayList<>();
        }
        int shortestPathLength = Integer.MAX_VALUE;
        int shortestPathLoopHitCount = Integer.MAX_VALUE;
        boolean shortestPathOverused = false;
        boolean currentLoopOverused = false;
        boolean hasLongerCandidate = false;
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand == null || candidateCommand.getPathLength() == null || candidateCommand.getPathLength() <= 0) {
                continue;
            }
            shortestPathLength = Math.min(shortestPathLength, candidateCommand.getPathLength());
        }
        if (shortestPathLength == Integer.MAX_VALUE) {
            return candidateCommandList;
        }
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand == null || candidateCommand.getPathLength() == null || candidateCommand.getPathLength() <= 0) {
                continue;
            }
            if (candidateCommand.getPathLength() == shortestPathLength) {
                shortestPathLoopHitCount = Math.min(shortestPathLoopHitCount, safeInt(candidateCommand.getCurrentLoopHitCount()));
            }
            if (candidateCommand.getPathLength() > shortestPathLength) {
                hasLongerCandidate = true;
            }
            if (candidateCommand.getPathLength() == shortestPathLength
                    && candidateCommand.getIssuedCount() != null
                    && candidateCommand.getIssuedCount() >= SHORT_PATH_REPEAT_AVOID_THRESHOLD) {
                shortestPathOverused = true;
            }
            if (!Cools.isEmpty(candidateCommand.getLoopFingerprint())
                    && Boolean.TRUE.equals(candidateCommand.getLoopTriggered())) {
                currentLoopOverused = true;
            }
        }
        if (!shortestPathOverused && !currentLoopOverused) {
            return candidateCommandList;
        }
        if (shortestPathLoopHitCount == Integer.MAX_VALUE) {
            shortestPathLoopHitCount = 0;
        }
        boolean hasLoopExitCandidate = false;
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand == null) {
                continue;
            }
            if (safeInt(candidateCommand.getCurrentLoopHitCount()) < shortestPathLoopHitCount) {
                hasLoopExitCandidate = true;
                break;
            }
        }
        if (!hasLongerCandidate && !hasLoopExitCandidate) {
            return candidateCommandList;
        }
        List<RerouteCandidateCommand> reorderedList = new ArrayList<>();
        if (currentLoopOverused && hasLoopExitCandidate) {
            for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
                if (candidateCommand == null) {
                    continue;
                }
                if (safeInt(candidateCommand.getCurrentLoopHitCount()) < shortestPathLoopHitCount) {
                    appendCandidateIfAbsent(reorderedList, candidateCommand);
                }
            }
        }
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand != null
                    && candidateCommand.getPathLength() != null
                    && candidateCommand.getPathLength() > shortestPathLength) {
                appendCandidateIfAbsent(reorderedList, candidateCommand);
            }
        }
        for (RerouteCandidateCommand candidateCommand : candidateCommandList) {
            if (candidateCommand == null || candidateCommand.getPathLength() == null) {
                continue;
            }
            appendCandidateIfAbsent(reorderedList, candidateCommand);
        }
        return reorderedList;
    }
    private void appendCandidateIfAbsent(List<RerouteCandidateCommand> reorderedList,
                                         RerouteCandidateCommand candidateCommand) {
        if (reorderedList == null || candidateCommand == null) {
            return;
        }
        if (!reorderedList.contains(candidateCommand)) {
            reorderedList.add(candidateCommand);
        }
    }
    private RunBlockRerouteState loadRunBlockRerouteState(Integer taskNo, Integer blockStationId) {
        if (redisUtil == null || taskNo == null || taskNo <= 0 || blockStationId == null || blockStationId <= 0) {
            return new RunBlockRerouteState();
        }
        Object stateObj = redisUtil.get(buildRunBlockRerouteStateKey(taskNo, blockStationId));
        if (stateObj == null) {
            return new RunBlockRerouteState();
        }
        try {
            RunBlockRerouteState state = JSON.parseObject(String.valueOf(stateObj), RunBlockRerouteState.class);
            return state == null ? new RunBlockRerouteState() : state.normalize();
        } catch (Exception ignore) {
            return new RunBlockRerouteState();
        }
    }
    private void saveRunBlockRerouteState(RunBlockRerouteState rerouteState) {
        if (redisUtil == null
                || rerouteState == null
                || rerouteState.getTaskNo() == null
                || rerouteState.getTaskNo() <= 0
                || rerouteState.getBlockStationId() == null
                || rerouteState.getBlockStationId() <= 0) {
            return;
        }
        rerouteState.normalize();
        redisUtil.set(
                buildRunBlockRerouteStateKey(rerouteState.getTaskNo(), rerouteState.getBlockStationId()),
                JSON.toJSONString(rerouteState),
                RUN_BLOCK_REROUTE_STATE_EXPIRE_SECONDS
        );
    }
    private String buildRunBlockRerouteStateKey(Integer taskNo, Integer blockStationId) {
        return RedisKeyType.STATION_RUN_BLOCK_REROUTE_STATE_.key + taskNo + "_" + blockStationId;
    }
    private int countCurrentLoopStationHit(List<Integer> path, Set<Integer> currentLoopStationIdSet) {
        if (path == null || path.isEmpty() || currentLoopStationIdSet == null || currentLoopStationIdSet.isEmpty()) {
            return 0;
        }
        int hitCount = 0;
        for (Integer stationId : path) {
            if (stationId != null && currentLoopStationIdSet.contains(stationId)) {
                hitCount++;
            }
        }
        return hitCount;
    }
    private String buildPathSignature(List<Integer> path) {
        if (path == null || path.isEmpty()) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        for (Integer stationNo : path) {
            if (stationNo == null) {
                continue;
            }
            if (builder.length() > 0) {
                builder.append("->");
            }
            builder.append(stationNo);
        }
        return builder.toString();
    }
    private int safeInt(Integer value) {
        return value == null ? 0 : value;
    }
    private List<List<Integer>> copyRoutePathList(List<List<Integer>> source) {
        List<List<Integer>> copy = new ArrayList<>();
        if (source == null || source.isEmpty()) {
            return copy;
        }
        for (List<Integer> route : source) {
            copy.add(route == null ? new ArrayList<>() : new ArrayList<>(route));
        }
        return copy;
    }
    @Data
    public static class PlanResult {
        private final StationCommand command;
        private final int planCount;
        private final List<List<Integer>> issuedRoutePathList;
    }
    @Data
    private static class RunBlockRerouteState {
        private Integer taskNo;
        private Integer blockStationId;
        private Integer planCount = 0;
        private List<List<Integer>> issuedRoutePathList = new ArrayList<>();
        private List<Integer> lastSelectedRoute = new ArrayList<>();
        private Set<String> issuedRouteSignatureSet = new LinkedHashSet<>();
        private Map<String, Integer> routeIssueCountMap = new HashMap<>();
        private RunBlockRerouteState normalize() {
            if (planCount == null || planCount < 0) {
                planCount = 0;
            }
            if (issuedRoutePathList == null) {
                issuedRoutePathList = new ArrayList<>();
            }
            if (lastSelectedRoute == null) {
                lastSelectedRoute = new ArrayList<>();
            }
            if (issuedRouteSignatureSet == null) {
                issuedRouteSignatureSet = new LinkedHashSet<>();
            }
            if (routeIssueCountMap == null) {
                routeIssueCountMap = new HashMap<>();
            }
            for (List<Integer> routePath : issuedRoutePathList) {
                if (routePath == null || routePath.isEmpty()) {
                    continue;
                }
                String pathSignature = buildPathSignatureText(routePath);
                if (!Cools.isEmpty(pathSignature)) {
                    issuedRouteSignatureSet.add(pathSignature);
                    routeIssueCountMap.putIfAbsent(pathSignature, 1);
                }
            }
            return this;
        }
        private void resetIssuedRoutes() {
            this.issuedRoutePathList = new ArrayList<>();
            this.lastSelectedRoute = new ArrayList<>();
            this.issuedRouteSignatureSet = new LinkedHashSet<>();
        }
        private static String buildPathSignatureText(List<Integer> routePath) {
            if (routePath == null || routePath.isEmpty()) {
                return "";
            }
            StringBuilder builder = new StringBuilder();
            for (Integer stationId : routePath) {
                if (stationId == null) {
                    continue;
                }
                if (builder.length() > 0) {
                    builder.append("->");
                }
                builder.append(stationId);
            }
            return builder.toString();
        }
    }
    @Data
    private static class RerouteCandidateCommand {
        private StationCommand command;
        private String routeSignature;
        private Integer pathLength;
        private Integer issuedCount;
        private String loopFingerprint;
        private Boolean loopTriggered;
        private Integer currentLoopHitCount;
    }
}
src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutionPlan.java
File was deleted
src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentPlanner.java
File was deleted
src/main/java/com/zy/core/thread/impl/v5/StationV5StatusReader.java
New file
@@ -0,0 +1,127 @@
package com.zy.core.thread.impl.v5;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.Cools;
import com.core.common.DateUtils;
import com.core.common.SpringUtils;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.DeviceConfig;
import com.zy.asrs.entity.DeviceDataLog;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.utils.Utils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.cache.OutputQueue;
import com.zy.core.enums.SlaveType;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.network.ZyStationConnectDriver;
import com.zy.core.network.entity.ZyStationStatusEntity;
import com.zy.core.thread.support.RecentStationArrivalTracker;
import com.zy.core.utils.DeviceLogRedisKeyBuilder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class StationV5StatusReader {
    private final DeviceConfig deviceConfig;
    private final RedisUtil redisUtil;
    private final RecentStationArrivalTracker recentArrivalTracker;
    private final List<StationProtocol> statusList = new ArrayList<>();
    private boolean initialized = false;
    private long deviceDataLogTime = System.currentTimeMillis();
    public StationV5StatusReader(DeviceConfig deviceConfig,
                                 RedisUtil redisUtil,
                                 RecentStationArrivalTracker recentArrivalTracker) {
        this.deviceConfig = deviceConfig;
        this.redisUtil = redisUtil;
        this.recentArrivalTracker = recentArrivalTracker;
    }
    public void readStatus(ZyStationConnectDriver zyStationConnectDriver) {
        if (zyStationConnectDriver == null) {
            return;
        }
        if (statusList.isEmpty()) {
            BasDevpService basDevpService = null;
            try {
                basDevpService = SpringUtils.getBean(BasDevpService.class);
            } catch (Exception ignore) {
            }
            if (basDevpService == null) {
                return;
            }
            BasDevp basDevp = basDevpService
                    .getOne(new QueryWrapper<BasDevp>().eq("devp_no", deviceConfig.getDeviceNo()));
            if (basDevp == null) {
                return;
            }
            List<ZyStationStatusEntity> list = JSONObject.parseArray(basDevp.getStationList(), ZyStationStatusEntity.class);
            for (ZyStationStatusEntity entity : list) {
                StationProtocol stationProtocol = new StationProtocol();
                stationProtocol.setStationId(entity.getStationId());
                statusList.add(stationProtocol);
            }
            initialized = true;
        }
        int deviceLogCollectTime = initialized ? Utils.getDeviceLogCollectTime() : 200;
        List<ZyStationStatusEntity> zyStationStatusEntities = zyStationConnectDriver.getStatus();
        for (ZyStationStatusEntity statusEntity : zyStationStatusEntities) {
            for (StationProtocol stationProtocol : statusList) {
                if (stationProtocol.getStationId().equals(statusEntity.getStationId())) {
                    stationProtocol.setTaskNo(statusEntity.getTaskNo());
                    stationProtocol.setTargetStaNo(statusEntity.getTargetStaNo());
                    stationProtocol.setAutoing(statusEntity.isAutoing());
                    stationProtocol.setLoading(statusEntity.isLoading());
                    stationProtocol.setInEnable(statusEntity.isInEnable());
                    stationProtocol.setOutEnable(statusEntity.isOutEnable());
                    stationProtocol.setEmptyMk(statusEntity.isEmptyMk());
                    stationProtocol.setFullPlt(statusEntity.isFullPlt());
                    stationProtocol.setPalletHeight(statusEntity.getPalletHeight());
                    stationProtocol.setError(statusEntity.getError());
                    stationProtocol.setErrorMsg(statusEntity.getErrorMsg());
                    stationProtocol.setBarcode(statusEntity.getBarcode());
                    stationProtocol.setRunBlock(statusEntity.isRunBlock());
                    stationProtocol.setEnableIn(statusEntity.isEnableIn());
                    stationProtocol.setWeight(statusEntity.getWeight());
                    stationProtocol.setTaskWriteIdx(statusEntity.getTaskWriteIdx());
                    stationProtocol.setTaskBufferItems(statusEntity.getTaskBufferItems());
                    recentArrivalTracker.observe(statusEntity.getStationId(), statusEntity.getTaskNo(), statusEntity.isLoading());
                }
                if (!Cools.isEmpty(stationProtocol.getSystemWarning())) {
                    if (stationProtocol.isAutoing() && !stationProtocol.isLoading()) {
                        stationProtocol.setSystemWarning("");
                    }
                }
            }
        }
        OutputQueue.DEVP.offer(MessageFormat.format("【{0}】[id:{1}] <<<<< 实时数据更新成功",
                DateUtils.convert(new Date()), deviceConfig.getDeviceNo()));
        if (System.currentTimeMillis() - deviceDataLogTime > deviceLogCollectTime) {
            DeviceDataLog deviceDataLog = new DeviceDataLog();
            deviceDataLog.setOriginData(JSON.toJSONString(zyStationStatusEntities));
            deviceDataLog.setWcsData(JSON.toJSONString(statusList));
            deviceDataLog.setType(String.valueOf(SlaveType.Devp));
            deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo());
            deviceDataLog.setCreateTime(new Date());
            redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24);
            deviceDataLogTime = System.currentTimeMillis();
        }
    }
    public List<StationProtocol> getStatusList() {
        return statusList;
    }
}
src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java
@@ -24,6 +24,7 @@
import com.zy.core.News;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.*;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.Task;
@@ -69,6 +70,8 @@
    private StationOperateProcessUtils stationOperateProcessUtils;
    @Autowired
    private WrkAnalysisService wrkAnalysisService;
    @Autowired
    private StationCommandDispatcher stationCommandDispatcher;
    private static final String CRN_OUT_REQUIRE_STATION_OUT_ENABLE_CONFIG = "crnOutRequireStationOutEnable";
@@ -817,7 +820,7 @@
                        }
                        //生成仿真站点数据
                        StationCommand command = stationThread.getCommand(StationCommandType.WRITE_INFO, 9998, wrkMast.getSourceStaNo(), 0, 0);
                        MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                        stationCommandDispatcher.dispatch(stationObjModel.getDeviceNo(), command, "dual-crn-operate-process", "fake-out-complete-write-info");
                    }
                }
src/main/java/com/zy/core/utils/StationOperateProcessUtils.java
@@ -1,586 +1,74 @@
package com.zy.core.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.Cools;
import com.core.exception.CoolException;
import com.zy.asrs.domain.enums.NotifyMsgType;
import com.zy.asrs.domain.path.StationPathResolvedPolicy;
import com.zy.asrs.domain.vo.StationCycleCapacityVo;
import com.zy.asrs.domain.vo.StationCycleLoopVo;
import com.zy.asrs.entity.*;
import com.zy.asrs.service.*;
import com.zy.asrs.utils.NotifyUtils;
import com.zy.common.entity.FindCrnNoResult;
import com.zy.common.model.NavigateNode;
import com.zy.common.model.StartupDto;
import com.zy.common.service.CommonService;
import com.zy.common.utils.NavigateUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.move.StationMoveCoordinator;
import com.zy.core.move.StationMoveDispatchMode;
import com.zy.core.move.StationMoveSession;
import com.zy.core.News;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.*;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.Task;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.model.protocol.StationTaskBufferItem;
import com.zy.core.service.StationTaskLoopService;
import com.zy.core.thread.StationThread;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.WrkMastService;
import com.zy.core.enums.WrkIoType;
import com.zy.core.enums.WrkStsType;
import com.zy.core.utils.station.StationDispatchLoadSupport;
import com.zy.core.utils.station.StationOutboundDispatchProcessor;
import com.zy.core.utils.station.StationRegularDispatchProcessor;
import com.zy.core.utils.station.StationRerouteProcessor;
import com.zy.core.utils.station.model.RerouteCommandPlan;
import com.zy.core.utils.station.model.RerouteContext;
import com.zy.core.utils.station.model.RerouteDecision;
import com.zy.core.utils.station.model.RerouteExecutionResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.List;
@Component
public class StationOperateProcessUtils {
    private static final int LOOP_LOAD_RESERVE_EXPIRE_SECONDS = 120;
    private static final int OUT_ORDER_DISPATCH_LIMIT_SECONDS = 2;
    private static final int STATION_COMMAND_DISPATCH_DEDUP_SECONDS = 10;
    private static final int STATION_IDLE_RECOVER_SECONDS = 10;
    private static final int STATION_IDLE_RECOVER_LIMIT_SECONDS = 30;
    private static final int STATION_IDLE_TRACK_EXPIRE_SECONDS = 60 * 60;
    private static final long STATION_MOVE_RESET_WAIT_MS = 1000L;
    private static final String IDLE_RECOVER_CLEARED_MEMO = "idleRecoverRerouteCleared";
    @Autowired
    private BasDevpService basDevpService;
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private CommonService commonService;
    private StationRegularDispatchProcessor stationRegularDispatchProcessor;
    @Autowired
    private RedisUtil redisUtil;
    private StationDispatchLoadSupport stationDispatchLoadSupport;
    @Autowired
    private LocMastService locMastService;
    private StationOutboundDispatchProcessor stationOutboundDispatchProcessor;
    @Autowired
    private WmsOperateUtils wmsOperateUtils;
    @Autowired
    private NotifyUtils notifyUtils;
    @Autowired
    private NavigateUtils navigateUtils;
    @Autowired
    private BasStationService basStationService;
    @Autowired
    private StationCycleCapacityService stationCycleCapacityService;
    @Autowired
    private StationPathPolicyService stationPathPolicyService;
    @Autowired
    private BasStationOptService basStationOptService;
    @Autowired
    private StationTaskLoopService stationTaskLoopService;
    @Autowired
    private WrkAnalysisService wrkAnalysisService;
    @Autowired
    private StationMoveCoordinator stationMoveCoordinator;
    private StationRerouteProcessor stationRerouteProcessor;
    //执行输送站点入库任务
    public synchronized void stationInExecute() {
        try {
            DispatchLimitConfig baseLimitConfig = getDispatchLimitConfig(null, null);
            int[] currentStationTaskCountRef = new int[]{countCurrentStationTask()};
            LoadGuardState loadGuardState = buildLoadGuardState(baseLimitConfig);
            List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<>());
            for (BasDevp basDevp : basDevps) {
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
                if (stationThread == null) {
                    continue;
                }
                Map<Integer, StationProtocol> stationMap = stationThread.getStatusMap();
                List<StationObjModel> list = basDevp.getBarcodeStationList$();
                for (StationObjModel entity : list) {
                    Integer stationId = entity.getStationId();
                    if (!stationMap.containsKey(stationId)) {
                        continue;
                    }
                    StationProtocol stationProtocol = stationMap.get(stationId);
                    if (stationProtocol == null) {
                        continue;
                    }
                    Object lock = redisUtil.get(RedisKeyType.STATION_IN_EXECUTE_LIMIT.key + stationId);
                    if (lock != null) {
                        continue;
                    }
                    //满足自动、有物、有工作号
                    if (stationProtocol.isAutoing()
                            && stationProtocol.isLoading()
                            && stationProtocol.getTaskNo() > 0
                    ) {
                        //检测任务是否生成
                        WrkMast wrkMast = wrkMastService.getOne(new QueryWrapper<WrkMast>().eq("barcode", stationProtocol.getBarcode()));
                        if (wrkMast == null) {
                            continue;
                        }
                        if (!Objects.equals(wrkMast.getWrkSts(), WrkStsType.NEW_INBOUND.sts)) {
                            continue;
                        }
                        String locNo = wrkMast.getLocNo();
                        FindCrnNoResult findCrnNoResult = commonService.findCrnNoByLocNo(locNo);
                        if (findCrnNoResult == null) {
                            News.taskInfo(wrkMast.getWrkNo(), "{}工作,未匹配到堆垛机", wrkMast.getWrkNo());
                            continue;
                        }
                        Integer targetStationId = commonService.findInStationId(findCrnNoResult, stationId);
                        if (targetStationId == null) {
                            News.taskInfo(wrkMast.getWrkNo(), "{}站点,搜索入库站点失败", stationId);
                            continue;
                        }
                        DispatchLimitConfig limitConfig = getDispatchLimitConfig(stationProtocol.getStationId(), targetStationId);
                        LoopHitResult loopHitResult = findPathLoopHit(limitConfig, stationProtocol.getStationId(), targetStationId, loadGuardState);
                        if (isDispatchBlocked(limitConfig, currentStationTaskCountRef[0], loadGuardState, loopHitResult.isThroughLoop())) {
                            return;
                        }
                        StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationId, targetStationId, 0);
                        if (command == null) {
                            News.taskInfo(wrkMast.getWrkNo(), "{}工作,获取输送线命令失败", wrkMast.getWrkNo());
                            continue;
                        }
                        Date now = new Date();
                        wrkMast.setWrkSts(WrkStsType.INBOUND_STATION_RUN.sts);
                        wrkMast.setSourceStaNo(stationProtocol.getStationId());
                        wrkMast.setStaNo(targetStationId);
                        wrkMast.setSystemMsg("");
                        wrkMast.setIoTime(now);
                        wrkMast.setModiTime(now);
                        if (wrkMastService.updateById(wrkMast)) {
                            wrkAnalysisService.markInboundStationStart(wrkMast, now);
                            boolean offered = offerDevpCommandWithDedup(basDevp.getDevpNo(), command, "stationInExecute");
                            if (offered && stationMoveCoordinator != null) {
                                // 初始入库命令也纳入 session 跟踪,后续停留恢复/绕圈/堵塞重算才能基于同一条路线状态判断。
                                stationMoveCoordinator.recordDispatch(
                                        wrkMast.getWrkNo(),
                                        stationProtocol.getStationId(),
                                        "stationInExecute",
                                        command,
                                        false
                                );
                            }
                            News.info("输送站点入库命令下发成功,站点号={},工作号={},命令数据={}", stationId, wrkMast.getWrkNo(), JSON.toJSONString(command));
                            redisUtil.set(RedisKeyType.STATION_IN_EXECUTE_LIMIT.key + stationId, "lock", 5);
                            loadGuardState.reserveLoopTask(loopHitResult.getLoopNo());
                            saveLoopLoadReserve(wrkMast.getWrkNo(), loopHitResult);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        stationRegularDispatchProcessor.stationInExecute();
    }
    //执行堆垛机输送站点出库任务
    public synchronized void crnStationOutExecute() {
        try {
            DispatchLimitConfig baseLimitConfig = getDispatchLimitConfig(null, null);
            int[] currentStationTaskCountRef = new int[]{countCurrentStationTask()};
            LoadGuardState loadGuardState = buildLoadGuardState(baseLimitConfig);
            List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>()
                    .eq("wrk_sts", WrkStsType.OUTBOUND_RUN_COMPLETE.sts)
                    .isNotNull("crn_no")
            );
            List<Integer> outOrderList = getAllOutOrderList();
            for (WrkMast wrkMast : wrkMasts) {
                Object infoObj = redisUtil.get(RedisKeyType.CRN_OUT_TASK_COMPLETE_STATION_INFO.key + wrkMast.getWrkNo());
                if (infoObj == null) {
                    News.info("出库任务{}数据缓存不存在", wrkMast.getWrkNo());
                    continue;
                }
                StationObjModel stationObjModel = JSON.parseObject(infoObj.toString(), StationObjModel.class);
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo());
                if (stationThread == null) {
                    continue;
                }
                Map<Integer, StationProtocol> stationMap = stationThread.getStatusMap();
                StationProtocol stationProtocol = stationMap.get(stationObjModel.getStationId());
                if (stationProtocol == null) {
                    continue;
                }
                Object lock = redisUtil.get(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId());
                if (lock != null) {
                    continue;
                }
                //满足自动、有物、工作号0
                if (stationProtocol.isAutoing()
                        && stationProtocol.isLoading()
                        && stationProtocol.getTaskNo() == 0
                ) {
                    // 先算当前任务在批次出库中的路径倾向系数,再带着这个系数去决策目标站,
                    // 这样同一批次不同序号任务在排序点、绕圈点和堵塞重算时会得到一致的目标裁决。
                    Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast);
                    OutOrderDispatchDecision dispatchDecision = resolveOutboundDispatchDecision(
                            stationProtocol.getStationId(),
                            wrkMast,
                            outOrderList,
                            pathLenFactor
                    );
                    Integer moveStaNo = dispatchDecision == null ? null : dispatchDecision.getTargetStationId();
                    if (moveStaNo == null) {
                        continue;
                    }
                    DispatchLimitConfig limitConfig = getDispatchLimitConfig(stationProtocol.getStationId(), moveStaNo);
                    LoopHitResult loopHitResult = findPathLoopHit(limitConfig, stationProtocol.getStationId(), moveStaNo, loadGuardState, wrkMast, pathLenFactor);
                    if (isDispatchBlocked(limitConfig, currentStationTaskCountRef[0], loadGuardState, loopHitResult.isThroughLoop())) {
                        return;
                    }
                    StationCommand command = buildOutboundMoveCommand(
                            stationThread,
                            wrkMast,
                            stationProtocol.getStationId(),
                            moveStaNo,
                            pathLenFactor
                    );
                    if (command == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
                        continue;
                    }
                    Date now = new Date();
                    wrkMast.setWrkSts(WrkStsType.STATION_RUN.sts);
                    wrkMast.setSystemMsg("");
                    wrkMast.setIoTime(now);
                    wrkMast.setModiTime(now);
                    if (wrkMastService.updateById(wrkMast)) {
                        wrkAnalysisService.markOutboundStationStart(wrkMast, now);
                        boolean offered = offerDevpCommandWithDedup(stationObjModel.getDeviceNo(), command, "crnStationOutExecute");
                        if (offered && stationMoveCoordinator != null) {
                            stationMoveCoordinator.recordDispatch(
                                    wrkMast.getWrkNo(),
                                    stationProtocol.getStationId(),
                                    "crnStationOutExecute",
                                    command,
                                    false
                            );
                        }
                        News.info("输送站点出库命令下发成功,站点号={},工作号={},命令数据={}", stationProtocol.getStationId(), wrkMast.getWrkNo(), JSON.toJSONString(command));
                        redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId(), "lock", 5);
                        redisUtil.del(RedisKeyType.CRN_OUT_TASK_COMPLETE_STATION_INFO.key + wrkMast.getWrkNo());
                        currentStationTaskCountRef[0]++;
                        loadGuardState.reserveLoopTask(loopHitResult.getLoopNo());
                        saveLoopLoadReserve(wrkMast.getWrkNo(), loopHitResult);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        stationOutboundDispatchProcessor.crnStationOutExecute();
    }
    //执行双工位堆垛机输送站点出库任务
    public synchronized void dualCrnStationOutExecute() {
        try {
            List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>()
                    .eq("wrk_sts", WrkStsType.OUTBOUND_RUN_COMPLETE.sts)
                    .isNotNull("dual_crn_no")
            );
            for (WrkMast wrkMast : wrkMasts) {
                Object infoObj = redisUtil.get(RedisKeyType.DUAL_CRN_OUT_TASK_STATION_INFO.key + wrkMast.getWrkNo());
                if (infoObj == null) {
                    News.info("出库任务{}数据缓存不存在", wrkMast.getWrkNo());
                    continue;
                }
                StationObjModel stationObjModel = JSON.parseObject(infoObj.toString(), StationObjModel.class);
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo());
                if (stationThread == null) {
                    continue;
                }
                Map<Integer, StationProtocol> stationMap = stationThread.getStatusMap();
                StationProtocol stationProtocol = stationMap.get(stationObjModel.getStationId());
                if (stationProtocol == null) {
                    continue;
                }
                Object lock = redisUtil.get(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId());
                if (lock != null) {
                    continue;
                }
                //满足自动、有物、工作号0
                if (stationProtocol.isAutoing()
                        && stationProtocol.isLoading()
                        && stationProtocol.getTaskNo() == 0
                ) {
                    Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast);
                    StationCommand command = buildOutboundMoveCommand(
                            stationThread,
                            wrkMast,
                            stationProtocol.getStationId(),
                            wrkMast.getStaNo(),
                            pathLenFactor
                    );
                    if (command == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
                        continue;
                    }
                    wrkMast.setWrkSts(WrkStsType.STATION_RUN.sts);
                    wrkMast.setSystemMsg("");
                    wrkMast.setIoTime(new Date());
                    if (wrkMastService.updateById(wrkMast)) {
                        boolean offered = offerDevpCommandWithDedup(stationObjModel.getDeviceNo(), command, "dualCrnStationOutExecute");
                        if (offered && stationMoveCoordinator != null) {
                            // 双工位堆垛机转入输送线后同样要登记 session,否则后续重算只能看到 PLC 命令,看不到路线语义。
                            stationMoveCoordinator.recordDispatch(
                                    wrkMast.getWrkNo(),
                                    stationProtocol.getStationId(),
                                    "dualCrnStationOutExecute",
                                    command,
                                    false
                            );
                        }
                        notifyUtils.notify(String.valueOf(SlaveType.Devp), stationObjModel.getDeviceNo(), String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.STATION_OUT_TASK_RUN, null);
                        News.info("输送站点出库命令下发成功,站点号={},工作号={},命令数据={}", stationProtocol.getStationId(), wrkMast.getWrkNo(), JSON.toJSONString(command));
                        redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId(), "lock", 5);
                        redisUtil.del(RedisKeyType.DUAL_CRN_OUT_TASK_STATION_INFO.key + wrkMast.getWrkNo());
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        stationOutboundDispatchProcessor.dualCrnStationOutExecute();
    }
    //检测输送站点出库任务执行完成
    public synchronized void stationOutExecuteFinish() {
        try {
            List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>().eq("wrk_sts", WrkStsType.STATION_RUN.sts));
            for (WrkMast wrkMast : wrkMasts) {
                Integer wrkNo = wrkMast.getWrkNo();
                Integer targetStaNo = wrkMast.getStaNo();
                if (wrkNo == null || targetStaNo == null) {
                    continue;
                }
                boolean complete = false;
                Integer targetDeviceNo = null;
                StationThread stationThread = null;
                BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>().eq("station_id", targetStaNo));
                if (basStation != null) {
                    targetDeviceNo = basStation.getDeviceNo();
                    stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basStation.getDeviceNo());
                    if (stationThread != null) {
                        Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
                        StationProtocol stationProtocol = statusMap.get(basStation.getStationId());
                        if (stationProtocol != null && wrkNo.equals(stationProtocol.getTaskNo())) {
                            complete = true;
                        }
                    }
                }
                if (complete) {
                    attemptClearTaskPath(stationThread, wrkNo);
                    completeStationRunTask(wrkMast, targetDeviceNo);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void attemptClearTaskPath(StationThread stationThread, Integer taskNo) {
        if (stationThread == null || taskNo == null || taskNo <= 0) {
            return;
        }
        try {
            boolean cleared = stationThread.clearPath(taskNo);
            if (cleared) {
                News.info("输送站点任务运行完成后清理残留路径,工作号={}", taskNo);
            }
        } catch (Exception e) {
            News.error("输送站点任务运行完成后清理残留路径异常,工作号={}", taskNo, e);
        }
    }
    private void completeStationRunTask(WrkMast wrkMast, Integer deviceNo) {
        if (wrkMast == null || wrkMast.getWrkNo() == null) {
            return;
        }
        if (stationMoveCoordinator != null) {
            stationMoveCoordinator.finishSession(wrkMast.getWrkNo());
        }
        Date now = new Date();
        wrkMast.setWrkSts(WrkStsType.STATION_RUN_COMPLETE.sts);
        wrkMast.setIoTime(now);
        wrkMast.setModiTime(now);
        wrkMastService.updateById(wrkMast);
        wrkAnalysisService.markOutboundStationComplete(wrkMast, now);
        if (deviceNo != null) {
            notifyUtils.notify(String.valueOf(SlaveType.Devp), deviceNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.STATION_OUT_TASK_RUN_COMPLETE, null);
        }
        redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_COMPLETE_LIMIT.key + wrkMast.getWrkNo(), "lock", 60);
        stationRegularDispatchProcessor.stationOutExecuteFinish();
    }
    // 检测任务转完成
    public synchronized void checkTaskToComplete() {
        try {
            List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>().eq("wrk_sts", WrkStsType.STATION_RUN_COMPLETE.sts));
            for (WrkMast wrkMast : wrkMasts) {
                Integer wrkNo = wrkMast.getWrkNo();
                Integer targetStaNo = wrkMast.getStaNo();
                Object lock = redisUtil.get(RedisKeyType.STATION_OUT_EXECUTE_COMPLETE_LIMIT.key + wrkNo);
                if (lock != null) {
                    continue;
                }
                boolean complete = false;
                BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>().eq("station_id", targetStaNo));
                if (basStation == null) {
                    continue;
                }
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basStation.getDeviceNo());
                if (stationThread == null) {
                    continue;
                }
                Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
                StationProtocol stationProtocol = statusMap.get(basStation.getStationId());
                if (stationProtocol == null) {
                    continue;
                }
                if (!stationProtocol.getTaskNo().equals(wrkNo)) {
                    complete = true;
                }
                if (complete) {
                    if (stationMoveCoordinator != null) {
                        stationMoveCoordinator.finishSession(wrkNo);
                    }
                    wrkMast.setWrkSts(WrkStsType.COMPLETE_OUTBOUND.sts);
                    wrkMast.setIoTime(new Date());
                    wrkMastService.updateById(wrkMast);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        stationRegularDispatchProcessor.checkTaskToComplete();
    }
    //检测输送站点是否运行堵塞
    public synchronized void checkStationRunBlock() {
        try {
            List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<>());
            for (BasDevp basDevp : basDevps) {
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
                if (stationThread == null) {
                    continue;
                }
                List<Integer> runBlockReassignLocStationList = new ArrayList<>();
                for (StationObjModel stationObjModel : basDevp.getRunBlockReassignLocStationList$()) {
                    runBlockReassignLocStationList.add(stationObjModel.getStationId());
                }
                List<Integer> outOrderStationIds = basDevp.getOutOrderIntList();
                List<StationProtocol> list = stationThread.getStatus();
                for (StationProtocol stationProtocol : list) {
                    if (stationProtocol.isAutoing()
                            && stationProtocol.isLoading()
                            && stationProtocol.getTaskNo() > 0
                            && stationProtocol.isRunBlock()
                    ) {
                        WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
                        if (wrkMast == null) {
                            News.info("输送站点号={} 运行阻塞,但无法找到对应任务,工作号={}", stationProtocol.getStationId(), stationProtocol.getTaskNo());
                            continue;
                        }
                        Object lock = redisUtil.get(RedisKeyType.CHECK_STATION_RUN_BLOCK_LIMIT_.key + stationProtocol.getTaskNo());
                        if (lock != null) {
                            continue;
                        }
                        redisUtil.set(RedisKeyType.CHECK_STATION_RUN_BLOCK_LIMIT_.key + stationProtocol.getTaskNo(), "lock", 15);
                        if (shouldUseRunBlockDirectReassign(wrkMast, stationProtocol.getStationId(), runBlockReassignLocStationList)) {
                            executeRunBlockDirectReassign(basDevp, stationThread, stationProtocol, wrkMast);
                            continue;
                        }
                        Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast);
                        // 运行堵塞不单独决定业务目标站,仍然复用出库排序/绕圈的目标裁决,
                        // 这里只是要求用 run-block 专用算路,并在重发前清掉旧 session/segment 状态。
                        RerouteContext context = RerouteContext.create(
                                RerouteSceneType.RUN_BLOCK_REROUTE,
                                basDevp,
                                stationThread,
                                stationProtocol,
                                wrkMast,
                                outOrderStationIds,
                                pathLenFactor,
                                "checkStationRunBlock_reroute"
                        ).withRunBlockCommand()
                                .withSuppressDispatchGuard()
                                .withCancelSessionBeforeDispatch()
                                .withResetSegmentCommandsBeforeDispatch();
                        executeSharedReroute(context);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        stationRerouteProcessor.checkStationRunBlock();
    }
    //检测输送站点任务停留超时后重新计算路径
    public synchronized void checkStationIdleRecover() {
        try {
            List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<>());
            for (BasDevp basDevp : basDevps) {
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
                if (stationThread == null) {
                    continue;
                }
                List<StationProtocol> list = stationThread.getStatus();
                for (StationProtocol stationProtocol : list) {
                    if (stationProtocol.isAutoing()
                            && stationProtocol.isLoading()
                            && stationProtocol.getTaskNo() > 0
                            && !stationProtocol.isRunBlock()
                    ) {
                        checkStationIdleRecover(basDevp, stationThread, stationProtocol, basDevp.getOutOrderIntList());
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        stationRerouteProcessor.checkStationIdleRecover();
    }
    //获取输送线任务数量
    public synchronized int getCurrentStationTaskCount() {
        return countCurrentStationTask();
        return stationDispatchLoadSupport.countCurrentStationTask();
    }
    public synchronized int getCurrentOutboundTaskCountByTargetStation(Integer stationId) {
@@ -598,2457 +86,35 @@
    // 检测出库排序
    public synchronized void checkStationOutOrder() {
        List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
            if (stationThread == null) {
                continue;
            }
            Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
            List<StationObjModel> orderList = basDevp.getOutOrderList$();
            List<Integer> outOrderStationIds = basDevp.getOutOrderIntList();
            for (StationObjModel stationObjModel : orderList) {
                StationProtocol stationProtocol = statusMap.get(stationObjModel.getStationId());
                if (stationProtocol == null) {
                    continue;
                }
                if (!stationProtocol.isAutoing()) {
                    continue;
                }
                if (!stationProtocol.isLoading()) {
                    continue;
                }
                if (stationProtocol.getTaskNo() <= 0) {
                    continue;
                }
                // 排序点本身已经堵塞时,不在 out-order 里做二次决策,统一交给 run-block 重规划处理。
                if (stationProtocol.isRunBlock()) {
                    continue;
                }
                if (!stationProtocol.getStationId().equals(stationProtocol.getTargetStaNo())) {
                    continue;
                }
                WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
                if (wrkMast == null) {
                    continue;
                }
                if (!Objects.equals(wrkMast.getWrkSts(), WrkStsType.STATION_RUN.sts)) {
                    continue;
                }
                if (Objects.equals(stationProtocol.getStationId(), wrkMast.getStaNo())) {
                    continue;
                }
                // 只有活动中的现有路线才会压制 out-order;BLOCKED 路线要允许排序点重新启动。
                if (shouldSkipOutOrderDispatchForExistingRoute(wrkMast.getWrkNo(), stationProtocol.getStationId())) {
                    continue;
                }
                Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast);
                RerouteContext context = RerouteContext.create(
                        RerouteSceneType.OUT_ORDER,
                        basDevp,
                        stationThread,
                        stationProtocol,
                        wrkMast,
                        outOrderStationIds,
                        pathLenFactor,
                        "checkStationOutOrder"
                ).withDispatchDeviceNo(stationObjModel.getDeviceNo())
                        .withSuppressDispatchGuard()
                        .withOutOrderDispatchLock()
                        .withResetSegmentCommandsBeforeDispatch();
                executeSharedReroute(context);
            }
        }
        stationRerouteProcessor.checkStationOutOrder();
    }
    // 监控绕圈站点
    public synchronized void watchCircleStation() {
        List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
            if (stationThread == null) {
                continue;
            }
            List<Integer> outOrderList = basDevp.getOutOrderIntList();
            for (StationProtocol stationProtocol : stationThread.getStatus()) {
                if (!stationProtocol.isAutoing()) {
                    continue;
                }
                if (!stationProtocol.isLoading()) {
                    continue;
                }
                if (stationProtocol.getTaskNo() <= 0) {
                    continue;
                }
                // 绕圈触发优先读 session 的下一决策站,legacy WATCH_CIRCLE key 只做兼容回退。
                if (!isWatchingCircleArrival(stationProtocol.getTaskNo(), stationProtocol.getStationId())) {
                    continue;
                }
                WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
                if (wrkMast == null) {
                    continue;
                }
                if (!Objects.equals(wrkMast.getWrkSts(), WrkStsType.STATION_RUN.sts)) {
                    continue;
                }
                if (Objects.equals(stationProtocol.getStationId(), wrkMast.getStaNo())) {
                    continue;
                }
                Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast);
                RerouteContext context = RerouteContext.create(
                        RerouteSceneType.WATCH_CIRCLE,
                        basDevp,
                        stationThread,
                        stationProtocol,
                        wrkMast,
                        outOrderList,
                        pathLenFactor,
                        "watchCircleStation"
                ).withSuppressDispatchGuard()
                        .withOutOrderDispatchLock()
                        .withResetSegmentCommandsBeforeDispatch();
                executeSharedReroute(context);
            }
        }
    }
    private StationCommand buildOutboundMoveCommand(StationThread stationThread,
                                                    WrkMast wrkMast,
                                                    Integer stationId,
                                                    Integer targetStationId,
                                                    Double pathLenFactor) {
        if (stationThread == null || wrkMast == null) {
            return null;
        }
        return stationThread.getCommand(
                StationCommandType.MOVE,
                wrkMast.getWrkNo(),
                stationId,
                targetStationId,
                0,
                normalizePathLenFactor(pathLenFactor)
        );
        stationRerouteProcessor.watchCircleStation();
    }
    RerouteCommandPlan buildRerouteCommandPlan(RerouteContext context,
                                               RerouteDecision decision) {
        if (context == null) {
            return RerouteCommandPlan.skip("missing-context");
        }
        if (decision == null) {
            return RerouteCommandPlan.skip("missing-decision");
        }
        if (decision.skip()) {
            return RerouteCommandPlan.skip(decision.skipReason());
        }
        if (context.stationThread() == null || context.stationProtocol() == null || context.wrkMast() == null) {
            return RerouteCommandPlan.skip("missing-runtime-dependency");
        }
        Integer currentStationId = context.stationProtocol().getStationId();
        Integer targetStationId = decision.targetStationId();
        if (currentStationId == null || targetStationId == null) {
            return RerouteCommandPlan.skip("missing-target-station");
        }
        if (Objects.equals(currentStationId, targetStationId)) {
            return RerouteCommandPlan.skip("same-station");
        }
        StationCommand command = context.useRunBlockCommand()
                ? context.stationThread().getRunBlockRerouteCommand(
                context.wrkMast().getWrkNo(),
                currentStationId,
                targetStationId,
                0,
                context.pathLenFactor()
        )
                : buildOutboundMoveCommand(
                context.stationThread(),
                context.wrkMast(),
                currentStationId,
                targetStationId,
                context.pathLenFactor()
        );
        if (command == null) {
            if (context.sceneType() == RerouteSceneType.RUN_BLOCK_REROUTE) {
                News.taskInfo(context.wrkMast().getWrkNo(),
                        "输送站点堵塞重规划未找到可下发路线,当前站点={},目标站点={}",
                        currentStationId,
                        targetStationId);
            } else if (context.sceneType() == RerouteSceneType.IDLE_RECOVER) {
                News.taskInfo(context.wrkMast().getWrkNo(),
                        "站点任务停留超时后重算路径失败,当前站点={},目标站点={}",
                        currentStationId,
                        targetStationId);
            } else {
                News.taskInfo(context.wrkMast().getWrkNo(), "获取输送线命令失败");
            }
            return RerouteCommandPlan.skip("missing-command");
        }
        return RerouteCommandPlan.dispatch(command, decision, context.dispatchScene());
        return stationRerouteProcessor.buildRerouteCommandPlan(context, decision);
    }
    RerouteExecutionResult executeReroutePlan(RerouteContext context,
                                              RerouteCommandPlan plan) {
        if (context == null) {
            return RerouteExecutionResult.skip("missing-context");
        }
        if (plan == null) {
            return RerouteExecutionResult.skip("missing-plan");
        }
        if (plan.skip()) {
            return RerouteExecutionResult.skip(plan.skipReason());
        }
        StationProtocol stationProtocol = context.stationProtocol();
        if (stationProtocol == null) {
            return RerouteExecutionResult.skip("missing-station-protocol");
        }
        Integer taskNo = stationProtocol.getTaskNo();
        Integer stationId = stationProtocol.getStationId();
        if (taskNo == null || taskNo <= 0 || stationId == null) {
            return RerouteExecutionResult.skip("invalid-station-task");
        }
        if (stationMoveCoordinator != null) {
            return stationMoveCoordinator.withTaskDispatchLock(taskNo,
                    () -> executeReroutePlanWithTaskLock(context, plan, stationProtocol, taskNo, stationId));
        }
        return executeReroutePlanWithTaskLock(context, plan, stationProtocol, taskNo, stationId);
    }
    private RerouteExecutionResult executeReroutePlanWithTaskLock(RerouteContext context,
                                                                  RerouteCommandPlan plan,
                                                                  StationProtocol stationProtocol,
                                                                  Integer taskNo,
                                                                  Integer stationId) {
        boolean runBlockReroute = context.sceneType() == RerouteSceneType.RUN_BLOCK_REROUTE;
        if (context.checkRecentDispatch()
                && shouldSkipIdleRecoverForRecentDispatch(taskNo, stationId)) {
            return RerouteExecutionResult.skip("recent-dispatch");
        }
        int currentTaskBufferCommandCount = countCurrentTaskBufferCommands(stationProtocol.getTaskBufferItems(), taskNo);
        if (currentTaskBufferCommandCount > 0 && !runBlockReroute) {
            if (context.sceneType() == RerouteSceneType.IDLE_RECOVER) {
                News.info("输送站点任务停留超时,但缓存区仍存在当前任务命令,已跳过重算。站点号={},工作号={},当前任务命令数={}",
                        stationId,
                        taskNo,
                        currentTaskBufferCommandCount);
            }
            return RerouteExecutionResult.skip("buffer-has-current-task");
        }
        if (currentTaskBufferCommandCount > 0 && runBlockReroute) {
            // 堵塞重规划要替换的正是这些旧分段命令,不能再把残留 buffer 当成新的拦截条件。
            News.info("输送站点运行堵塞重规划检测到旧分段命令残留,已先清理本地状态后继续重发。站点号={},工作号={},当前任务命令数={}",
                    stationId,
                    taskNo,
                    currentTaskBufferCommandCount);
        }
        if (!runBlockReroute
                && context.checkSuppressDispatch()
                && stationMoveCoordinator != null
                && stationMoveCoordinator.shouldSuppressDispatch(taskNo, stationId, plan.command())) {
            return RerouteExecutionResult.skip("dispatch-suppressed");
        }
        // 进入堵塞重规划后,旧路线已经被显式取消,本轮命令不再参与 active-session suppress 判定。
        if (context.requireOutOrderDispatchLock()
                && !tryAcquireOutOrderDispatchLock(taskNo, stationId)) {
            return RerouteExecutionResult.skip("out-order-lock");
        }
        if (context.cancelSessionBeforeDispatch() && stationMoveCoordinator != null) {
            // 切路前先把旧 session 置为 CANCEL_PENDING,让已经排队中的旧分段线程在最终发送前停下。
            stationMoveCoordinator.markCancelPending(taskNo, "reroute_pending");
        }
        if (runBlockReroute) {
            // 站点进入堵塞后,设备侧可能已经把之前预下发的分段命令清掉了。
            // 先作废本地 session/segment 状态,再按新路线重发,避免被旧状态反向卡住。
            if (context.cancelSessionBeforeDispatch() && stationMoveCoordinator != null) {
                stationMoveCoordinator.cancelSession(taskNo);
            }
            if (context.resetSegmentCommandsBeforeDispatch()) {
                resetSegmentMoveCommandsBeforeReroute(taskNo);
            }
        }
        if (!runBlockReroute
                && context.cancelSessionBeforeDispatch() && stationMoveCoordinator != null) {
            stationMoveCoordinator.cancelSession(taskNo);
        }
        if (!isBlank(context.executionLockKey())) {
            Object lock = redisUtil.get(context.executionLockKey());
            if (lock != null) {
                return RerouteExecutionResult.skip("scene-lock");
            }
            redisUtil.set(context.executionLockKey(), "lock", context.executionLockSeconds());
        }
        if (!runBlockReroute && context.resetSegmentCommandsBeforeDispatch()) {
            resetSegmentMoveCommandsBeforeReroute(taskNo);
        }
        int clearedCommandCount = 0;
        if (context.clearIdleIssuedCommands()) {
            clearedCommandCount = clearIssuedMoveCommandsDuringIdleStay(context.idleTrack(), taskNo, stationId);
        }
        boolean offered = offerDevpCommandWithDedup(context.dispatchDeviceNo(), plan.command(), plan.dispatchScene());
        if (!offered) {
            return RerouteExecutionResult.skip("dispatch-dedup");
        }
        applyRerouteDispatchEffects(context, plan, clearedCommandCount);
        return RerouteExecutionResult.dispatched(plan.command(), clearedCommandCount);
        return stationRerouteProcessor.executeReroutePlan(context, plan);
    }
    RerouteDecision resolveSharedRerouteDecision(RerouteContext context) {
        if (context == null || context.wrkMast() == null || context.stationProtocol() == null) {
            return RerouteDecision.skip("missing-runtime-dependency");
        }
        Integer currentStationId = context.stationProtocol().getStationId();
        if (currentStationId == null) {
            return RerouteDecision.skip("missing-current-station");
        }
        if (context.sceneType() == RerouteSceneType.IDLE_RECOVER
                && !Objects.equals(context.wrkMast().getWrkSts(), WrkStsType.STATION_RUN.sts)) {
            Integer targetStationId = context.wrkMast().getStaNo();
            return targetStationId == null || Objects.equals(targetStationId, currentStationId)
                    ? RerouteDecision.skip("same-station")
                    : RerouteDecision.proceed(targetStationId);
        }
        OutOrderDispatchDecision dispatchDecision = resolveOutboundDispatchDecision(
                currentStationId,
                context.wrkMast(),
                context.outOrderStationIds(),
                context.pathLenFactor()
        );
        Integer targetStationId = dispatchDecision == null ? null : dispatchDecision.getTargetStationId();
        if (targetStationId == null || Objects.equals(targetStationId, currentStationId)) {
            return RerouteDecision.skip("same-station");
        }
        return RerouteDecision.proceed(targetStationId, dispatchDecision);
    }
    private RerouteExecutionResult executeSharedReroute(RerouteContext context) {
        RerouteDecision decision = resolveSharedRerouteDecision(context);
        if (decision.skip()) {
            return RerouteExecutionResult.skip(decision.skipReason());
        }
        RerouteCommandPlan plan = buildRerouteCommandPlan(context, decision);
        return executeReroutePlan(context, plan);
    }
    private void applyRerouteDispatchEffects(RerouteContext context,
                                             RerouteCommandPlan plan,
                                             int clearedCommandCount) {
        if (context == null || plan == null || plan.command() == null || context.wrkMast() == null || context.stationProtocol() == null) {
            return;
        }
        WrkMast wrkMast = context.wrkMast();
        StationProtocol stationProtocol = context.stationProtocol();
        OutOrderDispatchDecision dispatchDecision = plan.decision() == null ? null : plan.decision().dispatchDecision();
        syncOutOrderWatchState(wrkMast, stationProtocol.getStationId(), context.outOrderStationIds(), dispatchDecision, plan.command());
        if (stationMoveCoordinator != null) {
            stationMoveCoordinator.recordDispatch(
                    wrkMast.getWrkNo(),
                    stationProtocol.getStationId(),
                    plan.dispatchScene(),
                    plan.command(),
                    dispatchDecision != null && dispatchDecision.isCircle()
            );
        }
        if (context.sceneType() == RerouteSceneType.IDLE_RECOVER) {
            saveStationTaskIdleTrack(new StationTaskIdleTrack(wrkMast.getWrkNo(), stationProtocol.getStationId(), System.currentTimeMillis()));
            News.info("输送站点任务停留{}秒未运行,已重新计算路径并重启运行,站点号={},目标站={},工作号={},清理旧分段命令数={},命令数据={}",
                    STATION_IDLE_RECOVER_SECONDS,
                    stationProtocol.getStationId(),
                    plan.command().getTargetStaNo(),
                    wrkMast.getWrkNo(),
                    clearedCommandCount,
                    JSON.toJSONString(plan.command()));
            return;
        }
        if (context.sceneType() == RerouteSceneType.RUN_BLOCK_REROUTE) {
            News.info("输送站点堵塞后重新计算路径命令下发成功,站点号={},工作号={},命令数据={}",
                    stationProtocol.getStationId(),
                    wrkMast.getWrkNo(),
                    JSON.toJSONString(plan.command()));
            return;
        }
        if (context.sceneType() == RerouteSceneType.OUT_ORDER) {
            News.info(dispatchDecision != null && dispatchDecision.isCircle() ? "{}任务进行绕圈" : "{}任务直接去目标点", wrkMast.getWrkNo());
        }
    }
    private List<NavigateNode> calcOutboundNavigatePath(WrkMast wrkMast,
                                                        Integer sourceStationId,
                                                        Integer targetStationId,
                                                        Double pathLenFactor) {
        Double normalizedFactor = normalizePathLenFactor(pathLenFactor);
        Integer currentTaskNo = wrkMast == null ? null : wrkMast.getWrkNo();
        if (currentTaskNo == null) {
            return navigateUtils.calcByStationId(sourceStationId, targetStationId, normalizedFactor);
        }
        return navigateUtils.calcByStationId(sourceStationId, targetStationId, currentTaskNo, normalizedFactor);
    }
    /**
     * 计算当前出库任务的路径倾向系数。
     *
     * <p>这个系数不是业务目标站本身,而是“在多条可行路线之间更偏向哪一条”的辅助输入,
     * 目的是让同一批次、不同序号的任务在共享环线里尽量形成稳定、可重复的路径分布。
     *
     * <p>返回值范围固定在 {@code [0, 1]}:
     * 1. 非批次出库任务,直接返回 {@code 0.0},表示不引入额外路径偏置。
     * 2. 当前批次只有 1 个有效活动任务,返回 {@code 0.0},因为没有“前后顺序”可比较。
     * 3. 否则按“当前任务前面还有多少个有效前序任务”占“有效活动任务总数”的比例来算。
     *
     * <p>这里的“有效任务”只统计:
     * 1. ioType=OUT 的出库任务;
     * 2. 仍处于活动状态,未完成/未结算;
     * 3. 有 batchSeq;
     * 4. mk != taskCancel。
     *
     * <p>结果含义可以直观理解为:
     * 任务批次序号越靠后,前面已经存在的有效任务越多,得到的系数越大;
     * 后续算路时就更容易和前序任务形成稳定的路径分流,而不是所有任务都走同一条默认短路。
     *
     * <p>注意:
     * 这个方法不直接决定目标站,不负责排序放行,只提供“路径偏好”输入。
     * 真正的目标站仍由 {@link #resolveOutboundDispatchDecision(Integer, WrkMast, List, Double)} 决定。
     */
    private Double resolveOutboundPathLenFactor(WrkMast wrkMast) {
        if (!isBatchOutboundTaskWithSeq(wrkMast)) {
            return 0.0d;
        }
        List<WrkMast> activeBatchTaskList = loadActiveBatchTaskList(wrkMast.getBatch());
        if (activeBatchTaskList.size() <= 1) {
            return 0.0d;
        }
        int activeTaskCount = 0;
        int predecessorCount = 0;
        for (WrkMast item : activeBatchTaskList) {
            if (!isFactorCandidateTask(item)) {
                continue;
            }
            activeTaskCount++;
            if (item.getBatchSeq() < wrkMast.getBatchSeq()) {
                predecessorCount++;
            }
        }
        if (activeTaskCount <= 1 || predecessorCount <= 0) {
            return 0.0d;
        }
        return normalizePathLenFactor((double) predecessorCount / (double) (activeTaskCount - 1));
    }
    /**
     * 判断当前任务是否具备“按批次出库规则参与排序/路径偏好计算”的基础条件。
     *
     * <p>这里只做最基础的资格过滤,不关心当前是否真的需要排序点介入。
     * 只要不是批次出库任务,后面的路径偏好系数与排序目标决策都应该直接退化为默认行为。
     */
    private boolean isBatchOutboundTaskWithSeq(WrkMast wrkMast) {
        return wrkMast != null
                && Objects.equals(wrkMast.getIoType(), WrkIoType.OUT.id)
                && !Cools.isEmpty(wrkMast.getBatch())
                && wrkMast.getBatchSeq() != null
                && wrkMast.getWrkNo() != null;
    }
    /**
     * 加载同一批次下仍处于活动中的出库任务。
     *
     * <p>这里用于两类计算:
     * 1. 计算路径偏好系数时,统计当前任务前面还有多少个有效前序任务。
     * 2. 当前排序点重新决策时,找出这一批“首个未完成任务”的实际批次序号。
     *
     * <p>已经完成/结算的任务不再参与当前批次的排序与偏好计算。
     */
    private List<WrkMast> loadActiveBatchTaskList(String batch) {
        if (Cools.isEmpty(batch)) {
            return Collections.emptyList();
        }
        return wrkMastService.list(new QueryWrapper<WrkMast>()
                .eq("io_type", WrkIoType.OUT.id)
                .eq("batch", batch)
                .notIn("wrk_sts",
                        WrkStsType.STATION_RUN_COMPLETE.sts,
                        WrkStsType.COMPLETE_OUTBOUND.sts,
                        WrkStsType.SETTLE_OUTBOUND.sts));
    }
    /**
     * 判断某条批次任务是否应该计入路径偏好系数的分母/分子统计。
     *
     * <p>这里排除没有 batchSeq 的任务以及被显式标记为 taskCancel 的任务,
     * 避免无效任务把同批次的路径偏好计算拉偏。
     */
    private boolean isFactorCandidateTask(WrkMast wrkMast) {
        return wrkMast != null
                && Objects.equals(wrkMast.getIoType(), WrkIoType.OUT.id)
                && wrkMast.getBatchSeq() != null
                && !"taskCancel".equals(wrkMast.getMk());
    }
    public List<Integer> getAllOutOrderList() {
        List<Integer> list = new ArrayList<>();
        List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            List<Integer> orderList = basDevp.getOutOrderIntList();
            list.addAll(orderList);
        }
        return list;
    }
    /**
     * 统一计算当前任务“此刻应该朝哪个目标站继续运行”。
     *
     * <p>这是出库排序、绕圈、堵塞重规划共用的目标裁决入口。
     * 不管触发来源是 OUT_ORDER、WATCH_CIRCLE 还是 RUN_BLOCK_REROUTE,
     * 只要业务语义还是“当前这票出库任务下一步该往哪里走”,都从这里得出目标站。
     *
     * <p>它做三层判断:
     * 1. 如果当前任务根本不适用出库排序,直接返回任务业务目标站 {@code wrkMast.staNo}。
     * 2. 如果适用出库排序,先算出“当前批次规则下,此刻允许前往的 dispatchStationId”。
     * 3. 如果当前站点正好就是排序决策点,再进一步判断是:
     *    直接去目标点,还是先进入绕圈目标点,或者因为严格窗口限制而暂不放行。
     *
     * <p>返回的 {@link OutOrderDispatchDecision} 不只是一个目标站,
     * 还携带了这次决策是否属于绕圈、是否来自当前排序点重新裁决等语义信息,
     * 供后续日志、session 记录和 watch-circle 判定使用。
     *
     * <p>参数含义:
     * 1. {@code currentStationId}:任务当前所在站点。用于判断当前是不是排序决策点、
     *    当前是不是已经到达 watch-circle 的下一决策站。
     * 2. {@code wrkMast}:当前任务主状态,至少要提供业务目标站、来源站、批次、序号等信息。
     * 3. {@code outOrderStationIds}:当前设备配置的所有出库排序站点列表。
     * 4. {@code pathLenFactor}:由 {@link #resolveOutboundPathLenFactor(WrkMast)} 得到的路径偏好系数,
     *    用来让同一批次任务在选择 dispatch target 时保持稳定的路径倾向。
     *
     * <p>返回值语义:
     * 1. 返回 {@code null}:当前无法得到合法目标站,调用方应跳过本次派发。
     * 2. 返回 {@code targetStationId=wrkMast.staNo, circle=false}:
     *    当前不需要出库排序干预,或已允许直接去业务目标站。
     * 3. 返回 {@code targetStationId!=wrkMast.staNo}:
     *    当前应该先去一个中间 dispatch 目标站,后续再由排序点/绕圈点继续决策。
     * 4. 返回 {@code circle=true}:
     *    当前属于绕圈决策结果,后续 watch-circle 逻辑会据此接管。
     *
     * <p>注意:
     * 这个方法只决定“目标站”,不直接生成输送命令。
     * 真正的路径由普通算路或 run-block 专用算路在后续步骤生成。
     */
    private OutOrderDispatchDecision resolveOutboundDispatchDecision(Integer currentStationId,
                                                                     WrkMast wrkMast,
                                                                     List<Integer> outOrderStationIds,
                                                                     Double pathLenFactor) {
        if (wrkMast == null || wrkMast.getStaNo() == null) {
            return null;
        }
        if (!shouldApplyOutOrder(wrkMast, outOrderStationIds)) {
            return new OutOrderDispatchDecision(wrkMast.getStaNo(), false);
        }
        Integer dispatchStationId = resolveDispatchOutOrderTarget(
                wrkMast,
                wrkMast.getSourceStaNo(),
                wrkMast.getStaNo(),
                outOrderStationIds,
                pathLenFactor
        );
        if (dispatchStationId == null) {
            return null;
        }
        if (isCurrentOutOrderDispatchStation(currentStationId, wrkMast, outOrderStationIds, pathLenFactor)) {
            return resolveCurrentOutOrderDispatchDecision(currentStationId, wrkMast, outOrderStationIds, pathLenFactor);
        }
        if (!Objects.equals(dispatchStationId, wrkMast.getStaNo())
                && isCurrentOutOrderStation(currentStationId, outOrderStationIds)
                && isWatchingCircleArrival(wrkMast.getWrkNo(), currentStationId)) {
            return new OutOrderDispatchDecision(dispatchStationId, true, null, false);
        }
        return new OutOrderDispatchDecision(dispatchStationId, false);
    }
    /**
     * 在“当前站点就是本次排序决策点”时,计算这里到底该直接放行还是进入绕圈。
     *
     * <p>这是出库排序里最核心的局部决策方法。进入这里之前,已经满足:
     * 1. 当前任务适用 out-order;
     * 2. 当前站点就是这票任务此刻对应的 dispatch 排序点。
     *
     * <p>内部决策顺序:
     * 1. 先取出同批次仍未完成的任务,找出这批当前“应当被优先放行”的序号位置。
     * 2. 再结合当前任务在初始路径上的排序窗口位置,判断自己此刻能否直接去业务目标站。
     * 3. 如果理论上该自己放行,还要额外检查目标方向上是否存在可进入的 release slot。
     * 4. 如果不能直达,或者直达方向当前全部阻塞,就转成 circle 决策,寻找下一排序检测点。
     *
     * <p>返回值:
     * 1. {@code circle=false} 表示当前排序点已经允许直接去业务目标站。
     * 2. {@code circle=true} 表示当前只能先去下一绕圈目标站,后续由 watch-circle/排序点继续接力决策。
     * 3. {@code null} 表示当前既不能直达,也没找到合法的下一绕圈点,调用方应跳过本次派发。
     */
    private OutOrderDispatchDecision resolveCurrentOutOrderDispatchDecision(Integer currentStationId,
                                                                            WrkMast wrkMast,
                                                                            List<Integer> outOrderStationIds,
                                                                            Double pathLenFactor) {
        if (!isCurrentOutOrderDispatchStation(currentStationId, wrkMast, outOrderStationIds, pathLenFactor)) {
            return null;
        }
        List<WrkMast> batchWrkList = wrkMastService.list(new QueryWrapper<WrkMast>()
                .eq("io_type", WrkIoType.OUT.id)
                .notIn("wrk_sts",
                        WrkStsType.STATION_RUN_COMPLETE.sts,
                        WrkStsType.COMPLETE_OUTBOUND.sts,
                        WrkStsType.SETTLE_OUTBOUND.sts)
                .eq("batch", wrkMast.getBatch())
                .orderByAsc("batch_seq")
                .orderByAsc("wrk_no"));
        if (batchWrkList.isEmpty()) {
            return new OutOrderDispatchDecision(wrkMast.getStaNo(), false);
        }
        WrkMast firstWrkMast = batchWrkList.get(0);
        Integer currentBatchSeq = firstWrkMast.getBatchSeq();
        if (currentBatchSeq == null) {
            News.taskInfo(wrkMast.getWrkNo(), "批次:{} 首个未完成任务缺少批次序号,当前任务暂不放行", wrkMast.getBatch());
            return null;
        }
        List<NavigateNode> initPath;
        try {
            initPath = calcOutboundNavigatePath(wrkMast, wrkMast.getSourceStaNo(), wrkMast.getStaNo(), pathLenFactor);
        } catch (Exception e) {
            News.taskInfo(wrkMast.getWrkNo(), "批次:{} 计算排序路径失败,当前站点={}", wrkMast.getBatch(), currentStationId);
            return null;
        }
        Integer seq = getOutStationBatchSeq(initPath, currentStationId, wrkMast.getBatch());
        boolean toTarget;
        if (seq == null) {
            toTarget = currentBatchSeq.equals(wrkMast.getBatchSeq());
        } else {
            toTarget = Integer.valueOf(seq + 1).equals(wrkMast.getBatchSeq());
        }
        if (toTarget) {
            if (hasReachableOutReleaseSlot(wrkMast, currentStationId, wrkMast.getStaNo(), pathLenFactor)) {
                return new OutOrderDispatchDecision(wrkMast.getStaNo(), false);
            }
            StationTaskLoopService.LoopEvaluation loopEvaluation = evaluateOutOrderLoop(
                    wrkMast.getWrkNo(),
                    currentStationId,
                    outOrderStationIds
            );
            Integer circleTarget = resolveNextCircleOrderTarget(
                    wrkMast,
                    currentStationId,
                    outOrderStationIds,
                    loopEvaluation.getExpectedLoopIssueCount(),
                    pathLenFactor
            );
            if (circleTarget == null) {
                News.taskInfo(wrkMast.getWrkNo(), "目标站当前不可进,且未找到可执行的下一排序检测点,当前站点={}", currentStationId);
                return null;
            }
            return new OutOrderDispatchDecision(circleTarget, true, loopEvaluation, true);
        }
        StationTaskLoopService.LoopEvaluation loopEvaluation = evaluateOutOrderLoop(
                wrkMast.getWrkNo(),
                currentStationId,
                outOrderStationIds
        );
        Integer circleTarget = resolveNextCircleOrderTarget(
                wrkMast,
                currentStationId,
                outOrderStationIds,
                loopEvaluation.getExpectedLoopIssueCount(),
                pathLenFactor
        );
        if (circleTarget == null) {
            News.taskInfo(wrkMast.getWrkNo(), "未找到可执行的下一排序检测点,当前站点={}", currentStationId);
            return null;
        }
        return new OutOrderDispatchDecision(circleTarget, true, loopEvaluation, true);
    }
    /**
     * 判断这票任务在当前设备上是否应该启用出库排序逻辑。
     *
     * <p>只要缺少任何一个排序前提,例如不是出库任务、没有批次、没有序号、
     * 当前设备也没有配置排序点,就应该直接退回“普通目标站决策”。
     */
    private boolean shouldApplyOutOrder(WrkMast wrkMast, List<Integer> outOrderStationIds) {
        return wrkMast != null
                && wrkMast.getStaNo() != null
                && Objects.equals(wrkMast.getIoType(), WrkIoType.OUT.id)
                && !Cools.isEmpty(wrkMast.getBatch())
                && wrkMast.getBatchSeq() != null
                && outOrderStationIds != null
                && !outOrderStationIds.isEmpty();
    }
    /**
     * 判断当前所在站点是否就是“这票任务此刻应该触发排序决策的 dispatch 站点”。
     *
     * <p>注意它不是简单判断“当前站点是否属于排序点列表”,
     * 而是先根据完整路径反推出当前任务对应的 dispatch 排序点,
     * 再判断当前位置是否正好等于这个 dispatch 点。
     */
    private boolean isCurrentOutOrderDispatchStation(Integer currentStationId,
                                                     WrkMast wrkMast,
                                                     List<Integer> outOrderStationIds,
                                                     Double pathLenFactor) {
        if (!shouldApplyOutOrder(wrkMast, outOrderStationIds) || currentStationId == null) {
            return false;
        }
        Integer dispatchStationId = resolveDispatchOutOrderTarget(
                wrkMast,
                wrkMast.getSourceStaNo(),
                wrkMast.getStaNo(),
                outOrderStationIds,
                pathLenFactor
        );
        return dispatchStationId != null
                && !Objects.equals(dispatchStationId, wrkMast.getStaNo())
                && Objects.equals(currentStationId, dispatchStationId);
    }
    /**
     * 判断当前位置是否属于设备配置里的任意一个排序点。
     *
     * <p>这个判断比 {@link #isCurrentOutOrderDispatchStation(Integer, WrkMast, List, Double)} 更宽,
     * 只回答“当前位置是不是排序点”,不回答“是不是这票任务当前应该命中的排序点”。
     */
    private boolean isCurrentOutOrderStation(Integer currentStationId,
                                             List<Integer> outOrderStationIds) {
        return currentStationId != null
                && outOrderStationIds != null
                && outOrderStationIds.contains(currentStationId);
    }
    private void syncOutOrderWatchState(WrkMast wrkMast,
                                        Integer currentStationId,
                                        List<Integer> outOrderStationIds,
                                        OutOrderDispatchDecision dispatchDecision,
                                        StationCommand command) {
        if (dispatchDecision == null || command == null || !shouldApplyOutOrder(wrkMast, outOrderStationIds)) {
            return;
        }
        if (dispatchDecision.isCircle()) {
            saveWatchCircleCommand(wrkMast.getWrkNo(), command);
            if (dispatchDecision.shouldCountLoopIssue()
                    && stationTaskLoopService != null
                    && dispatchDecision.getLoopEvaluation() != null) {
                stationTaskLoopService.recordLoopIssue(dispatchDecision.getLoopEvaluation(), "OUT_ORDER_CIRCLE");
            }
        } else {
            clearWatchCircleCommand(wrkMast.getWrkNo());
        }
    }
    /**
     * 为当前排序点决策预先做一次环线评估。
     *
     * <p>当本次决策最终进入绕圈时,评估结果会被带进 {@link OutOrderDispatchDecision},
     * 后续用于记录 loop issue 统计,而不是在真正下发后再重复评估一次。
     */
    private StationTaskLoopService.LoopEvaluation evaluateOutOrderLoop(Integer taskNo,
                                                                       Integer currentStationId,
                                                                       List<Integer> outOrderStationIds) {
        if (stationTaskLoopService == null) {
            return new StationTaskLoopService.LoopEvaluation(
                    taskNo,
                    currentStationId,
                    StationTaskLoopService.LoopIdentitySnapshot.empty(),
                    0,
                    0,
                    false
            );
        }
        return stationTaskLoopService.evaluateLoop(
                taskNo,
                currentStationId,
                true,
                outOrderStationIds,
                "outOrderCircle"
        );
    }
    /**
     * 从“源站到业务目标站”的完整路径里,反推出当前任务应当先命中的 dispatch 排序点。
     *
     * <p>做法是:
     * 1. 先计算从 sourceStationId 到 finalTargetStationId 的完整导航路径;
     * 2. 再从路径尾部向前扫描;
     * 3. 找到离最终目标最近的那个排序点,作为当前 dispatch 目标。
     *
     * <p>如果路径上根本没有排序点,或者缺少源站/排序点配置,
     * 就直接把业务目标站本身当成 dispatch 目标。
     */
    private Integer resolveDispatchOutOrderTarget(WrkMast wrkMast,
                                                  Integer sourceStationId,
                                                  Integer finalTargetStationId,
                                                  List<Integer> outOrderList,
                                                  Double pathLenFactor) {
        if (finalTargetStationId == null) {
            return null;
        }
        if (sourceStationId == null || outOrderList == null || outOrderList.isEmpty()) {
            return finalTargetStationId;
        }
        try {
            List<NavigateNode> nodes = calcOutboundNavigatePath(wrkMast, sourceStationId, finalTargetStationId, pathLenFactor);
            for (int i = nodes.size() - 1; i >= 0; i--) {
                Integer stationId = getStationIdFromNode(nodes.get(i));
                if (stationId == null) {
                    continue;
                }
                if (Objects.equals(stationId, finalTargetStationId)) {
                    continue;
                }
                if (outOrderList.contains(stationId)) {
                    return stationId;
                }
            }
        } catch (Exception ignore) {}
        return finalTargetStationId;
    }
    /**
     * 判断从当前排序点继续前往最终业务目标站时,路径上是否至少存在一个可进入的后续站点。
     *
     * <p>这里不是要求整条路径完全畅通,而是判断“当前是否有释放口可走”。
     * 只要后续路径上存在一个未阻塞站点,就认为当前仍可尝试直达;
     * 只有整段后续路径看起来都被阻塞时,才会强制转入绕圈。
     */
    private boolean hasReachableOutReleaseSlot(WrkMast wrkMast,
                                               Integer currentStationId,
                                               Integer finalTargetStationId,
                                               Double pathLenFactor) {
        if (currentStationId == null || finalTargetStationId == null) {
            return true;
        }
        try {
            List<NavigateNode> nodes = calcOutboundNavigatePath(wrkMast, currentStationId, finalTargetStationId, pathLenFactor);
            if (nodes == null || nodes.isEmpty()) {
                return true;
            }
            for (NavigateNode node : nodes) {
                Integer stationId = getStationIdFromNode(node);
                if (stationId == null || Objects.equals(stationId, currentStationId)) {
                    continue;
                }
                if (!isPathStationBlocked(stationId)) {
                    return true;
                }
            }
            return false;
        } catch (Exception ignore) {
            return true;
        }
    }
    private boolean isPathStationBlocked(Integer stationId) {
        if (stationId == null) {
            return true;
        }
        BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>().eq("station_id", stationId));
        if (basStation == null) {
            return true;
        }
        StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basStation.getDeviceNo());
        if (stationThread == null) {
            return true;
        }
        StationProtocol stationProtocol = stationThread.getStatusMap().get(stationId);
        if (stationProtocol == null) {
            return true;
        }
        return !stationProtocol.isAutoing()
                || stationProtocol.isLoading()
                || (stationProtocol.getTaskNo() != null && stationProtocol.getTaskNo() > 0);
    }
    /**
     * 为“排序点需要绕圈”的场景,从出库排序站点列表里挑出下一跳绕圈目标。
     * <p>
     * 这里的输入不是整张地图,而是已经按业务顺序排好的出库站点序列。方法会从当前站点在该序列中的后继开始,
     * 依次尝试每个候选站点,并计算“当前站点 -> 候选站点”的可行路径:
     * <p>
     * 1. 只要能算出路径,就把候选站点记录为一个可选绕圈目标;
     * 2. 记录两类排序信息:
     *    - pathLength:到该候选点的路径长度,越短说明越适合先拿来做临时缓冲;
     *    - offset:该站点在排序序列中距离当前站点有多远,用来在路径长度相同的时候保留既有顺序感;
     * 3. 最后把候选列表交给 {@link #resolveGradualCircleTargetByPathLength(Integer, List, Double)},
     *    再根据“当前已经绕圈多少次”与“路径长度偏好系数”从不同长度层级里挑最终目标。
     * <p>
     * 这个方法本身不判断“是否应该绕圈”,只负责在已经决定绕圈后,从排序站点链路里找一个下一跳缓冲点。
     *
     * @param wrkMast 当前任务,主要用于算路
     * @param currentStationId 当前站点,即本次准备从哪里发出绕圈命令
     * @param orderedOutStationList 已按业务顺序排好的出库站点列表
     * @param expectedLoopIssueCount 预计已发生的绕圈/堵塞轮次,用于决定是否逐步放大绕圈半径
     * @param pathLenFactor 当前任务对应的路径偏好系数,影响 calcOutboundNavigatePath 的选路结果
     * @return 下一跳绕圈目标站;如果没有任何可到达候选则返回 null
     */
    private Integer resolveNextCircleOrderTarget(WrkMast wrkMast,
                                                 Integer currentStationId,
                                                 List<Integer> orderedOutStationList,
                                                 Integer expectedLoopIssueCount,
                                                 Double pathLenFactor) {
        if (currentStationId == null || orderedOutStationList == null || orderedOutStationList.size() <= 1) {
            return null;
        }
        int startIndex = orderedOutStationList.indexOf(currentStationId);
        int total = orderedOutStationList.size();
        List<CircleTargetCandidate> candidateList = new ArrayList<>();
        for (int offset = 1; offset < total; offset++) {
            int candidateIndex = (startIndex + offset + total) % total;
            Integer candidateStationId = orderedOutStationList.get(candidateIndex);
            if (candidateStationId == null || currentStationId.equals(candidateStationId)) {
                continue;
            }
            try {
                List<NavigateNode> path = calcOutboundNavigatePath(wrkMast, currentStationId, candidateStationId, pathLenFactor);
                if (path != null && !path.isEmpty()) {
                    candidateList.add(new CircleTargetCandidate(candidateStationId, path.size(), offset));
                }
            } catch (Exception ignore) {}
        }
        if (candidateList.isEmpty()) {
            return null;
        }
        candidateList.sort(new Comparator<CircleTargetCandidate>() {
            @Override
            public int compare(CircleTargetCandidate left, CircleTargetCandidate right) {
                if (left == right) {
                    return 0;
                }
                if (left == null) {
                    return 1;
                }
                if (right == null) {
                    return -1;
                }
                int pathCompare = Integer.compare(left.getPathLength(), right.getPathLength());
                if (pathCompare != 0) {
                    return pathCompare;
                }
                return Integer.compare(left.getOffset(), right.getOffset());
            }
        });
        return resolveGradualCircleTargetByPathLength(expectedLoopIssueCount, candidateList, pathLenFactor);
    }
    /**
     * 在“已按路径长度升序排好”的绕圈候选列表中,按层级渐进地挑选目标站。
     * <p>
     * candidateList 里可能存在多个候选点拥有相同的 pathLength。对绕圈决策来说,
     * 同一长度层里的候选点都属于“同一绕圈半径”,真正需要控制的是:
     * <p>
     * 1. 初次/前几次堵塞时,优先选择最短可达层,尽量用最小绕行距离恢复流转;
     * 2. 如果任务已经连续多次在同一区域绕圈,说明短半径候选大概率已经试过或恢复效果差,
     *    就需要逐步放大到更远一层;
     * 3. pathLenFactor 代表当前任务对“短路径/长路径”的偏好,允许在相同堵塞轮次下适度往更远层偏移。
     * <p>
     * 因此这里先把 candidateList 压缩成“按 pathLength 去重后的 tierList”,每个 tier 只保留该长度层的首个候选。
     * 然后同时计算两个层级索引:
     * <p>
     * - defaultTierIndex:基于 expectedLoopIssueCount 的默认放大层级;
     * - factorTierIndex:基于 pathLenFactor 的偏好层级;
     * <p>
     * 最终取两者较大值,含义是“至少满足当前堵塞轮次需要的放大半径,同时允许路径偏好把目标推向更远层级”。
     *
     * @param expectedLoopIssueCount 预计已发生的绕圈/堵塞轮次
     * @param candidateList 已按 pathLength、offset 排序的候选列表
     * @param pathLenFactor 当前任务的路径偏好系数
     * @return 最终选中的绕圈目标站;若没有候选则返回 null
     */
    private Integer resolveGradualCircleTargetByPathLength(Integer expectedLoopIssueCount,
                                                           List<CircleTargetCandidate> candidateList,
                                                           Double pathLenFactor) {
        if (candidateList == null || candidateList.isEmpty()) {
            return null;
        }
        List<CircleTargetCandidate> tierList = new ArrayList<>();
        Integer lastPathLength = null;
        for (CircleTargetCandidate candidate : candidateList) {
            if (candidate == null) {
                continue;
            }
            if (lastPathLength == null || !Objects.equals(lastPathLength, candidate.getPathLength())) {
                tierList.add(candidate);
                lastPathLength = candidate.getPathLength();
            }
        }
        if (tierList.isEmpty()) {
            return candidateList.get(0).getStationId();
        }
        int defaultTierIndex = expectedLoopIssueCount == null || expectedLoopIssueCount <= 2
                ? 0
                : Math.min(expectedLoopIssueCount - 2, tierList.size() - 1);
        int factorTierIndex = (int) Math.round(normalizePathLenFactor(pathLenFactor) * (tierList.size() - 1));
        int tierIndex = Math.max(defaultTierIndex, factorTierIndex);
        return tierList.get(tierIndex).getStationId();
    }
    private boolean tryAcquireOutOrderDispatchLock(Integer wrkNo, Integer stationId) {
        if (wrkNo == null || wrkNo <= 0 || stationId == null) {
            return true;
        }
        String key = RedisKeyType.STATION_OUT_ORDER_DISPATCH_LIMIT_.key + wrkNo + "_" + stationId;
        Object lock = redisUtil.get(key);
        if (lock != null) {
            return false;
        }
        redisUtil.set(key, "lock", OUT_ORDER_DISPATCH_LIMIT_SECONDS);
        return true;
    }
    private boolean shouldSkipOutOrderDispatchForExistingRoute(Integer wrkNo, Integer stationId) {
        if (stationMoveCoordinator == null || wrkNo == null || wrkNo <= 0 || stationId == null) {
            return false;
        }
        StationMoveSession session = stationMoveCoordinator.loadSession(wrkNo);
        if (session == null) {
            return false;
        }
        if (!session.isActive() || !session.containsStation(stationId)) {
            return false;
        }
        // 绕圈路线在当前站点尚未走完时,排序点不应再次插手。
        if (StationMoveDispatchMode.CIRCLE == session.getDispatchMode()) {
            return true;
        }
        // 直接路线只在“当前站点已经被别的活动路线占住且目标不同”时才拦截。
        return !Objects.equals(stationId, session.getCurrentRouteTargetStationId());
    }
    private boolean isWatchingCircleArrival(Integer wrkNo, Integer stationId) {
        if (stationMoveCoordinator != null) {
            StationMoveSession session = stationMoveCoordinator.loadSession(wrkNo);
            if (session != null && session.isActive() && stationId != null) {
                // nextDecisionStationId 表示这条路线真正等待重新决策的站点,到站才触发 watch-circle。
                if (stationId.equals(session.getNextDecisionStationId())) {
                    return true;
                }
                // 还在 session 路径中间站运行时不应误触发。
                if (session.containsStation(stationId)) {
                    return false;
                }
            }
        }
        StationCommand command = getWatchCircleCommand(wrkNo);
        return command != null && stationId != null && stationId.equals(command.getTargetStaNo());
    }
    private boolean isWatchingCircleTransit(Integer wrkNo, Integer stationId) {
        if (stationMoveCoordinator != null) {
            StationMoveSession session = stationMoveCoordinator.loadSession(wrkNo);
            if (session != null && session.isActive() && stationId != null) {
                if (stationId.equals(session.getNextDecisionStationId())) {
                    return false;
                }
                if (session.containsStation(stationId)) {
                    return true;
                }
            }
        }
        StationCommand command = getWatchCircleCommand(wrkNo);
        if (command == null || stationId == null || Objects.equals(stationId, command.getTargetStaNo())) {
            return false;
        }
        List<Integer> navigatePath = command.getNavigatePath();
        return navigatePath != null && navigatePath.contains(stationId);
    }
    private StationCommand getWatchCircleCommand(Integer wrkNo) {
        if (wrkNo == null || wrkNo <= 0) {
            return null;
        }
        Object circleObj = redisUtil.get(RedisKeyType.WATCH_CIRCLE_STATION_.key + wrkNo);
        if (circleObj == null) {
            return null;
        }
        try {
            return JSON.parseObject(circleObj.toString(), StationCommand.class);
        } catch (Exception ignore) {
            return null;
        }
    }
    private void saveWatchCircleCommand(Integer wrkNo, StationCommand command) {
        if (wrkNo == null || wrkNo <= 0 || command == null) {
            return;
        }
        redisUtil.set(RedisKeyType.WATCH_CIRCLE_STATION_.key + wrkNo,
                JSON.toJSONString(command, SerializerFeature.DisableCircularReferenceDetect), 60 * 60 * 24);
    }
    private void clearWatchCircleCommand(Integer wrkNo) {
        if (wrkNo == null || wrkNo <= 0) {
            return;
        }
        redisUtil.del(RedisKeyType.WATCH_CIRCLE_STATION_.key + wrkNo);
    }
    private void checkStationIdleRecover(BasDevp basDevp,
                                         StationThread stationThread,
                                         StationProtocol stationProtocol,
                                         List<Integer> outOrderList) {
        if (stationProtocol == null || stationProtocol.getTaskNo() == null || stationProtocol.getTaskNo() <= 0) {
            return;
        }
        if (!Objects.equals(stationProtocol.getStationId(), stationProtocol.getTargetStaNo())) {
            return;
        }
        StationTaskIdleTrack idleTrack = touchStationTaskIdleTrack(stationProtocol.getTaskNo(), stationProtocol.getStationId());
        if (shouldSkipIdleRecoverForRecentDispatch(stationProtocol.getTaskNo(), stationProtocol.getStationId())) {
            return;
        }
        if (idleTrack == null || !idleTrack.isTimeout(STATION_IDLE_RECOVER_SECONDS)) {
            return;
        }
        WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
        if (!canRecoverIdleStationTask(wrkMast, stationProtocol.getStationId())) {
            return;
        }
        Object lock = redisUtil.get(RedisKeyType.CHECK_STATION_IDLE_RECOVER_LIMIT_.key + stationProtocol.getTaskNo());
        if (lock != null) {
            return;
        }
        Double pathLenFactor = resolveOutboundPathLenFactor(wrkMast);
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.IDLE_RECOVER,
                basDevp,
                stationThread,
                stationProtocol,
                wrkMast,
                outOrderList,
                pathLenFactor,
                "checkStationIdleRecover"
        ).withCancelSessionBeforeDispatch()
                .withExecutionLock(RedisKeyType.CHECK_STATION_IDLE_RECOVER_LIMIT_.key + stationProtocol.getTaskNo(), STATION_IDLE_RECOVER_LIMIT_SECONDS)
                .withResetSegmentCommandsBeforeDispatch()
                .clearIdleIssuedCommands(idleTrack);
        executeSharedReroute(context);
        return stationRerouteProcessor.resolveSharedRerouteDecision(context);
    }
    boolean shouldUseRunBlockDirectReassign(WrkMast wrkMast,
                                            Integer stationId,
                                            List<Integer> runBlockReassignLocStationList) {
        return wrkMast != null
                && Objects.equals(wrkMast.getIoType(), WrkIoType.IN.id)
                && stationId != null
                && runBlockReassignLocStationList != null
                && runBlockReassignLocStationList.contains(stationId);
    }
    private void executeRunBlockDirectReassign(BasDevp basDevp,
                                               StationThread stationThread,
                                               StationProtocol stationProtocol,
                                               WrkMast wrkMast) {
        if (basDevp == null || stationThread == null || stationProtocol == null || wrkMast == null) {
            return;
        }
        int currentTaskBufferCommandCount = countCurrentTaskBufferCommands(
                stationProtocol.getTaskBufferItems(),
                stationProtocol.getTaskNo()
        );
        if (currentTaskBufferCommandCount > 0) {
            News.info("输送站点运行堵塞重分配已跳过,缓存区仍存在当前任务命令。站点号={},工作号={},当前任务命令数={}",
                    stationProtocol.getStationId(),
                    stationProtocol.getTaskNo(),
                    currentTaskBufferCommandCount);
            return;
        }
        if (stationMoveCoordinator != null) {
            stationMoveCoordinator.cancelSession(wrkMast.getWrkNo());
        }
        String response = wmsOperateUtils.applyReassignTaskLocNo(wrkMast.getWrkNo(), stationProtocol.getStationId());
        if (Cools.isEmpty(response)) {
            News.taskError(wrkMast.getWrkNo(), "请求WMS重新分配库位接口失败,接口未响应!!!response:{}", response);
            return;
        }
        JSONObject jsonObject = JSON.parseObject(response);
        if (!jsonObject.getInteger("code").equals(200)) {
            News.error("请求WMS接口失败!!!response:{}", response);
            return;
        }
        StartupDto dto = jsonObject.getObject("data", StartupDto.class);
        String sourceLocNo = wrkMast.getLocNo();
        String locNo = dto.getLocNo();
        LocMast sourceLocMast = locMastService.queryByLoc(sourceLocNo);
        if (sourceLocMast == null) {
            News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 源库位信息不存在", sourceLocNo);
            return;
        }
        if (!sourceLocMast.getLocSts().equals("S")) {
            News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 源库位状态不处于入库预约", sourceLocNo);
            return;
        }
        LocMast locMast = locMastService.queryByLoc(locNo);
        if (locMast == null) {
            News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 目标库位信息不存在", locNo);
            return;
        }
        if (!locMast.getLocSts().equals("O")) {
            News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 目标库位状态不处于空库位", locNo);
            return;
        }
        FindCrnNoResult findCrnNoResult = commonService.findCrnNoByLocNo(locNo);
        if (findCrnNoResult == null) {
            News.taskInfo(wrkMast.getWrkNo(), "{}工作,未匹配到堆垛机", wrkMast.getWrkNo());
            return;
        }
        Integer crnNo = findCrnNoResult.getCrnNo();
        Integer targetStationId = commonService.findInStationId(findCrnNoResult, stationProtocol.getStationId());
        if (targetStationId == null) {
            News.taskInfo(wrkMast.getWrkNo(), "{}站点,搜索入库站点失败", stationProtocol.getStationId());
            return;
        }
        StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), targetStationId, 0);
        if (command == null) {
            News.taskInfo(wrkMast.getWrkNo(), "{}工作,获取输送线命令失败", wrkMast.getWrkNo());
            return;
        }
        sourceLocMast.setLocSts("O");
        sourceLocMast.setModiTime(new Date());
        locMastService.updateById(sourceLocMast);
        locMast.setLocSts("S");
        locMast.setModiTime(new Date());
        locMastService.updateById(locMast);
        wrkMast.setLocNo(locNo);
        wrkMast.setStaNo(targetStationId);
        if (findCrnNoResult.getCrnType().equals(SlaveType.Crn)) {
            wrkMast.setCrnNo(crnNo);
        } else if (findCrnNoResult.getCrnType().equals(SlaveType.DualCrn)) {
            wrkMast.setDualCrnNo(crnNo);
        } else {
            throw new CoolException("未知设备类型");
        }
        if (!wrkMastService.updateById(wrkMast)) {
            return;
        }
        boolean offered = offerDevpCommandWithDedup(basDevp.getDevpNo(), command, "checkStationRunBlock_direct");
        if (!offered) {
            return;
        }
        if (stationMoveCoordinator != null) {
            stationMoveCoordinator.recordDispatch(
                    wrkMast.getWrkNo(),
                    stationProtocol.getStationId(),
                    "checkStationRunBlock_direct",
                    command,
                    false
            );
        }
    }
    private boolean canRecoverIdleStationTask(WrkMast wrkMast, Integer currentStationId) {
        if (wrkMast == null || currentStationId == null || wrkMast.getStaNo() == null) {
            return false;
        }
        if (Objects.equals(currentStationId, wrkMast.getStaNo())) {
            return false;
        }
        return Objects.equals(wrkMast.getWrkSts(), WrkStsType.INBOUND_STATION_RUN.sts)
                || Objects.equals(wrkMast.getWrkSts(), WrkStsType.STATION_RUN.sts);
        return stationRerouteProcessor.shouldUseRunBlockDirectReassign(wrkMast, stationId, runBlockReassignLocStationList);
    }
    private boolean shouldSkipIdleRecoverForRecentDispatch(Integer taskNo, Integer stationId) {
        if (taskNo == null || taskNo <= 0 || stationId == null) {
            return false;
        }
        long thresholdMs = STATION_IDLE_RECOVER_SECONDS * 1000L;
        StationMoveSession session = stationMoveCoordinator == null ? null : stationMoveCoordinator.loadSession(taskNo);
        if (session != null && session.isActive() && session.getLastIssuedAt() != null) {
            // 分段执行过程中,刚下发下一段命令时,session 的 currentStationId/dispatchStationId
            // 可能还没来得及和当前观察站点完全对齐;只要当前站点仍在这条活动路线里,
            // 就说明这次 recent dispatch 仍然和它相关,idle recover 不应在 10 秒窗口内再次介入。
            if (Objects.equals(stationId, session.getCurrentStationId())
                    || Objects.equals(stationId, session.getDispatchStationId())
                    || session.containsStation(stationId)) {
                long elapsedMs = System.currentTimeMillis() - session.getLastIssuedAt();
                if (elapsedMs < thresholdMs) {
                    saveStationTaskIdleTrack(new StationTaskIdleTrack(taskNo, stationId, System.currentTimeMillis()));
                    News.info("输送站点任务刚完成命令下发,已跳过停留重算。站点号={},工作号={},距上次下发={}ms,routeVersion={}",
                            stationId, taskNo, elapsedMs, session.getRouteVersion());
                    return true;
                }
            }
        }
        if (!hasRecentIssuedMoveCommand(taskNo, stationId, thresholdMs)) {
            return false;
        }
        saveStationTaskIdleTrack(new StationTaskIdleTrack(taskNo, stationId, System.currentTimeMillis()));
        News.info("输送站点任务刚完成命令下发,已跳过停留重算。站点号={},工作号={},距最近命令下发<{}ms,routeVersion={}",
                stationId, taskNo, thresholdMs, session == null ? null : session.getRouteVersion());
        return true;
        return stationRerouteProcessor.shouldSkipIdleRecoverForRecentDispatch(taskNo, stationId);
    }
    private boolean hasRecentIssuedMoveCommand(Integer taskNo, Integer stationId, long thresholdMs) {
        if (taskNo == null || taskNo <= 0 || stationId == null || thresholdMs <= 0L || basStationOptService == null) {
            return false;
        }
        Date thresholdTime = new Date(System.currentTimeMillis() - thresholdMs);
        List<BasStationOpt> optList = basStationOptService.list(new QueryWrapper<BasStationOpt>()
                .select("id")
                .eq("task_no", taskNo)
                .eq("station_id", stationId)
                .eq("mode", String.valueOf(StationCommandType.MOVE))
                .eq("send", 1)
                .ge("send_time", thresholdTime)
                .orderByDesc("send_time")
                .last("limit 1"));
        return optList != null && !optList.isEmpty();
    }
    private void resetSegmentMoveCommandsBeforeReroute(Integer taskNo) {
        if (redisUtil == null || taskNo == null || taskNo <= 0) {
            return;
        }
        String key = RedisKeyType.DEVICE_STATION_MOVE_RESET.key + taskNo;
        redisUtil.set(key, "cancel", 3);
        try {
            Thread.sleep(STATION_MOVE_RESET_WAIT_MS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (Exception ignore) {
        }
        redisUtil.del(key);
    }
    private int countCurrentTaskBufferCommands(List<StationTaskBufferItem> taskBufferItems, Integer currentTaskNo) {
        if (taskBufferItems == null || taskBufferItems.isEmpty() || currentTaskNo == null || currentTaskNo <= 0) {
            return 0;
        }
        int count = 0;
        for (StationTaskBufferItem item : taskBufferItems) {
            if (item == null || item.getTaskNo() == null) {
                continue;
            }
            if (currentTaskNo.equals(item.getTaskNo())) {
                count++;
            }
        }
        return count;
    }
    private boolean offerDevpCommandWithDedup(Integer deviceNo, StationCommand command, String scene) {
        if (deviceNo == null || command == null) {
            return false;
        }
        String dedupKey = buildStationCommandDispatchDedupKey(deviceNo, command);
        if (redisUtil != null) {
            Object lock = redisUtil.get(dedupKey);
            if (lock != null) {
                News.info("输送站点命令短时重复派发,已跳过。scene={},deviceNo={},taskNo={},stationId={},targetStaNo={},commandType={}",
                        scene,
                        deviceNo,
                        command.getTaskNo(),
                        command.getStationId(),
                        command.getTargetStaNo(),
                        command.getCommandType());
                return false;
            }
            redisUtil.set(dedupKey, "lock", STATION_COMMAND_DISPATCH_DEDUP_SECONDS);
        }
        boolean offered = MessageQueue.offer(SlaveType.Devp, deviceNo, new Task(2, command));
        if (!offered && redisUtil != null) {
            redisUtil.del(dedupKey);
        }
        return offered;
    }
    private String buildStationCommandDispatchDedupKey(Integer deviceNo, StationCommand command) {
        return RedisKeyType.STATION_COMMAND_DISPATCH_DEDUP_.key
                + deviceNo + "_"
                + command.getTaskNo() + "_"
                + command.getStationId() + "_"
                + (stationMoveCoordinator == null ? Integer.toHexString(buildFallbackPathSignature(command).hashCode())
                : stationMoveCoordinator.buildPathSignatureHash(command));
    }
    private String buildFallbackPathSignature(StationCommand command) {
        if (command == null) {
            return "";
        }
        return String.valueOf(command.getCommandType())
                + "_" + command.getStationId()
                + "_" + command.getTargetStaNo()
                + "_" + command.getNavigatePath()
                + "_" + command.getLiftTransferPath()
                + "_" + command.getOriginalNavigatePath();
    }
    private int clearIssuedMoveCommandsDuringIdleStay(StationTaskIdleTrack idleTrack,
                                                      Integer taskNo,
                                                      Integer stationId) {
        if (basStationOptService == null) {
            return 0;
        }
        List<BasStationOpt> optList;
        try {
            optList = listIssuedMoveCommandsDuringIdleStay(idleTrack, taskNo);
        } catch (Exception e) {
            return 0;
        }
        if (optList == null || optList.isEmpty()) {
            return 0;
        }
        Date now = new Date();
        String cleanupMemo = buildIdleRecoverClearedMemo(stationId);
        int clearedCount = 0;
        for (BasStationOpt opt : optList) {
            if (opt == null || opt.getId() == null) {
                continue;
            }
            opt.setSend(0);
            opt.setUpdateTime(now);
            opt.setMemo(appendCleanupMemo(opt.getMemo(), cleanupMemo));
            clearedCount++;
        }
        if (clearedCount > 0) {
            basStationOptService.updateBatchById(optList);
        }
        return clearedCount;
    }
    private List<BasStationOpt> listIssuedMoveCommandsDuringIdleStay(StationTaskIdleTrack idleTrack,
                                                                     Integer taskNo) {
        if (idleTrack == null || taskNo == null || taskNo <= 0 || idleTrack.firstSeenTime == null || basStationOptService == null) {
            return Collections.emptyList();
        }
        List<BasStationOpt> optList = basStationOptService.list(new QueryWrapper<BasStationOpt>()
                .select("id", "task_no", "send_time", "target_station_id", "memo", "send")
                .eq("task_no", taskNo)
                .eq("mode", String.valueOf(StationCommandType.MOVE))
                .eq("send", 1)
                .ge("send_time", new Date(idleTrack.firstSeenTime))
                .orderByAsc("send_time"));
        if (optList == null || optList.isEmpty()) {
            return Collections.emptyList();
        }
        return optList;
    }
    private String buildIdleRecoverClearedMemo(Integer stationId) {
        if (stationId == null) {
            return IDLE_RECOVER_CLEARED_MEMO;
        }
        return IDLE_RECOVER_CLEARED_MEMO + "(stationId=" + stationId + ")";
    }
    private String appendCleanupMemo(String memo, String cleanupMemo) {
        if (Cools.isEmpty(cleanupMemo)) {
            return memo;
        }
        if (Cools.isEmpty(memo)) {
            return cleanupMemo;
        }
        if (memo.contains(cleanupMemo)) {
            return memo;
        }
        return memo + " | " + cleanupMemo;
    }
    private StationTaskIdleTrack touchStationTaskIdleTrack(Integer taskNo, Integer stationId) {
        if (taskNo == null || taskNo <= 0 || stationId == null) {
            return null;
        }
        long now = System.currentTimeMillis();
        StationTaskIdleTrack idleTrack = getStationTaskIdleTrack(taskNo);
        if (idleTrack == null || !Objects.equals(idleTrack.stationId, stationId)) {
            idleTrack = new StationTaskIdleTrack(taskNo, stationId, now);
            saveStationTaskIdleTrack(idleTrack);
        }
        return idleTrack;
    }
    private StationTaskIdleTrack getStationTaskIdleTrack(Integer taskNo) {
        if (taskNo == null || taskNo <= 0) {
            return null;
        }
        Object obj = redisUtil.get(RedisKeyType.STATION_TASK_IDLE_TRACK_.key + taskNo);
        if (obj == null) {
            return null;
        }
        try {
            return JSON.parseObject(obj.toString(), StationTaskIdleTrack.class);
        } catch (Exception e) {
            return null;
        }
    }
    private void saveStationTaskIdleTrack(StationTaskIdleTrack idleTrack) {
        if (idleTrack == null || idleTrack.taskNo == null || idleTrack.taskNo <= 0) {
            return;
        }
        redisUtil.set(
                RedisKeyType.STATION_TASK_IDLE_TRACK_.key + idleTrack.taskNo,
                JSON.toJSONString(idleTrack, SerializerFeature.DisableCircularReferenceDetect),
                STATION_IDLE_TRACK_EXPIRE_SECONDS
        );
    }
    /**
     * 沿“源站 -> 目标站”的理论路径,从当前站点往下游回看,找出同批次任务在后续站点上的已知序号。
     * <p>
     * 严格窗口控制要回答的问题不是“当前任务自己的 batchSeq 是多少”,而是:
     * “在当前站点后面,沿这条出库链路已经排着的同批次任务,最靠近目标端的序号是多少?”
     * 只有拿到这个序号,排序点才能判断当前任务是否应该紧跟其后放行。
     * <p>
     * 具体做法:
     * <p>
     * 1. 按 pathList 从尾到头回扫,截出 searchStationId 之后的全部下游站点;
     * 2. 读取这些站点当前正在执行的任务号;
     * 3. 如果某站点上的任务属于 searchBatch,就记录它的 batchSeq;
     * 4. 返回该批次在下游已知的序号。
     * <p>
     * 这里返回的是“路径下游现场已出现的批次序号”,不是批次理论最小/最大值。
     * 如果下游没有同批次任务,返回 null,调用方需要退回到“当前批次首个未完成任务是否就是自己”的判定。
     *
     * @param pathList 从任务源站到目标站的理论出库路径
     * @param searchStationId 当前正在做排序决策的站点
     * @param searchBatch 当前任务所属批次
     * @return 下游同批次任务的已知序号;若路径下游尚未出现该批次则返回 null
     */
    public Integer getOutStationBatchSeq(List<NavigateNode> pathList, Integer searchStationId, String searchBatch) {
        if (pathList == null || pathList.isEmpty() || searchStationId == null || Cools.isEmpty(searchBatch)) {
            return null;
        }
        // 只关心当前站点之后的下游站点,当前站点之前的节点不会影响“谁应该排在我前面”。
        List<Integer> checkList = new ArrayList<>();
        for (int i = pathList.size() - 1; i >= 0; i--) {
            NavigateNode node = pathList.get(i);
            JSONObject v = JSONObject.parseObject(node.getNodeValue());
            if (v != null) {
                Integer stationId = v.getInteger("stationId");
                if (searchStationId.equals(stationId)) {
                    break;
                } else {
                    checkList.add(stationId);
                }
            }
        }
        // 下游站点可能同时挂着多个不同批次任务;这里只抽取与当前批次相关的现场序号快照。
        HashMap<String, Integer> batchMap = new HashMap<>();
        for (Integer station : checkList) {
            BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>().eq("station_id", station));
            if (basStation == null) {
                continue;
            }
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basStation.getDeviceNo());
            if (stationThread == null) {
                continue;
            }
            Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
            StationProtocol checkStationProtocol = statusMap.get(station);
            if (checkStationProtocol == null) {
                continue;
            }
            if (checkStationProtocol.getTaskNo() > 0) {
                WrkMast checkWrkMast = wrkMastService.selectByWorkNo(checkStationProtocol.getTaskNo());
                if (checkWrkMast == null) {
                    continue;
                }
                if (!Cools.isEmpty(checkWrkMast.getBatch())) {
                    batchMap.put(checkWrkMast.getBatch(), checkWrkMast.getBatchSeq());
                }
            }
        }
        Integer seq = batchMap.get(searchBatch);
        return seq;
    }
    private int countCurrentStationTask() {
        int currentStationTaskCount = 0;
        List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
            if (stationThread == null) {
                continue;
            }
            for (StationProtocol stationProtocol : stationThread.getStatus()) {
                if (stationProtocol.getTaskNo() > 0) {
                    currentStationTaskCount++;
                }
            }
        }
        return currentStationTaskCount;
    }
    private boolean isDispatchBlocked(DispatchLimitConfig config,
                                      int currentStationTaskCount,
                                      LoadGuardState loadGuardState,
                                      boolean needReserveLoopLoad) {
        if (config.loopModeEnable) {
            double currentLoad = loadGuardState.currentLoad();
            if (currentLoad >= config.circleMaxLoadLimit) {
                News.warn("当前承载量达到上限,已停止站点任务下发。当前承载量={},上限={}", formatPercent(currentLoad), formatPercent(config.circleMaxLoadLimit));
                return true;
            }
            if (needReserveLoopLoad) {
                double reserveLoad = loadGuardState.loadAfterReserve();
                if (reserveLoad >= config.circleMaxLoadLimit) {
                    News.warn("预占后承载量达到上限,已停止站点任务下发。预占后承载量={},上限={}", formatPercent(reserveLoad), formatPercent(config.circleMaxLoadLimit));
                    return true;
                }
            }
        }
        return false;
    }
    private LoadGuardState buildLoadGuardState(DispatchLimitConfig config) {
        LoadGuardState state = new LoadGuardState();
        if (!config.loopModeEnable) {
            return state;
        }
        StationCycleCapacityVo capacityVo = stationCycleCapacityService.getLatestSnapshot();
        if (capacityVo == null) {
            return state;
        }
        state.totalStationCount = toNonNegative(capacityVo.getTotalStationCount());
        Integer occupiedStationCount = capacityVo.getOccupiedStationCount();
        state.projectedTaskStationCount = toNonNegative(occupiedStationCount != null ? occupiedStationCount : capacityVo.getTaskStationCount());
        List<StationCycleLoopVo> loopList = capacityVo.getLoopList();
        if (loopList != null) {
            for (StationCycleLoopVo loopVo : loopList) {
                if (loopVo == null || loopVo.getStationIdList() == null) {
                    continue;
                }
                Integer loopNo = loopVo.getLoopNo();
                for (Integer stationId : loopVo.getStationIdList()) {
                    if (stationId != null) {
                        if (loopNo != null) {
                            state.stationLoopNoMap.put(stationId, loopNo);
                        }
                    }
                }
            }
        }
        return state;
    }
    private LoopHitResult findPathLoopHit(DispatchLimitConfig config,
                                          Integer sourceStationId,
                                          Integer targetStationId,
                                          LoadGuardState loadGuardState) {
        return findPathLoopHit(config, sourceStationId, targetStationId, loadGuardState, null, null);
    }
    private LoopHitResult findPathLoopHit(DispatchLimitConfig config,
                                          Integer sourceStationId,
                                          Integer targetStationId,
                                          LoadGuardState loadGuardState,
                                          WrkMast wrkMast,
                                          Double pathLenFactor) {
        if (!config.loopModeEnable) {
            return LoopHitResult.NO_HIT;
        }
        if (sourceStationId == null || targetStationId == null) {
            return LoopHitResult.NO_HIT;
        }
        if (loadGuardState.stationLoopNoMap.isEmpty()) {
            return LoopHitResult.NO_HIT;
        }
        try {
            List<NavigateNode> nodes = wrkMast == null
                    ? navigateUtils.calcByStationId(sourceStationId, targetStationId)
                    : calcOutboundNavigatePath(wrkMast, sourceStationId, targetStationId, pathLenFactor);
            if (nodes == null || nodes.isEmpty()) {
                return LoopHitResult.NO_HIT;
            }
            for (NavigateNode node : nodes) {
                Integer stationId = getStationIdFromNode(node);
                if (stationId == null) {
                    continue;
                }
                Integer loopNo = loadGuardState.stationLoopNoMap.get(stationId);
                if (loopNo != null) {
                    return new LoopHitResult(true, loopNo, stationId);
                }
            }
        } catch (Exception e) {
            return LoopHitResult.NO_HIT;
        }
        return LoopHitResult.NO_HIT;
    }
    private Integer getStationIdFromNode(NavigateNode node) {
        if (node == null || isBlank(node.getNodeValue())) {
            return null;
        }
        try {
            JSONObject v = JSONObject.parseObject(node.getNodeValue());
            if (v == null) {
                return null;
            }
            return v.getInteger("stationId");
        } catch (Exception e) {
            return null;
        }
    }
    private int toNonNegative(Integer value) {
        if (value == null || value < 0) {
            return 0;
        }
        return value;
    }
    private Double normalizePathLenFactor(Double pathLenFactor) {
        if (pathLenFactor == null || pathLenFactor < 0.0d) {
            return 0.0d;
        }
        if (pathLenFactor > 1.0d) {
            return 1.0d;
        }
        return pathLenFactor;
    }
    enum RerouteSceneType {
        RUN_BLOCK_REROUTE,
        IDLE_RECOVER,
        OUT_ORDER,
        WATCH_CIRCLE
    }
    static final class RerouteDecision {
        private final boolean skip;
        private final String skipReason;
        private final Integer targetStationId;
        private final OutOrderDispatchDecision dispatchDecision;
        private RerouteDecision(boolean skip,
                                String skipReason,
                                Integer targetStationId,
                                OutOrderDispatchDecision dispatchDecision) {
            this.skip = skip;
            this.skipReason = skipReason;
            this.targetStationId = targetStationId;
            this.dispatchDecision = dispatchDecision;
        }
        static RerouteDecision skip(String reason) {
            return new RerouteDecision(true, reason, null, null);
        }
        static RerouteDecision proceed(Integer targetStationId) {
            return new RerouteDecision(false, null, targetStationId, null);
        }
        static RerouteDecision proceed(Integer targetStationId,
                                       OutOrderDispatchDecision dispatchDecision) {
            return new RerouteDecision(false, null, targetStationId, dispatchDecision);
        }
        boolean skip() {
            return skip;
        }
        String skipReason() {
            return skipReason;
        }
        Integer targetStationId() {
            return targetStationId;
        }
        OutOrderDispatchDecision dispatchDecision() {
            return dispatchDecision;
        }
    }
    /**
     * 重算/重发链路的一次性执行上下文。
     *
     * <p>这个对象只在一次 reroute 执行过程中存在,不会落库,也不会长期缓存。
     * 它的职责不是表达业务状态,而是把“这次为什么进来、当前用哪套运行时对象、
     * 下发前后要不要启用额外控制逻辑”集中打包,供统一执行链路使用。
     *
     * <p>统一执行链路大致分三段:
     * 1. {@code resolveSharedRerouteDecision} 根据当前任务和场景先决策目标站。
     * 2. {@code buildRerouteCommandPlan} 决定用普通出库算路还是 run-block 专用算路。
     * 3. {@code executeReroutePlan} 按上下文里的开关决定是否做 suppress、是否加锁、
     *    是否先 cancel 旧 session、是否先清旧分段命令,然后真正下发。
     *
     * <p>字段可以分成四组理解:
     * 1. 场景与运行时对象:
     *    {@code sceneType} / {@code basDevp} / {@code stationThread} /
     *    {@code stationProtocol} / {@code wrkMast}
     *    表示“谁在什么场景下触发了这次重算”。
     * 2. 目标决策输入:
     *    {@code outOrderStationIds} / {@code pathLenFactor}
     *    表示“目标站如何算、路径倾向系数是多少”。
     * 3. 下发目标信息:
     *    {@code dispatchScene} / {@code dispatchDeviceNo}
     *    表示“这次命令最终往哪个输送设备队列发,以及日志/去重场景名是什么”。
     * 4. 执行控制开关:
     *    {@code useRunBlockCommand} / {@code checkSuppressDispatch} /
     *    {@code requireOutOrderDispatchLock} / {@code cancelSessionBeforeDispatch} /
     *    {@code resetSegmentCommandsBeforeDispatch} / {@code clearIdleIssuedCommands} /
     *    {@code checkRecentDispatch} / {@code executionLockKey} / {@code executionLockSeconds}
     *    表示“真正执行前后要打开哪些保护动作”。
     *
     * <p>它本质上是一个参数对象加布尔开关集合:
     * {@code create(...)} 先把这次 reroute 的基础现场填进来,
     * 再通过 {@code withXxx(...)} 逐项声明这次执行需要附加哪些控制语义。
     *
     * <p>例如:
     * RUN_BLOCK_REROUTE 会打开 {@code useRunBlockCommand},
     * 并要求在下发前先 {@code cancelSession}、先清旧分段命令;
     * OUT_ORDER 会打开 suppress guard 和 out-order lock;
     * IDLE_RECOVER 则会打开 recent dispatch guard,并记录/清理停留期间已下发命令。
     */
    static final class RerouteContext {
        // 本次 reroute 的触发来源。决定后面走哪类目标裁决、日志文案和特殊保护分支。
        private final RerouteSceneType sceneType;
        // 当前输送设备配置,主要用于拿默认下发设备号和相关站点配置。
        private final BasDevp basDevp;
        // 当前站点线程,后面构造输送命令时直接依赖它取 command。
        private final StationThread stationThread;
        // 触发这次 reroute 的现场站点状态快照,包含 stationId/taskNo/runBlock/targetStaNo 等运行时信息。
        private final StationProtocol stationProtocol;
        // 当前工作主表记录,表示这次重算对应的任务主状态。
        private final WrkMast wrkMast;
        // 当前设备的出库排序点列表。目标站决策时需要知道哪些站点属于 out-order 节点。
        private final List<Integer> outOrderStationIds;
        // 路径长度倾向系数。批次出库时用于让不同任务对路径选择有稳定偏好。
        private final Double pathLenFactor;
        // 这次派发在日志、去重、session 记录中使用的场景名,例如 checkStationRunBlock_reroute。
        private final String dispatchScene;
        // 实际投递命令的设备号。默认取 basDevp.getDevpNo(),某些场景可显式覆盖。
        private Integer dispatchDeviceNo;
        // true 表示命令构造阶段改走 stationThread.getRunBlockRerouteCommand,而不是普通出库算路。
        private boolean useRunBlockCommand;
        // true 表示执行前要先做 active-session suppress,避免旧活动路线被重复插入新命令。
        private boolean checkSuppressDispatch;
        // true 表示执行前要加 out-order 专用短锁,防止同一排序点短时间重复计算/下发。
        private boolean requireOutOrderDispatchLock;
        // true 表示真正下发前先取消旧 session。通常用于 reroute 替换旧路线。
        private boolean cancelSessionBeforeDispatch;
        // true 表示真正下发前先清理旧分段输送命令,避免 segment executor 还持有旧路线。
        private boolean resetSegmentCommandsBeforeDispatch;
        // true 表示要清理 idle recover 期间已经下发过但未实际生效的旧命令痕迹。
        private boolean clearIdleIssuedCommands;
        // true 表示执行前要检查“最近刚下发过”,用于 idle recover 避免刚发完就重算。
        private boolean checkRecentDispatch;
        // 可选执行锁 key。用于给某个 reroute 场景加短时间互斥。
        private String executionLockKey;
        // executionLockKey 对应的锁秒数。
        private int executionLockSeconds;
        // 仅 idle recover 需要,记录停留跟踪上下文,供清理旧命令与更新时间使用。
        private StationTaskIdleTrack idleTrack;
        private RerouteContext(RerouteSceneType sceneType,
                               BasDevp basDevp,
                               StationThread stationThread,
                               StationProtocol stationProtocol,
                               WrkMast wrkMast,
                               List<Integer> outOrderStationIds,
                               Double pathLenFactor,
                               String dispatchScene) {
            this.sceneType = sceneType;
            this.basDevp = basDevp;
            this.stationThread = stationThread;
            this.stationProtocol = stationProtocol;
            this.wrkMast = wrkMast;
            this.outOrderStationIds = outOrderStationIds == null ? Collections.emptyList() : outOrderStationIds;
            this.pathLenFactor = pathLenFactor;
            this.dispatchScene = dispatchScene;
            this.dispatchDeviceNo = basDevp == null ? null : basDevp.getDevpNo();
        }
        static RerouteContext create(RerouteSceneType sceneType,
                                     BasDevp basDevp,
                                     StationThread stationThread,
                                     StationProtocol stationProtocol,
                                     WrkMast wrkMast,
                                     List<Integer> outOrderStationIds,
                                     Double pathLenFactor,
                                     String dispatchScene) {
            // create 只负责填基础现场,不默认打开任何执行开关。
            // 每个场景后面通过 withXxx 明确声明自己需要哪些附加控制。
            return new RerouteContext(sceneType, basDevp, stationThread, stationProtocol, wrkMast, outOrderStationIds, pathLenFactor, dispatchScene);
        }
        RerouteContext withDispatchDeviceNo(Integer dispatchDeviceNo) {
            // 覆盖默认下发设备号。典型场景是 out-order 站点配置的 deviceNo 与 basDevp 默认值不同。
            this.dispatchDeviceNo = dispatchDeviceNo;
            return this;
        }
        RerouteContext withRunBlockCommand() {
            // 命令构造阶段切换到 run-block 专用算路器。
            // 目标站仍由统一决策逻辑决定,只是“去目标站的路径”改为堵塞重规划算法生成。
            this.useRunBlockCommand = true;
            return this;
        }
        RerouteContext withSuppressDispatchGuard() {
            // 执行前启用 session suppress:
            // 如果当前 task 在当前位置已经有一条活动中的同路径/同覆盖范围路线,则本次不再重复派发。
            this.checkSuppressDispatch = true;
            return this;
        }
        RerouteContext withOutOrderDispatchLock() {
            // 执行前启用排序点短锁。
            // 主要防止同一个 out-order/watch-circle 触发点在极短时间内被并发重复重算。
            this.requireOutOrderDispatchLock = true;
            return this;
        }
        RerouteContext withCancelSessionBeforeDispatch() {
            // 执行前显式取消旧 session。
            // 语义是“本次命令准备替换旧路线”,旧 routeVersion 之后不应再继续推进。
            this.cancelSessionBeforeDispatch = true;
            return this;
        }
        RerouteContext withResetSegmentCommandsBeforeDispatch() {
            // 执行前清掉 segment executor 侧旧分段命令。
            // 这对 run-block/idle recover 很关键,否则系统可能还拿着旧 segment 状态阻断新路线。
            this.resetSegmentCommandsBeforeDispatch = true;
            return this;
        }
        RerouteContext clearIdleIssuedCommands(StationTaskIdleTrack idleTrack) {
            // 仅 idle recover 使用:
            // 表示重启前要把“停留期间已经发过但可能未真正执行的命令痕迹”清理掉。
            this.clearIdleIssuedCommands = true;
            this.idleTrack = idleTrack;
            return this;
        }
        RerouteContext withRecentDispatchGuard() {
            // 执行前检查最近是否刚下发过。
            // 避免 idle recover 在“刚重发完”的窗口内又马上触发一次。
            this.checkRecentDispatch = true;
            return this;
        }
        RerouteContext withExecutionLock(String executionLockKey, int executionLockSeconds) {
            // 为某个场景挂一个独立执行锁。
            // 和 out-order lock 不同,这里是泛化锁,谁传 key 谁负责定义锁语义。
            this.executionLockKey = executionLockKey;
            this.executionLockSeconds = executionLockSeconds;
            return this;
        }
        RerouteSceneType sceneType() {
            return sceneType;
        }
        BasDevp basDevp() {
            return basDevp;
        }
        StationThread stationThread() {
            return stationThread;
        }
        StationProtocol stationProtocol() {
            return stationProtocol;
        }
        WrkMast wrkMast() {
            return wrkMast;
        }
        List<Integer> outOrderStationIds() {
            return outOrderStationIds;
        }
        Double pathLenFactor() {
            return pathLenFactor;
        }
        String dispatchScene() {
            return dispatchScene;
        }
        Integer dispatchDeviceNo() {
            return dispatchDeviceNo;
        }
        boolean useRunBlockCommand() {
            return useRunBlockCommand;
        }
        boolean checkSuppressDispatch() {
            return checkSuppressDispatch;
        }
        boolean requireOutOrderDispatchLock() {
            return requireOutOrderDispatchLock;
        }
        boolean cancelSessionBeforeDispatch() {
            return cancelSessionBeforeDispatch;
        }
        boolean resetSegmentCommandsBeforeDispatch() {
            return resetSegmentCommandsBeforeDispatch;
        }
        boolean clearIdleIssuedCommands() {
            return clearIdleIssuedCommands;
        }
        boolean checkRecentDispatch() {
            return checkRecentDispatch;
        }
        String executionLockKey() {
            return executionLockKey;
        }
        int executionLockSeconds() {
            return executionLockSeconds;
        }
        StationTaskIdleTrack idleTrack() {
            return idleTrack;
        }
    }
    static final class RerouteCommandPlan {
        private final boolean skip;
        private final String skipReason;
        private final StationCommand command;
        private final RerouteDecision decision;
        private final String dispatchScene;
        private RerouteCommandPlan(boolean skip,
                                   String skipReason,
                                   StationCommand command,
                                   RerouteDecision decision,
                                   String dispatchScene) {
            this.skip = skip;
            this.skipReason = skipReason;
            this.command = command;
            this.decision = decision;
            this.dispatchScene = dispatchScene;
        }
        static RerouteCommandPlan skip(String reason) {
            return new RerouteCommandPlan(true, reason, null, null, null);
        }
        static RerouteCommandPlan dispatch(StationCommand command,
                                           RerouteDecision decision,
                                           String dispatchScene) {
            return new RerouteCommandPlan(false, null, command, decision, dispatchScene);
        }
        boolean skip() {
            return skip;
        }
        String skipReason() {
            return skipReason;
        }
        StationCommand command() {
            return command;
        }
        RerouteDecision decision() {
            return decision;
        }
        String dispatchScene() {
            return dispatchScene;
        }
    }
    static final class RerouteExecutionResult {
        private final boolean skipped;
        private final String skipReason;
        private final boolean dispatched;
        private final StationCommand command;
        private final int clearedCommandCount;
        private RerouteExecutionResult(boolean skipped,
                                       String skipReason,
                                       boolean dispatched,
                                       StationCommand command,
                                       int clearedCommandCount) {
            this.skipped = skipped;
            this.skipReason = skipReason;
            this.dispatched = dispatched;
            this.command = command;
            this.clearedCommandCount = clearedCommandCount;
        }
        static RerouteExecutionResult skip(String reason) {
            return new RerouteExecutionResult(true, reason, false, null, 0);
        }
        static RerouteExecutionResult dispatched(StationCommand command,
                                                 int clearedCommandCount) {
            return new RerouteExecutionResult(false, null, true, command, clearedCommandCount);
        }
        boolean skipped() {
            return skipped;
        }
        String skipReason() {
            return skipReason;
        }
        boolean dispatched() {
            return dispatched;
        }
        StationCommand command() {
            return command;
        }
        int clearedCommandCount() {
            return clearedCommandCount;
        }
    }
    private static class OutOrderDispatchDecision {
        private final Integer targetStationId;
        private final boolean circle;
        private final StationTaskLoopService.LoopEvaluation loopEvaluation;
        private final boolean countLoopIssue;
        private OutOrderDispatchDecision(Integer targetStationId, boolean circle) {
            this(targetStationId, circle, null, false);
        }
        private OutOrderDispatchDecision(Integer targetStationId,
                                         boolean circle,
                                         StationTaskLoopService.LoopEvaluation loopEvaluation,
                                         boolean countLoopIssue) {
            this.targetStationId = targetStationId;
            this.circle = circle;
            this.loopEvaluation = loopEvaluation;
            this.countLoopIssue = countLoopIssue;
        }
        private Integer getTargetStationId() {
            return targetStationId;
        }
        private boolean isCircle() {
            return circle;
        }
        private StationTaskLoopService.LoopEvaluation getLoopEvaluation() {
            return loopEvaluation;
        }
        private boolean shouldCountLoopIssue() {
            return countLoopIssue;
        }
    }
    private static class CircleTargetCandidate {
        private final Integer stationId;
        private final Integer pathLength;
        private final Integer offset;
        private CircleTargetCandidate(Integer stationId, Integer pathLength, Integer offset) {
            this.stationId = stationId;
            this.pathLength = pathLength == null ? 0 : pathLength;
            this.offset = offset == null ? 0 : offset;
        }
        private Integer getStationId() {
            return stationId;
        }
        private Integer getPathLength() {
            return pathLength;
        }
        private Integer getOffset() {
            return offset;
        }
    }
    private void saveLoopLoadReserve(Integer wrkNo, LoopHitResult loopHitResult) {
        if (wrkNo == null || wrkNo <= 0 || loopHitResult == null || !loopHitResult.isThroughLoop()) {
            return;
        }
        JSONObject reserveJson = new JSONObject();
        reserveJson.put("wrkNo", wrkNo);
        reserveJson.put("loopNo", loopHitResult.getLoopNo());
        reserveJson.put("hitStationId", loopHitResult.getHitStationId());
        reserveJson.put("createTime", System.currentTimeMillis());
        redisUtil.hset(RedisKeyType.STATION_CYCLE_LOAD_RESERVE.key, String.valueOf(wrkNo), reserveJson.toJSONString());
        redisUtil.expire(RedisKeyType.STATION_CYCLE_LOAD_RESERVE.key, LOOP_LOAD_RESERVE_EXPIRE_SECONDS);
    }
    private DispatchLimitConfig getDispatchLimitConfig(Integer startStationId, Integer endStationId) {
        DispatchLimitConfig config = new DispatchLimitConfig();
        Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
        if (systemConfigMapObj instanceof Map) {
            Map<?, ?> systemConfigMap = (Map<?, ?>) systemConfigMapObj;
            config.circleMaxLoadLimit = parseLoadLimit(getConfigValue(systemConfigMap, "circleMaxLoadLimit"), config.circleMaxLoadLimit);
            String loopModeValue = getConfigValue(systemConfigMap, "circleLoopModeEnable");
            if (isBlank(loopModeValue)) {
                loopModeValue = getConfigValue(systemConfigMap, "circleModeEnable");
            }
            if (isBlank(loopModeValue)) {
                loopModeValue = getConfigValue(systemConfigMap, "isCircleMode");
            }
            config.loopModeEnable = parseBoolean(loopModeValue, config.loopModeEnable);
        }
        if (stationPathPolicyService != null && startStationId != null && endStationId != null) {
            try {
                StationPathResolvedPolicy resolvedPolicy = stationPathPolicyService.resolvePolicy(startStationId, endStationId);
                if (resolvedPolicy != null && resolvedPolicy.getProfileConfig() != null) {
                    config.circleMaxLoadLimit = parseLoadLimit(String.valueOf(resolvedPolicy.getProfileConfig().getCircleMaxLoadLimit()), config.circleMaxLoadLimit);
                }
            } catch (Exception ignore) {
            }
        }
        return config;
    }
    private String getConfigValue(Map<?, ?> configMap, String key) {
        Object value = configMap.get(key);
        if (value == null) {
            return null;
        }
        return String.valueOf(value).trim();
    }
    private boolean parseBoolean(String value, boolean defaultValue) {
        if (isBlank(value)) {
            return defaultValue;
        }
        String lowValue = value.toLowerCase(Locale.ROOT);
        if ("y".equals(lowValue) || "yes".equals(lowValue) || "true".equals(lowValue)
                || "1".equals(lowValue) || "on".equals(lowValue)) {
            return true;
        }
        if ("n".equals(lowValue) || "no".equals(lowValue) || "false".equals(lowValue)
                || "0".equals(lowValue) || "off".equals(lowValue)) {
            return false;
        }
        return defaultValue;
    }
    private double parseLoadLimit(String value, double defaultValue) {
        if (isBlank(value)) {
            return defaultValue;
        }
        try {
            String normalized = value.replace("%", "").trim();
            double parsed = Double.parseDouble(normalized);
            if (parsed > 1.0) {
                parsed = parsed / 100.0;
            }
            if (parsed < 0.0) {
                return 0.0;
            }
            if (parsed > 1.0) {
                return 1.0;
            }
            return parsed;
        } catch (Exception e) {
            return defaultValue;
        }
    }
    private int parseInt(String value, int defaultValue) {
        if (isBlank(value)) {
            return defaultValue;
        }
        try {
            int parsed = Integer.parseInt(value.trim());
            return parsed < 0 ? defaultValue : parsed;
        } catch (Exception e) {
            return defaultValue;
        }
    }
    private String formatPercent(double value) {
        return String.format(Locale.ROOT, "%.1f%%", value * 100.0);
    }
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
    private static class DispatchLimitConfig {
        // 圈最大承载能力,默认80%
        private double circleMaxLoadLimit = 0.8d;
        // 是否启用绕圈模式(仅启用时才生效承载限制)
        private boolean loopModeEnable = false;
    }
    private static class LoadGuardState {
        private int totalStationCount = 0;
        private int projectedTaskStationCount = 0;
        private final Map<Integer, Integer> stationLoopNoMap = new HashMap<>();
        private double currentLoad() {
            return calcLoad(this.projectedTaskStationCount, this.totalStationCount);
        }
        private double loadAfterReserve() {
            return calcLoad(this.projectedTaskStationCount + 1, this.totalStationCount);
        }
        private void reserveLoopTask(Integer loopNo) {
            if (loopNo == null || loopNo <= 0) {
                return;
            }
            if (this.totalStationCount <= 0) {
                return;
            }
            this.projectedTaskStationCount++;
        }
        private double calcLoad(int taskCount, int stationCount) {
            if (stationCount <= 0 || taskCount <= 0) {
                return 0.0;
            }
            double load = (double) taskCount / (double) stationCount;
            if (load < 0.0) {
                return 0.0;
            }
            if (load > 1.0) {
                return 1.0;
            }
            return load;
        }
    }
    private static class LoopHitResult {
        private static final LoopHitResult NO_HIT = new LoopHitResult(false, null, null);
        private final boolean throughLoop;
        private final Integer loopNo;
        private final Integer hitStationId;
        private LoopHitResult(boolean throughLoop, Integer loopNo, Integer hitStationId) {
            this.throughLoop = throughLoop;
            this.loopNo = loopNo;
            this.hitStationId = hitStationId;
        }
        private boolean isThroughLoop() {
            return throughLoop;
        }
        private Integer getLoopNo() {
            return loopNo;
        }
        private Integer getHitStationId() {
            return hitStationId;
        }
    }
    private static class StationTaskIdleTrack {
        private Integer taskNo;
        private Integer stationId;
        private Long firstSeenTime;
        private StationTaskIdleTrack() {}
        private StationTaskIdleTrack(Integer taskNo, Integer stationId, Long firstSeenTime) {
            this.taskNo = taskNo;
            this.stationId = stationId;
            this.firstSeenTime = firstSeenTime;
        }
        private boolean isTimeout(int seconds) {
            if (firstSeenTime == null) {
                return false;
            }
            return System.currentTimeMillis() - firstSeenTime >= seconds * 1000L;
        }
        public Integer getTaskNo() {
            return taskNo;
        }
        public void setTaskNo(Integer taskNo) {
            this.taskNo = taskNo;
        }
        public Integer getStationId() {
            return stationId;
        }
        public void setStationId(Integer stationId) {
            this.stationId = stationId;
        }
        public Long getFirstSeenTime() {
            return firstSeenTime;
        }
        public void setFirstSeenTime(Long firstSeenTime) {
            this.firstSeenTime = firstSeenTime;
        }
    }
}
src/main/java/com/zy/core/utils/station/StationDispatchLoadSupport.java
New file
@@ -0,0 +1,300 @@
package com.zy.core.utils.station;
import com.alibaba.fastjson.JSONObject;
import com.zy.asrs.domain.path.StationPathResolvedPolicy;
import com.zy.asrs.domain.vo.StationCycleCapacityVo;
import com.zy.asrs.domain.vo.StationCycleLoopVo;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.StationCycleCapacityService;
import com.zy.asrs.service.StationPathPolicyService;
import com.zy.common.model.NavigateNode;
import com.zy.common.utils.NavigateUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.thread.StationThread;
import com.zy.core.utils.station.model.DispatchLimitConfig;
import com.zy.core.utils.station.model.LoadGuardState;
import com.zy.core.utils.station.model.LoopHitResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@Component
public class StationDispatchLoadSupport {
    private static final int LOOP_LOAD_RESERVE_EXPIRE_SECONDS = 120;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private BasDevpService basDevpService;
    @Autowired
    private StationCycleCapacityService stationCycleCapacityService;
    @Autowired
    private StationPathPolicyService stationPathPolicyService;
    @Autowired
    private NavigateUtils navigateUtils;
    public int countCurrentStationTask() {
        int currentStationTaskCount = 0;
        List<BasDevp> basDevps = basDevpService.list();
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
            if (stationThread == null) {
                continue;
            }
            for (StationProtocol stationProtocol : stationThread.getStatus()) {
                if (stationProtocol.getTaskNo() > 0) {
                    currentStationTaskCount++;
                }
            }
        }
        return currentStationTaskCount;
    }
    public boolean isDispatchBlocked(DispatchLimitConfig config,
                                     int currentStationTaskCount,
                                     LoadGuardState loadGuardState,
                                     boolean needReserveLoopLoad) {
        if (config != null && config.isLoopModeEnable()) {
            double currentLoad = loadGuardState.currentLoad();
            if (currentLoad >= config.getCircleMaxLoadLimit()) {
                News.warn("当前承载量达到上限,已停止站点任务下发。当前承载量={},上限={}", formatPercent(currentLoad), formatPercent(config.getCircleMaxLoadLimit()));
                return true;
            }
            if (needReserveLoopLoad) {
                double reserveLoad = loadGuardState.loadAfterReserve();
                if (reserveLoad >= config.getCircleMaxLoadLimit()) {
                    News.warn("预占后承载量达到上限,已停止站点任务下发。预占后承载量={},上限={}", formatPercent(reserveLoad), formatPercent(config.getCircleMaxLoadLimit()));
                    return true;
                }
            }
        }
        return false;
    }
    public LoadGuardState buildLoadGuardState(DispatchLimitConfig config) {
        LoadGuardState state = new LoadGuardState();
        if (config == null || !config.isLoopModeEnable()) {
            return state;
        }
        StationCycleCapacityVo capacityVo = stationCycleCapacityService.getLatestSnapshot();
        if (capacityVo == null) {
            return state;
        }
        Integer occupiedStationCount = capacityVo.getOccupiedStationCount();
        state.setTotalStationCount(toNonNegative(capacityVo.getTotalStationCount()));
        state.setProjectedTaskStationCount(toNonNegative(occupiedStationCount != null ? occupiedStationCount : capacityVo.getTaskStationCount()));
        List<StationCycleLoopVo> loopList = capacityVo.getLoopList();
        if (loopList != null) {
            for (StationCycleLoopVo loopVo : loopList) {
                if (loopVo == null || loopVo.getStationIdList() == null) {
                    continue;
                }
                Integer loopNo = loopVo.getLoopNo();
                for (Integer stationId : loopVo.getStationIdList()) {
                    if (stationId != null && loopNo != null) {
                        state.putStationLoopNo(stationId, loopNo);
                    }
                }
            }
        }
        return state;
    }
    public LoopHitResult findPathLoopHit(DispatchLimitConfig config,
                                         Integer sourceStationId,
                                         Integer targetStationId,
                                         LoadGuardState loadGuardState) {
        return findPathLoopHit(config, sourceStationId, targetStationId, loadGuardState, null, null);
    }
    public LoopHitResult findPathLoopHit(DispatchLimitConfig config,
                                         Integer sourceStationId,
                                         Integer targetStationId,
                                         LoadGuardState loadGuardState,
                                         WrkMast wrkMast,
                                         Double pathLenFactor) {
        if (config == null || !config.isLoopModeEnable()) {
            return LoopHitResult.noHit();
        }
        if (sourceStationId == null || targetStationId == null) {
            return LoopHitResult.noHit();
        }
        if (loadGuardState == null || loadGuardState.getStationLoopNoMap().isEmpty()) {
            return LoopHitResult.noHit();
        }
        try {
            List<NavigateNode> nodes = wrkMast == null
                    ? navigateUtils.calcByStationId(sourceStationId, targetStationId)
                    : calcOutboundNavigatePath(wrkMast, sourceStationId, targetStationId, pathLenFactor);
            if (nodes == null || nodes.isEmpty()) {
                return LoopHitResult.noHit();
            }
            for (NavigateNode node : nodes) {
                Integer stationId = getStationIdFromNode(node);
                if (stationId == null) {
                    continue;
                }
                Integer loopNo = loadGuardState.getStationLoopNoMap().get(stationId);
                if (loopNo != null) {
                    return new LoopHitResult(true, loopNo, stationId);
                }
            }
        } catch (Exception ignore) {
            return LoopHitResult.noHit();
        }
        return LoopHitResult.noHit();
    }
    public void saveLoopLoadReserve(Integer wrkNo, LoopHitResult loopHitResult) {
        if (wrkNo == null || wrkNo <= 0 || loopHitResult == null || !loopHitResult.isThroughLoop()) {
            return;
        }
        JSONObject reserveJson = new JSONObject();
        reserveJson.put("wrkNo", wrkNo);
        reserveJson.put("loopNo", loopHitResult.getLoopNo());
        reserveJson.put("hitStationId", loopHitResult.getHitStationId());
        reserveJson.put("createTime", System.currentTimeMillis());
        redisUtil.hset(RedisKeyType.STATION_CYCLE_LOAD_RESERVE.key, String.valueOf(wrkNo), reserveJson.toJSONString());
        redisUtil.expire(RedisKeyType.STATION_CYCLE_LOAD_RESERVE.key, LOOP_LOAD_RESERVE_EXPIRE_SECONDS);
    }
    public DispatchLimitConfig getDispatchLimitConfig(Integer startStationId, Integer endStationId) {
        DispatchLimitConfig config = new DispatchLimitConfig();
        Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
        if (systemConfigMapObj instanceof Map) {
            Map<?, ?> systemConfigMap = (Map<?, ?>) systemConfigMapObj;
            config.setCircleMaxLoadLimit(parseLoadLimit(getConfigValue(systemConfigMap, "circleMaxLoadLimit"), config.getCircleMaxLoadLimit()));
            String loopModeValue = getConfigValue(systemConfigMap, "circleLoopModeEnable");
            if (isBlank(loopModeValue)) {
                loopModeValue = getConfigValue(systemConfigMap, "circleModeEnable");
            }
            if (isBlank(loopModeValue)) {
                loopModeValue = getConfigValue(systemConfigMap, "isCircleMode");
            }
            config.setLoopModeEnable(parseBoolean(loopModeValue, config.isLoopModeEnable()));
        }
        if (stationPathPolicyService != null && startStationId != null && endStationId != null) {
            try {
                StationPathResolvedPolicy resolvedPolicy = stationPathPolicyService.resolvePolicy(startStationId, endStationId);
                if (resolvedPolicy != null && resolvedPolicy.getProfileConfig() != null) {
                    config.setCircleMaxLoadLimit(parseLoadLimit(String.valueOf(resolvedPolicy.getProfileConfig().getCircleMaxLoadLimit()), config.getCircleMaxLoadLimit()));
                }
            } catch (Exception ignore) {
            }
        }
        return config;
    }
    private List<NavigateNode> calcOutboundNavigatePath(WrkMast wrkMast,
                                                        Integer sourceStationId,
                                                        Integer targetStationId,
                                                        Double pathLenFactor) {
        Double normalizedFactor = normalizePathLenFactor(pathLenFactor);
        Integer currentTaskNo = wrkMast == null ? null : wrkMast.getWrkNo();
        if (currentTaskNo == null) {
            return navigateUtils.calcByStationId(sourceStationId, targetStationId, normalizedFactor);
        }
        return navigateUtils.calcByStationId(sourceStationId, targetStationId, currentTaskNo, normalizedFactor);
    }
    private Integer getStationIdFromNode(NavigateNode node) {
        if (node == null || isBlank(node.getNodeValue())) {
            return null;
        }
        try {
            JSONObject value = JSONObject.parseObject(node.getNodeValue());
            return value == null ? null : value.getInteger("stationId");
        } catch (Exception ignore) {
            return null;
        }
    }
    private int toNonNegative(Integer value) {
        if (value == null || value < 0) {
            return 0;
        }
        return value;
    }
    private Double normalizePathLenFactor(Double pathLenFactor) {
        if (pathLenFactor == null || pathLenFactor < 0.0d) {
            return 0.0d;
        }
        if (pathLenFactor > 1.0d) {
            return 1.0d;
        }
        return pathLenFactor;
    }
    private String getConfigValue(Map<?, ?> configMap, String key) {
        Object value = configMap.get(key);
        return value == null ? null : String.valueOf(value).trim();
    }
    private boolean parseBoolean(String value, boolean defaultValue) {
        if (isBlank(value)) {
            return defaultValue;
        }
        String lowValue = value.toLowerCase(Locale.ROOT);
        if ("y".equals(lowValue) || "yes".equals(lowValue) || "true".equals(lowValue)
                || "1".equals(lowValue) || "on".equals(lowValue)) {
            return true;
        }
        if ("n".equals(lowValue) || "no".equals(lowValue) || "false".equals(lowValue)
                || "0".equals(lowValue) || "off".equals(lowValue)) {
            return false;
        }
        return defaultValue;
    }
    private double parseLoadLimit(String value, double defaultValue) {
        if (isBlank(value)) {
            return defaultValue;
        }
        try {
            String normalized = value.replace("%", "").trim();
            double parsed = Double.parseDouble(normalized);
            if (parsed > 1.0) {
                parsed = parsed / 100.0;
            }
            if (parsed < 0.0) {
                return 0.0;
            }
            if (parsed > 1.0) {
                return 1.0;
            }
            return parsed;
        } catch (Exception ignore) {
            return defaultValue;
        }
    }
    private String formatPercent(double value) {
        return String.format(Locale.ROOT, "%.1f%%", value * 100.0);
    }
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
}
src/main/java/com/zy/core/utils/station/StationDispatchRuntimeStateSupport.java
New file
@@ -0,0 +1,231 @@
package com.zy.core.utils.station;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.Cools;
import com.zy.asrs.entity.BasStationOpt;
import com.zy.asrs.service.BasStationOptService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.model.command.StationCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@Component
public class StationDispatchRuntimeStateSupport {
    private static final int STATION_IDLE_TRACK_EXPIRE_SECONDS = 60 * 60;
    private static final String IDLE_RECOVER_CLEARED_MEMO = "idleRecoverRerouteCleared";
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private BasStationOptService basStationOptService;
    public StationTaskIdleTrack touchIdleTrack(Integer taskNo, Integer stationId) {
        if (taskNo == null || taskNo <= 0 || stationId == null) {
            return null;
        }
        long now = System.currentTimeMillis();
        StationTaskIdleTrack idleTrack = loadIdleTrack(taskNo);
        if (idleTrack == null || !Objects.equals(idleTrack.getStationId(), stationId)) {
            idleTrack = new StationTaskIdleTrack(taskNo, stationId, now);
            saveIdleTrack(idleTrack);
        }
        return idleTrack;
    }
    public StationTaskIdleTrack loadIdleTrack(Integer taskNo) {
        if (taskNo == null || taskNo <= 0 || redisUtil == null) {
            return null;
        }
        Object obj = redisUtil.get(RedisKeyType.STATION_TASK_IDLE_TRACK_.key + taskNo);
        if (obj == null) {
            return null;
        }
        try {
            return JSON.parseObject(obj.toString(), StationTaskIdleTrack.class);
        } catch (Exception e) {
            return null;
        }
    }
    public void saveIdleTrack(StationTaskIdleTrack idleTrack) {
        if (idleTrack == null || idleTrack.getTaskNo() == null || idleTrack.getTaskNo() <= 0 || redisUtil == null) {
            return;
        }
        redisUtil.set(
                RedisKeyType.STATION_TASK_IDLE_TRACK_.key + idleTrack.getTaskNo(),
                JSON.toJSONString(idleTrack, SerializerFeature.DisableCircularReferenceDetect),
                STATION_IDLE_TRACK_EXPIRE_SECONDS
        );
    }
    public boolean hasRecentIssuedMoveCommand(Integer taskNo, Integer stationId, long thresholdMs) {
        if (taskNo == null || taskNo <= 0 || stationId == null || thresholdMs <= 0L || basStationOptService == null) {
            return false;
        }
        Date thresholdTime = new Date(System.currentTimeMillis() - thresholdMs);
        List<BasStationOpt> optList = basStationOptService.list(new QueryWrapper<BasStationOpt>()
                .select("id")
                .eq("task_no", taskNo)
                .eq("station_id", stationId)
                .eq("mode", String.valueOf(StationCommandType.MOVE))
                .eq("send", 1)
                .ge("send_time", thresholdTime)
                .orderByDesc("send_time")
                .last("limit 1"));
        return optList != null && !optList.isEmpty();
    }
    public int clearIssuedMoveCommandsDuringIdleStay(StationTaskIdleTrack idleTrack,
                                                     Integer taskNo,
                                                     Integer stationId) {
        if (basStationOptService == null) {
            return 0;
        }
        List<BasStationOpt> optList;
        try {
            optList = listIssuedMoveCommandsDuringIdleStay(idleTrack, taskNo);
        } catch (Exception e) {
            return 0;
        }
        if (optList == null || optList.isEmpty()) {
            return 0;
        }
        Date now = new Date();
        String cleanupMemo = buildIdleRecoverClearedMemo(stationId);
        int clearedCount = 0;
        for (BasStationOpt opt : optList) {
            if (opt == null || opt.getId() == null) {
                continue;
            }
            opt.setSend(0);
            opt.setUpdateTime(now);
            opt.setMemo(appendCleanupMemo(opt.getMemo(), cleanupMemo));
            clearedCount++;
        }
        if (clearedCount > 0) {
            basStationOptService.updateBatchById(optList);
        }
        return clearedCount;
    }
    public boolean tryAcquireLock(String key, int seconds) {
        if (redisUtil == null || isBlank(key)) {
            return true;
        }
        Object lock = redisUtil.get(key);
        if (lock != null) {
            return false;
        }
        redisUtil.set(key, "lock", seconds);
        return true;
    }
    public boolean tryAcquireOutOrderDispatchLock(Integer wrkNo, Integer stationId, int seconds) {
        if (wrkNo == null || wrkNo <= 0 || stationId == null) {
            return true;
        }
        return tryAcquireLock(RedisKeyType.STATION_OUT_ORDER_DISPATCH_LIMIT_.key + wrkNo + "_" + stationId, seconds);
    }
    public void signalSegmentReset(Integer taskNo, long waitMs) {
        if (redisUtil == null || taskNo == null || taskNo <= 0) {
            return;
        }
        String key = RedisKeyType.DEVICE_STATION_MOVE_RESET.key + taskNo;
        redisUtil.set(key, "cancel", 3);
        try {
            if (waitMs > 0L) {
                Thread.sleep(waitMs);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (Exception ignore) {
        }
        redisUtil.del(key);
    }
    public StationCommand loadWatchCircleCommand(Integer wrkNo) {
        if (wrkNo == null || wrkNo <= 0 || redisUtil == null) {
            return null;
        }
        Object circleObj = redisUtil.get(RedisKeyType.WATCH_CIRCLE_STATION_.key + wrkNo);
        if (circleObj == null) {
            return null;
        }
        try {
            return JSON.parseObject(circleObj.toString(), StationCommand.class);
        } catch (Exception ignore) {
            return null;
        }
    }
    public void saveWatchCircleCommand(Integer wrkNo, StationCommand command) {
        if (wrkNo == null || wrkNo <= 0 || command == null || redisUtil == null) {
            return;
        }
        redisUtil.set(
                RedisKeyType.WATCH_CIRCLE_STATION_.key + wrkNo,
                JSON.toJSONString(command, SerializerFeature.DisableCircularReferenceDetect),
                60 * 60 * 24
        );
    }
    public void clearWatchCircleCommand(Integer wrkNo) {
        if (wrkNo == null || wrkNo <= 0 || redisUtil == null) {
            return;
        }
        redisUtil.del(RedisKeyType.WATCH_CIRCLE_STATION_.key + wrkNo);
    }
    private List<BasStationOpt> listIssuedMoveCommandsDuringIdleStay(StationTaskIdleTrack idleTrack,
                                                                     Integer taskNo) {
        if (idleTrack == null || taskNo == null || taskNo <= 0 || idleTrack.getFirstSeenTime() == null || basStationOptService == null) {
            return Collections.emptyList();
        }
        List<BasStationOpt> optList = basStationOptService.list(new QueryWrapper<BasStationOpt>()
                .select("id", "task_no", "send_time", "target_station_id", "memo", "send")
                .eq("task_no", taskNo)
                .eq("mode", String.valueOf(StationCommandType.MOVE))
                .eq("send", 1)
                .ge("send_time", new Date(idleTrack.getFirstSeenTime()))
                .orderByAsc("send_time"));
        if (optList == null || optList.isEmpty()) {
            return Collections.emptyList();
        }
        return optList;
    }
    private String buildIdleRecoverClearedMemo(Integer stationId) {
        if (stationId == null) {
            return IDLE_RECOVER_CLEARED_MEMO;
        }
        return IDLE_RECOVER_CLEARED_MEMO + "(stationId=" + stationId + ")";
    }
    private String appendCleanupMemo(String memo, String cleanupMemo) {
        if (Cools.isEmpty(cleanupMemo)) {
            return memo;
        }
        if (Cools.isEmpty(memo)) {
            return cleanupMemo;
        }
        if (memo.contains(cleanupMemo)) {
            return memo;
        }
        return memo + " | " + cleanupMemo;
    }
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
}
src/main/java/com/zy/core/utils/station/StationOutboundDecisionSupport.java
New file
@@ -0,0 +1,615 @@
package com.zy.core.utils.station;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.Cools;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.BasStation;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.BasStationService;
import com.zy.asrs.service.WrkMastService;
import com.zy.common.model.NavigateNode;
import com.zy.common.utils.NavigateUtils;
import com.zy.core.News;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.enums.WrkIoType;
import com.zy.core.enums.WrkStsType;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.move.StationMoveCoordinator;
import com.zy.core.move.StationMoveDispatchMode;
import com.zy.core.move.StationMoveSession;
import com.zy.core.service.StationTaskLoopService;
import com.zy.core.thread.StationThread;
import com.zy.core.utils.station.model.CircleTargetCandidate;
import com.zy.core.utils.station.model.OutOrderDispatchDecision;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Component
public class StationOutboundDecisionSupport {
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private BasDevpService basDevpService;
    @Autowired
    private BasStationService basStationService;
    @Autowired
    private NavigateUtils navigateUtils;
    @Autowired
    private StationTaskLoopService stationTaskLoopService;
    @Autowired
    private StationMoveCoordinator stationMoveCoordinator;
    @Autowired
    private StationDispatchRuntimeStateSupport stationDispatchRuntimeStateSupport;
    public StationCommand buildOutboundMoveCommand(StationThread stationThread,
                                                   WrkMast wrkMast,
                                                   Integer stationId,
                                                   Integer targetStationId,
                                                   Double pathLenFactor) {
        if (stationThread == null || wrkMast == null) {
            return null;
        }
        return stationThread.getCommand(
                StationCommandType.MOVE,
                wrkMast.getWrkNo(),
                stationId,
                targetStationId,
                0,
                normalizePathLenFactor(pathLenFactor)
        );
    }
    public Double resolveOutboundPathLenFactor(WrkMast wrkMast) {
        if (!isBatchOutboundTaskWithSeq(wrkMast)) {
            return 0.0d;
        }
        List<WrkMast> activeBatchTaskList = loadActiveBatchTaskList(wrkMast.getBatch());
        if (activeBatchTaskList.size() <= 1) {
            return 0.0d;
        }
        int activeTaskCount = 0;
        int predecessorCount = 0;
        for (WrkMast item : activeBatchTaskList) {
            if (!isFactorCandidateTask(item)) {
                continue;
            }
            activeTaskCount++;
            if (item.getBatchSeq() < wrkMast.getBatchSeq()) {
                predecessorCount++;
            }
        }
        if (activeTaskCount <= 1 || predecessorCount <= 0) {
            return 0.0d;
        }
        return normalizePathLenFactor((double) predecessorCount / (double) (activeTaskCount - 1));
    }
    public List<Integer> getAllOutOrderList() {
        List<Integer> list = new ArrayList<>();
        List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            List<Integer> orderList = basDevp.getOutOrderIntList();
            list.addAll(orderList);
        }
        return list;
    }
    public OutOrderDispatchDecision resolveOutboundDispatchDecision(Integer currentStationId,
                                                                    WrkMast wrkMast,
                                                                    List<Integer> outOrderStationIds,
                                                                    Double pathLenFactor) {
        if (wrkMast == null || wrkMast.getStaNo() == null) {
            return null;
        }
        if (!shouldApplyOutOrder(wrkMast, outOrderStationIds)) {
            return OutOrderDispatchDecision.direct(wrkMast.getStaNo());
        }
        Integer dispatchStationId = resolveDispatchOutOrderTarget(
                wrkMast,
                wrkMast.getSourceStaNo(),
                wrkMast.getStaNo(),
                outOrderStationIds,
                pathLenFactor
        );
        if (dispatchStationId == null) {
            return null;
        }
        if (isCurrentOutOrderDispatchStation(currentStationId, wrkMast, outOrderStationIds, pathLenFactor)) {
            return resolveCurrentOutOrderDispatchDecision(currentStationId, wrkMast, outOrderStationIds, pathLenFactor);
        }
        if (!Objects.equals(dispatchStationId, wrkMast.getStaNo())
                && isCurrentOutOrderStation(currentStationId, outOrderStationIds)
                && isWatchingCircleArrival(wrkMast.getWrkNo(), currentStationId)) {
            return OutOrderDispatchDecision.circle(dispatchStationId, null, false);
        }
        return OutOrderDispatchDecision.direct(dispatchStationId);
    }
    public void syncOutOrderWatchState(WrkMast wrkMast,
                                       Integer currentStationId,
                                       List<Integer> outOrderStationIds,
                                       OutOrderDispatchDecision dispatchDecision,
                                       StationCommand command) {
        if (dispatchDecision == null || command == null || !shouldApplyOutOrder(wrkMast, outOrderStationIds)) {
            return;
        }
        if (dispatchDecision.isCircle()) {
            stationDispatchRuntimeStateSupport.saveWatchCircleCommand(wrkMast.getWrkNo(), command);
            if (dispatchDecision.shouldCountLoopIssue()
                    && stationTaskLoopService != null
                    && dispatchDecision.getLoopEvaluation() != null) {
                stationTaskLoopService.recordLoopIssue(dispatchDecision.getLoopEvaluation(), "OUT_ORDER_CIRCLE");
            }
        } else {
            stationDispatchRuntimeStateSupport.clearWatchCircleCommand(wrkMast.getWrkNo());
        }
    }
    public boolean shouldSkipOutOrderDispatchForExistingRoute(Integer wrkNo, Integer stationId) {
        if (stationMoveCoordinator == null || wrkNo == null || wrkNo <= 0 || stationId == null) {
            return false;
        }
        StationMoveSession session = stationMoveCoordinator.loadSession(wrkNo);
        if (session == null) {
            return false;
        }
        if (!session.isActive() || !session.containsStation(stationId)) {
            return false;
        }
        if (StationMoveDispatchMode.CIRCLE == session.getDispatchMode()) {
            return true;
        }
        return !Objects.equals(stationId, session.getCurrentRouteTargetStationId());
    }
    public boolean isWatchingCircleArrival(Integer wrkNo, Integer stationId) {
        if (stationMoveCoordinator != null) {
            StationMoveSession session = stationMoveCoordinator.loadSession(wrkNo);
            if (session != null && session.isActive() && stationId != null) {
                if (stationId.equals(session.getNextDecisionStationId())) {
                    return true;
                }
                if (session.containsStation(stationId)) {
                    return false;
                }
            }
        }
        StationCommand command = stationDispatchRuntimeStateSupport.loadWatchCircleCommand(wrkNo);
        return command != null && stationId != null && stationId.equals(command.getTargetStaNo());
    }
    private List<NavigateNode> calcOutboundNavigatePath(WrkMast wrkMast,
                                                        Integer sourceStationId,
                                                        Integer targetStationId,
                                                        Double pathLenFactor) {
        Double normalizedFactor = normalizePathLenFactor(pathLenFactor);
        Integer currentTaskNo = wrkMast == null ? null : wrkMast.getWrkNo();
        if (currentTaskNo == null) {
            return navigateUtils.calcByStationId(sourceStationId, targetStationId, normalizedFactor);
        }
        return navigateUtils.calcByStationId(sourceStationId, targetStationId, currentTaskNo, normalizedFactor);
    }
    private boolean isBatchOutboundTaskWithSeq(WrkMast wrkMast) {
        return wrkMast != null
                && Objects.equals(wrkMast.getIoType(), WrkIoType.OUT.id)
                && !Cools.isEmpty(wrkMast.getBatch())
                && wrkMast.getBatchSeq() != null
                && wrkMast.getWrkNo() != null;
    }
    private List<WrkMast> loadActiveBatchTaskList(String batch) {
        if (Cools.isEmpty(batch)) {
            return Collections.emptyList();
        }
        return wrkMastService.list(new QueryWrapper<WrkMast>()
                .eq("io_type", WrkIoType.OUT.id)
                .eq("batch", batch)
                .notIn("wrk_sts",
                        WrkStsType.STATION_RUN_COMPLETE.sts,
                        WrkStsType.COMPLETE_OUTBOUND.sts,
                        WrkStsType.SETTLE_OUTBOUND.sts));
    }
    private boolean isFactorCandidateTask(WrkMast wrkMast) {
        return wrkMast != null
                && Objects.equals(wrkMast.getIoType(), WrkIoType.OUT.id)
                && wrkMast.getBatchSeq() != null
                && !"taskCancel".equals(wrkMast.getMk());
    }
    private boolean shouldApplyOutOrder(WrkMast wrkMast, List<Integer> outOrderStationIds) {
        return wrkMast != null
                && wrkMast.getStaNo() != null
                && Objects.equals(wrkMast.getIoType(), WrkIoType.OUT.id)
                && !Cools.isEmpty(wrkMast.getBatch())
                && wrkMast.getBatchSeq() != null
                && outOrderStationIds != null
                && !outOrderStationIds.isEmpty();
    }
    private boolean isCurrentOutOrderDispatchStation(Integer currentStationId,
                                                     WrkMast wrkMast,
                                                     List<Integer> outOrderStationIds,
                                                     Double pathLenFactor) {
        if (!shouldApplyOutOrder(wrkMast, outOrderStationIds) || currentStationId == null) {
            return false;
        }
        Integer dispatchStationId = resolveDispatchOutOrderTarget(
                wrkMast,
                wrkMast.getSourceStaNo(),
                wrkMast.getStaNo(),
                outOrderStationIds,
                pathLenFactor
        );
        return dispatchStationId != null
                && !Objects.equals(dispatchStationId, wrkMast.getStaNo())
                && Objects.equals(currentStationId, dispatchStationId);
    }
    private boolean isCurrentOutOrderStation(Integer currentStationId,
                                             List<Integer> outOrderStationIds) {
        return currentStationId != null
                && outOrderStationIds != null
                && outOrderStationIds.contains(currentStationId);
    }
    private OutOrderDispatchDecision resolveCurrentOutOrderDispatchDecision(Integer currentStationId,
                                                                           WrkMast wrkMast,
                                                                           List<Integer> outOrderStationIds,
                                                                           Double pathLenFactor) {
        if (!isCurrentOutOrderDispatchStation(currentStationId, wrkMast, outOrderStationIds, pathLenFactor)) {
            return null;
        }
        List<WrkMast> batchWrkList = wrkMastService.list(new QueryWrapper<WrkMast>()
                .eq("io_type", WrkIoType.OUT.id)
                .notIn("wrk_sts",
                        WrkStsType.STATION_RUN_COMPLETE.sts,
                        WrkStsType.COMPLETE_OUTBOUND.sts,
                        WrkStsType.SETTLE_OUTBOUND.sts)
                .eq("batch", wrkMast.getBatch())
                .orderByAsc("batch_seq")
                .orderByAsc("wrk_no"));
        if (batchWrkList.isEmpty()) {
            return OutOrderDispatchDecision.direct(wrkMast.getStaNo());
        }
        WrkMast firstWrkMast = batchWrkList.get(0);
        Integer currentBatchSeq = firstWrkMast.getBatchSeq();
        if (currentBatchSeq == null) {
            News.taskInfo(wrkMast.getWrkNo(), "批次:{} 首个未完成任务缺少批次序号,当前任务暂不放行", wrkMast.getBatch());
            return null;
        }
        List<NavigateNode> initPath;
        try {
            initPath = calcOutboundNavigatePath(wrkMast, wrkMast.getSourceStaNo(), wrkMast.getStaNo(), pathLenFactor);
        } catch (Exception e) {
            News.taskInfo(wrkMast.getWrkNo(), "批次:{} 计算排序路径失败,当前站点={}", wrkMast.getBatch(), currentStationId);
            return null;
        }
        Integer seq = getOutStationBatchSeq(initPath, currentStationId, wrkMast.getBatch());
        boolean toTarget = seq == null
                ? currentBatchSeq.equals(wrkMast.getBatchSeq())
                : Integer.valueOf(seq + 1).equals(wrkMast.getBatchSeq());
        if (toTarget) {
            if (hasReachableOutReleaseSlot(wrkMast, currentStationId, wrkMast.getStaNo(), pathLenFactor)) {
                return OutOrderDispatchDecision.direct(wrkMast.getStaNo());
            }
            StationTaskLoopService.LoopEvaluation loopEvaluation = evaluateOutOrderLoop(
                    wrkMast.getWrkNo(),
                    currentStationId,
                    outOrderStationIds
            );
            Integer circleTarget = resolveNextCircleOrderTarget(
                    wrkMast,
                    currentStationId,
                    outOrderStationIds,
                    loopEvaluation.getExpectedLoopIssueCount(),
                    pathLenFactor
            );
            if (circleTarget == null) {
                News.taskInfo(wrkMast.getWrkNo(), "目标站当前不可进,且未找到可执行的下一排序检测点,当前站点={}", currentStationId);
                return null;
            }
            return OutOrderDispatchDecision.circle(circleTarget, loopEvaluation, true);
        }
        StationTaskLoopService.LoopEvaluation loopEvaluation = evaluateOutOrderLoop(
                wrkMast.getWrkNo(),
                currentStationId,
                outOrderStationIds
        );
        Integer circleTarget = resolveNextCircleOrderTarget(
                wrkMast,
                currentStationId,
                outOrderStationIds,
                loopEvaluation.getExpectedLoopIssueCount(),
                pathLenFactor
        );
        if (circleTarget == null) {
            News.taskInfo(wrkMast.getWrkNo(), "未找到可执行的下一排序检测点,当前站点={}", currentStationId);
            return null;
        }
        return OutOrderDispatchDecision.circle(circleTarget, loopEvaluation, true);
    }
    private StationTaskLoopService.LoopEvaluation evaluateOutOrderLoop(Integer taskNo,
                                                                       Integer currentStationId,
                                                                       List<Integer> outOrderStationIds) {
        if (stationTaskLoopService == null) {
            return new StationTaskLoopService.LoopEvaluation(
                    taskNo,
                    currentStationId,
                    StationTaskLoopService.LoopIdentitySnapshot.empty(),
                    0,
                    0,
                    false
            );
        }
        return stationTaskLoopService.evaluateLoop(
                taskNo,
                currentStationId,
                true,
                outOrderStationIds,
                "outOrderCircle"
        );
    }
    private Integer resolveDispatchOutOrderTarget(WrkMast wrkMast,
                                                  Integer sourceStationId,
                                                  Integer finalTargetStationId,
                                                  List<Integer> outOrderList,
                                                  Double pathLenFactor) {
        if (finalTargetStationId == null) {
            return null;
        }
        if (sourceStationId == null || outOrderList == null || outOrderList.isEmpty()) {
            return finalTargetStationId;
        }
        try {
            List<NavigateNode> nodes = calcOutboundNavigatePath(wrkMast, sourceStationId, finalTargetStationId, pathLenFactor);
            for (int i = nodes.size() - 1; i >= 0; i--) {
                Integer stationId = getStationIdFromNode(nodes.get(i));
                if (stationId == null) {
                    continue;
                }
                if (Objects.equals(stationId, finalTargetStationId)) {
                    continue;
                }
                if (outOrderList.contains(stationId)) {
                    return stationId;
                }
            }
        } catch (Exception ignore) {
        }
        return finalTargetStationId;
    }
    private boolean hasReachableOutReleaseSlot(WrkMast wrkMast,
                                               Integer currentStationId,
                                               Integer finalTargetStationId,
                                               Double pathLenFactor) {
        if (currentStationId == null || finalTargetStationId == null) {
            return true;
        }
        try {
            List<NavigateNode> nodes = calcOutboundNavigatePath(wrkMast, currentStationId, finalTargetStationId, pathLenFactor);
            if (nodes == null || nodes.isEmpty()) {
                return true;
            }
            for (NavigateNode node : nodes) {
                Integer stationId = getStationIdFromNode(node);
                if (stationId == null || Objects.equals(stationId, currentStationId)) {
                    continue;
                }
                if (!isPathStationBlocked(stationId)) {
                    return true;
                }
            }
            return false;
        } catch (Exception ignore) {
            return true;
        }
    }
    private boolean isPathStationBlocked(Integer stationId) {
        if (stationId == null) {
            return true;
        }
        BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>().eq("station_id", stationId));
        if (basStation == null) {
            return true;
        }
        StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basStation.getDeviceNo());
        if (stationThread == null) {
            return true;
        }
        StationProtocol stationProtocol = stationThread.getStatusMap().get(stationId);
        if (stationProtocol == null) {
            return true;
        }
        return !stationProtocol.isAutoing()
                || stationProtocol.isLoading()
                || (stationProtocol.getTaskNo() != null && stationProtocol.getTaskNo() > 0);
    }
    private Integer resolveNextCircleOrderTarget(WrkMast wrkMast,
                                                 Integer currentStationId,
                                                 List<Integer> orderedOutStationList,
                                                 Integer expectedLoopIssueCount,
                                                 Double pathLenFactor) {
        if (currentStationId == null || orderedOutStationList == null || orderedOutStationList.size() <= 1) {
            return null;
        }
        int startIndex = orderedOutStationList.indexOf(currentStationId);
        int total = orderedOutStationList.size();
        List<CircleTargetCandidate> candidateList = new ArrayList<>();
        for (int offset = 1; offset < total; offset++) {
            int candidateIndex = (startIndex + offset + total) % total;
            Integer candidateStationId = orderedOutStationList.get(candidateIndex);
            if (candidateStationId == null || currentStationId.equals(candidateStationId)) {
                continue;
            }
            try {
                List<NavigateNode> path = calcOutboundNavigatePath(wrkMast, currentStationId, candidateStationId, pathLenFactor);
                if (path != null && !path.isEmpty()) {
                    candidateList.add(new CircleTargetCandidate(candidateStationId, path.size(), offset));
                }
            } catch (Exception ignore) {
            }
        }
        if (candidateList.isEmpty()) {
            return null;
        }
        candidateList.sort(new Comparator<CircleTargetCandidate>() {
            @Override
            public int compare(CircleTargetCandidate left, CircleTargetCandidate right) {
                if (left == right) {
                    return 0;
                }
                if (left == null) {
                    return 1;
                }
                if (right == null) {
                    return -1;
                }
                int pathCompare = Integer.compare(left.getPathLength(), right.getPathLength());
                if (pathCompare != 0) {
                    return pathCompare;
                }
                return Integer.compare(left.getOffset(), right.getOffset());
            }
        });
        return resolveGradualCircleTargetByPathLength(expectedLoopIssueCount, candidateList, pathLenFactor);
    }
    private Integer resolveGradualCircleTargetByPathLength(Integer expectedLoopIssueCount,
                                                           List<CircleTargetCandidate> candidateList,
                                                           Double pathLenFactor) {
        if (candidateList == null || candidateList.isEmpty()) {
            return null;
        }
        List<CircleTargetCandidate> tierList = new ArrayList<>();
        Integer lastPathLength = null;
        for (CircleTargetCandidate candidate : candidateList) {
            if (candidate == null) {
                continue;
            }
            if (lastPathLength == null || !Objects.equals(lastPathLength, candidate.getPathLength())) {
                tierList.add(candidate);
                lastPathLength = candidate.getPathLength();
            }
        }
        if (tierList.isEmpty()) {
            return candidateList.get(0).getStationId();
        }
        int defaultTierIndex = expectedLoopIssueCount == null || expectedLoopIssueCount <= 2
                ? 0
                : Math.min(expectedLoopIssueCount - 2, tierList.size() - 1);
        int factorTierIndex = (int) Math.round(normalizePathLenFactor(pathLenFactor) * (tierList.size() - 1));
        int tierIndex = Math.max(defaultTierIndex, factorTierIndex);
        return tierList.get(tierIndex).getStationId();
    }
    private Integer getOutStationBatchSeq(List<NavigateNode> pathList, Integer searchStationId, String searchBatch) {
        if (pathList == null || pathList.isEmpty() || searchStationId == null || Cools.isEmpty(searchBatch)) {
            return null;
        }
        List<Integer> checkList = new ArrayList<>();
        for (int i = pathList.size() - 1; i >= 0; i--) {
            NavigateNode node = pathList.get(i);
            JSONObject value = JSONObject.parseObject(node.getNodeValue());
            if (value == null) {
                continue;
            }
            Integer stationId = value.getInteger("stationId");
            if (searchStationId.equals(stationId)) {
                break;
            }
            checkList.add(stationId);
        }
        HashMap<String, Integer> batchMap = new HashMap<>();
        for (Integer station : checkList) {
            BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>().eq("station_id", station));
            if (basStation == null) {
                continue;
            }
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basStation.getDeviceNo());
            if (stationThread == null) {
                continue;
            }
            StationProtocol checkStationProtocol = stationThread.getStatusMap().get(station);
            if (checkStationProtocol == null) {
                continue;
            }
            if (checkStationProtocol.getTaskNo() > 0) {
                WrkMast checkWrkMast = wrkMastService.selectByWorkNo(checkStationProtocol.getTaskNo());
                if (checkWrkMast == null) {
                    continue;
                }
                if (!Cools.isEmpty(checkWrkMast.getBatch())) {
                    batchMap.put(checkWrkMast.getBatch(), checkWrkMast.getBatchSeq());
                }
            }
        }
        return batchMap.get(searchBatch);
    }
    private Integer getStationIdFromNode(NavigateNode node) {
        if (node == null || isBlank(node.getNodeValue())) {
            return null;
        }
        try {
            JSONObject value = JSONObject.parseObject(node.getNodeValue());
            return value == null ? null : value.getInteger("stationId");
        } catch (Exception ignore) {
            return null;
        }
    }
    private Double normalizePathLenFactor(Double pathLenFactor) {
        if (pathLenFactor == null || pathLenFactor < 0.0d) {
            return 0.0d;
        }
        if (pathLenFactor > 1.0d) {
            return 1.0d;
        }
        return pathLenFactor;
    }
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
}
src/main/java/com/zy/core/utils/station/StationOutboundDispatchProcessor.java
New file
@@ -0,0 +1,249 @@
package com.zy.core.utils.station;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zy.asrs.domain.enums.NotifyMsgType;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.WrkAnalysisService;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.utils.NotifyUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatchResult;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.WrkStsType;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.move.StationMoveCoordinator;
import com.zy.core.thread.StationThread;
import com.zy.core.utils.station.model.DispatchLimitConfig;
import com.zy.core.utils.station.model.LoadGuardState;
import com.zy.core.utils.station.model.LoopHitResult;
import com.zy.core.utils.station.model.OutOrderDispatchDecision;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Component
public class StationOutboundDispatchProcessor {
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WrkAnalysisService wrkAnalysisService;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private NotifyUtils notifyUtils;
    @Autowired
    private StationMoveCoordinator stationMoveCoordinator;
    @Autowired(required = false)
    private StationCommandDispatcher stationCommandDispatcher;
    @Autowired
    private StationDispatchLoadSupport stationDispatchLoadSupport;
    @Autowired
    private StationOutboundDecisionSupport stationOutboundDecisionSupport;
    public void crnStationOutExecute() {
        try {
            DispatchLimitConfig baseLimitConfig =
                    stationDispatchLoadSupport.getDispatchLimitConfig(null, null);
            int[] currentStationTaskCountRef = new int[]{stationDispatchLoadSupport.countCurrentStationTask()};
            LoadGuardState loadGuardState =
                    stationDispatchLoadSupport.buildLoadGuardState(baseLimitConfig);
            List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>()
                    .eq("wrk_sts", WrkStsType.OUTBOUND_RUN_COMPLETE.sts)
                    .isNotNull("crn_no"));
            List<Integer> outOrderList = stationOutboundDecisionSupport.getAllOutOrderList();
            for (WrkMast wrkMast : wrkMasts) {
                Object infoObj = redisUtil.get(RedisKeyType.CRN_OUT_TASK_COMPLETE_STATION_INFO.key + wrkMast.getWrkNo());
                if (infoObj == null) {
                    News.info("出库任务{}数据缓存不存在", wrkMast.getWrkNo());
                    continue;
                }
                StationObjModel stationObjModel = JSON.parseObject(infoObj.toString(), StationObjModel.class);
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo());
                if (stationThread == null) {
                    continue;
                }
                Map<Integer, StationProtocol> stationMap = stationThread.getStatusMap();
                StationProtocol stationProtocol = stationMap.get(stationObjModel.getStationId());
                if (stationProtocol == null) {
                    continue;
                }
                Object lock = redisUtil.get(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId());
                if (lock != null) {
                    continue;
                }
                if (stationProtocol.isAutoing()
                        && stationProtocol.isLoading()
                        && stationProtocol.getTaskNo() == 0) {
                    Double pathLenFactor = stationOutboundDecisionSupport.resolveOutboundPathLenFactor(wrkMast);
                    OutOrderDispatchDecision dispatchDecision =
                            stationOutboundDecisionSupport.resolveOutboundDispatchDecision(
                                    stationProtocol.getStationId(),
                                    wrkMast,
                                    outOrderList,
                                    pathLenFactor
                            );
                    Integer moveStaNo = dispatchDecision == null ? null : dispatchDecision.getTargetStationId();
                    if (moveStaNo == null) {
                        continue;
                    }
                    DispatchLimitConfig limitConfig =
                            stationDispatchLoadSupport.getDispatchLimitConfig(stationProtocol.getStationId(), moveStaNo);
                    LoopHitResult loopHitResult =
                            stationDispatchLoadSupport.findPathLoopHit(
                                    limitConfig,
                                    stationProtocol.getStationId(),
                                    moveStaNo,
                                    loadGuardState,
                                    wrkMast,
                                    pathLenFactor
                            );
                    if (stationDispatchLoadSupport.isDispatchBlocked(
                            limitConfig,
                            currentStationTaskCountRef[0],
                            loadGuardState,
                            loopHitResult.isThroughLoop())) {
                        return;
                    }
                    StationCommand command = stationOutboundDecisionSupport.buildOutboundMoveCommand(
                            stationThread,
                            wrkMast,
                            stationProtocol.getStationId(),
                            moveStaNo,
                            pathLenFactor
                    );
                    if (command == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
                        continue;
                    }
                    Date now = new Date();
                    wrkMast.setWrkSts(WrkStsType.STATION_RUN.sts);
                    wrkMast.setSystemMsg("");
                    wrkMast.setIoTime(now);
                    wrkMast.setModiTime(now);
                    if (wrkMastService.updateById(wrkMast)) {
                        wrkAnalysisService.markOutboundStationStart(wrkMast, now);
                        boolean offered = offerDevpCommandWithDedup(stationObjModel.getDeviceNo(), command, "crnStationOutExecute");
                        if (offered && stationMoveCoordinator != null) {
                            stationMoveCoordinator.recordDispatch(
                                    wrkMast.getWrkNo(),
                                    stationProtocol.getStationId(),
                                    "crnStationOutExecute",
                                    command,
                                    false
                            );
                        }
                        News.info("输送站点出库命令下发成功,站点号={},工作号={},命令数据={}",
                                stationProtocol.getStationId(), wrkMast.getWrkNo(), JSON.toJSONString(command));
                        redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId(), "lock", 5);
                        redisUtil.del(RedisKeyType.CRN_OUT_TASK_COMPLETE_STATION_INFO.key + wrkMast.getWrkNo());
                        currentStationTaskCountRef[0]++;
                        loadGuardState.reserveLoopTask(loopHitResult.getLoopNo());
                        stationDispatchLoadSupport.saveLoopLoadReserve(wrkMast.getWrkNo(), loopHitResult);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void dualCrnStationOutExecute() {
        try {
            List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>()
                    .eq("wrk_sts", WrkStsType.OUTBOUND_RUN_COMPLETE.sts)
                    .isNotNull("dual_crn_no"));
            for (WrkMast wrkMast : wrkMasts) {
                Object infoObj = redisUtil.get(RedisKeyType.DUAL_CRN_OUT_TASK_STATION_INFO.key + wrkMast.getWrkNo());
                if (infoObj == null) {
                    News.info("出库任务{}数据缓存不存在", wrkMast.getWrkNo());
                    continue;
                }
                StationObjModel stationObjModel = JSON.parseObject(infoObj.toString(), StationObjModel.class);
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo());
                if (stationThread == null) {
                    continue;
                }
                Map<Integer, StationProtocol> stationMap = stationThread.getStatusMap();
                StationProtocol stationProtocol = stationMap.get(stationObjModel.getStationId());
                if (stationProtocol == null) {
                    continue;
                }
                Object lock = redisUtil.get(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId());
                if (lock != null) {
                    continue;
                }
                if (stationProtocol.isAutoing()
                        && stationProtocol.isLoading()
                        && stationProtocol.getTaskNo() == 0) {
                    Double pathLenFactor = stationOutboundDecisionSupport.resolveOutboundPathLenFactor(wrkMast);
                    StationCommand command = stationOutboundDecisionSupport.buildOutboundMoveCommand(
                            stationThread,
                            wrkMast,
                            stationProtocol.getStationId(),
                            wrkMast.getStaNo(),
                            pathLenFactor
                    );
                    if (command == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
                        continue;
                    }
                    wrkMast.setWrkSts(WrkStsType.STATION_RUN.sts);
                    wrkMast.setSystemMsg("");
                    wrkMast.setIoTime(new Date());
                    if (wrkMastService.updateById(wrkMast)) {
                        boolean offered = offerDevpCommandWithDedup(stationObjModel.getDeviceNo(), command, "dualCrnStationOutExecute");
                        if (offered && stationMoveCoordinator != null) {
                            stationMoveCoordinator.recordDispatch(
                                    wrkMast.getWrkNo(),
                                    stationProtocol.getStationId(),
                                    "dualCrnStationOutExecute",
                                    command,
                                    false
                            );
                        }
                        notifyUtils.notify(String.valueOf(SlaveType.Devp), stationObjModel.getDeviceNo(),
                                String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(),
                                NotifyMsgType.STATION_OUT_TASK_RUN, null);
                        News.info("输送站点出库命令下发成功,站点号={},工作号={},命令数据={}",
                                stationProtocol.getStationId(), wrkMast.getWrkNo(), JSON.toJSONString(command));
                        redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId(), "lock", 5);
                        redisUtil.del(RedisKeyType.DUAL_CRN_OUT_TASK_STATION_INFO.key + wrkMast.getWrkNo());
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private boolean offerDevpCommandWithDedup(Integer deviceNo, StationCommand command, String scene) {
        StationCommandDispatchResult dispatchResult = stationCommandDispatcher
                .dispatch(deviceNo, command, "station-operate-process", scene);
        return dispatchResult.isAccepted();
    }
}
src/main/java/com/zy/core/utils/station/StationRegularDispatchProcessor.java
New file
@@ -0,0 +1,284 @@
package com.zy.core.utils.station;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zy.asrs.domain.enums.NotifyMsgType;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.BasStation;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.BasStationService;
import com.zy.asrs.service.WrkAnalysisService;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.utils.NotifyUtils;
import com.zy.common.entity.FindCrnNoResult;
import com.zy.common.service.CommonService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatchResult;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.enums.WrkStsType;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.model.protocol.StationTaskBufferItem;
import com.zy.core.move.StationMoveCoordinator;
import com.zy.core.thread.StationThread;
import com.zy.core.utils.station.model.DispatchLimitConfig;
import com.zy.core.utils.station.model.LoadGuardState;
import com.zy.core.utils.station.model.LoopHitResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Component
public class StationRegularDispatchProcessor {
    @Autowired
    private BasDevpService basDevpService;
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private CommonService commonService;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private WrkAnalysisService wrkAnalysisService;
    @Autowired
    private BasStationService basStationService;
    @Autowired
    private NotifyUtils notifyUtils;
    @Autowired
    private StationMoveCoordinator stationMoveCoordinator;
    @Autowired(required = false)
    private StationCommandDispatcher stationCommandDispatcher;
    @Autowired
    private StationDispatchLoadSupport stationDispatchLoadSupport;
    public void stationInExecute() {
        try {
            DispatchLimitConfig baseLimitConfig = stationDispatchLoadSupport.getDispatchLimitConfig(null, null);
            int[] currentStationTaskCountRef = new int[]{stationDispatchLoadSupport.countCurrentStationTask()};
            LoadGuardState loadGuardState = stationDispatchLoadSupport.buildLoadGuardState(baseLimitConfig);
            List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<>());
            for (BasDevp basDevp : basDevps) {
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
                if (stationThread == null) {
                    continue;
                }
                Map<Integer, StationProtocol> stationMap = stationThread.getStatusMap();
                List<StationObjModel> stationList = basDevp.getBarcodeStationList$();
                for (StationObjModel entity : stationList) {
                    Integer stationId = entity.getStationId();
                    if (!stationMap.containsKey(stationId)) {
                        continue;
                    }
                    StationProtocol stationProtocol = stationMap.get(stationId);
                    if (stationProtocol == null) {
                        continue;
                    }
                    Object lock = redisUtil.get(RedisKeyType.STATION_IN_EXECUTE_LIMIT.key + stationId);
                    if (lock != null) {
                        continue;
                    }
                    if (!stationProtocol.isAutoing()
                            || !stationProtocol.isLoading()
                            || stationProtocol.getTaskNo() <= 0) {
                        continue;
                    }
                    WrkMast wrkMast = wrkMastService.getOne(new QueryWrapper<WrkMast>().eq("barcode", stationProtocol.getBarcode()));
                    if (wrkMast == null || !Objects.equals(wrkMast.getWrkSts(), WrkStsType.NEW_INBOUND.sts)) {
                        continue;
                    }
                    String locNo = wrkMast.getLocNo();
                    FindCrnNoResult findCrnNoResult = commonService.findCrnNoByLocNo(locNo);
                    if (findCrnNoResult == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "{}工作,未匹配到堆垛机", wrkMast.getWrkNo());
                        continue;
                    }
                    Integer targetStationId = commonService.findInStationId(findCrnNoResult, stationId);
                    if (targetStationId == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "{}站点,搜索入库站点失败", stationId);
                        continue;
                    }
                    DispatchLimitConfig limitConfig = stationDispatchLoadSupport.getDispatchLimitConfig(stationProtocol.getStationId(), targetStationId);
                    LoopHitResult loopHitResult = stationDispatchLoadSupport.findPathLoopHit(
                            limitConfig,
                            stationProtocol.getStationId(),
                            targetStationId,
                            loadGuardState
                    );
                    if (stationDispatchLoadSupport.isDispatchBlocked(limitConfig, currentStationTaskCountRef[0], loadGuardState, loopHitResult.isThroughLoop())) {
                        return;
                    }
                    StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationId, targetStationId, 0);
                    if (command == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "{}工作,获取输送线命令失败", wrkMast.getWrkNo());
                        continue;
                    }
                    Date now = new Date();
                    wrkMast.setWrkSts(WrkStsType.INBOUND_STATION_RUN.sts);
                    wrkMast.setSourceStaNo(stationProtocol.getStationId());
                    wrkMast.setStaNo(targetStationId);
                    wrkMast.setSystemMsg("");
                    wrkMast.setIoTime(now);
                    wrkMast.setModiTime(now);
                    if (wrkMastService.updateById(wrkMast)) {
                        wrkAnalysisService.markInboundStationStart(wrkMast, now);
                        boolean offered = offerDevpCommandWithDedup(basDevp.getDevpNo(), command, "stationInExecute");
                        if (offered && stationMoveCoordinator != null) {
                            stationMoveCoordinator.recordDispatch(
                                    wrkMast.getWrkNo(),
                                    stationProtocol.getStationId(),
                                    "stationInExecute",
                                    command,
                                    false
                            );
                        }
                        News.info("输送站点入库命令下发成功,站点号={},工作号={},命令数据={}", stationId, wrkMast.getWrkNo(), JSON.toJSONString(command));
                        redisUtil.set(RedisKeyType.STATION_IN_EXECUTE_LIMIT.key + stationId, "lock", 5);
                        loadGuardState.reserveLoopTask(loopHitResult.getLoopNo());
                        stationDispatchLoadSupport.saveLoopLoadReserve(wrkMast.getWrkNo(), loopHitResult);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void stationOutExecuteFinish() {
        try {
            List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>().eq("wrk_sts", WrkStsType.STATION_RUN.sts));
            for (WrkMast wrkMast : wrkMasts) {
                Integer wrkNo = wrkMast.getWrkNo();
                Integer targetStaNo = wrkMast.getStaNo();
                if (wrkNo == null || targetStaNo == null) {
                    continue;
                }
                boolean complete = false;
                Integer targetDeviceNo = null;
                StationThread stationThread = null;
                BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>().eq("station_id", targetStaNo));
                if (basStation != null) {
                    targetDeviceNo = basStation.getDeviceNo();
                    stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basStation.getDeviceNo());
                    if (stationThread != null) {
                        Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
                        StationProtocol stationProtocol = statusMap.get(basStation.getStationId());
                        if (stationProtocol != null && wrkNo.equals(stationProtocol.getTaskNo())) {
                            complete = true;
                        }
                    }
                }
                if (complete) {
                    attemptClearTaskPath(stationThread, wrkNo);
                    completeStationRunTask(wrkMast, targetDeviceNo);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void checkTaskToComplete() {
        try {
            List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>().eq("wrk_sts", WrkStsType.STATION_RUN_COMPLETE.sts));
            for (WrkMast wrkMast : wrkMasts) {
                Integer wrkNo = wrkMast.getWrkNo();
                Integer targetStaNo = wrkMast.getStaNo();
                Object lock = redisUtil.get(RedisKeyType.STATION_OUT_EXECUTE_COMPLETE_LIMIT.key + wrkNo);
                if (lock != null) {
                    continue;
                }
                BasStation basStation = basStationService.getOne(new QueryWrapper<BasStation>().eq("station_id", targetStaNo));
                if (basStation == null) {
                    continue;
                }
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basStation.getDeviceNo());
                if (stationThread == null) {
                    continue;
                }
                Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
                StationProtocol stationProtocol = statusMap.get(basStation.getStationId());
                if (stationProtocol == null) {
                    continue;
                }
                if (!Objects.equals(stationProtocol.getTaskNo(), wrkNo)) {
                    if (stationMoveCoordinator != null) {
                        stationMoveCoordinator.finishSession(wrkNo);
                    }
                    wrkMast.setWrkSts(WrkStsType.COMPLETE_OUTBOUND.sts);
                    wrkMast.setIoTime(new Date());
                    wrkMastService.updateById(wrkMast);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void attemptClearTaskPath(StationThread stationThread, Integer taskNo) {
        if (stationThread == null || taskNo == null || taskNo <= 0) {
            return;
        }
        try {
            boolean cleared = stationThread.clearPath(taskNo);
            if (cleared) {
                News.info("输送站点任务运行完成后清理残留路径,工作号={}", taskNo);
            }
        } catch (Exception e) {
            News.error("输送站点任务运行完成后清理残留路径异常,工作号={}", taskNo, e);
        }
    }
    private void completeStationRunTask(WrkMast wrkMast, Integer deviceNo) {
        if (wrkMast == null || wrkMast.getWrkNo() == null) {
            return;
        }
        if (stationMoveCoordinator != null) {
            stationMoveCoordinator.finishSession(wrkMast.getWrkNo());
        }
        Date now = new Date();
        wrkMast.setWrkSts(WrkStsType.STATION_RUN_COMPLETE.sts);
        wrkMast.setIoTime(now);
        wrkMast.setModiTime(now);
        wrkMastService.updateById(wrkMast);
        wrkAnalysisService.markOutboundStationComplete(wrkMast, now);
        if (deviceNo != null) {
            notifyUtils.notify(String.valueOf(SlaveType.Devp), deviceNo, String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.STATION_OUT_TASK_RUN_COMPLETE, null);
        }
        redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_COMPLETE_LIMIT.key + wrkMast.getWrkNo(), "lock", 60);
    }
    private boolean offerDevpCommandWithDedup(Integer deviceNo, StationCommand command, String scene) {
        StationCommandDispatchResult dispatchResult = stationCommandDispatcher.dispatch(deviceNo, command, "station-operate-process", scene);
        return dispatchResult.isAccepted();
    }
}
src/main/java/com/zy/core/utils/station/StationRerouteProcessor.java
New file
@@ -0,0 +1,734 @@
package com.zy.core.utils.station;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.core.common.Cools;
import com.core.exception.CoolException;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.LocMast;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.LocMastService;
import com.zy.asrs.service.WrkMastService;
import com.zy.common.entity.FindCrnNoResult;
import com.zy.common.model.StartupDto;
import com.zy.common.service.CommonService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatchResult;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.enums.WrkIoType;
import com.zy.core.enums.WrkStsType;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.model.protocol.StationTaskBufferItem;
import com.zy.core.move.StationMoveCoordinator;
import com.zy.core.move.StationMoveSession;
import com.zy.core.thread.StationThread;
import com.zy.core.utils.station.model.OutOrderDispatchDecision;
import com.zy.core.utils.station.model.RerouteCommandPlan;
import com.zy.core.utils.station.model.RerouteContext;
import com.zy.core.utils.station.model.RerouteDecision;
import com.zy.core.utils.station.model.RerouteExecutionResult;
import com.zy.core.utils.station.model.RerouteSceneType;
import com.zy.core.utils.WmsOperateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Component
public class StationRerouteProcessor {
    private static final int OUT_ORDER_DISPATCH_LIMIT_SECONDS = 2;
    private static final int STATION_IDLE_RECOVER_SECONDS = 10;
    private static final int STATION_IDLE_RECOVER_LIMIT_SECONDS = 30;
    private static final long STATION_MOVE_RESET_WAIT_MS = 1000L;
    @Autowired
    private BasDevpService basDevpService;
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private CommonService commonService;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private LocMastService locMastService;
    @Autowired
    private WmsOperateUtils wmsOperateUtils;
    @Autowired
    private StationMoveCoordinator stationMoveCoordinator;
    @Autowired
    private StationCommandDispatcher stationCommandDispatcher;
    @Autowired
    private StationOutboundDecisionSupport stationOutboundDecisionSupport;
    @Autowired
    private StationDispatchRuntimeStateSupport stationDispatchRuntimeStateSupport;
    public void checkStationRunBlock() {
        try {
            List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<>());
            for (BasDevp basDevp : basDevps) {
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
                if (stationThread == null) {
                    continue;
                }
                List<Integer> runBlockReassignLocStationList = new ArrayList<>();
                for (StationObjModel stationObjModel : basDevp.getRunBlockReassignLocStationList$()) {
                    runBlockReassignLocStationList.add(stationObjModel.getStationId());
                }
                List<Integer> outOrderStationIds = basDevp.getOutOrderIntList();
                for (StationProtocol stationProtocol : stationThread.getStatus()) {
                    if (stationProtocol.isAutoing()
                            && stationProtocol.isLoading()
                            && stationProtocol.getTaskNo() > 0
                            && stationProtocol.isRunBlock()) {
                        WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
                        if (wrkMast == null) {
                            News.info("输送站点号={} 运行阻塞,但无法找到对应任务,工作号={}", stationProtocol.getStationId(), stationProtocol.getTaskNo());
                            continue;
                        }
                        Object lock = redisUtil.get(RedisKeyType.CHECK_STATION_RUN_BLOCK_LIMIT_.key + stationProtocol.getTaskNo());
                        if (lock != null) {
                            continue;
                        }
                        redisUtil.set(RedisKeyType.CHECK_STATION_RUN_BLOCK_LIMIT_.key + stationProtocol.getTaskNo(), "lock", 15);
                        if (shouldUseRunBlockDirectReassign(wrkMast, stationProtocol.getStationId(), runBlockReassignLocStationList)) {
                            executeRunBlockDirectReassign(basDevp, stationThread, stationProtocol, wrkMast);
                            continue;
                        }
                        Double pathLenFactor = stationOutboundDecisionSupport.resolveOutboundPathLenFactor(wrkMast);
                        RerouteContext context = RerouteContext.create(
                                RerouteSceneType.RUN_BLOCK_REROUTE,
                                basDevp,
                                stationThread,
                                stationProtocol,
                                wrkMast,
                                outOrderStationIds,
                                pathLenFactor,
                                "checkStationRunBlock_reroute"
                        ).withRunBlockCommand()
                                .withSuppressDispatchGuard()
                                .withCancelSessionBeforeDispatch()
                                .withResetSegmentCommandsBeforeDispatch();
                        executeSharedReroute(context);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void checkStationIdleRecover() {
        try {
            List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<>());
            for (BasDevp basDevp : basDevps) {
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
                if (stationThread == null) {
                    continue;
                }
                for (StationProtocol stationProtocol : stationThread.getStatus()) {
                    if (stationProtocol.isAutoing()
                            && stationProtocol.isLoading()
                            && stationProtocol.getTaskNo() > 0
                            && !stationProtocol.isRunBlock()) {
                        checkStationIdleRecover(basDevp, stationThread, stationProtocol, basDevp.getOutOrderIntList());
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void checkStationOutOrder() {
        List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
            if (stationThread == null) {
                continue;
            }
            Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
            List<StationObjModel> orderList = basDevp.getOutOrderList$();
            List<Integer> outOrderStationIds = basDevp.getOutOrderIntList();
            for (StationObjModel stationObjModel : orderList) {
                StationProtocol stationProtocol = statusMap.get(stationObjModel.getStationId());
                if (stationProtocol == null
                        || !stationProtocol.isAutoing()
                        || !stationProtocol.isLoading()
                        || stationProtocol.getTaskNo() <= 0
                        || stationProtocol.isRunBlock()
                        || !stationProtocol.getStationId().equals(stationProtocol.getTargetStaNo())) {
                    continue;
                }
                WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
                if (wrkMast == null
                        || !Objects.equals(wrkMast.getWrkSts(), WrkStsType.STATION_RUN.sts)
                        || Objects.equals(stationProtocol.getStationId(), wrkMast.getStaNo())) {
                    continue;
                }
                if (stationOutboundDecisionSupport.shouldSkipOutOrderDispatchForExistingRoute(wrkMast.getWrkNo(), stationProtocol.getStationId())) {
                    continue;
                }
                Double pathLenFactor = stationOutboundDecisionSupport.resolveOutboundPathLenFactor(wrkMast);
                RerouteContext context = RerouteContext.create(
                        RerouteSceneType.OUT_ORDER,
                        basDevp,
                        stationThread,
                        stationProtocol,
                        wrkMast,
                        outOrderStationIds,
                        pathLenFactor,
                        "checkStationOutOrder"
                ).withDispatchDeviceNo(stationObjModel.getDeviceNo())
                        .withSuppressDispatchGuard()
                        .withOutOrderDispatchLock()
                        .withResetSegmentCommandsBeforeDispatch();
                executeSharedReroute(context);
            }
        }
    }
    public void watchCircleStation() {
        List<BasDevp> basDevps = basDevpService.list(new QueryWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
            if (stationThread == null) {
                continue;
            }
            List<Integer> outOrderList = basDevp.getOutOrderIntList();
            for (StationProtocol stationProtocol : stationThread.getStatus()) {
                if (!stationProtocol.isAutoing()
                        || !stationProtocol.isLoading()
                        || stationProtocol.getTaskNo() <= 0
                        || !stationOutboundDecisionSupport.isWatchingCircleArrival(stationProtocol.getTaskNo(), stationProtocol.getStationId())) {
                    continue;
                }
                WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
                if (wrkMast == null
                        || !Objects.equals(wrkMast.getWrkSts(), WrkStsType.STATION_RUN.sts)
                        || Objects.equals(stationProtocol.getStationId(), wrkMast.getStaNo())) {
                    continue;
                }
                Double pathLenFactor = stationOutboundDecisionSupport.resolveOutboundPathLenFactor(wrkMast);
                RerouteContext context = RerouteContext.create(
                        RerouteSceneType.WATCH_CIRCLE,
                        basDevp,
                        stationThread,
                        stationProtocol,
                        wrkMast,
                        outOrderList,
                        pathLenFactor,
                        "watchCircleStation"
                ).withSuppressDispatchGuard()
                        .withOutOrderDispatchLock()
                        .withResetSegmentCommandsBeforeDispatch();
                executeSharedReroute(context);
            }
        }
    }
    public RerouteCommandPlan buildRerouteCommandPlan(RerouteContext context,
                                                      RerouteDecision decision) {
        if (context == null) {
            return RerouteCommandPlan.skip("missing-context");
        }
        if (decision == null) {
            return RerouteCommandPlan.skip("missing-decision");
        }
        if (decision.skip()) {
            return RerouteCommandPlan.skip(decision.skipReason());
        }
        if (context.stationThread() == null || context.stationProtocol() == null || context.wrkMast() == null) {
            return RerouteCommandPlan.skip("missing-runtime-dependency");
        }
        Integer currentStationId = context.stationProtocol().getStationId();
        Integer targetStationId = decision.targetStationId();
        if (currentStationId == null || targetStationId == null) {
            return RerouteCommandPlan.skip("missing-target-station");
        }
        if (Objects.equals(currentStationId, targetStationId)) {
            return RerouteCommandPlan.skip("same-station");
        }
        StationCommand command = context.useRunBlockCommand()
                ? context.stationThread().getRunBlockRerouteCommand(
                context.wrkMast().getWrkNo(),
                currentStationId,
                targetStationId,
                0,
                context.pathLenFactor()
        )
                : stationOutboundDecisionSupport.buildOutboundMoveCommand(
                context.stationThread(),
                context.wrkMast(),
                currentStationId,
                targetStationId,
                context.pathLenFactor()
        );
        if (command == null) {
            if (context.sceneType() == RerouteSceneType.RUN_BLOCK_REROUTE) {
                News.taskInfo(context.wrkMast().getWrkNo(),
                        "输送站点堵塞重规划未找到可下发路线,当前站点={},目标站点={}",
                        currentStationId,
                        targetStationId);
            } else if (context.sceneType() == RerouteSceneType.IDLE_RECOVER) {
                News.taskInfo(context.wrkMast().getWrkNo(),
                        "站点任务停留超时后重算路径失败,当前站点={},目标站点={}",
                        currentStationId,
                        targetStationId);
            } else {
                News.taskInfo(context.wrkMast().getWrkNo(), "获取输送线命令失败");
            }
            return RerouteCommandPlan.skip("missing-command");
        }
        return RerouteCommandPlan.dispatch(command, decision, context.dispatchScene());
    }
    public RerouteExecutionResult executeReroutePlan(RerouteContext context,
                                                     RerouteCommandPlan plan) {
        if (context == null) {
            return RerouteExecutionResult.skip("missing-context");
        }
        if (plan == null) {
            return RerouteExecutionResult.skip("missing-plan");
        }
        if (plan.skip()) {
            return RerouteExecutionResult.skip(plan.skipReason());
        }
        StationProtocol stationProtocol = context.stationProtocol();
        if (stationProtocol == null) {
            return RerouteExecutionResult.skip("missing-station-protocol");
        }
        Integer taskNo = stationProtocol.getTaskNo();
        Integer stationId = stationProtocol.getStationId();
        if (taskNo == null || taskNo <= 0 || stationId == null) {
            return RerouteExecutionResult.skip("invalid-station-task");
        }
        if (stationMoveCoordinator != null) {
            return stationMoveCoordinator.withTaskDispatchLock(taskNo,
                    () -> executeReroutePlanWithTaskLock(context, plan, stationProtocol, taskNo, stationId));
        }
        return executeReroutePlanWithTaskLock(context, plan, stationProtocol, taskNo, stationId);
    }
    public RerouteDecision resolveSharedRerouteDecision(RerouteContext context) {
        if (context == null || context.wrkMast() == null || context.stationProtocol() == null) {
            return RerouteDecision.skip("missing-runtime-dependency");
        }
        Integer currentStationId = context.stationProtocol().getStationId();
        if (currentStationId == null) {
            return RerouteDecision.skip("missing-current-station");
        }
        if (context.sceneType() == RerouteSceneType.IDLE_RECOVER
                && !Objects.equals(context.wrkMast().getWrkSts(), WrkStsType.STATION_RUN.sts)) {
            Integer targetStationId = context.wrkMast().getStaNo();
            return targetStationId == null || Objects.equals(targetStationId, currentStationId)
                    ? RerouteDecision.skip("same-station")
                    : RerouteDecision.proceed(targetStationId);
        }
        OutOrderDispatchDecision dispatchDecision =
                stationOutboundDecisionSupport.resolveOutboundDispatchDecision(
                        currentStationId,
                        context.wrkMast(),
                        context.outOrderStationIds(),
                        context.pathLenFactor()
                );
        Integer targetStationId = dispatchDecision == null ? null : dispatchDecision.getTargetStationId();
        if (targetStationId == null || Objects.equals(targetStationId, currentStationId)) {
            return RerouteDecision.skip("same-station");
        }
        return RerouteDecision.proceed(targetStationId, dispatchDecision);
    }
    public boolean shouldUseRunBlockDirectReassign(WrkMast wrkMast,
                                                   Integer stationId,
                                                   List<Integer> runBlockReassignLocStationList) {
        return wrkMast != null
                && Objects.equals(wrkMast.getIoType(), WrkIoType.IN.id)
                && stationId != null
                && runBlockReassignLocStationList != null
                && runBlockReassignLocStationList.contains(stationId);
    }
    public boolean shouldSkipIdleRecoverForRecentDispatch(Integer taskNo, Integer stationId) {
        if (taskNo == null || taskNo <= 0 || stationId == null) {
            return false;
        }
        long thresholdMs = STATION_IDLE_RECOVER_SECONDS * 1000L;
        StationMoveSession session = stationMoveCoordinator == null ? null : stationMoveCoordinator.loadSession(taskNo);
        if (session != null && session.isActive() && session.getLastIssuedAt() != null) {
            if (Objects.equals(stationId, session.getCurrentStationId())
                    || Objects.equals(stationId, session.getDispatchStationId())
                    || session.containsStation(stationId)) {
                long elapsedMs = System.currentTimeMillis() - session.getLastIssuedAt();
                if (elapsedMs < thresholdMs) {
                    stationDispatchRuntimeStateSupport.saveIdleTrack(new StationTaskIdleTrack(taskNo, stationId, System.currentTimeMillis()));
                    News.info("输送站点任务刚完成命令下发,已跳过停留重算。站点号={},工作号={},距上次下发={}ms,routeVersion={}",
                            stationId, taskNo, elapsedMs, session.getRouteVersion());
                    return true;
                }
            }
        }
        if (!stationDispatchRuntimeStateSupport.hasRecentIssuedMoveCommand(taskNo, stationId, thresholdMs)) {
            return false;
        }
        stationDispatchRuntimeStateSupport.saveIdleTrack(new StationTaskIdleTrack(taskNo, stationId, System.currentTimeMillis()));
        News.info("输送站点任务刚完成命令下发,已跳过停留重算。站点号={},工作号={},距最近命令下发<{}ms,routeVersion={}",
                stationId, taskNo, thresholdMs, session == null ? null : session.getRouteVersion());
        return true;
    }
    private RerouteExecutionResult executeReroutePlanWithTaskLock(RerouteContext context,
                                                                  RerouteCommandPlan plan,
                                                                  StationProtocol stationProtocol,
                                                                  Integer taskNo,
                                                                  Integer stationId) {
        boolean runBlockReroute = context.sceneType() == RerouteSceneType.RUN_BLOCK_REROUTE;
        if (context.checkRecentDispatch()
                && shouldSkipIdleRecoverForRecentDispatch(taskNo, stationId)) {
            return RerouteExecutionResult.skip("recent-dispatch");
        }
        int currentTaskBufferCommandCount = countCurrentTaskBufferCommands(stationProtocol.getTaskBufferItems(), taskNo);
        if (currentTaskBufferCommandCount > 0 && !runBlockReroute) {
            if (context.sceneType() == RerouteSceneType.IDLE_RECOVER) {
                News.info("输送站点任务停留超时,但缓存区仍存在当前任务命令,已跳过重算。站点号={},工作号={},当前任务命令数={}",
                        stationId,
                        taskNo,
                        currentTaskBufferCommandCount);
            }
            return RerouteExecutionResult.skip("buffer-has-current-task");
        }
        if (currentTaskBufferCommandCount > 0 && runBlockReroute) {
            News.info("输送站点运行堵塞重规划检测到旧分段命令残留,已先清理本地状态后继续重发。站点号={},工作号={},当前任务命令数={}",
                    stationId,
                    taskNo,
                    currentTaskBufferCommandCount);
        }
        if (!runBlockReroute
                && context.checkSuppressDispatch()
                && stationMoveCoordinator != null
                && stationMoveCoordinator.shouldSuppressDispatch(taskNo, stationId, plan.command())) {
            return RerouteExecutionResult.skip("dispatch-suppressed");
        }
        if (context.requireOutOrderDispatchLock()
                && !stationDispatchRuntimeStateSupport.tryAcquireOutOrderDispatchLock(taskNo, stationId, OUT_ORDER_DISPATCH_LIMIT_SECONDS)) {
            return RerouteExecutionResult.skip("out-order-lock");
        }
        if (context.cancelSessionBeforeDispatch() && stationMoveCoordinator != null) {
            stationMoveCoordinator.markCancelPending(taskNo, "reroute_pending");
        }
        if (runBlockReroute) {
            if (context.cancelSessionBeforeDispatch() && stationMoveCoordinator != null) {
                stationMoveCoordinator.cancelSession(taskNo);
            }
            if (context.resetSegmentCommandsBeforeDispatch()) {
                stationDispatchRuntimeStateSupport.signalSegmentReset(taskNo, STATION_MOVE_RESET_WAIT_MS);
            }
        }
        if (!runBlockReroute
                && context.cancelSessionBeforeDispatch() && stationMoveCoordinator != null) {
            stationMoveCoordinator.cancelSession(taskNo);
        }
        if (!isBlank(context.executionLockKey())
                && !stationDispatchRuntimeStateSupport.tryAcquireLock(context.executionLockKey(), context.executionLockSeconds())) {
                return RerouteExecutionResult.skip("scene-lock");
        }
        if (!runBlockReroute && context.resetSegmentCommandsBeforeDispatch()) {
            stationDispatchRuntimeStateSupport.signalSegmentReset(taskNo, STATION_MOVE_RESET_WAIT_MS);
        }
        int clearedCommandCount = 0;
        if (context.clearIdleIssuedCommands()) {
            clearedCommandCount = stationDispatchRuntimeStateSupport.clearIssuedMoveCommandsDuringIdleStay(context.idleTrack(), taskNo, stationId);
        }
        boolean offered = offerDevpCommandWithDedup(context.dispatchDeviceNo(), plan.command(), plan.dispatchScene());
        if (!offered) {
            return RerouteExecutionResult.skip("dispatch-dedup");
        }
        applyRerouteDispatchEffects(context, plan, clearedCommandCount);
        return RerouteExecutionResult.dispatched(plan.command(), clearedCommandCount);
    }
    private RerouteExecutionResult executeSharedReroute(RerouteContext context) {
        RerouteDecision decision = resolveSharedRerouteDecision(context);
        if (decision.skip()) {
            return RerouteExecutionResult.skip(decision.skipReason());
        }
        RerouteCommandPlan plan = buildRerouteCommandPlan(context, decision);
        return executeReroutePlan(context, plan);
    }
    private void applyRerouteDispatchEffects(RerouteContext context,
                                             RerouteCommandPlan plan,
                                             int clearedCommandCount) {
        if (context == null || plan == null || plan.command() == null || context.wrkMast() == null || context.stationProtocol() == null) {
            return;
        }
        WrkMast wrkMast = context.wrkMast();
        StationProtocol stationProtocol = context.stationProtocol();
        OutOrderDispatchDecision dispatchDecision =
                plan.decision() == null ? null : plan.decision().dispatchDecision();
        stationOutboundDecisionSupport.syncOutOrderWatchState(
                wrkMast,
                stationProtocol.getStationId(),
                context.outOrderStationIds(),
                dispatchDecision,
                plan.command()
        );
        if (stationMoveCoordinator != null) {
            stationMoveCoordinator.recordDispatch(
                    wrkMast.getWrkNo(),
                    stationProtocol.getStationId(),
                    plan.dispatchScene(),
                    plan.command(),
                    dispatchDecision != null && dispatchDecision.isCircle()
            );
        }
        if (context.sceneType() == RerouteSceneType.IDLE_RECOVER) {
            stationDispatchRuntimeStateSupport.saveIdleTrack(new StationTaskIdleTrack(wrkMast.getWrkNo(), stationProtocol.getStationId(), System.currentTimeMillis()));
            News.info("输送站点任务停留{}秒未运行,已重新计算路径并重启运行,站点号={},目标站={},工作号={},清理旧分段命令数={},命令数据={}",
                    STATION_IDLE_RECOVER_SECONDS,
                    stationProtocol.getStationId(),
                    plan.command().getTargetStaNo(),
                    wrkMast.getWrkNo(),
                    clearedCommandCount,
                    JSON.toJSONString(plan.command()));
            return;
        }
        if (context.sceneType() == RerouteSceneType.RUN_BLOCK_REROUTE) {
            News.info("输送站点堵塞后重新计算路径命令下发成功,站点号={},工作号={},命令数据={}",
                    stationProtocol.getStationId(),
                    wrkMast.getWrkNo(),
                    JSON.toJSONString(plan.command()));
            return;
        }
        if (context.sceneType() == RerouteSceneType.OUT_ORDER) {
            News.info(dispatchDecision != null && dispatchDecision.isCircle() ? "{}任务进行绕圈" : "{}任务直接去目标点", wrkMast.getWrkNo());
        }
    }
    private void checkStationIdleRecover(BasDevp basDevp,
                                         StationThread stationThread,
                                         StationProtocol stationProtocol,
                                         List<Integer> outOrderList) {
        if (stationProtocol == null || stationProtocol.getTaskNo() == null || stationProtocol.getTaskNo() <= 0) {
            return;
        }
        if (!Objects.equals(stationProtocol.getStationId(), stationProtocol.getTargetStaNo())) {
            return;
        }
        StationTaskIdleTrack idleTrack = stationDispatchRuntimeStateSupport.touchIdleTrack(stationProtocol.getTaskNo(), stationProtocol.getStationId());
        if (shouldSkipIdleRecoverForRecentDispatch(stationProtocol.getTaskNo(), stationProtocol.getStationId())) {
            return;
        }
        if (idleTrack == null || !idleTrack.isTimeout(STATION_IDLE_RECOVER_SECONDS)) {
            return;
        }
        WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
        if (!canRecoverIdleStationTask(wrkMast, stationProtocol.getStationId())) {
            return;
        }
        Object lock = redisUtil.get(RedisKeyType.CHECK_STATION_IDLE_RECOVER_LIMIT_.key + stationProtocol.getTaskNo());
        if (lock != null) {
            return;
        }
        Double pathLenFactor = stationOutboundDecisionSupport.resolveOutboundPathLenFactor(wrkMast);
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.IDLE_RECOVER,
                basDevp,
                stationThread,
                stationProtocol,
                wrkMast,
                outOrderList,
                pathLenFactor,
                "checkStationIdleRecover"
        ).withCancelSessionBeforeDispatch()
                .withExecutionLock(RedisKeyType.CHECK_STATION_IDLE_RECOVER_LIMIT_.key + stationProtocol.getTaskNo(), STATION_IDLE_RECOVER_LIMIT_SECONDS)
                .withResetSegmentCommandsBeforeDispatch()
                .clearIdleIssuedCommands(idleTrack);
        executeSharedReroute(context);
    }
    private void executeRunBlockDirectReassign(BasDevp basDevp,
                                               StationThread stationThread,
                                               StationProtocol stationProtocol,
                                               WrkMast wrkMast) {
        if (basDevp == null || stationThread == null || stationProtocol == null || wrkMast == null) {
            return;
        }
        int currentTaskBufferCommandCount = countCurrentTaskBufferCommands(
                stationProtocol.getTaskBufferItems(),
                stationProtocol.getTaskNo()
        );
        if (currentTaskBufferCommandCount > 0) {
            News.info("输送站点运行堵塞重分配已跳过,缓存区仍存在当前任务命令。站点号={},工作号={},当前任务命令数={}",
                    stationProtocol.getStationId(),
                    stationProtocol.getTaskNo(),
                    currentTaskBufferCommandCount);
            return;
        }
        if (stationMoveCoordinator != null) {
            stationMoveCoordinator.cancelSession(wrkMast.getWrkNo());
        }
        String response = wmsOperateUtils.applyReassignTaskLocNo(wrkMast.getWrkNo(), stationProtocol.getStationId());
        if (Cools.isEmpty(response)) {
            News.taskError(wrkMast.getWrkNo(), "请求WMS重新分配库位接口失败,接口未响应!!!response:{}", response);
            return;
        }
        JSONObject jsonObject = JSON.parseObject(response);
        if (!jsonObject.getInteger("code").equals(200)) {
            News.error("请求WMS接口失败!!!response:{}", response);
            return;
        }
        StartupDto dto = jsonObject.getObject("data", StartupDto.class);
        String sourceLocNo = wrkMast.getLocNo();
        String locNo = dto.getLocNo();
        LocMast sourceLocMast = locMastService.queryByLoc(sourceLocNo);
        if (sourceLocMast == null) {
            News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 源库位信息不存在", sourceLocNo);
            return;
        }
        if (!sourceLocMast.getLocSts().equals("S")) {
            News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 源库位状态不处于入库预约", sourceLocNo);
            return;
        }
        LocMast locMast = locMastService.queryByLoc(locNo);
        if (locMast == null) {
            News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 目标库位信息不存在", locNo);
            return;
        }
        if (!locMast.getLocSts().equals("O")) {
            News.taskInfo(wrkMast.getWrkNo(), "库位号:{} 目标库位状态不处于空库位", locNo);
            return;
        }
        FindCrnNoResult findCrnNoResult = commonService.findCrnNoByLocNo(locNo);
        if (findCrnNoResult == null) {
            News.taskInfo(wrkMast.getWrkNo(), "{}工作,未匹配到堆垛机", wrkMast.getWrkNo());
            return;
        }
        Integer crnNo = findCrnNoResult.getCrnNo();
        Integer targetStationId = commonService.findInStationId(findCrnNoResult, stationProtocol.getStationId());
        if (targetStationId == null) {
            News.taskInfo(wrkMast.getWrkNo(), "{}站点,搜索入库站点失败", stationProtocol.getStationId());
            return;
        }
        StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), targetStationId, 0);
        if (command == null) {
            News.taskInfo(wrkMast.getWrkNo(), "{}工作,获取输送线命令失败", wrkMast.getWrkNo());
            return;
        }
        sourceLocMast.setLocSts("O");
        sourceLocMast.setModiTime(new Date());
        locMastService.updateById(sourceLocMast);
        locMast.setLocSts("S");
        locMast.setModiTime(new Date());
        locMastService.updateById(locMast);
        wrkMast.setLocNo(locNo);
        wrkMast.setStaNo(targetStationId);
        if (findCrnNoResult.getCrnType().equals(SlaveType.Crn)) {
            wrkMast.setCrnNo(crnNo);
        } else if (findCrnNoResult.getCrnType().equals(SlaveType.DualCrn)) {
            wrkMast.setDualCrnNo(crnNo);
        } else {
            throw new CoolException("未知设备类型");
        }
        if (!wrkMastService.updateById(wrkMast)) {
            return;
        }
        boolean offered = offerDevpCommandWithDedup(basDevp.getDevpNo(), command, "checkStationRunBlock_direct");
        if (!offered) {
            return;
        }
        if (stationMoveCoordinator != null) {
            stationMoveCoordinator.recordDispatch(
                    wrkMast.getWrkNo(),
                    stationProtocol.getStationId(),
                    "checkStationRunBlock_direct",
                    command,
                    false
            );
        }
    }
    private boolean canRecoverIdleStationTask(WrkMast wrkMast, Integer currentStationId) {
        if (wrkMast == null || currentStationId == null || wrkMast.getStaNo() == null) {
            return false;
        }
        if (Objects.equals(currentStationId, wrkMast.getStaNo())) {
            return false;
        }
        return Objects.equals(wrkMast.getWrkSts(), WrkStsType.INBOUND_STATION_RUN.sts)
                || Objects.equals(wrkMast.getWrkSts(), WrkStsType.STATION_RUN.sts);
    }
    private int countCurrentTaskBufferCommands(List<StationTaskBufferItem> taskBufferItems, Integer currentTaskNo) {
        if (taskBufferItems == null || taskBufferItems.isEmpty() || currentTaskNo == null || currentTaskNo <= 0) {
            return 0;
        }
        int count = 0;
        for (StationTaskBufferItem item : taskBufferItems) {
            if (item == null || item.getTaskNo() == null) {
                continue;
            }
            if (currentTaskNo.equals(item.getTaskNo())) {
                count++;
            }
        }
        return count;
    }
    private boolean offerDevpCommandWithDedup(Integer deviceNo, StationCommand command, String scene) {
        StationCommandDispatchResult dispatchResult = stationCommandDispatcher
                .dispatch(deviceNo, command, "station-operate-process", scene);
        return dispatchResult.isAccepted();
    }
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
}
src/main/java/com/zy/core/utils/station/StationTaskIdleTrack.java
New file
@@ -0,0 +1,47 @@
package com.zy.core.utils.station;
public class StationTaskIdleTrack {
    private Integer taskNo;
    private Integer stationId;
    private Long firstSeenTime;
    public StationTaskIdleTrack() {
    }
    public StationTaskIdleTrack(Integer taskNo, Integer stationId, Long firstSeenTime) {
        this.taskNo = taskNo;
        this.stationId = stationId;
        this.firstSeenTime = firstSeenTime;
    }
    public boolean isTimeout(int seconds) {
        if (firstSeenTime == null) {
            return false;
        }
        return System.currentTimeMillis() - firstSeenTime >= seconds * 1000L;
    }
    public Integer getTaskNo() {
        return taskNo;
    }
    public void setTaskNo(Integer taskNo) {
        this.taskNo = taskNo;
    }
    public Integer getStationId() {
        return stationId;
    }
    public void setStationId(Integer stationId) {
        this.stationId = stationId;
    }
    public Long getFirstSeenTime() {
        return firstSeenTime;
    }
    public void setFirstSeenTime(Long firstSeenTime) {
        this.firstSeenTime = firstSeenTime;
    }
}
src/main/java/com/zy/core/utils/station/model/CircleTargetCandidate.java
New file
@@ -0,0 +1,25 @@
package com.zy.core.utils.station.model;
public final class CircleTargetCandidate {
    private final Integer stationId;
    private final Integer pathLength;
    private final Integer offset;
    public CircleTargetCandidate(Integer stationId, Integer pathLength, Integer offset) {
        this.stationId = stationId;
        this.pathLength = pathLength == null ? 0 : pathLength;
        this.offset = offset == null ? 0 : offset;
    }
    public Integer getStationId() {
        return stationId;
    }
    public Integer getPathLength() {
        return pathLength;
    }
    public Integer getOffset() {
        return offset;
    }
}
src/main/java/com/zy/core/utils/station/model/DispatchLimitConfig.java
New file
@@ -0,0 +1,11 @@
package com.zy.core.utils.station.model;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class DispatchLimitConfig {
    private double circleMaxLoadLimit = 0.8d;
    private boolean loopModeEnable = false;
}
src/main/java/com/zy/core/utils/station/model/LoadGuardState.java
New file
@@ -0,0 +1,52 @@
package com.zy.core.utils.station.model;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Getter
public class LoadGuardState {
    @Setter
    private int totalStationCount = 0;
    @Setter
    private int projectedTaskStationCount = 0;
    private final Map<Integer, Integer> stationLoopNoMap = new HashMap<>();
    public double currentLoad() {
        return calcLoad(this.projectedTaskStationCount, this.totalStationCount);
    }
    public double loadAfterReserve() {
        return calcLoad(this.projectedTaskStationCount + 1, this.totalStationCount);
    }
    public void reserveLoopTask(Integer loopNo) {
        if (loopNo == null || loopNo <= 0 || this.totalStationCount <= 0) {
            return;
        }
        this.projectedTaskStationCount++;
    }
    public void putStationLoopNo(Integer stationId, Integer loopNo) {
        if (stationId == null || loopNo == null) {
            return;
        }
        this.stationLoopNoMap.put(stationId, loopNo);
    }
    private double calcLoad(int taskCount, int stationCount) {
        if (stationCount <= 0 || taskCount <= 0) {
            return 0.0;
        }
        double load = (double) taskCount / (double) stationCount;
        if (load < 0.0) {
            return 0.0;
        }
        if (load > 1.0) {
            return 1.0;
        }
        return load;
    }
}
src/main/java/com/zy/core/utils/station/model/LoopHitResult.java
New file
@@ -0,0 +1,20 @@
package com.zy.core.utils.station.model;
import lombok.Getter;
@Getter
public class LoopHitResult {
    private final boolean throughLoop;
    private final Integer loopNo;
    private final Integer hitStationId;
    public LoopHitResult(boolean throughLoop, Integer loopNo, Integer hitStationId) {
        this.throughLoop = throughLoop;
        this.loopNo = loopNo;
        this.hitStationId = hitStationId;
    }
    public static LoopHitResult noHit() {
        return new LoopHitResult(false, null, null);
    }
}
src/main/java/com/zy/core/utils/station/model/OutOrderDispatchDecision.java
New file
@@ -0,0 +1,46 @@
package com.zy.core.utils.station.model;
import com.zy.core.service.StationTaskLoopService;
public final class OutOrderDispatchDecision {
    private final Integer targetStationId;
    private final boolean circle;
    private final StationTaskLoopService.LoopEvaluation loopEvaluation;
    private final boolean countLoopIssue;
    private OutOrderDispatchDecision(Integer targetStationId,
                                     boolean circle,
                                     StationTaskLoopService.LoopEvaluation loopEvaluation,
                                     boolean countLoopIssue) {
        this.targetStationId = targetStationId;
        this.circle = circle;
        this.loopEvaluation = loopEvaluation;
        this.countLoopIssue = countLoopIssue;
    }
    public static OutOrderDispatchDecision direct(Integer targetStationId) {
        return new OutOrderDispatchDecision(targetStationId, false, null, false);
    }
    public static OutOrderDispatchDecision circle(Integer targetStationId,
                                                  StationTaskLoopService.LoopEvaluation loopEvaluation,
                                                  boolean countLoopIssue) {
        return new OutOrderDispatchDecision(targetStationId, true, loopEvaluation, countLoopIssue);
    }
    public Integer getTargetStationId() {
        return targetStationId;
    }
    public boolean isCircle() {
        return circle;
    }
    public StationTaskLoopService.LoopEvaluation getLoopEvaluation() {
        return loopEvaluation;
    }
    public boolean shouldCountLoopIssue() {
        return countLoopIssue;
    }
}
src/main/java/com/zy/core/utils/station/model/RerouteCommandPlan.java
New file
@@ -0,0 +1,53 @@
package com.zy.core.utils.station.model;
import com.zy.core.model.command.StationCommand;
public final class RerouteCommandPlan {
    private final boolean skip;
    private final String skipReason;
    private final StationCommand command;
    private final RerouteDecision decision;
    private final String dispatchScene;
    private RerouteCommandPlan(boolean skip,
                               String skipReason,
                               StationCommand command,
                               RerouteDecision decision,
                               String dispatchScene) {
        this.skip = skip;
        this.skipReason = skipReason;
        this.command = command;
        this.decision = decision;
        this.dispatchScene = dispatchScene;
    }
    public static RerouteCommandPlan skip(String reason) {
        return new RerouteCommandPlan(true, reason, null, null, null);
    }
    public static RerouteCommandPlan dispatch(StationCommand command,
                                              RerouteDecision decision,
                                              String dispatchScene) {
        return new RerouteCommandPlan(false, null, command, decision, dispatchScene);
    }
    public boolean skip() {
        return skip;
    }
    public String skipReason() {
        return skipReason;
    }
    public StationCommand command() {
        return command;
    }
    public RerouteDecision decision() {
        return decision;
    }
    public String dispatchScene() {
        return dispatchScene;
    }
}
src/main/java/com/zy/core/utils/station/model/RerouteContext.java
New file
@@ -0,0 +1,185 @@
package com.zy.core.utils.station.model;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.WrkMast;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.thread.StationThread;
import com.zy.core.utils.station.StationTaskIdleTrack;
import java.util.Collections;
import java.util.List;
public final class RerouteContext {
    private final RerouteSceneType sceneType;
    private final BasDevp basDevp;
    private final StationThread stationThread;
    private final StationProtocol stationProtocol;
    private final WrkMast wrkMast;
    private final List<Integer> outOrderStationIds;
    private final Double pathLenFactor;
    private final String dispatchScene;
    private Integer dispatchDeviceNo;
    private boolean useRunBlockCommand;
    private boolean checkSuppressDispatch;
    private boolean requireOutOrderDispatchLock;
    private boolean cancelSessionBeforeDispatch;
    private boolean resetSegmentCommandsBeforeDispatch;
    private boolean clearIdleIssuedCommands;
    private boolean checkRecentDispatch;
    private String executionLockKey;
    private int executionLockSeconds;
    private StationTaskIdleTrack idleTrack;
    private RerouteContext(RerouteSceneType sceneType,
                           BasDevp basDevp,
                           StationThread stationThread,
                           StationProtocol stationProtocol,
                           WrkMast wrkMast,
                           List<Integer> outOrderStationIds,
                           Double pathLenFactor,
                           String dispatchScene) {
        this.sceneType = sceneType;
        this.basDevp = basDevp;
        this.stationThread = stationThread;
        this.stationProtocol = stationProtocol;
        this.wrkMast = wrkMast;
        this.outOrderStationIds = outOrderStationIds == null ? Collections.emptyList() : outOrderStationIds;
        this.pathLenFactor = pathLenFactor;
        this.dispatchScene = dispatchScene;
        this.dispatchDeviceNo = basDevp == null ? null : basDevp.getDevpNo();
    }
    public static RerouteContext create(RerouteSceneType sceneType,
                                        BasDevp basDevp,
                                        StationThread stationThread,
                                        StationProtocol stationProtocol,
                                        WrkMast wrkMast,
                                        List<Integer> outOrderStationIds,
                                        Double pathLenFactor,
                                        String dispatchScene) {
        return new RerouteContext(sceneType, basDevp, stationThread, stationProtocol, wrkMast, outOrderStationIds, pathLenFactor, dispatchScene);
    }
    public RerouteContext withDispatchDeviceNo(Integer dispatchDeviceNo) {
        this.dispatchDeviceNo = dispatchDeviceNo;
        return this;
    }
    public RerouteContext withRunBlockCommand() {
        this.useRunBlockCommand = true;
        return this;
    }
    public RerouteContext withSuppressDispatchGuard() {
        this.checkSuppressDispatch = true;
        return this;
    }
    public RerouteContext withOutOrderDispatchLock() {
        this.requireOutOrderDispatchLock = true;
        return this;
    }
    public RerouteContext withCancelSessionBeforeDispatch() {
        this.cancelSessionBeforeDispatch = true;
        return this;
    }
    public RerouteContext withResetSegmentCommandsBeforeDispatch() {
        this.resetSegmentCommandsBeforeDispatch = true;
        return this;
    }
    public RerouteContext clearIdleIssuedCommands(StationTaskIdleTrack idleTrack) {
        this.clearIdleIssuedCommands = true;
        this.idleTrack = idleTrack;
        return this;
    }
    public RerouteContext withRecentDispatchGuard() {
        this.checkRecentDispatch = true;
        return this;
    }
    public RerouteContext withExecutionLock(String executionLockKey, int executionLockSeconds) {
        this.executionLockKey = executionLockKey;
        this.executionLockSeconds = executionLockSeconds;
        return this;
    }
    public RerouteSceneType sceneType() {
        return sceneType;
    }
    public BasDevp basDevp() {
        return basDevp;
    }
    public StationThread stationThread() {
        return stationThread;
    }
    public StationProtocol stationProtocol() {
        return stationProtocol;
    }
    public WrkMast wrkMast() {
        return wrkMast;
    }
    public List<Integer> outOrderStationIds() {
        return outOrderStationIds;
    }
    public Double pathLenFactor() {
        return pathLenFactor;
    }
    public String dispatchScene() {
        return dispatchScene;
    }
    public Integer dispatchDeviceNo() {
        return dispatchDeviceNo;
    }
    public boolean useRunBlockCommand() {
        return useRunBlockCommand;
    }
    public boolean checkSuppressDispatch() {
        return checkSuppressDispatch;
    }
    public boolean requireOutOrderDispatchLock() {
        return requireOutOrderDispatchLock;
    }
    public boolean cancelSessionBeforeDispatch() {
        return cancelSessionBeforeDispatch;
    }
    public boolean resetSegmentCommandsBeforeDispatch() {
        return resetSegmentCommandsBeforeDispatch;
    }
    public boolean clearIdleIssuedCommands() {
        return clearIdleIssuedCommands;
    }
    public boolean checkRecentDispatch() {
        return checkRecentDispatch;
    }
    public String executionLockKey() {
        return executionLockKey;
    }
    public int executionLockSeconds() {
        return executionLockSeconds;
    }
    public StationTaskIdleTrack idleTrack() {
        return idleTrack;
    }
}
src/main/java/com/zy/core/utils/station/model/RerouteDecision.java
New file
@@ -0,0 +1,47 @@
package com.zy.core.utils.station.model;
public final class RerouteDecision {
    private final boolean skip;
    private final String skipReason;
    private final Integer targetStationId;
    private final OutOrderDispatchDecision dispatchDecision;
    private RerouteDecision(boolean skip,
                            String skipReason,
                            Integer targetStationId,
                            OutOrderDispatchDecision dispatchDecision) {
        this.skip = skip;
        this.skipReason = skipReason;
        this.targetStationId = targetStationId;
        this.dispatchDecision = dispatchDecision;
    }
    public static RerouteDecision skip(String reason) {
        return new RerouteDecision(true, reason, null, null);
    }
    public static RerouteDecision proceed(Integer targetStationId) {
        return new RerouteDecision(false, null, targetStationId, null);
    }
    public static RerouteDecision proceed(Integer targetStationId,
                                          OutOrderDispatchDecision dispatchDecision) {
        return new RerouteDecision(false, null, targetStationId, dispatchDecision);
    }
    public boolean skip() {
        return skip;
    }
    public String skipReason() {
        return skipReason;
    }
    public Integer targetStationId() {
        return targetStationId;
    }
    public OutOrderDispatchDecision dispatchDecision() {
        return dispatchDecision;
    }
}
src/main/java/com/zy/core/utils/station/model/RerouteExecutionResult.java
New file
@@ -0,0 +1,52 @@
package com.zy.core.utils.station.model;
import com.zy.core.model.command.StationCommand;
public final class RerouteExecutionResult {
    private final boolean skipped;
    private final String skipReason;
    private final boolean dispatched;
    private final StationCommand command;
    private final int clearedCommandCount;
    private RerouteExecutionResult(boolean skipped,
                                   String skipReason,
                                   boolean dispatched,
                                   StationCommand command,
                                   int clearedCommandCount) {
        this.skipped = skipped;
        this.skipReason = skipReason;
        this.dispatched = dispatched;
        this.command = command;
        this.clearedCommandCount = clearedCommandCount;
    }
    public static RerouteExecutionResult skip(String reason) {
        return new RerouteExecutionResult(true, reason, false, null, 0);
    }
    public static RerouteExecutionResult dispatched(StationCommand command,
                                                    int clearedCommandCount) {
        return new RerouteExecutionResult(false, null, true, command, clearedCommandCount);
    }
    public boolean skipped() {
        return skipped;
    }
    public String skipReason() {
        return skipReason;
    }
    public boolean dispatched() {
        return dispatched;
    }
    public StationCommand command() {
        return command;
    }
    public int clearedCommandCount() {
        return clearedCommandCount;
    }
}
src/main/java/com/zy/core/utils/station/model/RerouteSceneType.java
New file
@@ -0,0 +1,8 @@
package com.zy.core.utils.station.model;
public enum RerouteSceneType {
    RUN_BLOCK_REROUTE,
    IDLE_RECOVER,
    OUT_ORDER,
    WATCH_CIRCLE
}
src/test/java/com/zy/asrs/controller/StationControllerTest.java
@@ -4,8 +4,12 @@
import com.zy.asrs.domain.param.StationCommandMoveParam;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.service.BasDevpService;
import com.zy.core.dispatch.StationCommandDispatchResult;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.model.command.StationCommand;
import com.zy.core.thread.StationThread;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
@@ -13,12 +17,21 @@
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class StationControllerTest {
    @Test
    void controller_noLongerKeepsDispatcherFallbackHelper() {
        assertThrows(NoSuchMethodException.class,
                () -> StationController.class.getDeclaredMethod("getStationCommandDispatcher"));
    }
    @Test
    void commandClearPath_callsThreadClearPath() {
@@ -48,4 +61,39 @@
            SlaveConnection.remove(SlaveType.Devp, 1);
        }
    }
    @Test
    void commandMove_dispatchesViaStationCommandDispatcher() {
        StationController controller = new StationController();
        BasDevpService basDevpService = mock(BasDevpService.class);
        StationThread stationThread = mock(StationThread.class);
        StationCommandDispatcher dispatcher = mock(StationCommandDispatcher.class);
        StationCommand command = new StationCommand();
        ReflectionTestUtils.setField(controller, "basDevpService", basDevpService);
        ReflectionTestUtils.setField(controller, "stationCommandDispatcher", dispatcher);
        BasDevp basDevp = new BasDevp();
        basDevp.setStationList("[{\"deviceNo\":1,\"stationId\":145}]");
        when(basDevpService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
                .thenReturn(Collections.singletonList(basDevp));
        when(stationThread.getCommand(StationCommandType.MOVE, 10335, 145, 188, 0)).thenReturn(command);
        when(dispatcher.dispatch(1, command, "station-controller", "manual-move"))
                .thenReturn(StationCommandDispatchResult.accepted("accepted", 1, "station-controller", "manual-move"));
        StationCommandMoveParam param = new StationCommandMoveParam();
        param.setStationId(145);
        param.setTaskNo(10335);
        param.setTargetStationId(188);
        SlaveConnection.put(SlaveType.Devp, 1, stationThread);
        try {
            R result = controller.commandMove(param);
            assertEquals(200, result.get("code"));
            verify(dispatcher).dispatch(eq(1), same(command), eq("station-controller"), eq("manual-move"));
        } finally {
            SlaveConnection.remove(SlaveType.Devp, 1);
        }
    }
}
src/test/java/com/zy/core/dispatch/StationCommandDispatcherTest.java
New file
@@ -0,0 +1,100 @@
package com.zy.core.dispatch;
import com.zy.common.utils.RedisUtil;
import com.zy.core.cache.MessageQueue;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.model.command.StationCommand;
import com.zy.core.move.StationMoveCoordinator;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class StationCommandDispatcherTest {
    @Test
    void dispatch_acceptsMoveCommandAndReturnsQueueDepth() {
        StationCommandDispatcher dispatcher = new StationCommandDispatcher();
        StationCommand command = new StationCommand();
        command.setCommandType(StationCommandType.MOVE);
        command.setTaskNo(100);
        command.setStationId(10);
        command.setTargetStaNo(20);
        MessageQueue.init(SlaveType.Devp, 1);
        try {
            StationCommandDispatchResult result = dispatcher.dispatch(1, command, "unit-test", "move");
            assertTrue(result.isAccepted());
            assertEquals("accepted", result.getReason());
            assertEquals(1, result.getQueueDepth());
            assertEquals("unit-test", result.getSource());
            assertEquals("move", result.getScene());
        } finally {
            MessageQueue.clear(SlaveType.Devp, 1);
        }
    }
    @Test
    void dispatch_suppressesDuplicateMoveCommandWithinDedupWindow() {
        RedisUtil redisUtil = mock(RedisUtil.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        StationCommandDispatcher dispatcher = new StationCommandDispatcher(redisUtil, coordinator);
        Map<String, Object> dedupStore = new HashMap<>();
        when(redisUtil.get(anyString())).thenAnswer(invocation -> dedupStore.get(invocation.getArgument(0)));
        when(redisUtil.set(anyString(), eq("lock"), anyLong())).thenAnswer(invocation -> {
            dedupStore.put(invocation.getArgument(0), invocation.getArgument(1));
            return true;
        });
        when(coordinator.buildPathSignatureHash(org.mockito.ArgumentMatchers.any(StationCommand.class)))
                .thenReturn("same-path");
        StationCommand command = new StationCommand();
        command.setCommandType(StationCommandType.MOVE);
        command.setTaskNo(100);
        command.setStationId(10);
        command.setTargetStaNo(20);
        MessageQueue.init(SlaveType.Devp, 1);
        try {
            StationCommandDispatchResult first = dispatcher.dispatch(1, command, "unit-test", "move");
            StationCommandDispatchResult second = dispatcher.dispatch(1, command, "unit-test", "move");
            assertTrue(first.isAccepted());
            assertFalse(second.isAccepted());
            assertEquals("dedup-suppressed", second.getReason());
            assertEquals(1, second.getQueueDepth());
        } finally {
            MessageQueue.clear(SlaveType.Devp, 1);
        }
    }
    @Test
    void dispatch_rejectsWhenDevpQueueIsNotInitialized() {
        StationCommandDispatcher dispatcher = new StationCommandDispatcher();
        StationCommand command = new StationCommand();
        command.setCommandType(StationCommandType.MOVE);
        command.setTaskNo(100);
        command.setStationId(10);
        command.setTargetStaNo(20);
        StationCommandDispatchResult result = dispatcher.dispatch(999, command, "unit-test", "move");
        assertFalse(result.isAccepted());
        assertEquals("queue-not-initialized", result.getReason());
        assertEquals(0, result.getQueueDepth());
    }
}
src/test/java/com/zy/core/thread/impl/ZyStationV5ThreadTest.java
@@ -2,9 +2,15 @@
import com.zy.asrs.entity.DeviceConfig;
import com.zy.common.utils.RedisUtil;
import com.zy.core.cache.MessageQueue;
import com.zy.core.enums.SlaveType;
import com.zy.core.model.Task;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.model.protocol.StationTaskBufferItem;
import com.zy.core.network.ZyStationConnectDriver;
import com.zy.core.thread.impl.v5.StationV5SegmentExecutor;
import com.zy.core.thread.impl.v5.StationV5StatusReader;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
@@ -17,10 +23,35 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class ZyStationV5ThreadTest {
    @Test
    void pollAndDispatchQueuedCommand_submitsQueuedMoveCommandToSegmentExecutor() {
        DeviceConfig deviceConfig = new DeviceConfig();
        deviceConfig.setDeviceNo(1);
        RedisUtil redisUtil = mock(RedisUtil.class);
        StationCommand command = new StationCommand();
        StationV5SegmentExecutor segmentExecutor = mock(StationV5SegmentExecutor.class);
        ZyStationV5Thread thread = new ZyStationV5Thread(deviceConfig, redisUtil);
        ReflectionTestUtils.setField(thread, "segmentExecutor", segmentExecutor);
        MessageQueue.init(SlaveType.Devp, 1);
        try {
            MessageQueue.offer(SlaveType.Devp, 1, new Task(2, command));
            ReflectionTestUtils.invokeMethod(thread, "pollAndDispatchQueuedCommand");
            verify(segmentExecutor, timeout(1000)).execute(command);
        } finally {
            MessageQueue.clear(SlaveType.Devp, 1);
            thread.close();
        }
    }
    @Test
    void clearPath_delegatesPureSlotClearingToDriver() {
@@ -46,7 +77,8 @@
        station10.setStationId(10);
        station10.setTaskBufferItems(List.of(hitItem));
        ReflectionTestUtils.setField(thread, "statusList", Arrays.asList(station20, station10));
        StationV5StatusReader statusReader = (StationV5StatusReader) ReflectionTestUtils.getField(thread, "statusReader");
        ReflectionTestUtils.setField(statusReader, "statusList", Arrays.asList(station20, station10));
        boolean result = thread.clearPath(100);
src/test/java/com/zy/core/thread/impl/v5/StationV5RunBlockReroutePlannerTest.java
New file
@@ -0,0 +1,110 @@
package com.zy.core.thread.impl.v5;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.model.command.StationCommand;
import com.zy.core.service.StationTaskLoopService;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class StationV5RunBlockReroutePlannerTest {
    @Test
    void plan_prefersLongerCandidateWhenShortestPathIsOverusedInsideTriggeredLoop() {
        RedisUtil redisUtil = mock(RedisUtil.class);
        String stateKey = RedisKeyType.STATION_RUN_BLOCK_REROUTE_STATE_.key + "100_10";
        when(redisUtil.get(stateKey)).thenReturn(JSON.toJSONString(seedState(
                100,
                10,
                2,
                List.of(),
                Map.of("10->11->20", 2)
        )));
        StationV5RunBlockReroutePlanner planner = new StationV5RunBlockReroutePlanner(redisUtil);
        StationCommand shortest = moveCommand(100, 10, 20, 10, 11, 20);
        StationCommand longer = moveCommand(100, 10, 20, 10, 31, 32, 20);
        StationTaskLoopService.LoopIdentitySnapshot loopIdentity =
                new StationTaskLoopService.LoopIdentitySnapshot("10|11|20", new HashSet<>(List.of(10, 11, 20)), 3, 3, "wholeLoop");
        StationTaskLoopService.LoopEvaluation loopEvaluation =
                new StationTaskLoopService.LoopEvaluation(100, 10, loopIdentity, 2, 3, true);
        StationV5RunBlockReroutePlanner.PlanResult result = planner.plan(
                100,
                10,
                loopEvaluation,
                List.of(shortest, longer)
        );
        assertSame(longer, result.getCommand());
        assertEquals(3, result.getPlanCount());
    }
    @Test
    void plan_resetsIssuedRoutesWhenAllCandidatesHaveBeenTried() {
        RedisUtil redisUtil = mock(RedisUtil.class);
        String stateKey = RedisKeyType.STATION_RUN_BLOCK_REROUTE_STATE_.key + "100_10";
        when(redisUtil.get(stateKey)).thenReturn(JSON.toJSONString(seedState(
                100,
                10,
                1,
                List.of(List.of(10, 11, 20), List.of(10, 31, 32, 20)),
                Map.of("10->11->20", 1, "10->31->32->20", 1)
        )));
        StationV5RunBlockReroutePlanner planner = new StationV5RunBlockReroutePlanner(redisUtil);
        StationCommand first = moveCommand(100, 10, 20, 10, 11, 20);
        StationCommand second = moveCommand(100, 10, 20, 10, 31, 32, 20);
        StationTaskLoopService.LoopEvaluation loopEvaluation =
                new StationTaskLoopService.LoopEvaluation(100, 10, StationTaskLoopService.LoopIdentitySnapshot.empty(), 0, 0, false);
        StationV5RunBlockReroutePlanner.PlanResult result = planner.plan(
                100,
                10,
                loopEvaluation,
                List.of(first, second)
        );
        assertSame(first, result.getCommand());
        assertEquals(2, result.getPlanCount());
    }
    private static StationCommand moveCommand(Integer taskNo,
                                              Integer stationId,
                                              Integer targetStationId,
                                              Integer... path) {
        StationCommand command = new StationCommand();
        command.setTaskNo(taskNo);
        command.setStationId(stationId);
        command.setTargetStaNo(targetStationId);
        command.setNavigatePath(List.of(path));
        return command;
    }
    private static JSONObject seedState(Integer taskNo,
                                        Integer blockStationId,
                                        Integer planCount,
                                        List<List<Integer>> issuedRoutePathList,
                                        Map<String, Integer> routeIssueCountMap) {
        JSONObject state = new JSONObject();
        state.put("taskNo", taskNo);
        state.put("blockStationId", blockStationId);
        state.put("planCount", planCount);
        state.put("issuedRoutePathList", issuedRoutePathList);
        state.put("routeIssueCountMap", new HashMap<>(routeIssueCountMap));
        return state;
    }
}
src/test/java/com/zy/core/utils/StationOperateProcessUtilsReroutePipelineTest.java
@@ -21,16 +21,34 @@
import com.zy.core.move.StationMoveCoordinator;
import com.zy.core.move.StationMoveDispatchMode;
import com.zy.core.move.StationMoveSession;
import com.zy.core.dispatch.StationCommandDispatchResult;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.model.protocol.StationTaskBufferItem;
import com.zy.core.thread.StationThread;
import com.zy.common.utils.RedisUtil;
import com.zy.core.utils.station.StationDispatchLoadSupport;
import com.zy.core.utils.station.StationDispatchRuntimeStateSupport;
import com.zy.core.utils.station.StationOutboundDecisionSupport;
import com.zy.core.utils.station.StationOutboundDispatchProcessor;
import com.zy.core.utils.station.StationRegularDispatchProcessor;
import com.zy.core.utils.station.StationRerouteProcessor;
import com.zy.core.utils.station.model.DispatchLimitConfig;
import com.zy.core.utils.station.model.LoadGuardState;
import com.zy.core.utils.station.model.LoopHitResult;
import com.zy.core.utils.station.model.RerouteCommandPlan;
import com.zy.core.utils.station.model.RerouteContext;
import com.zy.core.utils.station.model.RerouteDecision;
import com.zy.core.utils.station.model.RerouteExecutionResult;
import com.zy.core.utils.station.model.RerouteSceneType;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Date;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -51,6 +69,89 @@
import static org.mockito.Mockito.when;
class StationOperateProcessUtilsReroutePipelineTest {
    private final Map<StationOperateProcessUtils, Map<String, Object>> dependencyOverrides = new IdentityHashMap<>();
    private StationOperateProcessUtils newUtils() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        stash(utils, "stationOutboundDecisionSupport", new StationOutboundDecisionSupport());
        wireRerouteProcessor(utils);
        return utils;
    }
    private void wireOutboundSupport(StationOperateProcessUtils utils) {
        StationOutboundDecisionSupport support = new StationOutboundDecisionSupport();
        copyIfPresent(utils, support, "wrkMastService");
        copyIfPresent(utils, support, "basDevpService");
        copyIfPresent(utils, support, "basStationService");
        copyIfPresent(utils, support, "navigateUtils");
        copyIfPresent(utils, support, "redisUtil");
        copyIfPresent(utils, support, "stationTaskLoopService");
        copyIfPresent(utils, support, "stationMoveCoordinator");
        StationDispatchRuntimeStateSupport runtimeStateSupport = new StationDispatchRuntimeStateSupport();
        copyIfPresent(utils, runtimeStateSupport, "redisUtil");
        copyIfPresent(utils, runtimeStateSupport, "basStationOptService");
        ReflectionTestUtils.setField(support, "stationDispatchRuntimeStateSupport", runtimeStateSupport);
        stash(utils, "stationOutboundDecisionSupport", support);
    }
    private void wireRerouteProcessor(StationOperateProcessUtils utils) {
        StationRerouteProcessor processor = new StationRerouteProcessor();
        copyIfPresent(utils, processor, "basDevpService");
        copyIfPresent(utils, processor, "wrkMastService");
        copyIfPresent(utils, processor, "commonService");
        copyIfPresent(utils, processor, "redisUtil");
        copyIfPresent(utils, processor, "locMastService");
        copyIfPresent(utils, processor, "wmsOperateUtils");
        copyIfPresent(utils, processor, "basStationOptService");
        copyIfPresent(utils, processor, "stationMoveCoordinator");
        Object dispatcher = readIfPresent(utils, "stationCommandDispatcher");
        if (dispatcher == null) {
            dispatcher = new StationCommandDispatcher(
                    (RedisUtil) readIfPresent(utils, "redisUtil"),
                    (StationMoveCoordinator) readIfPresent(utils, "stationMoveCoordinator")
            );
        }
        ReflectionTestUtils.setField(processor, "stationCommandDispatcher", dispatcher);
        Object outboundSupport = readIfPresent(utils, "stationOutboundDecisionSupport");
        if (outboundSupport == null) {
            wireOutboundSupport(utils);
            outboundSupport = readIfPresent(utils, "stationOutboundDecisionSupport");
        }
        if (outboundSupport != null) {
            ReflectionTestUtils.setField(processor, "stationOutboundDecisionSupport", outboundSupport);
        }
        StationDispatchRuntimeStateSupport runtimeStateSupport = new StationDispatchRuntimeStateSupport();
        copyIfPresent(utils, runtimeStateSupport, "redisUtil");
        copyIfPresent(utils, runtimeStateSupport, "basStationOptService");
        ReflectionTestUtils.setField(processor, "stationDispatchRuntimeStateSupport", runtimeStateSupport);
        ReflectionTestUtils.setField(utils, "stationRerouteProcessor", processor);
    }
    private void stash(StationOperateProcessUtils utils, String fieldName, Object value) {
        dependencyOverrides.computeIfAbsent(utils, key -> new HashMap<>()).put(fieldName, value);
    }
    private Object readIfPresent(Object source, String fieldName) {
        try {
            return ReflectionTestUtils.getField(source, fieldName);
        } catch (IllegalArgumentException ignore) {
            if (source instanceof StationOperateProcessUtils) {
                Map<String, Object> values = dependencyOverrides.get(source);
                return values == null ? null : values.get(fieldName);
            }
            return null;
        }
    }
    private void copyIfPresent(Object source, Object target, String fieldName) {
        Object value = readIfPresent(source, fieldName);
        if (value != null) {
            try {
                ReflectionTestUtils.setField(target, fieldName, value);
            } catch (IllegalArgumentException ignore) {
            }
        }
    }
    @SuppressWarnings("unchecked")
    private void stubTaskDispatchLock(StationMoveCoordinator coordinator) {
@@ -62,16 +163,14 @@
    @Test
    void choosesRunBlockCommandBuilderForRunBlockRerouteScene() {
        StationOperateProcessUtils.RerouteSceneType scene =
                StationOperateProcessUtils.RerouteSceneType.RUN_BLOCK_REROUTE;
        RerouteSceneType scene = RerouteSceneType.RUN_BLOCK_REROUTE;
        assertSame(StationOperateProcessUtils.RerouteSceneType.RUN_BLOCK_REROUTE, scene);
        assertSame(RerouteSceneType.RUN_BLOCK_REROUTE, scene);
    }
    @Test
    void resolveExecutionTarget_skipsWhenTargetEqualsCurrentStation() {
        StationOperateProcessUtils.RerouteDecision decision =
                StationOperateProcessUtils.RerouteDecision.skip("same-station");
        RerouteDecision decision = RerouteDecision.skip("same-station");
        assertTrue(decision.skip());
        assertEquals("same-station", decision.skipReason());
@@ -79,7 +178,7 @@
    @Test
    void buildCommandPlan_usesRunBlockCommandBuilderForRunBlockScene() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        StationThread stationThread = mock(StationThread.class);
        StationCommand command = new StationCommand();
        command.setTaskNo(100);
@@ -87,8 +186,8 @@
        command.setTargetStaNo(20);
        when(stationThread.getRunBlockRerouteCommand(100, 10, 20, 0, 0.25d)).thenReturn(command);
        StationOperateProcessUtils.RerouteContext context = StationOperateProcessUtils.RerouteContext.create(
                StationOperateProcessUtils.RerouteSceneType.RUN_BLOCK_REROUTE,
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.RUN_BLOCK_REROUTE,
                buildBasDevp(1),
                stationThread,
                buildStationProtocol(10, 100, 10),
@@ -100,9 +199,9 @@
                .withCancelSessionBeforeDispatch()
                .withResetSegmentCommandsBeforeDispatch();
        StationOperateProcessUtils.RerouteCommandPlan plan = utils.buildRerouteCommandPlan(
        RerouteCommandPlan plan = utils.buildRerouteCommandPlan(
                context,
                StationOperateProcessUtils.RerouteDecision.proceed(20)
                RerouteDecision.proceed(20)
        );
        verify(stationThread).getRunBlockRerouteCommand(100, 10, 20, 0, 0.25d);
@@ -111,7 +210,7 @@
    @Test
    void executePlan_skipsWhenCurrentTaskStillExistsInBuffer() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        StationCommand command = new StationCommand();
        command.setTaskNo(100);
        command.setStationId(10);
@@ -120,8 +219,8 @@
        StationTaskBufferItem bufferItem = new StationTaskBufferItem();
        bufferItem.setTaskNo(100);
        StationOperateProcessUtils.RerouteContext context = StationOperateProcessUtils.RerouteContext.create(
                StationOperateProcessUtils.RerouteSceneType.OUT_ORDER,
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.OUT_ORDER,
                buildBasDevp(1),
                mock(StationThread.class),
                buildStationProtocol(10, 100, 10, Collections.singletonList(bufferItem)),
@@ -131,11 +230,11 @@
                "checkStationOutOrder"
        );
        StationOperateProcessUtils.RerouteExecutionResult result = utils.executeReroutePlan(
        RerouteExecutionResult result = utils.executeReroutePlan(
                context,
                StationOperateProcessUtils.RerouteCommandPlan.dispatch(
                RerouteCommandPlan.dispatch(
                        command,
                        StationOperateProcessUtils.RerouteDecision.proceed(20),
                        RerouteDecision.proceed(20),
                        "checkStationOutOrder"
                )
        );
@@ -146,11 +245,11 @@
    @Test
    void outOrderAndWatchCircle_shareDecisionFlow() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        WrkMast wrkMast = buildWrkMast(100, 20);
        StationOperateProcessUtils.RerouteContext outOrderContext = StationOperateProcessUtils.RerouteContext.create(
                StationOperateProcessUtils.RerouteSceneType.OUT_ORDER,
        RerouteContext outOrderContext = RerouteContext.create(
                RerouteSceneType.OUT_ORDER,
                buildBasDevp(1),
                mock(StationThread.class),
                buildStationProtocol(10, 100, 10),
@@ -159,8 +258,8 @@
                0.0d,
                "checkStationOutOrder"
        );
        StationOperateProcessUtils.RerouteContext watchCircleContext = StationOperateProcessUtils.RerouteContext.create(
                StationOperateProcessUtils.RerouteSceneType.WATCH_CIRCLE,
        RerouteContext watchCircleContext = RerouteContext.create(
                RerouteSceneType.WATCH_CIRCLE,
                buildBasDevp(1),
                mock(StationThread.class),
                buildStationProtocol(10, 100, 10),
@@ -170,8 +269,8 @@
                "watchCircleStation"
        );
        StationOperateProcessUtils.RerouteDecision outOrderDecision = utils.resolveSharedRerouteDecision(outOrderContext);
        StationOperateProcessUtils.RerouteDecision watchCircleDecision = utils.resolveSharedRerouteDecision(watchCircleContext);
        RerouteDecision outOrderDecision = utils.resolveSharedRerouteDecision(outOrderContext);
        RerouteDecision watchCircleDecision = utils.resolveSharedRerouteDecision(watchCircleContext);
        assertEquals(20, outOrderDecision.targetStationId());
        assertEquals(20, watchCircleDecision.targetStationId());
@@ -179,7 +278,7 @@
    @Test
    void runBlockReroute_keepsDirectReassignAndNormalRerouteSeparate() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        WrkMast inboundWrkMast = buildWrkMast(100, 20);
        inboundWrkMast.setIoType(WrkIoType.IN.id);
@@ -189,12 +288,13 @@
    @Test
    void idleRecover_skipsWhenLastDispatchIsTooRecent() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "stationMoveCoordinator", coordinator);
        stash(utils, "redisUtil", redisUtil);
        wireRerouteProcessor(utils);
        StationMoveSession session = new StationMoveSession();
        session.setStatus(StationMoveSession.STATUS_RUNNING);
@@ -208,8 +308,8 @@
        command.setStationId(10);
        command.setTargetStaNo(20);
        StationOperateProcessUtils.RerouteContext context = StationOperateProcessUtils.RerouteContext.create(
                StationOperateProcessUtils.RerouteSceneType.IDLE_RECOVER,
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.IDLE_RECOVER,
                buildBasDevp(1),
                mock(StationThread.class),
                buildStationProtocol(10, 100, 10),
@@ -219,11 +319,11 @@
                "checkStationIdleRecover"
        ).withRecentDispatchGuard();
        StationOperateProcessUtils.RerouteExecutionResult result = utils.executeReroutePlan(
        RerouteExecutionResult result = utils.executeReroutePlan(
                context,
                StationOperateProcessUtils.RerouteCommandPlan.dispatch(
                RerouteCommandPlan.dispatch(
                        command,
                        StationOperateProcessUtils.RerouteDecision.proceed(20),
                        RerouteDecision.proceed(20),
                        "checkStationIdleRecover"
                )
        );
@@ -235,12 +335,13 @@
    @Test
    void idleRecover_skipsWhenCurrentStationIsStillInsideRecentlyIssuedActiveRoute() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "stationMoveCoordinator", coordinator);
        stash(utils, "redisUtil", redisUtil);
        wireRerouteProcessor(utils);
        StationMoveSession session = new StationMoveSession();
        session.setStatus(StationMoveSession.STATUS_RUNNING);
@@ -255,8 +356,8 @@
        command.setStationId(121);
        command.setTargetStaNo(124);
        StationOperateProcessUtils.RerouteContext context = StationOperateProcessUtils.RerouteContext.create(
                StationOperateProcessUtils.RerouteSceneType.IDLE_RECOVER,
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.IDLE_RECOVER,
                buildBasDevp(1),
                mock(StationThread.class),
                buildStationProtocol(121, 10510, 121),
@@ -266,11 +367,11 @@
                "checkStationIdleRecover"
        ).withRecentDispatchGuard();
        StationOperateProcessUtils.RerouteExecutionResult result = utils.executeReroutePlan(
        RerouteExecutionResult result = utils.executeReroutePlan(
                context,
                StationOperateProcessUtils.RerouteCommandPlan.dispatch(
                RerouteCommandPlan.dispatch(
                        command,
                        StationOperateProcessUtils.RerouteDecision.proceed(124),
                        RerouteDecision.proceed(124),
                        "checkStationIdleRecover"
                )
        );
@@ -282,14 +383,15 @@
    @Test
    void idleRecover_skipsWhenStationCommandLogShowsRecentIssuedMove() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        BasStationOptService basStationOptService = mock(BasStationOptService.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "basStationOptService", basStationOptService);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "stationMoveCoordinator", coordinator);
        stash(utils, "basStationOptService", basStationOptService);
        stash(utils, "redisUtil", redisUtil);
        wireRerouteProcessor(utils);
        StationMoveSession session = new StationMoveSession();
        session.setStatus(StationMoveSession.STATUS_RUNNING);
@@ -318,7 +420,7 @@
    @Test
    void checkStationOutOrder_skipsWhenActiveSessionAlreadyOwnsCurrentStation() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        BasDevpService basDevpService = mock(BasDevpService.class);
        WrkMastService wrkMastService = mock(WrkMastService.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
@@ -326,10 +428,12 @@
        StationThread stationThread = mock(StationThread.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "basDevpService", basDevpService);
        stash(utils, "basDevpService", basDevpService);
        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "stationMoveCoordinator", coordinator);
        stash(utils, "redisUtil", redisUtil);
        wireOutboundSupport(utils);
        wireRerouteProcessor(utils);
        BasDevp basDevp = buildBasDevp(1);
        basDevp.setIsOutOrderList("[{\"deviceNo\":1,\"stationId\":145}]");
@@ -380,7 +484,7 @@
    @Test
    void checkStationOutOrder_restartsWhenBlockedSessionExistsButStationNoLongerRunBlock() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        BasDevpService basDevpService = mock(BasDevpService.class);
        WrkMastService wrkMastService = mock(WrkMastService.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
@@ -388,10 +492,12 @@
        StationThread stationThread = mock(StationThread.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "basDevpService", basDevpService);
        stash(utils, "basDevpService", basDevpService);
        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "stationMoveCoordinator", coordinator);
        stash(utils, "redisUtil", redisUtil);
        wireOutboundSupport(utils);
        wireRerouteProcessor(utils);
        BasDevp basDevp = buildBasDevp(1);
        basDevp.setIsOutOrderList("[{\"deviceNo\":1,\"stationId\":145}]");
@@ -443,15 +549,17 @@
    @Test
    void checkStationOutOrder_skipsRunBlockStationBeforePlanning() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        BasDevpService basDevpService = mock(BasDevpService.class);
        WrkMastService wrkMastService = mock(WrkMastService.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        StationThread stationThread = mock(StationThread.class);
        ReflectionTestUtils.setField(utils, "basDevpService", basDevpService);
        stash(utils, "basDevpService", basDevpService);
        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "redisUtil", redisUtil);
        wireOutboundSupport(utils);
        wireRerouteProcessor(utils);
        BasDevp basDevp = buildBasDevp(1);
        basDevp.setIsOutOrderList("[{\"deviceNo\":1,\"stationId\":145}]");
@@ -493,12 +601,13 @@
    @Test
    void executePlan_runBlockReroute_reissuesWhenBlockedSessionMatchesCandidatePath() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "stationMoveCoordinator", coordinator);
        stash(utils, "redisUtil", redisUtil);
        wireRerouteProcessor(utils);
        StationCommand command = new StationCommand();
        command.setTaskNo(100);
@@ -516,8 +625,8 @@
        when(coordinator.loadSession(100)).thenReturn(session);
        when(coordinator.buildPathSignature(command)).thenReturn("same-path");
        StationOperateProcessUtils.RerouteContext context = StationOperateProcessUtils.RerouteContext.create(
                StationOperateProcessUtils.RerouteSceneType.RUN_BLOCK_REROUTE,
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.RUN_BLOCK_REROUTE,
                buildBasDevp(1),
                mock(StationThread.class),
                buildStationProtocol(145, 100, 145),
@@ -530,11 +639,11 @@
        MessageQueue.init(SlaveType.Devp, 1);
        try {
            StationOperateProcessUtils.RerouteExecutionResult result = utils.executeReroutePlan(
            RerouteExecutionResult result = utils.executeReroutePlan(
                    context,
                    StationOperateProcessUtils.RerouteCommandPlan.dispatch(
                    RerouteCommandPlan.dispatch(
                            command,
                            StationOperateProcessUtils.RerouteDecision.proceed(111),
                            RerouteDecision.proceed(111),
                            "checkStationRunBlock_reroute"
                    )
            );
@@ -551,12 +660,13 @@
    @Test
    void executePlan_runBlockReroute_ignoresCurrentTaskBufferAfterReset() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "stationMoveCoordinator", coordinator);
        stash(utils, "redisUtil", redisUtil);
        wireRerouteProcessor(utils);
        StationCommand command = new StationCommand();
        command.setTaskNo(10388);
@@ -570,8 +680,8 @@
        when(redisUtil.get(anyString())).thenReturn(null);
        StationOperateProcessUtils.RerouteContext context = StationOperateProcessUtils.RerouteContext.create(
                StationOperateProcessUtils.RerouteSceneType.RUN_BLOCK_REROUTE,
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.RUN_BLOCK_REROUTE,
                buildBasDevp(1),
                mock(StationThread.class),
                buildStationProtocol(186, 10388, 189, Collections.singletonList(bufferItem)),
@@ -584,11 +694,11 @@
        MessageQueue.init(SlaveType.Devp, 1);
        try {
            StationOperateProcessUtils.RerouteExecutionResult result = utils.executeReroutePlan(
            RerouteExecutionResult result = utils.executeReroutePlan(
                    context,
                    StationOperateProcessUtils.RerouteCommandPlan.dispatch(
                    RerouteCommandPlan.dispatch(
                            command,
                            StationOperateProcessUtils.RerouteDecision.proceed(124),
                            RerouteDecision.proceed(124),
                            "checkStationRunBlock_reroute"
                    )
            );
@@ -602,12 +712,13 @@
    @Test
    void executePlan_runBlockReroute_bypassesSuppressDispatchAfterReset() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "stationMoveCoordinator", coordinator);
        stash(utils, "redisUtil", redisUtil);
        wireRerouteProcessor(utils);
        StationCommand command = new StationCommand();
        command.setTaskNo(10388);
@@ -619,8 +730,8 @@
        when(redisUtil.get(anyString())).thenReturn(null);
        when(coordinator.shouldSuppressDispatch(10388, 186, command)).thenReturn(true);
        StationOperateProcessUtils.RerouteContext context = StationOperateProcessUtils.RerouteContext.create(
                StationOperateProcessUtils.RerouteSceneType.RUN_BLOCK_REROUTE,
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.RUN_BLOCK_REROUTE,
                buildBasDevp(1),
                mock(StationThread.class),
                buildStationProtocol(186, 10388, 186),
@@ -634,11 +745,11 @@
        MessageQueue.init(SlaveType.Devp, 1);
        try {
            StationOperateProcessUtils.RerouteExecutionResult result = utils.executeReroutePlan(
            RerouteExecutionResult result = utils.executeReroutePlan(
                    context,
                    StationOperateProcessUtils.RerouteCommandPlan.dispatch(
                    RerouteCommandPlan.dispatch(
                            command,
                            StationOperateProcessUtils.RerouteDecision.proceed(124),
                            RerouteDecision.proceed(124),
                            "checkStationRunBlock_reroute"
                    )
            );
@@ -652,22 +763,28 @@
    @Test
    void stationInExecute_recordsDispatchSessionAfterIssuingMoveCommand() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        BasDevpService basDevpService = mock(BasDevpService.class);
        WrkMastService wrkMastService = mock(WrkMastService.class);
        CommonService commonService = mock(CommonService.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        StationCommandDispatcher dispatcher = mock(StationCommandDispatcher.class);
        StationDispatchLoadSupport loadSupport = mock(StationDispatchLoadSupport.class);
        StationThread stationThread = mock(StationThread.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "basDevpService", basDevpService);
        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(utils, "commonService", commonService);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        ReflectionTestUtils.setField(utils, "wrkAnalysisService", wrkAnalysisService);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        StationRegularDispatchProcessor processor = new StationRegularDispatchProcessor();
        ReflectionTestUtils.setField(processor, "basDevpService", basDevpService);
        ReflectionTestUtils.setField(processor, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(processor, "commonService", commonService);
        ReflectionTestUtils.setField(processor, "redisUtil", redisUtil);
        ReflectionTestUtils.setField(processor, "wrkAnalysisService", wrkAnalysisService);
        ReflectionTestUtils.setField(processor, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(processor, "stationCommandDispatcher", dispatcher);
        ReflectionTestUtils.setField(processor, "stationDispatchLoadSupport", loadSupport);
        ReflectionTestUtils.setField(utils, "stationRegularDispatchProcessor", processor);
        BasDevp basDevp = buildBasDevp(1);
        basDevp.setBarcodeStationList("[{\"deviceNo\":1,\"stationId\":101}]");
@@ -700,6 +817,18 @@
        when(stationThread.getCommand(com.zy.core.enums.StationCommandType.MOVE, 500670, 101, 102, 0))
                .thenReturn(command);
        when(redisUtil.get(anyString())).thenReturn(null);
        when(dispatcher.dispatch(1, command, "station-operate-process", "stationInExecute"))
                .thenReturn(StationCommandDispatchResult.accepted("accepted", 1, "station-operate-process", "stationInExecute"));
        DispatchLimitConfig baseConfig = new DispatchLimitConfig();
        LoadGuardState loadGuardState = new LoadGuardState();
        LoopHitResult noHit = LoopHitResult.noHit();
        when(loadSupport.getDispatchLimitConfig(null, null)).thenReturn(baseConfig);
        when(loadSupport.countCurrentStationTask()).thenReturn(0);
        when(loadSupport.buildLoadGuardState(baseConfig)).thenReturn(loadGuardState);
        when(loadSupport.getDispatchLimitConfig(101, 102)).thenReturn(baseConfig);
        when(loadSupport.findPathLoopHit(baseConfig, 101, 102, loadGuardState)).thenReturn(noHit);
        when(loadSupport.isDispatchBlocked(baseConfig, 0, loadGuardState, false)).thenReturn(false);
        MessageQueue.init(SlaveType.Devp, 1);
        SlaveConnection.put(SlaveType.Devp, 1, stationThread);
@@ -714,62 +843,19 @@
    }
    @Test
    void dualCrnStationOutExecute_recordsDispatchSessionAfterIssuingMoveCommand() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        WrkMastService wrkMastService = mock(WrkMastService.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        NotifyUtils notifyUtils = mock(NotifyUtils.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        StationThread stationThread = mock(StationThread.class);
        stubTaskDispatchLock(coordinator);
    void dualCrnStationOutExecute_delegatesToOutboundProcessor() {
        StationOperateProcessUtils utils = newUtils();
        StationOutboundDispatchProcessor processor = mock(StationOutboundDispatchProcessor.class);
        ReflectionTestUtils.setField(utils, "stationOutboundDispatchProcessor", processor);
        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        ReflectionTestUtils.setField(utils, "notifyUtils", notifyUtils);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        utils.dualCrnStationOutExecute();
        WrkMast wrkMast = buildWrkMast(10335, 145);
        wrkMast.setDualCrnNo(1);
        wrkMast.setWrkSts(WrkStsType.OUTBOUND_RUN_COMPLETE.sts);
        when(wrkMastService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
                .thenReturn(Collections.singletonList(wrkMast));
        when(wrkMastService.updateById(wrkMast)).thenReturn(true);
        when(redisUtil.get(anyString())).thenAnswer(invocation -> {
            String key = invocation.getArgument(0);
            if (Objects.equals(key, RedisKeyType.DUAL_CRN_OUT_TASK_STATION_INFO.key + 10335)) {
                return "{\"deviceNo\":1,\"stationId\":198}";
            }
            return null;
        });
        StationProtocol stationProtocol = buildStationProtocol(198, 0, 198);
        stationProtocol.setAutoing(true);
        stationProtocol.setLoading(true);
        when(stationThread.getStatusMap()).thenReturn(Map.of(198, stationProtocol));
        StationCommand command = new StationCommand();
        command.setTaskNo(10335);
        command.setStationId(198);
        command.setTargetStaNo(145);
        when(stationThread.getCommand(eq(com.zy.core.enums.StationCommandType.MOVE), eq(10335), eq(198), eq(145), eq(0), eq(0.0d)))
                .thenReturn(command);
        MessageQueue.init(SlaveType.Devp, 1);
        SlaveConnection.put(SlaveType.Devp, 1, stationThread);
        try {
            utils.dualCrnStationOutExecute();
            verify(coordinator, times(1)).recordDispatch(eq(10335), eq(198), eq("dualCrnStationOutExecute"), same(command), eq(false));
        } finally {
            MessageQueue.clear(SlaveType.Devp, 1);
            SlaveConnection.remove(SlaveType.Devp, 1);
        }
        verify(processor, times(1)).dualCrnStationOutExecute();
    }
    @Test
    void stationOutExecuteFinish_attemptsClearPathBeforeCompletingTask() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        WrkMastService wrkMastService = mock(WrkMastService.class);
        BasStationService basStationService = mock(BasStationService.class);
        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
@@ -778,12 +864,14 @@
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        StationThread stationThread = mock(StationThread.class);
        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(utils, "basStationService", basStationService);
        ReflectionTestUtils.setField(utils, "wrkAnalysisService", wrkAnalysisService);
        ReflectionTestUtils.setField(utils, "notifyUtils", notifyUtils);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        StationRegularDispatchProcessor processor = new StationRegularDispatchProcessor();
        ReflectionTestUtils.setField(processor, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(processor, "basStationService", basStationService);
        ReflectionTestUtils.setField(processor, "wrkAnalysisService", wrkAnalysisService);
        ReflectionTestUtils.setField(processor, "notifyUtils", notifyUtils);
        ReflectionTestUtils.setField(processor, "redisUtil", redisUtil);
        ReflectionTestUtils.setField(processor, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "stationRegularDispatchProcessor", processor);
        WrkMast wrkMast = buildWrkMast(10335, 145);
        wrkMast.setWrkSts(WrkStsType.STATION_RUN.sts);
@@ -816,7 +904,7 @@
    @Test
    void watchCircleStation_usesSessionArrivalStateWhenLegacyCommandMissing() {
        StationOperateProcessUtils utils = new StationOperateProcessUtils();
        StationOperateProcessUtils utils = newUtils();
        BasDevpService basDevpService = mock(BasDevpService.class);
        WrkMastService wrkMastService = mock(WrkMastService.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
@@ -824,10 +912,12 @@
        StationThread stationThread = mock(StationThread.class);
        stubTaskDispatchLock(coordinator);
        ReflectionTestUtils.setField(utils, "basDevpService", basDevpService);
        stash(utils, "basDevpService", basDevpService);
        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(utils, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
        stash(utils, "stationMoveCoordinator", coordinator);
        stash(utils, "redisUtil", redisUtil);
        wireOutboundSupport(utils);
        wireRerouteProcessor(utils);
        BasDevp basDevp = buildBasDevp(1);
        when(basDevpService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
src/test/java/com/zy/core/utils/StationRerouteProcessorTest.java
New file
@@ -0,0 +1,133 @@
package com.zy.core.utils;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.WrkMast;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.model.protocol.StationTaskBufferItem;
import com.zy.core.thread.StationThread;
import com.zy.core.utils.station.StationRerouteProcessor;
import com.zy.core.utils.station.model.RerouteCommandPlan;
import com.zy.core.utils.station.model.RerouteContext;
import com.zy.core.utils.station.model.RerouteDecision;
import com.zy.core.utils.station.model.RerouteExecutionResult;
import com.zy.core.utils.station.model.RerouteSceneType;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class StationRerouteProcessorTest {
    @Test
    void rerouteProcessor_noLongerKeepsDispatcherFallbackHelper() {
        assertThrows(NoSuchMethodException.class,
                () -> StationRerouteProcessor.class.getDeclaredMethod("getStationCommandDispatcher"));
    }
    @Test
    void buildCommandPlan_usesRunBlockCommandBuilderForRunBlockScene() {
        StationRerouteProcessor processor = new StationRerouteProcessor();
        StationThread stationThread = mock(StationThread.class);
        StationCommand command = new StationCommand();
        command.setTaskNo(100);
        command.setStationId(10);
        command.setTargetStaNo(20);
        when(stationThread.getRunBlockRerouteCommand(100, 10, 20, 0, 0.25d)).thenReturn(command);
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.RUN_BLOCK_REROUTE,
                buildBasDevp(1),
                stationThread,
                buildStationProtocol(10, 100, 10),
                buildWrkMast(100, 99),
                Collections.emptyList(),
                0.25d,
                "checkStationRunBlock_reroute"
        ).withRunBlockCommand()
                .withCancelSessionBeforeDispatch()
                .withResetSegmentCommandsBeforeDispatch();
        RerouteCommandPlan plan = processor.buildRerouteCommandPlan(
                context,
                RerouteDecision.proceed(20)
        );
        verify(stationThread).getRunBlockRerouteCommand(100, 10, 20, 0, 0.25d);
        assertSame(command, plan.command());
    }
    @Test
    void executePlan_skipsWhenCurrentTaskStillExistsInBuffer() {
        StationRerouteProcessor processor = new StationRerouteProcessor();
        StationCommand command = new StationCommand();
        command.setTaskNo(100);
        command.setStationId(10);
        command.setTargetStaNo(20);
        StationTaskBufferItem bufferItem = new StationTaskBufferItem();
        bufferItem.setTaskNo(100);
        RerouteContext context = RerouteContext.create(
                RerouteSceneType.OUT_ORDER,
                buildBasDevp(1),
                mock(StationThread.class),
                buildStationProtocol(10, 100, 10, Collections.singletonList(bufferItem)),
                buildWrkMast(100, 20),
                List.of(10, 20),
                0.0d,
                "checkStationOutOrder"
        );
        RerouteExecutionResult result = processor.executeReroutePlan(
                context,
                RerouteCommandPlan.dispatch(
                        command,
                        RerouteDecision.proceed(20),
                        "checkStationOutOrder"
                )
        );
        assertTrue(result.skipped());
        assertEquals("buffer-has-current-task", result.skipReason());
    }
    private static BasDevp buildBasDevp(int devpNo) {
        BasDevp basDevp = new BasDevp();
        basDevp.setDevpNo(devpNo);
        return basDevp;
    }
    private static WrkMast buildWrkMast(int wrkNo, int targetStationId) {
        WrkMast wrkMast = new WrkMast();
        wrkMast.setWrkNo(wrkNo);
        wrkMast.setStaNo(targetStationId);
        return wrkMast;
    }
    private static StationProtocol buildStationProtocol(int stationId,
                                                        int taskNo,
                                                        int targetStationId) {
        return buildStationProtocol(stationId, taskNo, targetStationId, Collections.emptyList());
    }
    private static StationProtocol buildStationProtocol(int stationId,
                                                        int taskNo,
                                                        int targetStationId,
                                                        List<StationTaskBufferItem> taskBufferItems) {
        StationProtocol stationProtocol = new StationProtocol();
        stationProtocol.setStationId(stationId);
        stationProtocol.setTaskNo(taskNo);
        stationProtocol.setTargetStaNo(targetStationId);
        stationProtocol.setTaskBufferItems(taskBufferItems);
        return stationProtocol;
    }
}
src/test/java/com/zy/core/utils/station/StationDispatchRuntimeStateSupportTest.java
New file
@@ -0,0 +1,139 @@
package com.zy.core.utils.station;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.zy.asrs.entity.BasStationOpt;
import com.zy.asrs.service.BasStationOptService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.model.command.StationCommand;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Collections;
import java.util.Date;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class StationDispatchRuntimeStateSupportTest {
    @Test
    void idleTrack_isTimeoutUsesFirstSeenTime() {
        StationTaskIdleTrack track = new StationTaskIdleTrack(100, 145, System.currentTimeMillis() - 11_000L);
        assertTrue(track.isTimeout(10));
    }
    @Test
    void touchIdleTrack_replacesTrackWhenStationChanges() {
        StationDispatchRuntimeStateSupport support = new StationDispatchRuntimeStateSupport();
        RedisUtil redisUtil = mock(RedisUtil.class);
        ReflectionTestUtils.setField(support, "redisUtil", redisUtil);
        when(redisUtil.get(RedisKeyType.STATION_TASK_IDLE_TRACK_.key + 100))
                .thenReturn(JSON.toJSONString(new StationTaskIdleTrack(100, 145, System.currentTimeMillis() - 3_000L)));
        StationTaskIdleTrack track = support.touchIdleTrack(100, 146);
        assertEquals(146, track.getStationId());
        verify(redisUtil).set(eq(RedisKeyType.STATION_TASK_IDLE_TRACK_.key + 100), org.mockito.ArgumentMatchers.any(String.class), eq(3600L));
    }
    @Test
    void hasRecentIssuedMoveCommand_returnsTrueForRecentMoveLog() {
        StationDispatchRuntimeStateSupport support = new StationDispatchRuntimeStateSupport();
        BasStationOptService basStationOptService = mock(BasStationOptService.class);
        ReflectionTestUtils.setField(support, "basStationOptService", basStationOptService);
        when(basStationOptService.list(org.mockito.ArgumentMatchers.<Wrapper<BasStationOpt>>any()))
                .thenReturn(Collections.singletonList(new BasStationOpt()));
        boolean result = support.hasRecentIssuedMoveCommand(100, 145, 10_000L);
        assertTrue(result);
    }
    @Test
    void clearIssuedMoveCommandsDuringIdleStay_marksSendZeroAndAppendsMemo() {
        StationDispatchRuntimeStateSupport support = new StationDispatchRuntimeStateSupport();
        BasStationOptService basStationOptService = mock(BasStationOptService.class);
        ReflectionTestUtils.setField(support, "basStationOptService", basStationOptService);
        BasStationOpt opt = new BasStationOpt();
        opt.setId(1L);
        opt.setSend(1);
        opt.setMemo("existing");
        opt.setSendTime(new Date());
        when(basStationOptService.list(org.mockito.ArgumentMatchers.<Wrapper<BasStationOpt>>any()))
                .thenReturn(Collections.singletonList(opt));
        int clearedCount = support.clearIssuedMoveCommandsDuringIdleStay(
                new StationTaskIdleTrack(100, 145, System.currentTimeMillis() - 10_000L),
                100,
                145
        );
        assertEquals(1, clearedCount);
        assertEquals(0, opt.getSend());
        assertTrue(opt.getMemo().contains("existing"));
        assertTrue(opt.getMemo().contains("idleRecoverRerouteCleared(stationId=145)"));
        assertFalse(opt.getUpdateTime() == null);
        verify(basStationOptService).updateBatchById(Collections.singletonList(opt));
    }
    @Test
    void tryAcquireOutOrderDispatchLock_returnsFalseWhenKeyAlreadyExists() {
        StationDispatchRuntimeStateSupport support = new StationDispatchRuntimeStateSupport();
        RedisUtil redisUtil = mock(RedisUtil.class);
        ReflectionTestUtils.setField(support, "redisUtil", redisUtil);
        when(redisUtil.get(RedisKeyType.STATION_OUT_ORDER_DISPATCH_LIMIT_.key + "100_145")).thenReturn("lock");
        boolean acquired = support.tryAcquireOutOrderDispatchLock(100, 145, 2);
        assertFalse(acquired);
    }
    @Test
    void signalSegmentReset_setsAndClearsResetKey() {
        StationDispatchRuntimeStateSupport support = new StationDispatchRuntimeStateSupport();
        RedisUtil redisUtil = mock(RedisUtil.class);
        ReflectionTestUtils.setField(support, "redisUtil", redisUtil);
        support.signalSegmentReset(100, 0L);
        verify(redisUtil).set(RedisKeyType.DEVICE_STATION_MOVE_RESET.key + 100, "cancel", 3);
        verify(redisUtil).del(RedisKeyType.DEVICE_STATION_MOVE_RESET.key + 100);
    }
    @Test
    void watchCircleCommand_roundTripsThroughRedis() {
        StationDispatchRuntimeStateSupport support = new StationDispatchRuntimeStateSupport();
        RedisUtil redisUtil = mock(RedisUtil.class);
        ReflectionTestUtils.setField(support, "redisUtil", redisUtil);
        AtomicReference<Object> storedValue = new AtomicReference<>();
        when(redisUtil.get(RedisKeyType.WATCH_CIRCLE_STATION_.key + 100))
                .thenAnswer(invocation -> storedValue.get());
        when(redisUtil.set(eq(RedisKeyType.WATCH_CIRCLE_STATION_.key + 100), org.mockito.ArgumentMatchers.any(String.class), eq(60L * 60 * 24)))
                .thenAnswer(invocation -> {
                    storedValue.set(invocation.getArgument(1));
                    return true;
                });
        StationCommand command = new StationCommand();
        command.setTaskNo(100);
        command.setTargetStaNo(145);
        support.saveWatchCircleCommand(100, command);
        StationCommand loaded = support.loadWatchCircleCommand(100);
        assertNotNull(loaded);
        assertEquals(145, loaded.getTargetStaNo());
        support.clearWatchCircleCommand(100);
        verify(redisUtil).del(RedisKeyType.WATCH_CIRCLE_STATION_.key + 100);
    }
}
src/test/java/com/zy/core/utils/station/StationOutboundDispatchProcessorTest.java
New file
@@ -0,0 +1,202 @@
package com.zy.core.utils.station;
import com.zy.asrs.domain.enums.NotifyMsgType;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.WrkAnalysisService;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.utils.NotifyUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatchResult;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.WrkStsType;
import com.zy.core.model.StationObjModel;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.move.StationMoveCoordinator;
import com.zy.core.thread.StationThread;
import com.zy.core.utils.station.model.DispatchLimitConfig;
import com.zy.core.utils.station.model.LoadGuardState;
import com.zy.core.utils.station.model.LoopHitResult;
import com.zy.core.utils.station.model.OutOrderDispatchDecision;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class StationOutboundDispatchProcessorTest {
    @Test
    void crnStationOutExecute_recordsDispatchSessionAndClearsStationCacheAfterIssuingMoveCommand() {
        StationOutboundDispatchProcessor processor = new StationOutboundDispatchProcessor();
        WrkMastService wrkMastService = mock(WrkMastService.class);
        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        StationCommandDispatcher dispatcher = mock(StationCommandDispatcher.class);
        StationDispatchLoadSupport loadSupport = mock(StationDispatchLoadSupport.class);
        StationOutboundDecisionSupport decisionSupport = mock(StationOutboundDecisionSupport.class);
        StationThread stationThread = mock(StationThread.class);
        ReflectionTestUtils.setField(processor, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(processor, "wrkAnalysisService", wrkAnalysisService);
        ReflectionTestUtils.setField(processor, "redisUtil", redisUtil);
        ReflectionTestUtils.setField(processor, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(processor, "stationCommandDispatcher", dispatcher);
        ReflectionTestUtils.setField(processor, "stationDispatchLoadSupport", loadSupport);
        ReflectionTestUtils.setField(processor, "stationOutboundDecisionSupport", decisionSupport);
        WrkMast wrkMast = buildWrkMast(10001, 145);
        wrkMast.setWrkSts(WrkStsType.OUTBOUND_RUN_COMPLETE.sts);
        wrkMast.setCrnNo(4);
        when(wrkMastService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
                .thenReturn(Collections.singletonList(wrkMast));
        when(wrkMastService.updateById(wrkMast)).thenReturn(true);
        StationObjModel stationObjModel = new StationObjModel();
        stationObjModel.setDeviceNo(1);
        stationObjModel.setStationId(101);
        when(redisUtil.get(RedisKeyType.CRN_OUT_TASK_COMPLETE_STATION_INFO.key + 10001))
                .thenReturn(com.alibaba.fastjson.JSON.toJSONString(stationObjModel));
        when(redisUtil.get(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + 101)).thenReturn(null);
        StationProtocol stationProtocol = buildStationProtocol(101, 0, 101);
        stationProtocol.setAutoing(true);
        stationProtocol.setLoading(true);
        when(stationThread.getStatusMap()).thenReturn(Map.of(101, stationProtocol));
        DispatchLimitConfig limitConfig = new DispatchLimitConfig();
        LoadGuardState loadGuardState = new LoadGuardState();
        LoopHitResult noHit = LoopHitResult.noHit();
        OutOrderDispatchDecision decision = OutOrderDispatchDecision.direct(145);
        StationCommand command = buildCommand(10001, 101, 145);
        when(loadSupport.getDispatchLimitConfig(null, null)).thenReturn(limitConfig);
        when(loadSupport.countCurrentStationTask()).thenReturn(0);
        when(loadSupport.buildLoadGuardState(limitConfig)).thenReturn(loadGuardState);
        when(decisionSupport.getAllOutOrderList()).thenReturn(List.of(101, 121, 145));
        when(decisionSupport.resolveOutboundPathLenFactor(wrkMast)).thenReturn(0.25d);
        when(decisionSupport.resolveOutboundDispatchDecision(101, wrkMast, List.of(101, 121, 145), 0.25d)).thenReturn(decision);
        when(loadSupport.getDispatchLimitConfig(101, 145)).thenReturn(limitConfig);
        when(loadSupport.findPathLoopHit(limitConfig, 101, 145, loadGuardState, wrkMast, 0.25d)).thenReturn(noHit);
        when(loadSupport.isDispatchBlocked(limitConfig, 0, loadGuardState, false)).thenReturn(false);
        when(decisionSupport.buildOutboundMoveCommand(stationThread, wrkMast, 101, 145, 0.25d)).thenReturn(command);
        when(dispatcher.dispatch(1, command, "station-operate-process", "crnStationOutExecute"))
                .thenReturn(StationCommandDispatchResult.accepted("accepted", 1, "station-operate-process", "crnStationOutExecute"));
        SlaveConnection.put(SlaveType.Devp, 1, stationThread);
        try {
            processor.crnStationOutExecute();
            verify(wrkAnalysisService, times(1)).markOutboundStationStart(eq(wrkMast), any(Date.class));
            verify(coordinator, times(1)).recordDispatch(eq(10001), eq(101), eq("crnStationOutExecute"), same(command), eq(false));
            verify(redisUtil, times(1)).set(eq(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + 101), eq("lock"), eq(5L));
            verify(redisUtil, times(1)).del(RedisKeyType.CRN_OUT_TASK_COMPLETE_STATION_INFO.key + 10001);
            verify(loadSupport, times(1)).saveLoopLoadReserve(10001, noHit);
            assertEquals(WrkStsType.STATION_RUN.sts, wrkMast.getWrkSts());
        } finally {
            SlaveConnection.remove(SlaveType.Devp, 1);
        }
    }
    @Test
    void dualCrnStationOutExecute_recordsDispatchSessionAndClearsDualCacheAfterIssuingMoveCommand() {
        StationOutboundDispatchProcessor processor = new StationOutboundDispatchProcessor();
        WrkMastService wrkMastService = mock(WrkMastService.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        StationCommandDispatcher dispatcher = mock(StationCommandDispatcher.class);
        NotifyUtils notifyUtils = mock(NotifyUtils.class);
        StationOutboundDecisionSupport decisionSupport = mock(StationOutboundDecisionSupport.class);
        StationThread stationThread = mock(StationThread.class);
        ReflectionTestUtils.setField(processor, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(processor, "redisUtil", redisUtil);
        ReflectionTestUtils.setField(processor, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(processor, "stationCommandDispatcher", dispatcher);
        ReflectionTestUtils.setField(processor, "notifyUtils", notifyUtils);
        ReflectionTestUtils.setField(processor, "stationOutboundDecisionSupport", decisionSupport);
        WrkMast wrkMast = buildWrkMast(10002, 188);
        wrkMast.setWrkSts(WrkStsType.OUTBOUND_RUN_COMPLETE.sts);
        wrkMast.setDualCrnNo(2);
        when(wrkMastService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
                .thenReturn(Collections.singletonList(wrkMast));
        when(wrkMastService.updateById(wrkMast)).thenReturn(true);
        StationObjModel stationObjModel = new StationObjModel();
        stationObjModel.setDeviceNo(1);
        stationObjModel.setStationId(121);
        when(redisUtil.get(RedisKeyType.DUAL_CRN_OUT_TASK_STATION_INFO.key + 10002))
                .thenReturn(com.alibaba.fastjson.JSON.toJSONString(stationObjModel));
        when(redisUtil.get(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + 121)).thenReturn(null);
        StationProtocol stationProtocol = buildStationProtocol(121, 0, 121);
        stationProtocol.setAutoing(true);
        stationProtocol.setLoading(true);
        when(stationThread.getStatusMap()).thenReturn(Map.of(121, stationProtocol));
        StationCommand command = buildCommand(10002, 121, 188);
        when(decisionSupport.resolveOutboundPathLenFactor(wrkMast)).thenReturn(0.5d);
        when(decisionSupport.buildOutboundMoveCommand(stationThread, wrkMast, 121, 188, 0.5d)).thenReturn(command);
        when(dispatcher.dispatch(1, command, "station-operate-process", "dualCrnStationOutExecute"))
                .thenReturn(StationCommandDispatchResult.accepted("accepted", 1, "station-operate-process", "dualCrnStationOutExecute"));
        SlaveConnection.put(SlaveType.Devp, 1, stationThread);
        try {
            processor.dualCrnStationOutExecute();
            verify(coordinator, times(1)).recordDispatch(eq(10002), eq(121), eq("dualCrnStationOutExecute"), same(command), eq(false));
            verify(notifyUtils, times(1)).notify(String.valueOf(SlaveType.Devp), 1, "10002", wrkMast.getWmsWrkNo(), NotifyMsgType.STATION_OUT_TASK_RUN, null);
            verify(redisUtil, times(1)).set(eq(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + 121), eq("lock"), eq(5L));
            verify(redisUtil, times(1)).del(RedisKeyType.DUAL_CRN_OUT_TASK_STATION_INFO.key + 10002);
            assertEquals(WrkStsType.STATION_RUN.sts, wrkMast.getWrkSts());
        } finally {
            SlaveConnection.remove(SlaveType.Devp, 1);
        }
    }
    private static WrkMast buildWrkMast(int wrkNo, int targetStationId) {
        WrkMast wrkMast = new WrkMast();
        wrkMast.setWrkNo(wrkNo);
        wrkMast.setStaNo(targetStationId);
        wrkMast.setWmsWrkNo("WMS-" + wrkNo);
        wrkMast.setIoTime(new Date());
        return wrkMast;
    }
    private static StationProtocol buildStationProtocol(int stationId,
                                                        int taskNo,
                                                        int targetStationId) {
        StationProtocol stationProtocol = new StationProtocol();
        stationProtocol.setStationId(stationId);
        stationProtocol.setTaskNo(taskNo);
        stationProtocol.setTargetStaNo(targetStationId);
        return stationProtocol;
    }
    private static StationCommand buildCommand(int wrkNo,
                                               int stationId,
                                               int targetStationId) {
        StationCommand command = new StationCommand();
        command.setTaskNo(wrkNo);
        command.setStationId(stationId);
        command.setTargetStaNo(targetStationId);
        return command;
    }
}
src/test/java/com/zy/core/utils/station/StationRegularDispatchProcessorTest.java
New file
@@ -0,0 +1,240 @@
package com.zy.core.utils.station;
import com.zy.asrs.domain.enums.NotifyMsgType;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.BasStation;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.BasStationService;
import com.zy.asrs.service.WrkAnalysisService;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.utils.NotifyUtils;
import com.zy.common.entity.FindCrnNoResult;
import com.zy.common.service.CommonService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.dispatch.StationCommandDispatchResult;
import com.zy.core.dispatch.StationCommandDispatcher;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.WrkStsType;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.move.StationMoveCoordinator;
import com.zy.core.thread.StationThread;
import com.zy.core.utils.station.model.DispatchLimitConfig;
import com.zy.core.utils.station.model.LoadGuardState;
import com.zy.core.utils.station.model.LoopHitResult;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class StationRegularDispatchProcessorTest {
    @Test
    void stationInExecute_recordsDispatchSessionAfterIssuingMoveCommand() {
        StationRegularDispatchProcessor processor = new StationRegularDispatchProcessor();
        BasDevpService basDevpService = mock(BasDevpService.class);
        WrkMastService wrkMastService = mock(WrkMastService.class);
        CommonService commonService = mock(CommonService.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        StationCommandDispatcher dispatcher = mock(StationCommandDispatcher.class);
        StationDispatchLoadSupport loadSupport = mock(StationDispatchLoadSupport.class);
        StationThread stationThread = mock(StationThread.class);
        ReflectionTestUtils.setField(processor, "basDevpService", basDevpService);
        ReflectionTestUtils.setField(processor, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(processor, "commonService", commonService);
        ReflectionTestUtils.setField(processor, "redisUtil", redisUtil);
        ReflectionTestUtils.setField(processor, "wrkAnalysisService", wrkAnalysisService);
        ReflectionTestUtils.setField(processor, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(processor, "stationCommandDispatcher", dispatcher);
        ReflectionTestUtils.setField(processor, "stationDispatchLoadSupport", loadSupport);
        BasDevp basDevp = buildBasDevp(1);
        basDevp.setBarcodeStationList("[{\"deviceNo\":1,\"stationId\":101}]");
        when(basDevpService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
                .thenReturn(Collections.singletonList(basDevp));
        StationProtocol stationProtocol = buildStationProtocol(101, 500670, 101);
        stationProtocol.setAutoing(true);
        stationProtocol.setLoading(true);
        stationProtocol.setBarcode("GSL110005");
        when(stationThread.getStatusMap()).thenReturn(Map.of(101, stationProtocol));
        WrkMast wrkMast = buildWrkMast(500670, 102);
        wrkMast.setWrkSts(WrkStsType.NEW_INBOUND.sts);
        wrkMast.setLocNo("8-4-1");
        when(wrkMastService.getOne(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class))).thenReturn(wrkMast);
        when(wrkMastService.updateById(wrkMast)).thenReturn(true);
        FindCrnNoResult findCrnNoResult = new FindCrnNoResult();
        findCrnNoResult.setCrnNo(4);
        findCrnNoResult.setCrnType(SlaveType.Crn);
        when(commonService.findCrnNoByLocNo("8-4-1")).thenReturn(findCrnNoResult);
        when(commonService.findInStationId(findCrnNoResult, 101)).thenReturn(102);
        StationCommand command = new StationCommand();
        command.setTaskNo(500670);
        command.setStationId(101);
        command.setTargetStaNo(102);
        when(stationThread.getCommand(com.zy.core.enums.StationCommandType.MOVE, 500670, 101, 102, 0))
                .thenReturn(command);
        when(redisUtil.get(anyString())).thenReturn(null);
        when(dispatcher.dispatch(1, command, "station-operate-process", "stationInExecute"))
                .thenReturn(StationCommandDispatchResult.accepted("accepted", 1, "station-operate-process", "stationInExecute"));
        DispatchLimitConfig baseConfig = new DispatchLimitConfig();
        LoadGuardState loadGuardState = new LoadGuardState();
        LoopHitResult noHit = LoopHitResult.noHit();
        when(loadSupport.getDispatchLimitConfig(null, null)).thenReturn(baseConfig);
        when(loadSupport.countCurrentStationTask()).thenReturn(0);
        when(loadSupport.buildLoadGuardState(baseConfig)).thenReturn(loadGuardState);
        when(loadSupport.getDispatchLimitConfig(101, 102)).thenReturn(baseConfig);
        when(loadSupport.findPathLoopHit(baseConfig, 101, 102, loadGuardState)).thenReturn(noHit);
        when(loadSupport.isDispatchBlocked(baseConfig, 0, loadGuardState, false)).thenReturn(false);
        SlaveConnection.put(SlaveType.Devp, 1, stationThread);
        try {
            processor.stationInExecute();
            verify(coordinator, times(1)).recordDispatch(eq(500670), eq(101), eq("stationInExecute"), same(command), eq(false));
            verify(redisUtil, times(1)).set(eq(RedisKeyType.STATION_IN_EXECUTE_LIMIT.key + 101), eq("lock"), eq(5L));
            verify(loadSupport, times(1)).saveLoopLoadReserve(500670, noHit);
        } finally {
            SlaveConnection.remove(SlaveType.Devp, 1);
        }
    }
    @Test
    void stationOutExecuteFinish_attemptsClearPathBeforeCompletingTask() {
        StationRegularDispatchProcessor processor = new StationRegularDispatchProcessor();
        WrkMastService wrkMastService = mock(WrkMastService.class);
        BasStationService basStationService = mock(BasStationService.class);
        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
        NotifyUtils notifyUtils = mock(NotifyUtils.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        StationThread stationThread = mock(StationThread.class);
        ReflectionTestUtils.setField(processor, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(processor, "basStationService", basStationService);
        ReflectionTestUtils.setField(processor, "wrkAnalysisService", wrkAnalysisService);
        ReflectionTestUtils.setField(processor, "notifyUtils", notifyUtils);
        ReflectionTestUtils.setField(processor, "redisUtil", redisUtil);
        ReflectionTestUtils.setField(processor, "stationMoveCoordinator", coordinator);
        WrkMast wrkMast = buildWrkMast(10335, 145);
        wrkMast.setWrkSts(WrkStsType.STATION_RUN.sts);
        when(wrkMastService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
                .thenReturn(Collections.singletonList(wrkMast));
        when(wrkMastService.updateById(wrkMast)).thenReturn(true);
        BasStation basStation = new BasStation();
        basStation.setStationId(145);
        basStation.setDeviceNo(1);
        when(basStationService.getOne(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
                .thenReturn(basStation);
        StationProtocol stationProtocol = buildStationProtocol(145, 10335, 145);
        when(stationThread.getStatusMap()).thenReturn(Map.of(145, stationProtocol));
        when(stationThread.clearPath(10335)).thenReturn(true);
        SlaveConnection.put(SlaveType.Devp, 1, stationThread);
        try {
            processor.stationOutExecuteFinish();
            verify(stationThread, times(1)).clearPath(10335);
            verify(coordinator, times(1)).finishSession(10335);
            verify(wrkMastService, times(1)).updateById(wrkMast);
            verify(notifyUtils, times(1)).notify(String.valueOf(SlaveType.Devp), 1, "10335", wrkMast.getWmsWrkNo(), NotifyMsgType.STATION_OUT_TASK_RUN_COMPLETE, null);
            assertEquals(WrkStsType.STATION_RUN_COMPLETE.sts, wrkMast.getWrkSts());
        } finally {
            SlaveConnection.remove(SlaveType.Devp, 1);
        }
    }
    @Test
    void checkTaskToComplete_marksTaskCompleteAfterLeavingTargetStation() {
        StationRegularDispatchProcessor processor = new StationRegularDispatchProcessor();
        WrkMastService wrkMastService = mock(WrkMastService.class);
        BasStationService basStationService = mock(BasStationService.class);
        StationMoveCoordinator coordinator = mock(StationMoveCoordinator.class);
        RedisUtil redisUtil = mock(RedisUtil.class);
        StationThread stationThread = mock(StationThread.class);
        ReflectionTestUtils.setField(processor, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(processor, "basStationService", basStationService);
        ReflectionTestUtils.setField(processor, "stationMoveCoordinator", coordinator);
        ReflectionTestUtils.setField(processor, "redisUtil", redisUtil);
        WrkMast wrkMast = buildWrkMast(10335, 145);
        wrkMast.setWrkSts(WrkStsType.STATION_RUN_COMPLETE.sts);
        when(wrkMastService.list(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
                .thenReturn(Collections.singletonList(wrkMast));
        BasStation basStation = new BasStation();
        basStation.setStationId(145);
        basStation.setDeviceNo(1);
        when(basStationService.getOne(any(com.baomidou.mybatisplus.core.conditions.Wrapper.class)))
                .thenReturn(basStation);
        StationProtocol stationProtocol = buildStationProtocol(145, 0, 145);
        when(stationThread.getStatusMap()).thenReturn(Map.of(145, stationProtocol));
        when(redisUtil.get(anyString())).thenReturn(null);
        SlaveConnection.put(SlaveType.Devp, 1, stationThread);
        try {
            processor.checkTaskToComplete();
            verify(coordinator, times(1)).finishSession(10335);
            verify(wrkMastService, times(1)).updateById(wrkMast);
            assertEquals(WrkStsType.COMPLETE_OUTBOUND.sts, wrkMast.getWrkSts());
        } finally {
            SlaveConnection.remove(SlaveType.Devp, 1);
        }
    }
    private static BasDevp buildBasDevp(int devpNo) {
        BasDevp basDevp = new BasDevp();
        basDevp.setDevpNo(devpNo);
        return basDevp;
    }
    private static WrkMast buildWrkMast(int wrkNo, int targetStationId) {
        WrkMast wrkMast = new WrkMast();
        wrkMast.setWrkNo(wrkNo);
        wrkMast.setStaNo(targetStationId);
        wrkMast.setWmsWrkNo("WMS-" + wrkNo);
        wrkMast.setIoTime(new Date());
        return wrkMast;
    }
    private static StationProtocol buildStationProtocol(int stationId,
                                                        int taskNo,
                                                        int targetStationId) {
        StationProtocol stationProtocol = new StationProtocol();
        stationProtocol.setStationId(stationId);
        stationProtocol.setTaskNo(taskNo);
        stationProtocol.setTargetStaNo(targetStationId);
        return stationProtocol;
    }
}
src/test/java/com/zy/core/utils/station/model/StationModelTypePlacementTest.java
New file
@@ -0,0 +1,49 @@
package com.zy.core.utils.station.model;
import com.zy.core.utils.StationOperateProcessUtils;
import com.zy.core.utils.station.StationDispatchLoadSupport;
import com.zy.core.utils.station.StationOutboundDecisionSupport;
import com.zy.core.utils.station.StationRerouteProcessor;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class StationModelTypePlacementTest {
    @Test
    void rerouteTypes_areNoLongerNestedInsideStationOperateProcessUtils() {
        assertFalse(hasDeclaredClass(StationOperateProcessUtils.class, "RerouteSceneType"));
        assertFalse(hasDeclaredClass(StationOperateProcessUtils.class, "RerouteDecision"));
        assertFalse(hasDeclaredClass(StationOperateProcessUtils.class, "RerouteContext"));
        assertFalse(hasDeclaredClass(StationOperateProcessUtils.class, "RerouteCommandPlan"));
        assertFalse(hasDeclaredClass(StationOperateProcessUtils.class, "RerouteExecutionResult"));
    }
    @Test
    void dispatchLoadTypes_areNoLongerNestedInsideStationDispatchLoadSupport() {
        assertFalse(hasDeclaredClass(StationDispatchLoadSupport.class, "DispatchLimitConfig"));
        assertFalse(hasDeclaredClass(StationDispatchLoadSupport.class, "LoadGuardState"));
        assertFalse(hasDeclaredClass(StationDispatchLoadSupport.class, "LoopHitResult"));
    }
    @Test
    void outboundDecisionTypes_areNoLongerNestedInsideStationOutboundDecisionSupport() {
        assertFalse(hasDeclaredClass(StationOutboundDecisionSupport.class, "OutOrderDispatchDecision"));
        assertFalse(hasDeclaredClass(StationOutboundDecisionSupport.class, "CircleTargetCandidate"));
    }
    @Test
    void rerouteProcessor_isLocatedUnderStationPackage() {
        assertEquals("com.zy.core.utils.station", StationRerouteProcessor.class.getPackageName());
    }
    private static boolean hasDeclaredClass(Class<?> ownerType, String simpleName) {
        for (Class<?> declaredClass : ownerType.getDeclaredClasses()) {
            if (simpleName.equals(declaredClass.getSimpleName())) {
                return true;
            }
        }
        return false;
    }
}