自动化立体仓库 - WMS系统
chen.llin
3 天以前 cea1758e1f540e3f5f807951f128b7385b32afe6
src/main/java/com/zy/asrs/task/handler/AgvHandler.java
@@ -80,7 +80,7 @@
    /**
     * 呼叫AGV
     *
     * <p>
     * 重要:此方法只能从 AgvScheduler.callAgv() 定时任务中调用!
     * 所有AGV呼叫请求必须通过定时任务统一处理,确保:
     * 1. 任务按顺序处理,避免并发冲突
@@ -350,14 +350,6 @@
                return true; // 已达到最大重试次数,不再重试,返回true表示已处理(虽然失败)
            }
        
        // 打印请求信息(包含重试次数)
        // if (currentRetryCount > 0) {
        //     log.info("{}呼叫agv搬运(第{}次重试) - 请求地址:{}", namespace, currentRetryCount + 1, url);
        // } else {
        //     log.info("{}呼叫agv搬运 - 请求地址:{}", namespace, url);
        // }
        // log.info("{}呼叫agv搬运 - 请求参数:{}", namespace, body);
        boolean result = false; // 默认返回false,表示未成功处理
        try {
            // 使用仙工M4接口
@@ -368,23 +360,16 @@
                    .build()
                    .doPost();
            // 打印返回参数
             log.info("{}呼叫agv搬运,请求参数「{}」 - 返回参数:{}", namespace,body, response);
            log.info("仙工-{}呼叫agv搬运,请求参数「{}」 - 返回参数:{}", namespace, body, response);
            
            // 检查响应是否为空
            if (response == null || response.trim().isEmpty()) {
                String errorMsg = "AGV接口返回为空";
                log.warn("定时任务:{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, displayTaskId, errorMsg);
                handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                // 如果达到最大重试次数,返回true表示已处理(虽然失败)
                // 否则返回false,让定时任务重新尝试
                if (retryEnabled && currentRetryCount >= maxRetryCount) {
                    result = true; // 已达到最大重试次数,返回true表示已处理(虽然失败)
                    log.info("定时任务:任务ID:{},AGV呼叫失败且已达到最大重试次数({}次),标记为已处理,不再重试",
                        displayTaskId, maxRetryCount);
                } else {
                    result = false; // 返回false,让定时任务重新尝试
                    log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId);
                }
                task.setWrkSts(10L);
                task.setErrorTime(new Date());
                task.setErrorMemo(String.format("AGV接口返回为空"));
                taskService.updateById(task);
            } else {
                // 尝试解析JSON响应,捕获JSON解析异常
                JSONObject jsonObject = null;
@@ -394,44 +379,10 @@
                    // JSON解析失败,响应可能不是有效的JSON格式(如"Server Error"等)
                    String errorMsg = String.format("AGV接口返回非JSON格式响应,响应内容:%s,解析错误:%s", response, e.getMessage());
                    log.error("定时任务:{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, displayTaskId, errorMsg);
                    // 服务器错误时,标记站点为不可用,清空站点分配,不再为当前任务分配站点
                    try {
                        Integer siteNo = Integer.parseInt(staNo);
                        // 查询站点信息
                        List<BasDevp> basDevpList = basDevpMapper.selectList(new EntityWrapper<BasDevp>().eq("dev_no", siteNo));
                        if (basDevpList != null && !basDevpList.isEmpty()) {
                            BasDevp basDevp = basDevpList.get(0);
                            // 标记站点为不可用(设置canining='N')
                            basDevp.setCanining("N");
                            basDevpMapper.updateById(basDevp);
                            log.warn("定时任务:任务ID:{},AGV接口返回服务器错误,已标记站点{}为不可用(canining='N')",
                                displayTaskId, siteNo);
                            // 减少站点的入库任务数(之前分配站点时已经增加了in_qty)
                            basDevpMapper.decrementInQty(siteNo);
                            log.debug("定时任务:任务ID:{},站点{}的in_qty已减少", displayTaskId, siteNo);
                        }
                    } catch (Exception ex) {
                        log.error("定时任务:任务ID:{},标记站点{}为不可用时发生异常:{}", displayTaskId, staNo, ex.getMessage());
                    }
                    // 清空当前任务的站点分配
                    log.warn("定时任务:任务ID:{},AGV接口返回服务器错误,清空站点分配:{},不再为当前任务分配站点",
                        displayTaskId, staNo);
                    task.setStaNo(null);
                    taskService.updateById(task);
                    // 标记任务为失败,不再尝试分配
                    task.setWrkSts(10L);
                    task.setErrorTime(new Date());
                    task.setErrorMemo(String.format("AGV接口返回服务器错误,站点已标记为不可用:%s", errorMsg));
                    task.setErrorMemo(errorMsg);
                    taskService.updateById(task);
                    handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                    // 服务器错误时,不再尝试分配,直接标记为已处理
                    result = true; // 返回true表示已处理(虽然失败),不再尝试分配
                    log.info("定时任务:任务ID:{},AGV呼叫失败(服务器错误),站点已标记为不可用,任务已标记为失败,不再尝试分配",
                        displayTaskId);
                }
                
                // 如果JSON解析成功,继续处理
@@ -444,13 +395,15 @@
                        Long currentStatus = task.getWrkSts();
                        if (currentStatus == null || currentStatus != 8L) {
                            task.setWrkSts(8L);
                            log.info("定时任务:{}呼叫agv搬运成功 - 任务ID:{},状态从{}更新为8", namespace, displayTaskId, currentStatus);
                        } else {
                            log.info("定时任务:{}呼叫agv搬运成功(重试) - 任务ID:{},状态保持为8", namespace, displayTaskId);
                        }
                        task.setLocNo(task.getStaNo());
                        task.setMemo(clearRetryInfo(task.getMemo())); // 清除重试信息
                        task.setErrorTime(null);
                        task.setErrorMemo(null);
                        task.setErrorTime(new Date());
                        task.setErrorMemo(jsonObject.toJSONString());
                        taskService.updateById(task);
                        log.info("定时任务:{}呼叫agv搬运成功 - 任务ID:{}", namespace, displayTaskId);
                        result = true; // 返回true,表示成功处理
@@ -459,44 +412,12 @@
                        String errorMsg = String.format("错误码:%s,错误信息:%s", code, message);
                        log.warn("定时任务:{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, displayTaskId, errorMsg);
                        
                        // 检查是否是站点或库位相关的错误,如果是,清空站点分配,让定时任务重新分配
                        boolean shouldReallocateSite = false;
                        if (message != null) {
                            String lowerMessage = message.toLowerCase();
                            // 库位不存在、站点不存在等错误,应该重新分配站点
                            if (lowerMessage.contains("库位不存在") ||
                                lowerMessage.contains("站点不存在") ||
                                lowerMessage.contains("位置不存在") ||
                                lowerMessage.contains("库位无效") ||
                                lowerMessage.contains("站点无效")) {
                                shouldReallocateSite = true;
                            }
                        }
                        if (shouldReallocateSite) {
                            // 清空站点分配,让定时任务重新分配站点
                            log.warn("定时任务:任务ID:{},AGV呼叫失败({}),清空站点分配:{},下次将重新分配站点",
                                displayTaskId, errorMsg, staNo);
                            task.setStaNo(null);
                        task.setWrkSts(10L);
                        task.setLocNo(task.getStaNo());
                        task.setErrorTime(new Date());
                        task.setErrorMemo(jsonObject.toJSONString());
                            taskService.updateById(task);
                        }
                        
                        handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                        // 如果达到最大重试次数,返回true表示已处理(虽然失败)
                        // 否则返回false,让定时任务重新尝试(如果站点被清空,会重新分配站点;如果站点未清空,会重新发送AGV)
                        if (retryEnabled && currentRetryCount >= maxRetryCount) {
                            result = true; // 已达到最大重试次数,返回true表示已处理(虽然失败)
                            log.info("定时任务:任务ID:{},AGV呼叫失败且已达到最大重试次数({}次),标记为已处理,不再重试",
                                displayTaskId, maxRetryCount);
                        } else {
                            result = false; // 返回false,让定时任务重新尝试(重新分配站点或重新发送AGV)
                            if (shouldReallocateSite) {
                                log.info("定时任务:任务ID:{},站点已清空,下次将重新分配站点", displayTaskId);
                            } else {
                                log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId);
                            }
                        }
                    }
                }
            }
@@ -504,16 +425,15 @@
            String errorMsg = "异常信息:" + e.getMessage();
            log.error("定时任务:{}呼叫agv搬运异常 - 任务ID:{},请求地址:{},请求参数:{},{}", 
                    namespace, displayTaskId, url, body, errorMsg, e);
            handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
            // 如果达到最大重试次数,返回true表示已处理(虽然失败)
            // 否则返回false,让定时任务重新尝试
            if (retryEnabled && currentRetryCount >= maxRetryCount) {
                result = true; // 已达到最大重试次数,返回true表示已处理(虽然失败)
                log.info("定时任务:任务ID:{},AGV呼叫异常且已达到最大重试次数({}次),标记为已处理,不再重试",
                    displayTaskId, maxRetryCount);
            if (errorMsg.contains("connect timed out")) {
                log.error("定时任务:{}呼叫agv搬运异常 - 任务ID:{},网络请求超时",
                        namespace, displayTaskId);
            } else {
                result = false; // 返回false,让定时任务重新尝试
                log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId);
                task.setWrkSts(10L);
                task.setLocNo(task.getStaNo());
                task.setErrorTime(new Date());
                task.setErrorMemo(errorMsg);
                taskService.updateById(task);
            }
        } finally {
            try {
@@ -536,6 +456,7 @@
    /**
     * 处理AGV呼叫失败的情况
     *
     * @param task 任务对象
     * @param namespace 命名空间(入库/出库/转移)
     * @param errorMsg 错误信息
@@ -571,6 +492,7 @@
    /**
     * 检查并自动结束已完成工作档的AGV任务
     * 如果任务对应的工作档已经完成(入库成功),则自动结束该AGV任务
     *
     * @param transportingTasks 正在搬运的任务列表
     * @param taskTypeName 任务类型名称(用于日志)
     * @return 仍然有效的正在搬运的任务列表(已完成的已被移除)
@@ -678,10 +600,10 @@
    }
    /**
     * 从memo字段中获取重试次数
     * memo格式:如果包含"retryCount:数字",则返回该数字,否则返回0
     *
     * @param task 任务对象
     * @return 重试次数
     */
