From 5d5d6b55f439a9cb42d948e816a9db70e3fb2805 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期二, 31 三月 2026 08:14:53 +0800
Subject: [PATCH] #堆垛机移动

---
 src/test/java/com/zy/asrs/task/WrkAnalysisStationArrivalScannerTest.java |  224 ++++++++++++
 src/main/java/com/zy/asrs/domain/enums/CrnStatusType.java                |    8 
 src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java       |   15 
 src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java              |  206 +++++++++++
 src/main/java/com/zy/asrs/task/WrkMastScheduler.java                     |   21 +
 src/main/java/com/zy/core/service/WrkCommandRollbackService.java         |   12 
 src/main/java/com/zy/core/enums/WrkStsType.java                          |    6 
 src/main/java/com/zy/core/enums/WrkIoType.java                           |    1 
 src/test/java/com/zy/core/utils/CrnOperateProcessUtilsTest.java          |  397 ++++++++++++++++++++++
 src/main/resources/sql/20260330_add_crn_move_task_type.sql               |   27 +
 src/main/java/com/zy/common/service/CommonService.java                   |    4 
 src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java     |   92 +++++
 src/main/java/com/zy/core/plugin/FakeProcess.java                        |    2 
 src/test/java/com/zy/asrs/task/WrkMastSchedulerCrnMoveTest.java          |   54 +++
 14 files changed, 1,061 insertions(+), 8 deletions(-)

diff --git a/src/main/java/com/zy/asrs/domain/enums/CrnStatusType.java b/src/main/java/com/zy/asrs/domain/enums/CrnStatusType.java
index 99b6d01..38efac2 100644
--- a/src/main/java/com/zy/asrs/domain/enums/CrnStatusType.java
+++ b/src/main/java/com/zy/asrs/domain/enums/CrnStatusType.java
@@ -1,5 +1,7 @@
 package com.zy.asrs.domain.enums;
 
+import com.zy.core.enums.WrkIoType;
+
 /**
  * 鍫嗗灈鏈虹姸鎬佹灇涓�
  */
@@ -37,6 +39,12 @@
     }
 
     public static CrnStatusType process(Integer ioType){
+        if (ioType == null) {
+            return MACHINE_ERROR;
+        }
+        if (ioType.equals(WrkIoType.CRN_MOVE.id)) {
+            return MACHINE_SITE_MOVE;
+        }
         if (ioType>100) {
             return MACHINE_PAKOUT;
         } else if (ioType < 100 && ioType!=3 && ioType!=6 && ioType!=11) {
diff --git a/src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java
index f647010..6bbaac2 100644
--- a/src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java
+++ b/src/main/java/com/zy/asrs/service/impl/WrkAnalysisServiceImpl.java
@@ -30,7 +30,8 @@
             WrkStsType.SETTLE_INBOUND.sts,
             WrkStsType.COMPLETE_OUTBOUND.sts,
             WrkStsType.SETTLE_OUTBOUND.sts,
-            WrkStsType.COMPLETE_LOC_MOVE.sts
+            WrkStsType.COMPLETE_LOC_MOVE.sts,
+            WrkStsType.COMPLETE_CRN_MOVE.sts
     );
     private static final String MODE_TASK = "TASK";
     private static final String MODE_TIME = "TIME";
@@ -186,7 +187,9 @@
         entity.setRgvNo(wrkMast.getRgvNo());
         entity.setFinalWrkSts(wrkMast.getWrkSts());
         entity.setUpdateTime(time);
-        if (Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id) && entity.getStationDurationMs() == null) {
+        if ((Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id)
+                || Objects.equals(wrkMast.getIoType(), WrkIoType.CRN_MOVE.id))
+                && entity.getStationDurationMs() == null) {
             entity.setStationDurationMs(0L);
         }
         this.updateById(entity);
@@ -225,7 +228,9 @@
         if (entity.getAppeTime() != null) {
             entity.setTotalDurationMs(durationMs(entity.getAppeTime(), time));
         }
-        if (Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id) && entity.getStationDurationMs() == null) {
+        if ((Objects.equals(wrkMast.getIoType(), WrkIoType.LOC_MOVE.id)
+                || Objects.equals(wrkMast.getIoType(), WrkIoType.CRN_MOVE.id))
+                && entity.getStationDurationMs() == null) {
             entity.setStationDurationMs(0L);
         }
         FaultSummary faultSummary = buildFaultSummary(wrkMast.getWrkNo(), time);
@@ -265,6 +270,7 @@
         ioTypes.add(option("1", "IN", "鍏ュ簱", WrkIoType.IN.id));
         ioTypes.add(option("2", "OUT", "鍑哄簱", WrkIoType.OUT.id));
         ioTypes.add(option("3", "LOC_MOVE", "绉诲簱", WrkIoType.LOC_MOVE.id));
+        ioTypes.add(option("4", "CRN_MOVE", "鍫嗗灈鏈虹Щ鍔�", WrkIoType.CRN_MOVE.id));
         List<Map<String, Object>> timeFields = new ArrayList<>();
         timeFields.add(option(TIME_FIELD_FINISH, TIME_FIELD_FINISH, "瀹屾垚鏃堕棿", TIME_FIELD_FINISH));
         timeFields.add(option(TIME_FIELD_APPE, TIME_FIELD_APPE, "鍒涘缓鏃堕棿", TIME_FIELD_APPE));
@@ -819,6 +825,9 @@
         if (Objects.equals(ioType, WrkIoType.LOC_MOVE.id)) {
             return "绉诲簱";
         }
+        if (Objects.equals(ioType, WrkIoType.CRN_MOVE.id)) {
+            return "鍫嗗灈鏈虹Щ鍔�";
+        }
         return String.valueOf(ioType);
     }
 
diff --git a/src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java b/src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java
index cf33ff0..f299636 100644
--- a/src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java
+++ b/src/main/java/com/zy/asrs/task/WrkAnalysisStationArrivalScanner.java
@@ -1,17 +1,27 @@
 package com.zy.asrs.task;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.common.Cools;
+import com.zy.asrs.entity.BasCrnp;
 import com.zy.asrs.entity.BasStation;
 import com.zy.asrs.entity.WrkMast;
+import com.zy.asrs.service.BasCrnpService;
 import com.zy.asrs.service.BasStationService;
 import com.zy.asrs.service.WrkAnalysisService;
 import com.zy.asrs.service.WrkMastService;
+import com.zy.asrs.utils.Utils;
+import com.zy.common.entity.FindCrnNoResult;
+import com.zy.common.service.CommonService;
 import com.zy.core.News;
 import com.zy.core.cache.SlaveConnection;
 import com.zy.core.enums.SlaveType;
 import com.zy.core.enums.WrkStsType;
+import com.zy.core.model.StationObjModel;
+import com.zy.core.move.StationMoveCoordinator;
+import com.zy.core.move.StationMoveSession;
 import com.zy.core.model.protocol.StationProtocol;
 import com.zy.core.thread.StationThread;
+import com.zy.core.utils.CrnOperateProcessUtils;
 import com.zy.core.utils.StationOperateProcessUtils;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
@@ -19,6 +29,7 @@
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 @Component
 public class WrkAnalysisStationArrivalScanner {
@@ -27,15 +38,27 @@
     private final BasStationService basStationService;
     private final WrkAnalysisService wrkAnalysisService;
     private final StationOperateProcessUtils stationOperateProcessUtils;
+    private final StationMoveCoordinator stationMoveCoordinator;
+    private final CommonService commonService;
+    private final BasCrnpService basCrnpService;
+    private final CrnOperateProcessUtils crnOperateProcessUtils;
 
     public WrkAnalysisStationArrivalScanner(WrkMastService wrkMastService,
                                             BasStationService basStationService,
                                             WrkAnalysisService wrkAnalysisService,
-                                            StationOperateProcessUtils stationOperateProcessUtils) {
+                                            StationOperateProcessUtils stationOperateProcessUtils,
+                                            StationMoveCoordinator stationMoveCoordinator,
+                                            CommonService commonService,
+                                            BasCrnpService basCrnpService,
+                                            CrnOperateProcessUtils crnOperateProcessUtils) {
         this.wrkMastService = wrkMastService;
         this.basStationService = basStationService;
         this.wrkAnalysisService = wrkAnalysisService;
         this.stationOperateProcessUtils = stationOperateProcessUtils;
+        this.stationMoveCoordinator = stationMoveCoordinator;
+        this.commonService = commonService;
+        this.basCrnpService = basCrnpService;
+        this.crnOperateProcessUtils = crnOperateProcessUtils;
     }
 
     @Scheduled(fixedDelay = 1000L)
@@ -66,6 +89,7 @@
             }
             Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
             StationProtocol stationProtocol = statusMap == null ? null : statusMap.get(basStation.getStationId());
+            tryDispatchInboundCrnMove(wrkMast, stationProtocol);
             boolean arrived = stationProtocol != null
                     && wrkMast.getWrkNo().equals(stationProtocol.getTaskNo())
                     && stationProtocol.isLoading();
@@ -78,4 +102,70 @@
             }
         }
     }
