From c0df58a6fb50ee32e7336b5901000549700ebc82 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期一, 16 三月 2026 15:02:59 +0800
Subject: [PATCH] #

---
 src/main/java/com/zy/core/thread/impl/ZySiemensCrnV2Thread.java     |   49 +++--
 src/main/resources/mapper/WrkMastLogMapper.xml                      |    1 
 src/main/webapp/views/wrkMast/wrkMast.html                          |    3 
 src/main/java/com/zy/asrs/controller/OpenController.java            |   13 +
 src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java       |   49 +++--
 src/main/java/com/zy/asrs/entity/WrkMast.java                       |   14 +
 src/main/resources/mapper/WrkMastMapper.xml                         |    1 
 src/main/java/com/zy/asrs/entity/WrkMastLog.java                    |   10 +
 src/main/webapp/static/js/wrkMast/wrkMast.js                        |   47 +++++
 src/main/java/com/zy/core/service/WrkCommandRollbackService.java    |  212 ++++++++++++++++++++++++++
 src/main/java/com/zy/core/enums/WrkStsType.java                     |    3 
 src/main/java/com/zy/asrs/domain/param/ManualRollbackTaskParam.java |   12 +
 src/main/java/com/zy/common/service/CommonService.java              |    7 
 src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java   |    6 
 src/main/resources/sql/20260316_add_wrk_send_fail_rollback.sql      |   21 ++
 15 files changed, 403 insertions(+), 45 deletions(-)

diff --git a/src/main/java/com/zy/asrs/controller/OpenController.java b/src/main/java/com/zy/asrs/controller/OpenController.java
index 6f0447b..efeb5ed 100644
--- a/src/main/java/com/zy/asrs/controller/OpenController.java
+++ b/src/main/java/com/zy/asrs/controller/OpenController.java
@@ -152,6 +152,19 @@
         return R.error("浠诲姟鍙栨秷澶辫触");
     }
 
+    @PostMapping("/manualRollbackTask")
+    @OpenApiLog(memo = "浠诲姟浜哄伐鍥炴粴")
+    public R manualRollbackTask(@RequestBody ManualRollbackTaskParam param) {
+        if (param == null) {
+            return R.error("鍙傛暟涓嶈兘涓虹┖");
+        }
+        boolean rollback = commonService.manualRollbackTask(param);
+        if (rollback) {
+            return R.ok();
+        }
+        return R.error("浠诲姟浜哄伐鍥炴粴澶辫触");
+    }
+
     @PostMapping("/cancelTaskBatch")
     @OpenApiLog(memo = "浠诲姟鎵归噺鍙栨秷")
     public R cancelTaskBatch(@RequestBody CancelTaskBatchParam param) {
diff --git a/src/main/java/com/zy/asrs/domain/param/ManualRollbackTaskParam.java b/src/main/java/com/zy/asrs/domain/param/ManualRollbackTaskParam.java
new file mode 100644
index 0000000..5b3990d
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/param/ManualRollbackTaskParam.java
@@ -0,0 +1,12 @@
+package com.zy.asrs.domain.param;
+
+import lombok.Data;
+
+@Data
+public class ManualRollbackTaskParam {
+
+    private Integer wrkNo;
+
+    // wms浠诲姟鍙�
+    private String taskNo;
+}
diff --git a/src/main/java/com/zy/asrs/entity/WrkMast.java b/src/main/java/com/zy/asrs/entity/WrkMast.java
index bcf4d6c..bf1c20c 100644
--- a/src/main/java/com/zy/asrs/entity/WrkMast.java
+++ b/src/main/java/com/zy/asrs/entity/WrkMast.java
@@ -175,6 +175,13 @@
     private String systemMsg;
 
     /**
+     * 涓嬪彂澶辫触娆℃暟
+     */
+    @ApiModelProperty(value= "涓嬪彂澶辫触娆℃暟")
+    @TableField(value = "send_fail_count")
+    private Integer sendFailCount;
+
+    /**
      * 鎵规鍙�
      */
     @ApiModelProperty(value= "鎵规鍙�")
@@ -194,6 +201,9 @@
         if (entity != null) {
             return entity.getWrkDesc();
         }
+        try {
+            return com.zy.core.enums.WrkStsType.query(this.wrkSts).desc;
+        } catch (Exception ignore) {}
         return null;
     }
 
