package com.zy.asrs.task.handler; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.core.common.Cools; import com.zy.asrs.entity.Task; 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.asrs.service.WrkMastService; import com.zy.asrs.service.WrkMastLogService; import com.zy.asrs.entity.WrkMastLog; import com.zy.common.constant.ApiInterfaceConstant; import com.zy.common.properties.AgvProperties; import com.zy.common.utils.HttpHandler; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** * @author pang.jiabao * @description AGV交互相关定时任务处理类 * @createDate 2025/11/18 14:42 */ @Slf4j @Service public class AgvHandler { @Resource private WrkMastMapper wrkMastMapper; @Resource private ApiLogService apiLogService; @Resource private TaskService taskService; @Resource private TaskLogService taskLogService; @Resource private BasStationMapper basStationMapper; @Resource private BasDevpMapper basDevpMapper; @Resource private LocCacheService locCacheService; @Resource private AgvProperties agvProperties; @Resource private WrkMastService wrkMastService; @Resource private WrkMastLogService wrkMastLogService; /** * 站点轮询计数器,用于平均分配站点 * Key: 站点组标识(如 "east" 或 "west"),Value: 当前轮询索引 */ private final Map siteRoundRobinCounters = new ConcurrentHashMap<>(); /** * 呼叫AGV * * 重要:此方法只能从 AgvScheduler.callAgv() 定时任务中调用! * 所有AGV呼叫请求必须通过定时任务统一处理,确保: * 1. 任务按顺序处理,避免并发冲突 * 2. 站点分配和AGV呼叫分离,职责清晰 * 3. 统一的错误处理和重试机制 * * @param taskList 任务列表(通常只包含一个任务) * @return 是否成功处理了任务(成功呼叫AGV,状态从7变为8) */ public boolean callAgv(List taskList) { // 记录调用堆栈,确保只能从定时任务调用 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); boolean calledFromScheduler = false; for (StackTraceElement element : stackTrace) { if (element.getClassName().contains("AgvScheduler") && element.getMethodName().equals("callAgv")) { calledFromScheduler = true; break; } } if (!calledFromScheduler) { log.error("严重错误:callAgv方法只能从AgvScheduler.callAgv()定时任务中调用!当前调用堆栈:{}", Arrays.stream(stackTrace).limit(5).map(StackTraceElement::toString).collect(Collectors.joining("\n"))); return false; } if (!agvProperties.isSendTask()) { log.warn("AGV呼叫:配置isSendTask=false,不发送AGV任务"); return false; } // 每次只处理一个任务,避免高并发执行 if (taskList == null || taskList.isEmpty()) { return false; } // 只处理第一个任务 Task task = taskList.get(0); // 呼叫AGV定时任务只处理已分配站点的任务,站点分配由单独的定时任务处理 String staNo = task.getStaNo(); String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId()); // 检查站点是否有效(不为空、不为空字符串、不为0) if (staNo == null || staNo.isEmpty() || staNo.equals("0")) { // 没有有效站点,跳过(站点分配由allocateSite定时任务处理) log.warn("定时任务callAgv:任务ID:{}没有有效站点分配(sta_no={}),跳过发送,等待分配站点定时任务处理", displayTaskId, staNo); return false; // 返回false,表示未成功处理 } // 检查站点是否有效(不为0且存在) try { Integer siteNo = Integer.parseInt(staNo); if (siteNo == null || siteNo == 0) { log.warn("定时任务callAgv:任务ID:{}的目标站点{}无效(为0),清空站点分配,将重新分配", displayTaskId, staNo); task.setStaNo(null); taskService.updateById(task); return false; // 返回false,让分配站点定时任务重新分配 } // 检查站点是否存在 List basDevpList = basDevpMapper.selectList(new EntityWrapper().eq("dev_no", siteNo)); if (basDevpList == null || basDevpList.isEmpty()) { log.warn("定时任务callAgv:任务ID:{}的目标站点{}不存在,清空站点分配,将重新分配", displayTaskId, staNo); task.setStaNo(null); taskService.updateById(task); return false; // 返回false,让分配站点定时任务重新分配 } } catch (NumberFormatException e) { // 站点格式错误,清空站点,让分配站点定时任务重新分配 log.warn("定时任务callAgv:任务ID:{}的目标站点{}格式错误,清空站点分配,将重新分配", displayTaskId, staNo); task.setStaNo(null); taskService.updateById(task); return false; // 返回false,让分配站点定时任务重新分配 } log.info("定时任务callAgv:任务ID:{}已有站点分配:{},准备发送AGV命令", displayTaskId, staNo); // 检查站点是否有状态8的同类型任务,有则跳过(不清空站点) // 规则:最多给每个站点分配1条任务,未完成则等待 // 判断任务是否完成:通过查询agv工作档(wrk_mast),如果查不到就是完成了 if (staNo != null && !staNo.isEmpty() && task.getIoType() != null) { // 根据当前任务类型,只检查同类型的正在搬运任务(状态8) // 入库任务(ioType < 100):只检查入库类型的正在搬运任务 // 出库任务(ioType >= 100):只检查出库类型的正在搬运任务 List 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 = "出库"; } // 通过SQL查询该站点所有AGV任务(包括状态7和8) // 规则:最多给每个站点分配1条任务,未完成则等待 // 判断任务是否完成:通过查询agv工作档(wrk_mast),如果查不到就是完成了 List allTasks = taskService.selectList( new EntityWrapper() .eq("sta_no", staNo) .eq("task_type", "agv") .in("wrk_sts", 7L, 8L) .in("io_type", ioTypes) .ne("id", task.getId()) // 排除当前任务本身 .eq("is_deleted", 0) // 排除已删除的任务 ); int taskCount = allTasks != null ? allTasks.size() : 0; log.info("定时任务:任务ID:{},站点{}查询到{}个AGV任务(排除当前任务),开始检查是否有未完成的任务", displayTaskId, staNo, taskCount); // 检查是否有有效的未完成任务(通过查询agv工作档判断) boolean hasValidTask = false; if (taskCount > 0) { for (Task agvTask : allTasks) { if (agvTask.getWrkNo() != null) { // 查询工作档,如果查不到就是完成了 WrkMast wrkMast = wrkMastService.selectOne( new EntityWrapper().eq("wrk_no", agvTask.getWrkNo()) ); if (wrkMast != null) { // 工作档存在,检查是否已完成 Long wrkSts = wrkMast.getWrkSts(); Integer ioType = agvTask.getIoType(); if (wrkSts != null && ioType != null) { // 入库任务:状态4或5表示完成 boolean isCompleted = false; if ((ioType == 1 || ioType == 10 || ioType == 53 || ioType == 57) && (wrkSts == 4L || wrkSts == 5L)) { isCompleted = true; } // 出库任务:状态14或15表示完成 else if ((ioType == 101 || ioType == 110 || ioType == 103 || ioType == 107) && (wrkSts == 14L || wrkSts == 15L)) { isCompleted = true; } if (!isCompleted) { // 工作档存在且未完成,视为有效任务 hasValidTask = true; log.info("定时任务:任务ID:{},站点{}已有1条未完成的{}AGV任务(工作号:{},工作档状态:{}),跳过当前任务,等待完成", displayTaskId, staNo, taskType, agvTask.getWrkNo(), wrkSts); break; } else { log.info("定时任务:任务ID:{},站点{}的任务{}(工作号:{})已完成(工作档状态:{}),不算有效任务", displayTaskId, staNo, agvTask.getId(), agvTask.getWrkNo(), wrkSts); } } else { // 工作档存在但状态未知,视为有效任务 hasValidTask = true; log.info("定时任务:任务ID:{},站点{}已有1条{}AGV任务(工作号:{},工作档状态未知),跳过当前任务,等待完成", displayTaskId, staNo, taskType, agvTask.getWrkNo()); break; } } else { // 如果工作档查不到,视为已完成,不算有效任务 log.info("定时任务:任务ID:{},站点{}的任务{}(工作号:{})工作档查不到,视为已完成,不算有效任务", displayTaskId, staNo, agvTask.getId(), agvTask.getWrkNo()); } } } } else { log.info("定时任务:任务ID:{},站点{}没有其他AGV任务,可以分配", displayTaskId, staNo); } // 如果站点有有效的未完成任务,跳过当前任务 if (hasValidTask) { log.warn("定时任务:任务ID:{},站点{}有未完成的{}AGV任务,跳过当前任务,等待完成。下次将重新尝试处理此任务", displayTaskId, staNo, taskType); return false; // 返回false,表示未成功处理,下次会重新尝试 } // 检查是否有状态8且已收到AGV确认的任务(用于日志记录) List transportingTasks = taskService.selectList( new EntityWrapper() .eq("sta_no", staNo) .eq("task_type", "agv") .eq("wrk_sts", 8L) // 只检查正在搬运(8)的任务 .isNotNull("plc_str_time") // 只检查已收到AGV确认的任务(plc_str_time不为空) .in("io_type", ioTypes) .ne("id", task.getId()) // 排除当前任务本身 .eq("is_deleted", 0) // 排除已删除的任务 ); // 检查并自动结束已完成工作档的AGV任务 int originalCount = transportingTasks.size(); List validTransportingTasks = checkAndCompleteFinishedTasks(transportingTasks, taskType); int completedCount = originalCount - validTransportingTasks.size(); if (completedCount > 0) { log.info("定时任务:站点{}自动结束了{}个已完成工作档的AGV任务,剩余{}个正在搬运的任务", staNo, completedCount, validTransportingTasks.size()); } if (!validTransportingTasks.isEmpty()) { List transportingTaskIds = validTransportingTasks.stream() .map(Task::getWrkNo) .filter(wrkNo -> wrkNo != null) // 过滤掉null值 .collect(Collectors.toList()); // 记录被占用任务的详细信息 StringBuilder taskDetailInfo = new StringBuilder(); for (Task t : validTransportingTasks) { if (taskDetailInfo.length() > 0) { taskDetailInfo.append("; "); } String tDisplayTaskId = (t.getWrkNo() != null) ? String.valueOf(t.getWrkNo()) : String.valueOf(t.getId()); taskDetailInfo.append("任务").append(tDisplayTaskId) .append("(wrk_no=").append(t.getWrkNo()) .append(",确认时间=").append(t.getPlcStrTime() != null ? t.getPlcStrTime().toString() : "未确认") .append(",创建时间=").append(t.getAppeTime() != null ? t.getAppeTime().toString() : "未知") .append(")"); } log.info("定时任务:站点{}有{}个正在搬运的{}AGV任务(工作号:{}),跳过当前任务ID:{},下次将重新尝试。任务详情:{}", staNo, validTransportingTasks.size(), taskType, transportingTaskIds, displayTaskId, taskDetailInfo.toString()); return false; // 返回false,表示未成功处理,下次会重新尝试 } else if (completedCount > 0) { // 所有占用任务都已结束,站点已释放,可以继续处理当前任务 log.info("定时任务:站点{}的所有占用任务已自动结束,站点已释放,可以分配给当前任务ID:{}", staNo, task.getId()); } } log.info("定时任务:任务ID:{},站点{}检查通过,准备发送AGV命令", displayTaskId, staNo); // 呼叫agv String response = ""; boolean success = false; String url = ApiInterfaceConstant.AGV_IP + ApiInterfaceConstant.AGV_CREATE_TASK_PATH; String namespace = ""; switch (task.getIoType()) { case 1: case 10: case 53: case 57: namespace = "入库"; break; case 3: namespace = "转移"; break; case 101: case 110: case 103: case 107: namespace = "出库"; break; 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); 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接口 response = new HttpHandler.Builder() .setUri(ApiInterfaceConstant.AGV_IP) .setPath(ApiInterfaceConstant.AGV_CREATE_TASK_PATH) .setJson(body) .build() .doPost(); // 打印返回参数 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); } } else { // 尝试解析JSON响应,捕获JSON解析异常 JSONObject jsonObject = null; try { jsonObject = JSON.parseObject(response); } catch (com.alibaba.fastjson.JSONException e) { // 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 basDevpList = basDevpMapper.selectList(new EntityWrapper().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.setErrorTime(new Date()); task.setErrorMemo(String.format("AGV接口返回服务器错误,站点已标记为不可用:%s", errorMsg)); taskService.updateById(task); handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount); // 服务器错误时,不再尝试分配,直接标记为已处理 result = true; // 返回true表示已处理(虽然失败),不再尝试分配 log.info("定时任务:任务ID:{},AGV呼叫失败(服务器错误),站点已标记为不可用,任务已标记为失败,不再尝试分配", displayTaskId); } // 如果JSON解析成功,继续处理 if (jsonObject != null) { Integer code = jsonObject.getInteger("code"); if (code != null && code.equals(200)) { // 呼叫成功,清除重试次数和错误信息 success = true; // 如果当前状态不是8,更新为8;如果已经是8,保持不变(可能是重试成功) 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.setMemo(clearRetryInfo(task.getMemo())); // 清除重试信息 task.setErrorTime(null); task.setErrorMemo(null); taskService.updateById(task); log.info("定时任务:{}呼叫agv搬运成功 - 任务ID:{}", namespace, displayTaskId); result = true; // 返回true,表示成功处理 } else { String message = jsonObject.getString("message"); 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); 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); } } } } } } catch (Exception e) { 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); } else { result = false; // 返回false,让定时任务重新尝试 log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId); } } finally { try { // 保存接口日志 apiLogService.save( namespace + "呼叫agv搬运", url, null, "127.0.0.1", body, response, success ); } catch (Exception e) { // log.error(namespace + "呼叫agv保存接口日志异常:", e); } } return result; } /** * 处理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); } } /** * 检查并自动结束已完成工作档的AGV任务 * 如果任务对应的工作档已经完成(入库成功),则自动结束该AGV任务 * @param transportingTasks 正在搬运的任务列表 * @param taskTypeName 任务类型名称(用于日志) * @return 仍然有效的正在搬运的任务列表(已完成的已被移除) */ @Transactional(rollbackFor = Exception.class) public List checkAndCompleteFinishedTasks(List transportingTasks, String taskTypeName) { if (transportingTasks == null || transportingTasks.isEmpty()) { return transportingTasks; } List validTasks = new ArrayList<>(); Date now = new Date(); for (Task agvTask : transportingTasks) { boolean isCompleted = false; String reason = ""; // 检查工作档是否存在 WrkMast wrkMast = null; if (agvTask.getWrkNo() != null) { wrkMast = wrkMastService.selectOne( new EntityWrapper().eq("wrk_no", agvTask.getWrkNo()) ); } // 检查历史档是否存在 WrkMastLog wrkMastLog = null; if (agvTask.getWrkNo() != null) { wrkMastLog = wrkMastLogService.selectOne( new EntityWrapper().eq("wrk_no", agvTask.getWrkNo()) ); } // 如果通过wrk_no没找到,且有条码,则通过条码查询 if (wrkMastLog == null && !Cools.isEmpty(agvTask.getBarcode())) { List logList = wrkMastLogService.selectList( new EntityWrapper().eq("barcode", agvTask.getBarcode()) ); if (!logList.isEmpty()) { wrkMastLog = logList.get(0); } } // 如果工作档存在,检查是否已完成 if (wrkMast != null) { Long wrkSts = wrkMast.getWrkSts(); Integer ioType = agvTask.getIoType(); if (wrkSts != null && ioType != null) { // 入库任务:状态4(入库完成)或5(库存更新完成) if ((ioType == 1 || ioType == 10 || ioType == 53 || ioType == 57) && (wrkSts == 4L || wrkSts == 5L)) { isCompleted = true; reason = String.format("工作档已完成,状态:%d", wrkSts); } // 出库任务:状态14(已出库未确认)或15(出库更新完成) else if ((ioType == 101 || ioType == 110 || ioType == 103 || ioType == 107) && (wrkSts == 14L || wrkSts == 15L)) { isCompleted = true; reason = String.format("工作档已完成,状态:%d", wrkSts); } } } // 1. 如果工作档进入历史档,立即结束AGV任务(只要历史档存在就结束) if (!isCompleted && wrkMastLog != null) { isCompleted = true; reason = String.format("工作档已转历史档,立即结束AGV任务,历史档状态:%d", wrkMastLog.getWrkSts()); } // 如果已完成,更新AGV任务状态为完成 if (isCompleted) { agvTask.setWrkSts(9L); agvTask.setModiTime(now); if (taskService.updateById(agvTask)) { // taskId使用工作号(wrk_no),如果工作号为空则使用任务ID String displayTaskId = (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId()); log.info("{},自动结束AGV任务,taskId:{},wrkNo:{},barcode:{},站点:{},释放站点供新任务使用", reason, displayTaskId, agvTask.getWrkNo(), agvTask.getBarcode(), agvTask.getStaNo()); // 转移到历史表(会自动减少站点的in_qty) try { moveTaskToHistory(Collections.singletonList(agvTask)); log.info("自动结束AGV任务后已转移到历史表,taskId:{},站点:{}已释放", displayTaskId, agvTask.getStaNo()); } catch (Exception e) { log.error("自动结束AGV任务后转移历史表失败,taskId:{}", displayTaskId, e); } } else { log.error("自动结束AGV任务失败,更新任务状态失败,taskId:{}", (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId())); } } else { // 任务仍然有效,保留在列表中 // 记录任务仍然有效的原因(用于调试) String displayTaskId = (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId()); if (wrkMast == null && wrkMastLog == null) { log.debug("任务ID:{}(站点:{})仍然有效:工作档和历史档都不存在,可能工作档还未创建", displayTaskId, agvTask.getStaNo()); } else if (wrkMast != null) { log.debug("任务ID:{}(站点:{})仍然有效:工作档状态={},任务类型={},未达到完成条件", displayTaskId, agvTask.getStaNo(), wrkMast.getWrkSts(), agvTask.getIoType()); } validTasks.add(agvTask); } } return validTasks; } /** * 从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; } /** * 构造请求内容(仙工M4格式) */ public String getRequest(Task task, String nameSpace) { JSONObject object = new JSONObject(); // taskId使用工作号(wrk_no),格式:T + 工作号 // 如果工作号为空,则使用任务ID作为备选 String taskIdValue = (task.getWrkNo() != null) ? "T" + task.getWrkNo() : "T" + task.getId(); object.put("taskId", taskIdValue); // fromBin使用源库位编号(sourceLocNo),如果为空则使用源站点编号(sourceStaNo)作为备选 String fromBin = task.getSourceLocNo(); if (fromBin == null || fromBin.isEmpty()) { fromBin = task.getSourceStaNo(); } if (fromBin == null || fromBin.isEmpty() || "0".equals(fromBin)) { // log.warn("任务{}的源库位和源站点都为空,使用默认值", task.getId()); fromBin = "0"; } object.put("fromBin", fromBin); // toBin使用目标站点编号 object.put("toBin", task.getStaNo()); // robotGroup从invWh字段获取,如果没有则根据站点编号判断 String robotGroup = task.getInvWh(); if (robotGroup == null || robotGroup.isEmpty()) { robotGroup = determineRobotGroupByStation(task.getStaNo()); } object.put("robotGroup", robotGroup); // kind根据任务类型映射 String kind = ""; switch (nameSpace) { case "入库": // 判断是否为空托入库:ioType=10 或 emptyMk="Y" if (task.getIoType() == 10 || "Y".equals(task.getEmptyMk())) { kind = "空托入库"; } else { kind = "实托入库"; } break; case "出库": kind = "实托出库"; break; case "转移": kind = "货物转运"; break; default: kind = "货物转运"; } object.put("kind", kind); return object.toJSONString(); } /** * 为任务分配站点(定时任务中调用) * 注意:只会分配一个站点,找到第一个符合条件的站点就分配并退出 * @param task 任务对象 * @return 如果无法分配站点,返回错误信息;如果分配成功,返回null并更新task的staNo */ @Transactional(rollbackFor = Exception.class) public String allocateSiteForTask(Task task) { // taskId使用工作号(wrk_no),如果工作号为空则使用任务ID String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId()); log.debug("开始为任务ID:{}分配站点,任务类型:{},机器人组:{}", displayTaskId, task.getIoType(), task.getInvWh()); // 根据任务的invWh(机器人组)判断是东侧还是西侧 String robotGroup = task.getInvWh(); List targetStations; String groupKey; if (robotGroup != null && robotGroup.equals(agvProperties.getRobotGroupEast())) { // 东侧站点 targetStations = agvProperties.getEastStations(); groupKey = "east"; } else if (robotGroup != null && robotGroup.equals(agvProperties.getRobotGroupWest())) { // 西侧站点 targetStations = agvProperties.getWestStations(); groupKey = "west"; } else { // 默认使用东侧 targetStations = agvProperties.getEastStations(); groupKey = "east"; log.warn("任务ID:{}的机器人组{}未识别,使用默认东侧站点", displayTaskId, robotGroup); } if (targetStations.isEmpty()) { String errorMsg = "没有可用的目标站点配置"; log.warn("任务ID:{},{}", displayTaskId, errorMsg); return errorMsg; } // 将站点字符串列表转换为整数列表 List siteIntList = targetStations.stream() .map(Integer::parseInt) .collect(Collectors.toList()); log.info("任务ID:{},{}站点组配置的站点:{},共{}个站点", displayTaskId, groupKey.equals("east") ? agvProperties.getEastDisplayName() : agvProperties.getWestDisplayName(), targetStations, targetStations.size()); // 判断能入站点(in_enable="Y"表示能入),排除dev_no=0的无效站点 List allDevList = basDevpMapper.selectList( new EntityWrapper() .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 sites = allDevList.stream() .filter(dev -> "Y".equals(dev.getInEnable())) .map(BasDevp::getDevNo) .filter(devNo -> devNo != null && devNo != 0) // 再次过滤,确保不为null或0 .collect(Collectors.toList()); // 检查是否有站点不可用,如果有,说明需要在可用的站点之间平均分配 List 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 devListWithConfig = basDevpMapper.selectList(new EntityWrapper() .in("dev_no", sites) .eq("in_enable", "Y") .eq("canining", "Y") .eq("loading", "N") .ne("dev_no", 0) // 排除dev_no=0的无效站点 ); if (devListWithConfig==null || devListWithConfig.isEmpty()) { log.warn("任务ID:{}没有可入站点(站点未开通可入允许:canining='Y'),暂不分配站点,等待配置开通。能入站点列表:{}", displayTaskId, sites); return null; } // 先按规则排序(入库任务数排序) devListWithConfig.sort(Comparator.comparing(BasDevp::getInQty)); // 根据任务类型确定要检查的io_type列表 Integer taskIoType = task.getIoType(); List 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 = "出库"; } } // 先查询agv工作档中未被分配站点的站点 // 查询agv工作档中所有已分配站点的任务(sta_no不为空、不为空字符串、不为0) final List allocatedSiteNos; if (checkIoTypes != null && !checkIoTypes.isEmpty()) { List allocatedTasks = taskService.selectList( new EntityWrapper() .eq("task_type", "agv") .in("wrk_sts", 7L, 8L) // 待呼叫AGV和正在搬运的任务 .in("io_type", checkIoTypes) .isNotNull("sta_no") .ne("sta_no", "") .ne("sta_no", "0") .andNew("(is_deleted = 0)") ); // 获取已分配的站点编号列表 allocatedSiteNos = allocatedTasks.stream() .map(Task::getStaNo) .filter(staNo -> staNo != null && !staNo.isEmpty() && !staNo.equals("0")) .distinct() .collect(Collectors.toList()); } else { allocatedSiteNos = new ArrayList<>(); } // 从可用站点中筛选出未被分配的站点 List unallocatedSites = devListWithConfig.stream() .filter(dev -> { String staNo = String.valueOf(dev.getDevNo()); return !allocatedSiteNos.contains(staNo); }) .collect(Collectors.toList()); List unallocatedSites2= new ArrayList<>(); for(int i=0;devListWithConfig.size()>i;i++){ unallocatedSites2.add(devListWithConfig.get(i)); } // if(unallocatedSites==null || unallocatedSites.isEmpty()){ // unallocatedSites=unallocatedSites2; // // } // 只使用未分配站点 if (unallocatedSites.isEmpty()) { // 未分配站点为空:不分配站点 StringBuilder allocatedSitesInfo = new StringBuilder(); for (String staNo : allocatedSiteNos) { if (allocatedSitesInfo.length() > 0) { allocatedSitesInfo.append("; "); } allocatedSitesInfo.append("站点").append(staNo).append("已被分配"); } log.warn("任务ID:{},所有可用站点都已被分配,暂不分配站点,等待下次定时任务再尝试。已分配站点:{}", displayTaskId, allocatedSitesInfo.length() > 0 ? allocatedSitesInfo.toString() : "无详细信息"); return null; // 返回null,表示暂不分配,等待下次定时任务再尝试 } // 存在未分配站点:根据配置的分配策略选择具体站点 // 先按规则排序(入库任务数排序) unallocatedSites.sort(Comparator.comparing(BasDevp::getInQty)); // 根据配置选择分配策略,确定优先分配的站点顺序 List orderedSites = new ArrayList<>(); String strategy = agvProperties.getSiteAllocation().getStrategy(); boolean enableRoundRobin = agvProperties.getSiteAllocation().isEnableRoundRobin(); // 记录是否使用轮询策略,以及轮询计数器(用于在成功分配站点后递增) AtomicInteger roundRobinCounter = null; int roundRobinStartIndex = 0; if (unallocatedSites.size() > 1 && enableRoundRobin && "round-robin".equals(strategy)) { // 轮询分配:先按轮询策略排序 roundRobinCounter = siteRoundRobinCounters.computeIfAbsent(groupKey, k -> new AtomicInteger(0)); roundRobinStartIndex = roundRobinCounter.get() % unallocatedSites.size(); // 获取当前索引,但不递增(成功分配后再递增) // 将轮询选中的站点放在最前面 orderedSites.addAll(unallocatedSites.subList(roundRobinStartIndex, unallocatedSites.size())); orderedSites.addAll(unallocatedSites.subList(0, roundRobinStartIndex)); log.info("任务ID:{},使用轮询分配策略,站点组:{},轮询起始索引:{},候选站点:{}(共{}个未分配站点)", displayTaskId, groupKey, roundRobinStartIndex, unallocatedSites.stream().map(d -> String.valueOf(d.getDevNo())).collect(Collectors.joining(",")), unallocatedSites.size()); } else if (unallocatedSites.size() > 1 && enableRoundRobin && "random".equals(strategy)) { // 随机分配:先随机排序未分配站点 List shuffledSites = new ArrayList<>(unallocatedSites); Collections.shuffle(shuffledSites); orderedSites.addAll(shuffledSites); log.info("任务ID:{},使用随机分配策略,候选站点:{}", displayTaskId, unallocatedSites.stream().map(d -> String.valueOf(d.getDevNo())).collect(Collectors.joining(","))); } else { // 默认:按入库任务数排序(已经排序好了) orderedSites = unallocatedSites; } // 既然已经筛选出了未分配站点,直接根据分配策略选择第一个站点即可 // 未分配站点在AGV工作档中都没有已分配的任务,可以直接分配 BasDevp selectedSite = orderedSites.get(0); Integer endSite = selectedSite.getDevNo(); String staNo = String.valueOf(endSite); log.info("任务ID:{},从{}个未分配站点中选择站点{}(入库任务数:{}),候选站点:{}", displayTaskId, orderedSites.size(), staNo, selectedSite.getInQty(), orderedSites.stream().map(d -> String.valueOf(d.getDevNo())).collect(Collectors.joining(","))); // 如果使用轮询策略且成功分配站点,递增轮询计数器(确保下次从下一个站点开始) if (roundRobinCounter != null && unallocatedSites.size() > 1) { roundRobinCounter.getAndIncrement(); log.debug("任务ID:{}成功分配到站点{},轮询计数器已递增,下次将从下一个站点开始轮询", displayTaskId, staNo); } // 检查站点是否有效(不能为0或null) if (endSite == null || endSite == 0) { String errorMsg = String.format("分配的站点无效(dev_no=%s)", endSite); log.error("任务ID:{},{}", displayTaskId, errorMsg); return errorMsg; } // 入库暂存+1 basDevpMapper.incrementInQty(endSite); // 更新任务的站点编号,并确保状态为7(待呼叫AGV) task.setStaNo(String.valueOf(endSite)); if (task.getWrkSts() == null || task.getWrkSts() != 7L) { task.setWrkSts(7L); // 确保状态为7(待呼叫AGV) log.debug("任务ID:{}分配站点时,状态不是7,已更新为7(待呼叫AGV)", displayTaskId); } taskService.updateById(task); log.info("任务ID:{}已分配站点:{},机器人组:{},任务类型:{}", displayTaskId, endSite, robotGroup, taskTypeName); return null; // 分配成功,返回null } /** * 根据站点编号判断机器人组 * @param staNo 站点编号 * @return 机器人组名称 */ private String determineRobotGroupByStation(String staNo) { if (staNo == null || staNo.isEmpty()) { return agvProperties.getRobotGroupEast(); // 默认使用东侧机器人组 } // 从配置中获取站点列表 Set eastStations = new HashSet<>(agvProperties.getEastStations()); Set westStations = new HashSet<>(agvProperties.getWestStations()); // 判断站点属于哪一侧 if (eastStations.contains(staNo)) { return agvProperties.getRobotGroupEast(); // 东侧机器人 } else if (westStations.contains(staNo)) { return agvProperties.getRobotGroupWest(); // 西侧机器人 } else { log.warn("站点编号不在配置列表中,使用默认机器人组:{}", staNo); return agvProperties.getRobotGroupEast(); // 默认使用东侧机器人组 } } /** * 任务完成转历史 释放暂存点 */ @Transactional(rollbackFor = Exception.class) public void moveTaskToHistory(List taskList) { // 写入历史表,保持ID一致 for (Task task : taskList) { TaskLog log = new TaskLog(); BeanUtils.copyProperties(task, log); // 保持ID一致,不设置为null taskLogService.insert(log); } // 批量删除原任务 List taskIds = taskList.stream().map(Task::getId).collect(Collectors.toList()); taskService.delete(new EntityWrapper().in("id",taskIds)); // 批量更新暂存点状态 List locOList = new ArrayList<>(); List locFList = new ArrayList<>(); // 收集需要减少in_qty的站点(入库任务) Set sitesToDecrement = new HashSet<>(); for (Task task : taskList) { String sourceStaNo = task.getSourceStaNo(); String staNo = task.getStaNo(); if (task.getIoType() == 3) { locOList.add(sourceStaNo); locFList.add(staNo); } else if (task.getIoType() < 100) { // 入库任务:减少目标站点的in_qty locOList.add(sourceStaNo); if (staNo != null && !staNo.isEmpty()) { try { Integer siteNo = Integer.parseInt(staNo); if (siteNo != null && siteNo > 0) { sitesToDecrement.add(siteNo); } } catch (NumberFormatException e) { log.warn("任务ID:{}的站点编号格式错误:{},跳过减少in_qty", task.getId(), staNo); } } } else { locFList.add(staNo); } } // 减少站点的入库任务数(in_qty) for (Integer siteNo : sitesToDecrement) { try { basDevpMapper.decrementInQty(siteNo); log.debug("任务转移到历史表,站点{}的in_qty已减少", siteNo); } catch (Exception e) { log.error("任务转移到历史表,减少站点{}的in_qty失败", siteNo, e); } } if (!locOList.isEmpty()) { basStationMapper.updateLocStsBatch(locOList, "O"); } if (!locFList.isEmpty()) { basStationMapper.updateLocStsBatch(locFList, "F"); } log.info("agv任务档转历史成功:{}", taskIds); } @Transactional(rollbackFor = Exception.class) public void moveTaskToHistory(Task agvTask) { moveTaskToHistory(Collections.singletonList(agvTask)); } /** * 货物到达出库口,生成agv任务 */ public void createAgvOutTasks(List sites) { // 获取到可用出库站点的任务 List wrkMastList = wrkMastMapper.selectList(new EntityWrapper().eq("call_agv", 1).in("sta_no",sites)); for(WrkMast wrkMast:wrkMastList) { // todo 计算agv目标暂存位 int endSite = 2004; // 插入agv任务 Task task = new Task(wrkMast.getWrkNo(), 7L, wrkMast.getIoType(), String.valueOf(wrkMast.getStaNo()), String.valueOf(endSite), null, wrkMast.getBarcode()); taskService.insert(task); // 更新任务档agv搬运标识 wrkMast.setCallAgv(2); wrkMastMapper.updateById(wrkMast); // 更新暂存位状态 S.入库预约 basStationMapper.updateLocStsBatch( Collections.singletonList(String.valueOf(endSite)), "S"); } } /** * 取消AGV任务(仙工M4接口) * @param task 任务对象 * @return 是否成功 */ public boolean cancelAgvTask(Task task) { if (!agvProperties.isSendTask()) { return false; } if (task == null || task.getId() == null) { log.error("取消AGV任务失败:任务或任务ID为空"); return false; } String response = ""; boolean success = false; String url = ApiInterfaceConstant.AGV_IP + ApiInterfaceConstant.AGV_CANCEL_TASK_PATH; String namespace = ""; String kind = ""; // 根据任务类型确定kind和namespace switch (task.getIoType()) { case 1: case 10: case 53: case 57: namespace = "入库"; kind = "实托入库"; break; case 3: namespace = "转移"; kind = "货物转运"; break; case 101: case 110: case 103: case 107: namespace = "出库"; kind = "实托出库"; break; default: kind = "货物转运"; } // 构造取消任务请求 JSONObject cancelRequest = new JSONObject(); // taskId使用工作号(wrk_no),格式:T + 工作号 // 如果工作号为空,则使用任务ID作为备选 String taskIdValue = (task.getWrkNo() != null) ? "T" + task.getWrkNo() : "T" + task.getId(); cancelRequest.put("taskId", taskIdValue); cancelRequest.put("kind", kind); String body = cancelRequest.toJSONString(); try { response = new HttpHandler.Builder() .setUri(ApiInterfaceConstant.AGV_IP) .setPath(ApiInterfaceConstant.AGV_CANCEL_TASK_PATH) .setJson(body) .build() .doPost(); JSONObject jsonObject = JSON.parseObject(response); if (jsonObject.getInteger("code") != null && jsonObject.getInteger("code").equals(200)) { success = true; // taskId使用工作号(wrk_no),如果工作号为空则使用任务ID String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId()); log.info(namespace + "取消AGV任务成功:{}", displayTaskId); } else { log.error(namespace + "取消AGV任务失败!!!url:{};request:{};response:{}", url, body, response); } } catch (Exception e) { log.error(namespace + "取消AGV任务异常", e); } finally { try { // 保存接口日志 apiLogService.save( namespace + "取消AGV任务", url, null, "127.0.0.1", body, response, success ); } catch (Exception e) { log.error(namespace + "取消AGV任务保存接口日志异常:", e); } } return success; } }