Administrator
2026-04-25 3f797dd834a2de283cf5eff2ff1124e5a0ccb233
更改 2026-4-25
61个文件已修改
3815 ■■■■ 已修改文件
src/main/java/com/zy/ai/controller/WcsDiagnosisController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/LlmChatService.java 727 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/PythonService.java 60 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/WcsDiagnosisService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/BasMapController.java 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/ConsoleController.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/CrnController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/DualCrnController.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/OpenController.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/StationController.java 90 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/enums/NotifyMsgType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/vo/DualCrnStateTableVo.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/vo/StationLatestDataVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/BasDevp.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/WrkMastScheduler.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/utils/MapExcelUtils.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/model/NavigateNode.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/service/CommonService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/utils/NavigateUtils.java 213 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/ServerBootstrap.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/enums/RedisKeyType.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/enums/WrkStsType.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/model/command/StationCommand.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/model/protocol/DualCrnProtocol.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/model/protocol/StationProtocol.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/ZyStationConnectDriver.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/entity/ZyDualCrnStatusEntity.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/entity/ZyStationStatusEntity.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/fake/ZyDualCrnFakeConnect.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/fake/ZyStationFakeConnect.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java 117 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/real/ZyCrnV2RealConnect.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/real/ZyDualCrnRealConnect.java 132 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/real/ZyStationV3RealConnect.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/FakeProcess.java 108 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/NormalProcess.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/plugin/XiaosongProcess.java 13 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/task/InitLocMapScheduler.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/CrnThread.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/DualCrnThread.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/ZySiemensCrnV2Thread.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java 202 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java 53 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/utils/StationOperateProcessUtils.java 672 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/controller/LicenseCreatorController.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/CustomLicenseManager.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LinuxServerInfos.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/WindowsServerInfos.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/timer/LicenseTimer.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/DevpCard.js 210 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/MapCanvas.js 196 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/WatchDualCrnCard.js 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basDevp/basDevp.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basDevp/basDevp.html 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/locMap/locMap.html 232 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/login.html 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/watch/console.html 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
@@ -33,7 +33,7 @@
                WcsDiagnosisRequest request = aiUtils.makeAiRequest(1000, "对当前系统进行巡检,如果有异常情况就进行详细的分析,如果没有异常情况则当成一次检查\n\n");
                wcsDiagnosisService.diagnoseStream(request, emitter);
            } catch (Exception e) {
                emitter.completeWithError(e);
                try { emitter.complete(); } catch (Exception ignore) {}
            }
        }).start();
@@ -50,7 +50,7 @@
                WcsDiagnosisRequest request = aiUtils.makeAiRequest(100, null);
                wcsDiagnosisService.askStream(request, prompt, chatId, reset, emitter);
            } catch (Exception e) {
                emitter.completeWithError(e);
                try { emitter.complete(); } catch (Exception ignore) {}
            }
        }).start();
        return emitter;
