自动化立体仓库 - WMS系统
chen.llin
2 天以前 2816415f539ef54839e331657edae7055c243ad8
src/main/java/com/zy/asrs/task/handler/AgvHandler.java
@@ -7,10 +7,12 @@
import com.zy.asrs.entity.TaskLog;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.LocCache;
import com.zy.asrs.mapper.BasDevpMapper;
import com.zy.asrs.mapper.BasStationMapper;
import com.zy.asrs.mapper.WrkMastMapper;
import com.zy.asrs.service.ApiLogService;
import com.zy.asrs.service.LocCacheService;
import com.zy.asrs.service.TaskLogService;
import com.zy.asrs.service.TaskService;
import com.zy.common.constant.ApiInterfaceConstant;
@@ -55,6 +57,9 @@
    private BasDevpMapper basDevpMapper;
    @Resource
    private LocCacheService locCacheService;
    @Resource
    private AgvProperties agvProperties;
    /**
@@ -73,23 +78,56 @@
        }
        for (Task task : taskList) {
            // 如果任务没有分配站点,先分配站点
            // 如果任务状态已经是8(已呼叫AGV,正在搬运),则不再发送指令
            if (task.getWrkSts() != null && task.getWrkSts() == 8L) {
                log.debug("任务ID:{}状态已是8(正在搬运),跳过发送", task.getId());
                continue;
            }
            // 如果任务没有分配站点,先分配站点(只有为空时才分配,已经分配了不要清空)
            String staNo = task.getStaNo();
            if (staNo == null || staNo.isEmpty()) {
                Integer allocatedSite = allocateSiteForTask(task);
                if (allocatedSite == null) {
                    log.warn("任务ID:{}无法分配站点,跳过本次发送", task.getId());
                    continue; // 无法分配站点,跳过本次发送
                String errorMsg = allocateSiteForTask(task);
                if (errorMsg != null) {
                    // 无法分配站点,只在日志中记录,不记录到任务中(app不需要知道)
                    log.warn("任务ID:{}无法分配站点:{}", task.getId(), errorMsg);
                    continue;
                }
                staNo = String.valueOf(allocatedSite);
                task.setStaNo(staNo);
                taskService.updateById(task);
                // 检查是否成功分配了站点(如果返回null且staNo仍为空,说明所有站点都在搬运,等待下次再试)
                staNo = task.getStaNo();
                if (staNo == null || staNo.isEmpty()) {
                    // 所有站点都在搬运,暂不分配,等待下次定时任务再尝试
                    continue;
                }
                log.info("任务ID:{}已分配站点:{}", task.getId(), staNo);
            }
            
            // 检查目标站点是否有正在搬运的同类型AGV任务(出库和入库互不干扰)
            // 只有状态8(已呼叫AGV,正在搬运)的任务才会阻塞,状态7(待呼叫)的任务不阻塞
            // 这样可以避免所有任务都卡在呼叫状态,按id最小的优先呼叫
            // 检查目标站点是否有效(不为0且存在)
            if (staNo != null && !staNo.isEmpty()) {
                try {
                    Integer siteNo = Integer.parseInt(staNo);
                    // 检查站点是否为0(无效站点),如果是0则不发送,但不清空站点
                    if (siteNo == null || siteNo == 0) {
                        log.warn("任务ID:{}的目标站点{}无效(为0),跳过发送,保留站点分配", task.getId(), staNo);
                        continue;
                    }
                    List<BasDevp> basDevpList = basDevpMapper.selectList(new EntityWrapper<BasDevp>().eq("dev_no", siteNo));
                    if (basDevpList == null || basDevpList.isEmpty()) {
                        // 站点不存在,跳过发送,不清空站点
                        log.warn("任务ID:{}的目标站点{}不存在,跳过发送,保留站点分配", task.getId(), staNo);
                        continue;
                    }
                } catch (NumberFormatException e) {
                    // 站点格式错误,跳过发送,不清空站点
                    log.warn("任务ID:{}的目标站点{}格式错误,跳过发送,保留站点分配", task.getId(), staNo);
                    continue;
                }
            } else {
                // 没有站点,跳过
                continue;
            }
            // 检查站点是否有状态8的同类型任务,有则跳过(不清空站点)
            if (staNo != null && !staNo.isEmpty() && task.getIoType() != null) {
                // 根据当前任务类型,只检查同类型的正在搬运任务(状态8)
                // 入库任务(ioType < 100):只检查入库类型的正在搬运任务
@@ -106,7 +144,7 @@
                    taskType = "出库";
                }
                
                // 只检查状态为8(已呼叫AGV,正在搬运)的同类型任务
                // 检查状态为8(已呼叫AGV,正在搬运)的同类型任务
                List<Task> transportingTasks = taskService.selectList(
                    new EntityWrapper<Task>()
                        .eq("sta_no", staNo)
@@ -117,9 +155,9 @@
                );
                
                if (!transportingTasks.isEmpty()) {
                    log.info("站点{}有{}个正在搬运的{}AGV任务,跳过本次发送,等待搬运完成。当前任务ID:{}",
                    log.info("站点{}有{}个正在搬运的{}AGV任务,跳过当前任务ID:{}",
                            staNo, transportingTasks.size(), taskType, task.getId());
                    continue; // 跳过本次发送,等待下次
                    continue;
                }
            }
            
@@ -147,9 +185,31 @@
                default:
            }
            String body = getRequest(task,namespace);
            // 打印请求信息
            // 获取当前重试次数
            int currentRetryCount = getRetryCount(task);
            int maxRetryCount = agvProperties.getCallRetry().getMaxRetryCount();
            boolean retryEnabled = agvProperties.getCallRetry().isEnabled();
            // 如果重试次数已达到最大值,跳过本次发送
            if (retryEnabled && currentRetryCount >= maxRetryCount) {
                log.warn("{}呼叫agv搬运 - 任务ID:{}已达到最大重试次数({}),停止重试",
                        namespace, task.getId(), maxRetryCount);
                // 记录最终失败信息
                task.setErrorTime(new Date());
                task.setErrorMemo(String.format("AGV呼叫失败,已达到最大重试次数(%d次)", maxRetryCount));
                taskService.updateById(task);
                continue;
            }
            // 打印请求信息(包含重试次数)
            if (currentRetryCount > 0) {
                log.info("{}呼叫agv搬运(第{}次重试) - 请求地址:{}", namespace, currentRetryCount + 1, url);
            } else {
            log.info("{}呼叫agv搬运 - 请求地址:{}", namespace, url);
            }
            log.info("{}呼叫agv搬运 - 请求参数:{}", namespace, body);
            try {
                // 使用仙工M4接口
                response = new HttpHandler.Builder()
@@ -163,30 +223,41 @@
                
                // 检查响应是否为空
                if (response == null || response.trim().isEmpty()) {
                    log.error("{}呼叫agv搬运失败 - 任务ID:{},AGV接口返回为空", namespace, task.getId());
                    String errorMsg = "AGV接口返回为空";
                    log.error("{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, task.getId(), errorMsg);
                    handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                    continue;
                }
                
                JSONObject jsonObject = JSON.parseObject(response);
                if (jsonObject == null) {
                    log.error("{}呼叫agv搬运失败 - 任务ID:{},响应JSON解析失败,响应内容:{}", namespace, task.getId(), response);
                    String errorMsg = "响应JSON解析失败,响应内容:" + response;
                    log.error("{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, task.getId(), errorMsg);
                    handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                    continue;
                }
                
                Integer code = jsonObject.getInteger("code");
                if (code != null && code.equals(200)) {
                    // 呼叫成功,清除重试次数和错误信息
                    success = true;
                    task.setWrkSts(8L);
                    task.setMemo(clearRetryInfo(task.getMemo())); // 清除重试信息
                    task.setErrorTime(null);
                    task.setErrorMemo(null);
                    taskService.updateById(task);
                    log.info("{}呼叫agv搬运成功 - 任务ID:{}", namespace, task.getId());
                } else {
                    String message = jsonObject.getString("message");
                    log.error("{}呼叫agv搬运失败 - 任务ID:{},错误码:{},错误信息:{}",
                            namespace, task.getId(), code, message);
                    String errorMsg = String.format("错误码:%s,错误信息:%s", code, message);
                    log.error("{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, task.getId(), errorMsg);
                    handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                }
            } catch (Exception e) {
                log.error("{}呼叫agv搬运异常 - 任务ID:{},请求地址:{},请求参数:{},异常信息:{}",
                        namespace, task.getId(), url, body, e.getMessage(), e);
                String errorMsg = "异常信息:" + e.getMessage();
                log.error("{}呼叫agv搬运异常 - 任务ID:{},请求地址:{},请求参数:{},{}",
                        namespace, task.getId(), url, body, errorMsg, e);
                handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
            } finally {
                try {
                    // 保存接口日志
@@ -204,6 +275,106 @@
                }
            }
        }
    }
    /**
     * 处理AGV呼叫失败的情况
     * @param task 任务对象
     * @param namespace 命名空间(入库/出库/转移)
     * @param errorMsg 错误信息
     * @param retryEnabled 是否启用重试
     * @param maxRetryCount 最大重试次数
     * @param currentRetryCount 当前重试次数
     */
    private void handleCallFailure(Task task, String namespace, String errorMsg,
                                    boolean retryEnabled, int maxRetryCount, int currentRetryCount) {
        if (retryEnabled && currentRetryCount < maxRetryCount) {
            // 增加重试次数
            int newRetryCount = currentRetryCount + 1;
            task.setMemo(updateRetryCount(task.getMemo(), newRetryCount));
            task.setErrorTime(new Date());
            task.setErrorMemo(String.format("AGV呼叫失败(第%d次重试):%s", newRetryCount, errorMsg));
            taskService.updateById(task);
            log.info("{}呼叫agv搬运失败 - 任务ID:{},已重试{}次,将在下次定时任务时继续重试(最多{}次)",
                    namespace, task.getId(), newRetryCount, maxRetryCount);
        } else {
            // 不启用重试或已达到最大重试次数,停止重试
            task.setErrorTime(new Date());
            if (retryEnabled) {
                task.setErrorMemo(String.format("AGV呼叫失败,已达到最大重试次数(%d次):%s", maxRetryCount, errorMsg));
            } else {
                task.setErrorMemo(String.format("AGV呼叫失败(重试未启用):%s", errorMsg));
            }
            taskService.updateById(task);
            log.warn("{}呼叫agv搬运失败 - 任务ID:{},停止重试。错误信息:{}",
                    namespace, task.getId(), errorMsg);
        }
    }
    /**
     * 从memo字段中获取重试次数
     * memo格式:如果包含"retryCount:数字",则返回该数字,否则返回0
     * @param task 任务对象
     * @return 重试次数
     */
    private int getRetryCount(Task task) {
        String memo = task.getMemo();
        if (memo == null || memo.trim().isEmpty()) {
            return 0;
        }
        try {
            // 查找 "retryCount:数字" 格式
            String prefix = "retryCount:";
            int index = memo.indexOf(prefix);
            if (index >= 0) {
                int startIndex = index + prefix.length();
                int endIndex = memo.indexOf(",", startIndex);
                if (endIndex < 0) {
                    endIndex = memo.length();
                }
                String countStr = memo.substring(startIndex, endIndex).trim();
                return Integer.parseInt(countStr);
            }
        } catch (Exception e) {
            log.warn("解析任务ID:{}的重试次数失败,memo:{}", task.getId(), memo, e);
        }
        return 0;
    }
    /**
     * 更新memo字段中的重试次数
     * @param memo 原始memo内容
     * @param retryCount 新的重试次数
     * @return 更新后的memo内容
     */
    private String updateRetryCount(String memo, int retryCount) {
        if (memo == null) {
            memo = "";
        }
        // 移除旧的retryCount信息
        String cleanedMemo = clearRetryInfo(memo);
        // 添加新的retryCount信息
        if (cleanedMemo.isEmpty()) {
            return "retryCount:" + retryCount;
        } else {
            return cleanedMemo + ",retryCount:" + retryCount;
        }
    }
    /**
     * 清除memo字段中的重试信息
     * @param memo 原始memo内容
     * @return 清除后的memo内容
     */
    private String clearRetryInfo(String memo) {
        if (memo == null || memo.trim().isEmpty()) {
            return "";
        }
        // 移除 "retryCount:数字" 格式的内容
        String result = memo.replaceAll("retryCount:\\d+", "").trim();
        // 清理多余的逗号
        result = result.replaceAll("^,|,$", "").replaceAll(",,+", ",");
        return result;
    }
    /**
@@ -258,9 +429,9 @@
    /**
     * 为任务分配站点(定时任务中调用)
     * @param task 任务对象
     * @return 分配的站点编号,如果无法分配则返回null
     * @return 如果无法分配站点,返回错误信息;如果分配成功,返回null并更新task的staNo
     */
    private Integer allocateSiteForTask(Task task) {
    private String allocateSiteForTask(Task task) {
        // 根据任务的invWh(机器人组)判断是东侧还是西侧
        String robotGroup = task.getInvWh();
        List<String> targetStations;
@@ -282,8 +453,9 @@
        }
        
        if (targetStations.isEmpty()) {
            log.warn("任务ID:{}没有可用的目标站点配置", task.getId());
            return null;
            String errorMsg = "没有可用的目标站点配置";
            log.warn("任务ID:{}", errorMsg, task.getId());
            return errorMsg;
        }
        
        // 将站点字符串列表转换为整数列表
@@ -291,35 +463,45 @@
                .map(Integer::parseInt)
                .collect(Collectors.toList());
        
        // 判断能入站点(in_enable="Y"表示能入)
        // 判断能入站点(in_enable="Y"表示能入),排除dev_no=0的无效站点
        List<Integer> sites = basDevpMapper.selectList(
                new EntityWrapper<BasDevp>()
                        .eq("in_enable", "Y")
                        .in("dev_no", siteIntList)
        ).stream().map(BasDevp::getDevNo).collect(Collectors.toList());
                        .ne("dev_no", 0) // 排除dev_no=0的无效站点
        ).stream()
                .map(BasDevp::getDevNo)
                .filter(devNo -> devNo != null && devNo != 0) // 再次过滤,确保不为null或0
                .collect(Collectors.toList());
        
        if (sites.isEmpty()) {
            log.warn("任务ID:{}没有能入站点", task.getId());
            return null;
            String errorMsg = "没有能入站点";
            log.warn("任务ID:{}", errorMsg, task.getId());
            return errorMsg;
        }
        
        // 获取没有出库任务的站点
        List<Integer> canInSites = basDevpMapper.getCanInSites(sites);
        if (canInSites.isEmpty()) {
            log.warn("任务ID:{}没有可入站点(请等待出库完成)", task.getId());
            return null;
            String errorMsg = "请等待出库完成";
            log.warn("任务ID:{}没有可入站点({})", task.getId(), errorMsg);
            return errorMsg;
        }
        
        // 寻找入库任务最少的站点(且必须in_enable="Y"能入 和 canining="Y"可入)
        // 寻找入库任务最少的站点(且必须in_enable="Y"能入 和 canining="Y"可入),排除dev_no=0的无效站点
        List<BasDevp> devList = basDevpMapper.selectList(new EntityWrapper<BasDevp>()
                .in("dev_no", canInSites)
                .eq("in_enable", "Y")
                .eq("canining", "Y")
        );
                .ne("dev_no", 0) // 排除dev_no=0的无效站点
        ).stream()
                .filter(dev -> dev.getDevNo() != null && dev.getDevNo() != 0) // 再次过滤,确保不为null或0
                .collect(Collectors.toList());
        
        if (devList.isEmpty()) {
            log.warn("任务ID:{}没有可入站点(in_enable='Y'且canining='Y')", task.getId());
            return null;
            String errorMsg = "没有可入站点(in_enable='Y'且canining='Y')";
            log.warn("任务ID:{}", errorMsg, task.getId());
            return errorMsg;
        }
        
        // 先按规则排序(入库任务数排序)