+
+    private void tryDispatchInboundCrnMove(WrkMast wrkMast, StationProtocol targetStationProtocol) {
+        if (wrkMast == null || wrkMast.getWrkNo() == null || wrkMast.getStaNo() == null || Cools.isEmpty(wrkMast.getLocNo())) {
+            return;
+        }
+        if (targetStationProtocol != null
+                && targetStationProtocol.isLoading()
+                && targetStationProtocol.getTaskNo() > 0
+                && targetStationProtocol.isInEnable()) {
+            return;
+        }
+
+        StationMoveSession session = stationMoveCoordinator == null ? null : stationMoveCoordinator.loadSession(wrkMast.getWrkNo());
+        if (!isInboundCrnMoveDispatchWindow(wrkMast, session)) {
+            return;
+        }
+
+        FindCrnNoResult findCrnNoResult = commonService.findCrnNoByLocNo(wrkMast.getLocNo());
+        if (findCrnNoResult == null || !Objects.equals(findCrnNoResult.getCrnType(), SlaveType.Crn) || findCrnNoResult.getCrnNo() == null) {
+            return;
+        }
+
+        BasCrnp basCrnp = basCrnpService.getOne(new QueryWrapper<BasCrnp>()
+                .eq("crn_no", findCrnNoResult.getCrnNo())
+                .last("limit 1"));
+        if (basCrnp == null) {
+            return;
+        }
+
+        for (StationObjModel stationObjModel : basCrnp.getInStationList$()) {
+            if (stationObjModel == null || !Objects.equals(stationObjModel.getStationId(), wrkMast.getStaNo())) {
+                continue;
+            }
+            if (stationObjModel.getDeviceRow() == null || stationObjModel.getDeviceBay() == null || stationObjModel.getDeviceLev() == null) {
+                continue;
+            }
+            String inletLocNo = Utils.getLocNo(stationObjModel.getDeviceRow(), stationObjModel.getDeviceBay(), stationObjModel.getDeviceLev());
+            boolean dispatched = crnOperateProcessUtils.dispatchCrnMove(findCrnNoResult.getCrnNo(), inletLocNo);
+            if (dispatched) {
+                News.info("鍏ュ簱浠诲姟鍗冲皢鍒拌揪鍏ュ簱鍙o紝宸茶Е鍙戝爢鍨涙満棰勭Щ杞︼紝宸ヤ綔鍙�={}锛屽爢鍨涙満鍙�={}锛屽叆搴撳彛浣嶇疆={}",
+                        wrkMast.getWrkNo(), findCrnNoResult.getCrnNo(), inletLocNo);
+            }
+            return;
+        }
+    }
+
+    private boolean isInboundCrnMoveDispatchWindow(WrkMast wrkMast, StationMoveSession session) {
+        if (wrkMast == null || session == null || !session.isActive() || wrkMast.getStaNo() == null) {
+            return false;
+        }
+        List<Integer> fullPathStationIds = session.getFullPathStationIds();
+        Integer currentStationId = session.getCurrentStationId();
+        if (fullPathStationIds == null || fullPathStationIds.isEmpty() || currentStationId == null) {
+            return false;
+        }
+        int currentIndex = fullPathStationIds.lastIndexOf(currentStationId);
+        if (currentIndex < 0 || currentIndex >= fullPathStationIds.size() - 1) {
+            return false;
+        }
+        int remainingStationCount = fullPathStationIds.size() - currentIndex - 1;
+        if (remainingStationCount != 1) {
+            return false;
+        }
+        Integer nextStationId = fullPathStationIds.get(currentIndex + 1);
+        return Objects.equals(nextStationId, wrkMast.getStaNo());
+    }
 }
diff --git a/src/main/java/com/zy/asrs/task/WrkMastScheduler.java b/src/main/java/com/zy/asrs/task/WrkMastScheduler.java
index 87dfaea..9ac3fac 100644
--- a/src/main/java/com/zy/asrs/task/WrkMastScheduler.java
+++ b/src/main/java/com/zy/asrs/task/WrkMastScheduler.java
@@ -205,6 +205,27 @@
 
     @Scheduled(cron = "0/1 * * * * ? ")
     @Transactional
+    public void executeCrnMove(){
+        List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>().eq("wrk_sts", WrkStsType.COMPLETE_CRN_MOVE.sts));
+        if (wrkMasts.isEmpty()) {
+            return;
+        }
+
+        for (WrkMast wrkMast : wrkMasts) {
+            if (!wrkMastLogService.save(wrkMast.getWrkNo())) {
+                log.info("淇濆瓨宸ヤ綔鍘嗗彶妗workNo={}]澶辫触", wrkMast.getWrkNo());
+            } else {
+                wrkAnalysisService.finishTask(wrkMast, resolveFinishTime(wrkMast));
+            }
+
+            if (!wrkMastService.removeById(wrkMast.getWrkNo())) {
+                log.info("鍒犻櫎宸ヤ綔涓绘。[workNo={}]澶辫触", wrkMast.getWrkNo());
+            }
+        }
+    }
+
+    @Scheduled(cron = "0/1 * * * * ? ")
+    @Transactional
     public void executeCancelTask(){
         List<WrkMast> wrkMasts = wrkMastService.list(new QueryWrapper<WrkMast>().eq("mk", "taskCancel"));
         if (wrkMasts.isEmpty()) {
diff --git a/src/main/java/com/zy/common/service/CommonService.java b/src/main/java/com/zy/common/service/CommonService.java
index bf6fa7f..f03a253 100644
--- a/src/main/java/com/zy/common/service/CommonService.java
+++ b/src/main/java/com/zy/common/service/CommonService.java
@@ -113,6 +113,8 @@
             wrkMast.setWrkSts(WrkStsType.COMPLETE_OUTBOUND.sts);
         } else if (wrkMast.getIoType() == WrkIoType.LOC_MOVE.id) {
             wrkMast.setWrkSts(WrkStsType.COMPLETE_LOC_MOVE.sts);
+        } else if (wrkMast.getIoType() == WrkIoType.CRN_MOVE.id) {
+            wrkMast.setWrkSts(WrkStsType.COMPLETE_CRN_MOVE.sts);
         }
 
         wrkMast.setModiTime(new Date());
@@ -144,6 +146,8 @@
             cancelSuccess = true;
         } else if (wrkMast.getIoType().equals(WrkIoType.LOC_MOVE.id) && !wrkMast.getWrkSts().equals(WrkStsType.NEW_LOC_MOVE.sts)) {
             cancelSuccess = true;
+        } else if (wrkMast.getIoType().equals(WrkIoType.CRN_MOVE.id) && !wrkMast.getWrkSts().equals(WrkStsType.NEW_CRN_MOVE.sts)) {
+            cancelSuccess = true;
         }
 
         if (cancelSuccess) {
diff --git a/src/main/java/com/zy/core/enums/WrkIoType.java b/src/main/java/com/zy/core/enums/WrkIoType.java
index 9307c60..14d432a 100644
--- a/src/main/java/com/zy/core/enums/WrkIoType.java
+++ b/src/main/java/com/zy/core/enums/WrkIoType.java
@@ -7,6 +7,7 @@
     IN(1, "鍏ュ簱"),
     OUT(101, "鍑哄簱"),
     LOC_MOVE(201, "绉诲簱浠诲姟"),
+    CRN_MOVE(301, "鍫嗗灈鏈虹Щ鍔ㄤ换鍔�"),
     FAKE_TASK_NO(9999, "浠跨湡闅忔満宸ヤ綔鍙�"),
     ENABLE_IN(9998, "鍚姩鍏ュ簱"),
     STATION_BACK(9997, "绔欑偣鍥為��"),
diff --git a/src/main/java/com/zy/core/enums/WrkStsType.java b/src/main/java/com/zy/core/enums/WrkStsType.java
index 1c713c1..66789fb 100644
--- a/src/main/java/com/zy/core/enums/WrkStsType.java
+++ b/src/main/java/com/zy/core/enums/WrkStsType.java
@@ -27,6 +27,12 @@
     LOC_MOVE_RUN_COMPLETE(503, "璁惧鎼繍瀹屾垚"),
     LOC_MOVE_MANUAL(506, "绉诲簱寰呬汉宸ュ洖婊�"),
     COMPLETE_LOC_MOVE(509, "绉诲簱瀹屾垚"),
+
+    NEW_CRN_MOVE(601, "鐢熸垚鍫嗗灈鏈虹Щ鍔ㄤ换鍔�"),
+    CRN_MOVE_RUN(602, "鍫嗗灈鏈虹Щ鍔ㄤ腑"),
+    CRN_MOVE_RUN_COMPLETE(603, "鍫嗗灈鏈虹Щ鍔ㄥ畬鎴�"),
+    CRN_MOVE_MANUAL(606, "鍫嗗灈鏈虹Щ鍔ㄥ緟浜哄伐鍥炴粴"),
+    COMPLETE_CRN_MOVE(609, "鍫嗗灈鏈虹Щ鍔ㄤ换鍔″畬鎴�"),
     ;
 
 
diff --git a/src/main/java/com/zy/core/plugin/FakeProcess.java b/src/main/java/com/zy/core/plugin/FakeProcess.java
index a136dd6..99a25f7 100644
--- a/src/main/java/com/zy/core/plugin/FakeProcess.java
+++ b/src/main/java/com/zy/core/plugin/FakeProcess.java
@@ -704,6 +704,8 @@
                     }
                 } else if (wrkMast.getWrkSts() == WrkStsType.LOC_MOVE_RUN.sts) {
                     updateWrkSts = WrkStsType.COMPLETE_LOC_MOVE.sts;
+                } else if (wrkMast.getWrkSts() == WrkStsType.CRN_MOVE_RUN.sts) {
+                    updateWrkSts = WrkStsType.COMPLETE_CRN_MOVE.sts;
                 } else {
                     News.error("鍫嗗灈鏈哄浜庣瓑寰呯‘璁や笖浠诲姟瀹屾垚鐘舵�侊紝浣嗗伐浣滅姸鎬佸紓甯搞�傚爢鍨涙満鍙�={}锛屽伐浣滃彿={}", basCrnp.getCrnNo(), crnProtocol.getTaskNo());
                     continue;
diff --git a/src/main/java/com/zy/core/service/WrkCommandRollbackService.java b/src/main/java/com/zy/core/service/WrkCommandRollbackService.java
index 580fded..cd36992 100644
--- a/src/main/java/com/zy/core/service/WrkCommandRollbackService.java
+++ b/src/main/java/com/zy/core/service/WrkCommandRollbackService.java
@@ -168,7 +168,8 @@
     private boolean isSingleCrnRunStatus(Long wrkSts) {
         return Long.valueOf(WrkStsType.INBOUND_RUN.sts).equals(wrkSts)
                 || Long.valueOf(WrkStsType.OUTBOUND_RUN.sts).equals(wrkSts)
-                || Long.valueOf(WrkStsType.LOC_MOVE_RUN.sts).equals(wrkSts);
+                || Long.valueOf(WrkStsType.LOC_MOVE_RUN.sts).equals(wrkSts)
+                || Long.valueOf(WrkStsType.CRN_MOVE_RUN.sts).equals(wrkSts);
     }
 
     private Long getRollbackStatus(Long wrkSts) {
@@ -180,6 +181,9 @@
         }
         if (Long.valueOf(WrkStsType.LOC_MOVE_RUN.sts).equals(wrkSts)) {
             return WrkStsType.NEW_LOC_MOVE.sts;
+        }
+        if (Long.valueOf(WrkStsType.CRN_MOVE_RUN.sts).equals(wrkSts)) {
+            return WrkStsType.NEW_CRN_MOVE.sts;
         }
         return null;
     }