src/main/java/com/zy/ai/service/LlmChatService.java
@@ -1,7 +1,12 @@
package com.zy.ai.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.zy.ai.entity.ChatCompletionRequest;
import com.zy.ai.entity.ChatCompletionResponse;
import com.zy.ai.entity.LlmCallLog;
import com.zy.ai.entity.LlmRouteConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@@ -9,37 +14,38 @@
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.Consumer;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.util.function.Consumer;
@Slf4j
@Service
@RequiredArgsConstructor
public class LlmChatService {
    private final WebClient llmWebClient;
    private static final int LOG_TEXT_LIMIT = 16000;
    @Value("${llm.api-key}")
    private String apiKey;
    private final LlmRoutingService llmRoutingService;
    private final LlmCallLogService llmCallLogService;
    @Value("${llm.model}")
    private String model;
    @Value("${llm.base-url:}")
    private String fallbackBaseUrl;
    @Value("${llm.pythonPlatformUrl}")
    private String pythonPlatformUrl;
    @Value("${llm.api-key:}")
    private String fallbackApiKey;
    @Value("${llm.thinking}")
    private String thinking;
    @Value("${llm.model:}")
    private String fallbackModel;
    @Value("${llm.thinking:false}")
    private String fallbackThinking;
    /**
     * 通用对话方法:传入 messages,返回大模型文本回复
@@ -49,27 +55,12 @@
                       Integer maxTokens) {
        ChatCompletionRequest req = new ChatCompletionRequest();
        req.setModel(model);
        req.setMessages(messages);
        req.setTemperature(temperature != null ? temperature : 0.3);
        req.setMax_tokens(maxTokens != null ? maxTokens : 1024);
        req.setStream(false);
        ChatCompletionResponse response = llmWebClient.post()
                .uri("/chat/completions")
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)
                .bodyValue(req)
                .exchangeToMono(resp -> resp.bodyToFlux(String.class)
                        .collectList()
                        .map(list -> {
                            String payload = String.join("\n\n", list);
                            return parseCompletion(payload);
                        }))
                .doOnError(ex -> log.error("调用 LLM 失败", ex))
                .onErrorResume(ex -> Mono.empty())
                .block();
        ChatCompletionResponse response = complete(req, "chat");
        if (response == null ||
                response.getChoices() == null ||
@@ -88,45 +79,81 @@
                                                 Integer maxTokens,
                                                 List<Object> tools) {
        ChatCompletionRequest req = new ChatCompletionRequest();
        req.setModel(model);
        req.setMessages(messages);
        req.setTemperature(temperature != null ? temperature : 0.3);
        req.setMax_tokens(maxTokens != null ? maxTokens : 1024);
        req.setStream(false);
        if(thinking.equals("enable")) {
            ChatCompletionRequest.Thinking thinking = new ChatCompletionRequest.Thinking();
            thinking.setType("enable");
            req.setThinking(thinking);
        }
        if (tools != null && !tools.isEmpty()) {
            req.setTools(tools);
            req.setTool_choice("auto");
        }
        return complete(req);
        return complete(req, tools != null && !tools.isEmpty() ? "chat_completion_tools" : "chat_completion");
    }
    public ChatCompletionResponse complete(ChatCompletionRequest req) {
        try {
            return llmWebClient.post()
                    .uri("/chat/completions")
                    .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)
                    .bodyValue(req)
                    .exchangeToMono(resp -> resp.bodyToFlux(String.class)
                            .collectList()
                            .map(list -> {
                                String payload = String.join("\n\n", list);
                                return parseCompletion(payload);
                            }))
                    .doOnError(ex -> log.error("调用 LLM 失败", ex))
                    .onErrorResume(ex -> Mono.empty())
                    .block();
        } catch (Exception e) {
            log.error("调用 LLM 失败", e);
        return complete(req, "completion");
    }
    private ChatCompletionResponse complete(ChatCompletionRequest req, String scene) {
        String traceId = nextTraceId();
        List<ResolvedRoute> routes = resolveRoutes();
        if (routes.isEmpty()) {
            log.error("调用 LLM 失败: 未配置可用 LLM 路由");
            recordCall(traceId, scene, false, 1, null, false, null, 0L, req, null, "none",
                    new RuntimeException("未配置可用 LLM 路由"), "no_route");
            return null;
        }
        Throwable last = null;
        for (int i = 0; i < routes.size(); i++) {
            ResolvedRoute route = routes.get(i);
            boolean hasNext = i < routes.size() - 1;
            ChatCompletionRequest routeReq = applyRoute(cloneRequest(req), route, false);
            long start = System.currentTimeMillis();
            try {
                CompletionCallResult callResult = callCompletion(route, routeReq);
                ChatCompletionResponse resp = callResult.response;
                if (!isValidCompletion(resp)) {
                    RuntimeException ex = new RuntimeException("LLM 响应为空");
                    boolean canSwitch = shouldSwitch(route, false);
                    markFailure(route, ex, canSwitch);
                    recordCall(traceId, scene, false, i + 1, route, false, callResult.statusCode,
                            System.currentTimeMillis() - start, routeReq, callResult.payload, "error", ex,
                            "invalid_completion");
                    if (hasNext && canSwitch) {
                        log.warn("LLM 切换到下一路由, current={}, reason={}", route.tag(), ex.getMessage());
                        continue;
                    }
                    log.error("调用 LLM 失败, route={}", route.tag(), ex);
                    last = ex;
                    break;
                }
                markSuccess(route);
                recordCall(traceId, scene, false, i + 1, route, true, callResult.statusCode,
                        System.currentTimeMillis() - start, routeReq, buildResponseText(resp, callResult.payload),
                        "none", null, null);
                return resp;
            } catch (Throwable ex) {
                last = ex;
                boolean quota = isQuotaExhausted(ex);
                boolean canSwitch = shouldSwitch(route, quota);
                markFailure(route, ex, canSwitch);
                recordCall(traceId, scene, false, i + 1, route, false, statusCodeOf(ex),
                        System.currentTimeMillis() - start, routeReq, responseBodyOf(ex),
                        quota ? "quota" : "error", ex, null);
                if (hasNext && canSwitch) {
                    log.warn("LLM 切换到下一路由, current={}, reason={}", route.tag(), errorText(ex));
                    continue;
                }
                log.error("调用 LLM 失败, route={}", route.tag(), ex);
                break;
            }
        }
        if (last != null) {
            log.error("调用 LLM 全部路由失败: {}", errorText(last));
        }
        return null;
    }
    public void chatStream(List<ChatCompletionRequest.Message> messages,
@@ -137,92 +164,12 @@
                           Consumer<Throwable> onError) {
        ChatCompletionRequest req = new ChatCompletionRequest();
        req.setModel(model);
        req.setMessages(messages);
        req.setTemperature(temperature != null ? temperature : 0.3);
        req.setMax_tokens(maxTokens != null ? maxTokens : 1024);
        req.setStream(true);
        Flux<String> flux = llmWebClient.post()
                .uri("/chat/completions")
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.TEXT_EVENT_STREAM)
                .bodyValue(req)
                .retrieve()
                .bodyToFlux(String.class)
                .doOnError(ex -> log.error("调用 LLM 流式失败", ex));
        AtomicBoolean doneSeen = new AtomicBoolean(false);
        AtomicBoolean errorSeen = new AtomicBoolean(false);
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
        Thread drain = new Thread(() -> {
            try {
                while (true) {
                    String s = queue.poll(2, TimeUnit.SECONDS);
                    if (s != null) {
                        try { onChunk.accept(s); } catch (Exception ignore) {}
                    }
                    if (doneSeen.get() && queue.isEmpty()) {
                        if (!errorSeen.get()) {
                            try { if (onComplete != null) onComplete.run(); } catch (Exception ignore) {}
                        }
                        break;
                    }
                }
            } catch (InterruptedException ignore) {
                ignore.printStackTrace();
            }
        });
        drain.setDaemon(true);
        drain.start();
        flux.subscribe(payload -> {
            if (payload == null || payload.isEmpty()) return;
            String[] events = payload.split("\\r?\\n\\r?\\n");
            for (String part : events) {
                String s = part;
                if (s == null || s.isEmpty()) continue;
                if (s.startsWith("data:")) {
                    s = s.substring(5);
                    if (s.startsWith(" ")) s = s.substring(1);
                }
                if ("[DONE]".equals(s.trim())) {
                    doneSeen.set(true);
                    continue;
                }
                try {
                    JSONObject obj = JSON.parseObject(s);
                    JSONArray choices = obj.getJSONArray("choices");
                    if (choices != null && !choices.isEmpty()) {
                        JSONObject c0 = choices.getJSONObject(0);
                        JSONObject delta = c0.getJSONObject("delta");
                        if (delta != null) {
                            String content = delta.getString("content");
                            if (content != null) {
                                try { queue.offer(content); } catch (Exception ignore) {}
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, err -> {
            errorSeen.set(true);
            doneSeen.set(true);
            if (onError != null) onError.accept(err);
        }, () -> {
            if (!doneSeen.get()) {
                errorSeen.set(true);
                doneSeen.set(true);
                if (onError != null) onError.accept(new RuntimeException("LLM 流意外完成"));
            } else {
                doneSeen.set(true);
            }
        });
        streamWithFailover(req, onChunk, onComplete, onError, "chat_stream");
    }
    public void chatStreamWithTools(List<ChatCompletionRequest.Message> messages,
@@ -233,120 +180,54 @@
                                    Runnable onComplete,
                                    Consumer<Throwable> onError) {
        ChatCompletionRequest req = new ChatCompletionRequest();
        req.setModel(model);
        req.setMessages(messages);
        req.setTemperature(temperature != null ? temperature : 0.3);
        req.setMax_tokens(maxTokens != null ? maxTokens : 1024);
        req.setStream(true);
        if(thinking.equals("enable")) {
            ChatCompletionRequest.Thinking thinking = new ChatCompletionRequest.Thinking();
            thinking.setType("enable");
            req.setThinking(thinking);
        }
        if (tools != null && !tools.isEmpty()) {
            req.setTools(tools);
            req.setTool_choice("auto");
        }
        Flux<String> flux = llmWebClient.post()
                .uri("/chat/completions")
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.TEXT_EVENT_STREAM)
                .bodyValue(req)
                .retrieve()
                .bodyToFlux(String.class)
                .doOnError(ex -> log.error("调用 LLM 流式失败", ex));
        AtomicBoolean doneSeen = new AtomicBoolean(false);
        AtomicBoolean errorSeen = new AtomicBoolean(false);
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
        Thread drain = new Thread(() -> {
            try {
                while (true) {
                    String s = queue.poll(5, TimeUnit.SECONDS);
                    if (s != null) {
                        try { onChunk.accept(s); } catch (Exception ignore) {}
                    }
                    if (doneSeen.get() && queue.isEmpty()) {
                        if (!errorSeen.get()) {
                            try { if (onComplete != null) onComplete.run(); } catch (Exception ignore) {}
                        }
                        break;
                    }
                }
            } catch (InterruptedException ignore) {
                ignore.printStackTrace();
            }
        });
        drain.setDaemon(true);
        drain.start();
        flux.subscribe(payload -> {
            if (payload == null || payload.isEmpty()) return;
            String[] events = payload.split("\\r?\\n\\r?\\n");
            for (String part : events) {
                String s = part;
                if (s == null || s.isEmpty()) continue;
                if (s.startsWith("data:")) {
                    s = s.substring(5);
                    if (s.startsWith(" ")) s = s.substring(1);
                }
                if ("[DONE]".equals(s.trim())) {
                    doneSeen.set(true);
                    continue;
                }
                try {
                    JSONObject obj = JSON.parseObject(s);
                    JSONArray choices = obj.getJSONArray("choices");
                    if (choices != null && !choices.isEmpty()) {
                        JSONObject c0 = choices.getJSONObject(0);
                        JSONObject delta = c0.getJSONObject("delta");
                        if (delta != null) {
                            String content = delta.getString("content");
                            if (content != null) {
                                try { queue.offer(content); } catch (Exception ignore) {}
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, err -> {
            errorSeen.set(true);
            doneSeen.set(true);
            if (onError != null) onError.accept(err);
        }, () -> {
            if (!doneSeen.get()) {
                errorSeen.set(true);
                doneSeen.set(true);
                if (onError != null) onError.accept(new RuntimeException("LLM 流意外完成"));
            } else {
                doneSeen.set(true);
            }
        });
        streamWithFailover(req, onChunk, onComplete, onError, tools != null && !tools.isEmpty() ? "chat_stream_tools" : "chat_stream");
    }
    public void chatStreamRunPython(String prompt, String chatId, Consumer<String> onChunk,
    private void streamWithFailover(ChatCompletionRequest req,
                                    Consumer<String> onChunk,
                                    Runnable onComplete,
                                    Consumer<Throwable> onError) {
        HashMap<String, Object> req = new HashMap<>();
        req.put("prompt", prompt);
        req.put("chatId", chatId);
                                    Consumer<Throwable> onError,
                                    String scene) {
        String traceId = nextTraceId();
        List<ResolvedRoute> routes = resolveRoutes();
        if (routes.isEmpty()) {
            recordCall(traceId, scene, true, 1, null, false, null, 0L, req, null, "none",
                    new RuntimeException("未配置可用 LLM 路由"), "no_route");
            if (onError != null) onError.accept(new RuntimeException("未配置可用 LLM 路由"));
            return;
        }
        attemptStream(routes, 0, req, onChunk, onComplete, onError, traceId, scene);
    }
        Flux<String> flux = llmWebClient.post()
                .uri(pythonPlatformUrl)
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.TEXT_EVENT_STREAM)
                .bodyValue(req)
                .retrieve()
                .bodyToFlux(String.class)
                .doOnError(ex -> log.error("调用 LLM 流式失败", ex));
    private void attemptStream(List<ResolvedRoute> routes,
                               int index,
                               ChatCompletionRequest req,
                               Consumer<String> onChunk,
                               Runnable onComplete,
                               Consumer<Throwable> onError,
                               String traceId,
                               String scene) {
        if (index >= routes.size()) {
            if (onError != null) onError.accept(new RuntimeException("LLM 路由全部失败"));
            return;
        }
        ResolvedRoute route = routes.get(index);
        ChatCompletionRequest routeReq = applyRoute(cloneRequest(req), route, true);
        long start = System.currentTimeMillis();
        StringBuilder outputBuffer = new StringBuilder();
        AtomicBoolean doneSeen = new AtomicBoolean(false);
        AtomicBoolean errorSeen = new AtomicBoolean(false);
        AtomicBoolean emitted = new AtomicBoolean(false);
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
        Thread drain = new Thread(() -> {
@@ -354,6 +235,7 @@
                while (true) {
                    String s = queue.poll(2, TimeUnit.SECONDS);
                    if (s != null) {
                        emitted.set(true);
                        try {
                            onChunk.accept(s);
                        } catch (Exception ignore) {
@@ -370,13 +252,12 @@
                    }
                }
            } catch (InterruptedException ignore) {
                ignore.printStackTrace();
            }
        });
        drain.setDaemon(true);
        drain.start();
        flux.subscribe(payload -> {
        streamFlux(route, routeReq).subscribe(payload -> {
            if (payload == null || payload.isEmpty()) return;
            String[] events = payload.split("\\r?\\n\\r?\\n");
            for (String part : events) {
@@ -390,10 +271,6 @@
                    doneSeen.set(true);
                    continue;
                }
                if("<think>".equals(s.trim()) || "</think>".equals(s.trim())) {
                    queue.offer(s.trim());
                    continue;
                }
                try {
                    JSONObject obj = JSON.parseObject(s);
                    JSONArray choices = obj.getJSONArray("choices");
@@ -403,30 +280,200 @@
                        if (delta != null) {
                            String content = delta.getString("content");
                            if (content != null) {
                                try {
                                    queue.offer(content);
                                } catch (Exception ignore) {
                                }
                                queue.offer(content);
                                appendLimited(outputBuffer, content);
                            }
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    log.warn("解析 LLM stream 片段失败: {}", e.getMessage());
                }
            }
        }, err -> {
            errorSeen.set(true);
            doneSeen.set(true);
            boolean quota = isQuotaExhausted(err);
            boolean canSwitch = shouldSwitch(route, quota);
            markFailure(route, err, canSwitch);
            recordCall(traceId, scene, true, index + 1, route, false, statusCodeOf(err),
                    System.currentTimeMillis() - start, routeReq, outputBuffer.toString(),
                    quota ? "quota" : "error", err, "emitted=" + emitted.get());
            if (!emitted.get() && canSwitch && index < routes.size() - 1) {
                log.warn("LLM 路由失败,自动切换,current={}, reason={}", route.tag(), errorText(err));
                attemptStream(routes, index + 1, req, onChunk, onComplete, onError, traceId, scene);
                return;
            }
            if (onError != null) onError.accept(err);
        }, () -> {
            if (!doneSeen.get()) {
                RuntimeException ex = new RuntimeException("LLM 流意外完成");
                errorSeen.set(true);
                doneSeen.set(true);
                if (onError != null) onError.accept(new RuntimeException("LLM 流意外完成"));
                boolean canSwitch = shouldSwitch(route, false);
                markFailure(route, ex, canSwitch);
                recordCall(traceId, scene, true, index + 1, route, false, 200,
                        System.currentTimeMillis() - start, routeReq, outputBuffer.toString(),
                        "error", ex, "unexpected_stream_end");
                if (!emitted.get() && canSwitch && index < routes.size() - 1) {
                    log.warn("LLM 路由流异常完成,自动切换,current={}", route.tag());
                    attemptStream(routes, index + 1, req, onChunk, onComplete, onError, traceId, scene);
                } else {
                    if (onError != null) onError.accept(ex);
                }
            } else {
                markSuccess(route);
                recordCall(traceId, scene, true, index + 1, route, true, 200,
                        System.currentTimeMillis() - start, routeReq, outputBuffer.toString(),
                        "none", null, null);
                doneSeen.set(true);
            }
        });
    }
    private Flux<String> streamFlux(ResolvedRoute route, ChatCompletionRequest req) {
        WebClient client = WebClient.builder().baseUrl(route.baseUrl).build();
        return client.post()
                .uri("/chat/completions")
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + route.apiKey)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.TEXT_EVENT_STREAM)
                .bodyValue(req)
                .exchangeToFlux(resp -> {
                    int status = resp.rawStatusCode();
                    if (status >= 200 && status < 300) {
                        return resp.bodyToFlux(String.class);
                    }
                    return resp.bodyToMono(String.class)
                            .defaultIfEmpty("")
                            .flatMapMany(body -> Flux.error(new LlmRouteException(status, body)));
                })
                .doOnError(ex -> log.error("调用 LLM 流式失败, route={}", route.tag(), ex));
    }
    private CompletionCallResult callCompletion(ResolvedRoute route, ChatCompletionRequest req) {
        WebClient client = WebClient.builder().baseUrl(route.baseUrl).build();
        RawCompletionResult raw = client.post()
                .uri("/chat/completions")
                .header(HttpHeaders.AUTHORIZATION, "Bearer " + route.apiKey)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)
                .bodyValue(req)
                .exchangeToMono(resp -> resp.bodyToFlux(String.class)
                        .collectList()
                        .map(list -> new RawCompletionResult(resp.rawStatusCode(), String.join("\\n\\n", list))))
                .block();
        if (raw == null) {
            throw new RuntimeException("LLM 返回为空");
        }
        if (raw.statusCode < 200 || raw.statusCode >= 300) {
            throw new LlmRouteException(raw.statusCode, raw.payload);
        }
        return new CompletionCallResult(raw.statusCode, raw.payload, parseCompletion(raw.payload));
    }
    private ChatCompletionRequest applyRoute(ChatCompletionRequest req, ResolvedRoute route, boolean stream) {
        req.setModel(route.model);
        req.setStream(stream);
        if (route.thinkingEnabled) {
            ChatCompletionRequest.Thinking t = new ChatCompletionRequest.Thinking();
            t.setType("enable");
            req.setThinking(t);
        } else {
            req.setThinking(null);
        }
        return req;
    }
    private ChatCompletionRequest cloneRequest(ChatCompletionRequest src) {
        ChatCompletionRequest req = new ChatCompletionRequest();
        req.setModel(src.getModel());
        req.setMessages(src.getMessages());
        req.setTemperature(src.getTemperature());
        req.setMax_tokens(src.getMax_tokens());
        req.setStream(src.getStream());
        req.setTools(src.getTools());
        req.setTool_choice(src.getTool_choice());
        req.setThinking(src.getThinking());
        return req;
    }
    private boolean isValidCompletion(ChatCompletionResponse response) {
        if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) {
            return false;
        }
        ChatCompletionRequest.Message message = response.getChoices().get(0).getMessage();
        if (message == null) {
            return false;
        }
        if (!isBlank(message.getContent())) {
            return true;
        }
        return message.getTool_calls() != null && !message.getTool_calls().isEmpty();
    }
    private boolean shouldSwitch(ResolvedRoute route, boolean quota) {
        return quota ? route.switchOnQuota : route.switchOnError;
    }
    private void markSuccess(ResolvedRoute route) {
        if (route.id != null) {
            llmRoutingService.markSuccess(route.id);
        }
    }
    private void markFailure(ResolvedRoute route, Throwable ex, boolean enterCooldown) {
        if (route.id != null) {
            llmRoutingService.markFailure(route.id, errorText(ex), enterCooldown, route.cooldownSeconds);
        }
    }
    private String errorText(Throwable ex) {
        if (ex == null) return "unknown";
        if (ex instanceof LlmRouteException) {
            LlmRouteException e = (LlmRouteException) ex;
            String body = e.body == null ? "" : e.body;
            if (body.length() > 240) {
                body = body.substring(0, 240);
            }
            return "status=" + e.statusCode + ", body=" + body;
        }
        return ex.getMessage() == null ? ex.toString() : ex.getMessage();
    }
    private boolean isQuotaExhausted(Throwable ex) {
        if (!(ex instanceof LlmRouteException)) return false;
        LlmRouteException e = (LlmRouteException) ex;
        if (e.statusCode == 429) return true;
        String text = (e.body == null ? "" : e.body).toLowerCase();
        return text.contains("insufficient_quota")
                || text.contains("quota")
                || text.contains("余额")
                || text.contains("用量")
                || text.contains("超限")
                || text.contains("rate limit");
    }
    private List<ResolvedRoute> resolveRoutes() {
        List<ResolvedRoute> routes = new ArrayList<>();
        List<LlmRouteConfig> dbRoutes = llmRoutingService.listAvailableRoutes();
        for (LlmRouteConfig c : dbRoutes) {
            routes.add(ResolvedRoute.fromDb(c));
        }
        // 兼容:数据库为空时,回退到 yml
        if (routes.isEmpty() && !isBlank(fallbackBaseUrl) && !isBlank(fallbackApiKey) && !isBlank(fallbackModel)) {
            routes.add(ResolvedRoute.fromFallback(fallbackBaseUrl, fallbackApiKey, fallbackModel, isFallbackThinkingEnabled()));
        }
        return routes;
    }
    private boolean isFallbackThinkingEnabled() {
        String x = fallbackThinking == null ? "" : fallbackThinking.trim().toLowerCase();
        return "true".equals(x) || "1".equals(x) || "enable".equals(x);
    }
    private boolean isBlank(String s) {
        return s == null || s.trim().isEmpty();
    }
    private ChatCompletionResponse mergeSseChunk(ChatCompletionResponse acc, String payload) {
@@ -452,7 +499,7 @@
                        ChatCompletionResponse.Choice choice = new ChatCompletionResponse.Choice();
                        ChatCompletionRequest.Message msg = new ChatCompletionRequest.Message();
                        choice.setMessage(msg);
                        java.util.ArrayList<ChatCompletionResponse.Choice> list = new java.util.ArrayList<>();
                        ArrayList<ChatCompletionResponse.Choice> list = new ArrayList<>();
                        list.add(choice);
                        acc.setChoices(list);
                    }
@@ -490,7 +537,8 @@
                if (created != null) acc.setCreated(created);
                String object = obj.getString("object");
                if (object != null && !object.isEmpty()) acc.setObjectName(object);
            } catch (Exception ignore) {}
            } catch (Exception ignore) {
            }
        }
        return acc;
    }
@@ -502,7 +550,8 @@
            if (r != null && r.getChoices() != null && !r.getChoices().isEmpty() && r.getChoices().get(0).getMessage() != null) {
                return r;
            }
        } catch (Exception ignore) {}
        } catch (Exception ignore) {
        }
        ChatCompletionResponse sse = mergeSseChunk(new ChatCompletionResponse(), payload);
        if (sse.getChoices() != null && !sse.getChoices().isEmpty() && sse.getChoices().get(0).getMessage() != null && sse.getChoices().get(0).getMessage().getContent() != null) {
            return sse;
@@ -513,9 +562,181 @@
        msg.setRole("assistant");
        msg.setContent(payload);
        choice.setMessage(msg);
        java.util.ArrayList<ChatCompletionResponse.Choice> list = new java.util.ArrayList<>();
        ArrayList<ChatCompletionResponse.Choice> list = new ArrayList<>();
        list.add(choice);
        r.setChoices(list);
        return r;
    }
    private String nextTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }
    private void appendLimited(StringBuilder sb, String text) {
        if (sb == null || text == null || text.isEmpty()) {
            return;
        }
        int remain = LOG_TEXT_LIMIT - sb.length();
        if (remain <= 0) {
            return;
        }
        if (text.length() <= remain) {
            sb.append(text);
        } else {
            sb.append(text, 0, remain);
        }
    }
    private Integer statusCodeOf(Throwable ex) {
        if (ex instanceof LlmRouteException) {
            return ((LlmRouteException) ex).statusCode;
        }
        return null;
    }
    private String responseBodyOf(Throwable ex) {
        if (ex instanceof LlmRouteException) {
            return cut(((LlmRouteException) ex).body, LOG_TEXT_LIMIT);
        }
        return null;
    }
    private String buildResponseText(ChatCompletionResponse resp, String fallbackPayload) {
        if (resp != null && resp.getChoices() != null && !resp.getChoices().isEmpty()
                && resp.getChoices().get(0) != null && resp.getChoices().get(0).getMessage() != null) {
            ChatCompletionRequest.Message m = resp.getChoices().get(0).getMessage();
            if (!isBlank(m.getContent())) {
                return cut(m.getContent(), LOG_TEXT_LIMIT);
            }
            if (m.getTool_calls() != null && !m.getTool_calls().isEmpty()) {
                return cut(JSON.toJSONString(m), LOG_TEXT_LIMIT);
            }
        }
        return cut(fallbackPayload, LOG_TEXT_LIMIT);
    }
    private String safeName(Throwable ex) {
        return ex == null ? null : ex.getClass().getSimpleName();
    }
    private String cut(String text, int maxLen) {
        if (text == null) return null;
        String clean = text.replace("\r", " ");
        return clean.length() > maxLen ? clean.substring(0, maxLen) : clean;
    }
    private void recordCall(String traceId,
                            String scene,
                            boolean stream,
                            int attemptNo,
                            ResolvedRoute route,
                            boolean success,
                            Integer httpStatus,
                            long latencyMs,
                            ChatCompletionRequest req,
                            String response,
                            String switchMode,
                            Throwable err,
                            String extra) {
        LlmCallLog item = new LlmCallLog();
        item.setTraceId(cut(traceId, 64));
        item.setScene(cut(scene, 64));
        item.setStream((short) (stream ? 1 : 0));
        item.setAttemptNo(attemptNo);
        if (route != null) {
            item.setRouteId(route.id);
            item.setRouteName(cut(route.name, 128));
            item.setBaseUrl(cut(route.baseUrl, 255));
            item.setModel(cut(route.model, 128));
        }
        item.setSuccess((short) (success ? 1 : 0));
        item.setHttpStatus(httpStatus);
        item.setLatencyMs(latencyMs < 0 ? 0 : latencyMs);
        item.setSwitchMode(cut(switchMode, 32));
        item.setRequestContent(cut(JSON.toJSONString(req), LOG_TEXT_LIMIT));
        item.setResponseContent(cut(response, LOG_TEXT_LIMIT));
        item.setErrorType(cut(safeName(err), 128));
        item.setErrorMessage(err == null ? null : cut(errorText(err), 1024));
        item.setExtra(cut(extra, 512));
        item.setCreateTime(new Date());
        llmCallLogService.saveIgnoreError(item);
    }
    private static class CompletionCallResult {
        private final int statusCode;
        private final String payload;
        private final ChatCompletionResponse response;
        private CompletionCallResult(int statusCode, String payload, ChatCompletionResponse response) {
            this.statusCode = statusCode;
            this.payload = payload;
            this.response = response;
        }
    }
    private static class RawCompletionResult {
        private final int statusCode;
        private final String payload;
        private RawCompletionResult(int statusCode, String payload) {
            this.statusCode = statusCode;
            this.payload = payload;
        }
    }
    private static class LlmRouteException extends RuntimeException {
        private final int statusCode;
        private final String body;
        private LlmRouteException(int statusCode, String body) {
            super("http status=" + statusCode);
            this.statusCode = statusCode;
            this.body = body;
        }
    }
    private static class ResolvedRoute {
        private Long id;
        private String name;
        private String baseUrl;
        private String apiKey;
        private String model;
        private boolean thinkingEnabled;
        private boolean switchOnQuota;
        private boolean switchOnError;
        private Integer cooldownSeconds;
        private static ResolvedRoute fromDb(LlmRouteConfig c) {
            ResolvedRoute r = new ResolvedRoute();
            r.id = c.getId();
            r.name = c.getName();
            r.baseUrl = c.getBaseUrl();
            r.apiKey = c.getApiKey();
            r.model = c.getModel();
            r.thinkingEnabled = c.getThinking() != null && c.getThinking() == 1;
            r.switchOnQuota = c.getSwitchOnQuota() == null || c.getSwitchOnQuota() == 1;
            r.switchOnError = c.getSwitchOnError() == null || c.getSwitchOnError() == 1;
            r.cooldownSeconds = c.getCooldownSeconds();
            return r;
        }
        private static ResolvedRoute fromFallback(String baseUrl, String apiKey, String model, boolean thinkingEnabled) {
            ResolvedRoute r = new ResolvedRoute();
            r.name = "fallback-yml";
            r.baseUrl = baseUrl;
            r.apiKey = apiKey;
            r.model = model;
            r.thinkingEnabled = thinkingEnabled;
            r.switchOnQuota = true;
            r.switchOnError = true;
            r.cooldownSeconds = 300;
            return r;
        }
        private String tag() {
            String showName = name == null ? "unnamed" : name;
            String showModel = model == null ? "" : (" model=" + model);
            return showName + showModel;
        }
    }
}
src/main/java/com/zy/ai/service/PythonService.java
@@ -12,36 +12,36 @@
    @Autowired
    private LlmChatService llmChatService;
    public boolean runPython(String prompt, String chatId, SseEmitter emitter) {
        try {
            llmChatService.chatStreamRunPython(prompt, chatId, s -> {
                try {
                    String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n");
                    if (!safe.isEmpty()) {
                        sse(emitter, safe);
                    }
                } catch (Exception ignore) {
                }
            }, () -> {
                try {
                    sse(emitter, "\\n\\n【AI】运行已停止(正常结束)\\n\\n");
                    log.info("AI MCP diagnose stopped: final end");
                    emitter.complete();
                } catch (Exception ignore) {
                }
            }, e -> {
                sse(emitter, "\\n\\n【AI】分析出错,正在回退...\\n\\n");
            });
            return true;
        } catch (Exception e) {
            try {
                sse(emitter, "\\n\\n【AI】运行已停止(异常)\\n\\n");
                log.error("AI MCP diagnose stopped: error", e);
                emitter.completeWithError(e);
            } catch (Exception ignore) {}
            return true;
        }
    }
//    public boolean runPython(String prompt, String chatId, SseEmitter emitter) {
//        try {
//            llmChatService.chatStreamRunPython(prompt, chatId, s -> {
//                try {
//                    String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n");
//                    if (!safe.isEmpty()) {
//                        sse(emitter, safe);
//                    }
//                } catch (Exception ignore) {
//                }
//            }, () -> {
//                try {
//                    sse(emitter, "\\n\\n【AI】运行已停止(正常结束)\\n\\n");
//                    log.info("AI MCP diagnose stopped: final end");
//                    emitter.complete();
//                } catch (Exception ignore) {
//                }
//            }, e -> {
//                sse(emitter, "\\n\\n【AI】分析出错,正在回退...\\n\\n");
//            });
//            return true;
//        } catch (Exception e) {
//            try {
//                sse(emitter, "\\n\\n【AI】运行已停止(异常)\\n\\n");
//                log.error("AI MCP diagnose stopped: error", e);
//                emitter.completeWithError(e);
//            } catch (Exception ignore) {}
//            return true;
//        }
//    }
    private void sse(SseEmitter emitter, String data) {
        if (data == null) return;
src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -11,7 +11,6 @@
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -28,8 +27,6 @@
    private static final long CHAT_TTL_SECONDS = 7L * 24 * 3600;
    @Value("${llm.platform}")
    private String platform;
    @Autowired
    private LlmChatService llmChatService;
    @Autowired
@@ -40,8 +37,6 @@
    private AiUtils aiUtils;
    @Autowired(required = false)
    private McpController mcpController;
    @Autowired
    private PythonService pythonService;
    public void diagnoseStream(WcsDiagnosisRequest request, SseEmitter emitter) {
        List<ChatCompletionRequest.Message> messages = new ArrayList<>();
@@ -85,7 +80,7 @@
            try {
                try { emitter.send(SseEmitter.event().data("【AI】运行已停止(异常)")); } catch (Exception ignore) {}
                log.error("AI diagnose stream stopped: error", e);
                emitter.completeWithError(e);
                emitter.complete();
            } catch (Exception ignore) {}
        });
    }
@@ -95,11 +90,6 @@
                          String chatId,
                          boolean reset,
                          SseEmitter emitter) {
        if (platform.equals("python")) {
            pythonService.runPython(prompt, chatId, emitter);
            return;
        }
        List<ChatCompletionRequest.Message> messages = new ArrayList<>();
        List<ChatCompletionRequest.Message> history = null;
@@ -187,7 +177,10 @@
                emitter.complete();
            } catch (Exception ignore) {}
        }, e -> {
            try { emitter.completeWithError(e); } catch (Exception ignore) {}
            try {
                try { emitter.send(SseEmitter.event().data("【AI】运行已停止(异常)")); } catch (Exception ignore) {}
                emitter.complete();
            } catch (Exception ignore) {}
        });
    }
@@ -380,7 +373,7 @@
            try {
                sse(emitter, "\\n\\n【AI】运行已停止(异常)\\n\\n");
                log.error("AI MCP diagnose stopped: error", e);
                emitter.completeWithError(e);
                emitter.complete();
            } catch (Exception ignore) {}
            return true;
        }
src/main/java/com/zy/asrs/controller/BasMapController.java
@@ -177,6 +177,8 @@
            HashMap<Integer, List<StationObjModel>> inStationMap = new HashMap<>();
            HashMap<Integer, List<StationObjModel>> outStationMap = new HashMap<>();
            HashMap<Integer, List<StationObjModel>> runBlockReassignStationMap = new HashMap<>();
            HashMap<Integer, List<StationObjModel>> isOutOrderStationMap = new HashMap<>();
            HashMap<Integer, List<StationObjModel>> isLiftTransferStationMap = new HashMap<>();
            for (Map.Entry<Integer, List<List<HashMap<String, Object>>>> entry : dataMap.entrySet()) {
                Integer lev = entry.getKey();
@@ -192,17 +194,18 @@
                        HashMap<String, Object> nodeData = new HashMap<>();
                        nodeData.put("value", map.get("value"));
                        String nodeType = map.get("bgColor").toString();
                        if (nodeType.equals("RGB(0,176,80)")) {
                        String bgColor = map.get("bgColor").toString();
                        String nodeType = map.get("nodeType").toString();
                        if (nodeType.equals("shelf")) {
                            //货架
                            nodeData.put("type", "shelf");
                        } else if (nodeType.equals("RGB(255,192,0)")) {
                        } else if (nodeType.equals("crn")) {
                            //堆垛机
                            nodeData.put("type", "crn");
                        } else if (nodeType.equals("RGB(255,255,0)")) {
                        } else if (nodeType.equals("dualCrn")) {
                            //双工位堆垛机
                            nodeData.put("type", "dualCrn");
                        } else if (nodeType.equals("RGB(0,112,192)")) {
                        } else if (nodeType.equals("devp")) {
                            //输送线
                            nodeData.put("type", "devp");
@@ -266,7 +269,21 @@
                                runBlockReassignStationList.add(stationObjModel);
                                runBlockReassignStationMap.put(deviceNo, runBlockReassignStationList);
                            }
                        } else if (nodeType.equals("RGB(0,176,240)")) {
                            Integer isOutOrder = value.getInteger("isOutOrder");
                            if (isOutOrder != null && isOutOrder == 1) {
                                List<StationObjModel> isOutOrderStationList = isOutOrderStationMap.getOrDefault(deviceNo, new ArrayList<>());
                                isOutOrderStationList.add(stationObjModel);
                                isOutOrderStationMap.put(deviceNo, isOutOrderStationList);
                            }
                            Integer isLiftTransfer = value.getInteger("isLiftTransfer");
                            if (isLiftTransfer != null && isLiftTransfer == 1) {
                                List<StationObjModel> isLiftTransferStationList = isLiftTransferStationMap.getOrDefault(deviceNo, new ArrayList<>());
                                isLiftTransferStationList.add(stationObjModel);
                                isLiftTransferStationMap.put(deviceNo, isLiftTransferStationList);
                            }
                        } else if (nodeType.equals("rgv")) {
                            //RGV
                            nodeData.put("type", "rgv");
                        } else if (nodeType.equals("none")) {
@@ -275,6 +292,7 @@
                        } else if (nodeType.equals("merge")) {
                            //合并区域
                            nodeData.put("type", "merge");
                            nodeData.put("mergeType", map.get("mergeType"));
                        }
                        nodeData.put("cellWidth", map.get("cellWidth"));
@@ -314,6 +332,8 @@
                List<StationObjModel> inStationList = inStationMap.get(deviceNo);
                List<StationObjModel> outStationList = outStationMap.get(deviceNo);
                List<StationObjModel> runBlockReassignStationList = runBlockReassignStationMap.get(deviceNo);
                List<StationObjModel> isOutOrderStationList = isOutOrderStationMap.get(deviceNo);
                List<StationObjModel> isLiftTransferStationList = isLiftTransferStationMap.get(deviceNo);
                if (barcodeStationList != null) {
                    basDevp.setBarcodeStationList(JSON.toJSONString(barcodeStationList, SerializerFeature.DisableCircularReferenceDetect));
@@ -331,6 +351,14 @@
                    basDevp.setRunBlockReassignLocStationList(JSON.toJSONString(runBlockReassignStationList, SerializerFeature.DisableCircularReferenceDetect));
                }
                if (isOutOrderStationList != null) {
                    basDevp.setIsOutOrderList(JSON.toJSONString(isOutOrderStationList, SerializerFeature.DisableCircularReferenceDetect));
                }
                if (isLiftTransferStationList != null) {
                    basDevp.setIsLiftTransferList(JSON.toJSONString(isLiftTransferStationList, SerializerFeature.DisableCircularReferenceDetect));
                }
                basDevp.setStationList(JSON.toJSONString(stationList, SerializerFeature.DisableCircularReferenceDetect));
                basDevp.setUpdateTime(new Date());
                basDevpService.insertOrUpdate(basDevp);
src/main/java/com/zy/asrs/controller/ConsoleController.java
@@ -57,6 +57,10 @@
    private RedisUtil redisUtil;
    @Autowired
    private LocMastService locMastService;
    @Autowired
    private BasMapService basMapService;
    @Autowired
    private StationCycleCapacityService stationCycleCapacityService;
    @PostMapping("/system/running/status")
    @ManagerAuth(memo = "系统运行状态")
@@ -122,6 +126,7 @@
                vo.setErrorMsg(stationProtocol.getErrorMsg()); // 报警信息
                vo.setBarcode(stationProtocol.getBarcode()); // 条码
                vo.setWeight(stationProtocol.getWeight());//重量
                vo.setTaskWriteIdx(stationProtocol.getTaskWriteIdx());//任务可写区
                String stationStatus = StationStatusType.process(stationProtocol).toString().toLowerCase().replaceAll("_", "-");
                if (stationProtocol.isAutoing() && stationProtocol.isLoading() && stationProtocol.getTaskNo() > 0 && !stationProtocol.isRunBlock()) {
                    String taskClass = getStationTaskClass(stationProtocol.getTaskNo(), inTaskRange, outTaskRange);
@@ -264,6 +269,11 @@
        return R.ok().add(vos);
    }
    @GetMapping("/latest/data/station/cycle/capacity")
    public R stationCycleCapacity() {
        return R.ok().add(stationCycleCapacityService.getLatestSnapshot());
    }
    // @PostMapping("/latest/data/barcode")
    // @ManagerAuth(memo = "条码扫描仪实时数据")
    // public R barcodeLatestData(){
@@ -341,10 +351,10 @@
    @GetMapping("/map/{lev}/auth")
    public R getLocMap(@PathVariable Integer lev) {
        Object object = redisUtil.get(RedisKeyType.LOC_MAP_BASE.key);
        if (object == null) {
            return R.error("地图未初始化");
        List<List<HashMap<String, Object>>> mapNodeList = null;
        if (object != null) {
            mapNodeList = (List<List<HashMap<String, Object>>>) object;
        }
        List<List<HashMap<String, Object>>> mapNodeList = (List<List<HashMap<String, Object>>>) object;
        List<LocMast> locMastList = locMastService.selectLocByLev(lev);
        for (LocMast locMast : locMastList) {
            String[] locType = locMast.getLocType().split("-");
src/main/java/com/zy/asrs/controller/CrnController.java
@@ -173,7 +173,7 @@
            return R.error("线程不存在");
        }
        CrnCommand command = crnThread.getResetCommand(crnNo);
        CrnCommand command = crnThread.getResetCommand(9999, crnNo);
        MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
        return R.ok();
    }
src/main/java/com/zy/asrs/controller/DualCrnController.java
@@ -5,11 +5,14 @@
import com.core.common.Cools;
import com.core.common.R;
import com.zy.asrs.domain.param.DualCrnCommandParam;
import com.zy.asrs.domain.param.DualCrnUpdateTaskNoParam;
import com.zy.asrs.domain.vo.DualCrnStateTableVo;
import com.zy.asrs.entity.BasDualCrnp;
import com.zy.asrs.service.BasDualCrnpService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.model.Task;
import com.zy.core.model.command.DualCrnCommand;
@@ -31,6 +34,8 @@
    @Autowired
    private BasDualCrnpService basDualCrnpService;
    @Autowired
    private RedisUtil redisUtil;
    @PostMapping("/dualcrn/table/crn/state")
    @ManagerAuth(memo = "双工位堆垛机信息表")
@@ -51,6 +56,8 @@
            }
            vo.setTaskNo(p.getTaskNo());
            vo.setTaskNoTwo(p.getTaskNoTwo());
            vo.setDeviceTaskNo(p.getDeviceTaskNo());
            vo.setDeviceTaskNoTwo(p.getDeviceTaskNoTwo());
            vo.setMode(p.getModeType() == null ? "-" : p.getModeType().desc);
            vo.setStatus(p.getStatusType() == null ? "-" : p.getStatusType().desc);
            vo.setStatusTwo(p.getStatusTypeTwo() == null ? "-" : p.getStatusTypeTwo().desc);
@@ -62,8 +69,10 @@
            vo.setForkOffsetTwo(p.getForkPosTypeTwo() == null ? "-" : p.getForkPosTypeTwo().desc);
            vo.setLiftPos(p.getLiftPosType() == null ? "-" : p.getLiftPosType().desc);
            vo.setWalkPos(p.getWalkPos() != null && p.getWalkPos() == 0 ? "在定位" : "不在定位");
            vo.setTaskReceive(p.getTaskReceive() != null && p.getTaskReceive() == 1 ? "接收" : "无任务");
            vo.setTaskReceiveTwo(p.getTaskReceiveTwo() != null && p.getTaskReceiveTwo() == 1 ? "接收" : "无任务");
            vo.setTaskReceive(String.valueOf(p.getTaskReceive()));
            vo.setTaskReceiveTwo(String.valueOf(p.getTaskReceiveTwo()));
            vo.setTaskSend(String.valueOf(p.getTaskSend()));
            vo.setTaskSendTwo(String.valueOf(p.getTaskSendTwo()));
            vo.setXspeed(String.format("%.2f", p.getXSpeed()));
            vo.setYspeed(String.format("%.2f", p.getYSpeed()));
@@ -175,8 +184,39 @@
        if (crnThread == null) {
            return R.error("线程不存在");
        }
        DualCrnCommand command = crnThread.getResetCommand(crnNo, station);
        DualCrnCommand command = crnThread.getResetCommand(9999, crnNo, station);
        MessageQueue.offer(SlaveType.DualCrn, crnNo, new Task(3, command));
        return R.ok();
    }
    @PostMapping("/dualcrn/command/updateTaskNo")
    @ManagerAuth(memo = "双工位堆垛机编辑任务号")
    public R dualCrnUpdateTaskNo(@RequestBody DualCrnUpdateTaskNoParam param) {
        if (Cools.isEmpty(param) || param.getCrnNo() == null || param.getStation() == null || param.getTaskNo() == null) {
            return R.error("缺少参数");
        }
        Integer station = param.getStation();
        if (station != 1 && station != 2) {
            return R.error("工位参数错误");
        }
        Integer taskNo = param.getTaskNo();
        if (taskNo < 0) {
            return R.error("任务号不能小于0");
        }
        Integer crnNo = param.getCrnNo();
        DualCrnThread crnThread = (DualCrnThread) SlaveConnection.get(SlaveType.DualCrn, crnNo);
        if (crnThread == null) {
            return R.error("线程不存在");
        }
        DualCrnProtocol protocol = crnThread.getStatus();
        if (protocol == null) {
            return R.error("设备状态不存在");
        }
        if (station == 1) {
            redisUtil.set(RedisKeyType.DUAL_CRN_STATION1_FLAG.key + crnNo, taskNo, 60 * 60 * 24);
        } else {
            redisUtil.set(RedisKeyType.DUAL_CRN_STATION2_FLAG.key + crnNo, taskNo, 60 * 60 * 24);
        }
        return R.ok();
    }
}
src/main/java/com/zy/asrs/controller/OpenController.java
@@ -235,8 +235,7 @@
        // 获取输送站点数据
        List<StationProtocol> stationProtocols = new ArrayList<>();
        List<DeviceConfig> devpList = deviceConfigService.selectList(new EntityWrapper<DeviceConfig>()
                .eq("device_type", String.valueOf(SlaveType.Devp))
        );
                .eq("device_type", String.valueOf(SlaveType.Devp)));
        for (DeviceConfig device : devpList) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, device.getDeviceNo());
            if (stationThread == null) {
src/main/java/com/zy/asrs/controller/StationController.java
@@ -1,12 +1,18 @@
package com.zy.asrs.controller;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.zy.asrs.domain.param.StationCommandBarcodeParam;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.DeviceConfig;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.DeviceConfigService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.StationCommandType;
import com.zy.core.model.StationObjModel;
import com.zy.system.entity.Config;
import com.zy.system.service.ConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -29,10 +35,16 @@
@RequestMapping("/station")
public class StationController {
    @Value("${mainProcessPlugin}")
    private String mainProcessPlugin;
    @Autowired
    private BasDevpService basDevpService;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private ConfigService configService;
    @Autowired
    private DeviceConfigService deviceConfigService;
    @PostMapping("/command/move")
    public R commandMove(@RequestBody StationCommandMoveParam param) {
@@ -44,21 +56,7 @@
        Integer taskNo = param.getTaskNo();
        Integer targetStationId = param.getTargetStationId();
        StationObjModel finalStation = null;
        List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            List<StationObjModel> list = basDevp.getStationList$();
            for (StationObjModel entity : list) {
                if(entity.getStationId().equals(stationId)){
                    finalStation = entity;
                    break;
                }
            }
            if(finalStation != null){
                break;
            }
        }
        StationObjModel finalStation = findStation(stationId);
        if(finalStation == null){
            return R.error("站点不存在");
@@ -76,6 +74,51 @@
        return R.ok();
    }
    @PostMapping("/command/barcode")
    public R commandBarcode(@RequestBody StationCommandBarcodeParam param) {
        if (Cools.isEmpty(param) || Cools.isEmpty(param.getStationId())) {
            return R.error("缺少参数");
        }
        if (!mainProcessPlugin.contains("Fake")) {
            return R.error("当前系统未启用仿真插件");
        }
        Config enableFakeConfig = configService.selectOne(new EntityWrapper<Config>().eq("code", "enableFake"));
        if (enableFakeConfig == null || !"Y".equals(enableFakeConfig.getValue())) {
            return R.error("当前非仿真运行模式,禁止修改条码");
        }
        Integer stationId = param.getStationId();
        StationObjModel finalStation = findStation(stationId);
        if (finalStation == null) {
            return R.error("站点不存在");
        }
        Integer devpNo = finalStation.getDeviceNo();
        DeviceConfig deviceConfig = deviceConfigService.selectOne(new EntityWrapper<DeviceConfig>()
                .eq("device_no", devpNo)
                .eq("device_type", String.valueOf(SlaveType.Devp)));
        if (deviceConfig == null || deviceConfig.getFake() == null || deviceConfig.getFake() != 1) {
            return R.error("当前站点设备未启用仿真,禁止修改条码");
        }
        StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, devpNo);
        if (stationThread == null) {
            return R.error("线程不存在");
        }
        String barcode = param.getBarcode();
        if (barcode == null) {
            barcode = "";
        }
        StationCommand command = stationThread.getCommand(StationCommandType.WRITE_INFO, 9997, stationId, stationId, 0);
        command.setBarcode(barcode.trim());
        MessageQueue.offer(SlaveType.Devp, devpNo, new Task(2, command));
        return R.ok();
    }
    @PostMapping("/command/reset")
    public R commandReset(@RequestBody StationCommandMoveParam param) {
        if (Cools.isEmpty(param)) {
@@ -87,4 +130,21 @@
        return R.ok();
    }
    private StationObjModel findStation(Integer stationId) {
        if (Cools.isEmpty(stationId)) {
            return null;
        }
        List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            List<StationObjModel> list = basDevp.getStationList$();
            for (StationObjModel entity : list) {
                if (entity.getStationId().equals(stationId)) {
                    return entity;
                }
            }
        }
        return null;
    }
}
src/main/java/com/zy/asrs/domain/enums/NotifyMsgType.java
@@ -20,6 +20,7 @@
    DUAL_CRN_TRANSFER_TASK_COMPLETE("dual_crn_transfer_task_complete", "双工位堆垛机移库任务执行完成"),
    STATION_OUT_TASK_RUN("station_out_task_run","输送站点出库任务运行中"),
    STATION_OUT_TASK_RUN_COMPLETE("station_out_task_run_complete","输送站点出库任务运行完成"),
    ;
    public String flag;
src/main/java/com/zy/asrs/domain/vo/DualCrnStateTableVo.java
@@ -15,6 +15,10 @@
    private Integer taskNoTwo = 0;
    private Integer deviceTaskNo = 0;
    private Integer deviceTaskNoTwo = 0;
    private String status = "-";
    private String statusTwo = "-";
@@ -31,6 +35,10 @@
    private String taskReceiveTwo = "-";
    private String taskSend = "-";
    private String taskSendTwo = "-";
    private Integer bay;
    private Integer lev;
src/main/java/com/zy/asrs/domain/vo/StationLatestDataVo.java
@@ -55,4 +55,7 @@
    //重量
    private Double weight;
    //任务可写区
    private Integer taskWriteIdx;
}
src/main/java/com/zy/asrs/entity/BasDevp.java
@@ -113,6 +113,20 @@
    @TableField("run_block_reassign_loc_station_list")
    private String runBlockReassignLocStationList;
    /**
     * 出库排序交互点
     */
    @ApiModelProperty(value= "出库排序交互点")
    @TableField("is_out_order_list")
    private String isOutOrderList;
    /**
     * 顶升移栽点
     */
    @ApiModelProperty(value= "顶升移栽点")
    @TableField("is_lift_transfer_list")
    private String isLiftTransferList;
    public BasDevp() {}
    public BasDevp(Integer devpNo,Integer status,Long createBy,Date createTime,Long updateBy,Date updateTime,String memo,String stationList,String barcodeStationList,String inStationList,String outStationList) {
@@ -229,4 +243,43 @@
        return list;
    }
    public List<StationObjModel> getOutOrderList$(){
        List<StationObjModel> list = new ArrayList<>();
        if (Cools.isEmpty(this.isOutOrderList)){
            return list;
        }
        List<StationObjModel> jsonList = JSON.parseArray(this.isOutOrderList, StationObjModel.class);
        for (StationObjModel json : jsonList){
            list.add(json);
        }
        return list;
    }
    public List<Integer> getOutOrderIntList(){
        List<Integer> list = new ArrayList<>();
        if (Cools.isEmpty(this.isOutOrderList)){
            return list;
        }
        List<StationObjModel> jsonList = JSON.parseArray(this.isOutOrderList, StationObjModel.class);
        for (StationObjModel json : jsonList){
            list.add(json.getStationId());
        }
        return list;
    }
    public List<StationObjModel> getLiftTransferList$(){
        List<StationObjModel> list = new ArrayList<>();
        if (Cools.isEmpty(this.isLiftTransferList)){
            return list;
        }
        List<StationObjModel> jsonList = JSON.parseArray(this.isLiftTransferList, StationObjModel.class);
        for (StationObjModel json : jsonList){
            list.add(json);
        }
        return list;
    }
}
src/main/java/com/zy/asrs/task/WrkMastScheduler.java
@@ -95,8 +95,8 @@
                continue;
            }
            if (!locMast.getLocSts().equals("R")) {
                log.info("[workNo={}]库位状态不处于R", wrkMast.getWrkNo());
            if (!(locMast.getLocSts().equals("R") || locMast.getLocSts().equals("O"))) {
                log.info("[workNo={}]库位状态不处于R or O", wrkMast.getWrkNo());
                continue;
            }
@@ -237,4 +237,40 @@
        }
    }
    @Scheduled(cron = "0/1 * * * * ? ")
    @Transactional
    public void processOutStationRun(){
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("wrk_sts", WrkStsType.STATION_RUN.sts));
        if (wrkMasts.isEmpty()) {
            return;
        }
        for (WrkMast wrkMast : wrkMasts) {
            String locNo = wrkMast.getSourceLocNo();
            LocMast locMast = locMastService.queryByLoc(locNo);
            if (locMast == null) {
                log.info("[workNo={}]库位不存在", wrkMast.getWrkNo());
                continue;
            }
            if (locMast.getLocSts().equals("O")) {
                continue;
            }
            if (!locMast.getLocSts().equals("R")) {
                log.info("[workNo={}]库位状态不处于R", wrkMast.getWrkNo());
                continue;
            }
            locMast.setLocSts("O");
            locMast.setBarcode("");
            locMast.setModiTime(new Date());
            boolean result = locMastService.updateById(locMast);
            if (!result) {
                log.info("[workNo={}]库位状态O更新失败", wrkMast.getWrkNo());
                continue;
            }
        }
    }
}
src/main/java/com/zy/asrs/utils/MapExcelUtils.java
@@ -322,7 +322,9 @@
                            }
                            HashMap<String, Object> mapData = data.get(k - 2).get(l - 2);
                            mapData.put("bgColor", "merge");
                            Object devp = map.get("nodeType");
                            mapData.put("nodeType", "merge");
                            mapData.put("mergeType", devp);
                            mapData.put("value", map.get("value"));
                        }
                    }
