| | |
| | | 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.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.TaskLogService; |
| | | import com.zy.asrs.service.TaskService; |
| | | 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.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | |
| | | @Resource |
| | | private BasStationMapper basStationMapper; |
| | | |
| | | @Value("${Agv.sendTask}") |
| | | private boolean agvSendTask; |
| | | @Resource |
| | | private BasDevpMapper basDevpMapper; |
| | | |
| | | @Resource |
| | | private AgvProperties agvProperties; |
| | | |
| | | /** |
| | | * 站点轮询计数器,用于平均分配站点 |
| | | * Key: 站点组标识(如 "east" 或 "west"),Value: 当前轮询索引 |
| | | */ |
| | | private final Map<String, AtomicInteger> siteRoundRobinCounters = new ConcurrentHashMap<>(); |
| | | |
| | | /** |
| | | * 呼叫agv搬运 |
| | | */ |
| | | public void callAgv(List<Task> taskList) { |
| | | |
| | | if (!agvSendTask) { |
| | | if (!agvProperties.isSendTask()) { |
| | | return; |
| | | } |
| | | |
| | | for (Task task : taskList) { |
| | | // 如果任务没有分配站点,先分配站点 |
| | | String staNo = task.getStaNo(); |
| | | if (staNo == null || staNo.isEmpty()) { |
| | | Integer allocatedSite = allocateSiteForTask(task); |
| | | if (allocatedSite == null) { |
| | | log.warn("任务ID:{}无法分配站点,跳过本次发送", task.getId()); |
| | | continue; // 无法分配站点,跳过本次发送 |
| | | } |
| | | staNo = String.valueOf(allocatedSite); |
| | | task.setStaNo(staNo); |
| | | taskService.updateById(task); |
| | | log.info("任务ID:{}已分配站点:{}", task.getId(), staNo); |
| | | } |
| | | |
| | | // 检查目标站点是否有正在搬运的同类型AGV任务(出库和入库互不干扰) |
| | | // 只有状态8(已呼叫AGV,正在搬运)的任务才会阻塞,状态7(待呼叫)的任务不阻塞 |
| | | // 这样可以避免所有任务都卡在呼叫状态,按id最小的优先呼叫 |
| | | 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()) // 排除当前任务本身 |
| | | ); |
| | | |
| | | if (!transportingTasks.isEmpty()) { |
| | | log.info("站点{}有{}个正在搬运的{}AGV任务,跳过本次发送,等待搬运完成。当前任务ID:{}", |
| | | staNo, transportingTasks.size(), taskType, task.getId()); |
| | | continue; // 跳过本次发送,等待下次 |
| | | } |
| | | } |
| | | |
| | | // 呼叫agv |
| | | String response = ""; |
| | | boolean success = false; |
| | |
| | | default: |
| | | } |
| | | String body = getRequest(task,namespace); |
| | | // 打印请求信息 |
| | | log.info("{}呼叫agv搬运 - 请求地址:{}", namespace, url); |
| | | log.info("{}呼叫agv搬运 - 请求参数:{}", namespace, body); |
| | | try { |
| | | // 使用仙工M4接口 |
| | | response = new HttpHandler.Builder() |
| | |
| | | .setJson(body) |
| | | .build() |
| | | .doPost(); |
| | | // 打印返回参数 |
| | | log.info("{}呼叫agv搬运 - 返回参数:{}", namespace, response); |
| | | |
| | | // 检查响应是否为空 |
| | | if (response == null || response.trim().isEmpty()) { |
| | | log.error("{}呼叫agv搬运失败 - 任务ID:{},AGV接口返回为空", namespace, task.getId()); |
| | | continue; |
| | | } |
| | | |
| | | JSONObject jsonObject = JSON.parseObject(response); |
| | | if (jsonObject.getInteger("code").equals(200)) { |
| | | if (jsonObject == null) { |
| | | log.error("{}呼叫agv搬运失败 - 任务ID:{},响应JSON解析失败,响应内容:{}", namespace, task.getId(), response); |
| | | continue; |
| | | } |
| | | |
| | | Integer code = jsonObject.getInteger("code"); |
| | | if (code != null && code.equals(200)) { |
| | | success = true; |
| | | task.setWrkSts(8L); |
| | | taskService.updateById(task); |
| | | log.info(namespace + "呼叫agv搬运成功:{}", task.getId()); |
| | | log.info("{}呼叫agv搬运成功 - 任务ID:{}", namespace, task.getId()); |
| | | } else { |
| | | log.error(namespace + "呼叫agv搬运失败!!!url:{};request:{};response:{}", url, body, response); |
| | | String message = jsonObject.getString("message"); |
| | | log.error("{}呼叫agv搬运失败 - 任务ID:{},错误码:{},错误信息:{}", |
| | | namespace, task.getId(), code, message); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error(namespace + "呼叫agv搬运异常", e); |
| | | log.error("{}呼叫agv搬运异常 - 任务ID:{},请求地址:{},请求参数:{},异常信息:{}", |
| | | namespace, task.getId(), url, body, e.getMessage(), e); |
| | | } finally { |
| | | try { |
| | | // 保存接口日志 |
| | |
| | | JSONObject object = new JSONObject(); |
| | | // taskId使用任务ID,格式:T + 任务ID |
| | | object.put("taskId", "T" + task.getId()); |
| | | object.put("fromBin", task.getSourceStaNo()); |
| | | // 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字段获取,如果没有则使用默认值 |
| | | // robotGroup从invWh字段获取,如果没有则根据站点编号判断 |
| | | String robotGroup = task.getInvWh(); |
| | | if (robotGroup == null || robotGroup.isEmpty()) { |
| | | robotGroup = "Group-001"; // 默认机器人组 |
| | | robotGroup = determineRobotGroupByStation(task.getStaNo()); |
| | | } |
| | | object.put("robotGroup", robotGroup); |
| | | // kind根据任务类型映射 |
| | | String kind = ""; |
| | | switch (nameSpace) { |
| | | case "入库": |
| | | kind = "实托入库"; |
| | | // 判断是否为空托入库:ioType=10 或 emptyMk="Y" |
| | | if (task.getIoType() == 10 || "Y".equals(task.getEmptyMk())) { |
| | | kind = "空托入库"; |
| | | } else { |
| | | kind = "实托入库"; |
| | | } |
| | | break; |
| | | case "出库": |
| | | kind = "实托出库"; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 为任务分配站点(定时任务中调用) |
| | | * @param task 任务对象 |
| | | * @return 分配的站点编号,如果无法分配则返回null |
| | | */ |
| | | private Integer allocateSiteForTask(Task task) { |
| | | // 根据任务的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:{}的机器人组{}未识别,使用默认东侧站点", task.getId(), robotGroup); |
| | | } |
| | | |
| | | if (targetStations.isEmpty()) { |
| | | log.warn("任务ID:{}没有可用的目标站点配置", task.getId()); |
| | | return null; |
| | | } |
| | | |
| | | // 将站点字符串列表转换为整数列表 |
| | | List<Integer> siteIntList = targetStations.stream() |
| | | .map(Integer::parseInt) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 判断能入站点(in_enable="Y"表示能入) |
| | | List<Integer> sites = basDevpMapper.selectList( |
| | | new EntityWrapper<BasDevp>() |
| | | .eq("in_enable", "Y") |
| | | .in("dev_no", siteIntList) |
| | | ).stream().map(BasDevp::getDevNo).collect(Collectors.toList()); |
| | | |
| | | if (sites.isEmpty()) { |
| | | log.warn("任务ID:{}没有能入站点", task.getId()); |
| | | return null; |
| | | } |
| | | |
| | | // 获取没有出库任务的站点 |
| | | List<Integer> canInSites = basDevpMapper.getCanInSites(sites); |
| | | if (canInSites.isEmpty()) { |
| | | log.warn("任务ID:{}没有可入站点(请等待出库完成)", task.getId()); |
| | | return null; |
| | | } |
| | | |
| | | // 寻找入库任务最少的站点(且必须in_enable="Y"能入 和 canining="Y"可入) |
| | | List<BasDevp> devList = basDevpMapper.selectList(new EntityWrapper<BasDevp>() |
| | | .in("dev_no", canInSites) |
| | | .eq("in_enable", "Y") |
| | | .eq("canining", "Y") |
| | | ); |
| | | |
| | | if (devList.isEmpty()) { |
| | | log.warn("任务ID:{}没有可入站点(in_enable='Y'且canining='Y')", task.getId()); |
| | | return null; |
| | | } |
| | | |
| | | // 先按规则排序(入库任务数排序) |
| | | devList.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 = "出库"; |
| | | } |
| | | } |
| | | |
| | | // 筛选出任务数最少的站点列表(按规则排序后的候选站点) |
| | | 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 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)) { |
| | | // 随机分配:先随机排序任务数最少的站点 |
| | | 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 { |
| | | // 默认:按入库任务数排序(已经排序好了) |
| | | orderedSites = devList; |
| | | } |
| | | |
| | | // 依次检查每个站点是否在搬运,找到第一个空闲站点就分配 |
| | | 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; |
| | | } |
| | | |
| | | Integer endSite = selectedSite.getDevNo(); |
| | | |
| | | // 入库暂存+1 |
| | | basDevpMapper.incrementInQty(endSite); |
| | | |
| | | log.info("任务ID:{}已分配站点:{}", task.getId(), endSite); |
| | | return endSite; |
| | | } |
| | | |
| | | /** |
| | | * 根据站点编号判断机器人组 |
| | | * @param staNo 站点编号 |
| | | * @return 机器人组名称 |
| | | */ |
| | | private String determineRobotGroupByStation(String staNo) { |
| | | if (staNo == null || staNo.isEmpty()) { |
| | | return agvProperties.getRobotGroupEast(); // 默认使用东侧机器人组 |
| | | } |
| | | |
| | | // 从配置中获取站点列表 |
| | | Set<String> eastStations = new HashSet<>(agvProperties.getEastStations()); |
| | | Set<String> 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<Task> taskList) { |
| | | |
| | | // 写入历史表 |
| | | // 写入历史表,保持ID一致 |
| | | for (Task task : taskList) { |
| | | TaskLog log = new TaskLog(); |
| | | BeanUtils.copyProperties(task, log); |
| | | // 保持ID一致,不设置为null |
| | | taskLogService.insert(log); |
| | | } |
| | | |
| | |
| | | * @return 是否成功 |
| | | */ |
| | | public boolean cancelAgvTask(Task task) { |
| | | if (!agvSendTask) { |
| | | if (!agvProperties.isSendTask()) { |
| | | return false; |
| | | } |
| | | |