@@ -194,6 +198,9 @@
         if (Long.valueOf(WrkStsType.LOC_MOVE_RUN.sts).equals(wrkSts)) {
             return WrkStsType.LOC_MOVE_MANUAL.sts;
         }
+        if (Long.valueOf(WrkStsType.CRN_MOVE_RUN.sts).equals(wrkSts)) {
+            return WrkStsType.CRN_MOVE_MANUAL.sts;
+        }
         return null;
     }
 
@@ -207,6 +214,9 @@
         if (Long.valueOf(WrkStsType.LOC_MOVE_MANUAL.sts).equals(wrkSts)) {
             return WrkStsType.NEW_LOC_MOVE.sts;
         }
+        if (Long.valueOf(WrkStsType.CRN_MOVE_MANUAL.sts).equals(wrkSts)) {
+            return WrkStsType.NEW_CRN_MOVE.sts;
+        }
         return null;
     }
 }
diff --git a/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java b/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
index ec4c22d..614f005 100644
--- a/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
+++ b/src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
@@ -40,8 +40,11 @@
 import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 
 @Component
 public class CrnOperateProcessUtils {
@@ -83,11 +86,16 @@
 
     //鍏ュ嚭搴�  ===>>  鍫嗗灈鏈哄叆鍑哄簱浣滀笟涓嬪彂
     public synchronized void crnIoExecuteNormal() {
+        Set<Integer> crnMoveBlockedCrnNos = executeCrnMoveTask();
+
         List<BasCrnp> basCrnps = basCrnpService.list(new QueryWrapper<>());
         Map<Integer, BasCrnp> dispatchCrnMap = new HashMap<>();
         Map<Integer, CrnThread> dispatchThreadMap = new HashMap<>();
         Map<Integer, CrnProtocol> dispatchProtocolMap = new HashMap<>();
         for (BasCrnp basCrnp : basCrnps) {
+            if (basCrnp == null || basCrnp.getCrnNo() == null || crnMoveBlockedCrnNos.contains(basCrnp.getCrnNo())) {
+                continue;
+            }
             CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo());
             if (crnThread == null) {
                 continue;
@@ -100,7 +108,7 @@
 
             long runningCount = wrkMastService.count(new QueryWrapper<WrkMast>()
                     .eq("crn_no", basCrnp.getCrnNo())
-                    .in("wrk_sts", WrkStsType.INBOUND_RUN.sts, WrkStsType.OUTBOUND_RUN.sts, WrkStsType.LOC_MOVE_RUN.sts));
+                    .in("wrk_sts", WrkStsType.INBOUND_RUN.sts, WrkStsType.OUTBOUND_RUN.sts, WrkStsType.LOC_MOVE_RUN.sts, WrkStsType.CRN_MOVE_RUN.sts));
             if (runningCount > 0) {
                 continue;
             }
@@ -655,6 +663,8 @@
                 }else if(wrkMast.getWrkSts() == WrkStsType.LOC_MOVE_RUN.sts){
                     updateWrkSts = WrkStsType.COMPLETE_LOC_MOVE.sts;
                     notifyUtils.notify(String.valueOf(SlaveType.Crn), crnProtocol.getCrnNo(), String.valueOf(wrkMast.getWrkNo()), wrkMast.getWmsWrkNo(), NotifyMsgType.CRN_TRANSFER_TASK_COMPLETE, null);
+                }else if(wrkMast.getWrkSts() == WrkStsType.CRN_MOVE_RUN.sts){
+                    updateWrkSts = WrkStsType.COMPLETE_CRN_MOVE.sts;
                 }else{
                     News.error("鍫嗗灈鏈哄浜庣瓑寰呯‘璁や笖浠诲姟瀹屾垚鐘舵�侊紝浣嗗伐浣滅姸鎬佸紓甯搞�傚爢鍨涙満鍙�={}锛屽伐浣滃彿={}", basCrnp.getCrnNo(), crnProtocol.getTaskNo());
                     continue;
@@ -677,9 +687,14 @@
     }
 
     public synchronized void plannerExecute() {
+        Set<Integer> crnMoveBlockedCrnNos = executeCrnMoveTask();
+
         int nowSec = (int) (System.currentTimeMillis() / 1000);
         List<BasCrnp> basCrnps = basCrnpService.list(new QueryWrapper<>());
         for (BasCrnp basCrnp : basCrnps) {
+            if (basCrnp == null || basCrnp.getCrnNo() == null || crnMoveBlockedCrnNos.contains(basCrnp.getCrnNo())) {
+                continue;
+            }
             String key = RedisKeyType.PLANNER_SCHEDULE.key + "CRN-" + basCrnp.getCrnNo();
             List<Object> items = redisUtil.lGet(key, 0, -1);
             if (items == null || items.isEmpty()) {
@@ -696,7 +711,7 @@
             }
             List<WrkMast> running = wrkMastService.list(new QueryWrapper<WrkMast>()
                     .eq("crn_no", basCrnp.getCrnNo())
-                    .in("wrk_sts", WrkStsType.INBOUND_RUN.sts, WrkStsType.OUTBOUND_RUN.sts, WrkStsType.LOC_MOVE_RUN.sts)
+                    .in("wrk_sts", WrkStsType.INBOUND_RUN.sts, WrkStsType.OUTBOUND_RUN.sts, WrkStsType.LOC_MOVE_RUN.sts, WrkStsType.CRN_MOVE_RUN.sts)
             );
             if (!running.isEmpty()) {
                 continue;
@@ -827,6 +842,114 @@
         return false;
     }
 
+    private Set<Integer> executeCrnMoveTask() {
+        List<WrkMast> pendingTaskQueue = wrkMastService.list(new QueryWrapper<WrkMast>()
+                .eq("io_type", WrkIoType.CRN_MOVE.id)
+                .eq("wrk_sts", WrkStsType.NEW_CRN_MOVE.sts)
+                .orderByAsc("appe_time")
+                .orderByAsc("wrk_no"));
+        Set<Integer> blockedCrnNoSet = new HashSet<>();
+        for (WrkMast wrkMast : pendingTaskQueue) {
+            if (wrkMast != null && wrkMast.getCrnNo() != null) {
+                blockedCrnNoSet.add(wrkMast.getCrnNo());
+            }
+        }
+        if (blockedCrnNoSet.isEmpty()) {
+            return blockedCrnNoSet;
+        }
+
+        List<BasCrnp> basCrnps = basCrnpService.list(new QueryWrapper<>());
+        Map<Integer, CrnThread> dispatchThreadMap = new HashMap<>();
+        Map<Integer, CrnProtocol> dispatchProtocolMap = new HashMap<>();
+        for (BasCrnp basCrnp : basCrnps) {
+            if (basCrnp == null || basCrnp.getCrnNo() == null) {
+                continue;
+            }
+
+            Integer crnNo = basCrnp.getCrnNo();
+            CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, crnNo);
+            if (crnThread == null) {
+                continue;
+            }
+
+            CrnProtocol crnProtocol = crnThread.getStatus();
+            if (crnProtocol == null) {
+                continue;
+            }
+
+            long runningCount = wrkMastService.count(new QueryWrapper<WrkMast>()
+                    .eq("crn_no", crnNo)
+                    .in("wrk_sts",
+                            WrkStsType.INBOUND_RUN.sts,
+                            WrkStsType.OUTBOUND_RUN.sts,
+                            WrkStsType.LOC_MOVE_RUN.sts,
+                            WrkStsType.CRN_MOVE_RUN.sts));
+            if (runningCount > 0) {
+                continue;
+            }
+
+            if (!Objects.equals(crnProtocol.getMode(), CrnModeType.AUTO.id)
+                    || !Objects.equals(crnProtocol.getTaskNo(), 0)
+                    || !Objects.equals(crnProtocol.getStatus(), CrnStatusType.IDLE.id)
+                    || !Objects.equals(crnProtocol.getLoaded(), 0)
+                    || !Objects.equals(crnProtocol.getForkPos(), 0)
+                    || !Objects.equals(crnProtocol.getAlarm(), 0)) {
+                continue;
+            }
+
+            Object clearLock = redisUtil.get(RedisKeyType.CLEAR_CRN_TASK_LIMIT.key + crnNo);
+            if (clearLock != null) {
+                continue;
+            }
+
+            dispatchThreadMap.put(crnNo, crnThread);
+            dispatchProtocolMap.put(crnNo, crnProtocol);
+        }
+
+        if (dispatchThreadMap.isEmpty()) {
+            return blockedCrnNoSet;
+        }
+
+        List<WrkMast> taskQueue = wrkMastService.list(new QueryWrapper<WrkMast>()
+                .in("crn_no", new ArrayList<>(dispatchThreadMap.keySet()))
+                .eq("io_type", WrkIoType.CRN_MOVE.id)
+                .eq("wrk_sts", WrkStsType.NEW_CRN_MOVE.sts)
+                .orderByAsc("appe_time")
+                .orderByAsc("wrk_no"));
+
+        for (WrkMast wrkMast : taskQueue) {
+            if (wrkMast == null || wrkMast.getCrnNo() == null || Cools.isEmpty(wrkMast.getLocNo())) {
+                continue;
+            }
+
+            Integer crnNo = wrkMast.getCrnNo();
+            CrnThread crnThread = dispatchThreadMap.get(crnNo);
+            if (crnThread == null || dispatchProtocolMap.get(crnNo) == null) {
+                continue;
+            }
+
+            CrnCommand moveCommand = crnThread.getMoveCommand(wrkMast.getLocNo(), wrkMast.getWrkNo(), crnNo);
+            if (moveCommand == null) {
+                continue;
+            }
+
+            Date now = new Date();
+            wrkMast.setWrkSts(WrkStsType.CRN_MOVE_RUN.sts);
+            wrkMast.setCrnNo(crnNo);
+            wrkMast.setSystemMsg("");
+            wrkMast.setIoTime(now);
+            wrkMast.setModiTime(now);
+            if (wrkMastService.updateById(wrkMast)) {
+                wrkAnalysisService.markCraneStart(wrkMast, now);
+                MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, moveCommand));
+                News.info("鍫嗗灈鏈虹Щ鍔ㄥ懡浠や笅鍙戞垚鍔燂紝鍫嗗灈鏈哄彿={}锛屽伐浣滃彿={}锛岀洰鏍囦綅={}锛屼换鍔℃暟鎹�={}",
+                        crnNo, wrkMast.getWrkNo(), wrkMast.getLocNo(), JSON.toJSON(moveCommand));
+                return blockedCrnNoSet;
+            }
+        }
+        return blockedCrnNoSet;
+    }
+
     //妫�娴嬫祬搴撲綅鐘舵��
     public synchronized boolean checkShallowLocStatus(String locNo, Integer taskNo) {
         String checkDeepLocOutTaskBlockReport = "Y";
@@ -889,4 +1012,83 @@
         return false;
     }
 
