| | |
| | | import com.core.exception.CoolException; |
| | | import com.zy.asrs.entity.*; |
| | | import com.zy.asrs.enums.LocStsType; |
| | | import com.zy.asrs.mapper.BasDevpMapper; |
| | | import com.zy.asrs.service.*; |
| | | import com.zy.asrs.service.impl.BasStationServiceImpl; |
| | | import com.zy.asrs.task.AbstractHandler; |
| | |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.transaction.interceptor.TransactionAspectSupport; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | |
| | | |
| | | @Autowired |
| | | private WrkMastLogService wrkMastLogService; |
| | | |
| | | @Autowired |
| | | private BasDevpMapper basDevpMapper; |
| | | |
| | | /** |
| | | * 站点轮询计数器,用于平均分配站点 |
| | | * Key: 站点组标识(如 "east" 或 "west"),Value: 当前轮询索引 |
| | | */ |
| | | private final Map<String, AtomicInteger> siteRoundRobinCounters = new ConcurrentHashMap<>(); |
| | | |
| | | public ReturnT<String> start(WrkMast wrkMast) { |
| | | // 4.入库完成 |
| | |
| | | |
| | | log.info("出库任务完成,生成{}任务,任务ID:{},托盘码:{}", isEmptyPallet ? "空托出库" : "满托出库", outTask.getId(), outTask.getBarcode()); |
| | | |
| | | // 分配缓存库位(whs_type=2) |
| | | LocCache cacheLoc = locCacheService.selectOne(new EntityWrapper<LocCache>() |
| | | .eq("whs_type", agvProperties.getWhsTypeMapping().getCacheArea()) // whs_type=2 缓存区 |
| | | .eq("frozen", 0) |
| | | .eq("loc_sts", LocStsType.LOC_STS_TYPE_O.type) // O.闲置 |
| | | .ne("full_plt", isEmptyPallet ? "Y" : "N") // 空托不选满板库位,满托不选空板库位 |
| | | .orderAsc(Arrays.asList("row1", "bay1", "lev1")) |
| | | .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")); |
| | | |
| | | if (cacheLoc == null) { |
| | | log.warn("没有可用的缓存库位,无法生成{}任务,任务ID:{}", isEmptyPallet ? "空托出库" : "满托出库", outTask.getId()); |
| | | return; |
| | | } |
| | | |
| | | // 获取出库站点(出库任务的staNo是出库站点,将作为空托/满托出库任务的源站点) |
| | | String outboundStaNo = outTask.getStaNo(); |
| | | if (outboundStaNo == null || outboundStaNo.isEmpty()) { |
| | |
| | | return; |
| | | } |
| | | |
| | | // 根据缓存区配置选择站点和机器人组(西侧) |
| | | List<String> cacheStations = agvProperties.getWestStations(); |
| | | String robotGroup = agvProperties.getRobotGroupWest(); |
| | | // 根据出库站点判断是东侧还是西侧 |
| | | Set<String> eastStations = new HashSet<>(agvProperties.getEastStations()); |
| | | Set<String> westStations = new HashSet<>(agvProperties.getWestStations()); |
| | | |
| | | List<String> cacheStations; |
| | | String robotGroup; |
| | | Long targetWhsType; |
| | | String sideName; |
| | | |
| | | if (eastStations.contains(outboundStaNo)) { |
| | | // 东侧出库站点,查找东侧WA库位(whs_type=1) |
| | | cacheStations = agvProperties.getEastStations(); |
| | | robotGroup = agvProperties.getRobotGroupEast(); |
| | | targetWhsType = agvProperties.getWhsTypeMapping().getInboundArea(); // whs_type=1 |
| | | sideName = agvProperties.getEastDisplayName(); // 从配置读取显示名称 |
| | | log.info("出库站点{}在{},查找{}WA库位(whs_type={})", outboundStaNo, sideName, sideName, targetWhsType); |
| | | } else if (westStations.contains(outboundStaNo)) { |
| | | // 西侧出库站点,查找西侧WA库位(whs_type=2) |
| | | cacheStations = agvProperties.getWestStations(); |
| | | robotGroup = agvProperties.getRobotGroupWest(); |
| | | targetWhsType = agvProperties.getWhsTypeMapping().getCacheArea(); // whs_type=2 |
| | | sideName = agvProperties.getWestDisplayName(); // 从配置读取显示名称 |
| | | log.info("出库站点{}在{},查找{}WA库位(whs_type={})", outboundStaNo, sideName, sideName, targetWhsType); |
| | | } else { |
| | | log.warn("出库站点{}不在配置的站点列表中,无法判断{}/{},任务ID:{}", |
| | | outboundStaNo, agvProperties.getEastDisplayName(), agvProperties.getWestDisplayName(), outTask.getId()); |
| | | return; |
| | | } |
| | | |
| | | if (cacheStations.isEmpty()) { |
| | | log.warn("缓存区没有配置站点,无法生成{}任务,任务ID:{}", isEmptyPallet ? "空托出库" : "满托出库", outTask.getId()); |
| | | log.warn("{}侧没有配置站点,无法生成{}任务,任务ID:{}", |
| | | sideName, isEmptyPallet ? "空托出库" : "满托出库", outTask.getId()); |
| | | return; |
| | | } |
| | | |
| | | // 分配缓存库位:只查找WA开头的库位(CA开头只做入库,WA开头才会被出库分配缓存区) |
| | | String cacheAreaPrefix = agvProperties.getLocationPrefix().getCacheArea(); |
| | | LocCache cacheLoc = locCacheService.selectOne(new EntityWrapper<LocCache>() |
| | | .eq("whs_type", targetWhsType) // 根据出库站点判断的whs_type |
| | | .like("loc_no", cacheAreaPrefix + "%") // 只查找WA开头的库位(从配置读取) |
| | | .eq("frozen", 0) |
| | | .eq("loc_sts", LocStsType.LOC_STS_TYPE_O.type) // O.闲置 |
| | | .ne("full_plt", isEmptyPallet ? "Y" : "N") // 空托不选满板库位,满托不选空板库位 |
| | | .orderAsc(Arrays.asList("row1", "bay1", "lev1")) |
| | | .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")); |
| | | |
| | | if (cacheLoc == null) { |
| | | log.warn("{}侧没有可用的{}缓存库位,不生成{}AGV任务,任务ID:{}", |
| | | sideName, cacheAreaPrefix, isEmptyPallet ? "空托出库" : "满托出库", outTask.getId()); |
| | | return; |
| | | } |
| | | |
| | |
| | | outboundStaNo, transportingTasks.size(), outTask.getId()); |
| | | } |
| | | |
| | | // 选择缓存区目标站点(使用第一个可用站点,或可以优化为选择任务最少的站点) |
| | | String cacheStaNo = cacheStations.get(0); |
| | | // 选择缓存区目标站点(使用和入库一样的分配策略:轮询、最少任务数) |
| | | String cacheStaNo = allocateCacheStation(cacheStations, ioType); |
| | | if (cacheStaNo == null) { |
| | | log.warn("无法为出库任务ID:{}分配缓存区站点,所有站点都在使用中", outTask.getId()); |
| | | return; |
| | | } |
| | | |
| | | // 生成工作号 |
| | | int workNo = commonService.getWorkNo(WorkNoType.PAKOUT.type); |
| | |
| | | log.info("成功生成{}任务,任务ID:{},工作号:{},源站点:{},目标站点:{},缓存库位:{}", |
| | | isEmptyPallet ? "空托出库" : "满托出库", cacheTask.getId(), workNo, outboundStaNo, cacheStaNo, cacheLoc.getLocNo()); |
| | | } |
| | | |
| | | /** |
| | | * 为出库到缓存区的任务分配站点(使用和入库一样的分配策略) |
| | | * @param cacheStations 缓存区站点列表 |
| | | * @param ioType 任务类型(101=全板出库,110=空板出库) |
| | | * @return 分配的站点编号,如果无法分配则返回null |
| | | */ |
| | | private String allocateCacheStation(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())); |
| | | 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) |
| | | ); |
| | | |
| | | if (!transportingTasks.isEmpty()) { |
| | | log.debug("缓存区站点{}有{}个正在搬运的出库AGV任务,检查下一个站点", |
| | | staNo, transportingTasks.size()); |
| | | continue; // 该站点正在搬运,检查下一个站点 |
| | | } |
| | | |
| | | // 找到第一个空闲站点,分配 |
| | | selectedSite = staNo; |
| | | log.info("出库到缓存区任务按规则应分配到站点{},该站点空闲,分配成功", staNo); |
| | | break; |
| | | } |
| | | |
| | | // 如果所有站点都在搬运,则不分配站点 |
| | | if (selectedSite == null) { |
| | | log.warn("所有缓存区站点都有正在搬运的出库任务,暂不分配站点,等待空闲"); |
| | | return null; |
| | | } |
| | | |
| | | // 更新站点任务数(出库到缓存区也使用in_qty字段) |
| | | basDevpMapper.incrementInQty(Integer.parseInt(selectedSite)); |
| | | |
| | | return selectedSite; |
| | | } |
| | | |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public ReturnT<String> agvDoIn(Task wrkMast) { |
| | | if (wrkMast.getIoType().equals(1)) { |
| | | LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>().eq("loc_no", wrkMast.getLocNo())); |
| | | if (Objects.isNull(locCache)) { |
| | | throw new CoolException("数据错误,库位不存在!!"); |
| | | } |
| | | if (!locCache.getLocSts().equals(LocStsType.LOC_STS_TYPE_S.type)) { |
| | | throw new CoolException("当前库位状态为:" + LocStsType.LOC_STS_TYPE_S.type + "." + LocStsType.LOC_STS_TYPE_S.desc + ",不是出库预约状态"); |
| | | } |
| | | List<TaskDetl> taskDetls = taskDetlService.selectList(new EntityWrapper<TaskDetl>().eq("task_id", wrkMast.getId())); |
| | | if (Objects.isNull(taskDetls)) { |
| | | throw new CoolException("数据错误:组托数据不存在!!"); |
| | | } |
| | | taskDetls.forEach(pakin -> { |
| | | LocCacheDetl detl = new LocCacheDetl(); |
| | | BeanUtils.copyProperties(pakin, detl); |
| | | detl.setBarcode(pakin.getBarcode()) |
| | | .setLocId(locCache.getId()) |
| | | .setAnfme(pakin.getAnfme()) |
| | | .setBrand(pakin.getBrand()) |
| | | .setAppeTime(new Date()) |
| | | .setSpecs(pakin.getSpecs()) |
| | | .setColor(pakin.getColor()) |
| | | .setLocNo(locCache.getLocNo()) |
| | | .setAreaId(locCache.getAreaId()) |
| | | .setAreaName(locCache.getAreaName()) |
| | | .setUnit(pakin.getUnit()) |
| | | .setBatch(pakin.getBatch()); |
| | | if (!locCacheDetlService.insert(detl)) { |
| | | throw new CoolException("库位明细保存失败!!"); |
| | | Integer ioType = wrkMast.getIoType(); |
| | | if (ioType == null) { |
| | | throw new CoolException("数据错误:ioType为空!!"); |
| | | } |
| | | |
| | | // 处理入库任务类型:1(实托入库)、10(空托入库)、53、57 |
| | | if (ioType == 1 || ioType == 10 || ioType == 53 || ioType == 57) { |
| | | // ioType == 1 需要处理组托数据 |
| | | if (ioType == 1) { |
| | | LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>().eq("loc_no", wrkMast.getLocNo())); |
| | | if (Objects.isNull(locCache)) { |
| | | throw new CoolException("数据错误,库位不存在!!"); |
| | | } |
| | | }); |
| | | if (!locCache.getLocSts().equals(LocStsType.LOC_STS_TYPE_S.type)) { |
| | | throw new CoolException("当前库位状态为:" + LocStsType.LOC_STS_TYPE_S.type + "." + LocStsType.LOC_STS_TYPE_S.desc + ",不是出库预约状态"); |
| | | } |
| | | List<TaskDetl> taskDetls = taskDetlService.selectList(new EntityWrapper<TaskDetl>().eq("task_id", wrkMast.getId())); |
| | | if (Objects.isNull(taskDetls)) { |
| | | throw new CoolException("数据错误:组托数据不存在!!"); |
| | | } |
| | | taskDetls.forEach(pakin -> { |
| | | LocCacheDetl detl = new LocCacheDetl(); |
| | | BeanUtils.copyProperties(pakin, detl); |
| | | detl.setBarcode(pakin.getBarcode()) |
| | | .setLocId(locCache.getId()) |
| | | .setAnfme(pakin.getAnfme()) |
| | | .setBrand(pakin.getBrand()) |
| | | .setAppeTime(new Date()) |
| | | .setSpecs(pakin.getSpecs()) |
| | | .setColor(pakin.getColor()) |
| | | .setLocNo(locCache.getLocNo()) |
| | | .setAreaId(locCache.getAreaId()) |
| | | .setAreaName(locCache.getAreaName()) |
| | | .setUnit(pakin.getUnit()) |
| | | .setBatch(pakin.getBatch()); |
| | | if (!locCacheDetlService.insert(detl)) { |
| | | throw new CoolException("库位明细保存失败!!"); |
| | | } |
| | | }); |
| | | |
| | | locCache.setLocSts(LocStsType.LOC_STS_TYPE_F.type); |
| | | locCache.setModiTime(new Date()); |
| | | locCache.setBarcode(wrkMast.getBarcode()); |
| | | locCache.setModiTime(new Date()); |
| | | locCache.setIoTime(new Date()); |
| | | if (!locCacheService.updateById(locCache)) { |
| | | throw new CoolException("库位状态修改失败!"); |
| | | locCache.setLocSts(LocStsType.LOC_STS_TYPE_F.type); |
| | | locCache.setModiTime(new Date()); |
| | | locCache.setBarcode(wrkMast.getBarcode()); |
| | | locCache.setModiTime(new Date()); |
| | | locCache.setIoTime(new Date()); |
| | | if (!locCacheService.updateById(locCache)) { |
| | | throw new CoolException("库位状态修改失败!"); |
| | | } |
| | | } |
| | | |
| | | // 更新任务状态为5(库存更新完成) |
| | | wrkMast.setWrkSts(5L); |
| | | wrkMast.setModiTime(new Date()); |
| | | if (!taskService.updateById(wrkMast)) { |
| | | throw new CoolException("任务状态修改失败!!"); |
| | | } |
| | | }else { |
| | | throw new CoolException("数据错误:ioType不存在!!"); |
| | | } else { |
| | | // 非入库任务类型,记录日志但不抛出异常,允许其他类型的任务通过 |
| | | log.warn("agvDoIn方法收到非入库任务类型,ioType:{},taskId:{},跳过处理", ioType, wrkMast.getId()); |
| | | } |
| | | |
| | | |
| | | return SUCCESS; |
| | | } |