@@ -275,6 +285,10 @@
     }
 
     public void setErrorMemo(String errorMemo) {
+        if (errorMemo == null) {
+            this.errorMemo = null;
+            return;
+        }
         if (errorMemo.length() > 255) {
             errorMemo = errorMemo.substring(0, 150);
         }
diff --git a/src/main/java/com/zy/asrs/entity/WrkMastLog.java b/src/main/java/com/zy/asrs/entity/WrkMastLog.java
index 8436eb6..60cc356 100644
--- a/src/main/java/com/zy/asrs/entity/WrkMastLog.java
+++ b/src/main/java/com/zy/asrs/entity/WrkMastLog.java
@@ -181,6 +181,13 @@
     private String systemMsg;
 
     /**
+     * 涓嬪彂澶辫触娆℃暟
+     */
+    @ApiModelProperty(value= "涓嬪彂澶辫触娆℃暟")
+    @TableField(value = "send_fail_count")
+    private Integer sendFailCount;
+
+    /**
      * 鎵规鍙�
      */
     @ApiModelProperty(value= "鎵规鍙�")
@@ -202,6 +209,9 @@
         if (entity != null) {
             return entity.getWrkDesc();
         }
+        try {
+            return com.zy.core.enums.WrkStsType.query(this.wrkSts).desc;
+        } catch (Exception ignore) {}
         return null;
     }
 
diff --git a/src/main/java/com/zy/common/service/CommonService.java b/src/main/java/com/zy/common/service/CommonService.java
index cc2f16c..10b671c 100644
--- a/src/main/java/com/zy/common/service/CommonService.java
+++ b/src/main/java/com/zy/common/service/CommonService.java
@@ -13,6 +13,7 @@
 import com.zy.common.utils.NavigateUtils;
 import com.zy.common.utils.RedisUtil;
 import com.zy.core.News;
+import com.zy.core.service.WrkCommandRollbackService;
 import com.zy.core.enums.*;
 import com.zy.core.model.StationObjModel;
 import lombok.extern.slf4j.Slf4j;
@@ -44,6 +45,8 @@
     private RedisUtil redisUtil;
     @Autowired
     private BasOutStationAreaService basOutStationAreaService;
