#
Junjie
1 天以前 a0d8732c1d698b25850c0949f7b8967333d67d21
#
1个文件已添加
4个文件已修改
742 ■■■■■ 已修改文件
src/main/java/com/zy/core/service/StationTaskLoopService.java 570 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/ZyStationV5Thread.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/StationOperateProcessUtils.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/watch/stationTrace.html 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/service/StationTaskLoopService.java
New file
@@ -0,0 +1,570 @@
package com.zy.core.service;
import com.alibaba.fastjson.JSON;
import com.core.common.Cools;
import com.zy.asrs.domain.vo.StationCycleCapacityVo;
import com.zy.asrs.domain.vo.StationCycleLoopVo;
import com.zy.asrs.service.StationCycleCapacityService;
import com.zy.common.utils.NavigateUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.trace.StationTaskTraceRegistry;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
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;
@Component
public class StationTaskLoopService {
    private static final int LOOP_STATE_EXPIRE_SECONDS = 60 * 60 * 24;
    private static final int LOOP_REPEAT_TRIGGER_COUNT = 3;
    private static final int LOCAL_LOOP_NEIGHBOR_HOP = 3;
    private static final String OUT_ORDER_SCOPE_TYPE = "outOrderCircle";
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private NavigateUtils navigateUtils;
    @Autowired
    private StationCycleCapacityService stationCycleCapacityService;
    @Autowired
    private StationTaskTraceRegistry stationTaskTraceRegistry;
    public LoopEvaluation evaluateLoop(Integer taskNo,
                                       Integer stationId,
                                       boolean includePendingIssue) {
        return evaluateLoop(taskNo, stationId, includePendingIssue, null, null);
    }
    public LoopEvaluation evaluateLoop(Integer taskNo,
                                       Integer stationId,
                                       boolean includePendingIssue,
                                       List<Integer> customStationIdList,
                                       String customScopeType) {
        LoopIdentitySnapshot loopIdentity = buildEffectiveLoopIdentity(stationId, customStationIdList, customScopeType);
        if (taskNo == null || taskNo <= 0) {
            return new LoopEvaluation(taskNo, stationId, loopIdentity, 0, 0, false);
        }
        TaskLoopState state = loadTaskLoopState(taskNo);
        int currentLoopIssueCount = resolveCurrentLoopIssueCount(state, loopIdentity);
        int expectedLoopIssueCount = includePendingIssue && loopIdentity.hasFingerprint()
                ? currentLoopIssueCount + 1
                : currentLoopIssueCount;
        return new LoopEvaluation(
                taskNo,
                stationId,
                loopIdentity,
                currentLoopIssueCount,
                expectedLoopIssueCount,
                shouldTriggerLargeLoop(expectedLoopIssueCount)
        );
    }
    public void recordLoopIssue(LoopEvaluation evaluation, String triggerSource) {
        if (evaluation == null
                || evaluation.getTaskNo() == null
                || evaluation.getTaskNo() <= 0
                || evaluation.getLoopIdentity() == null
                || !evaluation.getLoopIdentity().hasFingerprint()) {
            return;
        }
        TaskLoopState state = loadTaskLoopState(evaluation.getTaskNo());
        int nextLoopIssueCount = state.getLoopIssueCountMap().getOrDefault(evaluation.getLoopIdentity().getLoopFingerprint(), 0) + 1;
        state.getLoopIssueCountMap().put(evaluation.getLoopIdentity().getLoopFingerprint(), nextLoopIssueCount);
        state.setTaskNo(evaluation.getTaskNo());
        state.setLastLoopFingerprint(evaluation.getLoopIdentity().getLoopFingerprint());
        state.setLastIssueTime(System.currentTimeMillis());
        saveTaskLoopState(state);
        syncTaskTraceLoopAlert(evaluation, nextLoopIssueCount, triggerSource);
    }
    public boolean shouldTriggerLargeLoop(int loopIssueCount) {
        return loopIssueCount >= LOOP_REPEAT_TRIGGER_COUNT;
    }
    private void syncTaskTraceLoopAlert(LoopEvaluation evaluation,
                                        int loopIssueCount,
                                        String triggerSource) {
        if (stationTaskTraceRegistry == null || evaluation == null || evaluation.getTaskNo() == null || evaluation.getTaskNo() <= 0) {
            return;
        }
        LoopIdentitySnapshot loopIdentity = evaluation.getLoopIdentity();
        boolean active = loopIdentity != null
                && loopIdentity.hasFingerprint()
                && shouldTriggerLargeLoop(loopIssueCount);
        String loopAlertType = resolveLoopAlertType(loopIdentity);
        String loopAlertText = buildLoopAlertText(loopAlertType, loopIdentity, loopIssueCount);
        Map<String, Object> details = new HashMap<>();
        details.put("stationId", evaluation.getStationId());
        details.put("loopScopeType", loopIdentity == null ? "" : loopIdentity.getScopeType());
        details.put("loopStationCount", loopIdentity == null ? 0 : loopIdentity.getLocalStationCount());
        details.put("sourceLoopStationCount", loopIdentity == null ? 0 : loopIdentity.getSourceLoopStationCount());
        details.put("loopRepeatCount", loopIssueCount);
        details.put("loopTriggerSource", triggerSource);
        stationTaskTraceRegistry.updateLoopHint(
                evaluation.getTaskNo(),
                active,
                loopAlertType,
                loopAlertText,
                loopIssueCount,
                details
        );
    }
    private String resolveLoopAlertType(LoopIdentitySnapshot loopIdentity) {
        if (loopIdentity == null || !loopIdentity.hasFingerprint()) {
            return "";
        }
        if (OUT_ORDER_SCOPE_TYPE.equals(loopIdentity.getScopeType())) {
            return "OUT_ORDER_CIRCLE";
        }
        return "wholeLoop".equals(loopIdentity.getScopeType()) ? "LARGE_LOOP" : "SMALL_LOOP";
    }
    private String buildLoopAlertText(String loopAlertType,
                                      LoopIdentitySnapshot loopIdentity,
                                      int loopIssueCount) {
        if (Cools.isEmpty(loopAlertType) || loopIdentity == null || !shouldTriggerLargeLoop(loopIssueCount)) {
            return "";
        }
        String typeLabel;
        if ("OUT_ORDER_CIRCLE".equals(loopAlertType)) {
            typeLabel = "排序环线";
        } else if ("LARGE_LOOP".equals(loopAlertType)) {
            typeLabel = "大环线";
        } else {
            typeLabel = "小环线";
        }
        return typeLabel + "绕圈预警,累计绕圈" + loopIssueCount + "次,已启用绕大圈,当前识别范围"
                + loopIdentity.getLocalStationCount() + "站";
    }
    private LoopIdentitySnapshot buildEffectiveLoopIdentity(Integer stationId,
                                                            List<Integer> customStationIdList,
                                                            String customScopeType) {
        LoopIdentitySnapshot customLoopIdentity = buildCustomLoopIdentity(customStationIdList, customScopeType);
        if (customLoopIdentity.hasFingerprint()) {
            return customLoopIdentity;
        }
        return resolveStationLoopIdentity(stationId);
    }
    private LoopIdentitySnapshot buildCustomLoopIdentity(List<Integer> customStationIdList,
                                                         String customScopeType) {
        List<Integer> normalizedStationIdList = normalizeStationIdList(customStationIdList);
        if (normalizedStationIdList.isEmpty()) {
            return LoopIdentitySnapshot.empty();
        }
        String scopeType = Cools.isEmpty(customScopeType) ? OUT_ORDER_SCOPE_TYPE : customScopeType;
        return new LoopIdentitySnapshot(
                buildLoopFingerprint(normalizedStationIdList),
                new HashSet<>(normalizedStationIdList),
                normalizedStationIdList.size(),
                normalizedStationIdList.size(),
                scopeType
        );
    }
    public LoopIdentitySnapshot resolveStationLoopIdentity(Integer stationId) {
        if (stationId == null || stationId <= 0) {
            return LoopIdentitySnapshot.empty();
        }
        try {
            if (stationCycleCapacityService != null) {
                StationCycleCapacityVo capacityVo = stationCycleCapacityService.getLatestSnapshot();
                if (capacityVo != null && capacityVo.getLoopList() != null && !capacityVo.getLoopList().isEmpty()) {
                    for (StationCycleLoopVo loopVo : capacityVo.getLoopList()) {
                        List<Integer> loopStationIdList = normalizeStationIdList(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 buildStationFallbackLoopIdentity(stationId);
    }
    private LoopIdentitySnapshot buildStationFallbackLoopIdentity(Integer stationId) {
        if (stationId == null || stationId <= 0) {
            return LoopIdentitySnapshot.empty();
        }
        List<Integer> stationIdList = new ArrayList<>();
        stationIdList.add(stationId);
        return new LoopIdentitySnapshot(
                "station:" + stationId,
                new HashSet<>(stationIdList),
                1,
                1,
                "stationFallback"
        );
    }
    private LoopIdentitySnapshot buildLoopIdentity(List<Integer> stationIdList,
                                                   int sourceLoopStationCount,
                                                   String scopeType) {
        List<Integer> normalizedStationIdList = normalizeStationIdList(stationIdList);
        if (normalizedStationIdList.isEmpty()) {
            return LoopIdentitySnapshot.empty();
        }
        return new LoopIdentitySnapshot(
                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 = normalizeStationIdList(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 normalizeStationIdList(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() {
        if (navigateUtils == null) {
            return new HashMap<>();
        }
        Map<Integer, Set<Integer>> stationGraph = navigateUtils.loadUndirectedStationGraphSnapshot();
        return stationGraph == null ? new HashMap<>() : stationGraph;
    }
    private TaskLoopState loadTaskLoopState(Integer taskNo) {
        if (redisUtil == null || taskNo == null || taskNo <= 0) {
            return new TaskLoopState();
        }
        Object stateObj = redisUtil.get(RedisKeyType.STATION_RUN_BLOCK_TASK_LOOP_STATE_.key + taskNo);
        if (stateObj == null) {
            return new TaskLoopState();
        }
        try {
            TaskLoopState state = JSON.parseObject(String.valueOf(stateObj), TaskLoopState.class);
            return state == null ? new TaskLoopState() : state.normalize();
        } catch (Exception ignore) {
            return new TaskLoopState();
        }
    }
    private void saveTaskLoopState(TaskLoopState taskLoopState) {
        if (redisUtil == null
                || taskLoopState == null
                || taskLoopState.getTaskNo() == null
                || taskLoopState.getTaskNo() <= 0) {
            return;
        }
        taskLoopState.normalize();
        redisUtil.set(
                RedisKeyType.STATION_RUN_BLOCK_TASK_LOOP_STATE_.key + taskLoopState.getTaskNo(),
                JSON.toJSONString(taskLoopState),
                LOOP_STATE_EXPIRE_SECONDS
        );
    }
    private int resolveCurrentLoopIssueCount(TaskLoopState taskLoopState,
                                             LoopIdentitySnapshot loopIdentity) {
        if (taskLoopState == null || loopIdentity == null || !loopIdentity.hasFingerprint()) {
            return 0;
        }
        return taskLoopState.getLoopIssueCountMap().getOrDefault(loopIdentity.getLoopFingerprint(), 0);
    }
    private List<Integer> normalizeStationIdList(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();
    }
    @Data
    public static class LoopEvaluation {
        private final Integer taskNo;
        private final Integer stationId;
        private final LoopIdentitySnapshot loopIdentity;
        private final int currentLoopIssueCount;
        private final int expectedLoopIssueCount;
        private final boolean largeLoopTriggered;
    }
    @Data
    public static class LoopIdentitySnapshot {
        private final String loopFingerprint;
        private final Set<Integer> stationIdSet;
        private final int sourceLoopStationCount;
        private final int localStationCount;
        private final String scopeType;
        public boolean hasFingerprint() {
            return !Cools.isEmpty(loopFingerprint);
        }
        public static LoopIdentitySnapshot empty() {
            return new LoopIdentitySnapshot("", new HashSet<>(), 0, 0, "none");
        }
    }
    @Data
    private static class TaskLoopState {
        private Integer taskNo;
        private String lastLoopFingerprint;
        private Long lastIssueTime;
        private Map<String, Integer> loopIssueCountMap = new HashMap<>();
        private TaskLoopState normalize() {
            if (loopIssueCountMap == null) {
                loopIssueCountMap = new HashMap<>();
            }
            return this;
        }
    }
    @Data
    private static class StationHopNode {
        private final Integer stationId;
        private final int hop;
    }
}
src/main/java/com/zy/core/thread/impl/ZyStationV5Thread.java
@@ -31,6 +31,7 @@
import com.zy.core.network.DeviceConnectPool;
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.StationV5SegmentExecutor;
import com.zy.core.trace.StationTaskTraceRegistry;
import com.zy.core.utils.DeviceLogRedisKeyBuilder;
@@ -59,7 +60,7 @@
    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 = 2;
    private static final int LOOP_REPEAT_TRIGGER_COUNT = 3;
    private static final int LOCAL_LOOP_NEIGHBOR_HOP = 3;
    private List<StationProtocol> statusList = new ArrayList<>();
@@ -264,20 +265,21 @@
        }
        RunBlockRerouteState rerouteState = loadRunBlockRerouteState(taskNo, stationId);
        TaskLoopRerouteState taskLoopRerouteState = loadTaskLoopRerouteState(taskNo);
        LoopIdentity currentLoopIdentity = resolveStationLoopIdentity(stationId);
        StationTaskLoopService taskLoopService = loadStationTaskLoopService();
        StationTaskLoopService.LoopEvaluation loopEvaluation = taskLoopService == null
                ? new StationTaskLoopService.LoopEvaluation(taskNo, stationId, StationTaskLoopService.LoopIdentitySnapshot.empty(), 0, 0, false)
                : taskLoopService.evaluateLoop(taskNo, stationId, true);
        log.info("输送线堵塞重规划环线识别,taskNo={}, stationId={}, scopeType={}, localStationCount={}, sourceLoopStationCount={}",
                taskNo,
                stationId,
                currentLoopIdentity.getScopeType(),
                currentLoopIdentity.getLocalStationCount(),
                currentLoopIdentity.getSourceLoopStationCount());
                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());
        taskLoopRerouteState.setTaskNo(taskNo);
        List<List<NavigateNode>> candidatePathList = calcCandidatePathNavigateNodes(taskNo, stationId, targetStationId);
        if (candidatePathList.isEmpty()) {
@@ -289,8 +291,7 @@
        StationCommand rerouteCommand = selectAvailableRerouteCommand(
                rerouteState,
                taskLoopRerouteState,
                currentLoopIdentity,
                loopEvaluation,
                candidatePathList,
                taskNo,
                stationId,
@@ -303,8 +304,7 @@
            rerouteState.resetIssuedRoutes();
            rerouteCommand = selectAvailableRerouteCommand(
                    rerouteState,
                    taskLoopRerouteState,
                    currentLoopIdentity,
                    loopEvaluation,
                    candidatePathList,
                    taskNo,
                    stationId,
@@ -315,10 +315,9 @@
        if (rerouteCommand != null) {
            saveRunBlockRerouteState(rerouteState);
            touchTaskLoopRerouteState(taskLoopRerouteState, currentLoopIdentity);
            syncTaskTraceLoopAlert(taskNo, stationId, currentLoopIdentity,
                    resolveCurrentLoopIssuedCount(taskLoopRerouteState, currentLoopIdentity));
            saveTaskLoopRerouteState(taskLoopRerouteState);
            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()));
            return rerouteCommand;
@@ -455,8 +454,7 @@
    }
    private StationCommand selectAvailableRerouteCommand(RunBlockRerouteState rerouteState,
                                                         TaskLoopRerouteState taskLoopRerouteState,
                                                         LoopIdentity currentLoopIdentity,
                                                         StationTaskLoopService.LoopEvaluation loopEvaluation,
                                                         List<List<NavigateNode>> candidatePathList,
                                                         Integer taskNo,
                                                         Integer stationId,
@@ -467,7 +465,6 @@
        }
        Set<String> issuedRouteSignatureSet = rerouteState.getIssuedRouteSignatureSet();
        int currentLoopIssuedCount = resolveExpectedLoopIssuedCount(taskLoopRerouteState, currentLoopIdentity);
        List<RerouteCandidateCommand> candidateCommandList = new ArrayList<>();
        for (List<NavigateNode> candidatePath : candidatePathList) {
            StationCommand rerouteCommand = buildMoveCommand(taskNo, stationId, targetStationId, palletSize, candidatePath);
@@ -483,9 +480,13 @@
            candidateCommand.setRouteSignature(routeSignature);
            candidateCommand.setPathLength(rerouteCommand.getNavigatePath().size());
            candidateCommand.setIssuedCount(rerouteState.getRouteIssueCountMap().getOrDefault(routeSignature, 0));
            candidateCommand.setLoopFingerprint(currentLoopIdentity.getLoopFingerprint());
            candidateCommand.setLoopIssuedCount(currentLoopIssuedCount);
            candidateCommand.setCurrentLoopHitCount(countCurrentLoopStationHit(rerouteCommand.getNavigatePath(), currentLoopIdentity.getStationIdSet()));
            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()) {
@@ -550,8 +551,7 @@
                shortestPathOverused = true;
            }
            if (!Cools.isEmpty(candidateCommand.getLoopFingerprint())
                    && candidateCommand.getLoopIssuedCount() != null
                    && isLoopRepeatTriggered(candidateCommand.getLoopIssuedCount())) {
                    && Boolean.TRUE.equals(candidateCommand.getLoopTriggered())) {
                currentLoopOverused = true;
            }
        }
@@ -784,6 +784,14 @@
            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) {
@@ -1161,6 +1169,7 @@
        private Integer issuedCount;
        private String loopFingerprint;
        private Integer loopIssuedCount;
        private Boolean loopTriggered;
        private Integer currentLoopHitCount;
    }
src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java
@@ -637,7 +637,7 @@
                                                 Map<String, Object> details) {
            boolean active = Boolean.TRUE.equals(loopAlertActive)
                    && loopAlertCount != null
                    && loopAlertCount > 1
                    && loopAlertCount > 2
                    && loopAlertText != null
                    && !loopAlertText.trim().isEmpty();
            String nextType = active ? loopAlertType : null;
@@ -810,7 +810,7 @@
        }
        private void appendLoopHintDetails(Map<String, Object> details) {
            if (details == null || !Boolean.TRUE.equals(this.loopAlertActive) || this.loopAlertCount == null || this.loopAlertCount <= 1) {
            if (details == null || !Boolean.TRUE.equals(this.loopAlertActive) || this.loopAlertCount == null || this.loopAlertCount <= 2) {
                return;
            }
            details.put("loopAlertActive", Boolean.TRUE);
src/main/java/com/zy/core/utils/StationOperateProcessUtils.java
@@ -27,6 +27,7 @@
import com.zy.core.model.Task;
import com.zy.core.model.command.StationCommand;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.service.StationTaskLoopService;
import com.zy.core.thread.StationThread;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -67,6 +68,8 @@
    private StationPathPolicyService stationPathPolicyService;
    @Autowired
    private BasStationOptService basStationOptService;
    @Autowired
    private StationTaskLoopService stationTaskLoopService;
    //执行输送站点入库任务
    public synchronized void stationInExecute() {
@@ -772,7 +775,7 @@
        if (!Objects.equals(dispatchStationId, wrkMast.getStaNo())
                && isCurrentOutOrderStation(currentStationId, outOrderStationIds)
                && isWatchingCircleArrival(wrkMast.getWrkNo(), currentStationId)) {
            return new OutOrderDispatchDecision(dispatchStationId, true);
            return new OutOrderDispatchDecision(dispatchStationId, true, null, false);
        }
        return new OutOrderDispatchDecision(dispatchStationId, false);
    }
@@ -823,20 +826,38 @@
            if (hasReachableOutReleaseSlot(currentStationId, wrkMast.getStaNo())) {
                return new OutOrderDispatchDecision(wrkMast.getStaNo(), false);
            }
            Integer circleTarget = resolveNextCircleOrderTarget(currentStationId, outOrderStationIds);
            StationTaskLoopService.LoopEvaluation loopEvaluation = evaluateOutOrderLoop(
                    wrkMast.getWrkNo(),
                    currentStationId,
                    outOrderStationIds
            );
            Integer circleTarget = resolveNextCircleOrderTarget(
                    currentStationId,
                    outOrderStationIds,
                    loopEvaluation.isLargeLoopTriggered()
            );
            if (circleTarget == null) {
                News.taskInfo(wrkMast.getWrkNo(), "目标站当前不可进,且未找到可执行的下一排序检测点,当前站点={}", currentStationId);
                return null;
            }
            return new OutOrderDispatchDecision(circleTarget, true);
            return new OutOrderDispatchDecision(circleTarget, true, loopEvaluation, true);
        }
        Integer circleTarget = resolveNextCircleOrderTarget(currentStationId, outOrderStationIds);
        StationTaskLoopService.LoopEvaluation loopEvaluation = evaluateOutOrderLoop(
                wrkMast.getWrkNo(),
                currentStationId,
                outOrderStationIds
        );
        Integer circleTarget = resolveNextCircleOrderTarget(
                currentStationId,
                outOrderStationIds,
                loopEvaluation.isLargeLoopTriggered()
        );
        if (circleTarget == null) {
            News.taskInfo(wrkMast.getWrkNo(), "未找到可执行的下一排序检测点,当前站点={}", currentStationId);
            return null;
        }
        return new OutOrderDispatchDecision(circleTarget, true);
        return new OutOrderDispatchDecision(circleTarget, true, loopEvaluation, true);
    }
    private boolean shouldApplyOutOrder(WrkMast wrkMast, List<Integer> outOrderStationIds) {
@@ -882,9 +903,36 @@
        }
        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());
        }
    }
    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(Integer sourceStationId,
@@ -968,13 +1016,18 @@
                || (stationProtocol.getTaskNo() != null && stationProtocol.getTaskNo() > 0);
    }
    private Integer resolveNextCircleOrderTarget(Integer currentStationId, List<Integer> orderedOutStationList) {
    private Integer resolveNextCircleOrderTarget(Integer currentStationId,
                                                 List<Integer> orderedOutStationList,
                                                 boolean preferLargeCircle) {
        if (currentStationId == null || orderedOutStationList == null || orderedOutStationList.size() <= 1) {
            return null;
        }
        int startIndex = orderedOutStationList.indexOf(currentStationId);
        int total = orderedOutStationList.size();
        Integer bestStationId = null;
        int bestPathLength = -1;
        int bestOffset = -1;
        for (int offset = 1; offset < total; offset++) {
            int candidateIndex = (startIndex + offset + total) % total;
            Integer candidateStationId = orderedOutStationList.get(candidateIndex);
@@ -984,11 +1037,19 @@
            try {
                List<NavigateNode> path = navigateUtils.calcByStationId(currentStationId, candidateStationId);
                if (path != null && !path.isEmpty()) {
                    return candidateStationId;
                    if (!preferLargeCircle) {
                        return candidateStationId;
                    }
                    int pathLength = path.size();
                    if (pathLength > bestPathLength || (pathLength == bestPathLength && offset > bestOffset)) {
                        bestStationId = candidateStationId;
                        bestPathLength = pathLength;
                        bestOffset = offset;
                    }
                }
            } catch (Exception ignore) {}
        }
        return null;
        return bestStationId;
    }
    private boolean tryAcquireOutOrderDispatchLock(Integer wrkNo, Integer stationId) {
@@ -1423,10 +1484,21 @@
    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() {
@@ -1436,6 +1508,14 @@
        private boolean isCircle() {
            return circle;
        }
        private StationTaskLoopService.LoopEvaluation getLoopEvaluation() {
            return loopEvaluation;
        }
        private boolean shouldCountLoopIssue() {
            return countLoopIssue;
        }
    }
    private void saveLoopLoadReserve(Integer wrkNo, LoopHitResult loopHitResult) {
src/main/webapp/views/watch/stationTrace.html
@@ -874,6 +874,7 @@
                    loopAlertText: '绕圈提示',
                    loopAlertCount: '绕圈累计',
                    loopRepeatCount: '绕圈累计',
                    loopTriggerSource: '触发来源',
                    loopScopeType: '识别范围',
                    loopStationCount: '当前环范围站点数',
                    sourceLoopStationCount: '所在大环站点数',
@@ -891,6 +892,22 @@
                    if (value == null || value === '') {
                        return;
                    }
                    if (key === 'loopAlertType') {
                        if (value === 'OUT_ORDER_CIRCLE') {
                            value = '排序环线';
                        } else if (value === 'LARGE_LOOP') {
                            value = '大环线';
                        } else if (value === 'SMALL_LOOP') {
                            value = '小环线';
                        }
                    }
                    if (key === 'loopTriggerSource') {
                        if (value === 'OUT_ORDER_CIRCLE') {
                            value = '排序绕圈';
                        } else if (value === 'RUN_BLOCK_REROUTE') {
                            value = '堵塞重规划';
                        }
                    }
                    var text = Array.isArray(value) ? value.join(' -> ') : String(value);
                    result.push((labelMap[key] || key) + ': ' + text);
                });