From aa151079c2d02047f6cb5f8ad56ff92b98544e99 Mon Sep 17 00:00:00 2001
From: Junjie <DELL@qq.com>
Date: 星期三, 14 一月 2026 08:30:49 +0800
Subject: [PATCH] #

---
 src/main/java/com/zy/core/utils/StationOperateProcessUtils.java     |   27 
 src/main/java/com/zy/asrs/planner/PlannerOrtoolsSolverService.java  |  701 +++++++++++++++++++++
 src/main/java/com/zy/asrs/service/PlannerService.java               |    7 
 src/main/java/com/zy/asrs/task/PlannerScheduler.java                |   25 
 src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java       |   52 
 src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java         |  337 ++++++++++
 src/main/java/com/zy/core/model/command/CrnCommand.java             |   22 
 pom.xml                                                             |    6 
 src/main/java/com/zy/asrs/controller/PlannerController.java         |   25 
 src/main/java/com/zy/asrs/task/PlannerExecutor.java                 |   20 
 src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java      |  548 +++++++++++++++++
 src/main/java/com/zy/core/MainProcess.java                          |   45 +
 src/main/java/com/zy/core/enums/RedisKeyType.java                   |    1 
 src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java |   68 +
 src/main/resources/application.yml                                  |    2 
 src/main/java/com/zy/core/network/real/ZyCrnRealConnect.java        |   16 
 16 files changed, 1,830 insertions(+), 72 deletions(-)

diff --git a/pom.xml b/pom.xml
index 80b8f0e..a5e538c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,6 +20,7 @@
         <mybatis-plus.version>2.3.2</mybatis-plus.version>
         <fastjson.version>1.2.83</fastjson.version>
         <springfox.version>2.7.0</springfox.version>
+        <ortools.version>9.10.4067</ortools.version>
     </properties>
 
     <dependencies>