+    @Autowired
+    private WrkCommandRollbackService wrkCommandRollbackService;
 
     /**
      * 鐢熸垚宸ヤ綔鍙�
@@ -186,6 +189,10 @@
         return result;
     }
 
+    public boolean manualRollbackTask(ManualRollbackTaskParam param) {
+        return wrkCommandRollbackService.manualRollbackTask(param);
+    }
+
     //绉诲簱浠诲姟
     public boolean createLocMoveTask(CreateLocMoveTaskParam param) {
         Date now = new Date();
diff --git a/src/main/java/com/zy/core/enums/WrkStsType.java b/src/main/java/com/zy/core/enums/WrkStsType.java
index 381ccfc..143e36f 100644
--- a/src/main/java/com/zy/core/enums/WrkStsType.java
+++ b/src/main/java/com/zy/core/enums/WrkStsType.java
@@ -8,6 +8,7 @@
     INBOUND_DEVICE_RUN(2, "璁惧涓婅蛋"),
     INBOUND_RUN(3, "璁惧鎼繍涓�"),
     INBOUND_RUN_COMPLETE(4, "璁惧鎼繍瀹屾垚"),
+    INBOUND_MANUAL(6, "鍏ュ簱寰呬汉宸ュ洖婊�"),
     COMPLETE_INBOUND(9, "鍏ュ簱瀹屾垚"),
     SETTLE_INBOUND(10, "鍏ュ簱搴撳瓨鏇存柊"),
 
@@ -16,12 +17,14 @@
     OUTBOUND_RUN_COMPLETE(103, "璁惧鎼繍瀹屾垚"),
     STATION_RUN(104, "绔欑偣杩愯涓�"),
     STATION_RUN_COMPLETE(105, "绔欑偣杩愯瀹屾垚"),
+    OUTBOUND_MANUAL(106, "鍑哄簱寰呬汉宸ュ洖婊�"),
     COMPLETE_OUTBOUND(109, "鍑哄簱瀹屾垚"),
     SETTLE_OUTBOUND(110, "鍑哄簱搴撳瓨鏇存柊"),
 
     NEW_LOC_MOVE(501, "鐢熸垚绉诲簱浠诲姟"),
     LOC_MOVE_RUN(502, "璁惧鎼繍涓�"),
     LOC_MOVE_RUN_COMPLETE(503, "璁惧鎼繍瀹屾垚"),
+    LOC_MOVE_MANUAL(506, "绉诲簱寰呬汉宸ュ洖婊�"),
     COMPLETE_LOC_MOVE(509, "绉诲簱瀹屾垚"),
     ;
 
diff --git a/src/main/java/com/zy/core/service/WrkCommandRollbackService.java b/src/main/java/com/zy/core/service/WrkCommandRollbackService.java
new file mode 100644
index 0000000..bd6ac11
--- /dev/null
+++ b/src/main/java/com/zy/core/service/WrkCommandRollbackService.java
@@ -0,0 +1,212 @@
+package com.zy.core.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.common.Cools;
+import com.core.exception.CoolException;
+import com.zy.asrs.domain.param.ManualRollbackTaskParam;
+import com.zy.asrs.entity.WrkMast;
+import com.zy.asrs.service.WrkMastLogService;
+import com.zy.asrs.service.WrkMastService;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.News;
+import com.zy.core.enums.CrnTaskModeType;
+import com.zy.core.enums.RedisKeyType;
+import com.zy.core.enums.WrkStsType;
+import com.zy.core.model.CommandResponse;
+import com.zy.core.model.command.CrnCommand;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.HashMap;
+
+@Slf4j
+@Service
+public class WrkCommandRollbackService {
+
+    private static final int DEFAULT_AUTO_ROLLBACK_LIMIT = 3;
+
+    @Autowired
+    private WrkMastService wrkMastService;
+    @Autowired
+    private WrkMastLogService wrkMastLogService;
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Transactional(rollbackFor = Exception.class)
+    public void handleSingleCrnSendResult(CrnCommand command, CommandResponse response, String deviceName) {
+        if (command == null || command.getTaskNo() == null || command.getTaskNo() <= 0) {
+            return;
+        }
+
+        if (CrnTaskModeType.RESET.id.equals(command.getTaskMode())) {
+            return;
+        }
+
+        WrkMast wrkMast = wrkMastService.selectByWorkNo(command.getTaskNo());
+        if (wrkMast == null) {
+            return;
+        }
+
+        if (!isSingleCrnRunStatus(wrkMast.getWrkSts())) {
+            return;
+        }
+
+        if (response != null && Boolean.TRUE.equals(response.getResult())) {
+            clearSendFailState(wrkMast);
+            return;
+        }
+
+        String failMessage = buildFailMessage(deviceName, response);
+        int nextFailCount = (wrkMast.getSendFailCount() == null ? 0 : wrkMast.getSendFailCount()) + 1;
+        int rollbackLimit = getAutoRollbackLimit();
+        boolean manualRequired = nextFailCount >= rollbackLimit;
+        Long updateStatus = manualRequired ? getManualStatus(wrkMast.getWrkSts()) : getRollbackStatus(wrkMast.getWrkSts());
+        if (updateStatus == null) {
+            return;
+        }
+
+        saveWrkLog(wrkMast.getWrkNo());
+
+        wrkMast.setWrkSts(updateStatus);
+        wrkMast.setSendFailCount(nextFailCount);
+        wrkMast.setErrorTime(new Date());
+        wrkMast.setErrorMemo(failMessage);
+        wrkMast.setSystemMsg(failMessage);
+        wrkMast.setModiTime(new Date());
+        wrkMastService.updateById(wrkMast);
+
+        if (manualRequired) {
+            News.taskError(wrkMast.getWrkNo(), "{}涓嬪彂鍛戒护澶辫触锛屽凡杈惧埌鑷姩鍥炴粴涓婇檺({})锛岃鍦ㄥ伐浣滄。鎵嬪姩鍥炴粴銆傚師鍥�: {}", deviceName, rollbackLimit, failMessage);
+        } else {
+            News.taskWarn(wrkMast.getWrkNo(), "{}涓嬪彂鍛戒护澶辫触锛屼换鍔″凡鑷姩鍥炴粴({}/{}). 鍘熷洜: {}", deviceName, nextFailCount, rollbackLimit, failMessage);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public boolean manualRollbackTask(ManualRollbackTaskParam param) {
+        WrkMast wrkMast = findWrkMast(param == null ? null : param.getWrkNo(), param == null ? null : param.getTaskNo());
+        if (wrkMast == null) {
+            throw new CoolException("浠诲姟涓嶅瓨鍦�");
+        }
+
+        Long rollbackStatus = getRollbackStatusFromManual(wrkMast.getWrkSts());
+        if (rollbackStatus == null) {
+            throw new CoolException("褰撳墠浠诲姟涓嶅浜庡緟浜哄伐鍥炴粴鐘舵��");
+        }
+
+        saveWrkLog(wrkMast.getWrkNo());
+
+        wrkMast.setWrkSts(rollbackStatus);
+        wrkMast.setSendFailCount(0);
+        wrkMast.setSystemMsg("浜哄伐鍥炴粴瀹屾垚锛岀瓑寰呴噸鏂颁笅鍙�");
+        wrkMast.setModiTime(new Date());
+        wrkMastService.updateById(wrkMast);
+        News.taskInfo(wrkMast.getWrkNo(), "浜哄伐鍥炴粴瀹屾垚锛屼换鍔$姸鎬佸凡鎭㈠涓哄緟鎵ц");
+        return true;
+    }
+
+    private void clearSendFailState(WrkMast wrkMast) {
+        boolean needUpdate = (wrkMast.getSendFailCount() != null && wrkMast.getSendFailCount() > 0)
+                || wrkMast.getErrorTime() != null
+                || !Cools.isEmpty(wrkMast.getErrorMemo());
+        if (!needUpdate) {
+            return;
+        }
+
+        saveWrkLog(wrkMast.getWrkNo());
+
+        wrkMast.setSendFailCount(0);
+        wrkMast.setErrorTime(null);
+        wrkMast.setErrorMemo(null);
+        wrkMast.setSystemMsg("");
+        wrkMast.setModiTime(new Date());
+        wrkMastService.updateById(wrkMast);
+    }
+
+    private WrkMast findWrkMast(Integer wrkNo, String taskNo) {
+        if (wrkNo != null) {
+            return wrkMastService.selectByWorkNo(wrkNo);
+        }
+        if (!Cools.isEmpty(taskNo)) {
+            return wrkMastService.getOne(new QueryWrapper<WrkMast>().eq("wms_wrk_no", taskNo));
+        }
+        return null;
+    }
+
+    private void saveWrkLog(Integer wrkNo) {
+        try {
+            wrkMastLogService.save(wrkNo);
+        } catch (Exception e) {
+            log.warn("淇濆瓨宸ヤ綔妗f棩蹇楀け璐�, wrkNo={}", wrkNo, e);
+        }
+    }
+
+    private int getAutoRollbackLimit() {
+        Object systemConfigMapObj = redisUtil.get(RedisKeyType.SYSTEM_CONFIG_MAP.key);
+        if (!(systemConfigMapObj instanceof HashMap)) {
+            return DEFAULT_AUTO_ROLLBACK_LIMIT;
+        }
+
+        try {
+            HashMap<String, String> systemConfigMap = (HashMap<String, String>) systemConfigMapObj;
+            return Math.max(1, Integer.parseInt(systemConfigMap.getOrDefault("deviceCommandAutoRollbackLimit", String.valueOf(DEFAULT_AUTO_ROLLBACK_LIMIT))));
+        } catch (Exception ignore) {
+            return DEFAULT_AUTO_ROLLBACK_LIMIT;
+        }
+    }
+
+    private String buildFailMessage(String deviceName, CommandResponse response) {
+        if (response != null && !Cools.isEmpty(response.getMessage())) {
+            return response.getMessage();
+        }
+        return deviceName + "鍛戒护涓嬪彂澶辫触";
+    }
+
+    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);
+    }
+
+    private Long getRollbackStatus(Long wrkSts) {
+        if (Long.valueOf(WrkStsType.INBOUND_RUN.sts).equals(wrkSts)) {
+            return WrkStsType.INBOUND_DEVICE_RUN.sts;
+        }
+        if (Long.valueOf(WrkStsType.OUTBOUND_RUN.sts).equals(wrkSts)) {
+            return WrkStsType.NEW_OUTBOUND.sts;
+        }
+        if (Long.valueOf(WrkStsType.LOC_MOVE_RUN.sts).equals(wrkSts)) {
+            return WrkStsType.NEW_LOC_MOVE.sts;
+        }
+        return null;
+    }
+
+    private Long getManualStatus(Long wrkSts) {
+        if (Long.valueOf(WrkStsType.INBOUND_RUN.sts).equals(wrkSts)) {
+            return WrkStsType.INBOUND_MANUAL.sts;
+        }
+        if (Long.valueOf(WrkStsType.OUTBOUND_RUN.sts).equals(wrkSts)) {
+            return WrkStsType.OUTBOUND_MANUAL.sts;
+        }
+        if (Long.valueOf(WrkStsType.LOC_MOVE_RUN.sts).equals(wrkSts)) {
+            return WrkStsType.LOC_MOVE_MANUAL.sts;
+        }
+        return null;
+    }
+
+    private Long getRollbackStatusFromManual(Long wrkSts) {
+        if (Long.valueOf(WrkStsType.INBOUND_MANUAL.sts).equals(wrkSts)) {
+            return WrkStsType.INBOUND_DEVICE_RUN.sts;
+        }
+        if (Long.valueOf(WrkStsType.OUTBOUND_MANUAL.sts).equals(wrkSts)) {
+            return WrkStsType.NEW_OUTBOUND.sts;
+        }
+        if (Long.valueOf(WrkStsType.LOC_MOVE_MANUAL.sts).equals(wrkSts)) {
+            return WrkStsType.NEW_LOC_MOVE.sts;
+        }
+        return null;
+    }
+}
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 0205cc1..2c0013f 100644
--- a/src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java
+++ b/src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java
@@ -24,6 +24,7 @@
 import com.zy.core.network.DeviceConnectPool;
 import com.zy.core.network.ZyCrnConnectDriver;
 import com.zy.core.network.entity.ZyCrnStatusEntity;
+import com.zy.core.service.WrkCommandRollbackService;
 import com.zy.core.thread.CrnThread;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
@@ -307,27 +308,35 @@
             response = zyCrnConnectDriver.sendCommand(command);
             return response;
         } finally {
-            String sourceLocNo = Utils.getLocNo(command.getSourcePosX(), command.getSourcePosY(), command.getSourcePosZ());
-            String targetLocNo = Utils.getLocNo(command.getDestinationPosX(), command.getDestinationPosY(), command.getDestinationPosZ());
-            BasCrnpOptService bean = SpringUtils.getBean(BasCrnpOptService.class);
-            ZyCrnStatusEntity statusEntity = zyCrnConnectDriver.getStatus();
-            BasCrnpOpt basCrnpOpt = new BasCrnpOpt(
-                    command.getTaskNo().intValue(),
-                    command.getCrnNo(),
-                    new Date(),
-                    String.valueOf(command.getTaskMode()),
-                    sourceLocNo,
-                    targetLocNo,
-                    null,
-                    null,
-                    null,
-                    JSON.toJSONString(command),
-                    JSON.toJSONString(statusEntity),
-                    1,
-                    JSON.toJSONString(response)
-            );
-            if (bean != null) {
+            if (command != null) {
+                String sourceLocNo = Utils.getLocNo(command.getSourcePosX(), command.getSourcePosY(), command.getSourcePosZ());
+                String targetLocNo = Utils.getLocNo(command.getDestinationPosX(), command.getDestinationPosY(), command.getDestinationPosZ());
+                BasCrnpOptService bean = SpringUtils.getBean(BasCrnpOptService.class);
+                ZyCrnStatusEntity statusEntity = zyCrnConnectDriver.getStatus();
+                BasCrnpOpt basCrnpOpt = new BasCrnpOpt(
+                        command.getTaskNo(),
+                        command.getCrnNo(),
+                        new Date(),
+                        String.valueOf(command.getTaskMode()),
+                        sourceLocNo,
+                        targetLocNo,
+                        null,
+                        null,
+                        null,
+                        JSON.toJSONString(command),
+                        JSON.toJSONString(statusEntity),
+                        1,
+                        JSON.toJSONString(response)
+                );
                 bean.save(basCrnpOpt);
+                try {
+                    WrkCommandRollbackService rollbackService = SpringUtils.getBean(WrkCommandRollbackService.class);
+                    if (rollbackService != null) {
+                        rollbackService.handleSingleCrnSendResult(command, response, "鍗曞伐浣嶅爢鍨涙満");
+                    }
+                } catch (Exception e) {
+                    log.error("澶勭悊鍗曞伐浣嶅懡浠や笅鍙戠粨鏋滃け璐�, wrkNo={}", command.getTaskNo(), e);
+                }
             }
         }
     }
diff --git a/src/main/java/com/zy/core/thread/impl/ZySiemensCrnV2Thread.java b/src/main/java/com/zy/core/thread/impl/ZySiemensCrnV2Thread.java
index 24dfdd8..1801e71 100644
--- a/src/main/java/com/zy/core/thread/impl/ZySiemensCrnV2Thread.java
+++ b/src/main/java/com/zy/core/thread/impl/ZySiemensCrnV2Thread.java
@@ -22,6 +22,7 @@
 import com.zy.core.network.DeviceConnectPool;
 import com.zy.core.network.ZyCrnV2ConnectDriver;
 import com.zy.core.network.entity.ZyCrnStatusEntity;
+import com.zy.core.service.WrkCommandRollbackService;
 import com.zy.core.thread.CrnThread;
 import lombok.Data;
 import lombok.extern.slf4j.Slf4j;
@@ -307,27 +308,35 @@
             response = zyCrnConnectDriver.sendCommand(command);
             return response;
         } finally {
-            String sourceLocNo = Utils.getLocNo(command.getSourcePosX(), command.getSourcePosY(), command.getSourcePosZ());
-            String targetLocNo = Utils.getLocNo(command.getDestinationPosX(), command.getDestinationPosY(), command.getDestinationPosZ());
-            BasCrnpOptService bean = SpringUtils.getBean(BasCrnpOptService.class);
-            ZyCrnStatusEntity statusEntity = zyCrnConnectDriver.getStatus();
-            BasCrnpOpt basCrnpOpt = new BasCrnpOpt(
-                    command.getTaskNo().intValue(),
-                    command.getCrnNo(),
-                    new Date(),
-                    String.valueOf(command.getTaskMode()),
-                    sourceLocNo,
-                    targetLocNo,
-                    null,
-                    null,
-                    null,
-                    JSON.toJSONString(command),
-                    JSON.toJSONString(statusEntity),
-                    1,
-                    JSON.toJSONString(response)
-            );
-            if (bean != null) {
+            if (command != null) {
+                String sourceLocNo = Utils.getLocNo(command.getSourcePosX(), command.getSourcePosY(), command.getSourcePosZ());
+                String targetLocNo = Utils.getLocNo(command.getDestinationPosX(), command.getDestinationPosY(), command.getDestinationPosZ());
+                BasCrnpOptService bean = SpringUtils.getBean(BasCrnpOptService.class);
+                ZyCrnStatusEntity statusEntity = zyCrnConnectDriver.getStatus();
+                BasCrnpOpt basCrnpOpt = new BasCrnpOpt(
+                        command.getTaskNo(),
+                        command.getCrnNo(),
+                        new Date(),
+                        String.valueOf(command.getTaskMode()),
+                        sourceLocNo,
+                        targetLocNo,
+                        null,
+                        null,
+                        null,
+                        JSON.toJSONString(command),
+                        JSON.toJSONString(statusEntity),
+                        1,
+                        JSON.toJSONString(response)
+                );
                 bean.save(basCrnpOpt);
+                try {
+                    WrkCommandRollbackService rollbackService = SpringUtils.getBean(WrkCommandRollbackService.class);
+                    if (rollbackService != null) {
+                        rollbackService.handleSingleCrnSendResult(command, response, "鍗曞伐浣嶅爢鍨涙満V2");
+                    }
+                } catch (Exception e) {
+                    log.error("澶勭悊鍗曞伐浣峍2鍛戒护涓嬪彂缁撴灉澶辫触, wrkNo={}", command.getTaskNo(), e);
+                }
             }
         }
     }
diff --git a/src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java b/src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java
index 679402d..ebbe876 100644
--- a/src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java
+++ b/src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java
@@ -523,7 +523,7 @@
             BasDualCrnpOptService bean = SpringUtils.getBean(BasDualCrnpOptService.class);
             ZyDualCrnStatusEntity statusEntity = zyDualCrnConnectDriver.getStatus();
             BasDualCrnpOpt basDualCrnpOpt = new BasDualCrnpOpt(
-                    command.getTaskNo().intValue(),
+                    command.getTaskNo(),
                     command.getCrnNo(),
                     new Date(),
                     String.valueOf(command.getTaskMode()),
@@ -537,9 +537,7 @@
                     1,
                     JSON.toJSONString(response)
             );
-            if (bean != null) {
-                bean.save(basDualCrnpOpt);
-            }
+            bean.save(basDualCrnpOpt);
         }
     }
 
diff --git a/src/main/resources/mapper/WrkMastLogMapper.xml b/src/main/resources/mapper/WrkMastLogMapper.xml
index 714d6b0..c5a1d03 100644
--- a/src/main/resources/mapper/WrkMastLogMapper.xml
+++ b/src/main/resources/mapper/WrkMastLogMapper.xml
@@ -28,6 +28,7 @@
         <result column="rgv_no" property="rgvNo" />
         <result column="wms_wrk_no" property="wmsWrkNo" />
         <result column="system_msg" property="systemMsg" />
+        <result column="send_fail_count" property="sendFailCount" />
         <result column="batch" property="batch" />
         <result column="batch_seq" property="batchSeq" />
     </resultMap>
diff --git a/src/main/resources/mapper/WrkMastMapper.xml b/src/main/resources/mapper/WrkMastMapper.xml
index 8af8efe..35186ad 100644
--- a/src/main/resources/mapper/WrkMastMapper.xml
+++ b/src/main/resources/mapper/WrkMastMapper.xml
@@ -27,6 +27,7 @@
         <result column="rgv_no" property="rgvNo" />
         <result column="wms_wrk_no" property="wmsWrkNo" />
         <result column="system_msg" property="systemMsg" />
+        <result column="send_fail_count" property="sendFailCount" />
         <result column="batch" property="batch" />
         <result column="batch_seq" property="batchSeq" />
 
diff --git a/src/main/resources/sql/20260316_add_wrk_send_fail_rollback.sql b/src/main/resources/sql/20260316_add_wrk_send_fail_rollback.sql
new file mode 100644
index 0000000..c196a20
--- /dev/null
+++ b/src/main/resources/sql/20260316_add_wrk_send_fail_rollback.sql
@@ -0,0 +1,21 @@
+ALTER TABLE `asr_wrk_mast`
+    ADD COLUMN `send_fail_count` INT NOT NULL DEFAULT 0 COMMENT '鍛戒护涓嬪彂澶辫触娆℃暟' AFTER `system_msg`;
+
+ALTER TABLE `asr_wrk_mast_log`
+    ADD COLUMN `send_fail_count` INT NOT NULL DEFAULT 0 COMMENT '鍛戒护涓嬪彂澶辫触娆℃暟' AFTER `system_msg`;
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `modi_user`, `modi_time`, `appe_user`, `appe_time`)
+SELECT 6, '鍏ュ簱寰呬汉宸ュ洖婊�', NULL, NOW(), NULL, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `asr_bas_wrk_status` WHERE `wrk_sts` = 6);
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `modi_user`, `modi_time`, `appe_user`, `appe_time`)
+SELECT 106, '鍑哄簱寰呬汉宸ュ洖婊�', NULL, NOW(), NULL, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `asr_bas_wrk_status` WHERE `wrk_sts` = 106);
+
+INSERT INTO `asr_bas_wrk_status` (`wrk_sts`, `wrk_desc`, `modi_user`, `modi_time`, `appe_user`, `appe_time`)
+SELECT 506, '绉诲簱寰呬汉宸ュ洖婊�', NULL, NOW(), NULL, NOW()
+WHERE NOT EXISTS (SELECT 1 FROM `asr_bas_wrk_status` WHERE `wrk_sts` = 506);
+
+INSERT INTO `sys_config` (`name`, `code`, `value`, `type`, `status`, `select_type`)
+SELECT '璁惧鍛戒护鑷姩鍥炴粴涓婇檺', 'deviceCommandAutoRollbackLimit', '3', 1, 1, 'crn'
+WHERE NOT EXISTS (SELECT 1 FROM `sys_config` WHERE `code` = 'deviceCommandAutoRollbackLimit');
diff --git a/src/main/webapp/static/js/wrkMast/wrkMast.js b/src/main/webapp/static/js/wrkMast/wrkMast.js
index 31007e4..bb926fb 100644
--- a/src/main/webapp/static/js/wrkMast/wrkMast.js
+++ b/src/main/webapp/static/js/wrkMast/wrkMast.js
@@ -17,8 +17,13 @@
         { key: "dualCrnNo", prop: "dualCrnNo", label: "鍙屽伐浣嶅爢鍨涙満", minWidth: 120, sortable: true, align: "center" },
         { key: "batch", prop: "batch", label: "鎵规", minWidth: 120, sortable: true },
         { key: "batchSeq", prop: "batchSeq", label: "鎵规搴忓垪", width: 100, sortable: true, align: "center" },
+        { key: "sendFailCount", prop: "sendFailCount", label: "澶辫触娆℃暟", width: 96, sortable: true, align: "center" },
+        { key: "errorTime$", prop: "errorTime$", label: "澶辫触鏃堕棿", minWidth: 168, sortable: true },
+        { key: "errorMemo", prop: "errorMemo", label: "澶辫触鍘熷洜", minWidth: 220, sortable: false, showOverflow: false },
         { key: "systemMsg", prop: "systemMsg", label: "绯荤粺娑堟伅", minWidth: 220, sortable: false, showOverflow: false }
     ];
+
+    var MANUAL_ROLLBACK_STATUSES = [6, 106, 506];
 
     function cloneSearchForm() {
         return {
@@ -237,7 +242,14 @@
                 }
                 if (command === "cancel") {
                     this.cancelTask(row);
+                    return;
                 }
+                if (command === "manualRollback") {
+                    this.manualRollbackTask(row);
+                }
+            },
+            canManualRollback: function (row) {
+                return !!row && MANUAL_ROLLBACK_STATUSES.indexOf(Number(row.wrkSts)) > -1;
             },
             completeTask: function (row) {
                 var vm = this;
@@ -301,6 +313,41 @@
                     });
                 }).catch(function () {});
             },
+            manualRollbackTask: function (row) {
+                var vm = this;
+                if (!vm.canManualRollback(row)) {
+                    vm.$message.warning("褰撳墠浠诲姟涓嶅浜庡緟浜哄伐鍥炴粴鐘舵��");
+                    return;
+                }
+                vm.$confirm("纭畾鎵嬪姩鍥炴粴璇ヤ换鍔″悧锛熷洖婊氬悗浠诲姟浼氭仮澶嶄负寰呮墽琛岀姸鎬併��", "鎻愮ず", {
+                    type: "warning",
+                    confirmButtonText: "纭畾",
+                    cancelButtonText: "鍙栨秷"
+                }).then(function () {
+                    $.ajax({
+                        url: baseUrl + "/openapi/manualRollbackTask",
+                        contentType: "application/json",
+                        headers: { token: localStorage.getItem("token") },
+                        data: JSON.stringify({ wrkNo: row.wrkNo }),
+                        method: "POST",
+                        success: function (res) {
+                            if (res.code === 200) {
+                                vm.$message.success("浜哄伐鍥炴粴鎴愬姛");
+                                vm.loadList();
+                                return;
+                            }
+                            if (res.code === 403) {
+                                top.location.href = baseUrl + "/";
+                                return;
+                            }
+                            vm.$message.error(res.msg || "浜哄伐鍥炴粴澶辫触");
+                        },
+                        error: function () {
+                            vm.$message.error("浜哄伐鍥炴粴澶辫触");
+                        }
+                    });
+                }).catch(function () {});
+            },
             updateTableHeight: function () {
                 var viewport = window.innerHeight || document.documentElement.clientHeight || 860;
                 this.tableHeight = Math.max(340, viewport - (this.advancedVisible ? 276 : 222));
diff --git a/src/main/webapp/views/wrkMast/wrkMast.html b/src/main/webapp/views/wrkMast/wrkMast.html
index 979e39f..3877200 100644
--- a/src/main/webapp/views/wrkMast/wrkMast.html
+++ b/src/main/webapp/views/wrkMast/wrkMast.html
@@ -378,7 +378,7 @@
                         :show-overflow-tooltip="column.showOverflow !== false"
                         :align="column.align || 'left'">
                         <template slot-scope="scope">
-                            <span :class="column.key === 'systemMsg' ? 'payload-cell' : ''">
+                            <span :class="(column.key === 'systemMsg' || column.key === 'errorMemo') ? 'payload-cell' : ''">
                                 {{ displayCellValue(scope.row, column) }}
                             </span>
                         </template>
@@ -390,6 +390,7 @@
                                     鎿嶄綔<i class="el-icon-arrow-down el-icon--right"></i>
                                 </el-button>
                                 <el-dropdown-menu slot="dropdown">
+                                    <el-dropdown-item v-if="canManualRollback(scope.row)" command="manualRollback">鎵嬪姩鍥炴粴</el-dropdown-item>
                                     <el-dropdown-item command="complete">瀹屾垚浠诲姟</el-dropdown-item>
                                     <el-dropdown-item command="cancel" divided>鍙栨秷浠诲姟</el-dropdown-item>
                                 </el-dropdown-menu>

--
Gitblit v1.9.1