+    //璋冨害鍫嗗灈鏈虹Щ鍔�
+    public synchronized boolean dispatchCrnMove(Integer crnNo, String targetLocNo) {
+        if (crnNo == null || Cools.isEmpty(targetLocNo)) {
+            return false;
+        }
+
+        int targetRow = Utils.getRow(targetLocNo);
+        int targetBay = Utils.getBay(targetLocNo);
+        int targetLev = Utils.getLev(targetLocNo);
+        try {
+            targetRow = Utils.getRow(targetLocNo);
+            targetBay = Utils.getBay(targetLocNo);
+            targetLev = Utils.getLev(targetLocNo);
+        } catch (Exception e) {
+            News.error("鐢熸垚鍫嗗灈鏈虹Щ鍔ㄤ换鍔″け璐ワ紝鐩爣浣�:{} 瑙f瀽寮傚父", targetLocNo);
+            return false;
+        }
+
+        CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, crnNo);
+        if (crnThread == null) {
+            return false;
+        }
+
+        CrnProtocol crnProtocol = crnThread.getStatus();
+        if (crnProtocol == null) {
+            return false;
+        }
+
+        if (crnProtocol.getBay() == targetBay) {
+            return false;
+        }
+
+        if (crnProtocol.getLevel() == targetLev) {
+            return false;
+        }
+
+        long runningCount = wrkMastService.count(new QueryWrapper<WrkMast>()
+                .eq("crn_no", crnNo)
+                .in("wrk_sts",
+                        WrkStsType.INBOUND_RUN.sts,
+                        WrkStsType.OUTBOUND_RUN.sts,
+                        WrkStsType.LOC_MOVE_RUN.sts,
+                        WrkStsType.CRN_MOVE_RUN.sts));
+        if (runningCount > 0) {
+            News.info("鍫嗗灈鏈�:{} 瀛樺湪鎵ц涓殑浠诲姟锛屾殏涓嶇敓鎴愮Щ鍔ㄤ换鍔�", crnNo);
+            return false;
+        }
+
+        WrkMast activeTask = wrkMastService.getOne(new QueryWrapper<WrkMast>()
+                .eq("crn_no", crnNo)
+                .eq("io_type", WrkIoType.CRN_MOVE.id)
+                .in("wrk_sts", WrkStsType.NEW_CRN_MOVE.sts, WrkStsType.CRN_MOVE_RUN.sts, WrkStsType.CRN_MOVE_MANUAL.sts)
+                .orderByDesc("appe_time")
+                .last("limit 1"));
+        if (activeTask != null) {
+            News.info("鍫嗗灈鏈�:{} 宸插瓨鍦ㄦ湭瀹屾垚绉诲姩浠诲姟锛屽伐浣滃彿={}", crnNo, activeTask.getWrkNo());
+            return false;
+        }
+
+        Date now = new Date();
+        WrkMast wrkMast = new WrkMast();
+        wrkMast.setWrkNo(commonService.getWorkNo(WrkIoType.CRN_MOVE.id));
+        wrkMast.setIoTime(now);
+        wrkMast.setWrkSts(WrkStsType.NEW_CRN_MOVE.sts);
+        wrkMast.setIoType(WrkIoType.CRN_MOVE.id);
+        wrkMast.setIoPri(0D);
+        wrkMast.setLocNo(targetLocNo);
+        wrkMast.setCrnNo(crnNo);
+        wrkMast.setAppeTime(now);
+        wrkMast.setModiTime(now);
+        wrkMast.setMemo("dispatchCrnMove");
+        if (!wrkMastService.save(wrkMast)) {
+            News.info("鐢熸垚鍫嗗灈鏈虹Щ鍔ㄤ换鍔″け璐ワ紝宸ヤ綔鍙�={}锛屼换鍔℃暟鎹�={}", wrkMast.getWrkNo(), JSON.toJSON(wrkMast));
+            return false;
+        }
+        wrkAnalysisService.initForTask(wrkMast);
+        return true;
+    }
+
 }
