自动化立体仓库 - 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,34 +78,86 @@
        }
        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任务
            // 如果站点有未完成的任务,则跳过本次发送,等待下次
            // 检查目标站点是否有效(不为0且存在)
            if (staNo != null && !staNo.isEmpty()) {
                List<Task> unfinishedTasks = taskService.selectList(new EntityWrapper<Task>()
                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):只检查入库类型的正在搬运任务
                // 出库任务(ioType >= 100):只检查出库类型的正在搬运任务
                List<Integer> ioTypes;
                String taskType;
                if (task.getIoType() < 100) {
                    // 入库任务:只检查入库类型(1, 10, 53, 57)
                    ioTypes = Arrays.asList(1, 10, 53, 57);
                    taskType = "入库";
                } else {
                    // 出库任务:只检查出库类型(101, 110, 103, 107)
                    ioTypes = Arrays.asList(101, 110, 103, 107);
                    taskType = "出库";
                }
                // 检查状态为8(已呼叫AGV,正在搬运)的同类型任务
                List<Task> transportingTasks = taskService.selectList(
                    new EntityWrapper<Task>()
                        .eq("sta_no", staNo)
                        .eq("task_type", "agv")
                        .eq("wrk_sts", 8L) // 只检查正在搬运状态的任务
                        .in("io_type", ioTypes)
                        .ne("id", task.getId()) // 排除当前任务本身
                        .last("AND wrk_sts NOT IN (5, 15)") // 排除已完成状态
                );
                
                if (!unfinishedTasks.isEmpty()) {
                    log.info("站点{}有{}个未完成的AGV任务,跳过本次发送,等待任务完成。当前任务ID:{}",
                            staNo, unfinishedTasks.size(), task.getId());
                    continue; // 跳过本次发送,等待下次
                if (!transportingTasks.isEmpty()) {
                    log.info("站点{}有{}个正在搬运的{}AGV任务,跳过当前任务ID:{}",
                            staNo, transportingTasks.size(), taskType, task.getId());
                    continue;
                }
            }
            
@@ -128,9 +185,31 @@
                default:
            }
            String body = getRequest(task,namespace);
            // 打印请求信息
            log.info("{}呼叫agv搬运 - 请求地址:{}", namespace, url);
            // 获取当前重试次数
            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()