@@ -711,6 +633,7 @@
    /**
     * 更新memo字段中的重试次数
     *
     * @param memo 原始memo内容
     * @param retryCount 新的重试次数
     * @return 更新后的memo内容
@@ -731,6 +654,7 @@
    /**
     * 清除memo字段中的重试信息
     *
     * @param memo 原始memo内容
     * @return 清除后的memo内容
     */
@@ -800,6 +724,7 @@
    /**
     * 为任务分配站点(定时任务中调用)
     * 注意:只会分配一个站点,找到第一个符合条件的站点就分配并退出
     *
     * @param task 任务对象
     * @return 如果无法分配站点,返回错误信息;如果分配成功,返回null并更新task的staNo
     */
@@ -844,48 +769,10 @@
            displayTaskId, groupKey.equals("east") ? agvProperties.getEastDisplayName() : agvProperties.getWestDisplayName(),
            targetStations, targetStations.size());
        // 判断能入站点(in_enable="Y"表示能入),排除dev_no=0的无效站点
        List<BasDevp> allDevList = basDevpMapper.selectList(
                new EntityWrapper<BasDevp>()
                        .in("dev_no", siteIntList)
                        .ne("dev_no", 0) // 排除dev_no=0的无效站点
        );
        // 记录所有站点的状态信息
        StringBuilder siteStatusInfo = new StringBuilder();
        for (BasDevp dev : allDevList) {
            if (siteStatusInfo.length() > 0) {
                siteStatusInfo.append("; ");
            }
            siteStatusInfo.append("站点").append(dev.getDevNo())
                    .append("(in_enable=").append(dev.getInEnable())
                    .append(",canining=").append(dev.getCanining()).append(")");
        }
        log.info("任务ID:{},候选站点状态:{}", displayTaskId, siteStatusInfo.toString());
        List<Integer> sites = allDevList.stream()
                .filter(dev -> "Y".equals(dev.getInEnable()))
                .map(BasDevp::getDevNo)
                .filter(devNo -> devNo != null && devNo != 0) // 再次过滤,确保不为null或0
                .collect(Collectors.toList());
        // 检查是否有站点不可用,如果有,说明需要在可用的站点之间平均分配
        List<Integer> unavailableSites = new ArrayList<>(siteIntList);
        unavailableSites.removeAll(sites);
        if (!unavailableSites.isEmpty()) {
            log.info("任务ID:{},{}站点组中有{}个站点不可用(in_enable!='Y'):{},将在{}个可用站点之间平均分配",
                displayTaskId, groupKey.equals("east") ? agvProperties.getEastDisplayName() : agvProperties.getWestDisplayName(),
                unavailableSites.size(), unavailableSites, sites.size());
        }
        if (sites.isEmpty()) {
            String errorMsg = "没有能入站点(in_enable='Y')";
            log.warn("任务ID:{},{},候选站点列表:{},站点状态:{}", displayTaskId, errorMsg, targetStations, siteStatusInfo.toString());
            return errorMsg;
        }
        // 先检查站点配置(canining="Y"可入),排除dev_no=0的无效站点
        List<BasDevp> devListWithConfig = basDevpMapper.selectList(new EntityWrapper<BasDevp>()
                .in("dev_no", sites)
                .in("dev_no", siteIntList)
                .eq("in_enable", "Y")
                .eq("canining", "Y")
                .eq("loading", "N")
@@ -893,8 +780,8 @@
        );
        if (devListWithConfig==null || devListWithConfig.isEmpty()) {
            log.warn("任务ID:{}没有可入站点(站点未开通可入允许:canining='Y'),暂不分配站点,等待配置开通。能入站点列表:{}",
                    displayTaskId, sites);
            log.warn("任务ID:{}没有可入站点(站点未开通可入允许:canining='Y'),暂不分配站点,等待配置开通。站点列表:{}",
                    displayTaskId, targetStations);
            return null;
        }
@@ -1042,8 +929,10 @@
        log.info("任务ID:{}已分配站点:{},机器人组:{},任务类型:{}", displayTaskId, endSite, robotGroup, taskTypeName);
        return null; // 分配成功,返回null
    }
    /**
     * 根据站点编号判断机器人组
     *
     * @param staNo 站点编号
     * @return 机器人组名称
     */
@@ -1134,11 +1023,13 @@
        log.info("agv任务档转历史成功:{}", taskIds);
    }
    @Transactional(rollbackFor = Exception.class)
    public void moveTaskToHistory(Task agvTask) {
        moveTaskToHistory(Collections.singletonList(agvTask));
    }
    /**
     * 货物到达出库口,生成agv任务
     */
@@ -1165,6 +1056,7 @@
    /**
     * 取消AGV任务(仙工M4接口)
     *
     * @param task 任务对象
     * @return 是否成功
     */