diff --git a/src/main/resources/sql/20260330_add_crn_move_task_type.sql b/src/main/resources/sql/20260330_add_crn_move_task_type.sql
new file mode 100644
index 0000000..b791af6
--- /dev/null
+++ b/src/main/resources/sql/20260330_add_crn_move_task_type.sql
@@ -0,0 +1,27 @@
+INSERT INTO `asr_bas_wrk_iotype` (`io_type`, `io_pri`, `io_desc`, `modi_user`, `modi_time`, `appe_user`, `appe_time`)
+SELECT 301, NULL, '301.鍫嗗灈鏈虹Щ鍔ㄤ换鍔�', NULL, NOW(), NULL, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `asr_bas_wrk_iotype` WHERE `io_type` = 301);
+
+INSERT INTO `asr_wrk_lastno` (`wrk_mk`, `wrk_no`, `modi_user`, `modi_time`, `appe_user`, `appe_time`, `s_no`, `e_no`, `memo_m`)
+SELECT 301, 30000, NULL, NOW(), NULL, NOW(), 30001, 39999, '鍫嗗灈鏈虹Щ鍔ㄤ换鍔″彿娈�'
+WHERE NOT EXISTS (SELECT 1 FROM `asr_wrk_lastno` WHERE `wrk_mk` = 301);
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `modi_user`, `modi_time`, `appe_user`, `appe_time`)
+SELECT 601, '601.鐢熸垚鍫嗗灈鏈虹Щ鍔ㄤ换鍔�', NULL, NOW(), NULL, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `asr_bas_wrk_status` WHERE `wrk_sts` = 601);
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `modi_user`, `modi_time`, `appe_user`, `appe_time`)
+SELECT 602, '602.鍫嗗灈鏈虹Щ鍔ㄤ腑', NULL, NOW(), NULL, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `asr_bas_wrk_status` WHERE `wrk_sts` = 602);
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `modi_user`, `modi_time`, `appe_user`, `appe_time`)
+SELECT 603, '603.鍫嗗灈鏈虹Щ鍔ㄥ畬鎴�', NULL, NOW(), NULL, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `asr_bas_wrk_status` WHERE `wrk_sts` = 603);
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `modi_user`, `modi_time`, `appe_user`, `appe_time`)
+SELECT 606, '606.鍫嗗灈鏈虹Щ鍔ㄥ緟浜哄伐鍥炴粴', NULL, NOW(), NULL, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `asr_bas_wrk_status` WHERE `wrk_sts` = 606);
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `modi_user`, `modi_time`, `appe_user`, `appe_time`)
+SELECT 609, '609.鍫嗗灈鏈虹Щ鍔ㄤ换鍔″畬鎴�', NULL, NOW(), NULL, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `asr_bas_wrk_status` WHERE `wrk_sts` = 609);
diff --git a/src/test/java/com/zy/asrs/task/WrkAnalysisStationArrivalScannerTest.java b/src/test/java/com/zy/asrs/task/WrkAnalysisStationArrivalScannerTest.java
index 076d862..0415228 100644
--- a/src/test/java/com/zy/asrs/task/WrkAnalysisStationArrivalScannerTest.java
+++ b/src/test/java/com/zy/asrs/task/WrkAnalysisStationArrivalScannerTest.java
@@ -2,21 +2,29 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.zy.asrs.entity.BasStation;
+import com.zy.asrs.entity.BasCrnp;
 import com.zy.asrs.entity.WrkMast;
 import com.zy.asrs.service.BasStationService;
+import com.zy.asrs.service.BasCrnpService;
 import com.zy.asrs.service.WrkAnalysisService;
 import com.zy.asrs.service.WrkMastService;
+import com.zy.common.entity.FindCrnNoResult;
+import com.zy.common.service.CommonService;
 import com.zy.core.cache.SlaveConnection;
 import com.zy.core.enums.SlaveType;
 import com.zy.core.enums.StationCommandType;
 import com.zy.core.enums.WrkStsType;
+import com.zy.core.move.StationMoveCoordinator;
+import com.zy.core.move.StationMoveSession;
 import com.zy.core.model.CommandResponse;
 import com.zy.core.model.command.StationCommand;
 import com.zy.core.model.protocol.StationProtocol;
 import com.zy.core.thread.StationThread;
+import com.zy.core.utils.CrnOperateProcessUtils;
 import com.zy.core.utils.StationOperateProcessUtils;
 import org.junit.jupiter.api.Test;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -25,6 +33,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -36,12 +45,20 @@
         BasStationService basStationService = mock(BasStationService.class);
         WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
         StationOperateProcessUtils stationOperateProcessUtils = mock(StationOperateProcessUtils.class);
+        StationMoveCoordinator stationMoveCoordinator = mock(StationMoveCoordinator.class);
+        CommonService commonService = mock(CommonService.class);
+        BasCrnpService basCrnpService = mock(BasCrnpService.class);
+        CrnOperateProcessUtils crnOperateProcessUtils = mock(CrnOperateProcessUtils.class);
 
         WrkAnalysisStationArrivalScanner scanner = new WrkAnalysisStationArrivalScanner(
                 wrkMastService,
                 basStationService,
                 wrkAnalysisService,
-                stationOperateProcessUtils
+                stationOperateProcessUtils,
+                stationMoveCoordinator,
+                commonService,
+                basCrnpService,
+                crnOperateProcessUtils
         );
 
         WrkMast wrkMast = new WrkMast();
@@ -75,6 +92,211 @@
         verify(wrkAnalysisService).completeInboundStationRun(any(WrkMast.class), any(Date.class));
     }
 