@@ -144,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 {
                    // 保存接口日志
@@ -185,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;
    }
    /**
@@ -239,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;
@@ -263,8 +453,9 @@
        }
        
        if (targetStations.isEmpty()) {
            log.warn("任务ID:{}没有可用的目标站点配置", task.getId());
            return null;
            String errorMsg = "没有可用的目标站点配置";
            log.warn("任务ID:{}", errorMsg, task.getId());
            return errorMsg;
        }
        
        // 将站点字符串列表转换为整数列表
@@ -272,77 +463,159 @@
                .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;
        }
        
        // 入库任务数排序
        // 先按规则排序(入库任务数排序)
        devList.sort(Comparator.comparing(BasDevp::getInQty));
        
        // 选择站点
        BasDevp basDevp;
        int minInQty = devList.get(0).getInQty();
        // 根据任务类型确定要检查的io_type列表
        Integer taskIoType = task.getIoType();
        List<Integer> checkIoTypes = null;
        String taskTypeName = "";
        if (taskIoType != null) {
            if (taskIoType < 100) {
                // 入库任务:只检查入库类型(1, 10, 53, 57)
                checkIoTypes = Arrays.asList(1, 10, 53, 57);
                taskTypeName = "入库";
            } else {
                // 出库任务:只检查出库类型(101, 110, 103, 107)
                checkIoTypes = Arrays.asList(101, 110, 103, 107);
                taskTypeName = "出库";
            }
        }
        
        // 筛选出任务数最少的站点列表
        // 筛选出任务数最少的站点列表(按规则排序后的候选站点)
        int minInQty = devList.get(0).getInQty();
        List<BasDevp> minTaskSites = devList.stream()
                .filter(dev -> dev.getInQty() == minInQty)
                .collect(Collectors.toList());
        
        // 根据配置选择分配策略
        // 根据配置选择分配策略,确定优先分配的站点顺序
        List<BasDevp> orderedSites = new ArrayList<>();
        String strategy = agvProperties.getSiteAllocation().getStrategy();
        boolean enableRoundRobin = agvProperties.getSiteAllocation().isEnableRoundRobin();
        
        if (minTaskSites.size() > 1 && enableRoundRobin && "round-robin".equals(strategy)) {
            // 轮询分配
            // 轮询分配:先按轮询策略排序
            AtomicInteger counter = siteRoundRobinCounters.computeIfAbsent(groupKey, k -> new AtomicInteger(0));
            int index = counter.getAndIncrement() % minTaskSites.size();
            basDevp = minTaskSites.get(index);
            log.info("使用轮询分配策略,站点组:{},轮询索引:{},选中站点:{}", groupKey, index, basDevp.getDevNo());
            int startIndex = counter.get() % minTaskSites.size();
            // 将轮询选中的站点放在最前面
            orderedSites.addAll(minTaskSites.subList(startIndex, minTaskSites.size()));
            orderedSites.addAll(minTaskSites.subList(0, startIndex));
            // 添加其他站点(任务数更多的)
            orderedSites.addAll(devList.stream()
                    .filter(dev -> dev.getInQty() > minInQty)
                    .collect(Collectors.toList()));
            log.debug("使用轮询分配策略,站点组:{},轮询起始索引:{}", groupKey, startIndex);
        } else if (minTaskSites.size() > 1 && enableRoundRobin && "random".equals(strategy)) {
            // 随机分配
            Random random = new Random();
            int index = random.nextInt(minTaskSites.size());
            basDevp = minTaskSites.get(index);
            log.info("使用随机分配策略,选中站点:{}", basDevp.getDevNo());
            // 随机分配:先随机排序任务数最少的站点
            List<BasDevp> shuffledMinSites = new ArrayList<>(minTaskSites);
            Collections.shuffle(shuffledMinSites);
            orderedSites.addAll(shuffledMinSites);
            // 添加其他站点(任务数更多的)
            orderedSites.addAll(devList.stream()
                    .filter(dev -> dev.getInQty() > minInQty)
                    .collect(Collectors.toList()));
            log.debug("使用随机分配策略");
        } else {
            // 默认:选择第一个(任务最少的)
            basDevp = devList.get(0);
            // 默认:按入库任务数排序(已经排序好了)
            orderedSites = devList;
        }
        
        Integer endSite = basDevp.getDevNo();
        // 依次检查每个站点是否在搬运,找到第一个空闲站点就分配
        BasDevp selectedSite = null;
        for (BasDevp dev : orderedSites) {
            String staNo = String.valueOf(dev.getDevNo());
            // 如果任务类型不为空,检查该站点是否有正在搬运的同类型任务
            boolean isTransporting = false;
            if (checkIoTypes != null && !checkIoTypes.isEmpty()) {
                List<Task> transportingTasks = taskService.selectList(
                    new EntityWrapper<Task>()
                        .eq("sta_no", staNo)
                        .eq("task_type", "agv")
                        .eq("wrk_sts", 8L) // 只检查正在搬运状态的任务
                        .in("io_type", checkIoTypes)
                );
                isTransporting = !transportingTasks.isEmpty();
                if (isTransporting) {
                    log.debug("站点{}有{}个正在搬运的{}AGV任务,检查下一个站点",
                        staNo, transportingTasks.size(), taskTypeName);
                    continue; // 该站点正在搬运,检查下一个站点
                }
            }
            // 找到第一个空闲站点,分配
            selectedSite = dev;
            log.info("任务ID:{}按规则应分配到站点{},该站点空闲,分配成功", task.getId(), staNo);
            break;
        }
        // 如果所有站点都在搬运,则不分配站点(只在定时任务中记录日志,不返回错误信息)
        if (selectedSite == 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
    }
    /**
@@ -376,10 +649,11 @@
    @Transactional(rollbackFor = Exception.class)
    public void moveTaskToHistory(List<Task> taskList) {
        // 写入历史表
        // 写入历史表,保持ID一致
        for (Task task : taskList) {
            TaskLog log = new TaskLog();
            BeanUtils.copyProperties(task, log);
            // 保持ID一致,不设置为null
            taskLogService.insert(log);
        }