@@ -342,6 +344,7 @@
        if (cell == null) {
            HashMap<String, Object> map = new HashMap<>();
            map.put("bgColor", "none");
            map.put("nodeType", "none");
            map.put("cellWidth", "");
            map.put("cellHeight", "");
            map.put("value", "");
@@ -354,6 +357,25 @@
        String bgColor = getCellBackgroundColor(cell);
        map.put("bgColor", bgColor);
        if(bgColor.equals("RGB(0,176,80)")) {
            //货架
            map.put("nodeType", "shelf");
        } else if (bgColor.equals("RGB(255,192,0)")) {
            //堆垛机
            map.put("nodeType", "crn");
        } else if (bgColor.equals("RGB(255,255,0)")) {
            //双工位堆垛机
            map.put("nodeType", "dualCrn");
        } else if (bgColor.equals("RGB(0,112,192)")) {
            //输送线
            map.put("nodeType", "devp");
        } else if (bgColor.equals("RGB(0,176,240)")) {
            //RGV
            map.put("nodeType", "rgv");
        }else {
            //空白区域
            map.put("nodeType", "none");
        }
        int columnIndex = cell.getColumnIndex();
        int columnWidth = sheet.getColumnWidth(columnIndex);//获取列宽
src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java
@@ -103,6 +103,9 @@
            } else if ("/console/latest/data/dualcrn".equals(url)) {
                ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class);
                resObj = consoleController.dualCrnLatestData();
            } else if ("/console/latest/data/station/cycle/capacity".equals(url)) {
                ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class);
                resObj = consoleController.stationCycleCapacity();
            } else if ("/crn/table/crn/state".equals(url)) {
                resObj = SpringUtils.getBean(CrnController.class).crnStateTable();
            } else if ("/rgv/table/rgv/state".equals(url)) {
src/main/java/com/zy/common/model/NavigateNode.java
@@ -22,6 +22,7 @@
    private NavigateNode Father;//父节点
    private List<String> directionList;//允许行走方向
    private Boolean isInflectionPoint;//是否为拐点
    private Boolean isLiftTransferPoint;//是否为顶升移栽点
    private String direction;//行走方向
    private String nodeValue;//节点数据
    private String nodeType;//节点类型
src/main/java/com/zy/common/service/CommonService.java
@@ -12,19 +12,13 @@
import com.zy.common.utils.NavigateUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.WrkIoType;
import com.zy.core.enums.WrkStsType;
import com.zy.core.enums.*;
import com.zy.core.model.StationObjModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.*;
@Slf4j
@Service
@@ -226,10 +220,10 @@
        if (null == locMast) {
            throw new CoolException(param.getLocNo() + "目标库位不存在");
        }
        if (!locMast.getLocSts().equals("O")) {
            throw new CoolException(locMast.getLocNo() + "目标库位不处于空库状态");
        }
        News.info("任务号:{} 目标库位信息:{}",param.getTaskNo(), param.getLocNo());
        double ioPri = 100D;
        if (param.getTaskPri() != null) {
@@ -261,7 +255,6 @@
        if (findCrnResult.getCrnType().equals(SlaveType.Crn)) {
            wrkMast.setCrnNo(findCrnResult.getCrnNo());
            //缓存记录当前命令堆垛机编号
            redisUtil.set(RedisKeyType.CURRENT_CIRCLE_TASK_CRN_NO.key, crnNo, 60 * 60 * 24);
        } else if (findCrnResult.getCrnType().equals(SlaveType.DualCrn)) {
@@ -400,10 +393,15 @@
    }
    public FindCrnNoResult findCrnNoByLocNo(String locNo) {
        if (Objects.isNull(locNo)) {
            News.error("任务号为空!");
            return null;
        }
        List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<>());
        for (BasCrnp basCrnp : basCrnps) {
            List<List<Integer>> rowList = basCrnp.getControlRows$();
            for (List<Integer> rows : rowList) {
//                System.out.println("库位号:" + locNo);
                if(rows.contains(Utils.getRow(locNo))) {
                    FindCrnNoResult result = new FindCrnNoResult();
                    result.setCrnNo(basCrnp.getCrnNo());
src/main/java/com/zy/common/utils/NavigateUtils.java
@@ -6,16 +6,20 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.BasStation;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.BasStationService;
import com.zy.core.News;
import com.zy.core.model.StationObjModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.common.SpringUtils;
import com.core.exception.CoolException;
import com.zy.common.model.NavigateNode;
@@ -31,6 +35,11 @@
@Component
public class NavigateUtils {
    private static final String CFG_STATION_PATH_LEN_WEIGHT_PERCENT = "stationPathLenWeightPercent";
    private static final String CFG_STATION_PATH_CONG_WEIGHT_PERCENT = "stationPathCongWeightPercent";
    private static final String CFG_STATION_PATH_PASS_OTHER_OUT_STATION_WEIGHT_PERCENT = "stationPathPassOtherOutStationWeightPercent";
    private static final String CFG_STATION_PATH_PASS_OTHER_OUT_STATION_FORCE_SKIP = "stationPathPassOtherOutStationForceSkip";
    @Autowired
    private BasStationService basStationService;
@@ -45,12 +54,12 @@
        List<List<NavigateNode>> stationMap = navigateSolution.getStationMap(lev);
        NavigateNode startNode = navigateSolution.findStationNavigateNode(stationMap, startStationId);
        if (startNode == null){
        if (startNode == null) {
            throw new CoolException("未找到该 起点 对应的节点");
        }
        NavigateNode endNode = navigateSolution.findStationNavigateNode(stationMap, endStationId);
        if (endNode == null){
        if (endNode == null) {
            throw new CoolException("未找到该 终点 对应的节点");
        }
@@ -58,7 +67,8 @@
        News.info("[WCS Debug] 站点路径开始计算,startStationId={},endStationId={}", startStationId, endStationId);
        List<List<NavigateNode>> allList = navigateSolution.allSimplePaths(stationMap, startNode, endNode, 120, 500, 300);
        if (allList.isEmpty()) {
            throw new CoolException("未找到该路径");
//            throw new CoolException("未找到该路径");
            return new ArrayList<>();
        }
        News.info("[WCS Debug] 站点路径计算完成,耗时:{}ms", System.currentTimeMillis() - startTime);
@@ -70,9 +80,12 @@
        //去重
        HashSet<Integer> set = new HashSet<>();
        List<NavigateNode> fitlerList = new ArrayList<>();
        for(NavigateNode navigateNode : list){
        for (NavigateNode navigateNode : list) {
            JSONObject valuObject = JSON.parseObject(navigateNode.getNodeValue());
            if(set.add(valuObject.getInteger("stationId"))){
            if (valuObject.containsKey("rgvCalcFlag")) {
                continue;
            }
            if (set.add(valuObject.getInteger("stationId"))) {
                fitlerList.add(navigateNode);
            }
        }
@@ -80,6 +93,20 @@
        for (int i = 0; i < fitlerList.size(); i++) {
            NavigateNode currentNode = fitlerList.get(i);
            currentNode.setIsInflectionPoint(false);
            currentNode.setIsLiftTransferPoint(false);
            try {
                JSONObject valueObject = JSON.parseObject(currentNode.getNodeValue());
                if (valueObject != null) {
                    Object isLiftTransfer = valueObject.get("isLiftTransfer");
                    if (isLiftTransfer != null) {
                        String isLiftTransferStr = isLiftTransfer.toString();
                        if ("1".equals(isLiftTransferStr) || "true".equalsIgnoreCase(isLiftTransferStr)) {
                            currentNode.setIsLiftTransferPoint(true);
                        }
                    }
                }
            } catch (Exception ignore) {}
            NavigateNode nextNode = (i + 1 < fitlerList.size()) ? fitlerList.get(i + 1) : null;
            NavigateNode prevNode = (i - 1 >= 0) ? fitlerList.get(i - 1) : null;
@@ -112,7 +139,8 @@
        News.info("[WCS Debug] RGV路径开始计算,startTrackSiteNo:{},endTrackSiteNo={}", startTrackSiteNo, endTrackSiteNo);
        NavigateNode res_node = navigateSolution.astarSearchJava(rgvTrackMap, startNode, endNode);
        if (res_node == null) {
            throw new CoolException("未找到该路径");
//            throw new CoolException("未找到该路径");
            return new ArrayList<>();
        }
        News.info("[WCS Debug] RGV路径计算完成,耗时:{}ms", System.currentTimeMillis() - startTime);
@@ -214,10 +242,28 @@
            }
        } catch (Exception ignore) {}
        Set<Integer> outStationIdSet = loadAllOutStationIdSet();
        double lenWeightPercent = 50.0;
        double congWeightPercent = 50.0;
        double passOtherOutStationWeightPercent = 100.0;
        boolean forceSkipPassOtherOutStation = false;
        try {
            ConfigService configService = SpringUtils.getBean(ConfigService.class);
            if (configService != null) {
                lenWeightPercent = loadDoubleConfig(configService, CFG_STATION_PATH_LEN_WEIGHT_PERCENT, lenWeightPercent);
                congWeightPercent = loadDoubleConfig(configService, CFG_STATION_PATH_CONG_WEIGHT_PERCENT, congWeightPercent);
                passOtherOutStationWeightPercent = loadDoubleConfig(configService, CFG_STATION_PATH_PASS_OTHER_OUT_STATION_WEIGHT_PERCENT, passOtherOutStationWeightPercent);
                forceSkipPassOtherOutStation = loadBooleanConfig(configService, CFG_STATION_PATH_PASS_OTHER_OUT_STATION_FORCE_SKIP, false);
            }
        } catch (Exception ignore) {}
        List<List<NavigateNode>> candidates = new ArrayList<>();
        List<Integer> lens = new ArrayList<>();
        List<Integer> tasksList = new ArrayList<>();
        List<Double> congs = new ArrayList<>();
        List<Integer> passOtherOutStationCounts = new ArrayList<>();
        int skippedByPassOtherOutStation = 0;
        for (List<NavigateNode> path : allList) {
            if (path == null || path.isEmpty()) {
@@ -247,13 +293,27 @@
                }
            }
            double cong = len <= 0 ? 0.0 : (double) tasks / (double) len;
            int passOtherOutStationCount = countPassOtherOutStations(path, outStationIdSet);
            if (forceSkipPassOtherOutStation && passOtherOutStationCount > 0) {
                skippedByPassOtherOutStation++;
                News.info("[WCS Debug] 站点路径候选已跳过,因经过其他出库站点,startStationId={},endStationId={},passOtherOutStationCount={}",
                        extractStationId(path.get(0)),
                        extractStationId(path.get(path.size() - 1)),
                        passOtherOutStationCount);
                continue;
            }
            candidates.add(path);
            lens.add(len);
            tasksList.add(tasks);
            congs.add(cong);
            passOtherOutStationCounts.add(passOtherOutStationCount);
        }
        if (candidates.isEmpty()) {
            if (forceSkipPassOtherOutStation && skippedByPassOtherOutStation > 0) {
                News.info("[WCS Debug] 所有站点路径候选均因经过其他出库站点被强制跳过");
                return new ArrayList<>();
            }
            return allList.get(0);
        }
@@ -261,59 +321,50 @@
        int maxLen = Integer.MIN_VALUE;
        double minCong = Double.MAX_VALUE;
        double maxCong = -Double.MAX_VALUE;
        int minPassOtherOutStationCount = Integer.MAX_VALUE;
        int maxPassOtherOutStationCount = Integer.MIN_VALUE;
        for (int i = 0; i < candidates.size(); i++) {
            int l = lens.get(i);
            double c = congs.get(i);
            int p = passOtherOutStationCounts.get(i);
            if (l < minLen) minLen = l;
            if (l > maxLen) maxLen = l;
            if (c < minCong) minCong = c;
            if (c > maxCong) maxCong = c;
            if (p < minPassOtherOutStationCount) minPassOtherOutStationCount = p;
            if (p > maxPassOtherOutStationCount) maxPassOtherOutStationCount = p;
        }
        //长度权重百分比
        double lenWeightPercent = 50.0;
        //拥堵权重百分比
        double congWeightPercent = 50.0;
        try {
            ConfigService configService = SpringUtils.getBean(ConfigService.class);
            if (configService != null) {
                Config cfgLen = configService.selectOne(new EntityWrapper<Config>().eq("code", "stationPathLenWeightPercent"));
                if (cfgLen != null && cfgLen.getValue() != null) {
                    String v = cfgLen.getValue().trim();
                    if (v.endsWith("%")) v = v.substring(0, v.length() - 1);
                    try { lenWeightPercent = Double.parseDouble(v); } catch (Exception ignore) {}
                }
                Config cfgCong = configService.selectOne(new EntityWrapper<Config>().eq("code", "stationPathCongWeightPercent"));
                if (cfgCong != null && cfgCong.getValue() != null) {
                    String v = cfgCong.getValue().trim();
                    if (v.endsWith("%")) v = v.substring(0, v.length() - 1);
                    try { congWeightPercent = Double.parseDouble(v); } catch (Exception ignore) {}
                }
            }
        } catch (Exception ignore) {}
        double weightSum = lenWeightPercent + congWeightPercent;
        double weightSum = lenWeightPercent + congWeightPercent + passOtherOutStationWeightPercent;
        double lenW = weightSum <= 0 ? 0.5 : lenWeightPercent / weightSum;
        double congW = weightSum <= 0 ? 0.5 : congWeightPercent / weightSum;
        double passOtherOutStationW = weightSum <= 0 ? 0.0 : passOtherOutStationWeightPercent / weightSum;
        List<NavigateNode> best = null;
        double bestCost = Double.MAX_VALUE;
        int bestPassOtherOutStationCount = Integer.MAX_VALUE;
        int bestTasks = Integer.MAX_VALUE;
        int bestLen = Integer.MAX_VALUE;
        for (int i = 0; i < candidates.size(); i++) {
            int l = lens.get(i);
            int t = tasksList.get(i);
            double c = congs.get(i);
            int p = passOtherOutStationCounts.get(i);
            //归一化
            double lenNorm = (maxLen - minLen) <= 0 ? 0.0 : (l - minLen) / (double) (maxLen - minLen);
            double congNorm = (maxCong - minCong) <= 0 ? 0.0 : (c - minCong) / (double) (maxCong - minCong);
            double passOtherOutStationNorm = (maxPassOtherOutStationCount - minPassOtherOutStationCount) <= 0
                    ? 0.0
                    : (p - minPassOtherOutStationCount) / (double) (maxPassOtherOutStationCount - minPassOtherOutStationCount);
            //获取权重
            double cost = lenNorm * lenW + congNorm * congW;
            double cost = lenNorm * lenW + congNorm * congW + passOtherOutStationNorm * passOtherOutStationW;
            if (cost < bestCost
                    || (cost == bestCost && t < bestTasks)
                    || (cost == bestCost && t == bestTasks && l < bestLen)) {
                    || (cost == bestCost && p < bestPassOtherOutStationCount)
                    || (cost == bestCost && p == bestPassOtherOutStationCount && t < bestTasks)
                    || (cost == bestCost && p == bestPassOtherOutStationCount && t == bestTasks && l < bestLen)) {
                best = candidates.get(i);
                bestCost = cost;
                bestPassOtherOutStationCount = p;
                bestTasks = t;
                bestLen = l;
            }
@@ -325,6 +376,102 @@
        return best;
    }
    private Set<Integer> loadAllOutStationIdSet() {
        Set<Integer> outStationIdSet = new HashSet<>();
        try {
            BasDevpService basDevpService = SpringUtils.getBean(BasDevpService.class);
            if (basDevpService == null) {
                return outStationIdSet;
            }
            List<BasDevp> basDevpList = basDevpService.selectList(new EntityWrapper<BasDevp>().eq("status", 1));
            for (BasDevp basDevp : basDevpList) {
                List<StationObjModel> outStationList = basDevp.getOutStationList$();
                for (StationObjModel stationObjModel : outStationList) {
                    if (stationObjModel != null && stationObjModel.getStationId() != null) {
                        outStationIdSet.add(stationObjModel.getStationId());
                    }
                }
            }
        } catch (Exception ignore) {}
        return outStationIdSet;
    }
    private int countPassOtherOutStations(List<NavigateNode> path, Set<Integer> outStationIdSet) {
        if (path == null || path.size() < 3 || outStationIdSet == null || outStationIdSet.isEmpty()) {
            return 0;
        }
        Integer startStationId = extractStationId(path.get(0));
        Integer endStationId = extractStationId(path.get(path.size() - 1));
        Set<Integer> hitStationIdSet = new HashSet<>();
        for (int i = 1; i < path.size() - 1; i++) {
            Integer stationId = extractStationId(path.get(i));
            if (stationId == null) {
                continue;
            }
            if (startStationId != null && startStationId.equals(stationId)) {
                continue;
            }
            if (endStationId != null && endStationId.equals(stationId)) {
                continue;
            }
            if (outStationIdSet.contains(stationId)) {
                hitStationIdSet.add(stationId);
            }
        }
        return hitStationIdSet.size();
    }
    private Integer extractStationId(NavigateNode node) {
        if (node == null || node.getNodeValue() == null) {
            return null;
        }
        try {
            JSONObject value = JSON.parseObject(node.getNodeValue());
            if (value == null) {
                return null;
            }
            return value.getInteger("stationId");
        } catch (Exception ignore) {}
        return null;
    }
    private double loadDoubleConfig(ConfigService configService, String code, double defaultValue) {
        if (configService == null || code == null) {
            return defaultValue;
        }
        Config config = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
        if (config == null || config.getValue() == null) {
            return defaultValue;
        }
        String value = config.getValue().trim();
        if (value.endsWith("%")) {
            value = value.substring(0, value.length() - 1);
        }
        try {
            return Double.parseDouble(value);
        } catch (Exception ignore) {}
        return defaultValue;
    }
    private boolean loadBooleanConfig(ConfigService configService, String code, boolean defaultValue) {
        if (configService == null || code == null) {
            return defaultValue;
        }
        Config config = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
        if (config == null || config.getValue() == null) {
            return defaultValue;
        }
        String value = config.getValue().trim();
        if (value.isEmpty()) {
            return defaultValue;
        }
        return "1".equals(value)
                || "true".equalsIgnoreCase(value)
                || "yes".equalsIgnoreCase(value)
                || "y".equalsIgnoreCase(value)
                || "on".equalsIgnoreCase(value);
    }
    //判断当前节点到下一个节点是否为拐点
    public HashMap<String,Object> searchInflectionPoint(NavigateNode currentNode, NavigateNode fatherNode, NavigateNode nextNode) {
        HashMap<String, Object> map = new HashMap<>();
src/main/java/com/zy/core/ServerBootstrap.java
@@ -135,6 +135,8 @@
                    thread = new ZyStationThread(deviceConfig, redisUtil);
                } else if (deviceConfig.getThreadImpl().equals("ZyStationV3Thread")) {
                    thread = new ZyStationV3Thread(deviceConfig, redisUtil);
                } else if (deviceConfig.getThreadImpl().equals("ZyStationV4Thread")) {
                    thread = new ZyStationV4Thread(deviceConfig, redisUtil);
                } else {
                    throw new CoolException("未知的线程实现");
                }
src/main/java/com/zy/core/enums/RedisKeyType.java
@@ -2,15 +2,11 @@
public enum RedisKeyType {
    SHUTTLE_WORK_FLAG("shuttle_wrk_no_"),
    SHUTTLE_FLAG("shuttle_"),
    FORK_LIFT_WORK_FLAG("fork_lift_wrk_no_"),
    FORK_LIFT_FLAG("fork_lift_"),
    LIFT_WORK_FLAG("lift_wrk_no_"),
    LIFT_FLAG("lift_"),
    DUAL_CRN_STATION1_FLAG("dual_crn_station1_flag_"),
    DUAL_CRN_STATION2_FLAG("dual_crn_station2_flag_"),
    DUAL_CRN_COMMAND_("dual_crn_command_"),
    DUAL_CRN_COMMAND_IDX("dual_crn_command_idx_"),
    DUAL_CRN_COMMAND_LAST("dual_crn_command_LAST_"),
    QUEUE_CRN("queue_crn_"),
    QUEUE_DUAL_CRN("queue_dual_crn_"),
@@ -59,6 +55,9 @@
    DUAL_CRN_OUT_TASK_COMPLETE_STATION_INFO("dual_crn_out_task_complete_station_info_"),
    CRN_OUT_TASK_COMPLETE_STATION_INFO("crn_out_task_complete_station_info_"),
    WATCH_CIRCLE_STATION_("watch_circle_station_"),
    STATION_CYCLE_LOAD_RESERVE("station_cycle_load_reserve"),
    CURRENT_CIRCLE_TASK_CRN_NO("current_circle_task_crn_no_"),
    ASYNC_WMS_IN_TASK_REQUEST("async_wms_in_task_request_"),
    ASYNC_WMS_IN_TASK_RESPONSE("async_wms_in_task_response_"),
src/main/java/com/zy/core/enums/WrkStsType.java
@@ -15,6 +15,7 @@
    OUTBOUND_RUN(102, "设备搬运中"),
    OUTBOUND_RUN_COMPLETE(103, "设备搬运完成"),
    STATION_RUN(104, "站点运行中"),
    STATION_RUN_COMPLETE(105, "站点运行完成"),
    COMPLETE_OUTBOUND(109, "出库完成"),
    SETTLE_OUTBOUND(110, "出库库存更新"),
src/main/java/com/zy/core/model/command/StationCommand.java
@@ -19,8 +19,14 @@
    private List<Integer> navigatePath;
    // 路径中的顶升移栽点(按路径顺序)
    private List<Integer> liftTransferPath;
    private List<Integer> originalNavigatePath;
    private StationCommandType commandType;
    // 仿真模式下可选:手工指定条码
    private String barcode;
}
src/main/java/com/zy/core/model/protocol/DualCrnProtocol.java
@@ -1,9 +1,9 @@
package com.zy.core.model.protocol;
import com.zy.core.enums.DualCrnForkPosType;
import com.zy.core.enums.DualCrnLiftPosType;
import com.zy.core.enums.DualCrnModeType;
import com.zy.core.enums.DualCrnStatusType;
import com.core.common.Cools;
import com.core.common.SpringUtils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.*;
import lombok.Data;
@@ -12,31 +12,41 @@
@Data
public class DualCrnProtocol {
    private Integer crnNo;
    private volatile Integer crnNo;
    /**
     * 1 = 手动模式
     * 2 = 自动模式
     * 3 = 电脑模式
     */
    public Integer mode;
    public volatile Integer mode;
    public DualCrnModeType modeType;
    public volatile DualCrnModeType modeType;
    /**
     * 异常码
     */
    public Integer alarm;
    public volatile Integer alarm;
    /**
     * 工位1任务号
     * WCS工位1任务号
     */
    public Integer taskNo = 0;
    public volatile Integer taskNo = 0;
    /**
     * 工位2任务号
     * WCS工位2任务号
     */
    public Integer taskNoTwo = 0;
    public volatile Integer taskNoTwo = 0;
    /**
     * 设备工位1任务号
     */
    public volatile Integer deviceTaskNo = 0;
    /**
     * 设备工位2任务号
     */
    public volatile Integer deviceTaskNoTwo = 0;
    /**
     * 工位1当前状态
@@ -51,42 +61,42 @@
     * 90:任务完成等待WCS确认
     * 99:报警
     */
    public Integer status;
    public volatile Integer status;
    /**
     * 工位2当前状态
     */
    public Integer statusTwo;
    public volatile Integer statusTwo;
    /**
     * 工位1状态枚举
     */
    public DualCrnStatusType statusType;
    public volatile DualCrnStatusType statusType;
    /**
     * 工位2状态枚举
     */
    public DualCrnStatusType statusTypeTwo;
    public volatile DualCrnStatusType statusTypeTwo;
    /**
     * 工位1堆垛机当前列号
     */
    public Integer bay;
    public volatile Integer bay;
    /**
     * 工位2堆垛机当前列号
     */
    public Integer bayTwo;
    public volatile Integer bayTwo;
    /**
     * 工位1堆垛机当前层号
     */
    public Integer level;
    public volatile Integer level;
    /**
     * 工位2堆垛机当前层号
     */
    public Integer levelTwo;
    public volatile Integer levelTwo;
    /**
     * 工位1当前货叉位置
@@ -94,7 +104,7 @@
     * 1 = 货叉在左侧
     * 2 = 货叉在右侧
     */
    public Integer forkPos;
    public volatile Integer forkPos;
    /**
     * 工位2当前货叉位置
@@ -102,51 +112,55 @@
     * 1 = 货叉在左侧
     * 2 = 货叉在右侧
     */
    public Integer forkPosTwo;
    public volatile Integer forkPosTwo;
    public DualCrnForkPosType forkPosType;
    public volatile DualCrnForkPosType forkPosType;
    public DualCrnForkPosType forkPosTypeTwo;
    public volatile DualCrnForkPosType forkPosTypeTwo;
    /**
     * 当前载货台位置
     * 0 = 下定位
     * 1 = 上定位
     */
    public Integer liftPos;
    public volatile Integer liftPos;
    public Integer liftPosTwo;
    public volatile Integer liftPosTwo;
    public DualCrnLiftPosType liftPosType;
    public volatile DualCrnLiftPosType liftPosType;
    public DualCrnLiftPosType liftPosTypeTwo;
    public volatile DualCrnLiftPosType liftPosTypeTwo;
    /**
     * 走行在定位
     * 0 = 在定位
     * 1 = 不在定位
     */
    public Integer walkPos;
    public volatile Integer walkPos;
    public Integer walkPosTwo;
    public volatile Integer walkPosTwo;
    /**
     * 载货台有物
     */
    public Integer loaded;
    public volatile  Integer loaded;
    public Integer loadedTwo;
    public volatile Integer loadedTwo;
    /**
     * 任务接收状态
     * 0 = 未接收
     * 1 = 已接收
     */
    public Integer taskReceive;
    public volatile Integer taskReceive;
    public Integer taskReceiveTwo;
    public volatile Integer taskReceiveTwo;
    private Integer temp1;
    public volatile Integer taskSend;
    public volatile Integer taskSendTwo;
    private volatile Integer temp1;
    private Integer temp2;
@@ -292,4 +306,32 @@
        this.statusTwo = DualCrnStatusType.get(type).id;
    }
    public Integer getTaskNo() {
        RedisUtil redisUtil = null;
        try {
            redisUtil = SpringUtils.getBean(RedisUtil.class);
        }catch (Exception e) {}
        if (null != redisUtil) {
            Object o = redisUtil.get(RedisKeyType.DUAL_CRN_STATION1_FLAG.key + this.crnNo);
            if (!Cools.isEmpty(o)) {
                this.taskNo = Integer.parseInt(String.valueOf(o));
            }
        }
        return this.taskNo == null ? 0 : this.taskNo;
    }
    public Integer getTaskNoTwo() {
        RedisUtil redisUtil = null;
        try {
            redisUtil = SpringUtils.getBean(RedisUtil.class);
        }catch (Exception e) {}
        if (null != redisUtil) {
            Object o = redisUtil.get(RedisKeyType.DUAL_CRN_STATION2_FLAG.key + this.crnNo);
            if (!Cools.isEmpty(o)) {
                this.taskNoTwo = Integer.parseInt(String.valueOf(o));
            }
        }
        return this.taskNoTwo == null ? 0 : this.taskNoTwo;
    }
}
src/main/java/com/zy/core/model/protocol/StationProtocol.java
@@ -64,6 +64,9 @@
    //WCS系统报警
    private String systemWarning;
    //任务可写区
    private Integer taskWriteIdx;
   /**
    * 扩展数据
    */
src/main/java/com/zy/core/network/ZyStationConnectDriver.java
@@ -11,8 +11,10 @@
import java.util.List;
import com.zy.core.network.fake.ZyStationFakeConnect;
import com.zy.core.network.fake.ZyStationFakeSegConnect;
import com.zy.core.network.fake.ZyStationV4FakeSegConnect;
import com.zy.core.network.real.ZyStationRealConnect;
import com.zy.core.network.real.ZyStationV3RealConnect;
import com.zy.core.network.real.ZyStationV4RealConnect;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -27,6 +29,7 @@
    private static final ZyStationFakeConnect zyStationFakeConnect = new ZyStationFakeConnect();
    private static final ZyStationFakeSegConnect zyStationFakeSegConnect = new ZyStationFakeSegConnect();
    private static final ZyStationV4FakeSegConnect zyStationV4FakeSegConnect = new ZyStationV4FakeSegConnect();
    private boolean connected = false;
    private DeviceConfig deviceConfig;
@@ -50,6 +53,8 @@
        if (deviceConfig.getFake() == 0) {
            if ("ZyStationV3Thread".equals(deviceConfig.getThreadImpl())) {
                zyStationConnectApi = new ZyStationV3RealConnect(deviceConfig, redisUtil);
            } else if ("ZyStationV4Thread".equals(deviceConfig.getThreadImpl())) {
                zyStationConnectApi = new ZyStationV4RealConnect(deviceConfig, redisUtil);
            } else {
                zyStationConnectApi = new ZyStationRealConnect(deviceConfig, redisUtil);
            }
@@ -57,6 +62,9 @@
            if ("ZyStationV3Thread".equals(deviceConfig.getThreadImpl())) {
                zyStationFakeSegConnect.addFakeConnect(deviceConfig, redisUtil);
                zyStationConnectApi = zyStationFakeSegConnect;
            } else if ("ZyStationV4Thread".equals(deviceConfig.getThreadImpl())) {
                zyStationV4FakeSegConnect.addFakeConnect(deviceConfig, redisUtil);
                zyStationConnectApi = zyStationV4FakeSegConnect;
            } else {
                zyStationFakeConnect.addFakeConnect(deviceConfig, redisUtil);
                zyStationConnectApi = zyStationFakeConnect;
src/main/java/com/zy/core/network/entity/ZyDualCrnStatusEntity.java
@@ -72,6 +72,10 @@
    public Integer taskReceiveTwo;
    public Integer taskSend;
    public Integer taskSendTwo;
    /**
     * 堆垛机当前列号
     */
src/main/java/com/zy/core/network/entity/ZyStationStatusEntity.java
@@ -50,6 +50,9 @@
    //重量
    private Double weight;
    //任务可写区
    private Integer taskWriteIdx;
    //运行堵塞
    private boolean runBlock = false;
src/main/java/com/zy/core/network/fake/ZyDualCrnFakeConnect.java
@@ -2,7 +2,7 @@
import com.alibaba.fastjson.JSON;
import com.zy.asrs.entity.DeviceConfig;
import com.zy.core.enums.CrnStatusType;
import com.zy.core.enums.DualCrnStatusType;
import com.zy.core.enums.DualCrnTaskModeType;
import com.zy.core.model.CommandResponse;
import com.zy.core.model.command.DualCrnCommand;
@@ -20,6 +20,8 @@
    private final ExecutorService executor = Executors
            .newFixedThreadPool(9999);
    private int taskExecuteStation = 0;
    private DualCrnCommand station1LastCommand = null;
    private DualCrnCommand station2LastCommand = null;
    public ZyDualCrnFakeConnect(DeviceConfig deviceConfig) {
        this.deviceConfig = deviceConfig;
@@ -71,13 +73,29 @@
                this.crnStatus.setTaskNo(0);
            }
            this.crnStatus.setTaskReceive(0);
            this.crnStatus.setStatus(CrnStatusType.IDLE.id);
            if (station1LastCommand == null) {
                this.crnStatus.setStatus(DualCrnStatusType.IDLE.id);
            }else {
                if (station1LastCommand.getTaskMode().intValue() == DualCrnTaskModeType.PICK.id) {
                    this.crnStatus.setStatus(DualCrnStatusType.FETCH_COMPLETE.id);
                }else {
                    this.crnStatus.setStatus(DualCrnStatusType.IDLE.id);
                }
            }
        }else {
            if (crnStatus.getLoadedTwo() == 0) {
                this.crnStatus.setTaskNoTwo(0);
            }
            this.crnStatus.setTaskReceiveTwo(0);
            this.crnStatus.setStatusTwo(CrnStatusType.IDLE.id);
            if (station2LastCommand == null) {
                this.crnStatus.setStatusTwo(DualCrnStatusType.IDLE.id);
            }else {
                if (station1LastCommand.getTaskMode().intValue() == DualCrnTaskModeType.PICK.id) {
                    this.crnStatus.setStatusTwo(DualCrnStatusType.FETCH_COMPLETE.id);
                }else {
                    this.crnStatus.setStatusTwo(DualCrnStatusType.IDLE.id);
                }
            }
        }
    }
@@ -103,16 +121,16 @@
        if(command.getStation() == 1) {
            this.crnStatus.setTaskNo(taskNo);
            this.crnStatus.setStatus(CrnStatusType.MOVING.id);
            this.crnStatus.setStatus(DualCrnStatusType.PUT_MOVING.id);
            this.crnStatus.setTaskReceive(1);
            moveYZ(this.crnStatus.getBay(), destinationPosY, this.crnStatus.getLevel(), destinationPosZ, command.getStation());
            this.crnStatus.setStatus(CrnStatusType.WAITING.id);
            this.crnStatus.setStatus(DualCrnStatusType.WAITING.id);
        }else {
            this.crnStatus.setTaskNoTwo(taskNo);
            this.crnStatus.setStatusTwo(CrnStatusType.MOVING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.PUT_MOVING.id);
            this.crnStatus.setTaskReceive(1);
            moveYZ(this.crnStatus.getBay(), destinationPosY, this.crnStatus.getLevel(), destinationPosZ, command.getStation());
            this.crnStatus.setStatusTwo(CrnStatusType.WAITING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.WAITING.id);
        }
        taskExecuteStation = 0;
@@ -143,48 +161,52 @@
        if(command.getStation() == 1) {
            this.crnStatus.setTaskNo(taskNo);
            this.crnStatus.setStatus(CrnStatusType.FETCH_MOVING.id);
            this.crnStatus.setStatus(DualCrnStatusType.FETCH_MOVING.id);
            this.crnStatus.setTaskReceive(1);
            moveYZ(this.crnStatus.getBay(), sourcePosY,this.crnStatus.getLevel(), sourcePosZ,command.getStation());
            this.crnStatus.setStatus(CrnStatusType.FETCHING.id);
            this.crnStatus.setStatus(DualCrnStatusType.FETCHING.id);
            sleep(2000);
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
            this.crnStatus.setLoaded(1);
            this.crnStatus.setStatus(CrnStatusType.PUT_MOVING.id);
            this.crnStatus.setStatus(DualCrnStatusType.PUT_MOVING.id);
            moveYZ(this.crnStatus.getBay(), destinationPosY,this.crnStatus.getLevel(), destinationPosZ, command.getStation());
            this.crnStatus.setStatus(CrnStatusType.PUTTING.id);
            this.crnStatus.setStatus(DualCrnStatusType.PUTTING.id);
            sleep(2000);
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
            this.crnStatus.setLoaded(0);
            this.crnStatus.setStatus(CrnStatusType.WAITING.id);
            this.crnStatus.setStatus(DualCrnStatusType.WAITING.id);
            this.station1LastCommand = command;
        }else {
            this.crnStatus.setTaskNoTwo(taskNo);
            this.crnStatus.setStatusTwo(CrnStatusType.FETCH_MOVING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.FETCH_MOVING.id);
            this.crnStatus.setTaskReceiveTwo(1);
            moveYZ(this.crnStatus.getBay(), sourcePosY,this.crnStatus.getLevel(), sourcePosZ, command.getStation());
            this.crnStatus.setStatusTwo(CrnStatusType.FETCHING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.FETCHING.id);
            sleep(2000);
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
            this.crnStatus.setLoadedTwo(1);
            this.crnStatus.setStatusTwo(CrnStatusType.PUT_MOVING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.PUT_MOVING.id);
            moveYZ(this.crnStatus.getBay(), destinationPosY,this.crnStatus.getLevel(), destinationPosZ, command.getStation());
            this.crnStatus.setStatusTwo(CrnStatusType.PUTTING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.PUTTING.id);
            sleep(2000);
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
            this.crnStatus.setLoadedTwo(0);
            this.crnStatus.setStatusTwo(CrnStatusType.WAITING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.WAITING.id);
            this.station2LastCommand = command;
        }
        taskExecuteStation = 0;
@@ -212,30 +234,34 @@
        if(command.getStation() == 1) {
            this.crnStatus.setTaskNo(taskNo);
            this.crnStatus.setStatus(CrnStatusType.FETCH_MOVING.id);
            this.crnStatus.setStatus(DualCrnStatusType.FETCH_MOVING.id);
            this.crnStatus.setTaskReceive(1);
            moveYZ(this.crnStatus.getBay(), destinationPosY,this.crnStatus.getLevel(), destinationPosZ, command.getStation());
            this.crnStatus.setStatus(CrnStatusType.FETCHING.id);
            this.crnStatus.setStatus(DualCrnStatusType.FETCHING.id);
            sleep(3000);
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
            this.crnStatus.setLoaded(1);
            this.crnStatus.setStatus(CrnStatusType.WAITING.id);
            this.crnStatus.setStatus(DualCrnStatusType.WAITING.id);
            this.station1LastCommand = command;
        }else {
            this.crnStatus.setTaskNoTwo(taskNo);
            this.crnStatus.setStatusTwo(CrnStatusType.FETCH_MOVING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.FETCH_MOVING.id);
            this.crnStatus.setTaskReceiveTwo(1);
            moveYZ(this.crnStatus.getBay(), destinationPosY,this.crnStatus.getLevel(), destinationPosZ, command.getStation());
            this.crnStatus.setStatusTwo(CrnStatusType.FETCHING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.FETCHING.id);
            sleep(3000);
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
            this.crnStatus.setLoadedTwo(1);
            this.crnStatus.setStatusTwo(CrnStatusType.WAITING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.WAITING.id);
            this.station2LastCommand = command;
        }
        taskExecuteStation = 0;
@@ -263,30 +289,34 @@
        if(command.getStation() == 1) {
            this.crnStatus.setTaskNo(taskNo);
            this.crnStatus.setStatus(CrnStatusType.PUT_MOVING.id);
            this.crnStatus.setStatus(DualCrnStatusType.PUT_MOVING.id);
            this.crnStatus.setTaskReceive(1);
            moveYZ(this.crnStatus.getBay(), destinationPosY,this.crnStatus.getLevel(), destinationPosZ, command.getStation());
            this.crnStatus.setStatus(CrnStatusType.PUTTING.id);
            this.crnStatus.setStatus(DualCrnStatusType.PUTTING.id);
            sleep(3000);
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
            this.crnStatus.setLoaded(0);
            this.crnStatus.setStatus(CrnStatusType.WAITING.id);
            this.crnStatus.setStatus(DualCrnStatusType.WAITING.id);
            this.station1LastCommand = command;
        }else {
            this.crnStatus.setTaskNoTwo(taskNo);
            this.crnStatus.setStatusTwo(CrnStatusType.PUT_MOVING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.PUT_MOVING.id);
            this.crnStatus.setTaskReceiveTwo(1);
            moveYZ(this.crnStatus.getBay(), destinationPosY,this.crnStatus.getLevel(), destinationPosZ, command.getStation());
            this.crnStatus.setStatusTwo(CrnStatusType.PUTTING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.PUTTING.id);
            sleep(3000);
            if (Thread.currentThread().isInterrupted()) {
                return;
            }
            this.crnStatus.setLoadedTwo(0);
            this.crnStatus.setStatusTwo(CrnStatusType.WAITING.id);
            this.crnStatus.setStatusTwo(DualCrnStatusType.WAITING.id);
            this.station2LastCommand = command;
        }
        taskExecuteStation = 0;
src/main/java/com/zy/core/network/fake/ZyStationFakeConnect.java
@@ -129,6 +129,10 @@
        }
        if(commandType == StationCommandType.WRITE_INFO){
            if (command.getBarcode() != null) {
                updateStationBarcode(deviceNo, stationId, command.getBarcode());
                return;
            }
            if (taskNo == 9998 && targetStationId == 0) {
                //生成出库站点仿真数据
                generateFakeOutStationData(deviceNo, stationId);
@@ -199,6 +203,23 @@
        }
    }
    private void updateStationBarcode(Integer deviceNo, Integer stationId, String barcode) {
        List<ZyStationStatusEntity> statusList = deviceStatusMap.get(deviceNo);
        if (statusList == null) {
            return;
        }
        ZyStationStatusEntity status = statusList.stream()
                .filter(item -> item.getStationId().equals(stationId)).findFirst().orElse(null);
        if (status == null) {
            return;
        }
        synchronized (status) {
            status.setBarcode(barcode);
        }
    }
    private void currentLevCommand(StationCommand command, boolean generateBarcode) {
        NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class);
        if (navigateUtils == null) {
src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java
@@ -144,12 +144,20 @@
                if (command != null) {
                    taskLastUpdateTime.put(taskNo, System.currentTimeMillis());
                    // 首次接收命令时初始化
                    if (finalTargetStationId == null) {
                        finalTargetStationId = command.getTargetStaNo();
                        if (checkTaskNoInArea(taskNo)) {
                            generateBarcode = true;
                    // 每次接收命令都刷新目标,避免沿用旧目标导致状态抖动
                    Integer commandTargetStationId = command.getTargetStaNo();
                    if (commandTargetStationId != null) {
                        if (!commandTargetStationId.equals(finalTargetStationId)) {
                            News.info("[WCS Debug] 任务{}切换目标: {} -> {}", taskNo, finalTargetStationId,
                                    commandTargetStationId);
                        }
                        finalTargetStationId = commandTargetStationId;
                        // 当前站点先同步最新目标,避免上层在窗口期重复下发同一路径
                        syncCurrentStationTarget(taskNo, currentStationId, finalTargetStationId);
                    }
                    if (!generateBarcode && checkTaskNoInArea(taskNo)) {
                        generateBarcode = true;
                    }
                    // 将新路径追加到待执行队列
@@ -157,15 +165,7 @@
                    if (newPath != null && !newPath.isEmpty()) {
                        // 获取队列中最后一个站点(用于衔接点去重)
                        Integer lastInQueue = getLastInQueue(pendingPathQueue);
                        if (lastInQueue == null) {
                            lastInQueue = currentStationId;
                        }
                        int startIndex = 0;
                        // 如果新路径的起点与当前位置或队列末尾重复,则跳过
                        if (lastInQueue != null && !newPath.isEmpty() && newPath.get(0).equals(lastInQueue)) {
                            startIndex = 1;
                        }
                        int startIndex = getPathAppendStartIndex(newPath, currentStationId, lastInQueue);
                        for (int i = startIndex; i < newPath.size(); i++) {
                            pendingPathQueue.offer(newPath.get(i));
@@ -261,7 +261,7 @@
                    // 路径队列为空,等待新的分段命令
                    if (currentStationId != null && finalTargetStationId != null
                            && currentStationId.equals(finalTargetStationId)) {
                        // 已到达最终目标,正常结束
                        // 已到达目标:立即清空当前队列并结束本轮执行,后续新命令重新创建执行线程
                        if (generateBarcode) {
                            Integer targetDeviceNo = getDeviceNoByStationId(finalTargetStationId);
                            if (targetDeviceNo != null) {
@@ -269,10 +269,13 @@
                                News.info("[WCS Debug] 任务{}到达目标{}并生成条码", taskNo, finalTargetStationId);
                            }
                        }
                        commandQueue.clear();
                        pendingPathQueue.clear();
                        News.info("[WCS Debug] 任务{}到达目标后清空队列并结束,等待后续新命令重启", taskNo);
                        break;
                    }
                    // 未到达最终目标,等待新的分段命令
                    // 继续等待新的分段命令
                    Long lastTime = taskLastUpdateTime.get(taskNo);
                    if (lastTime != null && System.currentTimeMillis() - lastTime > 30000) {
                        // 超时:30秒内没有收到新分段命令
@@ -302,6 +305,67 @@
            last = item;
        }
        return last;
    }
    /**
     * 计算新路径在队列中的追加起点,避免重复下发导致路径来回跳
     */
    private int getPathAppendStartIndex(List<Integer> newPath, Integer currentStationId, Integer lastInQueue) {
        if (newPath == null || newPath.isEmpty()) {
            return 0;
        }
        if (lastInQueue != null) {
            int idx = newPath.lastIndexOf(lastInQueue);
            if (idx >= 0) {
                return idx + 1;
            }
        }
        if (currentStationId != null) {
            int idx = newPath.lastIndexOf(currentStationId);
            if (idx >= 0) {
                return idx + 1;
            }
        }
        return 0;
    }
    /**
     * 命令刚到达时同步当前站点目标,降低上层重复发同一路径的概率
     */
    private void syncCurrentStationTarget(Integer taskNo, Integer currentStationId, Integer targetStationId) {
        if (currentStationId == null || targetStationId == null) {
            return;
        }
        Integer currentDeviceNo = getDeviceNoByStationId(currentStationId);
        if (currentDeviceNo == null) {
            return;
        }
        lockStations(currentStationId);
        try {
            List<ZyStationStatusEntity> statusList = deviceStatusMap.get(currentDeviceNo);
            if (statusList == null) {
                return;
            }
            ZyStationStatusEntity currentStatus = statusList.stream()
                    .filter(item -> item.getStationId().equals(currentStationId)).findFirst().orElse(null);
            if (currentStatus == null) {
                return;
            }
            if (currentStatus.getTaskNo() != null && currentStatus.getTaskNo() > 0
                    && !currentStatus.getTaskNo().equals(taskNo) && currentStatus.isLoading()) {
                return;
            }
            updateStationDataInternal(currentStationId, currentDeviceNo, taskNo, targetStationId, null, null, null);
        } finally {
            unlockStations(currentStationId);
        }
    }
    /**
@@ -347,6 +411,10 @@
        }
        if (command.getCommandType() == StationCommandType.WRITE_INFO) {
            if (command.getBarcode() != null) {
                updateStationBarcode(deviceNo, stationId, command.getBarcode());
                return;
            }
            if (taskNo == 9998 && targetStationId == 0) {
                // 生成出库站点仿真数据
                generateFakeOutStationData(deviceNo, stationId);
@@ -402,6 +470,23 @@
        }
    }
    private void updateStationBarcode(Integer deviceNo, Integer stationId, String barcode) {
        List<ZyStationStatusEntity> statusList = deviceStatusMap.get(deviceNo);
        if (statusList == null) {
            return;
        }
        ZyStationStatusEntity status = statusList.stream()
                .filter(item -> item.getStationId().equals(stationId)).findFirst().orElse(null);
        if (status == null) {
            return;
        }
        synchronized (status) {
            status.setBarcode(barcode);
        }
    }
    // segmentedPathCommand 方法已删除,功能已整合到 runTaskLoop
    private Integer getDeviceNoByStationId(Integer stationId) {
src/main/java/com/zy/core/network/real/ZyCrnV2RealConnect.java
@@ -109,7 +109,7 @@
                            Object lock = redisUtil.get(RedisKeyType.CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo());
                            if (lock == null) {
                                OperateResult taskClearResult = siemensNet.Write("DB100.0", array);
                                redisUtil.set(RedisKeyType.CLEAR_CRN_TASK_LIMIT.key + deviceConfig.getDeviceNo(), "lock", 3);
                                redisUtil.set(RedisKeyType.CLEAR_CRN_TASK_LIMIT.key + deviceConfig.getDeviceNo(), "lock", 1);
                            }
                        }
                    }
@@ -131,7 +131,7 @@
    @Override
    public CommandResponse sendCommand(CrnCommand command) {
        RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
        redisUtil.set(RedisKeyType.CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo(), "lock", 10);
        redisUtil.set(RedisKeyType.CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo(), "lock", 2);
        CommandResponse response = new CommandResponse(false);
        try {
            if (null == command) {
@@ -239,7 +239,7 @@
                OutputQueue.CRN.offer(MessageFormat.format("【{0}】[id:{1}] >>>>> 命令下发: {2}", DateUtils.convert(new Date()), command.getCrnNo(), JSON.toJSON(command)));
                response.setResult(true);
                response.setMessage("命令下发成功");
                redisUtil.set(RedisKeyType.CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo(), "lock", 5);
                redisUtil.set(RedisKeyType.CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo(), "lock", 1);
            } else {
                News.error("写入堆垛机plc数据失败 ===>> [id:{}]", command.getCrnNo());
                OutputQueue.CRN.offer(MessageFormat.format("【{0}】写入堆垛机plc数据失败 ===>> [id:{1}]", DateUtils.convert(new Date()), command.getCrnNo()));
src/main/java/com/zy/core/network/real/ZyDualCrnRealConnect.java
@@ -17,6 +17,8 @@
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
import com.zy.core.cache.OutputQueue;
import com.zy.core.enums.CrnTaskModeType;
import com.zy.core.enums.DualCrnTaskModeType;
import com.zy.core.enums.RedisKeyType;
import com.zy.core.model.CommandResponse;
import com.zy.core.model.command.DualCrnCommand;
@@ -25,6 +27,7 @@
import java.text.MessageFormat;
import java.util.Date;
import java.util.HashMap;
public class ZyDualCrnRealConnect implements ZyDualCrnConnectApi {
@@ -97,20 +100,33 @@
                crnStatus.setXDuration((double) siemensNet.getByteTransform().TransSingle(result.Content, 62));
                crnStatus.setYDuration((double) siemensNet.getByteTransform().TransSingle(result.Content, 66));
                if ((crnStatus.getStatus() == 0 || crnStatus.getStatus() == 20) && crnStatus.getTaskReceive() == 0) {
                    OperateResultExOne<byte[]> taskResult = siemensNet.Read("DB100.0", (short) 18);
                    if (taskResult.IsSuccess) {
                        short taskNo = siemensNet.getByteTransform().TransInt16(taskResult.Content, 0);
                        short taskMode = siemensNet.getByteTransform().TransInt16(taskResult.Content, 2);
                        short sourcePosX = siemensNet.getByteTransform().TransInt16(taskResult.Content, 4);
                        short sourcePosY = siemensNet.getByteTransform().TransInt16(taskResult.Content, 6);
                        short sourcePosZ = siemensNet.getByteTransform().TransInt16(taskResult.Content, 8);
                        short destinationPosX = siemensNet.getByteTransform().TransInt16(taskResult.Content, 10);
                        short destinationPosY = siemensNet.getByteTransform().TransInt16(taskResult.Content, 12);
                        short destinationPosZ = siemensNet.getByteTransform().TransInt16(taskResult.Content, 14);
                        short confirm = siemensNet.getByteTransform().TransInt16(taskResult.Content, 16);
                OperateResultExOne<byte[]> taskResult = siemensNet.Read("DB100.0", (short) 18);
                if (taskResult.IsSuccess) {
                    short taskNo = siemensNet.getByteTransform().TransInt16(taskResult.Content, 0);
                    short taskMode = siemensNet.getByteTransform().TransInt16(taskResult.Content, 2);
                    short sourcePosX = siemensNet.getByteTransform().TransInt16(taskResult.Content, 4);
                    short sourcePosY = siemensNet.getByteTransform().TransInt16(taskResult.Content, 6);
                    short sourcePosZ = siemensNet.getByteTransform().TransInt16(taskResult.Content, 8);
                    short destinationPosX = siemensNet.getByteTransform().TransInt16(taskResult.Content, 10);
                    short destinationPosY = siemensNet.getByteTransform().TransInt16(taskResult.Content, 12);
                    short destinationPosZ = siemensNet.getByteTransform().TransInt16(taskResult.Content, 14);
                    short confirm = siemensNet.getByteTransform().TransInt16(taskResult.Content, 16);
                        if(taskNo != 0 || taskMode != 0 || sourcePosX != 0 || sourcePosY != 0 || sourcePosZ != 0 || destinationPosX != 0 || destinationPosY != 0 || destinationPosZ != 0 || confirm != 0) {
                    int hasData = 0;
                    if(taskNo != 0 || taskMode != 0 || sourcePosX != 0 || sourcePosY != 0 || sourcePosZ != 0 || destinationPosX != 0 || destinationPosY != 0 || destinationPosZ != 0 || confirm != 0) {
                        hasData = 1;
                        if (crnStatus.getTaskReceive() == 1) {
                            HashMap<String, Object> map = new HashMap<>();
                            map.put("taskNo", taskNo);
                            map.put("taskMode", taskMode);
                            map.put("sourcePosX", sourcePosX);
                            map.put("sourcePosY", sourcePosY);
                            map.put("sourcePosZ", sourcePosZ);
                            map.put("destinationPosX", destinationPosX);
                            map.put("destinationPosY", destinationPosY);
                            map.put("destinationPosZ", destinationPosZ);
                            map.put("confirm", confirm);
                            short[] array = new short[9];
                            array[0] = (short) 0;
                            array[1] = (short) 0;
@@ -122,30 +138,57 @@
                            array[7] = (short) 0;
                            array[8] = (short) 0;
                            RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
                            Object lock = redisUtil.get(RedisKeyType.DUAL_CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo());
                            if (lock == null) {
                                OperateResult taskClearResult = siemensNet.Write("DB100.0", array);
                                redisUtil.set(RedisKeyType.CLEAR_DUAL_CRN_TASK_LIMIT.key + deviceConfig.getDeviceNo(), "lock", 3);
                            boolean clear = false;
                            if (taskMode == DualCrnTaskModeType.CONFIRM.id) {
//                                if (crnStatus.getStatus() == 0) {
//                                    clear = true;
//                                }
                                clear = true;
                            } else {
                                clear = true;
                            }
                            if (clear) {
                                RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
                                Object lock = redisUtil.get(RedisKeyType.DUAL_CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo());
                                if (lock == null) {
                                    OperateResult taskClearResult = siemensNet.Write("DB100.0", array);
                                    redisUtil.set(RedisKeyType.CLEAR_DUAL_CRN_TASK_LIMIT.key + deviceConfig.getDeviceNo(), "lock", 3);
                                    News.info("双工位堆垛机:{}号{}工位命令区域清空,原始数据:{}", deviceConfig.getDeviceNo(), 1, JSON.toJSONString(map));
                                }
                            }
                        }
                    }
                    crnStatus.setTaskSend(hasData);
                }
                if ((crnStatus.getStatusTwo() == 0 || crnStatus.getStatusTwo() == 20) && crnStatus.getTaskReceiveTwo() == 0) {
                    OperateResultExOne<byte[]> taskResult = siemensNet.Read("DB100.18", (short) 18);
                    if (taskResult.IsSuccess) {
                        short taskNo = siemensNet.getByteTransform().TransInt16(taskResult.Content, 0);
                        short taskMode = siemensNet.getByteTransform().TransInt16(taskResult.Content, 2);
                        short sourcePosX = siemensNet.getByteTransform().TransInt16(taskResult.Content, 4);
                        short sourcePosY = siemensNet.getByteTransform().TransInt16(taskResult.Content, 6);
                        short sourcePosZ = siemensNet.getByteTransform().TransInt16(taskResult.Content, 8);
                        short destinationPosX = siemensNet.getByteTransform().TransInt16(taskResult.Content, 10);
                        short destinationPosY = siemensNet.getByteTransform().TransInt16(taskResult.Content, 12);
                        short destinationPosZ = siemensNet.getByteTransform().TransInt16(taskResult.Content, 14);
                        short confirm = siemensNet.getByteTransform().TransInt16(taskResult.Content, 16);
                OperateResultExOne<byte[]> taskResult2 = siemensNet.Read("DB100.18", (short) 18);
                if (taskResult2.IsSuccess) {
                    short taskNo = siemensNet.getByteTransform().TransInt16(taskResult2.Content, 0);
                    short taskMode = siemensNet.getByteTransform().TransInt16(taskResult2.Content, 2);
                    short sourcePosX = siemensNet.getByteTransform().TransInt16(taskResult2.Content, 4);
                    short sourcePosY = siemensNet.getByteTransform().TransInt16(taskResult2.Content, 6);
                    short sourcePosZ = siemensNet.getByteTransform().TransInt16(taskResult2.Content, 8);
                    short destinationPosX = siemensNet.getByteTransform().TransInt16(taskResult2.Content, 10);
                    short destinationPosY = siemensNet.getByteTransform().TransInt16(taskResult2.Content, 12);
                    short destinationPosZ = siemensNet.getByteTransform().TransInt16(taskResult2.Content, 14);
                    short confirm = siemensNet.getByteTransform().TransInt16(taskResult2.Content, 16);
                        if(taskNo != 0 || taskMode != 0 || sourcePosX != 0 || sourcePosY != 0 || sourcePosZ != 0 || destinationPosX != 0 || destinationPosY != 0 || destinationPosZ != 0 || confirm != 0) {
                    int hasData = 0;
                    if(taskNo != 0 || taskMode != 0 || sourcePosX != 0 || sourcePosY != 0 || sourcePosZ != 0 || destinationPosX != 0 || destinationPosY != 0 || destinationPosZ != 0 || confirm != 0) {
                        hasData = 1;
                        if (crnStatus.getTaskReceiveTwo() == 1) {
                            HashMap<String, Object> map = new HashMap<>();
                            map.put("taskNo", taskNo);
                            map.put("taskMode", taskMode);
                            map.put("sourcePosX", sourcePosX);
                            map.put("sourcePosY", sourcePosY);
                            map.put("sourcePosZ", sourcePosZ);
                            map.put("destinationPosX", destinationPosX);
                            map.put("destinationPosY", destinationPosY);
                            map.put("destinationPosZ", destinationPosZ);
                            map.put("confirm", confirm);
                            short[] array = new short[9];
                            array[0] = (short) 0;
                            array[1] = (short) 0;
@@ -157,13 +200,28 @@
                            array[7] = (short) 0;
                            array[8] = (short) 0;
                            RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
                            Object lock = redisUtil.get(RedisKeyType.DUAL_CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo());
                            if (lock == null) {
                                OperateResult taskClearResult = siemensNet.Write("DB100.18", array);
                            boolean clear = false;
                            if (taskMode == DualCrnTaskModeType.CONFIRM.id) {
//                                if (crnStatus.getStatusTwo() == 0) {
//                                    clear = true;
//                                }
                                clear = true;
                            } else {
                                clear = true;
                            }
                            if (clear) {
                                RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
                                Object lock = redisUtil.get(RedisKeyType.DUAL_CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo());
                                if (lock == null) {
                                    OperateResult taskClearResult = siemensNet.Write("DB100.18", array);
                                    redisUtil.set(RedisKeyType.CLEAR_DUAL_CRN_TASK_LIMIT.key + deviceConfig.getDeviceNo(), "lock", 3);
                                    News.info("双工位堆垛机:{}号{}工位命令区域清空,原始数据:{}", deviceConfig.getDeviceNo(), 2, JSON.toJSONString(map));
                                }
                            }
                        }
                    }
                    crnStatus.setTaskSendTwo(hasData);
                }
                return crnStatus;
@@ -182,7 +240,7 @@
    @Override
    public CommandResponse sendCommand(DualCrnCommand command) {
        RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
        redisUtil.set(RedisKeyType.DUAL_CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo(), "lock", 10);
        redisUtil.set(RedisKeyType.DUAL_CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo(), "lock", 3);
        CommandResponse response = new CommandResponse(false);
         try {
             if (null == command) {
@@ -261,7 +319,7 @@
                 OutputQueue.CRN.offer(MessageFormat.format("【{0}】[id:{1}] >>>>> 命令下发: {2}", DateUtils.convert(new Date()), command.getCrnNo(), JSON.toJSON(command)));
                 response.setResult(true);
                 response.setMessage("命令下发成功");
                 redisUtil.set(RedisKeyType.DUAL_CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo(), "lock", 5);
                 redisUtil.set(RedisKeyType.DUAL_CRN_SEND_COMMAND_LOCK.key + deviceConfig.getDeviceNo(), "lock", 3);
             } else {
                 News.error("双工位堆垛机写入堆垛机plc数据失败 ===>> [id:{}]", command.getCrnNo());
                 OutputQueue.CRN.offer(MessageFormat.format("【{0}】写入堆垛机plc数据失败 ===>> [id:{1}]", DateUtils.convert(new Date()), command.getCrnNo()));
src/main/java/com/zy/core/network/real/ZyStationV3RealConnect.java
@@ -146,7 +146,8 @@
                if (barcodeEntity == null) {
                    continue;
                }
                String barcode = siemensNet.getByteTransform().TransString(result2.Content, i * 16 + 2, 14, "UTF-8");
                byte length = 10;//result2.Content[1];
                String barcode = siemensNet.getByteTransform().TransString(result2.Content, i * 16 + 2, length, "UTF-8");
                barcode = barcode.trim();
                barcodeEntity.setBarcode(barcode);
            }
src/main/java/com/zy/core/plugin/FakeProcess.java
@@ -125,6 +125,12 @@
        stationOperateProcessUtils.crnStationOutExecute();
        // 检测输送站点出库任务执行完成
        stationOperateProcessUtils.stationOutExecuteFinish();
        // 检测任务转完成
        stationOperateProcessUtils.checkTaskToComplete();
        // 检测出库排序
        stationOperateProcessUtils.checkStationOutOrder();
        // 监控绕圈站点
        stationOperateProcessUtils.watchCircleStation();
        // 执行双工位堆垛机任务
        dualCrnOperateProcessUtils.dualCrnIoExecute();
@@ -349,64 +355,68 @@
    // 生成仿真模拟出库任务
    private synchronized void generateFakeOutTask() {
        if (!enableFake.equals("Y")) {
            return;
        }
        if (fakeRealTaskRequestWms.equals("Y")) {
            return;
        }
        if (!fakeGenerateOutTask.equals("Y")) {
            return;
        }
        List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<>());
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
            if (stationThread == null) {
                continue;
        try {
            if (!enableFake.equals("Y")) {
                return;
            }
            Map<Integer, StationProtocol> stationMap = stationThread.getStatusMap();
            if (fakeRealTaskRequestWms.equals("Y")) {
                return;
            }
            List<StationObjModel> list = basDevp.getOutStationList$();
            for (StationObjModel entity : list) {
                Integer stationId = entity.getStationId();
                if (!stationMap.containsKey(stationId)) {
            if (!fakeGenerateOutTask.equals("Y")) {
                return;
            }
            List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<>());
            for (BasDevp basDevp : basDevps) {
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
                if (stationThread == null) {
                    continue;
                }
                StationProtocol stationProtocol = stationMap.get(stationId);
                if (stationProtocol == null) {
                    continue;
                }
                Map<Integer, StationProtocol> stationMap = stationThread.getStatusMap();
                Object object = redisUtil.get(RedisKeyType.GENERATE_FAKE_OUT_TASK_LIMIT.key + stationId);
                if (object != null) {
                    return;
                }
                // 满足自动、无物、工作号0,生成出库数据
                if (stationProtocol.isAutoing()
                        && !stationProtocol.isLoading()
                        && stationProtocol.getTaskNo() == 0) {
                    List<LocMast> locMastList = locMastService
                            .selectList(new EntityWrapper<LocMast>().eq("loc_sts", String.valueOf(LocStsType.F)));
                    if (locMastList.isEmpty()) {
                List<StationObjModel> list = basDevp.getOutStationList$();
                for (StationObjModel entity : list) {
                    Integer stationId = entity.getStationId();
                    if (!stationMap.containsKey(stationId)) {
                        continue;
                    }
                    LocMast locMast = locMastList.get(0);
                    StationProtocol stationProtocol = stationMap.get(stationId);
                    if (stationProtocol == null) {
                        continue;
                    }
                    CreateOutTaskParam taskParam = new CreateOutTaskParam();
                    taskParam.setTaskNo(String.valueOf(commonService.getWorkNo(WrkIoType.OUT.id)));
                    taskParam.setStaNo(stationId);
                    taskParam.setLocNo(locMast.getLocNo());
                    boolean result = commonService.createOutTask(taskParam);
                    redisUtil.set(RedisKeyType.GENERATE_FAKE_OUT_TASK_LIMIT.key + stationId, "lock", 10);
                    Object object = redisUtil.get(RedisKeyType.GENERATE_FAKE_OUT_TASK_LIMIT.key + stationId);
                    if (object != null) {
                        return;
                    }
                    // 满足自动、无物、工作号0,生成出库数据
                    if (stationProtocol.isAutoing()
                            && !stationProtocol.isLoading()
                            && stationProtocol.getTaskNo() == 0) {
                        List<LocMast> locMastList = locMastService
                                .selectList(new EntityWrapper<LocMast>().eq("loc_sts", String.valueOf(LocStsType.F)));
                        if (locMastList.isEmpty()) {
                            continue;
                        }
                        LocMast locMast = locMastList.get(0);
                        CreateOutTaskParam taskParam = new CreateOutTaskParam();
                        taskParam.setTaskNo(String.valueOf(commonService.getWorkNo(WrkIoType.OUT.id)));
                        taskParam.setStaNo(stationId);
                        taskParam.setLocNo(locMast.getLocNo());
                        boolean result = commonService.createOutTask(taskParam);
                        redisUtil.set(RedisKeyType.GENERATE_FAKE_OUT_TASK_LIMIT.key + stationId, "lock", 10);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
@@ -477,7 +487,7 @@
                        if (lock != null) {
                            continue;
                        }
                        redisUtil.set(RedisKeyType.GENERATE_IN_TASK_LIMIT.key + stationId, "lock", 10);
                        redisUtil.set(RedisKeyType.GENERATE_IN_TASK_LIMIT.key + stationId, "lock", 3);
                        String barcode = stationProtocol.getBarcode();
                        Integer stationIdVal = stationProtocol.getStationId();
@@ -539,7 +549,7 @@
                                        stationProtocol.getPalletHeight());
                                redisUtil.set(RedisKeyType.GENERATE_IN_TASK_LIMIT.key + stationId, "lock", 2);
                                stationProtocol.setSystemWarning("请求入库失败,WMS无返回");
//                                stationProtocol.setSystemWarning("请求入库失败,WMS无返回");
                            }
                        }
                    }
@@ -596,7 +606,7 @@
                    continue;
                }
                if (System.currentTimeMillis() - stayTime > 1000 * 15) {
                if (System.currentTimeMillis() - stayTime > 1000 * 60) {
                    StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp,
                            stationObjModel.getDeviceNo());
                    if (stationThread == null) {
@@ -801,7 +811,7 @@
                wrkMast.setSystemMsg("");
                wrkMast.setIoTime(new Date());
                if (wrkMastService.updateById(wrkMast)) {
                    CrnCommand resetCommand = crnThread.getResetCommand(crnProtocol.getCrnNo());
                    CrnCommand resetCommand = crnThread.getResetCommand(crnProtocol.getTaskNo(), crnProtocol.getCrnNo());
                    MessageQueue.offer(SlaveType.Crn, crnProtocol.getCrnNo(), new Task(2, resetCommand));
                    News.info("堆垛机任务状态更新成功,堆垛机号={},工作号={}", basCrnp.getCrnNo(), crnProtocol.getTaskNo());
                }
src/main/java/com/zy/core/plugin/NormalProcess.java
@@ -190,7 +190,7 @@
                                wmsOperateUtils.applyInTaskAsync(barcode, stationIdVal,
                                        stationProtocol.getPalletHeight());
                                redisUtil.set(RedisKeyType.GENERATE_IN_TASK_LIMIT.key + stationId, "lock", 2);
                                stationProtocol.setSystemWarning("请求入库失败,WMS无返回");
//                                stationProtocol.setSystemWarning("请求入库失败,WMS无返回");
                            }
                        }
                    }
src/main/java/com/zy/core/plugin/XiaosongProcess.java
@@ -77,6 +77,8 @@
        stationOperateProcessUtils.dualCrnStationOutExecute();
        //检测输送站点出库任务执行完成
        stationOperateProcessUtils.stationOutExecuteFinish();
        // 检测任务转完成
        stationOperateProcessUtils.checkTaskToComplete();
        //检测输送站点是否运行堵塞
        stationOperateProcessUtils.checkStationRunBlock();
@@ -140,6 +142,10 @@
                        }
                        if (stationProtocol.getError() > 0) {
                            Object lock = redisUtil.get(RedisKeyType.GENERATE_STATION_BACK_LIMIT.key + stationProtocol.getTaskNo());
                            if (lock != null) {
                                continue;
                            }
                            StationObjModel backStation = entity.getBackStation();
                            StationCommand command = stationThread.getCommand(StationCommandType.MOVE, commonService.getWorkNo(WrkIoType.STATION_BACK.id), stationId, backStation.getStationId(), 0);
                            if (command == null) {
@@ -148,11 +154,14 @@
                            }
                            MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command));
                            News.taskInfo(stationProtocol.getTaskNo(), "{}扫码异常,已退回至{}", backStation.getStationId());
                            redisUtil.set(RedisKeyType.GENERATE_STATION_BACK_LIMIT.key + stationProtocol.getTaskNo(), "lock", 10);
                                continue;
                        }
                        // 检测任务是否生成
                        List<WrkMast> wrkMasts = wrkMastService
                                .selectList(new EntityWrapper<WrkMast>().eq("barcode", stationProtocol.getBarcode()));
                                .selectList(new EntityWrapper<WrkMast>()
                                        .eq("barcode", stationProtocol.getBarcode()));
                        if (!wrkMasts.isEmpty()) {
                            continue;
                        }
@@ -210,7 +219,7 @@
                                wmsOperateUtils.applyInTaskAsync(barcode, stationIdVal,
                                        stationProtocol.getPalletHeight());
                                redisUtil.set(RedisKeyType.GENERATE_IN_TASK_LIMIT.key + stationId, "lock", 2);
                                stationProtocol.setSystemWarning("请求入库失败,WMS无返回");
//                                stationProtocol.setSystemWarning("请求入库失败,WMS无返回");
                            }
                        }
                    }
src/main/java/com/zy/core/task/InitLocMapScheduler.java
@@ -53,9 +53,14 @@
                mapNode.put("id", i + "-" + j);
                String nodeType = map.getString("type");
                mapNode.put("type", nodeType);
                if("shelf".equals(nodeType)) {
                    mapNode.put("value", MapNodeType.NORMAL_PATH.id);
                }else {
                } else if ("devp".equals(nodeType)) {
                    mapNode.put("value", MapNodeType.DISABLE.id);
                } else if ("crn".equals(nodeType) || "dualCrn".equals(nodeType) || "rgv".equals(nodeType)) {
                    mapNode.put("value", MapNodeType.MAIN_PATH.id);
                } else {
                    mapNode.put("value", MapNodeType.DISABLE.id);
                }
src/main/java/com/zy/core/thread/CrnThread.java
@@ -13,7 +13,7 @@
    CrnCommand getMoveCommand(String targetLocNo, Integer taskNo, Integer crnNo);//移动
    CrnCommand getResetCommand(Integer crnNo);//复位
    CrnCommand getResetCommand(Integer taskNo, Integer crnNo);//复位
    CommandResponse sendCommand(CrnCommand command);//下发命令
src/main/java/com/zy/core/thread/DualCrnThread.java
@@ -17,7 +17,7 @@
    DualCrnCommand getMoveCommand(String targetLocNo, Integer taskNo, Integer crnNo);//移动
    DualCrnCommand getResetCommand(Integer crnNo, Integer station);//复位
    DualCrnCommand getResetCommand(Integer taskNo, Integer crnNo, Integer station);//复位
    CommandResponse sendCommand(DualCrnCommand command);//下发命令
src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java
@@ -283,10 +283,10 @@
    }
    @Override
    public CrnCommand getResetCommand(Integer crnNo) {
    public CrnCommand getResetCommand(Integer taskNo, Integer crnNo) {
        CrnCommand crnCommand = new CrnCommand();
        crnCommand.setCrnNo(crnNo); // 堆垛机编号
        crnCommand.setTaskNo(0); // 工作号
        crnCommand.setTaskNo(taskNo); // 工作号
        crnCommand.setAckFinish(1);  // 任务完成确认位
        crnCommand.setTaskMode(CrnTaskModeType.NONE.id); // 任务模式
        crnCommand.setSourcePosX(0);     // 源库位排
src/main/java/com/zy/core/thread/impl/ZySiemensCrnV2Thread.java
@@ -283,10 +283,10 @@
    }
    @Override
    public CrnCommand getResetCommand(Integer crnNo) {
    public CrnCommand getResetCommand(Integer taskNo, Integer crnNo) {
        CrnCommand crnCommand = new CrnCommand();
        crnCommand.setCrnNo(crnNo); // 堆垛机编号
        crnCommand.setTaskNo(0); // 工作号
        crnCommand.setTaskNo(taskNo); // 工作号
        crnCommand.setAckFinish(1);  // 任务完成确认位
        crnCommand.setTaskMode(CrnTaskModeType.RESET.id); // 任务模式
        crnCommand.setSourcePosX(0);     // 源库位排
src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java
@@ -9,6 +9,7 @@
import com.zy.asrs.entity.*;
import com.zy.asrs.service.BasDualCrnpOptService;
import com.zy.asrs.service.BasDualCrnpService;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.utils.Utils;
import com.zy.common.utils.RedisUtil;
import com.zy.core.News;
@@ -48,7 +49,7 @@
    private RedisUtil redisUtil;
    private ZyDualCrnConnectDriver zyDualCrnConnectDriver;
    private DualCrnProtocol crnProtocol;
    private int deviceLogCollectTime = 200;
    private volatile int deviceLogCollectTime = 200;
    public ZySiemensDualCrnThread(DeviceConfig deviceConfig, RedisUtil redisUtil) {
        this.deviceConfig = deviceConfig;
@@ -66,7 +67,7 @@
                try {
                    deviceLogCollectTime = Utils.getDeviceLogCollectTime();
                    readStatus();
                    Thread.sleep(50);
                    Thread.sleep(100);
                } catch (Exception e) {
                    log.error("DualCrnThread Fail", e);
                }
@@ -87,10 +88,14 @@
                        List<SendDualCrnCommandParam> commandList = (List<SendDualCrnCommandParam>) task.getData();
                        for (SendDualCrnCommandParam sendDualCrnCommandParam : commandList) {
                            DualCrnCommand dualCrnCommand = sendDualCrnCommandParam.getCommands().get(0);
                            redisUtil.set(RedisKeyType.DUAL_CRN_COMMAND_IDX.key + dualCrnCommand.getTaskNo(), sendDualCrnCommandParam.getCommandIdx(), 60 * 60 * 24);
                            redisUtil.set(RedisKeyType.DUAL_CRN_COMMAND_IDX.key + dualCrnCommand.getTaskNo(), 0, 60 * 60 * 24);
                            redisUtil.set(RedisKeyType.DUAL_CRN_COMMAND_.key + dualCrnCommand.getTaskNo(), JSON.toJSONString(sendDualCrnCommandParam, SerializerFeature.DisableCircularReferenceDetect), 60 * 60 * 24);
                            if (sendDualCrnCommandParam.getStation() == 1) {
                                redisUtil.set(RedisKeyType.DUAL_CRN_STATION1_FLAG.key + crnProtocol.getCrnNo(), dualCrnCommand.getTaskNo(), 60 * 60 * 24);
                            } else {
                                redisUtil.set(RedisKeyType.DUAL_CRN_STATION2_FLAG.key + crnProtocol.getCrnNo(), dualCrnCommand.getTaskNo(), 60 * 60 * 24);
                            }
                        }
                        redisUtil.set(RedisKeyType.DUAL_CRN_COMMAND_.key + crnProtocol.getCrnNo(), JSON.toJSONString(commandList, SerializerFeature.DisableCircularReferenceDetect), 60 * 60 * 24);
                    } else if (step == 3) {
                        sendCommand((DualCrnCommand) task.getData());
                    }
@@ -106,11 +111,13 @@
        Thread commandThread = new Thread(() -> {
            while (true) {
                try {
                    if(crnProtocol.getMode() != DualCrnModeType.AUTO.id) {
                    Thread.sleep(100);
                    if (crnProtocol.getMode() != DualCrnModeType.AUTO.id) {
                        continue;
                    }
                    if(crnProtocol.getAlarm() != 0) {
                    if (crnProtocol.getAlarm() != 0) {
                        continue;
                    }
@@ -120,16 +127,11 @@
                        continue;
                    }
                    Object commandListObj = redisUtil.get(RedisKeyType.DUAL_CRN_COMMAND_.key + crnProtocol.getCrnNo());
                    if (commandListObj == null) {
                        continue;
                    }
                    List<SendDualCrnCommandParam> commandList = JSON.parseArray(commandListObj.toString(), SendDualCrnCommandParam.class);
                    List<SendDualCrnCommandParam> commandList = getDualCrnCommandList();
                    for (SendDualCrnCommandParam commandParam : commandList) {
                        processStation(commandParam);
                    }
                    Thread.sleep(100);
                } catch (Exception e) {
                    log.error("DualCrnCommand Fail", e);
                }
@@ -143,7 +145,7 @@
        List<DualCrnCommand> commandList = commandParam.getCommands();
        DualCrnCommand firstCommand = commandList.get(0);
        Object idxObj = redisUtil.get(RedisKeyType.DUAL_CRN_COMMAND_IDX.key + firstCommand.getTaskNo());
        if(idxObj == null) {
        if (idxObj == null) {
            return;
        }
        Integer idx = (Integer) idxObj;
@@ -153,29 +155,61 @@
        DualCrnCommand dualCommand = commandList.get(idx);
        boolean send = false;
        //TODO 判断站点1,2任务类型是否一致
        if (station == 1) {
            if (crnProtocol.getStatus().equals(DualCrnStatusType.FETCH_COMPLETE.id)
                    || crnProtocol.getStatus().equals(DualCrnStatusType.IDLE.id)
            ) {
                send = true;
            log.info("工位2任务==========>{}, 任务模式---->{}", crnProtocol.getTaskNoTwo(), crnProtocol.getMode());
            log.info("工位1任务==========>{}, 任务模式---->{}", crnProtocol.getTaskNo(), crnProtocol.getMode());
            Integer taskNo = crnProtocol.getTaskNoTwo();
            if (crnProtocol.getTaskSend() == 0) {
                //两个工位只允许放相同类型任务
//                if (taskNo != null && taskNo > 0) {
//                    WrkMastService mastService = SpringUtils.getBean(WrkMastService.class);
//                    WrkMast wrkMast = mastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", taskNo));
//                    WrkMast wrkMast1 = mastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", dualCommand.getTaskNo()));
//                    if (!Objects.isNull(wrkMast) && !Objects.isNull(wrkMast1)) {
//                        if (wrkMast1.getIoType() != wrkMast.getIoType()) {
////                            return;
//                        }
//                    }
//                }
                // && sendComm(taskNo, dualCommand.getTaskNo())
                if (dualCommand.getTaskMode().intValue() == DualCrnTaskModeType.PICK.id) {
                    if (crnProtocol.getStatus().equals(DualCrnStatusType.IDLE.id)) {
                        send = true;
                    }
                } else if (dualCommand.getTaskMode().intValue() == DualCrnTaskModeType.PUT.id) {
                    if (crnProtocol.getStatus().equals(DualCrnStatusType.FETCH_COMPLETE.id)) {
                        send = true;
                    }
                }
            }
        }else {
            if (crnProtocol.getStatusTwo().equals(DualCrnStatusType.FETCH_COMPLETE.id)
                    || crnProtocol.getStatusTwo().equals(DualCrnStatusType.IDLE.id)
            ) {
                send = true;
        } else {
            log.info("工位1任务==========>{}, 任务模式---->{}", crnProtocol.getTaskNo(), crnProtocol.getMode());
            log.info("工位2任务==========>{}, 任务模式---->{}", crnProtocol.getTaskNoTwo(), crnProtocol.getMode());
            Integer taskNo = crnProtocol.getTaskNo();
            // && sendComm(taskNo, dualCommand.getTaskNo())
            if (crnProtocol.getTaskSendTwo() == 0) {
                if (dualCommand.getTaskMode().intValue() == DualCrnTaskModeType.PICK.id) {
                    if (crnProtocol.getStatusTwo().equals(DualCrnStatusType.IDLE.id)) {
                        send = true;
                    }
                } else if (dualCommand.getTaskMode().intValue() == DualCrnTaskModeType.PUT.id) {
                    if (crnProtocol.getStatusTwo().equals(DualCrnStatusType.FETCH_COMPLETE.id)) {
                        send = true;
                    }
                }
            }
        }
        if (idx == 0) {
            if(send) {
            if (send) {
                idx++;
                redisUtil.set(RedisKeyType.DUAL_CRN_COMMAND_IDX.key + firstCommand.getTaskNo(), idx, 60 * 60 * 24);
                sendCommand(dualCommand);
                redisUtil.set(RedisKeyType.DUAL_CRN_PICK_WAIT_NEXT_TASK.key + crnProtocol.getCrnNo(), "lock", 5);
                redisUtil.set(RedisKeyType.DUAL_CRN_PICK_WAIT_NEXT_TASK.key + crnProtocol.getCrnNo(), "lock", 3);
            }
            return;
        }else {
        } else {
            if (dualCommand.getTaskMode() == DualCrnTaskModeType.PUT.id.shortValue()) {
                //等待下一个任务
                Object wait = redisUtil.get(RedisKeyType.DUAL_CRN_PICK_WAIT_NEXT_TASK.key + crnProtocol.getCrnNo());
@@ -213,13 +247,29 @@
                }
            }
            if(send) {
            if (send) {
                idx++;
                redisUtil.set(RedisKeyType.DUAL_CRN_COMMAND_IDX.key + firstCommand.getTaskNo(), idx, 60 * 60 * 24);
                sendCommand(dualCommand);
            }
            return;
        }
    }
    public boolean sendComm(Integer taskNo, Integer current) {
        //两个工位只允许放相同类型任务
        if (taskNo != null && taskNo > 0) {
            WrkMastService mastService = SpringUtils.getBean(WrkMastService.class);
            WrkMast wrkMast = mastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", taskNo));
            WrkMast wrkMast1 = mastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", current));
            if (!Objects.isNull(wrkMast) && !Objects.isNull(wrkMast1)) {
                if (wrkMast1.getIoType() != wrkMast.getIoType()) {
                    return false;
                }
            }
        }
        return true;
    }
    /**
@@ -234,6 +284,7 @@
        //工位1
        crnProtocol.setTaskNo(0);
        crnProtocol.setDeviceTaskNo(0);
        crnProtocol.setStatus(-1);
        crnProtocol.setBay(0);
        crnProtocol.setLevel(0);
@@ -244,6 +295,7 @@
        //工位2
        crnProtocol.setTaskNoTwo(0);
        crnProtocol.setDeviceTaskNoTwo(0);
        crnProtocol.setStatusTwo(-1);
        crnProtocol.setBayTwo(0);
        crnProtocol.setLevelTwo(0);
@@ -264,12 +316,12 @@
        BasDualCrnpService basDualCrnpService = null;
        try {
            basDualCrnpService = SpringUtils.getBean(BasDualCrnpService.class);
        }catch (Exception e){
        } catch (Exception e) {
        }
        if (basDualCrnpService != null) {
            BasDualCrnp basDualCrnp = basDualCrnpService.selectOne(new EntityWrapper<BasDualCrnp>().eq("crn_no", deviceConfig.getDeviceNo()));
            if(basDualCrnp == null) {
            if (basDualCrnp == null) {
                basDualCrnp = new BasDualCrnp();
                basDualCrnp.setCrnNo(deviceConfig.getDeviceNo());
                basDualCrnp.setStatus(1);
@@ -294,7 +346,7 @@
    /**
     * 读取状态
     */
    private void readStatus(){
    private void readStatus() {
        ZyDualCrnStatusEntity crnStatus = zyDualCrnConnectDriver.getStatus();
        if (crnStatus == null) {
            OutputQueue.DUAL_CRN.offer(MessageFormat.format("【{0}】读取双工位堆垛机plc状态信息失败 ===>> [id:{1}] [ip:{2}] [port:{3}]", DateUtils.convert(new Date()), deviceConfig.getDeviceNo(), deviceConfig.getIp(), deviceConfig.getPort()));
@@ -303,18 +355,20 @@
        crnProtocol.setMode(crnStatus.getMode());
        //工位1
        crnProtocol.setTaskNo(crnStatus.getTaskNo());
        crnProtocol.setDeviceTaskNo(crnStatus.getTaskNo());
        crnProtocol.setStatus(crnStatus.getStatus());
        crnProtocol.setForkPos(crnStatus.getForkPos());
        crnProtocol.setLoaded(crnStatus.getLoaded());
        crnProtocol.setTaskReceive(crnStatus.getTaskReceive());
        crnProtocol.setTaskSend(crnStatus.getTaskSend());
        //工位2
        crnProtocol.setTaskNoTwo(crnStatus.getTaskNoTwo());
        crnProtocol.setDeviceTaskNoTwo(crnStatus.getTaskNoTwo());
        crnProtocol.setStatusTwo(crnStatus.getStatusTwo());
        crnProtocol.setForkPosTwo(crnStatus.getForkPosTwo());
        crnProtocol.setLoadedTwo(crnStatus.getLoadedTwo());
        crnProtocol.setTaskReceiveTwo(crnStatus.getTaskReceiveTwo());
        crnProtocol.setTaskSendTwo(crnStatus.getTaskSendTwo());
        crnProtocol.setBay(crnStatus.getBay());
        crnProtocol.setLevel(crnStatus.getLevel());
@@ -335,7 +389,7 @@
        crnProtocol.setGoodsType(crnStatus.getGoodsType());
        crnProtocol.setBarcode(crnStatus.getBarcode());
        OutputQueue.DUAL_CRN.offer(MessageFormat.format("【{0}】[id:{1}] <<<<< 实时数据更新成功",DateUtils.convert(new Date()), deviceConfig.getDeviceNo()));
        OutputQueue.DUAL_CRN.offer(MessageFormat.format("【{0}】[id:{1}] <<<<< 实时数据更新成功", DateUtils.convert(new Date()), deviceConfig.getDeviceNo()));
        if (crnProtocol.getAlarm() > 0) {
            crnProtocol.setLastCommandTime(-1L);
@@ -358,7 +412,7 @@
            //更新采集时间
            crnProtocol.setDeviceDataLog(System.currentTimeMillis());
        }
        try {
            BasDualCrnpErrLogService errLogService = SpringUtils.getBean(BasDualCrnpErrLogService.class);
            if (errLogService != null) {
@@ -394,7 +448,8 @@
                    }
                }
            }
        } catch (Exception ignore) {}
        } catch (Exception ignore) {
        }
    }
    @Override
@@ -477,10 +532,10 @@
    }
    @Override
    public DualCrnCommand getResetCommand(Integer crnNo, Integer station) {
    public DualCrnCommand getResetCommand(Integer taskNo, Integer crnNo, Integer station) {
        DualCrnCommand crnCommand = new DualCrnCommand();
        crnCommand.setCrnNo(crnNo); // 堆垛机编号
        crnCommand.setTaskNo(0); // 工作号
        crnCommand.setTaskNo(taskNo); // 工作号
        crnCommand.setTaskMode(DualCrnTaskModeType.CONFIRM.id); // 任务模式:  确认
        crnCommand.setSourcePosX(0);     // 源库位排
        crnCommand.setSourcePosY(0);     // 源库位列
@@ -525,4 +580,77 @@
            }
        }
    }
    public List<SendDualCrnCommandParam> getDualCrnCommandList() {
        List<SendDualCrnCommandParam> commandList = new ArrayList<>();
        SendDualCrnCommandParam command1 = getDualCrnCommandList(1);
        SendDualCrnCommandParam command2 = getDualCrnCommandList(2);
        if (command1 != null) {
            commandList.add(command1);
        }
        if (command2 != null) {
            commandList.add(command2);
        }
        return commandList;
    }
    public SendDualCrnCommandParam getDualCrnCommandList(int station) {
        SendDualCrnCommandParam sendDualCrnCommandParam = null;
        String key = null;
        if (station == 1 && crnProtocol.getTaskNo() > 0) {
            key = RedisKeyType.DUAL_CRN_COMMAND_.key + crnProtocol.getTaskNo();
        } else if (station == 2 && crnProtocol.getTaskNoTwo() > 0) {
            key = RedisKeyType.DUAL_CRN_COMMAND_.key + crnProtocol.getTaskNoTwo();
        }
        if (key == null) {
            return null;
        }
        Object commandObj = redisUtil.get(key);
        if (commandObj == null) {
            return null;
        }
        sendDualCrnCommandParam = JSON.parseObject(commandObj.toString(), SendDualCrnCommandParam.class);
        if (sendDualCrnCommandParam == null) {
            return null;
        }
        if (station == 1) {
            if (crnProtocol.getTaskNoTwo() > 0) {
                Object object = redisUtil.get(RedisKeyType.DUAL_CRN_COMMAND_.key + crnProtocol.getTaskNoTwo());
                if (object != null) {
                    SendDualCrnCommandParam jsonObject = JSON.parseObject(object.toString(), SendDualCrnCommandParam.class);
                    ;
                    if (!jsonObject.getCommands().get(0).getTaskMode().equals(sendDualCrnCommandParam.getCommands().get(0).getTaskMode())) {
                        return null;
                    }
                }
            }
        } else {
            if (crnProtocol.getTaskNo() > 0) {
                WrkMastService mastService = SpringUtils.getBean(WrkMastService.class);
                WrkMast wrkMast = mastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", crnProtocol.getTaskNo()));
                WrkMast wrkMast1 = mastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", crnProtocol.getTaskNoTwo()));
                if (!Objects.isNull(wrkMast) && !Objects.isNull(wrkMast1)) {
                    if (!wrkMast1.getIoType().equals(wrkMast.getIoType())) {
                        return null;
                    }
                }
//                Object object = redisUtil.get(RedisKeyType.DUAL_CRN_COMMAND_.key + crnProtocol.getTaskNo());
//                if (object != null) {
//                    SendDualCrnCommandParam jsonObject = JSON.parseObject(object.toString(), SendDualCrnCommandParam.class);
//                    ;
//                    if (!jsonObject.getCommands().get(0).getTaskMode().equals(sendDualCrnCommandParam.getCommands().get(0).getTaskMode())) {
//                        return null;
//                    }
//                }
            }
        }
        return sendDualCrnCommandParam;
    }
}
src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
@@ -54,6 +54,8 @@
    private CommonService commonService;
    @Autowired
    private NotifyUtils notifyUtils;
    @Autowired
    private StationOperateProcessUtils stationOperateProcessUtils;
    public synchronized void crnIoExecute() {
        Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
@@ -84,6 +86,7 @@
            List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                    .eq("crn_no", basCrnp.getCrnNo())
                    .in("wrk_sts", WrkStsType.INBOUND_RUN.sts, WrkStsType.OUTBOUND_RUN.sts)
                    .orderBy("batch_seq", false)
            );
            if(!wrkMasts.isEmpty()){
                continue;
@@ -257,11 +260,27 @@
            return false;
        }
        int stationMaxTaskCount = 30;
        Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
        if (systemConfigMapObj != null) {
            try {
                HashMap<String, String> systemConfigMap = (HashMap<String, String>) systemConfigMapObj;
                stationMaxTaskCount = Integer.parseInt(systemConfigMap.getOrDefault("stationMaxTaskCountLimit", "30"));
            } catch (Exception ignore) {}
        }
        int currentStationTaskCount = stationOperateProcessUtils.getCurrentStationTaskCount();
        if (stationMaxTaskCount > 0 && currentStationTaskCount >= stationMaxTaskCount) {
            News.warn("输送站点任务数量达到上限,已停止任务下发。当前任务数={},上限={}", currentStationTaskCount, stationMaxTaskCount);
            return false;
        }
        Integer crnNo = basCrnp.getCrnNo();
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                .eq("crn_no", crnNo)
                .eq("wrk_sts", WrkStsType.NEW_OUTBOUND.sts)
                .orderBy("batch_seq", false)
        );
        for (WrkMast wrkMast : wrkMasts) {
@@ -628,7 +647,7 @@
                wrkMast.setSystemMsg("");
                wrkMast.setIoTime(new Date());
                if (wrkMastService.updateById(wrkMast)) {
                    CrnCommand resetCommand = crnThread.getResetCommand(crnProtocol.getCrnNo());
                    CrnCommand resetCommand = crnThread.getResetCommand(crnProtocol.getTaskNo(), crnProtocol.getCrnNo());
                    MessageQueue.offer(SlaveType.Crn, crnProtocol.getCrnNo(), new Task(2, resetCommand));
                    News.info("堆垛机任务状态更新成功,堆垛机号={},工作号={}", basCrnp.getCrnNo(), crnProtocol.getTaskNo());
                }
@@ -813,8 +832,7 @@
        String shallowLocNo = Utils.getLocNo(shallowRow, Utils.getBay(locNo), Utils.getLev(locNo));
        LocMast shallowLocMast = locMastService.queryByLoc(shallowLocNo);
        if (shallowLocMast == null) {
            News.taskInfo(taskNo, "浅库位:{} 数据不存在", shallowLocNo);
            return false;
            return true;
        }
        if (shallowLocMast.getLocSts().equals("O")) {
src/main/java/com/zy/core/utils/DualCrnOperateProcessUtils.java
@@ -91,6 +91,10 @@
                continue;
            }
            if(dualCrnProtocol.getTaskSend() != 0 || dualCrnProtocol.getTaskSendTwo() != 0) {
                continue;
            }
            this.crnExecute(basDualCrnp, dualCrnThread);
        }
    }
@@ -195,18 +199,19 @@
        WrkMast stationOneWrkMast = null;
        WrkMast stationTwoWrkMast = null;
        List<Integer> disableList = basDualCrnp.getDisableStationOneBays$();
        List<Integer> disableOneList = basDualCrnp.getDisableStationOneBays$();
        List<Integer> disableTwoList = basDualCrnp.getDisableStationTwoBays$();
        for (WrkMast wrkMast : outTaskList) {
            if (stationOneWrkMast == null) {
                if (!disableList.contains(Utils.getBay(wrkMast.getSourceLocNo()))) {
                if (!disableOneList.contains(Utils.getBay(wrkMast.getSourceLocNo()))) {
                    stationOneWrkMast = wrkMast;
                    continue;
                }
            }
            if (stationTwoWrkMast == null) {
                if (!disableList.contains(Utils.getBay(wrkMast.getSourceLocNo()))) {
                if (!disableTwoList.contains(Utils.getBay(wrkMast.getSourceLocNo()))) {
                    stationTwoWrkMast = wrkMast;
                    continue;
                }
@@ -461,6 +466,10 @@
        Integer station = inStationObjModel.getDualCrnExecuteStation();
        if (station == 1) {
            if (dualCrnProtocol.getTaskNo() > 0) {
                News.taskInfo(wrkMast.getWrkNo(), "工位1系统内部记录有任务");
                return null;
            }
            List<Integer> basList = basDualCrnp.getDisableStationOneBays$();
            if (basList.contains(Utils.getBay(wrkMast.getLocNo()))) {
                //禁止放货列,申请重新分配
@@ -468,6 +477,10 @@
                return null;
            }
        }else {
            if (dualCrnProtocol.getTaskNoTwo() > 0) {
                News.taskInfo(wrkMast.getWrkNo(), "工位2系统内部记录有任务");
                return null;
            }
            List<Integer> basList = basDualCrnp.getDisableStationTwoBays$();
            if (basList.contains(Utils.getBay(wrkMast.getLocNo()))) {
                //禁止放货列,申请重新分配
@@ -516,6 +529,18 @@
        if (outStationList.isEmpty()) {
            News.info("双工位堆垛机:{} 出库站点未设置", basDualCrnp.getCrnNo());
            return null;
        }
        if (station == 1) {
            if (dualCrnProtocol.getTaskNo() > 0) {
                News.taskInfo(wrkMast.getWrkNo(), "工位1系统内部记录有任务");
                return null;
            }
        }else {
            if (dualCrnProtocol.getTaskNoTwo() > 0) {
                News.taskInfo(wrkMast.getWrkNo(), "工位2系统内部记录有任务");
                return null;
            }
        }
        Integer crnNo = basDualCrnp.getCrnNo();
@@ -684,12 +709,14 @@
                continue;
            }
            if(dualCrnProtocol.getTaskNo() > 0 && dualCrnProtocol.getStatus() == DualCrnStatusType.WAITING.id) {
            if((dualCrnProtocol.getTaskNo() > 0 && dualCrnProtocol.getDeviceTaskNo() > 0) && dualCrnProtocol.getTaskSend() == 0 && dualCrnProtocol.getStatus().equals(DualCrnStatusType.WAITING.id)) {
                executeFinish(basDualCrnp, dualCrnThread, dualCrnProtocol, dualCrnProtocol.getTaskNo(), 1);
                continue;
            }
            if(dualCrnProtocol.getTaskNoTwo() > 0 && dualCrnProtocol.getStatusTwo() == DualCrnStatusType.WAITING.id) {
            if((dualCrnProtocol.getTaskNoTwo() > 0 && dualCrnProtocol.getDeviceTaskNoTwo() > 0) && dualCrnProtocol.getTaskSendTwo() == 0 && dualCrnProtocol.getStatusTwo().equals(DualCrnStatusType.WAITING.id)) {
                executeFinish(basDualCrnp, dualCrnThread, dualCrnProtocol, dualCrnProtocol.getTaskNoTwo(), 2);
                continue;
            }
        }
    }
@@ -753,21 +780,28 @@
                return;
            }
            DualCrnCommand resetCommand = dualCrnThread.getResetCommand(dualCrnProtocol.getCrnNo(), station);
            DualCrnCommand resetCommand = dualCrnThread.getResetCommand(taskNo, dualCrnProtocol.getCrnNo(), station);
            boolean offer = MessageQueue.offer(SlaveType.DualCrn, dualCrnProtocol.getCrnNo(), new Task(3, resetCommand));
            if (offer) {
                if (station == 1) {
                    redisUtil.set(RedisKeyType.DUAL_CRN_STATION1_FLAG.key + basDualCrnp.getCrnNo(), 0, 60 * 60 * 24);
                }else {
                    redisUtil.set(RedisKeyType.DUAL_CRN_STATION2_FLAG.key + basDualCrnp.getCrnNo(), 0, 60 * 60 * 24);
                }
                wrkMast.setWrkSts(updateWrkSts);
                wrkMast.setSystemMsg("");
                wrkMast.setIoTime(new Date());
                if (wrkMastService.updateById(wrkMast)) {
                    News.info("双工位堆垛机任务状态更新成功,堆垛机号={},工作号={}", basDualCrnp.getCrnNo(), taskNo);
                }
                redisUtil.set(RedisKeyType.DUAL_CRN_IO_EXECUTE_FINISH_LIMIT.key + basDualCrnp.getCrnNo() + "_" + taskNo, "lock", 10);
                redisUtil.set(RedisKeyType.DUAL_CRN_IO_EXECUTE_FINISH_LIMIT.key + basDualCrnp.getCrnNo() + "_" + taskNo, "lock", 3);
            }
        }else {
            DualCrnCommand resetCommand = dualCrnThread.getResetCommand(dualCrnProtocol.getCrnNo(), station);
            DualCrnCommand resetCommand = dualCrnThread.getResetCommand(taskNo, dualCrnProtocol.getCrnNo(), station);
            MessageQueue.offer(SlaveType.DualCrn, dualCrnProtocol.getCrnNo(), new Task(3, resetCommand));
            News.info("双工位堆垛机命令完成确认成功,堆垛机号={},工作号={}", basDualCrnp.getCrnNo(), taskNo);
            redisUtil.set(RedisKeyType.DUAL_CRN_IO_EXECUTE_FINISH_LIMIT.key + basDualCrnp.getCrnNo() + "_" + taskNo, "lock", 3);
        }
    }
@@ -798,8 +832,7 @@
        String shallowLocNo = Utils.getLocNo(shallowRow, Utils.getBay(locNo), Utils.getLev(locNo));
        LocMast shallowLocMast = locMastService.queryByLoc(shallowLocNo);
        if (shallowLocMast == null) {
            News.taskInfo(taskNo, "浅库位:{} 数据不存在", shallowLocNo);
            return false;
            return true;
        }
        if (shallowLocMast.getLocSts().equals("O")) {
src/main/java/com/zy/core/utils/StationOperateProcessUtils.java
@@ -2,15 +2,21 @@
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.common.Cools;
import com.core.exception.CoolException;
import com.zy.asrs.domain.enums.NotifyMsgType;
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.News;
import com.zy.core.cache.MessageQueue;
@@ -28,6 +34,7 @@
@Component
public class StationOperateProcessUtils {
    private static final int LOOP_LOAD_RESERVE_EXPIRE_SECONDS = 120;
    @Autowired
    private BasDevpService basDevpService;
@@ -36,10 +43,6 @@
    @Autowired
    private CommonService commonService;
    @Autowired
    private BasCrnpService basCrnpService;
    @Autowired
    private BasDualCrnpService basDualCrnpService;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private LocMastService locMastService;
@@ -47,10 +50,20 @@
    private WmsOperateUtils wmsOperateUtils;
    @Autowired
    private NotifyUtils notifyUtils;
    @Autowired
    private NavigateUtils navigateUtils;
    @Autowired
    private BasStationService basStationService;
    @Autowired
    private StationCycleCapacityService stationCycleCapacityService;
    //执行输送站点入库任务
    public synchronized void stationInExecute() {
        try {
            DispatchLimitConfig limitConfig = getDispatchLimitConfig();
            int[] currentStationTaskCountRef = new int[]{countCurrentStationTask()};
            LoadGuardState loadGuardState = buildLoadGuardState(limitConfig);
            List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<>());
            for (BasDevp basDevp : basDevps) {
                StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
@@ -105,6 +118,12 @@
                            continue;
                        }
                        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());
@@ -120,6 +139,8 @@
                            MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command));
                            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);
                        }
                    }
                }
@@ -132,10 +153,16 @@
    //执行堆垛机输送站点出库任务
    public synchronized void crnStationOutExecute() {
        try {
            DispatchLimitConfig limitConfig = getDispatchLimitConfig();
            int[] currentStationTaskCountRef = new int[]{countCurrentStationTask()};
            LoadGuardState loadGuardState = buildLoadGuardState(limitConfig);
            List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<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) {
@@ -165,7 +192,30 @@
                        && stationProtocol.isLoading()
                        && stationProtocol.getTaskNo() == 0
                ) {
                    StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), wrkMast.getStaNo(), 0);
                    Integer moveStaNo = wrkMast.getStaNo();
                    if (!outOrderList.isEmpty()) {
                        List<NavigateNode> nodes = navigateUtils.calcByStationId(stationProtocol.getStationId(), wrkMast.getStaNo());
                        for (int i = nodes.size() - 1; i >= 0; i--) {
                            NavigateNode node = nodes.get(i);
                            JSONObject v = JSONObject.parseObject(node.getNodeValue());
                            if (v != null) {
                                Integer stationId = v.getInteger("stationId");
                                if (outOrderList.contains(stationId)) {
                                    moveStaNo = stationId;
                                    break;
                                }
                            }
                        }
                    }
                    LoopHitResult loopHitResult = findPathLoopHit(limitConfig, stationProtocol.getStationId(), moveStaNo, loadGuardState);
                    if (isDispatchBlocked(limitConfig, currentStationTaskCountRef[0], loadGuardState, loopHitResult.isThroughLoop())) {
                        return;
                    }
                    StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), moveStaNo, 0);
                    if (command == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
                        continue;
@@ -178,8 +228,10 @@
                        MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                        News.info("输送站点出库命令下发成功,站点号={},工作号={},命令数据={}", stationProtocol.getStationId(), wrkMast.getWrkNo(), JSON.toJSONString(command));
                        redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_LIMIT.key + stationProtocol.getStationId(), "lock", 5);
                        redisUtil.set(RedisKeyType.STATION_OUT_EXECUTE_COMPLETE_LIMIT.key + wrkMast.getWrkNo(), "lock", 60);
                        redisUtil.del(RedisKeyType.CRN_OUT_TASK_COMPLETE_STATION_INFO.key + wrkMast.getWrkNo());
                        currentStationTaskCountRef[0]++;
                        loadGuardState.reserveLoopTask(loopHitResult.getLoopNo());
                        saveLoopLoadReserve(wrkMast.getWrkNo(), loopHitResult);
                    }
                }
            }