+    @Test
+    void scanInboundStationArrival_dispatchesCrnMoveWhenOnlyFinalInletStationRemains() {
+        WrkMastService wrkMastService = mock(WrkMastService.class);
+        BasStationService basStationService = mock(BasStationService.class);
+        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
+        StationOperateProcessUtils stationOperateProcessUtils = mock(StationOperateProcessUtils.class);
+        StationMoveCoordinator stationMoveCoordinator = mock(StationMoveCoordinator.class);
+        CommonService commonService = mock(CommonService.class);
+        BasCrnpService basCrnpService = mock(BasCrnpService.class);
+        CrnOperateProcessUtils crnOperateProcessUtils = mock(CrnOperateProcessUtils.class);
+
+        WrkAnalysisStationArrivalScanner scanner = new WrkAnalysisStationArrivalScanner(
+                wrkMastService,
+                basStationService,
+                wrkAnalysisService,
+                stationOperateProcessUtils,
+                stationMoveCoordinator,
+                commonService,
+                basCrnpService,
+                crnOperateProcessUtils
+        );
+
+        WrkMast wrkMast = new WrkMast();
+        wrkMast.setWrkNo(1002);
+        wrkMast.setIoType(1);
+        wrkMast.setWrkSts(WrkStsType.INBOUND_STATION_RUN.sts);
+        wrkMast.setStaNo(12);
+        wrkMast.setLocNo("5-6-7");
+
+        BasStation basStation = new BasStation();
+        basStation.setStationId(12);
+        basStation.setDeviceNo(3);
+
+        StationMoveSession session = new StationMoveSession();
+        session.setStatus(StationMoveSession.STATUS_RUNNING);
+        session.setCurrentStationId(11);
+        session.setFullPathStationIds(new ArrayList<>(List.of(10, 11, 12)));
+
+        FindCrnNoResult findCrnNoResult = new FindCrnNoResult();
+        findCrnNoResult.setCrnNo(1);
+        findCrnNoResult.setCrnType(SlaveType.Crn);
+
+        BasCrnp basCrnp = new BasCrnp();
+        basCrnp.setCrnNo(1);
+        basCrnp.setInStationList("[{\"stationId\":12,\"deviceRow\":2,\"deviceBay\":1,\"deviceLev\":1}]");
+
+        when(wrkMastService.list(any(QueryWrapper.class))).thenReturn(List.of(wrkMast));
+        when(basStationService.getOne(any())).thenReturn(basStation);
+        when(stationMoveCoordinator.loadSession(1002)).thenReturn(session);
+        when(commonService.findCrnNoByLocNo("5-6-7")).thenReturn(findCrnNoResult);
+        when(basCrnpService.getOne(any())).thenReturn(basCrnp);
+        when(crnOperateProcessUtils.dispatchCrnMove(1, "2-1-1")).thenReturn(true);
+
+        ArrivalAwareStationThread stationThread = new ArrivalAwareStationThread(false);
+        StationProtocol stationProtocol = new StationProtocol();
+        stationProtocol.setStationId(12);
+        stationProtocol.setTaskNo(0);
+        stationProtocol.setLoading(false);
+        stationThread.putStatus(stationProtocol);
+
+        SlaveConnection.put(SlaveType.Devp, 3, stationThread);
+        try {
+            scanner.scanInboundStationArrival();
+        } finally {
+            SlaveConnection.remove(SlaveType.Devp, 3);
+        }
+
+        verify(crnOperateProcessUtils).dispatchCrnMove(1, "2-1-1");
+        verify(wrkAnalysisService, never()).completeInboundStationRun(any(WrkMast.class), any(Date.class));
+    }
+
+    @Test
+    void scanInboundStationArrival_skipsCrnMoveWhenTargetInletHasLoadTaskAndInEnable() {
+        WrkMastService wrkMastService = mock(WrkMastService.class);
+        BasStationService basStationService = mock(BasStationService.class);
+        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
+        StationOperateProcessUtils stationOperateProcessUtils = mock(StationOperateProcessUtils.class);
+        StationMoveCoordinator stationMoveCoordinator = mock(StationMoveCoordinator.class);
+        CommonService commonService = mock(CommonService.class);
+        BasCrnpService basCrnpService = mock(BasCrnpService.class);
+        CrnOperateProcessUtils crnOperateProcessUtils = mock(CrnOperateProcessUtils.class);
+
+        WrkAnalysisStationArrivalScanner scanner = new WrkAnalysisStationArrivalScanner(
+                wrkMastService,
+                basStationService,
+                wrkAnalysisService,
+                stationOperateProcessUtils,
+                stationMoveCoordinator,
+                commonService,
+                basCrnpService,
+                crnOperateProcessUtils
+        );
+
+        WrkMast wrkMast = new WrkMast();
+        wrkMast.setWrkNo(1003);
+        wrkMast.setIoType(1);
+        wrkMast.setWrkSts(WrkStsType.INBOUND_STATION_RUN.sts);
+        wrkMast.setStaNo(12);
+        wrkMast.setLocNo("5-6-7");
+
+        BasStation basStation = new BasStation();
+        basStation.setStationId(12);
+        basStation.setDeviceNo(3);
+
+        StationMoveSession session = new StationMoveSession();
+        session.setStatus(StationMoveSession.STATUS_RUNNING);
+        session.setCurrentStationId(11);
+        session.setFullPathStationIds(new ArrayList<>(List.of(10, 11, 12)));
+
+        when(wrkMastService.list(any(QueryWrapper.class))).thenReturn(List.of(wrkMast));
+        when(basStationService.getOne(any())).thenReturn(basStation);
+        when(stationMoveCoordinator.loadSession(1003)).thenReturn(session);
+
+        ArrivalAwareStationThread stationThread = new ArrivalAwareStationThread(false);
+        StationProtocol stationProtocol = new StationProtocol();
+        stationProtocol.setStationId(12);
+        stationProtocol.setTaskNo(9999);
+        stationProtocol.setLoading(true);
+        stationProtocol.setInEnable(true);
+        stationThread.putStatus(stationProtocol);
+
+        SlaveConnection.put(SlaveType.Devp, 3, stationThread);
+        try {
+            scanner.scanInboundStationArrival();
+        } finally {
+            SlaveConnection.remove(SlaveType.Devp, 3);
+        }
+
+        verify(crnOperateProcessUtils, never()).dispatchCrnMove(any(), any());
+        verify(commonService, never()).findCrnNoByLocNo(any());
+        verify(wrkAnalysisService, never()).completeInboundStationRun(any(WrkMast.class), any(Date.class));
+    }
+
+    @Test
+    void scanInboundStationArrival_dispatchesCrnMoveWhenTargetInletOnlyHasLoad() {
+        WrkMastService wrkMastService = mock(WrkMastService.class);
+        BasStationService basStationService = mock(BasStationService.class);
+        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
+        StationOperateProcessUtils stationOperateProcessUtils = mock(StationOperateProcessUtils.class);
+        StationMoveCoordinator stationMoveCoordinator = mock(StationMoveCoordinator.class);
+        CommonService commonService = mock(CommonService.class);
+        BasCrnpService basCrnpService = mock(BasCrnpService.class);
+        CrnOperateProcessUtils crnOperateProcessUtils = mock(CrnOperateProcessUtils.class);
+
+        WrkAnalysisStationArrivalScanner scanner = new WrkAnalysisStationArrivalScanner(
+                wrkMastService,
+                basStationService,
+                wrkAnalysisService,
+                stationOperateProcessUtils,
+                stationMoveCoordinator,
+                commonService,
+                basCrnpService,
+                crnOperateProcessUtils
+        );
+
+        WrkMast wrkMast = new WrkMast();
+        wrkMast.setWrkNo(1004);
+        wrkMast.setIoType(1);
+        wrkMast.setWrkSts(WrkStsType.INBOUND_STATION_RUN.sts);
+        wrkMast.setStaNo(12);
+        wrkMast.setLocNo("5-6-7");
+
+        BasStation basStation = new BasStation();
+        basStation.setStationId(12);
+        basStation.setDeviceNo(3);
+
+        StationMoveSession session = new StationMoveSession();
+        session.setStatus(StationMoveSession.STATUS_RUNNING);
+        session.setCurrentStationId(11);
+        session.setFullPathStationIds(new ArrayList<>(List.of(10, 11, 12)));
+
+        FindCrnNoResult findCrnNoResult = new FindCrnNoResult();
+        findCrnNoResult.setCrnNo(1);
+        findCrnNoResult.setCrnType(SlaveType.Crn);
+
+        BasCrnp basCrnp = new BasCrnp();
+        basCrnp.setCrnNo(1);
+        basCrnp.setInStationList("[{\"stationId\":12,\"deviceRow\":2,\"deviceBay\":1,\"deviceLev\":1}]");
+
+        when(wrkMastService.list(any(QueryWrapper.class))).thenReturn(List.of(wrkMast));
+        when(basStationService.getOne(any())).thenReturn(basStation);
+        when(stationMoveCoordinator.loadSession(1004)).thenReturn(session);
+        when(commonService.findCrnNoByLocNo("5-6-7")).thenReturn(findCrnNoResult);
+        when(basCrnpService.getOne(any())).thenReturn(basCrnp);
+        when(crnOperateProcessUtils.dispatchCrnMove(1, "2-1-1")).thenReturn(true);
+
+        ArrivalAwareStationThread stationThread = new ArrivalAwareStationThread(false);
+        StationProtocol stationProtocol = new StationProtocol();
+        stationProtocol.setStationId(12);
+        stationProtocol.setTaskNo(0);
+        stationProtocol.setLoading(true);
+        stationProtocol.setInEnable(false);
+        stationThread.putStatus(stationProtocol);
+
+        SlaveConnection.put(SlaveType.Devp, 3, stationThread);
+        try {
+            scanner.scanInboundStationArrival();
+        } finally {
+            SlaveConnection.remove(SlaveType.Devp, 3);
+        }
+
+        verify(crnOperateProcessUtils).dispatchCrnMove(1, "2-1-1");
+        verify(wrkAnalysisService, never()).completeInboundStationRun(any(WrkMast.class), any(Date.class));
+    }
+
     private static class ArrivalAwareStationThread implements StationThread {
 
         private final boolean recentArrival;
diff --git a/src/test/java/com/zy/asrs/task/WrkMastSchedulerCrnMoveTest.java b/src/test/java/com/zy/asrs/task/WrkMastSchedulerCrnMoveTest.java
new file mode 100644
index 0000000..d08297b
--- /dev/null
+++ b/src/test/java/com/zy/asrs/task/WrkMastSchedulerCrnMoveTest.java
@@ -0,0 +1,54 @@
+package com.zy.asrs.task;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.zy.asrs.entity.WrkMast;
+import com.zy.asrs.service.LocMastService;
+import com.zy.asrs.service.WrkAnalysisService;
+import com.zy.asrs.service.WrkMastLogService;
+import com.zy.asrs.service.WrkMastService;
+import com.zy.asrs.utils.NotifyUtils;
+import com.zy.core.enums.WrkIoType;
+import com.zy.core.enums.WrkStsType;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class WrkMastSchedulerCrnMoveTest {
+
+    @Test
+    void executeCrnMove_archivesAndRemovesCompletedCrnMoveTask() {
+        WrkMastService wrkMastService = mock(WrkMastService.class);
+        WrkMastLogService wrkMastLogService = mock(WrkMastLogService.class);
+        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
+        LocMastService locMastService = mock(LocMastService.class);
+        NotifyUtils notifyUtils = mock(NotifyUtils.class);
+
+        WrkMastScheduler scheduler = new WrkMastScheduler(
+                wrkMastService,
+                wrkMastLogService,
+                wrkAnalysisService,
+                locMastService,
+                notifyUtils
+        );
+
+        WrkMast wrkMast = new WrkMast();
+        wrkMast.setWrkNo(30001);
+        wrkMast.setIoType(WrkIoType.CRN_MOVE.id);
+        wrkMast.setWrkSts(WrkStsType.COMPLETE_CRN_MOVE.sts);
+
+        when(wrkMastService.list(any(QueryWrapper.class))).thenReturn(List.of(wrkMast));
+        when(wrkMastLogService.save(30001)).thenReturn(true);
+        when(wrkMastService.removeById(30001)).thenReturn(true);
+
+        scheduler.executeCrnMove();
+
+        verify(wrkMastLogService).save(30001);
+        verify(wrkAnalysisService).finishTask(any(WrkMast.class), any());
+        verify(wrkMastService).removeById(30001);
+    }
+}
diff --git a/src/test/java/com/zy/core/utils/CrnOperateProcessUtilsTest.java b/src/test/java/com/zy/core/utils/CrnOperateProcessUtilsTest.java
new file mode 100644
index 0000000..3fab458
--- /dev/null
+++ b/src/test/java/com/zy/core/utils/CrnOperateProcessUtilsTest.java
@@ -0,0 +1,397 @@
+package com.zy.core.utils;
+
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.zy.asrs.entity.BasCrnp;
+import com.zy.asrs.entity.LocMast;
+import com.zy.asrs.entity.WrkMast;
+import com.zy.asrs.service.BasCrnpService;
+import com.zy.asrs.service.LocMastService;
+import com.zy.asrs.service.WrkAnalysisService;
+import com.zy.asrs.service.WrkMastService;
+import com.zy.asrs.utils.NotifyUtils;
+import com.zy.common.service.CommonService;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.cache.MessageQueue;
+import com.zy.core.cache.SlaveConnection;
+import com.zy.core.enums.CrnModeType;
+import com.zy.core.enums.CrnStatusType;
+import com.zy.core.enums.SlaveType;
+import com.zy.core.enums.WrkIoType;
+import com.zy.core.enums.WrkStsType;
+import com.zy.core.model.Task;
+import com.zy.core.model.command.CrnCommand;
+import com.zy.core.model.protocol.CrnProtocol;
+import com.zy.core.thread.CrnThread;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class CrnOperateProcessUtilsTest {
+
+    @AfterEach
+    void tearDown() {
+        safeClearQueue(1);
+        safeClearQueue(2);
+        SlaveConnection.remove(SlaveType.Crn, 1);
+        SlaveConnection.remove(SlaveType.Crn, 2);
+        SlaveConnection.remove(SlaveType.Devp, 101);
+    }
+
+    @Test
+    void dispatchCrnMove_createsFormalTaskOnly() {
+        CrnOperateProcessUtils utils = new CrnOperateProcessUtils();
+        WrkMastService wrkMastService = mock(WrkMastService.class);
+        CommonService commonService = mock(CommonService.class);
+        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
+        CrnThread crnThread = mock(CrnThread.class);
+
+        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
+        ReflectionTestUtils.setField(utils, "commonService", commonService);
+        ReflectionTestUtils.setField(utils, "wrkAnalysisService", wrkAnalysisService);
+
+        when(wrkMastService.count(any())).thenReturn(0L);
+        when(wrkMastService.getOne(any())).thenReturn(null);
+        when(commonService.getWorkNo(WrkIoType.CRN_MOVE.id)).thenReturn(30001);
+        AtomicReference<WrkMast> savedSnapshot = new AtomicReference<>();
+        when(wrkMastService.save(any(WrkMast.class))).thenAnswer(invocation -> {
+            WrkMast source = invocation.getArgument(0);
+            WrkMast snapshot = new WrkMast();
+            snapshot.setWrkNo(source.getWrkNo());
+            snapshot.setIoType(source.getIoType());
+            snapshot.setWrkSts(source.getWrkSts());
+            snapshot.setLocNo(source.getLocNo());
+            snapshot.setCrnNo(source.getCrnNo());
+            savedSnapshot.set(snapshot);
+            return true;
+        });
+
+        MessageQueue.init(SlaveType.Crn, 1);
+        CrnProtocol protocol = buildProtocol(1, CrnStatusType.IDLE.id, 0);
+        protocol.setBay(8);
+        protocol.setLevel(3);
+        when(crnThread.getStatus()).thenReturn(protocol);
+        SlaveConnection.put(SlaveType.Crn, 1, crnThread);
+
+        boolean dispatched = utils.dispatchCrnMove(1, "2-1-1");
+
+        assertTrue(dispatched);
+
+        verify(wrkMastService).save(any(WrkMast.class));
+        WrkMast saved = savedSnapshot.get();
+        assertNotNull(saved);
+        assertEquals(30001, saved.getWrkNo());
+        assertEquals(WrkIoType.CRN_MOVE.id, saved.getIoType());
+        assertEquals(WrkStsType.NEW_CRN_MOVE.sts, saved.getWrkSts());
+        assertEquals("2-1-1", saved.getLocNo());
+        assertEquals(1, saved.getCrnNo());
+
+        verify(wrkMastService, never()).updateById(any(WrkMast.class));
+        verify(wrkAnalysisService, never()).markCraneStart(any(WrkMast.class), any());
+        assertNull(MessageQueue.peek(SlaveType.Crn, 1));
+    }
+
+    @Test
+    void dispatchCrnMove_returnsFalseWhenOtherTaskRunning() {
+        CrnOperateProcessUtils utils = new CrnOperateProcessUtils();
+        WrkMastService wrkMastService = mock(WrkMastService.class);
+        CommonService commonService = mock(CommonService.class);
+        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
+
+        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
+        ReflectionTestUtils.setField(utils, "commonService", commonService);
+        ReflectionTestUtils.setField(utils, "wrkAnalysisService", wrkAnalysisService);
+
+        when(wrkMastService.count(any())).thenReturn(1L);
+        when(wrkMastService.getOne(any())).thenReturn(null);
+        when(wrkMastService.save(any(WrkMast.class))).thenReturn(true);
+        when(commonService.getWorkNo(WrkIoType.CRN_MOVE.id)).thenReturn(30001);
+
+        boolean dispatched = utils.dispatchCrnMove(1, "2-1-1");
+
+        assertFalse(dispatched);
+        verify(wrkMastService, never()).save(any(WrkMast.class));
+        verify(commonService, never()).getWorkNo(anyInt());
+        verify(wrkAnalysisService, never()).initForTask(any(WrkMast.class));
+    }
+
+    @Test
+    void crnIoExecuteNormal_prioritizesCrnMoveTask() {
+        CrnOperateProcessUtils utils = new CrnOperateProcessUtils();
+        BasCrnpService basCrnpService = mock(BasCrnpService.class);
+        WrkMastService wrkMastService = mock(WrkMastService.class);
+        RedisUtil redisUtil = mock(RedisUtil.class);
+        LocMastService locMastService = mock(LocMastService.class);
+        NotifyUtils notifyUtils = mock(NotifyUtils.class);
+        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
+        CrnThread crnThread = mock(CrnThread.class);
+
+        ReflectionTestUtils.setField(utils, "basCrnpService", basCrnpService);
+        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
+        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
+        ReflectionTestUtils.setField(utils, "locMastService", locMastService);
+        ReflectionTestUtils.setField(utils, "notifyUtils", notifyUtils);
+        ReflectionTestUtils.setField(utils, "wrkAnalysisService", wrkAnalysisService);
+
+        when(basCrnpService.list(anyWrapper())).thenReturn(List.of(buildBasCrnp(1, "[[2,3]]", "[2]")));
+        when(redisUtil.get(anyString())).thenReturn(null);
+        when(wrkMastService.count(any())).thenReturn(0L);
+        when(wrkMastService.updateById(any(WrkMast.class))).thenReturn(true);
+
+        WrkMast crnMoveTask = new WrkMast();
+        crnMoveTask.setWrkNo(30001);
+        crnMoveTask.setCrnNo(1);
+        crnMoveTask.setIoType(WrkIoType.CRN_MOVE.id);
+        crnMoveTask.setWrkSts(WrkStsType.NEW_CRN_MOVE.sts);
+        crnMoveTask.setLocNo("2-1-1");
+        crnMoveTask.setIoPri(999D);
+
+        WrkMast locMoveTask = new WrkMast();
+        locMoveTask.setWrkNo(20001);
+        locMoveTask.setCrnNo(1);
+        locMoveTask.setIoType(WrkIoType.LOC_MOVE.id);
+        locMoveTask.setWrkSts(WrkStsType.NEW_LOC_MOVE.sts);
+        locMoveTask.setSourceLocNo("3-1-1");
+        locMoveTask.setLocNo("4-1-1");
+        locMoveTask.setIoPri(1D);
+
+        when(wrkMastService.list(anyWrapper()))
+                .thenReturn(new ArrayList<>(List.of(crnMoveTask)))
+                .thenReturn(new ArrayList<>(List.of(locMoveTask, crnMoveTask)));
+
+        LocMast sourceLoc = new LocMast();
+        sourceLoc.setLocNo("3-1-1");
+        sourceLoc.setLocSts("R");
+        LocMast targetLoc = new LocMast();
+        targetLoc.setLocNo("4-1-1");
+        targetLoc.setLocSts("S");
+        when(locMastService.getById("3-1-1")).thenReturn(sourceLoc);
+        when(locMastService.getById("4-1-1")).thenReturn(targetLoc);
+
+        CrnProtocol protocol = buildProtocol(1, CrnStatusType.IDLE.id, 0);
+        protocol.setBay(1);
+        protocol.setLevel(1);
+        when(crnThread.getStatus()).thenReturn(protocol);
+
+        CrnCommand moveCommand = new CrnCommand();
+        moveCommand.setCrnNo(1);
+        moveCommand.setTaskNo(30001);
+        when(crnThread.getMoveCommand("2-1-1", 30001, 1)).thenReturn(moveCommand);
+
+        CrnCommand locMoveCommand = new CrnCommand();
+        locMoveCommand.setCrnNo(1);
+        locMoveCommand.setTaskNo(20001);
+        when(crnThread.getPickAndPutCommand("3-1-1", "4-1-1", 20001, 1)).thenReturn(locMoveCommand);
+
+        MessageQueue.init(SlaveType.Crn, 1);
+        SlaveConnection.put(SlaveType.Crn, 1, crnThread);
+
+        utils.crnIoExecuteNormal();
+
+        ArgumentCaptor<WrkMast> updateCaptor = ArgumentCaptor.forClass(WrkMast.class);
+        verify(wrkMastService).updateById(updateCaptor.capture());
+        assertEquals(30001, updateCaptor.getValue().getWrkNo());
+        assertEquals(WrkStsType.CRN_MOVE_RUN.sts, updateCaptor.getValue().getWrkSts());
+        verify(wrkAnalysisService).markCraneStart(any(WrkMast.class), any());
+
+        Task task = MessageQueue.peek(SlaveType.Crn, 1);
+        assertNotNull(task);
+        assertSame(moveCommand, task.getData());
+    }
+
+    @Test
+    void crnIoExecuteNormal_allowsOtherCraneToRunWhenOneCraneHasMoveTask() {
+        CrnOperateProcessUtils utils = new CrnOperateProcessUtils();
+        BasCrnpService basCrnpService = mock(BasCrnpService.class);
+        WrkMastService wrkMastService = mock(WrkMastService.class);
+        RedisUtil redisUtil = mock(RedisUtil.class);
+        LocMastService locMastService = mock(LocMastService.class);
+        NotifyUtils notifyUtils = mock(NotifyUtils.class);
+        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
+        CrnThread crnThread1 = mock(CrnThread.class);
+        CrnThread crnThread2 = mock(CrnThread.class);
+
+        ReflectionTestUtils.setField(utils, "basCrnpService", basCrnpService);
+        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
+        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
+        ReflectionTestUtils.setField(utils, "locMastService", locMastService);
+        ReflectionTestUtils.setField(utils, "notifyUtils", notifyUtils);
+        ReflectionTestUtils.setField(utils, "wrkAnalysisService", wrkAnalysisService);
+
+        when(basCrnpService.list(anyWrapper())).thenReturn(List.of(
+                buildBasCrnp(1, "[[2,3]]", "[2]"),
+                buildBasCrnp(2, "[[4,5]]", "[4]")
+        ));
+        when(redisUtil.get(anyString())).thenReturn(null);
+        when(wrkMastService.count(any())).thenReturn(0L);
+        when(wrkMastService.updateById(any(WrkMast.class))).thenReturn(true);
+
+        WrkMast crnMoveTask = new WrkMast();
+        crnMoveTask.setWrkNo(30001);
+        crnMoveTask.setCrnNo(1);
+        crnMoveTask.setIoType(WrkIoType.CRN_MOVE.id);
+        crnMoveTask.setWrkSts(WrkStsType.NEW_CRN_MOVE.sts);
+        crnMoveTask.setLocNo("2-1-1");
+
+        WrkMast locMoveTask = new WrkMast();
+        locMoveTask.setWrkNo(20002);
+        locMoveTask.setCrnNo(2);
+        locMoveTask.setIoType(WrkIoType.LOC_MOVE.id);
+        locMoveTask.setWrkSts(WrkStsType.NEW_LOC_MOVE.sts);
+        locMoveTask.setSourceLocNo("4-1-1");
+        locMoveTask.setLocNo("5-1-1");
+        locMoveTask.setIoPri(1D);
+
+        when(wrkMastService.list(anyWrapper()))
+                .thenReturn(new ArrayList<>(List.of(crnMoveTask)))
+                .thenReturn(new ArrayList<>(List.of(crnMoveTask)))
+                .thenReturn(new ArrayList<>(List.of(locMoveTask)));
+
+        LocMast sourceLoc = new LocMast();
+        sourceLoc.setLocNo("4-1-1");
+        sourceLoc.setLocSts("R");
+        LocMast targetLoc = new LocMast();
+        targetLoc.setLocNo("5-1-1");
+        targetLoc.setLocSts("S");
+        when(locMastService.getById("4-1-1")).thenReturn(sourceLoc);
+        when(locMastService.getById("5-1-1")).thenReturn(targetLoc);
+
+        CrnProtocol protocol1 = buildProtocol(1, CrnStatusType.IDLE.id, 0);
+        when(crnThread1.getStatus()).thenReturn(protocol1);
+        CrnProtocol protocol2 = buildProtocol(2, CrnStatusType.IDLE.id, 0);
+        when(crnThread2.getStatus()).thenReturn(protocol2);
+
+        CrnCommand moveCommand = new CrnCommand();
+        moveCommand.setCrnNo(1);
+        moveCommand.setTaskNo(30001);
+        when(crnThread1.getMoveCommand("2-1-1", 30001, 1)).thenReturn(moveCommand);
+
+        CrnCommand locMoveCommand = new CrnCommand();
+        locMoveCommand.setCrnNo(2);
+        locMoveCommand.setTaskNo(20002);
+        when(crnThread2.getPickAndPutCommand("4-1-1", "5-1-1", 20002, 2)).thenReturn(locMoveCommand);
+
+        MessageQueue.init(SlaveType.Crn, 1);
+        MessageQueue.init(SlaveType.Crn, 2);
+        SlaveConnection.put(SlaveType.Crn, 1, crnThread1);
+        SlaveConnection.put(SlaveType.Crn, 2, crnThread2);
+
+        utils.crnIoExecuteNormal();
+
+        ArgumentCaptor<WrkMast> updateCaptor = ArgumentCaptor.forClass(WrkMast.class);
+        verify(wrkMastService, times(2)).updateById(updateCaptor.capture());
+        List<WrkMast> updatedList = updateCaptor.getAllValues();
+        assertEquals(30001, updatedList.get(0).getWrkNo());
+        assertEquals(WrkStsType.CRN_MOVE_RUN.sts, updatedList.get(0).getWrkSts());
+        assertEquals(20002, updatedList.get(1).getWrkNo());
+        assertEquals(WrkStsType.LOC_MOVE_RUN.sts, updatedList.get(1).getWrkSts());
+
+        Task task1 = MessageQueue.peek(SlaveType.Crn, 1);
+        assertNotNull(task1);
+        assertSame(moveCommand, task1.getData());
+
+        Task task2 = MessageQueue.peek(SlaveType.Crn, 2);
+        assertNotNull(task2);
+        assertSame(locMoveCommand, task2.getData());
+    }
+
+    @Test
+    void crnIoExecuteFinish_marksCrnMoveTaskComplete() {
+        CrnOperateProcessUtils utils = new CrnOperateProcessUtils();
+        BasCrnpService basCrnpService = mock(BasCrnpService.class);
+        WrkMastService wrkMastService = mock(WrkMastService.class);
+        RedisUtil redisUtil = mock(RedisUtil.class);
+        WrkAnalysisService wrkAnalysisService = mock(WrkAnalysisService.class);
+        CrnThread crnThread = mock(CrnThread.class);
+
+        ReflectionTestUtils.setField(utils, "basCrnpService", basCrnpService);
+        ReflectionTestUtils.setField(utils, "wrkMastService", wrkMastService);
+        ReflectionTestUtils.setField(utils, "redisUtil", redisUtil);
+        ReflectionTestUtils.setField(utils, "wrkAnalysisService", wrkAnalysisService);
+
+        when(basCrnpService.list(anyWrapper())).thenReturn(List.of(buildBasCrnp(1, "[[2,3]]", "[2]")));
+        when(redisUtil.get(anyString())).thenReturn(null);
+        when(wrkMastService.updateById(any(WrkMast.class))).thenReturn(true);
+
+        CrnProtocol protocol = buildProtocol(1, CrnStatusType.WAITING.id, 30001);
+        when(crnThread.getStatus()).thenReturn(protocol);
+
+        WrkMast wrkMast = new WrkMast();
+        wrkMast.setWrkNo(30001);
+        wrkMast.setIoType(WrkIoType.CRN_MOVE.id);
+        wrkMast.setWrkSts(WrkStsType.CRN_MOVE_RUN.sts);
+        wrkMast.setCrnNo(1);
+        when(wrkMastService.selectByWorkNo(30001)).thenReturn(wrkMast);
+
+        CrnCommand resetCommand = new CrnCommand();
+        resetCommand.setCrnNo(1);
+        resetCommand.setTaskNo(30001);
+        when(crnThread.getResetCommand(30001, 1)).thenReturn(resetCommand);
+
+        MessageQueue.init(SlaveType.Crn, 1);
+        SlaveConnection.put(SlaveType.Crn, 1, crnThread);
+
+        utils.crnIoExecuteFinish();
+
+        ArgumentCaptor<WrkMast> updateCaptor = ArgumentCaptor.forClass(WrkMast.class);
+        verify(wrkMastService, times(1)).updateById(updateCaptor.capture());
+        assertEquals(WrkStsType.COMPLETE_CRN_MOVE.sts, updateCaptor.getValue().getWrkSts());
+        verify(wrkAnalysisService).markCraneComplete(any(WrkMast.class), any(), eq(WrkStsType.COMPLETE_CRN_MOVE.sts));
+
+        Task task = MessageQueue.peek(SlaveType.Crn, 1);
+        assertNotNull(task);
+        assertSame(resetCommand, task.getData());
+    }
+
+    private BasCrnp buildBasCrnp(int crnNo, String controlRows, String deepRows) {
+        BasCrnp basCrnp = new BasCrnp();
+        basCrnp.setCrnNo(crnNo);
+        basCrnp.setControlRows(controlRows);
+        basCrnp.setDeepRows(deepRows);
+        return basCrnp;
+    }
+
+    private CrnProtocol buildProtocol(int crnNo, int status, int taskNo) {
+        CrnProtocol protocol = new CrnProtocol();
+        protocol.setCrnNo(crnNo);
+        protocol.setMode(CrnModeType.AUTO.id);
+        protocol.setTaskNo(taskNo);
+        protocol.setStatus(status);
+        protocol.setLoaded(0);
+        protocol.setForkPos(0);
+        protocol.setAlarm(0);
+        return protocol;
+    }
+
+    private void safeClearQueue(int crnNo) {
+        try {
+            MessageQueue.clear(SlaveType.Crn, crnNo);
+        } catch (Exception ignore) {
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> Wrapper<T> anyWrapper() {
+        return any(Wrapper.class);
+    }
+}

--
Gitblit v1.9.1