@@ -409,20 +591,31 @@
            break;
        }
        
        // 如果所有站点都在搬运,则不分配站点
        // 如果所有站点都在搬运,则不分配站点(只在定时任务中记录日志,不返回错误信息)
        if (selectedSite == null) {
            log.warn("任务ID:{}的所有候选站点都有正在搬运的{}任务,暂不分配站点,等待空闲",
                task.getId(), taskIoType != null && taskIoType < 100 ? "入库" : "出库");
            return null;
//            log.warn("任务ID:{},暂不分配站点,等待空闲 - 所有候选站点都有正在搬运的{}任务",
//                task.getId(), taskIoType != null && taskIoType < 100 ? "入库" : "出库");
            return null; // 返回null,表示暂不分配,等待下次定时任务再尝试
        }
        
        Integer endSite = selectedSite.getDevNo();
        
        // 检查站点是否有效(不能为0或null)
        if (endSite == null || endSite == 0) {
            String errorMsg = String.format("分配的站点无效(dev_no=%s)", endSite);
            log.error("任务ID:{},{}", task.getId(), errorMsg);
            return errorMsg;
        }
        // 入库暂存+1
        basDevpMapper.incrementInQty(endSite);
        
        // 更新任务的站点编号
        task.setStaNo(String.valueOf(endSite));
        taskService.updateById(task);
        log.info("任务ID:{}已分配站点:{}", task.getId(), endSite);
        return endSite;
        return null; // 分配成功,返回null
    }
    /**