@@ -135,6 +136,11 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.google.ortools</groupId>
+            <artifactId>ortools-java</artifactId>
+            <version>${ortools.version}</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/com/zy/asrs/controller/PlannerController.java b/src/main/java/com/zy/asrs/controller/PlannerController.java
new file mode 100644
index 0000000..3c3ad44
--- /dev/null
+++ b/src/main/java/com/zy/asrs/controller/PlannerController.java
@@ -0,0 +1,25 @@
+package com.zy.asrs.controller;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.alibaba.fastjson.JSONObject;
+import com.core.common.R;
+import com.zy.asrs.service.PlannerService;
+
+@RestController
+public class PlannerController {
+
+    @Autowired
+    private PlannerService plannerService;
+
+    @GetMapping("/planner/calc")
+    public R plannerCalc() {
+        JSONObject result = plannerService.calculateAndSaveSchedule();
+        if (result == null) {
+            return R.error("姹傝В澶辫触");
+        }
+        return R.ok(result);
+    }
+}
diff --git a/src/main/java/com/zy/asrs/planner/PlannerOrtoolsSolverService.java b/src/main/java/com/zy/asrs/planner/PlannerOrtoolsSolverService.java
new file mode 100644
index 0000000..5de21a2
--- /dev/null
+++ b/src/main/java/com/zy/asrs/planner/PlannerOrtoolsSolverService.java
@@ -0,0 +1,701 @@
+package com.zy.asrs.planner;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.google.ortools.Loader;
+import com.google.ortools.sat.BoolVar;
+import com.google.ortools.sat.CircuitConstraint;
+import com.google.ortools.sat.CpModel;
+import com.google.ortools.sat.CpSolver;
+import com.google.ortools.sat.CpSolverStatus;
+import com.google.ortools.sat.CumulativeConstraint;
+import com.google.ortools.sat.IntervalVar;
+import com.google.ortools.sat.IntVar;
+import com.google.ortools.sat.LinearExpr;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class PlannerOrtoolsSolverService {
+
+    private static volatile boolean loaded = false;
+
+    public JSONObject solve(JSONObject req) {
+        ensureLoaded();
+
+        JSONArray cranesIn = req.getJSONArray("cranes");
+        JSONArray tasksIn = req.getJSONArray("tasks");
+        JSONObject cfgIn = req.getJSONObject("config");
+
+        if (cranesIn == null || cranesIn.isEmpty()) {
+            throw new IllegalArgumentException("cranes is empty");
+        }
+        if (tasksIn == null || tasksIn.isEmpty()) {
+            JSONObject out = new JSONObject();
+            out.put("schedule", new JSONArray());
+            out.put("objectiveValue", 0);
+            return out;
+        }
+
+        List<String> craneCodes = new ArrayList<>();
+        Map<String, JSONObject> craneByCode = new HashMap<>();
+        for (int i = 0; i < cranesIn.size(); i++) {
+            JSONObject c = cranesIn.getJSONObject(i);
+            if (c == null) continue;
+            String code = c.getString("code");
+            if (code == null || code.trim().isEmpty()) continue;
+            craneCodes.add(code);
+            craneByCode.put(code, c);
+        }
+        if (craneCodes.isEmpty()) {
+            throw new IllegalArgumentException("cranes is empty");
+        }
+
+        int outMinGapSec = optInt(cfgIn, "outMinGapSec", 0);
+        int wMakespan = optInt(cfgIn, "wMakespan", 1000);
+        int wPriorityEarlyFinish = optInt(cfgIn, "wPriorityEarlyFinish", 5);
+        int wWaitTime = optInt(cfgIn, "wWaitTime", 1);
+        int wOutTardiness = optInt(cfgIn, "wOutTardiness", 0);
+        boolean enableOutDue = optBool(cfgIn, "enableOutDue", false);
+        int outTaktSec = optInt(cfgIn, "outTaktSec", 20);
+        double maxSolveSeconds = optDouble(cfgIn, "maxSolveSeconds", 5.0);
+        int numSearchWorkers = optInt(cfgIn, "numSearchWorkers", 8);
+        boolean logSearchProgress = optBool(cfgIn, "logSearchProgress", false);
+        int horizonPaddingSec = optInt(cfgIn, "horizonPaddingSec", 3600);
+        int conveyorStationTaskLimit = optInt(cfgIn, "conveyorStationTaskLimit", 0);
+        int conveyorStationRunningTasks = optInt(cfgIn, "conveyorStationRunningTasks", 0);
+
+        double conveyorDefaultSpeedMps = optDouble(req, "conveyorDefaultSpeedMps", 0.6);
+        if (conveyorDefaultSpeedMps <= 0) conveyorDefaultSpeedMps = 0.6;
+
+        List<Integer> taskIds = new ArrayList<>();
+        Map<Integer, JSONObject> taskById = new HashMap<>();
+        for (int i = 0; i < tasksIn.size(); i++) {
+            JSONObject t = tasksIn.getJSONObject(i);
+            if (t == null) continue;
+            Integer taskId = t.getInteger("taskId");
+            if (taskId == null || taskId <= 0) continue;
+            taskIds.add(taskId);
+            taskById.put(taskId, t);
+        }
+        if (taskIds.isEmpty()) {
+            JSONObject out = new JSONObject();
+            out.put("schedule", new JSONArray());
+            out.put("objectiveValue", 0);
+            return out;
+        }
+
+        Map<Integer, Integer> nodeByTaskId = new HashMap<>();
+        int nodeIdx = 1;
+        for (Integer taskId : taskIds) {
+            nodeByTaskId.put(taskId, nodeIdx);
+            nodeIdx++;
+        }
+
+        Map<Integer, Double> fromX = new HashMap<>();
+        Map<Integer, Double> fromY = new HashMap<>();
+        Map<Integer, Double> toX = new HashMap<>();
+        Map<Integer, Double> toY = new HashMap<>();
+        double minX = Double.POSITIVE_INFINITY;
+        double maxX = Double.NEGATIVE_INFINITY;
+        double minY = Double.POSITIVE_INFINITY;
+        double maxY = Double.NEGATIVE_INFINITY;
+        for (Integer taskId : taskIds) {
+            JSONObject t = taskById.get(taskId);
+            JSONObject fp = t == null ? null : t.getJSONObject("fromPos");
+            JSONObject tp = t == null ? null : t.getJSONObject("toPos");
+            double fx = optDouble(fp, "x", 0);
+            double fy = optDouble(fp, "y", 0);
+            double tx = optDouble(tp, "x", 0);
+            double ty = optDouble(tp, "y", 0);
+            fromX.put(taskId, fx);
+            fromY.put(taskId, fy);
+            toX.put(taskId, tx);
+            toY.put(taskId, ty);
+            minX = Math.min(minX, Math.min(fx, tx));
+            maxX = Math.max(maxX, Math.max(fx, tx));
+            minY = Math.min(minY, Math.min(fy, ty));
+            maxY = Math.max(maxY, Math.max(fy, ty));
+        }
+        if (!Double.isFinite(minX)) {
+            minX = 0;
+            maxX = 0;
+            minY = 0;
+            maxY = 0;
+        }
+
+        Map<Integer, Integer> outConv = new HashMap<>();
+        Map<String, Integer> baseDur = new HashMap<>();
+        int maxReady = 0;
+        int setupBoundPerTask = 0;
+        for (String cc : craneCodes) {
+            JSONObject c = craneByCode.get(cc);
+            int ready = optInt(c, "readySec", 0);
+            if (ready > maxReady) maxReady = ready;
+            int bound = craneMoveTimeSecForDelta(c, Math.abs(maxX - minX), Math.abs(maxY - minY));
+            setupBoundPerTask = Math.max(setupBoundPerTask, bound);
+        }
+        for (Integer taskId : taskIds) {
+            JSONObject t = taskById.get(taskId);
+            outConv.put(taskId, conveyorTimeSec(t, conveyorDefaultSpeedMps));
+            for (String cc : eligibleCodes(t, craneCodes, craneByCode)) {
+                baseDur.put(durKey(taskId, cc), craneBaseExecTimeSec(craneByCode.get(cc), t));
+            }
+        }
+
+        int horizon = estimateHorizon(taskIds, taskById, craneCodes, baseDur, outConv, maxReady, horizonPaddingSec, craneByCode, setupBoundPerTask);
+
+        CpModel m = new CpModel();
+
+        Map<Integer, IntVar> startVars = new HashMap<>();
+        Map<Integer, IntVar> endVars = new HashMap<>();
+        Map<String, BoolVar> chosen = new HashMap<>();
+        Map<String, Map<Integer, BoolVar>> presenceByCrane = new HashMap<>();
+        Map<String, List<Integer>> eligibleTasksByCrane = new HashMap<>();
+        for (String cc : craneCodes) {
+            presenceByCrane.put(cc, new HashMap<>());
+            eligibleTasksByCrane.put(cc, new ArrayList<>());
+        }
+        Map<Integer, IntVar> outEtaVars = new HashMap<>();
+        List<IntervalVar> convIntervals = new ArrayList<>();
+
+        for (Integer taskId : taskIds) {
+            JSONObject t = taskById.get(taskId);
+            IntVar s = m.newIntVar(0, horizon, "S_" + taskId);
+            IntVar e = m.newIntVar(0, horizon, "E_" + taskId);
+            startVars.put(taskId, s);
+            endVars.put(taskId, e);
+
+            List<String> eligible = eligibleCodes(t, craneCodes, craneByCode);
+            List<BoolVar> presences = new ArrayList<>();
+            for (String cc : eligible) {
+                BoolVar p = m.newBoolVar("P_" + taskId + "_" + cc);
+                presences.add(p);
+                chosen.put(chosenKey(taskId, cc), p);
+                presenceByCrane.get(cc).put(taskId, p);
+                eligibleTasksByCrane.get(cc).add(taskId);
+
+                int ready = optInt(craneByCode.get(cc), "readySec", 0);
+                m.addGreaterOrEqual(s, ready).onlyEnforceIf(p);
+            }
+            m.addExactlyOne(presences.toArray(new BoolVar[0]));
+
+            String taskType = optStr(t, "taskType", "");
+            if ("OUT".equalsIgnoreCase(taskType)) {
+                IntVar outEta = m.newIntVar(0, horizon, "OUT_" + taskId);
+                outEtaVars.put(taskId, outEta);
+                m.addEquality(outEta, LinearExpr.sum(new LinearExpr[]{
+                        LinearExpr.term(e, 1),
+                        LinearExpr.constant(outConv.get(taskId))
+                }));
+
+                int conv = outConv.get(taskId);
+                IntVar sConv = m.newIntVar(0, horizon, "CS_" + taskId);
+                IntVar eConv = m.newIntVar(0, horizon, "CE_" + taskId);
+                m.addEquality(sConv, e);
+                m.addEquality(eConv, LinearExpr.sum(new LinearExpr[]{
+                        LinearExpr.term(sConv, 1),
+                        LinearExpr.constant(conv)
+                }));
+                IntervalVar itvConv = m.newIntervalVar(sConv, LinearExpr.constant(conv), eConv, "CITV_" + taskId);
+                convIntervals.add(itvConv);
+            }
+        }
+
+        for (String cc : craneCodes) {
+            JSONObject crane = craneByCode.get(cc);
+            int ready = optInt(crane, "readySec", 0);
+            Map<Integer, BoolVar> pMap = presenceByCrane.get(cc);
+            List<Integer> eligibleTasks = eligibleTasksByCrane.get(cc);
+
+            CircuitConstraint circuit = m.addCircuit();
+            BoolVar loop0 = m.newBoolVar("LOOP0_" + cc);
+            circuit.addArc(0, 0, loop0);
+
+            for (Integer tid : taskIds) {
+                int node = nodeByTaskId.get(tid);
+                BoolVar p = pMap.get(tid);
+                if (p != null) {
+                    circuit.addArc(node, node, p.not());
+                    m.addImplication(p, loop0.not());
+                } else {
+                    circuit.addArc(node, node, m.trueLiteral());
+                }
+            }
+
+            for (Integer tid : eligibleTasks) {
+                int nodeJ = nodeByTaskId.get(tid);
+                BoolVar pJ = pMap.get(tid);
+
+                BoolVar arc0j = m.newBoolVar("ARC_" + cc + "_0_" + nodeJ);
+                circuit.addArc(0, nodeJ, arc0j);
+                m.addImplication(arc0j, loop0.not());
+                m.addImplication(arc0j, pJ);
+                int setup0j = craneSetupFromInitSec(crane, fromX.get(tid), fromY.get(tid));
+                int dJ = baseDur.get(durKey(tid, cc));
+                m.addGreaterOrEqual(startVars.get(tid), ready).onlyEnforceIf(arc0j);
+                m.addGreaterOrEqual(endVars.get(tid), LinearExpr.sum(new LinearExpr[]{
+                        LinearExpr.term(startVars.get(tid), 1),
+                        LinearExpr.constant(setup0j + dJ)
+                })).onlyEnforceIf(arc0j);
+
+                BoolVar arcj0 = m.newBoolVar("ARC_" + cc + "_" + nodeJ + "_0");
+                circuit.addArc(nodeJ, 0, arcj0);
+                m.addImplication(arcj0, loop0.not());
+                m.addImplication(arcj0, pJ);
+            }
+
+            for (int i = 0; i < eligibleTasks.size(); i++) {
+                int tidI = eligibleTasks.get(i);
+                int nodeI = nodeByTaskId.get(tidI);
+                BoolVar pI = pMap.get(tidI);
+                for (int j = 0; j < eligibleTasks.size(); j++) {
+                    if (i == j) continue;
+                    int tidJ = eligibleTasks.get(j);
+                    int nodeJ = nodeByTaskId.get(tidJ);
+                    BoolVar pJ = pMap.get(tidJ);
+                    BoolVar arc = m.newBoolVar("ARC_" + cc + "_" + nodeI + "_" + nodeJ);
+                    circuit.addArc(nodeI, nodeJ, arc);
+                    m.addImplication(arc, pI);
+                    m.addImplication(arc, pJ);
+                    int setup = craneMoveTimeSec(crane, toX.get(tidI), toY.get(tidI), fromX.get(tidJ), fromY.get(tidJ));
+                    int dJ = baseDur.get(durKey(tidJ, cc));
+                    m.addGreaterOrEqual(startVars.get(tidJ), endVars.get(tidI)).onlyEnforceIf(arc);
+                    m.addGreaterOrEqual(endVars.get(tidJ), LinearExpr.sum(new LinearExpr[]{
+                            LinearExpr.term(startVars.get(tidJ), 1),
+                            LinearExpr.constant(setup + dJ)
+                    })).onlyEnforceIf(arc);
+                }
+            }
+        }
+
+        if (conveyorStationTaskLimit > 0 && !convIntervals.isEmpty()) {
+            int effCap = Math.max(0, conveyorStationTaskLimit - Math.max(0, conveyorStationRunningTasks));
+            if (effCap > 0) {
+                CumulativeConstraint cumul = m.addCumulative(effCap);
+                for (IntervalVar itv : convIntervals) {
+                    cumul.addDemand(itv, 1);
+                }
+            }
+        }
+
+        addOutboundOrderConstraints(m, taskIds, taskById, outEtaVars, outMinGapSec);
+
+        IntVar makespan = m.newIntVar(0, horizon, "MAKESPAN");
+        List<IntVar> ends = new ArrayList<>();
+        for (Integer tid : taskIds) {
+            ends.add(endVars.get(tid));
+        }
+        m.addMaxEquality(makespan, ends.toArray(new IntVar[0]));
+
+        List<LinearExpr> obj = new ArrayList<>();
+        obj.add(LinearExpr.term(makespan, wMakespan));
+        if (wPriorityEarlyFinish > 0) {
+            for (Integer tid : taskIds) {
+                int pri = optInt(taskById.get(tid), "priority", 5);
+                if (pri < 0) pri = 0;
+                long coeff = (long) wPriorityEarlyFinish * (long) pri;
+                if (coeff != 0) {
+                    obj.add(LinearExpr.term(endVars.get(tid), coeff));
+                }
+            }
+        }
+        if (wWaitTime > 0) {
+            long now = System.currentTimeMillis();
+            for (Integer tid : taskIds) {
+                long createTime = optLong(taskById.get(tid), "createTime", now);
+                long waitSeconds = Math.max(0, (now - createTime) / 1000);
+                long coeff = (long) wWaitTime * waitSeconds; // 绛夊緟鏃堕棿瓒婇暱锛屾潈閲嶈秺澶э紝瓒婂笇鏈涘敖鏃╁畬鎴�
+                // 杩欓噷鎴戜滑甯屾湜灏芥棭瀹屾垚绛夊緟鏃堕棿闀跨殑浠诲姟锛屾墍浠ュ皢瀹屾垚鏃堕棿涔樹互绛夊緟鏃堕棿浣滀负鎯╃綒椤�
+                // 鎯╃綒 = endVar * wWaitTime * waitSeconds
+                if (coeff > 0) {
+                    obj.add(LinearExpr.term(endVars.get(tid), coeff));
+                }
+            }
+        }
+        if (enableOutDue && wOutTardiness > 0) {
+            IntVar tardSum = addOutboundDueTardiness(m, taskIds, taskById, outEtaVars, outTaktSec);
+            obj.add(LinearExpr.term(tardSum, wOutTardiness));
+        }
+        m.minimize(LinearExpr.sum(obj.toArray(new LinearExpr[0])));
+
+        CpSolver solver = new CpSolver();
+        solver.getParameters().setMaxTimeInSeconds(maxSolveSeconds);
+        solver.getParameters().setNumSearchWorkers(numSearchWorkers);
+        solver.getParameters().setLogSearchProgress(logSearchProgress);
+
+        CpSolverStatus status = solver.solve(m);
+        if (status != CpSolverStatus.OPTIMAL && status != CpSolverStatus.FEASIBLE) {
+            throw new IllegalStateException("No feasible solution");
+        }
+
+        JSONArray schedule = new JSONArray();
+        for (Integer tid : taskIds) {
+            JSONObject t = taskById.get(tid);
+            int s = (int) solver.value(startVars.get(tid));
+            int e = (int) solver.value(endVars.get(tid));
+
+            String chosenCrane = null;
+            for (String cc : eligibleCodes(t, craneCodes, craneByCode)) {
+                BoolVar p = chosen.get(chosenKey(tid, cc));
+                if (p != null && solver.booleanValue(p)) {
+                    chosenCrane = cc;
+                    break;
+                }
+            }
+            if (chosenCrane == null) {
+                throw new IllegalStateException("internal error: task " + tid + " no crane chosen");
+            }
+            int d = (int) (solver.value(endVars.get(tid)) - solver.value(startVars.get(tid)));
+
+            JSONObject item = new JSONObject();
+            item.put("craneCode", chosenCrane);
+            item.put("taskId", tid);
+            item.put("taskType", optStr(t, "taskType", ""));
+            item.put("startSec", s);
+            item.put("endSec", e);
+            item.put("durationSec", d);
+            item.put("priority", optInt(t, "priority", 5));
+            if ("OUT".equalsIgnoreCase(optStr(t, "taskType", ""))) {
+                IntVar outEta = outEtaVars.get(tid);
+                if (outEta != null) {
+                    item.put("outEtaSec", (int) solver.value(outEta));
+                }
+            }
+            schedule.add(item);
+        }
+
+        schedule.sort(new Comparator<Object>() {
+            @Override
+            public int compare(Object o1, Object o2) {
+                JSONObject a = (JSONObject) JSONObject.toJSON(o1);
+                JSONObject b = (JSONObject) JSONObject.toJSON(o2);
+                String ca = a.getString("craneCode");
+                String cb = b.getString("craneCode");
+                int cmp = (ca == null ? "" : ca).compareTo(cb == null ? "" : cb);
+                if (cmp != 0) return cmp;
+                return Integer.compare(a.getIntValue("startSec"), b.getIntValue("startSec"));
+            }
+        });
+
+        JSONObject out = new JSONObject();
+        out.put("schedule", schedule);
+        out.put("objectiveValue", solver.objectiveValue());
+        return out;
+    }
+
+    private static void ensureLoaded() {
+        if (loaded) return;
+        synchronized (PlannerOrtoolsSolverService.class) {
+            if (loaded) return;
+            Loader.loadNativeLibraries();
+            loaded = true;
+        }
+    }
+
+    private static String chosenKey(int taskId, String cc) {
+        return taskId + "|" + cc;
+    }
+
+    private static String durKey(int taskId, String cc) {
+        return taskId + "|" + cc;
+    }
+
+    private static int estimateHorizon(List<Integer> taskIds,
+                                       Map<Integer, JSONObject> taskById,
+                                       List<String> craneCodes,
+                                       Map<String, Integer> dur,
+                                       Map<Integer, Integer> outConv,
+                                       int maxReady,
+                                       int padding,
+                                       Map<String, JSONObject> craneByCode,
+                                       int setupBoundPerTask) {
+        int total = 0;
+        for (Integer tid : taskIds) {
+            JSONObject t = taskById.get(tid);
+            List<String> elig = eligibleCodes(t, craneCodes, craneByCode);
+            int best = Integer.MAX_VALUE;
+            for (String cc : elig) {
+                Integer d = dur.get(durKey(tid, cc));
+                if (d != null && d < best) best = d;
+            }
+            if (best != Integer.MAX_VALUE) total += best;
+            total += Math.max(0, setupBoundPerTask);
+            Integer conv = outConv.get(tid);
+            if (conv != null) total += conv;
+        }
+        return total + maxReady + Math.max(0, padding);
+    }
+
+    private static List<String> eligibleCodes(JSONObject task, List<String> craneCodes, Map<String, JSONObject> craneByCode) {
+        JSONArray arr = task.getJSONArray("eligibleCranes");
+        if (arr == null || arr.isEmpty()) {
+            return new ArrayList<>(craneCodes);
+        }
+        List<String> out = new ArrayList<>();
+        for (int i = 0; i < arr.size(); i++) {
+            String cc = arr.getString(i);
+            if (cc != null && craneByCode.containsKey(cc)) {
+                out.add(cc);
+            }
+        }
+        if (out.isEmpty()) {
+            throw new IllegalArgumentException("Task " + task.getInteger("taskId") + " has no eligible cranes after filtering");
+        }
+        return out;
+    }
+
+    private static int craneBaseExecTimeSec(JSONObject crane, JSONObject task) {
+        JSONObject fromPos = task.getJSONObject("fromPos");
+        JSONObject toPos = task.getJSONObject("toPos");
+        double fromX = optDouble(fromPos, "x", 0);
+        double fromY = optDouble(fromPos, "y", 0);
+        double toX = optDouble(toPos, "x", 0);
+        double toY = optDouble(toPos, "y", 0);
+        int move = craneMoveTimeSec(crane, fromX, fromY, toX, toY);
+        int pick = optInt(crane, "pickSec", 0);
+        int drop = optInt(crane, "dropSec", 0);
+        return move + pick + drop;
+    }
+
+    private static int craneSetupFromInitSec(JSONObject crane, double toX, double toY) {
+        JSONObject initPos = crane.getJSONObject("initPos");
+        if (initPos == null) {
+            return 0;
+        }
+        double initX = optDouble(initPos, "x", Double.NaN);
+        double initY = optDouble(initPos, "y", Double.NaN);
+        if (Double.isNaN(initX) || Double.isNaN(initY)) {
+            return 0;
+        }
+        return craneMoveTimeSec(crane, initX, initY, toX, toY);
+    }
+
+    private static int craneMoveTimeSecForDelta(JSONObject crane, double dx, double dy) {
+        double vx = optDouble(crane, "vx", 1.0);
+        double vy = optDouble(crane, "vy", 1.0);
+        String moveMode = optStr(crane, "moveMode", "max");
+        double tx = vx <= 0 ? 0 : Math.abs(dx) / vx;
+        double ty = vy <= 0 ? 0 : Math.abs(dy) / vy;
+        double t = "sum".equalsIgnoreCase(moveMode) ? (tx + ty) : Math.max(tx, ty);
+        return (int) Math.round(t);
+    }
+
+    private static int craneMoveTimeSec(JSONObject crane, double fromX, double fromY, double toX, double toY) {
+        return craneMoveTimeSecForDelta(crane, toX - fromX, toY - fromY);
+    }
+
+    private static int conveyorTimeSec(JSONObject task, double defaultSpeedMps) {
+        String taskType = optStr(task, "taskType", "");
+        if (!"OUT".equalsIgnoreCase(taskType)) {
+            return 0;
+        }
+        JSONArray segs = task.getJSONArray("conveyorPath");
+        if (segs != null && !segs.isEmpty()) {
+            double total = 0.0;
+            for (int i = 0; i < segs.size(); i++) {
+                JSONObject seg = segs.getJSONObject(i);
+                if (seg == null) continue;
+                double len = optDouble(seg, "length_m", 0);
+                double sp = optDouble(seg, "speed_mps", 0);
+                if (len > 0 && sp > 0) total += len / sp;
+            }
+            return (int) Math.round(total);
+        }
+        JSONArray nav = task.getJSONArray("navigatePath");
+        JSONArray edges = task.getJSONArray("pathEdges");
+        if (nav != null && nav.size() >= 2 && edges != null && !edges.isEmpty()) {
+            Map<String, Edge> edgeMap = new HashMap<>();
+            for (int i = 0; i < edges.size(); i++) {
+                JSONObject e = edges.getJSONObject(i);
+                if (e == null) continue;
+                int u = e.getIntValue("fromId");
+                int v = e.getIntValue("toId");
+                double len = optDouble(e, "length_m", 0);
+                Double sp = e.containsKey("speed_mps") ? e.getDouble("speed_mps") : null;
+                edgeMap.put(u + "->" + v, new Edge(len, sp));
+            }
+            double total = 0.0;
+            for (int i = 0; i < nav.size() - 1; i++) {
+                int u = nav.getIntValue(i);
+                int v = nav.getIntValue(i + 1);
+                Edge ed = edgeMap.get(u + "->" + v);
+                if (ed == null) ed = edgeMap.get(v + "->" + u);
+                if (ed == null) {
+                    continue;
+                }
+                double sp = ed.speedMps != null && ed.speedMps > 0 ? ed.speedMps : defaultSpeedMps;
+                if (ed.lengthM > 0 && sp > 0) total += ed.lengthM / sp;
+            }
+            return (int) Math.round(total);
+        }
+        return 0;
+    }
+
+    private static void addOutboundOrderConstraints(CpModel m,
+                                                   List<Integer> taskIds,
+                                                   Map<Integer, JSONObject> taskById,
+                                                   Map<Integer, IntVar> outEtaVars,
+                                                   int minGap) {
+        Map<String, List<GroupItem>> groups = new HashMap<>();
+        for (Integer tid : taskIds) {
+            JSONObject t = taskById.get(tid);
+            if (!"OUT".equalsIgnoreCase(optStr(t, "taskType", ""))) continue;
+            String g = t.getString("outGroup");
+            Integer seq = t.getInteger("outSeq");
+            if (g == null || seq == null) continue;
+            groups.computeIfAbsent(g, k -> new ArrayList<>()).add(new GroupItem(seq, tid));
+        }
+        for (Map.Entry<String, List<GroupItem>> e : groups.entrySet()) {
+            List<GroupItem> items = e.getValue();
+            Collections.sort(items, new Comparator<GroupItem>() {
+                @Override
+                public int compare(GroupItem a, GroupItem b) {
+                    return Integer.compare(a.seq, b.seq);
+                }
+            });
+            for (int i = 0; i < items.size() - 1; i++) {
+                int aId = items.get(i).taskId;
+                int bId = items.get(i + 1).taskId;
+                IntVar aEta = outEtaVars.get(aId);
+                IntVar bEta = outEtaVars.get(bId);
+                if (aEta != null && bEta != null) {
+                    m.addGreaterOrEqual(bEta, LinearExpr.sum(new LinearExpr[]{
+                            LinearExpr.term(aEta, 1),
+                            LinearExpr.constant(minGap)
+                    }));
+                }
+            }
+        }
+    }
+
+    private static IntVar addOutboundDueTardiness(CpModel m,
+                                                 List<Integer> taskIds,
+                                                 Map<Integer, JSONObject> taskById,
+                                                 Map<Integer, IntVar> outEtaVars,
+                                                 int taktSec) {
+        Map<String, List<GroupItem>> groups = new HashMap<>();
+        for (Integer tid : taskIds) {
+            JSONObject t = taskById.get(tid);
+            if (!"OUT".equalsIgnoreCase(optStr(t, "taskType", ""))) continue;
+            String g = t.getString("outGroup");
+            Integer seq = t.getInteger("outSeq");
+            if (g == null || seq == null) continue;
+            if (!outEtaVars.containsKey(tid)) continue;
+            groups.computeIfAbsent(g, k -> new ArrayList<>()).add(new GroupItem(seq, tid));
+        }
+        List<IntVar> tardVars = new ArrayList<>();
+        for (Map.Entry<String, List<GroupItem>> e : groups.entrySet()) {
+            List<GroupItem> items = e.getValue();
+            Collections.sort(items, new Comparator<GroupItem>() {
+                @Override
+                public int compare(GroupItem a, GroupItem b) {
+                    return Integer.compare(a.seq, b.seq);
+                }
+            });
+            for (int idx = 0; idx < items.size(); idx++) {
+                int tid = items.get(idx).taskId;
+                IntVar outEta = outEtaVars.get(tid);
+                int due = idx * taktSec;
+                IntVar tard = m.newIntVar(0, 1_000_000_000, "TARD_" + e.getKey() + "_" + tid);
+                m.addGreaterOrEqual(tard, LinearExpr.sum(new LinearExpr[]{
+                        LinearExpr.term(outEta, 1),
+                        LinearExpr.constant(-due)
+                }));
+                m.addGreaterOrEqual(tard, 0);
+                tardVars.add(tard);
+            }
+        }
+        if (tardVars.isEmpty()) {
+            IntVar z = m.newIntVar(0, 0, "TARD_SUM_ZERO");
+            m.addEquality(z, 0);
+            return z;
+        }
+        IntVar s = m.newIntVar(0, 1_000_000_000, "TARD_SUM");
+        m.addEquality(s, LinearExpr.sum(tardVars.toArray(new IntVar[0])));
+        return s;
+    }
+
+    private static int optInt(JSONObject o, String k, int def) {
+        if (o == null) return def;
+        try {
+            Integer v = o.getInteger(k);
+            return v == null ? def : v;
+        } catch (Exception e) {
+            return def;
+        }
+    }
+
+    private static double optDouble(JSONObject o, String k, double def) {
+        if (o == null) return def;
+        try {
+            Double v = o.getDouble(k);
+            return v == null ? def : v;
+        } catch (Exception e) {
+            return def;
+        }
+    }
+
+    private static String optStr(JSONObject o, String k, String def) {
+        if (o == null) return def;
+        try {
+            String v = o.getString(k);
+            return v == null ? def : v;
+        } catch (Exception e) {
+            return def;
+        }
+    }
+
+    private static boolean optBool(JSONObject o, String k, boolean def) {
+        if (o == null) return def;
+        try {
+            Object v = o.get(k);
+            if (v == null) return def;
+            if (v instanceof Boolean) return (Boolean) v;
+            String s = String.valueOf(v).trim().toUpperCase();
+            if ("Y".equals(s) || "TRUE".equals(s) || "1".equals(s)) return true;
+            if ("N".equals(s) || "FALSE".equals(s) || "0".equals(s)) return false;
+            return def;
+        } catch (Exception e) {
+            return def;
+        }
+    }
+
+    private static double optDouble(JSONObject o, String k, Double def) {
+        return optDouble(o, k, def == null ? 0.0 : def);
+    }
+
+    private static long optLong(JSONObject o, String k, long def) {
+        if (o == null) return def;
+        try {
+            Long v = o.getLong(k);
+            return v == null ? def : v;
+        } catch (Exception e) {
+            return def;
+        }
+    }
+
+    private static final class Edge {
+        final double lengthM;
+        final Double speedMps;
+        Edge(double lengthM, Double speedMps) {
+            this.lengthM = lengthM;
+            this.speedMps = speedMps;
+        }
+    }
+
+    private static final class GroupItem {
+        final int seq;
+        final int taskId;
+        GroupItem(int seq, int taskId) {
+            this.seq = seq;
+            this.taskId = taskId;
+        }
+    }
+}
diff --git a/src/main/java/com/zy/asrs/service/PlannerService.java b/src/main/java/com/zy/asrs/service/PlannerService.java
new file mode 100644
index 0000000..38423b7
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/PlannerService.java
@@ -0,0 +1,7 @@
+package com.zy.asrs.service;
+
+import com.alibaba.fastjson.JSONObject;
+
+public interface PlannerService {
+    JSONObject calculateAndSaveSchedule();
+}
diff --git a/src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java
new file mode 100644
index 0000000..e7cf395
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/impl/PlannerServiceImpl.java
@@ -0,0 +1,548 @@
+package com.zy.asrs.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.mapper.EntityWrapper;
+import com.zy.asrs.entity.BasCrnp;
+import com.zy.asrs.entity.LocMast;
+import com.zy.asrs.entity.WrkMast;
+import com.zy.asrs.planner.PlannerOrtoolsSolverService;
+import com.zy.asrs.service.*;
+import com.zy.common.utils.HttpHandler;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.News;
+import com.zy.core.cache.SlaveConnection;
+import com.zy.core.enums.RedisKeyType;
+import com.zy.core.enums.SlaveType;
+import com.zy.core.enums.WrkStsType;
+import com.zy.core.model.StationObjModel;
+import com.zy.core.model.protocol.CrnProtocol;
+import com.zy.core.model.protocol.StationProtocol;
+import com.zy.core.thread.CrnThread;
+import com.zy.core.thread.StationThread;
+import com.zy.core.utils.StationOperateProcessUtils;
+import com.zy.system.entity.Config;
+import com.zy.system.service.ConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class PlannerServiceImpl implements PlannerService {
+
+    @Autowired
+    private BasCrnpService basCrnpService;
+    @Autowired
+    private WrkMastService wrkMastService;
+    @Autowired
+    private LocMastService locMastService;
+    @Autowired
+    private ConfigService configService;
+    @Autowired
+    private RedisUtil redisUtil;
+    @Autowired
+    private PlannerOrtoolsSolverService plannerOrtoolsSolverService;
+    @Autowired
+    private StationOperateProcessUtils stationOperateProcessUtils;
+
+    @Override
+    public JSONObject calculateAndSaveSchedule() {
+        ArrayList<HashMap<String, Object>> crnDataList = new ArrayList<>();
+
+        List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<BasCrnp>().eq("status", 1));
+        Map<Integer, StationObjModel> stationIndex = new HashMap<>();
+        Map<Integer, String> stationCrnCodeIndex = new HashMap<>();
+        
+        Double bayWidth = getDoubleConfig("plannerBayWidthM", 1.0);
+        Double levHeight = getDoubleConfig("plannerLevHeightM", 1.0);
+
+        // First pass: Build station index
+        for (BasCrnp basCrnp : basCrnps) {
+            List<StationObjModel> inStations = basCrnp.getInStationList$();
+            if (inStations != null) {
+                for (StationObjModel stationObjModel : inStations) {
+                    Integer sid = stationObjModel.getStationId();
+                    if (sid != null) {
+                        stationIndex.put(sid, stationObjModel);
+                        stationCrnCodeIndex.put(sid, "CRN-" + basCrnp.getCrnNo());
+                    }
+                }
+            }
+            List<StationObjModel> outStations = basCrnp.getOutStationList$();
+            if (outStations != null) {
+                for (StationObjModel stationObjModel : outStations) {
+                    Integer sid = stationObjModel.getStationId();
+                    if (sid != null) {
+                        stationIndex.put(sid, stationObjModel);
+                    }
+                }
+            }
+        }
+
+        // Second pass: Build crane data
+        for (BasCrnp basCrnp : basCrnps) {
+            CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo());
+            if (crnThread == null) {
+                continue;
+            }
+
+            HashMap<String, Object> crnData = new HashMap<>();
+            crnData.put("code", "CRN-" + basCrnp.getCrnNo());
+            crnData.put("name", basCrnp.getCrnNo());
+            // 瑙勫垝鏃朵娇鐢ㄩ瀹氶�熷害锛岃�岄潪褰撳墠瀹炴椂閫熷害
+            Double vx = getDoubleConfig("plannerVxDefault", 2.6);
+            Double vy = getDoubleConfig("plannerVyDefault", 0.67);
+            crnData.put("vx", vx);
+            crnData.put("vy", vy);
+            try {
+                if (crnThread.getStatus() != null) {
+                    Integer bay = crnThread.getStatus().getBay();
+                    Integer lev = crnThread.getStatus().getLevel();
+                    if (bay != null && lev != null) {
+                        HashMap<String, Object> initPos = new HashMap<>();
+                        initPos.put("x", bay * bayWidth);
+                        initPos.put("y", lev * levHeight);
+                        crnData.put("initPos", initPos);
+                    }
+                }
+            } catch (Exception ignore) {}
+            Integer pickSec = getIntConfig("plannerPickSec", 8);
+            Integer dropSec = getIntConfig("plannerDropSec", 6);
+            crnData.put("pickSec", pickSec);
+            crnData.put("dropSec", dropSec);
+            crnData.put("moveMode", "max");
+
+            int readySec = 0;
+            try {
+                if (crnThread.getStatus() != null) {
+                    CrnProtocol p = crnThread.getStatus();
+                    Integer tNo = p.getTaskNo();
+                    if (tNo != null && tNo > 0) {
+                        WrkMast task = wrkMastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", tNo));
+                        if (task != null) {
+                            // Calculate current X, Y
+                            double curX = (p.getBay() == null ? 0 : p.getBay()) * bayWidth;
+                            double curY = (p.getLevel() == null ? 0 : p.getLevel()) * levHeight;
+                            
+                            double targetX = curX;
+                            double targetY = curY;
+                            boolean hasGoods = (p.getLoaded() != null && p.getLoaded() == 1);
+                            
+                            if (hasGoods) {
+                                // Moving to destination
+                                if (task.getWrkSts() != null && task.getWrkSts() < 50) { // IN task
+                                    // For IN task with goods, destination is Location
+                                    if (task.getLocNo() != null) {
+                                        LocMast loc = locMastService.queryByLoc(task.getLocNo());
+                                        if (loc != null) {
+                                            targetX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
+                                            targetY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
+                                        }
+                                    }
+                                } else { // OUT or LOC_MOVE
+                                    if (task.getStaNo() != null) { // OUT task
+                                        // For OUT task with goods, destination is Station
+                                        StationObjModel s = stationIndex.get(task.getStaNo());
+                                        if (s != null) {
+                                            targetX = (s.getDeviceBay() == null ? 0 : s.getDeviceBay()) * bayWidth;
+                                            targetY = (s.getDeviceLev() == null ? 0 : s.getDeviceLev()) * levHeight;
+                                        }
+                                    } else if (task.getLocNo() != null) { // LOC_MOVE
+                                        LocMast loc = locMastService.queryByLoc(task.getLocNo());
+                                        if (loc != null) {
+                                            targetX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
+                                            targetY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
+                                        }
+                                    }
+                                }
+                            } else {
+                                // Moving to source
+                                if (task.getWrkSts() != null && task.getWrkSts() < 50) { // IN task
+                                     StationObjModel s = stationIndex.get(task.getStaNo());
+                                     if (s != null) {
+                                        targetX = (s.getDeviceBay() == null ? 0 : s.getDeviceBay()) * bayWidth;
+                                        targetY = (s.getDeviceLev() == null ? 0 : s.getDeviceLev()) * levHeight;
+                                     }
+                                } else { // OUT or LOC_MOVE
+                                    if (task.getSourceLocNo() != null) {
+                                        LocMast loc = locMastService.queryByLoc(task.getSourceLocNo());
+                                        if (loc != null) {
+                                            targetX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
+                                            targetY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
+                                        }
+                                    }
+                                }
+                            }
+                            
+                            double dx = Math.abs(targetX - curX);
+                            double dy = Math.abs(targetY - curY);
+                            double tx = vx <= 0 ? 0 : dx / vx;
+                            double ty = vy <= 0 ? 0 : dy / vy;
+                            double moveTime = Math.max(tx, ty);
+                            
+                            double total = moveTime;
+                            if (hasGoods) {
+                                total += dropSec;
+                            } else {
+                                total += pickSec; 
+                                // Plus time to move to destination + drop
+                                double srcX = targetX;
+                                double srcY = targetY;
+                                double dstX = srcX;
+                                double dstY = srcY;
+                                
+                                if (task.getWrkSts() != null && task.getWrkSts() < 50) { // Inbound
+                                    if (task.getLocNo() != null) {
+                                        LocMast loc = locMastService.queryByLoc(task.getLocNo());
+                                        if (loc != null) {
+                                            dstX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
+                                            dstY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
+                                        }
+                                    }
+                                } else { // Outbound or LocMove
+                                    if (task.getStaNo() != null) {
+                                        StationObjModel s = stationIndex.get(task.getStaNo());
+                                        if (s != null) {
+                                            dstX = (s.getDeviceBay() == null ? 0 : s.getDeviceBay()) * bayWidth;
+                                            dstY = (s.getDeviceLev() == null ? 0 : s.getDeviceLev()) * levHeight;
+                                        }
+                                    } else if (task.getLocNo() != null) {
+                                        LocMast loc = locMastService.queryByLoc(task.getLocNo());
+                                        if (loc != null) {
+                                            dstX = (loc.getBay1() == null ? 0 : loc.getBay1()) * bayWidth;
+                                            dstY = (loc.getLev1() == null ? 0 : loc.getLev1()) * levHeight;
+                                        }
+                                    }
+                                }
+                                
+                                double d2x = Math.abs(dstX - srcX);
+                                double d2y = Math.abs(dstY - srcY);
+                                double t2x = vx <= 0 ? 0 : d2x / vx;
+                                double t2y = vy <= 0 ? 0 : d2y / vy;
+                                total += Math.max(t2x, t2y);
+                                total += dropSec;
+                            }
+                            readySec = (int) Math.ceil(total);
+                        }
+                    }
+                }
+            } catch (Exception ignore) {}
+            crnData.put("readySec", readySec);
+            crnDataList.add(crnData);
+        }
+
+        ArrayList<HashMap<String, Object>> taskDataList = new ArrayList<>();
+        Map<String, Integer> outGroupSeqCounter = new HashMap<>();
+        List<WrkMast> outTasks = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("wrk_sts", WrkStsType.NEW_OUTBOUND.sts));
+        for (WrkMast wrkMast : outTasks) {
+            HashMap<String, Object> t = new HashMap<>();
+            t.put("taskId", wrkMast.getWrkNo());
+            t.put("taskType", "OUT");
+            t.put("priority", wrkMast.getIoPri() == null ? 5 : wrkMast.getIoPri().intValue());
+            if (wrkMast.getAppeTime() != null) {
+                t.put("createTime", wrkMast.getAppeTime().getTime());
+            }
+            String src = wrkMast.getSourceLocNo();
+            LocMast srcLoc = src == null ? null : locMastService.queryByLoc(src);
+            int srcX = srcLoc == null ? 0 : (srcLoc.getBay1() == null ? 0 : srcLoc.getBay1());
+            int srcY = srcLoc == null ? 0 : (srcLoc.getLev1() == null ? 0 : srcLoc.getLev1());
+            HashMap<String, Object> fromPos = new HashMap<>();
+            fromPos.put("x", srcX * bayWidth);
+            fromPos.put("y", srcY * levHeight);
+            t.put("fromPos", fromPos);
+            Integer targetStationId = wrkMast.getStaNo();
+            double toX = 0;
+            double toY = 0;
+            try {
+                if (targetStationId != null) {
+                    StationObjModel s = stationIndex.get(targetStationId);
+                    if (s != null) {
+                        toX = s.getDeviceBay() == null ? 0 : s.getDeviceBay();
+                        toY = s.getDeviceLev() == null ? 0 : s.getDeviceLev();
+                    }
+                }
+            } catch (Exception ignore) {}
+            HashMap<String, Object> toPos = new HashMap<>();
+            toPos.put("x", toX * bayWidth);
+            toPos.put("y", toY * levHeight);
+            t.put("toPos", toPos);
+            ArrayList<String> eligible = new ArrayList<>();
+            Integer crnNo = wrkMast.getCrnNo();
+            if (crnNo != null && crnNo > 0) {
+                eligible.add("CRN-" + crnNo);
+            } else {
+                try {
+                    String locNo = src;
+                    if (locNo != null) {
+                        Integer row = com.zy.asrs.utils.Utils.getRow(locNo);
+                        for (BasCrnp basCrnp : basCrnps) {
+                            List<List<Integer>> controlRows = basCrnp.getControlRows$();
+                            for (List<Integer> rows : controlRows) {
+                                if (rows.contains(row)) {
+                                    eligible.add("CRN-" + basCrnp.getCrnNo());
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                } catch (Exception ignore) {}
+            }
+            t.put("eligibleCranes", eligible);
+            t.put("conveyorPath", new ArrayList<>());
+            String group = wrkMast.getWmsWrkNo();
+            if (group == null || group.trim().isEmpty()) {
+                group = "WCS";
+            }
+            int seq = outGroupSeqCounter.getOrDefault(group, 0) + 1;
+            outGroupSeqCounter.put(group, seq);
+            t.put("outGroup", group);
+            t.put("outSeq", seq);
+            taskDataList.add(t);
+        }
+
+        List<WrkMast> moveTasks = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("wrk_sts", WrkStsType.NEW_LOC_MOVE.sts));
+        for (WrkMast wrkMast : moveTasks) {
+            HashMap<String, Object> t = new HashMap<>();
+            t.put("taskId", wrkMast.getWrkNo());
+            t.put("taskType", "MOVE");
+            t.put("priority", wrkMast.getIoPri() == null ? 5 : wrkMast.getIoPri().intValue());
+            if (wrkMast.getAppeTime() != null) {
+                t.put("createTime", wrkMast.getAppeTime().getTime());
+            }
+            
+            String src = wrkMast.getSourceLocNo();
+            LocMast srcLoc = src == null ? null : locMastService.queryByLoc(src);
+            int srcX = srcLoc == null ? 0 : (srcLoc.getBay1() == null ? 0 : srcLoc.getBay1());
+            int srcY = srcLoc == null ? 0 : (srcLoc.getLev1() == null ? 0 : srcLoc.getLev1());
+            HashMap<String, Object> fromPos = new HashMap<>();
+            fromPos.put("x", srcX * bayWidth);
+            fromPos.put("y", srcY * levHeight);
+            t.put("fromPos", fromPos);
+
+            String dst = wrkMast.getLocNo();
+            LocMast dstLoc = dst == null ? null : locMastService.queryByLoc(dst);
+            int toX = dstLoc == null ? 0 : (dstLoc.getBay1() == null ? 0 : dstLoc.getBay1());
+            int toY = dstLoc == null ? 0 : (dstLoc.getLev1() == null ? 0 : dstLoc.getLev1());
+            HashMap<String, Object> toPos = new HashMap<>();
+            toPos.put("x", toX * bayWidth);
+            toPos.put("y", toY * levHeight);
+            t.put("toPos", toPos);
+
+            ArrayList<String> eligible = new ArrayList<>();
+            Integer crnNo = wrkMast.getCrnNo();
+            if (crnNo != null && crnNo > 0) {
+                eligible.add("CRN-" + crnNo);
+            } else {
+                try {
+                    String locNo = src;
+                    if (locNo != null) {
+                        Integer row = com.zy.asrs.utils.Utils.getRow(locNo);
+                        for (BasCrnp basCrnp : basCrnps) {
+                            List<List<Integer>> controlRows = basCrnp.getControlRows$();
+                            for (List<Integer> rows : controlRows) {
+                                if (rows.contains(row)) {
+                                    eligible.add("CRN-" + basCrnp.getCrnNo());
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                } catch (Exception ignore) {}
+            }
+            t.put("eligibleCranes", eligible);
+            t.put("conveyorPath", new ArrayList<>());
+            taskDataList.add(t);
+        }
+
+        List<WrkMast> inTasks = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("wrk_sts", WrkStsType.INBOUND_DEVICE_RUN.sts));
+        for (WrkMast wrkMast : inTasks) {
+            HashMap<String, Object> t = new HashMap<>();
+            t.put("taskId", wrkMast.getWrkNo());
+            t.put("taskType", "IN");
+            t.put("priority", wrkMast.getIoPri() == null ? 5 : wrkMast.getIoPri().intValue());
+            if (wrkMast.getAppeTime() != null) {
+                t.put("createTime", wrkMast.getAppeTime().getTime());
+            }
+            Integer targetStationId = wrkMast.getStaNo();
+            HashMap<String, Object> fromPos = new HashMap<>();
+            String matchedCrnCode = null;
+            boolean stationReady = false;
+            if (targetStationId != null) {
+                StationObjModel stationObjModel = stationIndex.get(targetStationId);
+                if (stationObjModel != null) {
+                    StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo());
+                    if (stationThread != null) {
+                        Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
+                        StationProtocol sp = statusMap == null ? null : statusMap.get(targetStationId);
+                        if (sp != null && sp.isAutoing() && sp.isLoading() && sp.getTaskNo().equals(wrkMast.getWrkNo())) {
+                            stationReady = true;
+                            matchedCrnCode = stationCrnCodeIndex.get(targetStationId);
+                            fromPos.put("x", stationObjModel.getDeviceBay() * bayWidth);
+                            fromPos.put("y", stationObjModel.getDeviceLev() * levHeight);
+                        }
+                    }
+                }
+            }
+            if (!stationReady) {
+                continue;
+            }
+            t.put("fromPos", fromPos);
+            HashMap<String, Object> toPos = new HashMap<>();
+            String dst = wrkMast.getLocNo();
+            LocMast dstLoc = dst == null ? null : locMastService.queryByLoc(dst);
+            int toX = dstLoc == null ? 0 : (dstLoc.getBay1() == null ? 0 : dstLoc.getBay1());
+            int toY = dstLoc == null ? 0 : (dstLoc.getLev1() == null ? 0 : dstLoc.getLev1());
+            toPos.put("x", toX * bayWidth);
+            toPos.put("y", toY * levHeight);
+            t.put("toPos", toPos);
+            ArrayList<String> eligible = new ArrayList<>();
+            if (matchedCrnCode != null) {
+                eligible.add(matchedCrnCode);
+            }
+            t.put("eligibleCranes", eligible);
+            t.put("conveyorPath", new ArrayList<>());
+            taskDataList.add(t);
+        }
+
+        //鑾峰彇杈撻�佺嚎浠诲姟鏁伴噺
+        int currentStationTaskCount = stationOperateProcessUtils.getCurrentStationTaskCount();
+
+        HashMap<String, Object> config = new HashMap<>();
+        config.put("outMinGapSec", getIntConfig("plannerOutMinGapSec", 0));
+        config.put("wMakespan", getIntConfig("plannerWeightMakespan", 1000));
+        config.put("wPriorityEarlyFinish", getIntConfig("plannerWeightPriorityEarlyFinish", 5));
+        config.put("wWaitTime", getIntConfig("plannerWeightWaitTime", 1));
+        config.put("enableOutDue", getBoolConfig("plannerEnableOutDue", false));
+        config.put("maxSolveSeconds", getIntConfig("plannerMaxSolveSeconds", 3));
+        config.put("numSearchWorkers", getIntConfig("plannerNumSearchWorkers", 8));
+        config.put("conveyorStationTaskLimit", getIntConfig("conveyorStationTaskLimit", 30));
+        config.put("conveyorStationRunningTasks", currentStationTaskCount);
+
+        HashMap<String, Object> request = new HashMap<>();
+        request.put("cranes", crnDataList);
+        request.put("tasks", taskDataList);
+        request.put("config", config);
+
+        JSONObject result;
+        boolean useInternal = getBoolConfig("plannerUseInternalSolver", true);
+        if (useInternal) {
+            try {
+                result = plannerOrtoolsSolverService.solve(JSONObject.parseObject(JSON.toJSONString(request)));
+            } catch (Throwable e) {
+                News.error("鍐呴儴姹傝В鍣ㄦ墽琛屽け璐�:{}", e.getMessage());
+                result = null;
+            }
+        } else {
+            Config uriCfg = configService.selectOne(new EntityWrapper<Config>().eq("code", "plannerSolverUri"));
+            Config pathCfg = configService.selectOne(new EntityWrapper<Config>().eq("code", "plannerSolverPath"));
+            if (uriCfg == null || pathCfg == null || uriCfg.getValue() == null || pathCfg.getValue() == null) {
+                // 濡傛灉鏄湇鍔¤皟鐢紝杩欓噷鏃犳硶鐩存帴杩斿洖R.error锛岃繑鍥瀗ull鎴栬�卐mpty json
+                return null;
+            }
+            String solverUri = uriCfg.getValue();
+            String solverPath = pathCfg.getValue();
+            String response;
+            try {
+                HttpHandler http = new HttpHandler.Builder()
+                        .setUri(solverUri)
+                        .setPath(solverPath)
+                        .setTimeout(30, TimeUnit.SECONDS)
+                        .setJson(JSON.toJSONString(request))
+                        .build();
+                response = http.doPost();
+            } catch (Exception e) {
+                News.error("姹傝В鍣ㄨ皟鐢ㄥけ璐�:{}", e.getMessage());
+                return null;
+            }
+            try {
+                result = JSON.parseObject(response);
+            } catch (Exception ignore) {
+                return null;
+            }
+        }
+
+        try {
+            try {
+                int nowSec = (int) (System.currentTimeMillis() / 1000);
+                if (result != null && result.containsKey("schedule")) {
+                    List<Object> schedule = result.getJSONArray("schedule");
+                    if (schedule != null) {
+                        Map<String, List<String>> groupByCrane = new HashMap<>();
+                        for (Object obj : schedule) {
+                            JSONObject item = (JSONObject) JSONObject.toJSON(obj);
+                            String craneCode = item.getString("craneCode");
+                            Integer taskId = item.getInteger("taskId");
+                            String taskType = item.getString("taskType");
+                            Integer startSec = item.getInteger("startSec");
+                            Integer endSec = item.getInteger("endSec");
+                            Integer outEtaSec = item.getInteger("outEtaSec");
+                            if (craneCode == null || taskId == null || startSec == null) {
+                                continue;
+                            }
+                            JSONObject store = new JSONObject();
+                            store.put("taskId", taskId);
+                            store.put("taskType", taskType);
+                            store.put("startEpochSec", nowSec + startSec);
+                            if (endSec != null) {
+                                store.put("endEpochSec", nowSec + endSec);
+                            }
+                            if (outEtaSec != null) {
+                                store.put("outEtaEpochSec", nowSec + outEtaSec);
+                            }
+                            groupByCrane.computeIfAbsent(craneCode, k -> new ArrayList<>()).add(store.toJSONString());
+                        }
+                        for (Map.Entry<String, List<String>> e : groupByCrane.entrySet()) {
+                            String key = RedisKeyType.PLANNER_SCHEDULE.key + e.getKey();
+                            redisUtil.del(key);
+                            for (String s : e.getValue()) {
+                                redisUtil.lSet(key, s);
+                            }
+                        }
+                    }
+                }
+            } catch (Exception ignore) {}
+            return result;
+        } catch (Exception ignore) {
+            return null;
+        }
+    }
+
+    private Double getDoubleConfig(String code, Double def) {
+        try {
+            Config c = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
+            if (c != null && c.getValue() != null && c.getValue().trim().length() > 0) {
+                return Double.parseDouble(c.getValue().trim());
+            }
+        } catch (Exception ignore) {}
+        return def;
+    }
+
+    private Integer getIntConfig(String code, Integer def) {
+        try {
+            Config c = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
+            if (c != null && c.getValue() != null && c.getValue().trim().length() > 0) {
+                String v = c.getValue().trim();
+                if (v.endsWith("%")) v = v.substring(0, v.length() - 1);
+                return Integer.parseInt(v);
+            }
+        } catch (Exception ignore) {}
+        return def;
+    }
+
+    private Boolean getBoolConfig(String code, Boolean def) {
+        try {
+            Config c = configService.selectOne(new EntityWrapper<Config>().eq("code", code));
+            if (c != null && c.getValue() != null) {
+                String v = c.getValue().trim().toUpperCase();
+                if ("Y".equals(v) || "TRUE".equals(v)) return true;
+                if ("N".equals(v) || "FALSE".equals(v)) return false;
+            }
+        } catch (Exception ignore) {}
+        return def;
+    }
+}
diff --git a/src/main/java/com/zy/asrs/task/PlannerExecutor.java b/src/main/java/com/zy/asrs/task/PlannerExecutor.java
new file mode 100644
index 0000000..56dec78
--- /dev/null
+++ b/src/main/java/com/zy/asrs/task/PlannerExecutor.java
@@ -0,0 +1,20 @@
+package com.zy.asrs.task;
+
+import com.zy.core.utils.CrnOperateProcessUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+public class PlannerExecutor {
+
+    @Autowired
+    private CrnOperateProcessUtils crnOperateProcessUtils;
+
+    @Scheduled(cron = "0/1 * * * * ? ")
+    @Transactional
+    public void executePlanner() {
+        crnOperateProcessUtils.plannerExecute();
+    }
+}
diff --git a/src/main/java/com/zy/asrs/task/PlannerScheduler.java b/src/main/java/com/zy/asrs/task/PlannerScheduler.java
new file mode 100644
index 0000000..9aef267
--- /dev/null
+++ b/src/main/java/com/zy/asrs/task/PlannerScheduler.java
@@ -0,0 +1,25 @@
+package com.zy.asrs.task;
+
+import com.alibaba.fastjson.JSONObject;
+import com.zy.asrs.service.PlannerService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+public class PlannerScheduler {
+
+    @Autowired
+    private PlannerService plannerService;
+
+    // 姣�3绉掕Е鍙戜竴娆℃眰瑙�
+    @Scheduled(fixedDelay = 3000)
+    public void schedulePlanner() {
+        try {
+            JSONObject result = plannerService.calculateAndSaveSchedule();
+            // 鏃ュ織璁板綍鍙互鏍规嵁闇�瑕佹坊鍔狅紝閬垮厤杩囦簬棰戠箒
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/main/java/com/zy/core/MainProcess.java b/src/main/java/com/zy/core/MainProcess.java
index be87702..70f7954 100644
--- a/src/main/java/com/zy/core/MainProcess.java
+++ b/src/main/java/com/zy/core/MainProcess.java
@@ -1,7 +1,6 @@
 package com.zy.core;
 
 import com.core.common.SpringUtils;
-import com.core.exception.CoolException;
 import com.zy.core.plugin.api.MainProcessPluginApi;
 import com.zy.core.properties.SystemProperties;
 import lombok.extern.slf4j.Slf4j;
@@ -20,38 +19,62 @@
     private String mainProcessPlugin;
     private MainProcessPluginApi mainProcessPluginApi;
     // 鎵�灞炵嚎绋�
-    private Thread thread;
+    private volatile Thread thread;
 
     /**
      * =====>>  寮�濮嬪伐浣�
      */
-    public void start(){
+    public void start() {
+        if (thread != null && thread.isAlive()) {
+            log.warn("MainProcess is already running.");
+            return;
+        }
+
         thread = new Thread(() -> {
             while (!Thread.currentThread().isInterrupted()) {
                 try {
+                    // 1. 鍔犺浇鎻掍欢
                     if (mainProcessPluginApi == null) {
-                        String className = mainProcessPlugin.contains(".") ? mainProcessPlugin : "com.zy.core.plugin." + mainProcessPlugin;
-                        Class<? extends MainProcessPluginApi> clazz = Class.forName(className).asSubclass(MainProcessPluginApi.class);
                         try {
+                            String className = mainProcessPlugin.contains(".") ? mainProcessPlugin : "com.zy.core.plugin." + mainProcessPlugin;
+                            Class<? extends MainProcessPluginApi> clazz = Class.forName(className).asSubclass(MainProcessPluginApi.class);
                             mainProcessPluginApi = SpringUtils.getBean(clazz);
-                        } catch (CoolException coolException) {
-                            Thread.sleep(300);
+                        } catch (Exception e) {
+                            log.error("Failed to load mainProcessPlugin: {}", mainProcessPlugin, e);
                         }
                     }
 
-                    // 绯荤粺杩愯鐘舵�佸垽鏂�
-                    if (!SystemProperties.WCS_RUNNING_STATUS.get()) {
+                    // 濡傛灉鍔犺浇澶辫触锛岀瓑寰呭悗閲嶈瘯锛岄槻姝㈢┖鎸囬拡
+                    if (mainProcessPluginApi == null) {
+                        Thread.sleep(3000);
                         continue;
                     }
 
+                    // 2. 绯荤粺杩愯鐘舵�佸垽鏂�
+                    if (!SystemProperties.WCS_RUNNING_STATUS.get()) {
+                        // 闃叉蹇欑瓑 (Busy-wait optimization)
+                        Thread.sleep(1000);
+                        continue;
+                    }
+
+                    // 3. 鎵ц涓绘祦绋�
                     mainProcessPluginApi.run();
-                    // 闂撮殧
+
+                    // 4. 闂撮殧
                     Thread.sleep(200);
+
                 } catch (InterruptedException ie) {
+                    log.info("MainProcess thread interrupted, stopping...");
                     Thread.currentThread().interrupt();
                     break;
                 } catch (Exception e) {
-                    e.printStackTrace();
+                    log.error("Error in MainProcess execution loop", e);
+                    try {
+                        // 閬垮厤寮傚父瀵艰嚧鐨勭媯鍒锋棩蹇�
+                        Thread.sleep(1000);
+                    } catch (InterruptedException ex) {
+                        Thread.currentThread().interrupt();
+                    }
                 }
             }
         });
diff --git a/src/main/java/com/zy/core/enums/RedisKeyType.java b/src/main/java/com/zy/core/enums/RedisKeyType.java
index 4673637..2e623ae 100644
--- a/src/main/java/com/zy/core/enums/RedisKeyType.java
+++ b/src/main/java/com/zy/core/enums/RedisKeyType.java
@@ -52,6 +52,7 @@
     AI_CHAT_HISTORY("ai_chat_history_"),
     AI_CHAT_META("ai_chat_meta_"),
     MAIN_PROCESS_PSEUDOCODE("main_process_pseudocode"),
+    PLANNER_SCHEDULE("planner_schedule_"),
     ;
 
     public String key;
diff --git a/src/main/java/com/zy/core/model/command/CrnCommand.java b/src/main/java/com/zy/core/model/command/CrnCommand.java
index 7bf7df9..dcc61c9 100644
--- a/src/main/java/com/zy/core/model/command/CrnCommand.java
+++ b/src/main/java/com/zy/core/model/command/CrnCommand.java
@@ -1,9 +1,9 @@
 package com.zy.core.model.command;
 
 import lombok.Data;
+
 /**
  * 鍫嗗灈鏈哄懡浠ゆ姤鏂�
- * Created by vincent on 2020/8/11
  */
 @Data
 public class CrnCommand {
@@ -12,10 +12,10 @@
     private Integer crnNo = 0;
 
     // 浠诲姟瀹屾垚纭浣�
-    private Short ackFinish = 0;
+    private Integer ackFinish = 0;
 
     // 浠诲姟鍙�
-    private Short taskNo = 0;
+    private Integer taskNo = 0;
 
     /**
      * 浠诲姟妯″紡锛�
@@ -30,27 +30,27 @@
      * 90 = 璁剧疆鏃堕棿
      * 99 = 鍙栨秷褰撳墠浠诲姟
      */
-    private Short taskMode = 0;
+    private Integer taskMode = 0;
 
     // 婧愪綅缃帓鍙�
-    private Short sourcePosX = 0;
+    private Integer sourcePosX = 0;
 
     // 婧愪綅缃垪鍙�
-    private Short sourcePosY = 0;
+    private Integer sourcePosY = 0;
 
     // 婧愪綅缃眰鍙�
-    private Short sourcePosZ = 0;
+    private Integer sourcePosZ = 0;
 
     // 鐩爣浣嶇疆鎺掑彿
-    private Short destinationPosX = 0;
+    private Integer destinationPosX = 0;
 
     // 鐩爣浣嶇疆鍒楀彿
-    private Short destinationPosY = 0;
+    private Integer destinationPosY = 0;
 
     // 鐩爣浣嶇疆灞傚彿
-    private Short destinationPosZ = 0;
+    private Integer destinationPosZ = 0;
 
     // 浠诲姟纭 0锛氭湭纭 1锛氬凡纭
-    private Short command = 0;
+    private Integer command = 0;
 
 }
diff --git a/src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java b/src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java
index 9680550..c78afa0 100644
--- a/src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java
+++ b/src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java
@@ -3,6 +3,7 @@
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.zy.asrs.entity.DeviceConfig;
+import com.zy.asrs.utils.Utils;
 import com.zy.common.model.NavigateNode;
 import com.zy.common.utils.RedisUtil;
 import com.zy.core.News;
@@ -49,6 +50,34 @@
 
     @Override
     public boolean connect() {
+        Thread checkThread = new Thread(() -> {
+            while (true) {
+                try {
+                    for (Map.Entry<Integer, List<ZyStationStatusEntity>> entry : deviceStatusMap.entrySet()) {
+                        List<ZyStationStatusEntity> stationList = entry.getValue();
+                        for (ZyStationStatusEntity statusEntity : stationList) {
+                            if (statusEntity.isAutoing()
+                                    && statusEntity.isLoading()
+                                    && statusEntity.getTaskNo() > 0
+                                    && !statusEntity.isRunBlock()
+                                    && !statusEntity.getStationId().equals(statusEntity.getTargetStaNo())
+                            ) {
+                                BlockingQueue<StationCommand> commands = taskQueues.get(statusEntity.getTaskNo());
+                                if (commands == null) {
+                                    statusEntity.setRunBlock(true);
+                                }
+                            }
+                        }
+                    }
+
+
+                    Thread.sleep(100);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        });
+        checkThread.start();
         return true;
     }
 
@@ -94,11 +123,16 @@
             return new CommandResponse(false, "浠诲姟鍙蜂负绌�");
         }
 
-        taskQueues.computeIfAbsent(taskNo, k -> new LinkedBlockingQueue<>()).offer(command);
-        taskLastUpdateTime.put(taskNo, System.currentTimeMillis());
+        // 澶勭悊闈炵Щ鍔ㄥ懡浠�
+        if (command.getCommandType() != StationCommandType.MOVE) {
+            handleCommand(deviceNo, command);
+        }else {
+            taskQueues.computeIfAbsent(taskNo, k -> new LinkedBlockingQueue<>()).offer(command);
+            taskLastUpdateTime.put(taskNo, System.currentTimeMillis());
 
-        if (taskRunning.putIfAbsent(taskNo, true) == null) {
-            executor.submit(() -> runTaskLoop(deviceNo, taskNo));
+            if (taskRunning.putIfAbsent(taskNo, true) == null) {
+                executor.submit(() -> runTaskLoop(deviceNo, taskNo));
+            }
         }
 
         return new CommandResponse(true, "鍛戒护宸插彈鐞嗭紙寮傛鎵ц锛�");
@@ -118,6 +152,7 @@
             StationCommand initialCommand = null;
             Integer finalTargetStationId = null;
             boolean generateBarcode = false;
+            long stepExecuteTime = System.currentTimeMillis();
 
             while (true) {
                 BlockingQueue<StationCommand> commandQueue = taskQueues.get(taskNo);
@@ -128,6 +163,7 @@
                 // 灏濊瘯鑾峰彇鏂板懡浠わ紝濡傛灉娌℃湁鏂板懡浠ゅ垯缁х画鎵ц鐜版湁璺緞
                 StationCommand command = commandQueue.poll(100, TimeUnit.MILLISECONDS);
                 if (command != null) {
+                    stepExecuteTime = System.currentTimeMillis();
                     taskLastUpdateTime.put(taskNo, System.currentTimeMillis());
                     
                     if (initialCommand == null) {
@@ -159,11 +195,6 @@
                                 pathQueue.offer(stationId);
                             }
                         }
-                    }
-                    
-                    // 澶勭悊闈炵Щ鍔ㄥ懡浠�
-                    if (command.getCommandType() != StationCommandType.MOVE) {
-                        handleCommand(deviceNo, command);
                     }
                 }
 
@@ -200,8 +231,27 @@
                             if (moveSuccess) {
                                 currentPathIndex++;
                                 pathQueue.poll();
+                                stepExecuteTime = System.currentTimeMillis();
                                 sleep(1000); // 妯℃嫙鑰楁椂
                             } else {
+                                if (!checkTaskNoInArea(taskNo)) {
+                                    boolean fakeAllowCheckBlock = true;
+                                    Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
+                                    if (systemConfigMapObj != null) {
+                                        HashMap<String, String> systemConfigMap = (HashMap<String, String>) systemConfigMapObj;
+                                        if (systemConfigMap.get("fakeAllowCheckBlock") != null && !systemConfigMap.get("fakeAllowCheckBlock").equals("Y")) {
+                                            fakeAllowCheckBlock = false;
+                                        }
+                                    }
+
+                                    if (fakeAllowCheckBlock && System.currentTimeMillis() - stepExecuteTime > 1000 * 10) {
+                                        //璁ゅ畾鍫靛
+                                        boolean result = runBlockStation(taskNo, currentStationId, currentDeviceNo, taskNo, currentStationId);
+                                        if(result) {
+                                            break;
+                                        }
+                                    }
+                                }
                                 sleep(1000); // 澶辫触閲嶈瘯绛夊緟
                             }
                         } else {
diff --git a/src/main/java/com/zy/core/network/real/ZyCrnRealConnect.java b/src/main/java/com/zy/core/network/real/ZyCrnRealConnect.java
index d7c7201..ba2a839 100644
--- a/src/main/java/com/zy/core/network/real/ZyCrnRealConnect.java
+++ b/src/main/java/com/zy/core/network/real/ZyCrnRealConnect.java
@@ -123,14 +123,14 @@
 
             short[] array = new short[9];
             array[0] = (short) 0;
-            array[1] = command.getTaskNo();
-            array[2] = command.getTaskMode();
-            array[3] = command.getSourcePosX();
-            array[4] = command.getSourcePosY();
-            array[5] = command.getSourcePosZ();
-            array[6] = command.getDestinationPosX();
-            array[7] = command.getDestinationPosY();
-            array[8] = command.getDestinationPosZ();
+            array[1] = command.getTaskNo().shortValue();
+            array[2] = command.getTaskMode().shortValue();
+            array[3] = command.getSourcePosX().shortValue();
+            array[4] = command.getSourcePosY().shortValue();
+            array[5] = command.getSourcePosZ().shortValue();
+            array[6] = command.getDestinationPosX().shortValue();
+            array[7] = command.getDestinationPosY().shortValue();
+            array[8] = command.getDestinationPosZ().shortValue();
 
             OperateResult result = siemensNet.Write("DB100.0", array);
             if (!result.IsSuccess) {
diff --git a/src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java b/src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java
index b7f2248..b680358 100644
--- a/src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java
+++ b/src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java
@@ -256,15 +256,15 @@
     public CrnCommand getPickAndPutCommand(String sourceLocNo, String targetLocNo, Integer taskNo, Integer crnNo) {
         CrnCommand crnCommand = new CrnCommand();
         crnCommand.setCrnNo(crnNo); // 鍫嗗灈鏈虹紪鍙�
-        crnCommand.setTaskNo(taskNo.shortValue()); // 宸ヤ綔鍙�
-        crnCommand.setTaskMode(CrnTaskModeType.LOC_MOVE.id.shortValue()); // 浠诲姟妯″紡:  搴撲綅绉昏浆
-        crnCommand.setSourcePosX((short) Utils.getRow(sourceLocNo));     // 婧愬簱浣嶆帓
-        crnCommand.setSourcePosY((short) Utils.getBay(sourceLocNo));     // 婧愬簱浣嶅垪
-        crnCommand.setSourcePosZ((short) Utils.getLev(sourceLocNo));     // 婧愬簱浣嶅眰
-        crnCommand.setDestinationPosX((short) Utils.getRow(targetLocNo));     // 鐩爣搴撲綅鎺�
-        crnCommand.setDestinationPosY((short) Utils.getBay(targetLocNo));     // 鐩爣搴撲綅鍒�
-        crnCommand.setDestinationPosZ((short) Utils.getLev(targetLocNo));     // 鐩爣搴撲綅灞�
-        crnCommand.setCommand((short) 1);     // 浠诲姟纭
+        crnCommand.setTaskNo(taskNo); // 宸ヤ綔鍙�
+        crnCommand.setTaskMode(CrnTaskModeType.LOC_MOVE.id); // 浠诲姟妯″紡:  搴撲綅绉昏浆
+        crnCommand.setSourcePosX(Utils.getRow(sourceLocNo));     // 婧愬簱浣嶆帓
+        crnCommand.setSourcePosY(Utils.getBay(sourceLocNo));     // 婧愬簱浣嶅垪
+        crnCommand.setSourcePosZ(Utils.getLev(sourceLocNo));     // 婧愬簱浣嶅眰
+        crnCommand.setDestinationPosX(Utils.getRow(targetLocNo));     // 鐩爣搴撲綅鎺�
+        crnCommand.setDestinationPosY(Utils.getBay(targetLocNo));     // 鐩爣搴撲綅鍒�
+        crnCommand.setDestinationPosZ(Utils.getLev(targetLocNo));     // 鐩爣搴撲綅灞�
+        crnCommand.setCommand(1);     // 浠诲姟纭
         return crnCommand;
     }
 
@@ -272,13 +272,13 @@
     public CrnCommand getMoveCommand(String targetLocNo, Integer taskNo, Integer crnNo) {
         CrnCommand crnCommand = new CrnCommand();
         crnCommand.setCrnNo(crnNo); // 鍫嗗灈鏈虹紪鍙�
-        crnCommand.setTaskNo(taskNo.shortValue()); // 宸ヤ綔鍙�
-        crnCommand.setAckFinish((short) 0);  // 浠诲姟瀹屾垚纭浣�
-        crnCommand.setTaskMode(CrnTaskModeType.CRN_MOVE.id.shortValue()); // 浠诲姟妯″紡:  鍫嗗灈鏈虹Щ鍔�
-        crnCommand.setDestinationPosX((short) Utils.getRow(targetLocNo));     // 鐩爣搴撲綅鎺�
-        crnCommand.setDestinationPosY((short) Utils.getBay(targetLocNo));     // 鐩爣搴撲綅鍒�
-        crnCommand.setDestinationPosZ((short) Utils.getLev(targetLocNo));     // 鐩爣搴撲綅灞�
-        crnCommand.setCommand((short) 1);     // 浠诲姟纭
+        crnCommand.setTaskNo(taskNo); // 宸ヤ綔鍙�
+        crnCommand.setAckFinish(0);  // 浠诲姟瀹屾垚纭浣�
+        crnCommand.setTaskMode(CrnTaskModeType.CRN_MOVE.id); // 浠诲姟妯″紡:  鍫嗗灈鏈虹Щ鍔�
+        crnCommand.setDestinationPosX(Utils.getRow(targetLocNo));     // 鐩爣搴撲綅鎺�
+        crnCommand.setDestinationPosY(Utils.getBay(targetLocNo));     // 鐩爣搴撲綅鍒�
+        crnCommand.setDestinationPosZ(Utils.getLev(targetLocNo));     // 鐩爣搴撲綅灞�
+        crnCommand.setCommand(1);     // 浠诲姟纭
         return crnCommand;
     }
 
@@ -286,16 +286,16 @@
     public CrnCommand getResetCommand(Integer crnNo) {
         CrnCommand crnCommand = new CrnCommand();
         crnCommand.setCrnNo(crnNo); // 鍫嗗灈鏈虹紪鍙�
-        crnCommand.setTaskNo((short) 0); // 宸ヤ綔鍙�
-        crnCommand.setAckFinish((short) 1);  // 浠诲姟瀹屾垚纭浣�
-        crnCommand.setTaskMode(CrnTaskModeType.NONE.id.shortValue()); // 浠诲姟妯″紡
-        crnCommand.setSourcePosX((short)0);     // 婧愬簱浣嶆帓
-        crnCommand.setSourcePosY((short)0);     // 婧愬簱浣嶅垪
-        crnCommand.setSourcePosZ((short)0);     // 婧愬簱浣嶅眰
-        crnCommand.setDestinationPosX((short)0);     // 鐩爣搴撲綅鎺�
-        crnCommand.setDestinationPosY((short)0);     // 鐩爣搴撲綅鍒�
-        crnCommand.setDestinationPosZ((short)0);     // 鐩爣搴撲綅灞�
-        crnCommand.setCommand((short) 1);     // 浠诲姟纭
+        crnCommand.setTaskNo(0); // 宸ヤ綔鍙�
+        crnCommand.setAckFinish(1);  // 浠诲姟瀹屾垚纭浣�
+        crnCommand.setTaskMode(CrnTaskModeType.NONE.id); // 浠诲姟妯″紡
+        crnCommand.setSourcePosX(0);     // 婧愬簱浣嶆帓
+        crnCommand.setSourcePosY(0);     // 婧愬簱浣嶅垪
+        crnCommand.setSourcePosZ(0);     // 婧愬簱浣嶅眰
+        crnCommand.setDestinationPosX(0);     // 鐩爣搴撲綅鎺�
+        crnCommand.setDestinationPosY(0);     // 鐩爣搴撲綅鍒�
+        crnCommand.setDestinationPosZ(0);     // 鐩爣搴撲綅灞�
+        crnCommand.setCommand(1);     // 浠诲姟纭
         return crnCommand;
     }
 
diff --git a/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java b/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
index d486a06..89f2361 100644
--- a/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
+++ b/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
@@ -50,8 +50,20 @@
     @Autowired
     private CommonService commonService;
 
-    //鍏ュ嚭搴�  ===>>  鍫嗗灈鏈哄叆鍑哄簱浣滀笟涓嬪彂
     public synchronized void crnIoExecute() {
+        Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
+        if (systemConfigMapObj != null) {
+            HashMap<String, String> systemConfigMap = (HashMap<String, String>) systemConfigMapObj;
+            if (systemConfigMap.get("crnRunMethod").equals("solver")) {
+                plannerExecute();
+            }else {
+                crnIoExecuteNormal();
+            }
+        }
+    }
+
+    //鍏ュ嚭搴�  ===>>  鍫嗗灈鏈哄叆鍑哄簱浣滀笟涓嬪彂
+    public synchronized void crnIoExecuteNormal() {
         List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<>());
         for (BasCrnp basCrnp : basCrnps) {
             CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo());
@@ -290,6 +302,182 @@
         }
     }
 
+    private synchronized boolean crnExecuteInPlanner(BasCrnp basCrnp, CrnThread crnThread, WrkMast wrkMast) {
+        CrnProtocol crnProtocol = crnThread.getStatus();
+        if (crnProtocol == null) {
+            return false;
+        }
+
+        if (!basCrnp.getInEnable().equals("Y")) {
+            News.info("鍫嗗灈鏈�:{} 鍙叆淇″彿涓嶆弧瓒�", basCrnp.getCrnNo());
+            return false;
+        }
+
+        List<StationObjModel> inStationList = basCrnp.getInStationList$();
+        if (inStationList.isEmpty()) {
+            News.info("鍫嗗灈鏈�:{} 鍏ュ簱绔欑偣鏈缃�", basCrnp.getCrnNo());
+            return false;
+        }
+
+        Integer crnNo = basCrnp.getCrnNo();
+
+        for (StationObjModel stationObjModel : inStationList) {
+            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo());
+            if (stationThread == null) {
+                continue;
+            }
+
+            Map<Integer, StationProtocol> stationProtocolMap = stationThread.getStatusMap();
+            StationProtocol stationProtocol = stationProtocolMap.get(stationObjModel.getStationId());
+            if (stationProtocol == null) {
+                continue;
+            }
+
+            if (!stationProtocol.isAutoing()) {
+                continue;
+            }
+
+            if (!stationProtocol.isLoading()) {
+                continue;
+            }
+
+            if (stationProtocol.getTaskNo() <= 0) {
+                continue;
+            }
+
+            if (!stationProtocol.isInEnable()) {
+                News.taskInfo(stationProtocol.getTaskNo(), "鍙栬揣绔欑偣:{} 娌℃湁鍙叆淇″彿", stationObjModel.getStationId());
+                continue;
+            }
+
+            if (!wrkMast.getWrkNo().equals(stationProtocol.getTaskNo())) {
+                continue;
+            }
+
+            if (wrkMast.getWrkSts() != WrkStsType.INBOUND_DEVICE_RUN.sts) {
+                continue;
+            }
+
+            // 鑾峰彇搴撲綅淇℃伅
+            LocMast locMast = locMastService.selectById(wrkMast.getLocNo());
+            if (locMast == null) {
+                News.taskInfo(wrkMast.getWrkNo(), "鐩爣搴撲綅:{} 淇℃伅涓嶅瓨鍦�", wrkMast.getLocNo());
+                continue;
+            }
+
+            if (!locMast.getLocSts().equals("S")) {
+                News.taskInfo(wrkMast.getWrkNo(), "鐩爣搴撲綅:{} 鐘舵�佸紓甯�", wrkMast.getLocNo());
+                continue;
+            }
+
+            //妫�娴嬫祬搴撲綅鐘舵��
+            boolean checkStatus = checkShallowLocStatus(locMast.getLocNo(), wrkMast.getWrkNo());
+            if (!checkStatus) {
+                News.taskInfo(wrkMast.getWrkNo(), "鍥犳祬搴撲綅鍫靛鏃犳硶鎵ц");
+                continue;
+            }
+
+            String sourceLocNo = Utils.getLocNo(stationObjModel.getDeviceRow(), stationObjModel.getDeviceBay(), stationObjModel.getDeviceLev());
+
+            CrnCommand command = crnThread.getPickAndPutCommand(sourceLocNo, wrkMast.getLocNo(), wrkMast.getWrkNo(), crnNo);
+
+            wrkMast.setWrkSts(WrkStsType.INBOUND_RUN.sts);
+            wrkMast.setCrnNo(crnNo);
+            wrkMast.setSystemMsg("");
+            wrkMast.setIoTime(new Date());
+            if (wrkMastService.updateById(wrkMast)) {
+                MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
+                News.info("鍫嗗灈鏈哄懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屼换鍔℃暟鎹�={}", crnNo, JSON.toJSON(command));
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private synchronized boolean crnExecuteOutPlanner(BasCrnp basCrnp, CrnThread crnThread, WrkMast wrkMast) {
+        CrnProtocol crnProtocol = crnThread.getStatus();
+        if (crnProtocol == null) {
+            return false;
+        }
+
+        if (!basCrnp.getOutEnable().equals("Y")) {
+            News.info("鍫嗗灈鏈�:{} 鍙嚭淇″彿涓嶆弧瓒�", basCrnp.getCrnNo());
+            return false;
+        }
+
+        List<StationObjModel> outStationList = basCrnp.getOutStationList$();
+        if (outStationList.isEmpty()) {
+            News.info("鍫嗗灈鏈�:{} 鍑哄簱绔欑偣鏈缃�", basCrnp.getCrnNo());
+            return false;
+        }
+
+        Integer crnNo = basCrnp.getCrnNo();
+
+        for (StationObjModel stationObjModel : outStationList) {
+            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo());
+            if (stationThread == null) {
+                continue;
+            }
+
+            Map<Integer, StationProtocol> stationProtocolMap = stationThread.getStatusMap();
+            StationProtocol stationProtocol = stationProtocolMap.get(stationObjModel.getStationId());
+            if (stationProtocol == null) {
+                continue;
+            }
+
+            if (!stationProtocol.isAutoing()) {
+                continue;
+            }
+
+            if (stationProtocol.isLoading()) {
+                continue;
+            }
+
+            if (stationProtocol.getTaskNo() != 0) {
+                continue;
+            }
+
+            if (!stationProtocol.isOutEnable()) {
+                News.info("鏀捐揣绔欑偣:{} 娌℃湁鍙嚭淇″彿", stationObjModel.getStationId());
+                continue;
+            }
+
+            // 鑾峰彇搴撲綅淇℃伅
+            LocMast locMast = locMastService.selectById(wrkMast.getSourceLocNo());
+            if (locMast == null) {
+                News.taskInfo(wrkMast.getWrkNo(), "婧愬簱浣�:{} 淇℃伅涓嶅瓨鍦�", wrkMast.getSourceLocNo());
+                continue;
+            }
+
+            if (!locMast.getLocSts().equals("R")) {
+                News.taskInfo(wrkMast.getWrkNo(), "婧愬簱浣�:{} 鐘舵�佸紓甯�", wrkMast.getSourceLocNo());
+                continue;
+            }
+
+            //妫�娴嬫祬搴撲綅鐘舵��
+            boolean checkStatus = checkShallowLocStatus(locMast.getLocNo(), wrkMast.getWrkNo());
+            if (!checkStatus) {
+                News.taskInfo(wrkMast.getWrkNo(), "鍥犳祬搴撲綅鍫靛鏃犳硶鎵ц");
+                continue;
+            }
+
+            String targetLocNo = Utils.getLocNo(stationObjModel.getDeviceRow(), stationObjModel.getDeviceBay(), stationObjModel.getDeviceLev());
+
+            CrnCommand command = crnThread.getPickAndPutCommand(wrkMast.getSourceLocNo(), targetLocNo, wrkMast.getWrkNo(), crnNo);
+
+            wrkMast.setWrkSts(WrkStsType.OUTBOUND_RUN.sts);
+            wrkMast.setCrnNo(crnNo);
+            wrkMast.setSystemMsg("");
+            wrkMast.setIoTime(new Date());
+            if (wrkMastService.updateById(wrkMast)) {
+                MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
+                News.info("鍫嗗灈鏈哄懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屼换鍔℃暟鎹�={}", crnNo, JSON.toJSON(command));
+                return true;
+            }
+        }
+        return false;
+    }
+
     private synchronized void crnExecuteLocTransfer(BasCrnp basCrnp, CrnThread crnThread) {
         CrnProtocol crnProtocol = crnThread.getStatus();
         if(crnProtocol == null){
@@ -398,6 +586,153 @@
         }
     }
 
+    public synchronized void plannerExecute() {
+        int nowSec = (int) (System.currentTimeMillis() / 1000);
+        List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<>());
+        for (BasCrnp basCrnp : basCrnps) {
+            String key = RedisKeyType.PLANNER_SCHEDULE.key + "CRN-" + basCrnp.getCrnNo();
+            List<Object> items = redisUtil.lGet(key, 0, -1);
+            if (items == null || items.isEmpty()) {
+                continue;
+            }
+
+            CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo());
+            if (crnThread == null) {
+                continue;
+            }
+            CrnProtocol crnProtocol = crnThread.getStatus();
+            if (crnProtocol == null) {
+                continue;
+            }
+            List<WrkMast> running = wrkMastService.selectList(new EntityWrapper<WrkMast>()
+                    .eq("crn_no", basCrnp.getCrnNo())
+                    .in("wrk_sts", WrkStsType.INBOUND_RUN.sts, WrkStsType.OUTBOUND_RUN.sts, WrkStsType.LOC_MOVE_RUN.sts)
+            );
+            if (!running.isEmpty()) {
+                continue;
+            }
+            if (!(crnProtocol.getMode() == CrnModeType.AUTO.id
+                    && crnProtocol.getTaskNo() == 0
+                    && crnProtocol.getStatus() == CrnStatusType.IDLE.id
+                    && crnProtocol.getLoaded() == 0
+                    && crnProtocol.getForkPos() == 0
+                    && crnProtocol.getAlarm() == 0)) {
+                continue;
+            }
+
+            for (Object v : items) {
+                String s = String.valueOf(v);
+                JSONObject obj = null;
+                try { obj = JSON.parseObject(s); } catch (Exception ignore) {}
+                if (obj == null) {
+                    continue;
+                }
+                Integer startEpochSec = obj.getInteger("startEpochSec");
+                Integer endEpochSec = obj.getInteger("endEpochSec");
+                Integer taskId = obj.getInteger("taskId");
+                String taskType = obj.getString("taskType");
+                if (startEpochSec == null || taskId == null || taskType == null) {
+                    continue;
+                }
+                int earlySlackSec = 5;
+                int lateSlackSec = 10;
+                Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
+                if (systemConfigMapObj != null) {
+                    try {
+                        HashMap<String, String> systemConfigMap = (HashMap<String, String>) systemConfigMapObj;
+                        String es = systemConfigMap.getOrDefault("plannerEarlySlackSec", "60");
+                        String ls = systemConfigMap.getOrDefault("plannerLateSlackSec", "10");
+                        earlySlackSec = Integer.parseInt(es);
+                        lateSlackSec = Integer.parseInt(ls);
+                    } catch (Exception ignore) {}
+                }
+                if (nowSec < startEpochSec - earlySlackSec) {
+                    continue;
+                }
+                if (endEpochSec != null && nowSec > endEpochSec + lateSlackSec) {
+                    redisUtil.lRemove(key, 1, s);
+                    continue;
+                }
+
+                WrkMast wrkMast = wrkMastService.selectByWorkNo(taskId);
+                if (wrkMast == null) {
+                    redisUtil.lRemove(key, 1, s);
+                    continue;
+                }
+
+                if ("IN".equalsIgnoreCase(taskType)) {
+                    boolean result = this.crnExecuteInPlanner(basCrnp, crnThread, wrkMast);//鍏ュ簱
+                    if (result) {
+                        redisUtil.lRemove(key, 1, s);
+                        break;
+                    }
+                } else if ("OUT".equalsIgnoreCase(taskType)) {
+                    boolean result = this.crnExecuteOutPlanner(basCrnp, crnThread, wrkMast);//鍑哄簱
+                    if (result) {
+                        redisUtil.lRemove(key, 1, s);
+                        break;
+                    }
+                } else if ("MOVE".equalsIgnoreCase(taskType)) {
+                    boolean result = this.crnExecuteMovePlanner(basCrnp, crnThread, wrkMast);//绉诲簱
+                    if (result) {
+                        redisUtil.lRemove(key, 1, s);
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    private synchronized boolean crnExecuteMovePlanner(BasCrnp basCrnp, CrnThread crnThread, WrkMast wrkMast) {
+        CrnProtocol crnProtocol = crnThread.getStatus();
+        if (crnProtocol == null) {
+            return false;
+        }
+
+        Integer crnNo = basCrnp.getCrnNo();
+
+        if (!wrkMast.getWrkSts().equals(WrkStsType.NEW_LOC_MOVE.sts)) {
+            return false;
+        }
+
+        // 鑾峰彇婧愬簱浣嶄俊鎭�
+        LocMast sourceLocMast = locMastService.selectById(wrkMast.getSourceLocNo());
+        if (sourceLocMast == null) {
+            News.taskInfo(wrkMast.getWrkNo(), "婧愬簱浣�:{} 淇℃伅涓嶅瓨鍦�", wrkMast.getSourceLocNo());
+            return false;
+        }
+
+        if(!sourceLocMast.getLocSts().equals("R")){
+            News.taskInfo(wrkMast.getWrkNo(), "婧愬簱浣�:{} 鐘舵�佸紓甯革紝涓嶅睘浜庡嚭搴撻绾︾姸鎬�", wrkMast.getSourceLocNo());
+            return false;
+        }
+
+        // 鑾峰彇搴撲綅淇℃伅
+        LocMast locMast = locMastService.selectById(wrkMast.getLocNo());
+        if (locMast == null) {
+            News.taskInfo(wrkMast.getWrkNo(), "搴撲綅:{} 淇℃伅涓嶅瓨鍦�", wrkMast.getLocNo());
+            return false;
+        }
+
+        if (!locMast.getLocSts().equals("S")) {
+            News.taskInfo(wrkMast.getWrkNo(), "搴撲綅:{} 鐘舵�佸紓甯革紝涓嶅睘浜庡叆搴撻绾︾姸鎬�", wrkMast.getLocNo());
+            return false;
+        }
+
+        CrnCommand command = crnThread.getPickAndPutCommand(wrkMast.getSourceLocNo(), wrkMast.getLocNo(), wrkMast.getWrkNo(), crnNo);
+
+        wrkMast.setWrkSts(WrkStsType.LOC_MOVE_RUN.sts);
+        wrkMast.setCrnNo(crnNo);
+        wrkMast.setSystemMsg("");
+        wrkMast.setIoTime(new Date());
+        if (wrkMastService.updateById(wrkMast)) {
+            MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command));
+            News.info("鍫嗗灈鏈哄懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屼换鍔℃暟鎹�={}", crnNo, JSON.toJSON(command));
+            return true;
+        }
+        return false;
+    }
+
     //妫�娴嬫祬搴撲綅鐘舵��
     public synchronized boolean checkShallowLocStatus(String locNo, Integer taskNo) {
         String checkDeepLocOutTaskBlockReport = "Y";
diff --git a/src/main/java/com/zy/core/utils/StationOperateProcessUtils.java b/src/main/java/com/zy/core/utils/StationOperateProcessUtils.java
index f0ed359..8237bbe 100644
--- a/src/main/java/com/zy/core/utils/StationOperateProcessUtils.java
+++ b/src/main/java/com/zy/core/utils/StationOperateProcessUtils.java
@@ -13,10 +13,7 @@
 import com.zy.core.News;
 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.enums.StationCommandType;
-import com.zy.core.enums.WrkStsType;
+import com.zy.core.enums.*;
 import com.zy.core.model.StationObjModel;
 import com.zy.core.model.Task;
 import com.zy.core.model.command.StationCommand;
@@ -258,7 +255,7 @@
                     }
                     redisUtil.set(RedisKeyType.CHECK_STATION_RUN_BLOCK_LIMIT_.key + stationProtocol.getTaskNo(), "lock", 15);
 
-                    if (runBlockReassignLocStationList.contains(stationProtocol.getStationId())) {
+                    if (wrkMast.getIoType() == WrkIoType.IN.id && runBlockReassignLocStationList.contains(stationProtocol.getStationId())) {
                         //绔欑偣澶勪簬閲嶆柊鍒嗛厤搴撲綅鍖哄煙
                         //杩愯鍫靛锛岄噸鏂扮敵璇蜂换鍔�
                         String response = wmsOperateUtils.applyReassignTaskLocNo(wrkMast.getWrkNo(), stationProtocol.getStationId());
@@ -358,5 +355,25 @@
         }
     }
 
+    //鑾峰彇杈撻�佺嚎浠诲姟鏁伴噺
+    public synchronized int getCurrentStationTaskCount() {
+        int currentStationTaskCount = 0;
+        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;
+            }
+
+            for (StationProtocol stationProtocol : stationThread.getStatus()) {
+                if (stationProtocol.getTaskNo() > 0) {
+                    currentStationTaskCount++;
+                }
+            }
+        }
+
+        return currentStationTaskCount;
+    }
+
 
 }
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index a2f2ff6..80eaef7 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -67,7 +67,7 @@
   threadControlCount: 10
   liftType: lift
 
-mainProcessPlugin: NormalProcess
+mainProcessPlugin: FakeProcess
 
 deviceLogStorage:
   # 璁惧鏃ュ織瀛樺偍鏂瑰紡 mysql file

--
Gitblit v1.9.1