| | |
| | | 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 org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | |
| | | private BasStationMapper basStationMapper; |
| | | |
| | | @Resource |
| | | private BasDevpMapper basDevpMapper; |
| | | |
| | | @Resource |
| | | private LocCacheService locCacheService; |
| | | |
| | | @Resource |
| | | private AgvProperties agvProperties; |
| | | |
| | | @Resource |
| | | private WrkMastService wrkMastService; |
| | | |
| | | @Resource |
| | | private WrkMastLogService wrkMastLogService; |
| | | |
| | | /** |
| | | * 呼叫agv搬运 |
| | | * 站点轮询计数器,用于平均分配站点 |
| | | * Key: 站点组标识(如 "east" 或 "west"),Value: 当前轮询索引 |
| | | */ |
| | | public void callAgv(List<Task> taskList) { |
| | | private final Map<String, AtomicInteger> siteRoundRobinCounters = new ConcurrentHashMap<>(); |
| | | |
| | | /** |
| | | * 呼叫AGV |
| | | * |
| | | * 重要:此方法只能从 AgvScheduler.callAgv() 定时任务中调用! |
| | | * 所有AGV呼叫请求必须通过定时任务统一处理,确保: |
| | | * 1. 任务按顺序处理,避免并发冲突 |
| | | * 2. 站点分配和AGV呼叫分离,职责清晰 |
| | | * 3. 统一的错误处理和重试机制 |
| | | * |
| | | * @param taskList 任务列表(通常只包含一个任务) |
| | | * @return 是否成功处理了任务(成功呼叫AGV,状态从7变为8) |
| | | */ |
| | | public boolean callAgv(List<Task> 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()) { |
| | | return; |
| | | log.warn("AGV呼叫:配置isSendTask=false,不发送AGV任务"); |
| | | return false; |
| | | } |
| | | |
| | | for (Task task : taskList) { |
| | | // 呼叫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: |
| | | // 每次只处理一个任务,避免高并发执行 |
| | | 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,让分配站点定时任务重新分配 |
| | | } |
| | | String body = getRequest(task,namespace); |
| | | try { |
| | | // 使用仙工M4接口 |
| | | response = new HttpHandler.Builder() |
| | | .setUri(ApiInterfaceConstant.AGV_IP) |
| | | .setPath(ApiInterfaceConstant.AGV_CREATE_TASK_PATH) |
| | | .setJson(body) |
| | | .build() |
| | | .doPost(); |
| | | JSONObject jsonObject = JSON.parseObject(response); |
| | | if (jsonObject.getInteger("code").equals(200)) { |
| | | success = true; |
| | | task.setWrkSts(8L); |
| | | taskService.updateById(task); |
| | | log.info(namespace + "呼叫agv搬运请求参数:{}", body); |
| | | log.info(namespace + "呼叫agv搬运成功:{}", task.getId()); |
| | | } else { |
| | | log.error(namespace + "呼叫agv搬运失败!!!url:{};request:{};response:{}", url, body, response); |
| | | // 检查站点是否存在 |
| | | List<BasDevp> basDevpList = basDevpMapper.selectList(new EntityWrapper<BasDevp>().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<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 = "出库"; |
| | | } |
| | | |
| | | // 通过SQL查询该站点所有AGV任务(包括状态7和8) |
| | | // 规则:最多给每个站点分配1条任务,未完成则等待 |
| | | // 判断任务是否完成:通过查询agv工作档(wrk_mast),如果查不到就是完成了 |
| | | List<Task> allTasks = taskService.selectList( |
| | | new EntityWrapper<Task>() |
| | | .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<WrkMast>().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()); |
| | | } |
| | | } |
| | | } |
| | | } 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); |
| | | } else { |
| | | log.info("定时任务:任务ID:{},站点{}没有其他AGV任务,可以分配", displayTaskId, staNo); |
| | | } |
| | | |
| | | // 如果站点有有效的未完成任务,跳过当前任务 |
| | | if (hasValidTask) { |
| | | log.warn("定时任务:任务ID:{},站点{}有未完成的{}AGV任务,跳过当前任务,等待完成。下次将重新尝试处理此任务", |
| | | displayTaskId, staNo, taskType); |
| | | return false; // 返回false,表示未成功处理,下次会重新尝试 |
| | | } |
| | | |
| | | // 检查是否有状态8且已收到AGV确认的任务(用于日志记录) |
| | | List<Task> transportingTasks = taskService.selectList( |
| | | new EntityWrapper<Task>() |
| | | .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<Task> validTransportingTasks = checkAndCompleteFinishedTasks(transportingTasks, taskType); |
| | | int completedCount = originalCount - validTransportingTasks.size(); |
| | | |
| | | if (completedCount > 0) { |
| | | log.info("定时任务:站点{}自动结束了{}个已完成工作档的AGV任务,剩余{}个正在搬运的任务", |
| | | staNo, completedCount, validTransportingTasks.size()); |
| | | } |
| | | |
| | | if (!validTransportingTasks.isEmpty()) { |
| | | List<Integer> 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<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.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<Task> checkAndCompleteFinishedTasks(List<Task> transportingTasks, String taskTypeName) { |
| | | if (transportingTasks == null || transportingTasks.isEmpty()) { |
| | | return transportingTasks; |
| | | } |
| | | |
| | | List<Task> 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<WrkMast>().eq("wrk_no", agvTask.getWrkNo()) |
| | | ); |
| | | } |
| | | |
| | | // 检查历史档是否存在 |
| | | WrkMastLog wrkMastLog = null; |
| | | if (agvTask.getWrkNo() != null) { |
| | | wrkMastLog = wrkMastLogService.selectOne( |
| | | new EntityWrapper<WrkMastLog>().eq("wrk_no", agvTask.getWrkNo()) |
| | | ); |
| | | } |
| | | // 如果通过wrk_no没找到,且有条码,则通过条码查询 |
| | | if (wrkMastLog == null && !Cools.isEmpty(agvTask.getBarcode())) { |
| | | List<WrkMastLog> logList = wrkMastLogService.selectList( |
| | | new EntityWrapper<WrkMastLog>().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格式) |
| | | */ |
| | | private String getRequest(Task task, String nameSpace) { |
| | | public String getRequest(Task task, String nameSpace) { |
| | | JSONObject object = new JSONObject(); |
| | | // taskId使用任务ID,格式:T + 任务ID |
| | | object.put("taskId", "T" + task.getId()); |
| | | // 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()); |
| | | // log.warn("任务{}的源库位和源站点都为空,使用默认值", task.getId()); |
| | | fromBin = "0"; |
| | | } |
| | | object.put("fromBin", fromBin); |
| | |
| | | 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<String> 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<Integer> 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<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) |
| | | .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<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 = "出库"; |
| | | } |
| | | } |
| | | |
| | | // 先查询agv工作档中未被分配站点的站点 |
| | | // 查询agv工作档中所有已分配站点的任务(sta_no不为空、不为空字符串、不为0) |
| | | final List<String> allocatedSiteNos; |
| | | if (checkIoTypes != null && !checkIoTypes.isEmpty()) { |
| | | List<Task> allocatedTasks = taskService.selectList( |
| | | new EntityWrapper<Task>() |
| | | .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<BasDevp> unallocatedSites = devListWithConfig.stream() |
| | | .filter(dev -> { |
| | | String staNo = String.valueOf(dev.getDevNo()); |
| | | return !allocatedSiteNos.contains(staNo); |
| | | }) |
| | | .collect(Collectors.toList()); |
| | | List<BasDevp> 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<BasDevp> 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<BasDevp> 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) { |
| | | private String determineRobotGroupByStation(String staNo) { |
| | | if (staNo == null || staNo.isEmpty()) { |
| | | return agvProperties.getRobotGroupEast(); // 默认使用东侧机器人组 |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 任务完成转历史 释放暂存点 |
| | | */ |
| | | @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); |
| | | } |
| | | |
| | |
| | | // 批量更新暂存点状态 |
| | | List<String> locOList = new ArrayList<>(); |
| | | List<String> locFList = new ArrayList<>(); |
| | | // 收集需要减少in_qty的站点(入库任务) |
| | | Set<Integer> sitesToDecrement = new HashSet<>(); |
| | | for (Task task : taskList) { |
| | | String sourceStaNo = task.getSourceStaNo(); |
| | | String staNo = task.getStaNo(); |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | log.info("agv任务档转历史成功:{}", taskIds); |
| | | } |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void moveTaskToHistory(Task agvTask) { |
| | | moveTaskToHistory(Collections.singletonList(agvTask)); |
| | | |
| | | } |
| | | /** |
| | | * 货物到达出库口,生成agv任务 |
| | | */ |
| | |
| | | |
| | | // 构造取消任务请求 |
| | | JSONObject cancelRequest = new JSONObject(); |
| | | cancelRequest.put("taskId", "T" + task.getId()); |
| | | // 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(); |
| | | |
| | |
| | | JSONObject jsonObject = JSON.parseObject(response); |
| | | if (jsonObject.getInteger("code") != null && jsonObject.getInteger("code").equals(200)) { |
| | | success = true; |
| | | log.info(namespace + "取消AGV任务成功:{}", task.getId()); |
| | | // 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); |
| | | } |