@@ -238,7 +290,6 @@
                        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.set(RedisKeyType.STATION_OUT_EXECUTE_COMPLETE_LIMIT.key + wrkMast.getWrkNo(), "lock", 60);
                        redisUtil.del(RedisKeyType.DUAL_CRN_OUT_TASK_STATION_INFO.key + wrkMast.getWrkNo());
                    }
                }
@@ -254,26 +305,74 @@
            List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("wrk_sts", WrkStsType.STATION_RUN.sts));
            for (WrkMast wrkMast : wrkMasts) {
                Integer wrkNo = wrkMast.getWrkNo();
                Integer targetStaNo = wrkMast.getStaNo();
                boolean complete = false;
                BasStation basStation = basStationService.selectOne(new EntityWrapper<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) {
                    wrkMast.setWrkSts(WrkStsType.STATION_RUN_COMPLETE.sts);
                    wrkMast.setIoTime(new Date());
                    wrkMastService.updateById(wrkMast);
                    notifyUtils.notify(String.valueOf(SlaveType.Devp), basStation.getDeviceNo(), 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);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    // 检测任务转完成
    public synchronized void checkTaskToComplete() {
        try {
            List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<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 = true;
                List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<>());
                for (BasDevp basDevp : basDevps) {
                    StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
                    if (stationThread == null) {
                        continue;
                    }
                boolean complete = false;
                BasStation basStation = basStationService.selectOne(new EntityWrapper<BasStation>().eq("station_id", targetStaNo));
                if (basStation == null) {
                    continue;
                }
                    List<StationProtocol> list = stationThread.getStatus();
                    for (StationProtocol stationProtocol : list) {
                        if (stationProtocol.getTaskNo().equals(wrkNo)) {
                            complete = false;
                        }
                    }
                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) {
@@ -426,10 +525,257 @@
    //获取输送线任务数量
    public synchronized int getCurrentStationTaskCount() {
        int currentStationTaskCount = 0;
        return countCurrentStationTask();
    }
    // 检测出库排序
    public synchronized void checkStationOutOrder() {
        List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getId());
            if (stationThread == null) {
                continue;
            }
            Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
            List<StationObjModel> orderList = basDevp.getOutOrderList$();
            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;
                }
                if (!stationProtocol.getStationId().equals(stationProtocol.getTargetStaNo())) {
                    continue;
                }
                WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
                if (wrkMast == null) {
                    continue;
                }
                if (Cools.isEmpty(wrkMast.getBatch())) {
                    continue;
                }
                if (Cools.isEmpty(wrkMast.getBatchSeq())) {
                    continue;
                }
                List<WrkMast> batchWrkList = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                        .notIn("wrk_sts", WrkStsType.STATION_RUN_COMPLETE.sts, WrkStsType.COMPLETE_OUTBOUND.sts)
                        .eq("batch", wrkMast.getBatch())
                        .orderBy("batch")
                );
                if (batchWrkList.isEmpty()) {
                    continue;
                }
                WrkMast firstWrkMast = batchWrkList.get(0);
                Integer currentBatchSeq = firstWrkMast.getBatchSeq();
                List<NavigateNode> initPath = navigateUtils.calcByStationId(wrkMast.getSourceStaNo(), wrkMast.getStaNo());
                String commandType = "none";
                Integer seq = getOutStationBatchSeq(initPath, stationProtocol.getStationId(), wrkMast.getBatch());
                if (seq == null) {
                    if (currentBatchSeq.equals(wrkMast.getBatchSeq())) {
                        commandType = "toTarget";
                    }else {
                        commandType = "toCircle";
                    }
                }else {
                    seq++;
                    if (seq.equals(wrkMast.getBatchSeq()) && currentBatchSeq.equals(wrkMast.getBatchSeq())) {
                        commandType = "toTarget";
                    }else {
                        commandType = "toCircle";
                    }
                }
                if (commandType.equals("toTarget")) {
                    StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), wrkMast.getStaNo(), 0);
                    if (command == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
                        continue;
                    }
                    MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                    News.info("{}任务直接去目标点", wrkMast.getWrkNo());
                } else if (commandType.equals("toCircle")) {
                    Integer circleTarget = null;
                    for (NavigateNode node : initPath) {
                        JSONObject v = JSONObject.parseObject(node.getNodeValue());
                        if (v != null) {
                            Integer stationId = v.getInteger("stationId");
                            try {
                                List<NavigateNode> enableMovePath = navigateUtils.calcByStationId(stationProtocol.getStationId(), stationId);
                                if (enableMovePath.isEmpty()) {
                                    continue;
                                }
                            } catch (Exception e) {
                                continue;
                            }
                            circleTarget = stationId;
                            break;
                        }
                    }
                    if (circleTarget == null) {
                        continue;
                    }
                    StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), circleTarget, 0);
                    if (command == null) {
                        News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
                        continue;
                    }
                    MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command));
                    redisUtil.set(RedisKeyType.WATCH_CIRCLE_STATION_.key + wrkMast.getWrkNo(), JSON.toJSONString(command, SerializerFeature.DisableCircularReferenceDetect), 60 * 60 * 24);
                    News.info("{}任务进行绕圈", wrkMast.getWrkNo());
                }
            }
        }
    }
    // 监控绕圈站点
    public synchronized void watchCircleStation() {
        List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getId());
            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;
                }
                Object circleObj = redisUtil.get(RedisKeyType.WATCH_CIRCLE_STATION_.key + stationProtocol.getTaskNo());
                if (circleObj == null) {
                    continue;
                }
                StationCommand circleCommand = JSON.parseObject(circleObj.toString(), StationCommand.class);
                if (!stationProtocol.getStationId().equals(circleCommand.getTargetStaNo())) {
                    continue;
                }
                WrkMast wrkMast = wrkMastService.selectByWorkNo(stationProtocol.getTaskNo());
                if (wrkMast == null) {
                    continue;
                }
                Integer moveStaNo = wrkMast.getStaNo();
                if (!outOrderList.isEmpty()) {
                    List<NavigateNode> nodes = navigateUtils.calcByStationId(stationProtocol.getStationId(), wrkMast.getStaNo());
                    for (int i = nodes.size() - 1; i >= 0; i--) {
                        NavigateNode node = nodes.get(i);
                        JSONObject v = JSONObject.parseObject(node.getNodeValue());
                        if (v != null) {
                            Integer stationId = v.getInteger("stationId");
                            if (outOrderList.contains(stationId)) {
                                moveStaNo = stationId;
                                break;
                            }
                        }
                    }
                }
                StationCommand command = stationThread.getCommand(StationCommandType.MOVE, wrkMast.getWrkNo(), stationProtocol.getStationId(), moveStaNo, 0);
                if (command == null) {
                    News.taskInfo(wrkMast.getWrkNo(), "获取输送线命令失败");
                    continue;
                }
                MessageQueue.offer(SlaveType.Devp, basDevp.getDevpNo(), new Task(2, command));
            }
        }
    }
    public List<Integer> getAllOutOrderList() {
        List<Integer> list = new ArrayList<>();
        List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            List<Integer> orderList = basDevp.getOutOrderIntList();
            list.addAll(orderList);
        }
        return list;
    }
    public Integer getOutStationBatchSeq(List<NavigateNode> pathList, Integer searchStationId, String searchBatch) {
        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.selectOne(new EntityWrapper<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.selectList(new EntityWrapper<BasDevp>());
        for (BasDevp basDevp : basDevps) {
            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
            if (stationThread == null) {
                continue;
            }
@@ -440,9 +786,291 @@
                }
            }
        }
        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());
        state.projectedTaskStationCount = toNonNegative(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) {
        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 = navigateUtils.calcByStationId(sourceStationId, targetStationId);
            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 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() {
        DispatchLimitConfig config = new DispatchLimitConfig();
        Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
        if (!(systemConfigMapObj instanceof Map)) {
            return config;
        }
        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);
        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;
        }
    }
}
src/main/java/com/zy/system/controller/LicenseCreatorController.java
@@ -1,6 +1,5 @@
package com.zy.system.controller;
import com.core.common.Cools;
import com.core.common.R;
import com.zy.system.entity.license.*;
import com.zy.system.timer.LicenseTimer;
@@ -37,24 +36,7 @@
     */
    @RequestMapping(value = "/getServerInfos",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    public LicenseCheck getServerInfos(@RequestParam(value = "osName",required = false) String osName) {
        //操作系统类型
        if(Cools.isEmpty(osName)){
            osName = System.getProperty("os.name");
        }
        osName = osName.toLowerCase();
        AbstractServerInfos abstractServerInfos = null;
        //根据不同操作系统类型选择不同的数据获取方法
        if (osName.startsWith("windows")) {
            abstractServerInfos = new WindowsServerInfos();
        } else if (osName.startsWith("linux")) {
            abstractServerInfos = new LinuxServerInfos();
        }else{//其他服务器类型
            abstractServerInfos = new WindowsServerInfos();
        }
        return abstractServerInfos.getServerInfos();
        return LicenseUtils.getServerInfos();
    }
    /**
src/main/java/com/zy/system/entity/license/CustomLicenseManager.java
@@ -124,7 +124,7 @@
        //License中可被允许的参数信息
        LicenseCheck expectedCheckModel = (LicenseCheck) content.getExtra();
        //当前服务器真实的参数信息
        LicenseCheck serverCheckModel = getServerInfos();
        LicenseCheck serverCheckModel = LicenseUtils.getServerInfos();
        if(expectedCheckModel != null && serverCheckModel != null){
            //校验IP地址
@@ -180,26 +180,6 @@
        }
        return null;
    }
    /**
     * 获取当前服务器需要额外校验的License参数
     */
    private LicenseCheck getServerInfos(){
        //操作系统类型
        String osName = System.getProperty("os.name").toLowerCase();
        AbstractServerInfos abstractServerInfos = null;
        //根据不同操作系统类型选择不同的数据获取方法
        if (osName.startsWith("windows")) {
            abstractServerInfos = new WindowsServerInfos();
        } else if (osName.startsWith("linux")) {
            abstractServerInfos = new LinuxServerInfos();
        }else{//其他服务器类型
            abstractServerInfos = new WindowsServerInfos();
        }
        return abstractServerInfos.getServerInfos();
    }
    /**
src/main/java/com/zy/system/entity/license/LinuxServerInfos.java
@@ -36,7 +36,11 @@
        if (inetAddresses != null && inetAddresses.size() > 0) {
            //2. 获取所有网络接口的Mac地址
            result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
            result = inetAddresses.stream()
                    .map(this::getMacByInetAddress)
                    .filter(mac -> mac != null && !mac.trim().isEmpty())
                    .distinct()
                    .collect(Collectors.toList());
        }
        return result;
@@ -88,4 +92,4 @@
        reader.close();
        return serialNumber;
    }
}
}
src/main/java/com/zy/system/entity/license/WindowsServerInfos.java
@@ -33,7 +33,11 @@
        if(inetAddresses != null && inetAddresses.size() > 0){
            //2. 获取所有网络接口的Mac地址
            result = inetAddresses.stream().map(this::getMacByInetAddress).distinct().collect(Collectors.toList());
            result = inetAddresses.stream()
                    .map(this::getMacByInetAddress)
                    .filter(mac -> mac != null && !mac.trim().isEmpty())
                    .distinct()
                    .collect(Collectors.toList());
        }
        return result;
@@ -82,4 +86,4 @@
        scanner.close();
        return serialNumber;
    }
}
}
src/main/java/com/zy/system/timer/LicenseTimer.java
@@ -73,17 +73,7 @@
    public void getRemoteLicense() {
        try {
            AbstractServerInfos abstractServerInfos = null;
            String osName = System.getProperty("os.name");
            // 根据不同操作系统类型选择不同的数据获取方法
            if (osName.startsWith("windows")) {
                abstractServerInfos = new WindowsServerInfos();
            } else if (osName.startsWith("linux")) {
                abstractServerInfos = new LinuxServerInfos();
            } else {// 其他服务器类型
                abstractServerInfos = new WindowsServerInfos();
            }
            LicenseCheck serverInfos = abstractServerInfos.getServerInfos();
            LicenseCheck serverInfos = LicenseUtils.getServerInfos();
            HashMap<String, Object> map = new HashMap<>();
            map.put("subject", subject);
src/main/resources/application.yml
@@ -17,9 +17,9 @@
      validation-timeout: 3000
      connection-test-query: select 1
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/wcs?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    url: jdbc:mysql://192.168.238.31:3306/wcs?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    password: zy@123
  mvc:
    static-path-pattern: /**
  redis:
@@ -61,7 +61,7 @@
#License相关配置
license:
  subject: crnWcsDev
  subject: jsxswcs1
  publicAlias: publicCert
  storePass: public_zhongyang_123456789
  licensePath: license.lic
@@ -72,7 +72,7 @@
  threadControlCount: 10
  liftType: lift
mainProcessPlugin: FakeProcess
mainProcessPlugin: XiaosongProcess
deviceLogStorage:
  # 设备日志存储方式 mysql file
src/main/webapp/components/DevpCard.js
@@ -43,8 +43,22 @@
                <el-descriptions-item label="运行阻塞">{{ item.runBlock ? 'Y' : 'N' }}</el-descriptions-item>
                <el-descriptions-item label="启动入库">{{ item.enableIn ? 'Y' : 'N' }}</el-descriptions-item>
                <el-descriptions-item label="托盘高度">{{ item.palletHeight }}</el-descriptions-item>
                <el-descriptions-item label="条码">{{ item.barcode }}</el-descriptions-item>
                <el-descriptions-item label="条码">
                  <el-popover v-if="item.barcode" placement="top" width="460" trigger="hover">
                    <div style="text-align: center;">
                      <img
                        :src="getBarcodePreview(item.barcode)"
                        :alt="'barcode-' + item.barcode"
                        style="display: block; max-width: 100%; height: auto; margin: 0 auto; image-rendering: pixelated; background: #fff;"
                      />
                      <div style="margin-top: 4px; font-size: 12px; word-break: break-all;">{{ item.barcode }}</div>
                    </div>
                    <span slot="reference" @click.stop="handleBarcodeClick(item)" style="cursor: pointer; color: #409EFF;">{{ item.barcode }}</span>
                  </el-popover>
                  <span v-else @click.stop="handleBarcodeClick(item)" style="cursor: pointer; color: #409EFF;">-</span>
                </el-descriptions-item>
                <el-descriptions-item label="重量">{{ item.weight }}</el-descriptions-item>
                <el-descriptions-item label="任务可写区">{{ item.taskWriteIdx }}</el-descriptions-item>
                <el-descriptions-item label="故障代码">{{ item.error }}</el-descriptions-item>
                <el-descriptions-item label="故障信息">{{ item.errorMsg }}</el-descriptions-item>
                <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item>
@@ -92,6 +106,7 @@
        taskNo: "",
        targetStationId: "",
      },
      barcodePreviewCache: {},
      pageSize: 25,
      currentPage: 1,
      timer: null
@@ -136,6 +151,124 @@
      this.pageSize = size;
      this.currentPage = 1;
    },
    getBarcodePreview(barcode) {
      const value = String(barcode || "").trim();
      if (!value) {
        return "";
      }
      if (this.barcodePreviewCache[value]) {
        return this.barcodePreviewCache[value];
      }
      const encodeResult = this.encodeCode128(value);
      if (!encodeResult) {
        return "";
      }
      const svg = this.buildCode128Svg(encodeResult, value);
      const dataUrl = "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg);
      this.$set(this.barcodePreviewCache, value, dataUrl);
      return dataUrl;
    },
    encodeCode128(value) {
      if (!value) {
        return null;
      }
      const isNumeric = /^\d+$/.test(value);
      if (isNumeric && value.length % 2 === 0) {
        return this.encodeCode128C(value);
      }
      return this.encodeCode128B(value);
    },
    encodeCode128B(value) {
      const codes = [104];
      for (let i = 0; i < value.length; i++) {
        const code = value.charCodeAt(i) - 32;
        if (code < 0 || code > 94) {
          return null;
        }
        codes.push(code);
      }
      return this.buildCode128Pattern(codes);
    },
    encodeCode128C(value) {
      if (value.length % 2 !== 0) {
        return null;
      }
      const codes = [105];
      for (let i = 0; i < value.length; i += 2) {
        codes.push(parseInt(value.substring(i, i + 2), 10));
      }
      return this.buildCode128Pattern(codes);
    },
    buildCode128Pattern(codes) {
      const patterns = this.getCode128Patterns();
      let checksum = codes[0];
      for (let i = 1; i < codes.length; i++) {
        checksum += codes[i] * i;
      }
      const checkCode = checksum % 103;
      const fullCodes = codes.concat([checkCode, 106]);
      let bars = "";
      for (let i = 0; i < fullCodes.length; i++) {
        const code = fullCodes[i];
        if (patterns[code] == null) {
          return null;
        }
        bars += patterns[code];
      }
      bars += "11";
      return bars;
    },
    buildCode128Svg(bars, text) {
      const quietModules = 20;
      const modules = quietModules * 2 + bars.split("").reduce((sum, n) => sum + parseInt(n, 10), 0);
      const moduleWidth = modules > 300 ? 1 : 2;
      const width = modules * moduleWidth;
      const barTop = 10;
      const barHeight = 110;
      let x = quietModules * moduleWidth;
      let black = true;
      let rects = "";
      for (let i = 0; i < bars.length; i++) {
        const w = parseInt(bars[i], 10) * moduleWidth;
        if (black) {
          rects += '<rect x="' + x + '" y="' + barTop + '" width="' + w + '" height="' + barHeight + '" fill="#000" shape-rendering="crispEdges" />';
        }
        x += w;
        black = !black;
      }
      return (
        '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="145" viewBox="0 0 ' + width + ' 145">' +
        '<rect width="100%" height="100%" fill="#fff" />' +
        rects +
        '<text x="' + (width / 2) + '" y="136" text-anchor="middle" font-family="monospace" font-size="14" fill="#111">' +
        this.escapeXml(text) +
        "</text>" +
        "</svg>"
      );
    },
    getCode128Patterns() {
      return [
        "212222", "222122", "222221", "121223", "121322", "131222", "122213", "122312", "132212", "221213",
        "221312", "231212", "112232", "122132", "122231", "113222", "123122", "123221", "223211", "221132",
        "221231", "213212", "223112", "312131", "311222", "321122", "321221", "312212", "322112", "322211",
        "212123", "212321", "232121", "111323", "131123", "131321", "112313", "132113", "132311", "211313",
        "231113", "231311", "112133", "112331", "132131", "113123", "113321", "133121", "313121", "211331",
        "231131", "213113", "213311", "213131", "311123", "311321", "331121", "312113", "312311", "332111",
        "314111", "221411", "431111", "111224", "111422", "121124", "121421", "141122", "141221", "112214",
        "112412", "122114", "122411", "142112", "142211", "241211", "221114", "413111", "241112", "134111",
        "111242", "121142", "121241", "114212", "124112", "124211", "411212", "421112", "421211", "212141",
        "214121", "412121", "111143", "111341", "131141", "114113", "114311", "411113", "411311", "113141",
        "114131", "311141", "411131", "211412", "211214", "211232", "2331112"
      ];
    },
    escapeXml(text) {
      return String(text)
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&apos;");
    },
    getDevpStateInfo() {
      if (this.readOnly) {
          // Frontend filtering for readOnly mode
@@ -171,6 +304,81 @@
        }
      }
    },
    handleBarcodeClick(item) {
      if (this.readOnly || !item || item.stationId == null) {
        return;
      }
      let that = this;
      $.ajax({
        url: baseUrl + "/openapi/getFakeSystemRunStatus",
        headers: {
          token: localStorage.getItem("token"),
        },
        method: "get",
        success: (res) => {
          if (res.code !== 200 || !res.data || !res.data.isFake || !res.data.running) {
            that.$message({
              message: "仅仿真模式运行中可修改条码",
              type: "warning",
            });
            return;
          }
          that.$prompt("请输入新的条码值(可留空清空)", "修改条码", {
            confirmButtonText: "确定",
            cancelButtonText: "取消",
            inputValue: item.barcode || "",
            inputPlaceholder: "请输入条码",
          }).then(({ value }) => {
            that.updateStationBarcode(item.stationId, value == null ? "" : String(value).trim());
          }).catch(() => {});
        },
      });
    },
    updateStationBarcode(stationId, barcode) {
      let that = this;
      $.ajax({
        url: baseUrl + "/station/command/barcode",
        headers: {
          token: localStorage.getItem("token"),
        },
        contentType: "application/json",
        method: "post",
        data: JSON.stringify({
          stationId: stationId,
          barcode: barcode,
        }),
        success: (res) => {
          if (res.code == 200) {
            that.syncLocalBarcode(stationId, barcode);
            that.$message({
              message: "条码修改成功",
              type: "success",
            });
          } else {
            that.$message({
              message: res.msg || "条码修改失败",
              type: "warning",
            });
          }
        },
      });
    },
    syncLocalBarcode(stationId, barcode) {
      let updateFn = (list) => {
        if (!list || list.length === 0) {
          return;
        }
        list.forEach((row) => {
          if (row.stationId == stationId) {
            row.barcode = barcode;
          }
        });
      };
      updateFn(this.stationList);
      updateFn(this.fullStationList);
    },
    openControl() {
      this.showControl = !this.showControl;
    },
src/main/webapp/components/MapCanvas.js
@@ -1,7 +1,21 @@
Vue.component('map-canvas', {
  template: `
    <div style="width: 100%; height: 100%; position: relative;">
      <div ref="pixiView"></div>
      <div ref="pixiView" style="position: absolute; inset: 0;"></div>
      <div style="position: absolute; top: 12px; left: 14px; z-index: 30; pointer-events: none; max-width: 52%;">
        <div style="display: flex; flex-direction: column; gap: 6px; align-items: flex-start;">
          <div v-for="item in cycleCapacity.loopList"
               :key="'loop-' + item.loopNo"
               @mouseenter="handleLoopCardEnter(item)"
               @mouseleave="handleLoopCardLeave(item)"
               style="padding: 6px 10px; border-radius: 4px; background: rgba(11, 35, 58, 0.72); color: #fff; font-size: 12px; line-height: 1.4; white-space: nowrap; pointer-events: auto;">
            圈{{ item.loopNo }} |
            站点: {{ item.stationCount || 0 }} |
            任务: {{ item.taskCount || 0 }} |
            承载: {{ formatLoadPercent(item.currentLoad) }}
          </div>
        </div>
      </div>
      <div v-show="shelfTooltip.visible"
           :style="shelfTooltipStyle()">
        {{ shelfTooltip.text }}
@@ -80,14 +94,25 @@
        item: null
      },
      shelfTooltipMinScale: 0.4,
      containerResizeObserver: null,
      timer: null,
      adjustLabelTimer: null,
      isSwitchingFloor: false
      isSwitchingFloor: false,
      cycleCapacity: {
        loopList: [],
        totalStationCount: 0,
        taskStationCount: 0,
        currentLoad: 0
      },
      hoverLoopNo: null,
      hoverLoopStationIdSet: new Set(),
      loopHighlightColor: 0xfff34d
    }
  },
    mounted() {
    this.currentLev = this.lev || 1;
    this.createMap();
    this.startContainerResizeObserve();
    this.loadMapTransformConfig();
    this.loadLocList();
    this.connectWs();
@@ -100,6 +125,7 @@
      this.getCrnInfo();
      this.getDualCrnInfo();
      this.getSiteInfo();
      this.getCycleCapacityInfo();
      this.getRgvInfo();
    }, 1000);
  },
@@ -108,6 +134,7 @@
    if (this.hoverRaf) { cancelAnimationFrame(this.hoverRaf); this.hoverRaf = null; }
    if (this.pixiApp) { this.pixiApp.destroy(true, { children: true }); }
    if (this.containerResizeObserver) { this.containerResizeObserver.disconnect(); this.containerResizeObserver = null; }
    window.removeEventListener('resize', this.resizeToContainer);
    if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; }
    if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { try { this.ws.close(); } catch (e) {} }
@@ -277,11 +304,33 @@
      });
      //*******************FPS*******************
    },
    startContainerResizeObserve() {
      if (typeof ResizeObserver === 'undefined' || !this.$el) { return; }
      this.containerResizeObserver = new ResizeObserver(() => {
        this.resizeToContainer();
      });
      this.containerResizeObserver.observe(this.$el);
    },
    getViewportSize() {
      if (!this.pixiApp || !this.pixiApp.renderer) { return { width: 0, height: 0 }; }
      const screen = this.pixiApp.renderer.screen;
      if (screen && screen.width > 0 && screen.height > 0) {
        return { width: screen.width, height: screen.height };
      }
      const rect = this.pixiApp.view ? this.pixiApp.view.getBoundingClientRect() : null;
      return { width: rect ? rect.width : 0, height: rect ? rect.height : 0 };
    },
    resizeToContainer() {
      const w = this.$el.clientWidth || 0;
      const h = this.$el.clientHeight || 0;
      if (w > 0 && h > 0 && this.pixiApp) {
        const vw = this.pixiApp.renderer && this.pixiApp.renderer.screen ? this.pixiApp.renderer.screen.width : 0;
        const vh = this.pixiApp.renderer && this.pixiApp.renderer.screen ? this.pixiApp.renderer.screen.height : 0;
        if (vw === w && vh === h) { return; }
        this.pixiApp.renderer.resize(w, h);
        if (this.mapContentSize && this.mapContentSize.width > 0 && this.mapContentSize.height > 0) {
          this.applyMapTransform(true);
        }
      }
    },
    getMap() {
@@ -289,6 +338,7 @@
    },
    changeFloor(lev) {
      this.currentLev = lev;
      this.clearLoopStationHighlight();
      this.isSwitchingFloor = true;
      this.hideShelfTooltip();
      this.hoveredShelfCell = null;
@@ -313,6 +363,7 @@
      this.getMap();
    },
    createMapData(map) {
      this.clearLoopStationHighlight();
      this.hideShelfTooltip();
      this.hoveredShelfCell = null;
      this.mapRowOffsets = [];
@@ -627,21 +678,21 @@
          sta.statusObj = null;
          if (sta.textObj.parent !== sta) { sta.addChild(sta.textObj); sta.textObj.position.set(sta.width / 2, sta.height / 2); }
        }
        let baseColor = 0xb8b8b8;
        if (status === "site-auto") {
          this.updateColor(sta, 0x78ff81);
          baseColor = 0x78ff81;
        } else if (status === "site-auto-run" || status === "site-auto-id" || status === "site-auto-run-id") {
          this.updateColor(sta, 0xfa51f6);
          baseColor = 0xfa51f6;
        } else if (status === "site-unauto") {
          this.updateColor(sta, 0xb8b8b8);
          baseColor = 0xb8b8b8;
        } else if (status === "machine-pakin") {
          this.updateColor(sta, 0x30bffc);
          baseColor = 0x30bffc;
        } else if (status === "machine-pakout") {
          this.updateColor(sta, 0x97b400);
          baseColor = 0x97b400;
        } else if (status === "site-run-block") {
          this.updateColor(sta, 0xe69138);
        } else {
          this.updateColor(sta, 0xb8b8b8);
          baseColor = 0xe69138;
        }
        this.setStationBaseColor(sta, baseColor);
      });
    },
    getCrnInfo() {
@@ -659,6 +710,38 @@
    getRgvInfo() {
      if (this.isSwitchingFloor) { return; }
      this.sendWs(JSON.stringify({ url: "/console/latest/data/rgv", data: {} }));
    },
    getCycleCapacityInfo() {
      if (this.isSwitchingFloor) { return; }
      this.sendWs(JSON.stringify({ url: "/console/latest/data/station/cycle/capacity", data: {} }));
    },
    setCycleCapacityInfo(res) {
      const payload = res && res.code === 200 ? res.data : null;
      if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; return; }
      if (!payload) { return; }
      const loopList = Array.isArray(payload.loopList) ? payload.loopList : [];
      this.cycleCapacity = {
        loopList: loopList,
        totalStationCount: payload.totalStationCount || 0,
        taskStationCount: payload.taskStationCount || 0,
        currentLoad: typeof payload.currentLoad === 'number' ? payload.currentLoad : parseFloat(payload.currentLoad || 0)
      };
      if (this.hoverLoopNo != null) {
        const targetLoop = loopList.find(v => v && v.loopNo === this.hoverLoopNo);
        if (targetLoop) {
          this.hoverLoopStationIdSet = this.buildStationIdSet(targetLoop.stationIdList);
          this.applyLoopStationHighlight();
        } else {
          this.clearLoopStationHighlight();
        }
      }
    },
    formatLoadPercent(load) {
      let value = typeof load === 'number' ? load : parseFloat(load || 0);
      if (!isFinite(value)) { value = 0; }
      if (value < 0) { value = 0; }
      if (value > 1) { value = 1; }
      return (value * 100).toFixed(1) + "%";
    },
    setCrnInfo(res) {
      let crns = Array.isArray(res) ? res : (res && res.code === 200 ? res.data : null);
@@ -814,6 +897,7 @@
      if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; }
      this.wsReconnectAttempts = 0;
      this.getMap(this.currentLev);
      this.getCycleCapacityInfo();
    },
    webSocketOnError(e) {
      this.scheduleReconnect();
@@ -828,6 +912,8 @@
        this.setDualCrnInfo(JSON.parse(result.data));
      } else if (result.url === "/console/latest/data/rgv") {
        this.setRgvInfo(JSON.parse(result.data));
      } else if (result.url === "/console/latest/data/station/cycle/capacity") {
        this.setCycleCapacityInfo(JSON.parse(result.data));
      } else if (typeof result.url === "string" && result.url.indexOf("/basMap/lev/") === 0) {
        this.setMap(JSON.parse(result.data));
      }
@@ -1175,7 +1261,11 @@
        text.position.set(sprite.width / 2, sprite.height / 2);
        sprite.addChild(text);
        sprite.textObj = text;
        if (siteId != null && siteId !== -1) { this.pixiStaMap.set(parseInt(siteId), sprite); }
        const stationIdInt = parseInt(siteId, 10);
        if (!isNaN(stationIdInt)) { this.pixiStaMap.set(stationIdInt, sprite); }
        sprite._stationId = isNaN(stationIdInt) ? null : stationIdInt;
        sprite._baseColor = 0x00ff7f;
        sprite._loopHighlighted = false;
        sprite.interactive = true;
        sprite.buttonMode = true;
        sprite.on('pointerdown', () => {
@@ -1376,6 +1466,74 @@
        return;
      }
      sprite.tint = color;
    },
    setStationBaseColor(sprite, color) {
      if (!sprite) { return; }
      sprite._baseColor = color;
      if (this.isStationInHoverLoop(sprite)) {
        this.applyHighlightColor(sprite);
      } else {
        this.updateColor(sprite, color);
        sprite._loopHighlighted = false;
      }
    },
    applyHighlightColor(sprite) {
      if (!sprite) { return; }
      this.updateColor(sprite, this.loopHighlightColor);
      sprite._loopHighlighted = true;
    },
    isStationInHoverLoop(sprite) {
      if (!sprite || sprite._stationId == null || !this.hoverLoopStationIdSet) { return false; }
      return this.hoverLoopStationIdSet.has(sprite._stationId);
    },
    buildStationIdSet(stationIdList) {
      const set = new Set();
      if (!Array.isArray(stationIdList)) { return set; }
      stationIdList.forEach((id) => {
        const v = parseInt(id, 10);
        if (!isNaN(v)) { set.add(v); }
      });
      return set;
    },
    applyLoopStationHighlight() {
      if (!this.pixiStaMap) { return; }
      this.pixiStaMap.forEach((sprite) => {
        if (!sprite) { return; }
        if (this.isStationInHoverLoop(sprite)) {
          this.applyHighlightColor(sprite);
        } else if (sprite._loopHighlighted) {
          const baseColor = (typeof sprite._baseColor === 'number') ? sprite._baseColor : 0xb8b8b8;
          this.updateColor(sprite, baseColor);
          sprite._loopHighlighted = false;
        }
      });
    },
    clearLoopStationHighlight() {
      if (this.pixiStaMap) {
        this.pixiStaMap.forEach((sprite) => {
          if (!sprite || !sprite._loopHighlighted) { return; }
          const baseColor = (typeof sprite._baseColor === 'number') ? sprite._baseColor : 0xb8b8b8;
          this.updateColor(sprite, baseColor);
          sprite._loopHighlighted = false;
        });
      }
      this.hoverLoopNo = null;
      this.hoverLoopStationIdSet = new Set();
    },
    handleLoopCardEnter(loopItem) {
      if (!loopItem) { return; }
      this.hoverLoopNo = loopItem.loopNo;
      this.hoverLoopStationIdSet = this.buildStationIdSet(loopItem.stationIdList);
      this.applyLoopStationHighlight();
    },
    handleLoopCardLeave(loopItem) {
      if (!loopItem) {
        this.clearLoopStationHighlight();
        return;
      }
      if (this.hoverLoopNo === loopItem.loopNo) {
        this.clearLoopStationHighlight();
      }
    },
    isJson(str) {
      try { JSON.parse(str); return true; } catch (e) { return false; }
@@ -1642,8 +1800,9 @@
    adjustLabelScale() {
      const s = this.pixiApp && this.pixiApp.stage ? Math.abs(this.pixiApp.stage.scale.x || 1) : 1;
      const minPx = 14;
      const vw = this.pixiApp.view.width;
      const vh = this.pixiApp.view.height;
      const viewport = this.getViewportSize();
      const vw = viewport.width;
      const vh = viewport.height;
      const margin = 50;
      const mirrorSign = this.mapMirrorX ? -1 : 1;
      const inverseRotation = -((this.mapRotation % 360) * Math.PI / 180);
@@ -1821,8 +1980,9 @@
      const contentW = size.width || 0;
      const contentH = size.height || 0;
      if (contentW <= 0 || contentH <= 0) { return; }
      const vw = this.pixiApp.view.width;
      const vh = this.pixiApp.view.height;
      const viewport = this.getViewportSize();
      const vw = viewport.width;
      const vh = viewport.height;
      let scale = Math.min(vw / contentW, vh / contentH) * 0.95;
      if (!isFinite(scale) || scale <= 0) { scale = 1; }
      const baseW = this.mapContentSize.width || contentW;
@@ -1856,12 +2016,6 @@
    }
  }
});
src/main/webapp/components/WatchDualCrnCard.js
@@ -47,8 +47,28 @@
              <el-descriptions border direction="vertical">
                <el-descriptions-item label="模式">{{ item.mode }}</el-descriptions-item>
                <el-descriptions-item label="异常码">{{ item.warnCode }}</el-descriptions-item>
                <el-descriptions-item label="工位1任务号">{{ item.taskNo }}</el-descriptions-item>
                <el-descriptions-item label="工位2任务号">{{ item.taskNoTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1任务号">
                  <span v-if="readOnly">{{ item.taskNo }}</span>
                  <el-button
                    v-else
                    type="text"
                    size="mini"
                    style="padding:0;"
                    @click.stop="editTaskNo(item, 1)"
                  >{{ item.taskNo }}</el-button>
                </el-descriptions-item>
                <el-descriptions-item label="工位2任务号">
                  <span v-if="readOnly">{{ item.taskNoTwo }}</span>
                  <el-button
                    v-else
                    type="text"
                    size="mini"
                    style="padding:0;"
                    @click.stop="editTaskNo(item, 2)"
                  >{{ item.taskNoTwo }}</el-button>
                </el-descriptions-item>
                <el-descriptions-item label="设备工位1任务号">{{ item.deviceTaskNo }}</el-descriptions-item>
                <el-descriptions-item label="设备工位2任务号">{{ item.deviceTaskNoTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1状态">{{ item.status }}</el-descriptions-item>
                <el-descriptions-item label="工位2状态">{{ item.statusTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1是否有物">{{ item.loading }}</el-descriptions-item>
@@ -57,6 +77,8 @@
                <el-descriptions-item label="工位2货叉定位">{{ item.forkOffsetTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1任务接收">{{ item.taskReceive }}</el-descriptions-item>
                <el-descriptions-item label="工位2任务接收">{{ item.taskReceiveTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1下发数据">{{ item.taskSend }}</el-descriptions-item>
                <el-descriptions-item label="工位2下发数据">{{ item.taskSendTwo }}</el-descriptions-item>
                <el-descriptions-item label="列">{{ item.bay }}</el-descriptions-item>
                <el-descriptions-item label="层">{{ item.lev }}</el-descriptions-item>
                <el-descriptions-item label="载货台定位">{{ item.liftPos }}</el-descriptions-item>
@@ -161,6 +183,50 @@
    openControl() {
      this.showControl = !this.showControl;
    },
    editTaskNo(item, station) {
      let that = this;
      const isStationOne = station === 1;
      const fieldName = isStationOne ? "taskNo" : "taskNoTwo";
      const stationName = isStationOne ? "工位1" : "工位2";
      const currentTaskNo = item[fieldName] == null ? "" : String(item[fieldName]);
      that.$prompt("请输入" + stationName + "任务号", "编辑任务号", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        inputValue: currentTaskNo,
        inputPattern: /^\d+$/,
        inputErrorMessage: "任务号必须是非负整数",
      }).then(({ value }) => {
        const taskNo = Number(value);
        $.ajax({
          url: baseUrl + "/dualcrn/command/updateTaskNo",
          headers: {
            token: localStorage.getItem("token"),
          },
          contentType: "application/json",
          method: "post",
          data: JSON.stringify({
            crnNo: item.crnNo,
            station: station,
            taskNo: taskNo,
          }),
          success: (res) => {
            if (res.code == 200) {
              item[fieldName] = taskNo;
              that.$message({
                message: stationName + "任务号更新成功",
                type: "success",
              });
              that.getDualCrnStateInfo();
            } else {
              that.$message({
                message: res.msg,
                type: "warning",
              });
            }
          },
        });
      }).catch(() => {});
    },
    getDualCrnStateInfo() {
      if (this.$root.sendWs) {
        this.$root.sendWs(JSON.stringify({
src/main/webapp/static/js/basDevp/basDevp.js
@@ -38,6 +38,8 @@
          { field: "inStationList", align: "center", title: "入库站点数据" },
          { field: "outStationList", align: "center", title: "出库站点数据" },
          { field: "runBlockReassignLocStationList", align: "center", title: "运行堵塞重新分配库位站点数据" },
          { field: "isOutOrderList", align: "center", title: "出库排序交互点" },
          { field: "isLiftTransferList", align: "center", title: "顶升移栽点" },
          {
            fixed: "right",
src/main/webapp/views/basDevp/basDevp.html
@@ -149,6 +149,18 @@
                        <input class="layui-input" name="runBlockReassignLocStationList" placeholder="请输入运行堵塞重新分配库位站点数据">
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label">出库排序交互点: </label>
                    <div class="layui-input-block">
                        <input class="layui-input" name="isOutOrderList" placeholder="请输入出库排序交互点">
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label">顶升移栽点: </label>
                    <div class="layui-input-block">
                        <input class="layui-input" name="isLiftTransferList" placeholder="请输入电梯中转点">
                    </div>
                </div>
             </div>
        </div>
@@ -188,4 +200,3 @@
    </form>
</script>
</html>
src/main/webapp/views/locMap/locMap.html
@@ -59,9 +59,9 @@
  >
    <div v-if="drawerLocNoData!=null">
      <div style="margin: 10px;">
        <div style="margin-top: 5px;">排:{{drawerLocNoData.row}}</div>
        <div style="margin-top: 5px;">列:{{drawerLocNoData.bay}}</div>
        <div style="margin-top: 5px;">层:{{drawerLocNoData.lev}}</div>
<!--        <div style="margin-top: 5px;">排:{{drawerLocNoData.row}}</div>-->
<!--        <div style="margin-top: 5px;">列:{{drawerLocNoData.bay}}</div>-->
<!--        <div style="margin-top: 5px;">层:{{drawerLocNoData.lev}}</div>-->
        <div style="margin-top: 5px;">库位号:{{drawerLocNoData.locNo}}</div>
        <div style="margin-top: 5px;">库位状态:{{drawerLocNoData.locSts}}</div>
      </div>
@@ -101,7 +101,12 @@
  let pixiStageList = [];
  let pixiStaMap = new Map();
  let objectsContainer;
  let objectsContainer3;
  let tracksGraphics;
  let mapRoot;
  let mapContentSize = { width: 0, height: 0 };
  let graphics0;
  let graphicsF;
  let graphics3;
  let graphics4;
  let graphics5;
@@ -125,6 +130,12 @@
      currentLevStaList: [],//当前楼层站点list
      drawerSta: false,
      drawerStaData: null,
      mapRotation: 0,
      mapMirrorX: false,
      mapConfigCodes: {
        rotate: 'map_canvas_rotation',
        mirror: 'map_canvas_mirror_x'
      }
    },
    mounted() {
      this.init()
@@ -157,6 +168,7 @@
        ws.onclose = this.webSocketClose
        this.loadMapTransformConfig()
        this.initLev()//初始化楼层信息
        setTimeout(() => {
          that.getMap(this.currentLev)
@@ -205,6 +217,7 @@
      },
      changeFloor(lev) {
        this.currentLev = lev
        this.loadMapTransformConfig()
        this.reloadMap = true
        this.getMap(lev)
      },
@@ -229,13 +242,18 @@
        graphics67 = pixiApp.renderer.generateTexture(getContainer(67));
        graphicsLock = pixiApp.renderer.generateTexture(getContainer(-999));
        mapRoot = new PIXI.Container();
        pixiApp.stage.addChild(mapRoot);
        // 创建一个容器来管理大批量的显示对象
        objectsContainer = new PIXI.Container();
        pixiApp.stage.addChild(objectsContainer);
        mapRoot.addChild(objectsContainer);
        tracksGraphics = new PIXI.Graphics();
        mapRoot.addChild(tracksGraphics);
        // 创建一个容器来管理大批量的显示对象
        objectsContainer3 = new PIXI.Container();
        pixiApp.stage.addChild(objectsContainer3);
        mapRoot.addChild(objectsContainer3);
        //*******************拖动画布*******************
        let stageOriginalPos;
@@ -285,16 +303,22 @@
        pixiApp.view.addEventListener('wheel', (event) => {
          event.stopPropagation();
          event.preventDefault();
          const sx = event.clientX;
          const sy = event.clientY;
          const oldZoom = pixiApp.stage.scale.x;
          const rect = pixiApp.view.getBoundingClientRect();
          const sx = event.clientX - rect.left;
          const sy = event.clientY - rect.top;
          const oldZoomX = pixiApp.stage.scale.x || 1;
          const oldZoomY = pixiApp.stage.scale.y || 1;
          const oldZoomAbs = Math.abs(oldZoomX) || 1;
          const delta = event.deltaY;
          let newZoom = oldZoom * 0.999 ** delta;
          const worldX = (sx - pixiApp.stage.position.x) / oldZoom;
          const worldY = (sy - pixiApp.stage.position.y) / oldZoom;
          const newPosX = sx - worldX * newZoom;
          const newPosY = sy - worldY * newZoom;
          pixiApp.stage.setTransform(newPosX, newPosY, newZoom, newZoom, 0, 0, 0, 0, 0);
          let newZoomAbs = oldZoomAbs * 0.999 ** delta;
          const mirrorX = this.mapMirrorX ? -1 : 1;
          const newZoomX = mirrorX * newZoomAbs;
          const newZoomY = newZoomAbs;
          const worldX = (sx - pixiApp.stage.position.x) / oldZoomX;
          const worldY = (sy - pixiApp.stage.position.y) / oldZoomY;
          const newPosX = sx - worldX * newZoomX;
          const newPosY = sy - worldY * newZoomY;
          pixiApp.stage.setTransform(newPosX, newPosY, newZoomX, newZoomY, 0, 0, 0, 0, 0);
        });
        //*******************缩放画布*******************
@@ -328,6 +352,7 @@
          pixiStageList = [map.length]//初始化列表
          pixiStaMap = new Map();//重置
          objectsContainer.removeChildren()
          if (tracksGraphics) { tracksGraphics.clear(); }
          map.forEach((item,index) => {
            pixiStageList[index] = [item.length]
            for (let idx = 0; idx < item.length; idx++) {
@@ -388,21 +413,160 @@
          });
          const b1 = objectsContainer.getLocalBounds();
          const minX = Math.min(b1.x);
          const minY = Math.min(b1.y);
          const maxX = Math.max(b1.x + b1.width);
          const maxY = Math.max(b1.y + b1.height);
          const minX = b1.x;
          const minY = b1.y;
          const maxX = b1.x + b1.width;
          const maxY = b1.y + b1.height;
          const contentW = Math.max(0, maxX - minX);
          const contentH = Math.max(0, maxY - minY);
          const vw = pixiApp.view.width;
          const vh = pixiApp.view.height;
          let scale = Math.min(vw / contentW, vh / contentH) * 0.95;
          if (!isFinite(scale) || scale <= 0) { scale = 1; }
          const posX = (vw - contentW * scale) / 2 - minX * scale;
          const posY = (vh - contentH * scale) / 2 - minY * scale;
          pixiApp.stage.setTransform(posX, posY, scale, scale, 0, 0, 0, 0, 0);
          mapContentSize = { width: contentW, height: contentH };
          this.drawTracks(map);
          this.applyMapTransform(true);
        }
        this.map = map;
      },
      isTrackCell(cell) {
        if (!cell) { return false; }
        const type = cell.type ? String(cell.type).toLowerCase() : '';
        if (type === 'track' || type === 'crn' || type === 'dualcrn' || type === 'rgv') { return true; }
        if (cell.trackSiteNo != null) { return true; }
        const v = parseInt(cell.value, 10);
        if (v === 3 || v === 9) { return true; }
        if (cell.value != null) {
          try {
            const obj = (typeof cell.value === 'string') ? JSON.parse(cell.value) : cell.value;
            if (obj && (obj.trackSiteNo != null || (obj.deviceNo != null && (type === 'crn' || type === 'dualcrn' || type === 'rgv')))) {
              return true;
            }
          } catch (e) {}
        }
        return false;
      },
      drawTracks(map) {
        if (!tracksGraphics || !Array.isArray(map)) { return; }
        tracksGraphics.clear();
        const railColor = 0x6c727a;
        const railWidth = Math.max(1, Math.round(Math.min(width, height) * 0.08));
        tracksGraphics.lineStyle(railWidth, railColor, 1);
        for (let r = 0; r < map.length; r++) {
          const row = map[r];
          if (!Array.isArray(row)) { continue; }
          for (let c = 0; c < row.length; c++) {
            const cell = row[c];
            if (!this.isTrackCell(cell)) { continue; }
            const cx = c * width + width / 2;
            const cy = r * height + height / 2;
            const up = (r - 1 >= 0 && Array.isArray(map[r - 1])) ? map[r - 1][c] : null;
            const right = (c + 1 < row.length) ? row[c + 1] : null;
            const down = (r + 1 < map.length && Array.isArray(map[r + 1])) ? map[r + 1][c] : null;
            const left = (c - 1 >= 0) ? row[c - 1] : null;
            const hasN = this.isTrackCell(up);
            const hasE = this.isTrackCell(right);
            const hasS = this.isTrackCell(down);
            const hasW = this.isTrackCell(left);
            const seg = Math.min(width, height) * 0.5;
            let drew = false;
            if (hasN) { tracksGraphics.moveTo(cx, cy); tracksGraphics.lineTo(cx, cy - seg); drew = true; }
            if (hasE) { tracksGraphics.moveTo(cx, cy); tracksGraphics.lineTo(cx + seg, cy); drew = true; }
            if (hasS) { tracksGraphics.moveTo(cx, cy); tracksGraphics.lineTo(cx, cy + seg); drew = true; }
            if (hasW) { tracksGraphics.moveTo(cx, cy); tracksGraphics.lineTo(cx - seg, cy); drew = true; }
            if (!drew) {
              tracksGraphics.moveTo(cx - seg * 0.4, cy);
              tracksGraphics.lineTo(cx + seg * 0.4, cy);
            }
          }
        }
      },
      parseRotation(value) {
        const num = parseInt(value, 10);
        if (!isFinite(num)) { return 0; }
        const rot = ((num % 360) + 360) % 360;
        return (rot === 90 || rot === 180 || rot === 270) ? rot : 0;
      },
      parseMirror(value) {
        if (value === true || value === false) { return value; }
        if (value == null) { return false; }
        const str = String(value).toLowerCase();
        return str === '1' || str === 'true' || str === 'y';
      },
      loadMapTransformConfig() {
        if (!window.$ || typeof baseUrl === 'undefined') { return; }
        $.ajax({
          url: baseUrl + "/config/listAll/auth",
          headers: { 'token': localStorage.getItem('token') },
          dataType: 'json',
          method: 'GET',
          success: (res) => {
            if (!res || res.code !== 200 || !Array.isArray(res.data)) {
              if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; }
              return;
            }
            const byCode = {};
            res.data.forEach((item) => {
              if (item && item.code) { byCode[item.code] = item; }
            });
            const rotateCfg = byCode[this.mapConfigCodes.rotate];
            const mirrorCfg = byCode[this.mapConfigCodes.mirror];
            if (rotateCfg && rotateCfg.value != null) {
              this.mapRotation = this.parseRotation(rotateCfg.value);
            }
            if (mirrorCfg && mirrorCfg.value != null) {
              this.mapMirrorX = this.parseMirror(mirrorCfg.value);
            }
            if (mapContentSize && mapContentSize.width > 0 && mapContentSize.height > 0) {
              this.applyMapTransform(true);
            }
          }
        });
      },
      getViewportSize() {
        if (!pixiApp || !pixiApp.renderer) { return { width: 0, height: 0 }; }
        const screen = pixiApp.renderer.screen;
        if (screen && screen.width > 0 && screen.height > 0) {
          return { width: screen.width, height: screen.height };
        }
        const rect = pixiApp.view ? pixiApp.view.getBoundingClientRect() : null;
        return { width: rect ? rect.width : 0, height: rect ? rect.height : 0 };
      },
      getTransformedContentSize() {
        const size = mapContentSize || { width: 0, height: 0 };
        const w = size.width || 0;
        const h = size.height || 0;
        const rot = ((this.mapRotation % 360) + 360) % 360;
        const swap = rot === 90 || rot === 270;
        return { width: swap ? h : w, height: swap ? w : h };
      },
      fitStageToContent() {
        if (!pixiApp || !mapContentSize) { return; }
        const size = this.getTransformedContentSize();
        const contentW = size.width || 0;
        const contentH = size.height || 0;
        if (contentW <= 0 || contentH <= 0) { return; }
        const viewport = this.getViewportSize();
        const vw = viewport.width;
        const vh = viewport.height;
        let scale = Math.min(vw / contentW, vh / contentH) * 0.95;
        if (!isFinite(scale) || scale <= 0) { scale = 1; }
        const baseW = mapContentSize.width || contentW;
        const baseH = mapContentSize.height || contentH;
        const mirrorX = this.mapMirrorX ? -1 : 1;
        const scaleX = scale * mirrorX;
        const scaleY = scale;
        const posX = (vw / 2) - (baseW / 2) * scaleX;
        const posY = (vh / 2) - (baseH / 2) * scaleY;
        pixiApp.stage.setTransform(posX, posY, scaleX, scaleY, 0, 0, 0, 0, 0);
      },
      applyMapTransform(fitToView) {
        if (!mapRoot || !mapContentSize) { return; }
        const contentW = mapContentSize.width || 0;
        const contentH = mapContentSize.height || 0;
        if (contentW <= 0 || contentH <= 0) { return; }
        mapRoot.pivot.set(contentW / 2, contentH / 2);
        mapRoot.position.set(contentW / 2, contentH / 2);
        mapRoot.rotation = (this.mapRotation % 360) * Math.PI / 180;
        mapRoot.scale.set(1, 1);
        if (fitToView) { this.fitStageToContent(); }
      },
      rightEvent(x, y, e) {
        this.drawerLocNo = true
@@ -520,12 +684,16 @@
      sprite = new PIXI.Sprite(graphics0);
    }
    sprite.position.set(x, y);
    sprite.interactive = true; // 必须要设置才能接收事件
    sprite.buttonMode = true; // 让光标在hover时变为手型指针
    sprite.on('pointerdown', (e) => {
      pointerDownEvent(e)
    })
    const type = item && item.type ? String(item.type).toLowerCase() : '';
    const numVal = parseInt(value, 10);
    const isTrackCell = numVal === 3 || numVal === 9 || type === 'track' || type === 'crn' || type === 'dualcrn' || type === 'rgv';
    if (!isTrackCell) {
      sprite.interactive = true; // 必须要设置才能接收事件
      sprite.buttonMode = true; // 让光标在hover时变为手型指针
      sprite.on('pointerdown', (e) => {
        pointerDownEvent(e)
      })
    }
    return sprite;
  }
@@ -542,4 +710,4 @@
</script>
</body>
</html>
</html>
src/main/webapp/views/login.html
@@ -184,7 +184,6 @@
        $('#btn-server-info').click(function() {
            $.ajax({
                url: baseUrl + "/license/getServerInfos",
                headers: {'token': localStorage.getItem('token')},
                method: 'GET',
                success: function (res) {
                    var pretty = '';
@@ -254,7 +253,6 @@
                layer.close(index);
                $.ajax({
                    url: baseUrl + "/license/activate",
                    headers: {'token': localStorage.getItem('token')},
                    method: 'POST',
                    success: function (res) {
                        if (res.code === 200){
@@ -275,7 +273,6 @@
        $('#btn-project-name').click(function() {
            $.ajax({
                url: baseUrl + "/license/getProjectName",
                headers: {'token': localStorage.getItem('token')},
                method: 'GET',
                success: function (res) {
                    if (res.code === 200){
src/main/webapp/views/watch/console.html
@@ -1,15 +1,23 @@
<!DOCTYPE html>
<html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>WCS控制中心</title>
            <link rel="stylesheet" href="../../static/css/animate.min.css">
            <link rel="stylesheet" href="../../static/vue/element/element.css">
            <link rel="stylesheet" href="../../static/css/watch/console_vue.css">
            <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
            <script type="text/javascript" src="../../static/layui/layui.js"></script>
            <script type="text/javascript" src="../../static/js/handlebars/handlebars-v4.5.3.js"></script>
            <script type="text/javascript" src="../../static/js/common.js"></script>
            <head>
                <meta charset="UTF-8">
                <title>WCS控制中心</title>
                <link rel="stylesheet" href="../../static/css/animate.min.css">
                <link rel="stylesheet" href="../../static/vue/element/element.css">
                <link rel="stylesheet" href="../../static/css/watch/console_vue.css">
                <style>
                    html, body, #app {
                        width: 100%;
                        height: 100%;
                        margin: 0;
                        overflow: hidden;
                    }
                </style>
                <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
                <script type="text/javascript" src="../../static/layui/layui.js"></script>
                <script type="text/javascript" src="../../static/js/handlebars/handlebars-v4.5.3.js"></script>
                <script type="text/javascript" src="../../static/js/common.js"></script>
            <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
            <script type="text/javascript" src="../../static/vue/element/element.js"></script>
            <script src="../../static/js/gsap.min.js"></script>