自动化立体仓库 - WMS系统
chen.lin
4 天以前 10778ff6207c31641187acb487d4b67c0de59b24
src/main/java/com/zy/asrs/service/impl/MobileServiceImpl.java
@@ -25,6 +25,7 @@
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;
@@ -128,6 +129,9 @@
    @Resource
    private AgvProperties agvProperties;
    @Autowired
    private com.zy.asrs.task.handler.AgvHandler agvHandler;
    /**
     * 站点轮询计数器,用于平均分配站点
     * Key: 站点组标识(如 "east" 或 "west"),Value: 当前轮询索引
@@ -214,7 +218,7 @@
                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搬运任务!");
        }
@@ -253,10 +257,13 @@
        // 获取工作号
        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.入库
@@ -1354,23 +1361,54 @@
     */
    @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();
    }
@@ -1441,9 +1479,12 @@
    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.库格移载
@@ -1528,15 +1569,20 @@
        }
        // 获取工作号
        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")// 退出
@@ -1545,9 +1591,9 @@
                .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("保存工作档失败");
        }
@@ -1588,4 +1634,448 @@
            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;
    }
}