| | |
| | | import com.zy.common.model.enums.WorkNoType; |
| | | import com.zy.common.properties.AgvProperties; |
| | | import com.zy.common.service.CommonService; |
| | | import com.zy.common.utils.AgvUtils; |
| | | import com.zy.common.utils.HttpHandler; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.formula.functions.T; |
| | |
| | | @Resource |
| | | private AgvProperties agvProperties; |
| | | |
| | | @Autowired |
| | | private com.zy.asrs.task.handler.AgvHandler agvHandler; |
| | | |
| | | /** |
| | | * 站点轮询计数器,用于平均分配站点 |
| | | * Key: 站点组标识(如 "east" 或 "west"),Value: 当前轮询索引 |
| | |
| | | int type = param.getType(); |
| | | String sourceSite = param.getSourceSite(); |
| | | String barcode = param.getBarcode(); |
| | | int ioType; |
| | | LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>().eq("loc_no", sourceSite)); |
| | | if (null == locCache) { |
| | | throw new CoolException("站点不存在:" + sourceSite); |
| | | |
| | | // 检查托盘码和暂存位编码是否相同 |
| | | if (barcode != null && sourceSite != null && barcode.trim().equals(sourceSite.trim())) { |
| | | throw new CoolException("托盘码和暂存位编码不能相同"); |
| | | } |
| | | |
| | | int ioType; |
| | | // 查询源站点(库位)信息,但不检查是否存在,允许下单成功 |
| | | // 站点不存在的检查将在定时任务(AgvHandler.callAgv)中进行 |
| | | LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>().eq("loc_no", sourceSite)); |
| | | switch (type) { |
| | | case 1: |
| | | // 判断有没有组托 |
| | |
| | | if (count == 0) { |
| | | throw new CoolException("条码未组托:" + barcode); |
| | | } |
| | | ioType = 101; |
| | | ioType = 1; // AGV容器入库(实托入库) |
| | | |
| | | locCache.setLocSts(LocStsType.LOC_STS_TYPE_R.type); |
| | | locCacheService.updateById(locCache); |
| | | // 如果库位存在,更新状态为入库预约;不存在则跳过,由定时任务处理 |
| | | if (locCache != null) { |
| | | locCache.setLocSts(LocStsType.LOC_STS_TYPE_S.type); // S.入库预约 |
| | | locCacheService.updateById(locCache); |
| | | } |
| | | break; |
| | | case 2: |
| | | // 判断是拣选回库托盘 |
| | |
| | | if (wrkMast.getIoType() != 103 && wrkMast.getIoType() != 107) { |
| | | throw new CoolException("条码不需要回库:" + barcode); |
| | | } |
| | | ioType = wrkMast.getIoType() - 50; |
| | | ioType = wrkMast.getIoType() - 50; // 103->53(拣料入库), 107->57(盘点入库) |
| | | |
| | | locCache.setLocSts(LocStsType.LOC_STS_TYPE_R.type); |
| | | locCacheService.updateById(locCache); |
| | | // 如果库位存在,更新状态为入库预约;不存在则跳过,由定时任务处理 |
| | | if (locCache != null) { |
| | | locCache.setLocSts(LocStsType.LOC_STS_TYPE_S.type); // S.入库预约(容器回库是入库操作) |
| | | locCacheService.updateById(locCache); |
| | | } |
| | | break; |
| | | case 3: |
| | | // 判断是否为空托入库:检查条码在wms中不存在,确认为空托盘 |
| | |
| | | throw new CoolException("入库类型错误,type:" + type); |
| | | } |
| | | // 条码存在agv任务 |
| | | int count = taskService.selectCount(new EntityWrapper<Task>().eq("barcode", barcode)); |
| | | int count = taskService.selectCount(new EntityWrapper<Task>().eq("barcode", barcode).eq("is_deleted", 0)); |
| | | if (count > 0) { |
| | | throw new CoolException(barcode+ ":条码存在agv搬运任务!"); |
| | | } |
| | | |
| | | // 根据whs_type选择站点和机器人组 |
| | | Long whsType = locCache.getWhsType(); |
| | | List<String> targetStations; |
| | | // 根据whs_type确定机器人组(站点分配完全由定时任务处理) |
| | | // 如果库位不存在,使用默认逻辑(根据type判断) |
| | | Long whsType = locCache != null ? locCache.getWhsType() : null; |
| | | String robotGroup; |
| | | |
| | | if (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getInboundArea())) { |
| | | // whs_type = 1: 入库区,使用东侧站点和Group-001 |
| | | targetStations = agvProperties.getEastStations(); |
| | | // whs_type = 1: 入库区,使用Group-001 |
| | | robotGroup = agvProperties.getRobotGroupEast(); |
| | | log.info("库位whs_type={},使用入库区配置(东侧站点和Group-001)", whsType); |
| | | log.info("库位whs_type={},使用入库区配置({})", whsType, robotGroup); |
| | | } else if (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getCacheArea())) { |
| | | // whs_type = 2: 缓存区,使用西侧站点和Group-002 |
| | | targetStations = agvProperties.getWestStations(); |
| | | // whs_type = 2: 缓存区,使用Group-002 |
| | | robotGroup = agvProperties.getRobotGroupWest(); |
| | | log.info("库位whs_type={},使用缓存区配置(西侧站点和Group-002)", whsType); |
| | | log.info("库位whs_type={},使用缓存区配置({})", whsType, robotGroup); |
| | | } else { |
| | | // whs_type为空或其他值,根据type判断(兼容旧逻辑) |
| | | // 如果库位不存在,也使用此逻辑 |
| | | if (type == 1) { |
| | | targetStations = agvProperties.getEastStations(); |
| | | robotGroup = agvProperties.getRobotGroupEast(); |
| | | } else { |
| | | targetStations = agvProperties.getWestStations(); |
| | | robotGroup = agvProperties.getRobotGroupWest(); |
| | | } |
| | | log.warn("库位whs_type={}未配置或不在映射范围内,使用type={}的默认逻辑", whsType, type); |
| | | } |
| | | |
| | | // 将站点字符串列表转换为整数列表 |
| | | 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_enable是能入 |
| | | .in("dev_no", siteIntList) |
| | | ).stream().map(BasDevp::getDevNo).collect(Collectors.toList()); |
| | | |
| | | if (sites.isEmpty()) { |
| | | throw new CoolException("没有能入站点,whs_type:" + whsType + ",type:" + type); |
| | | } |
| | | |
| | | // 获取没有出库任务的站点 |
| | | List<Integer> canInSites = basDevpMapper.getCanInSites(sites); |
| | | if (canInSites.isEmpty()) { |
| | | throw new CoolException("请等待出库完成,type:" + type); |
| | | } |
| | | |
| | | // 检查站点是否有未完成的AGV任务 |
| | | // 规则:当某个站点有未完成的AGV任务时,不分配该站点;只从没有未完成任务的站点中选择 |
| | | // 将站点列表转换为字符串列表(Task表的sta_no是String类型) |
| | | List<String> siteStrList = canInSites.stream() |
| | | .map(String::valueOf) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 查询这些站点中有未完成AGV任务的站点(wrk_sts不在5和15之间表示未完成) |
| | | List<Task> unfinishedTasks = taskService.selectList(new EntityWrapper<Task>() |
| | | .in("sta_no", siteStrList) |
| | | .eq("task_type", "agv") // 只查询AGV任务 |
| | | .last("AND wrk_sts NOT IN (5, 15)") // 排除已完成状态(5和15表示已完成) |
| | | ); |
| | | |
| | | // 获取有未完成任务的站点集合(这些站点将被排除,不参与分配) |
| | | Set<String> sitesWithUnfinishedTasks = unfinishedTasks.stream() |
| | | .map(Task::getStaNo) |
| | | .collect(Collectors.toSet()); |
| | | |
| | | // 从可用站点中排除有未完成任务的站点,只保留没有未完成任务的站点 |
| | | List<Integer> availableSites = canInSites.stream() |
| | | .filter(site -> !sitesWithUnfinishedTasks.contains(String.valueOf(site))) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 如果所有站点都有未完成任务,则没有可用站点,不分配 |
| | | if (availableSites.isEmpty()) { |
| | | String groupName = (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getInboundArea())) |
| | | ? "东侧" : "西侧"; |
| | | log.warn("{}所有站点({})都有未完成的AGV任务,无法分配站点,请等待任务完成", groupName, canInSites); |
| | | throw new CoolException(groupName + "所有站点都有未完成的AGV任务,请等待任务完成后再试"); |
| | | } |
| | | |
| | | // 记录站点分配信息 |
| | | if (!sitesWithUnfinishedTasks.isEmpty()) { |
| | | log.info("站点分配检查:总站点数={},有未完成任务的站点={}(已排除),可用站点数={},可用站点={}", |
| | | canInSites.size(), sitesWithUnfinishedTasks, availableSites.size(), availableSites); |
| | | } else { |
| | | log.info("站点分配检查:所有站点({})都没有未完成任务,全部可用", canInSites); |
| | | } |
| | | |
| | | // 寻找入库任务最少的站点(只从可用站点中选择,且必须in_enable="Y"能入 和 canining="Y"可入) |
| | | List<BasDevp> devList = basDevpMapper.selectList(new EntityWrapper<BasDevp>() |
| | | .in("dev_no", availableSites) |
| | | .eq("in_enable", "Y") // in_enable是能入 |
| | | .eq("canining", "Y") // canining是可入 |
| | | ); |
| | | |
| | | // 如果查询结果为空,说明没有可入的站点 |
| | | if (devList.isEmpty()) { |
| | | String groupName = (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getInboundArea())) |
| | | ? "东侧" : "西侧"; |
| | | log.warn("{}可用站点({})中没有可入站点(in_enable='Y'且canining='Y'),无法分配", groupName, availableSites); |
| | | throw new CoolException(groupName + "可用站点中没有可入站点,请检查站点配置"); |
| | | } |
| | | |
| | | // 入库任务数排序 |
| | | devList.sort(Comparator.comparing(BasDevp::getInQty)); |
| | | |
| | | // 选择站点 |
| | | BasDevp basDevp; |
| | | int endSite; |
| | | |
| | | // 获取最少任务数 |
| | | int minInQty = devList.get(0).getInQty(); |
| | | |
| | | // 筛选出任务数最少的站点列表 |
| | | List<BasDevp> minTaskSites = devList.stream() |
| | | .filter(dev -> dev.getInQty() == minInQty) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 根据配置选择分配策略 |
| | | String strategy = agvProperties.getSiteAllocation().getStrategy(); |
| | | boolean enableRoundRobin = agvProperties.getSiteAllocation().isEnableRoundRobin(); |
| | | |
| | | if (minTaskSites.size() > 1 && enableRoundRobin && "round-robin".equals(strategy)) { |
| | | // 轮询分配:当多个站点任务数相同时,使用轮询 |
| | | String groupKey = (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getInboundArea())) |
| | | ? "east" : "west"; |
| | | AtomicInteger counter = siteRoundRobinCounters.computeIfAbsent(groupKey, k -> new AtomicInteger(0)); |
| | | int index = counter.getAndIncrement() % minTaskSites.size(); |
| | | basDevp = minTaskSites.get(index); |
| | | log.info("使用轮询分配策略,站点组:{},轮询索引:{},选中站点:{}", groupKey, index, basDevp.getDevNo()); |
| | | } else if (minTaskSites.size() > 1 && enableRoundRobin && "random".equals(strategy)) { |
| | | // 随机分配 |
| | | Random random = new Random(); |
| | | int index = random.nextInt(minTaskSites.size()); |
| | | basDevp = minTaskSites.get(index); |
| | | log.info("使用随机分配策略,选中站点:{}", basDevp.getDevNo()); |
| | | } else { |
| | | // 默认:选择第一个(任务最少的) |
| | | basDevp = devList.get(0); |
| | | if (minTaskSites.size() > 1) { |
| | | log.info("多个站点任务数相同({}),但未启用轮询,选择第一个站点:{}", minInQty, basDevp.getDevNo()); |
| | | if (locCache == null) { |
| | | log.warn("源站点(库位){}不存在,使用type={}的默认逻辑,机器人组:{},站点分配将在定时任务中进行", sourceSite, type, robotGroup); |
| | | } else { |
| | | log.warn("库位whs_type={}未配置或不在映射范围内,使用type={}的默认逻辑,机器人组:{}", whsType, type, robotGroup); |
| | | } |
| | | } |
| | | |
| | | endSite = basDevp.getDevNo(); |
| | | |
| | | // 入库暂存+1 |
| | | basDevpMapper.incrementInQty(endSite); |
| | | // 站点分配完全由定时任务处理,此处不分配站点,只创建任务 |
| | | log.info("创建AGV任务,站点分配将在定时任务中处理,机器人组:{}", robotGroup); |
| | | |
| | | |
| | | // 获取工作号 |
| | | int workNo = commonService.getWorkNo(WorkNoType.PICK.type); |
| | | // 生成AGV工作号 |
| | | String agvWrkNo = AgvUtils.generateAgvWrkNo(workNo); |
| | | // 保存工作档 |
| | | Task task = new Task(); |
| | | Date now = new Date(); |
| | | task.setWrkNo(workNo) |
| | | .setAgvWrkNo(agvWrkNo) // 设置AGV工作号 |
| | | .setIoTime(now) |
| | | .setWrkSts(7L) // 工作状态:11.生成出库ID |
| | | .setIoType(ioType) // 入出库状态: 1.入库 |
| | | .setTaskType("agv") |
| | | .setIoPri(10D) |
| | | .setStaNo(String.valueOf(endSite)) |
| | | .setStaNo(null) // 站点分配完全由定时任务处理 |
| | | .setSourceStaNo(sourceSite) // 设置源站点 |
| | | .setInvWh(robotGroup) // 根据whs_type设置机器人组 |
| | | .setFullPlt(ioType != 10 ? "N" : "Y")// 满板:Y |
| | | .setFullPlt(ioType == 10 ? "N" : "Y")// 空托入库(ioType=10)设置为N,其他入库设置为Y(满板) |
| | | .setPicking("N") // 拣料 |
| | | .setExitMk("N")// 退出 |
| | | .setSourceLocNo(locCache.getLocNo()) // 设置源库位编号,用于AGV fromBin |
| | | .setEmptyMk(ioType == 10 ? "Y" : "N")// 空板 |
| | | .setSourceLocNo(locCache != null ? locCache.getLocNo() : sourceSite) // 设置源库位编号,用于AGV fromBin,如果库位不存在则使用sourceSite |
| | | .setEmptyMk(ioType == 10 ? "Y" : "N")// 空托入库(ioType=10)设置为Y,其他设置为N |
| | | .setBarcode(barcode)// 托盘码 |
| | | .setLinkMis("N") |
| | | .setAppeTime(now) |
| | |
| | | throw new CoolException("保存工作档失败"); |
| | | } |
| | | |
| | | // 更新暂存位状态为 R.出库预约 |
| | | basStationMapper.updateLocStsBatch( Collections.singletonList(String.valueOf(sourceSite)), "R"); |
| | | // 如果库位存在,根据ioType更新暂存位状态:入库任务设置为S(入库预约),出库任务设置为R(出库预约) |
| | | if (locCache != null) { |
| | | String locSts = (ioType < 100) ? "S" : "R"; // 入库任务(ioType < 100)设置为S,出库任务设置为R |
| | | basStationMapper.updateLocStsBatch( Collections.singletonList(String.valueOf(sourceSite)), locSts); |
| | | } |
| | | return R.ok("agv任务生成成功!"); |
| | | |
| | | } |
| | |
| | | */ |
| | | @Override |
| | | public R callAgvMove(AgvCallParams params, Long userId) { |
| | | if (Objects.isNull(params.getTarSite())) { |
| | | throw new CoolException("目标参数不能为空!!"); |
| | | } |
| | | if (Objects.isNull(params.getOrgSite())) { |
| | | throw new CoolException("源站点不能为空!!"); |
| | | } |
| | | LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>() |
| | | .eq("frozen", 0) |
| | | .eq("loc_sts", LocStsType.LOC_STS_TYPE_O.type) |
| | | .eq("loc_no", params.getTarSite()) |
| | | .orderAsc(Arrays.asList("loc_no")) |
| | | .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")); |
| | | if (Objects.isNull(locCache)) { |
| | | throw new CoolException("请检查目标库位是否闲置中!!"); |
| | | if (Objects.isNull(params.getBarcode())) { |
| | | throw new CoolException("托盘码不能为空!!"); |
| | | } |
| | | |
| | | generateAgvTask("agv", locCache, params.getOrgSite(), params.getBarcode(), userId); |
| | | |
| | | String callType = params.getCallType(); |
| | | if (callType == null || callType.isEmpty()) { |
| | | callType = "manual"; // 默认手动输入 |
| | | } |
| | | |
| | | if ("manual".equals(callType)) { |
| | | // 手动输入:需要目标站点 |
| | | if (Objects.isNull(params.getTarSite())) { |
| | | throw new CoolException("手动输入模式下,目标站点不能为空!!"); |
| | | } |
| | | LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>() |
| | | .eq("frozen", 0) |
| | | .eq("loc_sts", LocStsType.LOC_STS_TYPE_O.type) |
| | | .eq("loc_no", params.getTarSite()) |
| | | .orderAsc(Arrays.asList("loc_no")) |
| | | .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")); |
| | | if (Objects.isNull(locCache)) { |
| | | throw new CoolException("请检查目标库位是否闲置中!!"); |
| | | } |
| | | generateAgvTask("agv", locCache, params.getOrgSite(), params.getBarcode(), userId); |
| | | } else if ("outbound".equals(callType)) { |
| | | // 起点+出库:自动分配站点和缓存位 |
| | | generateOutboundAgvTask(params.getOrgSite(), params.getBarcode(), userId); |
| | | } else if ("inbound".equals(callType)) { |
| | | // 起点+入库:自动分配站点,库位手动输入 |
| | | if (Objects.isNull(params.getTarLoc())) { |
| | | throw new CoolException("入库模式下,目标库位不能为空!!"); |
| | | } |
| | | LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>() |
| | | .eq("frozen", 0) |
| | | .eq("loc_sts", LocStsType.LOC_STS_TYPE_O.type) |
| | | .eq("loc_no", params.getTarLoc()) |
| | | .orderAsc(Arrays.asList("loc_no")) |
| | | .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")); |
| | | if (Objects.isNull(locCache)) { |
| | | throw new CoolException("请检查目标库位是否闲置中!!"); |
| | | } |
| | | generateInboundAgvTask(locCache, params.getOrgSite(), params.getBarcode(), userId); |
| | | } else { |
| | | throw new CoolException("不支持的呼叫类型:" + callType); |
| | | } |
| | | |
| | | return R.ok(); |
| | | } |
| | |
| | | public void generateOutTask(BasStation station, LocCache loc, Long userId) { |
| | | // 获取工作号 |
| | | int workNo = commonService.getWorkNo(WorkNoType.PICK.type); |
| | | // 生成AGV工作号 |
| | | String agvWrkNo = AgvUtils.generateAgvWrkNo(workNo); |
| | | // 保存工作档 |
| | | Task task = new Task(); |
| | | task.setWrkNo(workNo) |
| | | .setAgvWrkNo(agvWrkNo) |
| | | .setIoTime(new Date()) |
| | | .setWrkSts(11L) // 工作状态:11.生成出库ID |
| | | .setIoType(101) // 入出库状态: 11.库格移载 |
| | |
| | | } |
| | | // 获取工作号 |
| | | int workNo = commonService.getWorkNo(WorkNoType.PICK.type); |
| | | // 生成AGV工作号 |
| | | String agvWrkNo = AgvUtils.generateAgvWrkNo(workNo); |
| | | // 保存工作档 |
| | | Task task = new Task(); |
| | | Date now = new Date(); |
| | | task.setWrkNo(workNo) |
| | | .setIoTime(new Date()) |
| | | .setWrkSts(1L) // 工作状态:11.生成出库ID |
| | | .setAgvWrkNo(agvWrkNo) // 设置AGV工作号 |
| | | .setIoTime(now) |
| | | .setWrkSts(7L) // 工作状态:7.待呼叫AGV(与自动创建保持一致,由定时任务分配站点并呼叫) |
| | | .setIoType(1) // 入出库状态: 11.库格移载 |
| | | .setTaskType("agv") |
| | | .setIoPri(10D) |
| | | .setLocNo(loc.getLocNo()) // 目标库位 |
| | | .setStaNo(null) // 站点分配由定时任务处理(与自动创建保持一致) |
| | | .setFullPlt("Y") // 满板:Y |
| | | .setPicking("N") // 拣料 |
| | | .setExitMk("N")// 退出 |
| | |
| | | .setBarcode(barcode)// 托盘码 |
| | | .setLinkMis("N") |
| | | .setAppeUser(userId) |
| | | .setAppeTime(new Date()) |
| | | .setAppeTime(now) |
| | | .setModiUser(userId) |
| | | .setModiTime(new Date()); |
| | | .setModiTime(now); |
| | | if (!taskService.insert(task)) { |
| | | throw new CoolException("保存工作档失败"); |
| | | } |
| | |
| | | throw new CoolException("移转失败,目标库位状态:" + loc.getLocSts$()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 生成出库AGV任务(起点+出库,自动分配站点和缓存位) |
| | | * @param orgSite 源站点 |
| | | * @param barcode 托盘码 |
| | | * @param userId 用户ID |
| | | */ |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void generateOutboundAgvTask(String orgSite, String barcode, Long userId) { |
| | | // 根据源站点判断是东侧还是西侧,确定缓存区站点和whs_type |
| | | List<String> eastStations = agvProperties.getEastStations(); |
| | | List<String> westStations = agvProperties.getWestStations(); |
| | | List<String> cacheStations; |
| | | Long targetWhsType; |
| | | String robotGroup; |
| | | String cacheAreaPrefix = agvProperties.getLocationPrefix().getCacheArea(); |
| | | boolean isEmptyPallet = false; // 默认满板出库,可根据需要调整 |
| | | |
| | | if (eastStations.contains(orgSite)) { |
| | | // 东侧出库站点,查找东侧WA库位(whs_type=1) |
| | | cacheStations = agvProperties.getEastStations(); |
| | | robotGroup = agvProperties.getRobotGroupEast(); |
| | | targetWhsType = agvProperties.getWhsTypeMapping().getInboundArea(); // whs_type=1 |
| | | } else if (westStations.contains(orgSite)) { |
| | | // 西侧出库站点,查找西侧WA库位(whs_type=2) |
| | | cacheStations = agvProperties.getWestStations(); |
| | | robotGroup = agvProperties.getRobotGroupWest(); |
| | | targetWhsType = agvProperties.getWhsTypeMapping().getCacheArea(); // whs_type=2 |
| | | } else { |
| | | // 默认使用西侧 |
| | | cacheStations = agvProperties.getWestStations(); |
| | | robotGroup = agvProperties.getRobotGroupWest(); |
| | | targetWhsType = agvProperties.getWhsTypeMapping().getCacheArea(); |
| | | log.warn("源站点{}不在配置的站点列表中,使用默认西侧配置", orgSite); |
| | | } |
| | | |
| | | // 自动分配缓存位(使用优先级分配逻辑) |
| | | LocCache locCache = allocateCacheLocationByPriority(targetWhsType, cacheAreaPrefix, isEmptyPallet); |
| | | if (Objects.isNull(locCache)) { |
| | | throw new CoolException("暂无满足需求的缓存位!"); |
| | | } |
| | | |
| | | // 自动分配站点(使用出库到缓存区的分配策略) |
| | | String allocatedSite = allocateCacheStationForOutbound(cacheStations, 101); |
| | | if (allocatedSite == null) { |
| | | throw new CoolException("无法分配缓存区站点,所有站点都在使用中!"); |
| | | } |
| | | |
| | | // 获取工作号 |
| | | int workNo = commonService.getWorkNo(WorkNoType.PICK.type); |
| | | // 生成AGV工作号 |
| | | String agvWrkNo = AgvUtils.generateAgvWrkNo(workNo); |
| | | // 保存工作档 |
| | | Task task = new Task(); |
| | | Date now = new Date(); |
| | | task.setWrkNo(workNo) |
| | | .setAgvWrkNo(agvWrkNo) |
| | | .setIoTime(now) |
| | | .setWrkSts(7L) // 工作状态:7.待呼叫AGV(站点已分配) |
| | | .setIoType(101) // 入出库状态:101.全板出库 |
| | | .setTaskType("agv") |
| | | .setIoPri(10D) |
| | | .setStaNo(allocatedSite) // 站点已自动分配 |
| | | .setLocNo(locCache.getLocNo()) // 目标缓存位 |
| | | .setFullPlt("Y") // 满板:Y |
| | | .setPicking("N") // 拣料 |
| | | .setExitMk("N")// 退出 |
| | | .setSourceStaNo(orgSite) // 源站点 |
| | | .setSourceLocNo(locCache.getLocNo()) // 源库位(缓存位) |
| | | .setEmptyMk(locCache.getLocSts().equals("D") ? "Y" : "N")// 空板 |
| | | .setBarcode(barcode)// 托盘码 |
| | | .setLinkMis("N") |
| | | .setInvWh(robotGroup) // 根据源站点设置机器人组 |
| | | .setAppeUser(userId) |
| | | .setAppeTime(now) |
| | | .setModiUser(userId) |
| | | .setModiTime(now); |
| | | if (!taskService.insert(task)) { |
| | | throw new CoolException("保存工作档失败"); |
| | | } |
| | | |
| | | // 更新缓存位状态:O(闲置)-> R(出库预约) |
| | | locCache.setLocSts(LocStsType.LOC_STS_TYPE_R.type); |
| | | locCache.setModiUser(userId); |
| | | locCache.setModiTime(now); |
| | | if (!locCacheService.updateById(locCache)) { |
| | | throw new CoolException("更新缓存位状态失败"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 生成入库AGV任务(起点+入库,自动分配站点,库位手动指定) |
| | | * @param locCache 目标库位 |
| | | * @param orgSite 源站点 |
| | | * @param barcode 托盘码 |
| | | * @param userId 用户ID |
| | | */ |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void generateInboundAgvTask(LocCache locCache, String orgSite, String barcode, Long userId) { |
| | | // 判断是否是空托入库:库位状态为"D"(空桶/空栈板)表示空托入库 |
| | | boolean isEmptyPallet = LocStsType.LOC_STS_TYPE_D.type.equals(locCache.getLocSts()); |
| | | Integer ioType = isEmptyPallet ? 10 : 1; // 10=空托入库,1=实托入库 |
| | | |
| | | // 只有实托入库才需要组托信息 |
| | | List<WaitPakin> pakins = null; |
| | | if (!isEmptyPallet) { |
| | | // 实托入库:检查组托信息是否存在 |
| | | pakins = waitPakinService.selectList(new EntityWrapper<WaitPakin>().eq("zpallet", barcode)); |
| | | if (Objects.isNull(pakins) || pakins.isEmpty()) { |
| | | throw new CoolException("组托信息不存在!!"); |
| | | } |
| | | } |
| | | |
| | | // 获取工作号 |
| | | int workNo = commonService.getWorkNo(WorkNoType.PICK.type); |
| | | // 生成AGV工作号 |
| | | String agvWrkNo = AgvUtils.generateAgvWrkNo(workNo); |
| | | // 保存工作档 |
| | | Task task = new Task(); |
| | | Date now = new Date(); |
| | | |
| | | // 根据库位whs_type确定机器人组 |
| | | Long whsType = locCache.getWhsType(); |
| | | String robotGroup; |
| | | if (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getInboundArea())) { |
| | | robotGroup = agvProperties.getRobotGroupEast(); |
| | | } else if (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getCacheArea())) { |
| | | robotGroup = agvProperties.getRobotGroupWest(); |
| | | } else { |
| | | robotGroup = agvProperties.getRobotGroupEast(); // 默认东侧 |
| | | } |
| | | |
| | | task.setWrkNo(workNo) |
| | | .setAgvWrkNo(agvWrkNo) // 设置AGV工作号 |
| | | .setIoTime(now) |
| | | .setWrkSts(7L) // 工作状态:7.待呼叫AGV(站点将自动分配) |
| | | .setIoType(ioType) // 入出库状态:10=空托入库,1=实托入库 |
| | | .setTaskType("agv") |
| | | .setIoPri(10D) |
| | | .setLocNo(locCache.getLocNo()) // 目标库位 |
| | | .setStaNo(null) // 站点将自动分配 |
| | | .setFullPlt(isEmptyPallet ? "N" : "Y") // 空托入库设置为N,实托入库设置为Y |
| | | .setPicking("N") // 拣料 |
| | | .setExitMk("N")// 退出 |
| | | .setSourceStaNo(orgSite) // 源站点 |
| | | .setSourceLocNo(orgSite) // 源库位(使用源站点) |
| | | .setEmptyMk(isEmptyPallet ? "Y" : "N")// 空板:空托入库为Y,实托入库为N |
| | | .setBarcode(barcode)// 托盘码 |
| | | .setLinkMis("N") |
| | | .setInvWh(robotGroup) // 根据whs_type设置机器人组 |
| | | .setAppeUser(userId) |
| | | .setAppeTime(now) |
| | | .setModiUser(userId) |
| | | .setModiTime(now); |
| | | if (!taskService.insert(task)) { |
| | | throw new CoolException("保存工作档失败"); |
| | | } |
| | | |
| | | // 立即分配站点(跟随入库逻辑) |
| | | String errorMsg = agvHandler.allocateSiteForTask(task); |
| | | if (errorMsg != null) { |
| | | throw new CoolException("分配站点失败:" + errorMsg); |
| | | } |
| | | // 重新查询任务以获取分配后的站点 |
| | | task = taskService.selectById(task.getId()); |
| | | if (task.getStaNo() == null || task.getStaNo().isEmpty() || task.getStaNo().equals("0")) { |
| | | throw new CoolException("站点分配失败,无法继续!"); |
| | | } |
| | | |
| | | // 只有实托入库才需要保存工作档明细 |
| | | if (!isEmptyPallet && pakins != null && !pakins.isEmpty()) { |
| | | List<TaskDetl> taskDetls = new ArrayList<>(); |
| | | pakins.forEach(pakin -> { |
| | | TaskDetl wrkDetl = new TaskDetl(); |
| | | BeanUtils.copyProperties(pakin, wrkDetl); |
| | | wrkDetl.setWrkNo(workNo) |
| | | .setIoTime(new Date()) |
| | | .setOrderNo(pakin.getOrderNo()) |
| | | .setAnfme(pakin.getAnfme()) |
| | | .setZpallet(pakin.getZpallet()) |
| | | .setBatch(pakin.getBatch()) |
| | | .setMatnr(pakin.getMatnr()) |
| | | .setMaktx(pakin.getMaktx()) |
| | | .setAppeUser(userId) |
| | | .setUnit(pakin.getUnit()) |
| | | .setModel(pakin.getModel()) |
| | | .setAppeTime(new Date()) |
| | | .setModiUser(userId); |
| | | taskDetls.add(wrkDetl); |
| | | }); |
| | | |
| | | if (!taskDetlService.insertBatch(taskDetls)) { |
| | | throw new CoolException("保存工作档明细失败"); |
| | | } |
| | | } |
| | | |
| | | // 修改目标库位状态:O(闲置)-> S(入库预约) |
| | | if (locCache.getLocSts().equals(LocStsType.LOC_STS_TYPE_O.type)) { |
| | | locCache.setLocSts(LocStsType.LOC_STS_TYPE_S.type); |
| | | locCache.setModiTime(now); |
| | | locCache.setModiUser(userId); |
| | | if (!locCacheService.updateById(locCache)) { |
| | | throw new CoolException("更新目标库位状态失败"); |
| | | } |
| | | } else { |
| | | throw new CoolException("移转失败,目标库位状态:" + locCache.getLocSts()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 按优先级分配缓存库位(跟随出库逻辑) |
| | | * @param whsType 库区类型 |
| | | * @param cacheAreaPrefix 缓存区库位前缀(如"WA") |
| | | * @param isEmptyPallet 是否空托 |
| | | * @return 分配的缓存库位,如果无法分配则返回null |
| | | */ |
| | | private LocCache allocateCacheLocationByPriority(Long whsType, String cacheAreaPrefix, boolean isEmptyPallet) { |
| | | // 查询所有符合条件的空库位 |
| | | List<LocCache> allLocations = locCacheService.selectList(new EntityWrapper<LocCache>() |
| | | .eq("whs_type", whsType) |
| | | .like("loc_no", cacheAreaPrefix + "%") |
| | | .eq("frozen", 0) |
| | | .eq("loc_sts", LocStsType.LOC_STS_TYPE_O.type) // O.闲置 |
| | | .ne("full_plt", isEmptyPallet ? "Y" : "N") // 空托不选满板库位,满托不选空板库位 |
| | | ); |
| | | |
| | | if (allLocations == null || allLocations.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | // 按row1分组 |
| | | Map<Integer, List<LocCache>> locationsByRow = allLocations.stream() |
| | | .filter(loc -> loc.getRow1() != null) |
| | | .collect(Collectors.groupingBy(LocCache::getRow1)); |
| | | |
| | | if (locationsByRow.isEmpty()) { |
| | | return null; |
| | | } |
| | | |
| | | // 优先级1:分配第三列(bay1=3),且该排的1、2、3列都是空的 |
| | | for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) { |
| | | Integer row = entry.getKey(); |
| | | List<LocCache> rowLocs = entry.getValue(); |
| | | |
| | | // 检查该排的1、2、3列是否都有空库位 |
| | | boolean hasBay1 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 1); |
| | | boolean hasBay2 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 2); |
| | | boolean hasBay3 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 3); |
| | | |
| | | if (hasBay1 && hasBay2 && hasBay3) { |
| | | // 该排的1、2、3列都是空的,分配第三列 |
| | | List<LocCache> bay3Locs = rowLocs.stream() |
| | | .filter(loc -> loc.getBay1() != null && loc.getBay1() == 3) |
| | | .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | if (!bay3Locs.isEmpty()) { |
| | | log.debug("优先级1:分配排{}的第三列,库位:{}", row, bay3Locs.get(0).getLocNo()); |
| | | return bay3Locs.get(0); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 优先级2:分配第二列(bay1=2),且该排的1、2列都是空的 |
| | | for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) { |
| | | Integer row = entry.getKey(); |
| | | List<LocCache> rowLocs = entry.getValue(); |
| | | |
| | | boolean hasBay1 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 1); |
| | | boolean hasBay2 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 2); |
| | | |
| | | if (hasBay1 && hasBay2) { |
| | | List<LocCache> bay2Locs = rowLocs.stream() |
| | | .filter(loc -> loc.getBay1() != null && loc.getBay1() == 2) |
| | | .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | if (!bay2Locs.isEmpty()) { |
| | | log.debug("优先级2:分配排{}的第二列,库位:{}", row, bay2Locs.get(0).getLocNo()); |
| | | return bay2Locs.get(0); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 优先级3:分配第一列(bay1=1) |
| | | for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) { |
| | | Integer row = entry.getKey(); |
| | | List<LocCache> rowLocs = entry.getValue(); |
| | | |
| | | List<LocCache> bay1Locs = rowLocs.stream() |
| | | .filter(loc -> loc.getBay1() != null && loc.getBay1() == 1) |
| | | .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | if (!bay1Locs.isEmpty()) { |
| | | log.debug("优先级3:分配排{}的第一列,库位:{}", row, bay1Locs.get(0).getLocNo()); |
| | | return bay1Locs.get(0); |
| | | } |
| | | } |
| | | |
| | | // 优先级4:分配第二列(不要求该排的1、2列都是空的) |
| | | for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) { |
| | | Integer row = entry.getKey(); |
| | | List<LocCache> rowLocs = entry.getValue(); |
| | | |
| | | List<LocCache> bay2Locs = rowLocs.stream() |
| | | .filter(loc -> loc.getBay1() != null && loc.getBay1() == 2) |
| | | .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | if (!bay2Locs.isEmpty()) { |
| | | log.debug("优先级4:分配排{}的第二列,库位:{}", row, bay2Locs.get(0).getLocNo()); |
| | | return bay2Locs.get(0); |
| | | } |
| | | } |
| | | |
| | | // 优先级5:分配第三列(不要求该排的1、2、3列都是空的) |
| | | for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) { |
| | | Integer row = entry.getKey(); |
| | | List<LocCache> rowLocs = entry.getValue(); |
| | | |
| | | List<LocCache> bay3Locs = rowLocs.stream() |
| | | .filter(loc -> loc.getBay1() != null && loc.getBay1() == 3) |
| | | .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | if (!bay3Locs.isEmpty()) { |
| | | log.debug("优先级5:分配排{}的第三列,库位:{}", row, bay3Locs.get(0).getLocNo()); |
| | | return bay3Locs.get(0); |
| | | } |
| | | } |
| | | |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 为出库到缓存区的任务分配站点(跟随出库逻辑) |
| | | * @param cacheStations 缓存区站点列表 |
| | | * @param ioType 任务类型(101=全板出库,110=空板出库) |
| | | * @return 分配的站点编号,如果无法分配则返回null |
| | | */ |
| | | private String allocateCacheStationForOutbound(List<String> cacheStations, Integer ioType) { |
| | | if (cacheStations == null || cacheStations.isEmpty()) { |
| | | log.warn("缓存区站点列表为空,无法分配站点"); |
| | | return null; |
| | | } |
| | | |
| | | // 将站点字符串列表转换为整数列表 |
| | | List<Integer> siteIntList = cacheStations.stream() |
| | | .map(Integer::parseInt) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 查询所有缓存区站点的设备信息(包含任务数) |
| | | List<BasDevp> devList = basDevpMapper.selectList(new EntityWrapper<BasDevp>() |
| | | .in("dev_no", siteIntList) |
| | | ); |
| | | |
| | | if (devList.isEmpty()) { |
| | | log.warn("缓存区站点{}在设备表中不存在", cacheStations); |
| | | return cacheStations.get(0); // 降级:返回第一个站点 |
| | | } |
| | | |
| | | // 按入库任务数排序(出库到缓存区也使用in_qty字段) |
| | | devList.sort(Comparator.comparing(BasDevp::getInQty)); |
| | | |
| | | // 获取最少任务数 |
| | | int minInQty = devList.get(0).getInQty(); |
| | | |
| | | // 筛选出任务数最少的站点列表 |
| | | List<BasDevp> minTaskSites = devList.stream() |
| | | .filter(dev -> dev.getInQty() == minInQty) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 根据配置选择分配策略 |
| | | String strategy = agvProperties.getSiteAllocation().getStrategy(); |
| | | boolean enableRoundRobin = agvProperties.getSiteAllocation().isEnableRoundRobin(); |
| | | |
| | | List<BasDevp> orderedSites = new ArrayList<>(); |
| | | String groupKey = "west"; // 缓存区使用西侧 |
| | | |
| | | 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())); |
| | | counter.getAndIncrement(); |
| | | 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; |
| | | } |
| | | |
| | | // 依次检查每个站点是否在搬运,找到第一个空闲站点就分配 |
| | | String selectedSite = null; |
| | | List<Integer> checkIoTypes = Arrays.asList(101, 110); // 出库到缓存区的任务类型 |
| | | |
| | | for (BasDevp dev : orderedSites) { |
| | | String staNo = String.valueOf(dev.getDevNo()); |
| | | |
| | | // 检查该站点是否有正在搬运的同类型任务 |
| | | List<Task> transportingTasks = taskService.selectList( |
| | | new EntityWrapper<Task>() |
| | | .eq("sta_no", staNo) |
| | | .eq("task_type", "agv") |
| | | .eq("wrk_sts", 8L) // 只检查正在搬运状态的任务 |
| | | .in("io_type", checkIoTypes) |
| | | .eq("is_deleted", 0) |
| | | ); |
| | | |
| | | if (!transportingTasks.isEmpty()) { |
| | | log.debug("缓存区站点{}有{}个正在搬运的出库AGV任务,检查下一个站点", |
| | | staNo, transportingTasks.size()); |
| | | continue; // 该站点正在搬运,检查下一个站点 |
| | | } |
| | | |
| | | // 找到第一个空闲站点,分配 |
| | | selectedSite = staNo; |
| | | log.info("出库到缓存区任务按规则应分配到站点{},该站点空闲,分配成功", staNo); |
| | | break; |
| | | } |
| | | |
| | | // 如果所有站点都在搬运,则不分配站点 |
| | | if (selectedSite == null) { |
| | | return null; |
| | | } |
| | | |
| | | // 更新站点任务数(出库到缓存区也使用in_qty字段) |
| | | basDevpMapper.incrementInQty(Integer.parseInt(selectedSite)); |
| | | |
| | | return selectedSite; |
| | | } |
| | | } |