自动化立体仓库 - WMS系统
#
lty
2026-01-16 053d403d024d9f6cf578ed5ca51800e60b7894ba
src/main/java/com/zy/asrs/service/impl/WorkServiceImpl.java
@@ -190,12 +190,80 @@
            throw new CoolException("库存不存在");
        }
    }
    /**
     * 检查深库位前方是否堵塞(按 locGroupAscOrder 配置的分组和顺序检查)
     * 只检查同一 bay + lev + 当前分组 rowList 内,前方位置是否有货(F) 或 入库任务
     *
     * @param locMast 当前库位主档
     * @param locNo 当前库位号
     * @param locGroupAscOrder 出库优先分组配置
     * @throws CoolException 如果前方有货或入库任务
     */
    private void checkDeepPositionBlocking(LocMast locMast, String locNo, List<LocGroupOrder> locGroupAscOrder) {
        // 查找当前 row 所属分组
        LocGroupOrder currentGroup = locGroupAscOrder.stream()
                .filter(g -> g.getRowList().contains(locMast.getRow1()))
                .findFirst()
                .orElseThrow(() -> new CoolException("当前排不在任何出库优先分组中,无法进行深库位检查: row=" + locMast.getRow1()));
        List<Integer> orderedRows = currentGroup.getRowList();
        // 查询同一 bay + lev + 本组 rowList 内的所有库位
        List<LocMast> sameBayLevLocs = locMastService.selectList(new EntityWrapper<LocMast>()
                .eq("bay1", locMast.getBay1())
                .eq("lev1", locMast.getLev1())
                .in("row1", orderedRows));
        // 按配置顺序重新排列
        List<LocMast> orderedLocs = orderedRows.stream()
                .map(row -> sameBayLevLocs.stream()
                        .filter(lm -> lm.getRow1().equals(row))
                        .findFirst()
                        .orElse(null))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        // 遍历检查前方
        boolean foundSelf = false;
        for (LocMast prevLoc : orderedLocs) {
            if (prevLoc.getLocNo().equals(locNo)) {
                foundSelf = true;
                break;  // 到达自己位置,停止
            }
            // 前方有货(F状态)
            if ("F".equals(prevLoc.getLocSts())) {
                throw new CoolException(locNo + " 的前方位置 " + prevLoc.getLocNo() + " 有货,禁止出库");
            }
            // 前方有入库任务(假设 io_type=100 为入库,根据实际调整)
            // 这里只查待发送的任务(wrk_sts=0),可根据需要加 wrk_sts=1 等
            WrkMast frontTask = wrkMastService.selectOne(
                    new EntityWrapper<WrkMast>()
                            .eq("source_loc_no", prevLoc.getLocNo())  // 入库任务的源位置
                            .eq("io_type", 100)                       // 入库类型(请确认你的系统定义)
                            .eq("wrk_sts", 0L)                        // 待发送
            );
            if (frontTask != null) {
                throw new CoolException(locNo + " 的前方位置 " + prevLoc.getLocNo() + " 存在入库任务,禁止出库");
            }
        }
        if (!foundSelf) {
            throw new CoolException("分组内未找到目标库位,请检查数据一致性: " + locNo);
        }
    }
    @Override
    @Transactional
    public void stockOut(BasDevp staNo, List<LocDetlDto> locDetlDtos, IoWorkType ioWorkType, Long userId) {
        Date now = new Date();
        // 合并同类项
        // 从配置获取分组(和空板出库保持一致)
        List<LocGroupOrder> locGroupAscOrder = slaveProperties.getLocGroupAscOrder();
        // 合并同类项(相同 locNo 的明细合并)
        Set<String> locNos = new HashSet<>();
        List<OutLocDto> dtos = new ArrayList<>();
        for (LocDetlDto locDetlDto : locDetlDtos) {
@@ -212,59 +280,47 @@
                dtos.add(new OutLocDto(locNo, locDetlDto));
            }
        }
        Integer ioType = null;
        // 按列和层分组库位
        // 按列和层分组库位(bay + lev)
        Map<String, List<OutLocDto>> locGroups = dtos.stream()
                .collect(Collectors.groupingBy(dto -> {
                    String column = dto.getLocNo().substring(3, 6); // 列号
                    String level = dto.getLocNo().substring(6);    // 层号
                    return column + "-" + level;                  // 组合成唯一的组标识
                    String level = dto.getLocNo().substring(6);     // 层号
                    return column + "-" + level;
                }));
        // 遍历每组
        for (Map.Entry<String, List<OutLocDto>> entry : locGroups.entrySet()) {
            String groupKey = entry.getKey(); // 组标识,例如 "003-02" 表示第3列第2层
            List<OutLocDto> groupDtos = entry.getValue();
            // 组内优先级重新从100开始
            // 组内优先级重新从100开始(原有逻辑)
            double priority = 100;
            // 排序组内库位(排号倒序)
            // 排序组内库位(排号倒序,原有逻辑)
            groupDtos.sort(Comparator.comparing((OutLocDto dto) -> Integer.valueOf(dto.getLocNo().substring(0, 2))).reversed());
            for (OutLocDto dto : groupDtos) {
                String locNo = dto.getLocNo();
                // 深库位规则检查,仅检查当前列和层内的前库位
                int currentRow = Integer.valueOf(locNo.substring(0, 2));
                for (int i = currentRow + 1; i <= 5; i++) { // 深库位为 1-5 排
                    String frontLoc = String.format("%02d%s", i, locNo.substring(2));
                    LocMast locMastFront = locMastService.selectOne(new EntityWrapper<LocMast>()
                            .eq("loc_no", frontLoc).eq("loc_sts", "F"));
                    if (!Cools.isEmpty(locMastFront)) {
                        throw new CoolException(locNo + " 的前库位 " + frontLoc + " 有货");
                    }
                    // 判断前库位是否有入库任务
                    WrkMast wrkMastFront = wrkMastService.selectOne(new EntityWrapper<WrkMast>().eq("loc_no", frontLoc));
                    if (!Cools.isEmpty(wrkMastFront)) {
                        throw new CoolException(locNo + " 的前库位 " + frontLoc + " 有入库任务");
                    }
                }
                // 计算优先级
                dto.setPriority(priority);
                priority--;
                // 获取库位
                LocMast locMast = locMastService.selectById(dto.getLocNo());
                // 获取库位信息(提前获取,用于检查和后续使用)
                LocMast locMast = locMastService.selectById(locNo);
                if (Cools.isEmpty(locMast)) {
                    throw new CoolException(dto.getLocNo() + "库位不存在");
                    throw new CoolException(locNo + "库位不存在");
                }
                if (!locMast.getLocSts().equals("F")) {
                    throw new CoolException(dto.getLocNo() + "托盘非在库状态");
                    throw new CoolException(locNo + "托盘非在库状态");
                }
                // =====================================
                // 深库位前方堵塞检查(新逻辑,按分组顺序)
                checkDeepPositionBlocking(locMast, locNo, locGroupAscOrder);
                // =====================================
                // 计算优先级(原有逻辑)
                dto.setPriority(priority);
                priority--;
                // 判断入出库类型
                if (ioWorkType == null) {
@@ -281,7 +337,7 @@
                // 生成工作号
                int workNo = commonService.getWorkNo(WorkNoType.getWorkNoType(ioType));
                String pick = ioType == 101 ? "N":"Y";
                String pick = ioType == 101 ? "N" : "Y";
                // 生成工作档
                WrkMast wrkMast = new WrkMast();
@@ -293,7 +349,7 @@
                wrkMast.setCrnNo(locMast.getCrnNo());
                wrkMast.setSourceStaNo(staDesc.getCrnStn());
                wrkMast.setStaNo(staDesc.getStnNo());
                wrkMast.setSourceLocNo(dto.getLocNo());
                wrkMast.setSourceLocNo(locNo);
                wrkMast.setFullPlt("Y");
                wrkMast.setPicking(pick);
                wrkMast.setExitMk("N");
@@ -305,7 +361,7 @@
                wrkMast.setModiUser(userId);
                wrkMast.setModiTime(now);
                if (!wrkMastService.insert(wrkMast)) {
                    throw new CoolException("保存工作档失败,出库库位号:" + dto.getLocNo());
                    throw new CoolException("保存工作档失败,出库库位号:" + locNo);
                }
                // 生成工作档明细
@@ -330,16 +386,16 @@
                }
                // 修改库位状态
                locMast = locMastService.selectById(dto.getLocNo());
                locMast = locMastService.selectById(locNo);  // 重新获取最新状态
                if (locMast.getLocSts().equals("F")) {
                    locMast.setLocSts(ioType == 101 ? "R" : "P");
                    locMast.setModiUser(userId);
                    locMast.setModiTime(now);
                    if (!locMastService.updateById(locMast)) {
                        throw new CoolException("预约库位状态失败,库位号:" + dto.getLocNo());
                        throw new CoolException("预约库位状态失败,库位号:" + locNo);
                    }
                } else {
                    throw new CoolException(dto.getLocNo() + "库位不是在库状态");
                    throw new CoolException(locNo + "库位不是在库状态");
                }
            }
        }
@@ -485,40 +541,174 @@
        }
        return dto.getLocNo();
    }
    /**
     * 检查空板出库分组内选中连续段的双向前方清空情况,并返回出库方向与排序后的库位列表
     *
     * @param selectedLocNos 选中的库位号列表
     * @param locGroupAscOrder 出库分组配置
     * @return LockingCheckResultParam 包含校验结果、方向、排序后库位列表
     */
    private LockingCheckResultParam checkEmptyPlateBlocking(
            List<String> selectedLocNos,
            List<LocGroupOrder> locGroupAscOrder) {
        if (Cools.isEmpty(selectedLocNos)) {
            return LockingCheckResultParam.success(Collections.emptyList());
        }
        // 1. 查询所有选中的库位信息
        List<LocMast> selectedMasts = locMastService.selectList(
                new EntityWrapper<LocMast>().in("loc_no", selectedLocNos)
        );
        if (selectedMasts.size() != selectedLocNos.size()) {
            return LockingCheckResultParam.fail("部分选中库位不存在或数据异常");
        }
        // 2. 按分组聚合选中的库位(支持多分组)
        Map<LocGroupOrder, List<LocMast>> groupSelected = new HashMap<>();
        for (LocMast lm : selectedMasts) {
            LocGroupOrder group = locGroupAscOrder.stream()
                    .filter(g -> g.getRowList().contains(lm.getRow1()))
                    .findFirst()
                    .orElseThrow(() -> new CoolException("排不在出库分组配置中: row=" + lm.getRow1()));
            groupSelected.computeIfAbsent(group, k -> new ArrayList<>()).add(lm);
        }
        // 由于通常一次请求在一个分组内,这里取第一个分组的结果
        // 如果需要支持多分组同时出库,可改为返回 Map<LocGroupOrder, LockingCheckResultParam>
        LockingCheckResultParam result = null;
        for (Map.Entry<LocGroupOrder, List<LocMast>> entry : groupSelected.entrySet()) {
            LocGroupOrder group = entry.getKey();
            List<LocMast> selected = entry.getValue();
            List<Integer> fullRows = group.getRowList();  // 如 [3,4,5,6,7,8,9,10]
            // 获取选中的 row,并按分组顺序排序
            List<Integer> selectedRows = selected.stream()
                    .map(LocMast::getRow1)
                    .distinct()
                    .sorted(Comparator.comparingInt(fullRows::indexOf))
                    .collect(Collectors.toList());
            // 检查是否重复或无效
            if (selectedRows.size() != selected.size()) {
                return LockingCheckResultParam.fail("选中库位存在重复或无效排");
            }
            int minIndex = fullRows.indexOf(selectedRows.get(0));
            int maxIndex = fullRows.indexOf(selectedRows.get(selectedRows.size() - 1));
            // 1. 必须是连续段(无缺口)
            if (maxIndex - minIndex + 1 != selectedRows.size()) {
                return LockingCheckResultParam.fail(
                        "选中排必须连续,无缺口。从 " + fullRows.get(minIndex) + " 到 " + fullRows.get(maxIndex)
                );
            }
            // 2. 检查左前方(正序方向:从左到右,前方是索引小的位置)
            boolean leftClear = true;
            for (int i = 0; i < minIndex; i++) {
                LocMast prev = getLocMastByRow(fullRows.get(i), selected.get(0).getBay1(), selected.get(0).getLev1());
                if (prev != null && ("D".equals(prev.getLocSts()) || "F".equals(prev.getLocSts()))) {
                    leftClear = false;
                    break;
                }
            }
            // 3. 检查右前方(倒序方向:从右到左,前方是索引大的位置)
            boolean rightClear = true;
            for (int i = maxIndex + 1; i < fullRows.size(); i++) {
                LocMast prev = getLocMastByRow(fullRows.get(i), selected.get(0).getBay1(), selected.get(0).getLev1());
                if (prev != null && ("D".equals(prev.getLocSts()) || "F".equals(prev.getLocSts()))) {
                    rightClear = false;
                    break;
                }
            }
            // 4. 至少有一侧清空才允许出库
            if (!leftClear && !rightClear) {
                return LockingCheckResultParam.fail(
                        "选中段 " + fullRows.get(minIndex) + "~" + fullRows.get(maxIndex) +
                                " 两侧前方都有空板/故障,无法出库(正序或倒序方向都堵塞)"
                );
            }
            // 5. 选中段内所有库位必须是 D 状态
            for (LocMast lm : selected) {
                if (!"D".equals(lm.getLocSts())) {
                    return LockingCheckResultParam.fail("选中库位非空板状态: " + lm.getLocNo());
                }
            }
            // 6. 决定出库方向和排序顺序
            String direction;
            List<LocMast> sortedSelected;
            // 优先选择正序(如果两侧都清空,默认正序)
            if (leftClear) {
                direction = "ASC";
                sortedSelected = selected.stream()
                        .sorted(Comparator.comparingInt(m -> fullRows.indexOf(m.getRow1())))
                        .collect(Collectors.toList());
            } else {
                direction = "DESC";
                sortedSelected = selected.stream()
                        .sorted(Comparator.comparingInt(m -> -fullRows.indexOf(m.getRow1()))) // 倒序
                        .collect(Collectors.toList());
            }
            result = LockingCheckResultParam.success(direction, sortedSelected);
        }
        // 如果没有分组(理论上不会发生),返回默认成功
        return result != null ? result : LockingCheckResultParam.success(Collections.emptyList());
    }
    // 辅助方法:根据 row 获取 LocMast
    private LocMast getLocMastByRow(Integer row, Integer bay1, Integer lev1) {
        return locMastService.selectOne(new EntityWrapper<LocMast>()
                .eq("bay1", bay1)
                .eq("lev1", lev1)
                .eq("row1", row));
    }
    @Override
    @Transactional
    public void emptyPlateOut(EmptyPlateOutParam param, Long userId) {
        if (Cools.isEmpty(param.getOutSite())) {
            throw new CoolException("站点不存在");
        }
        for (String locNo : param.getLocNos()) {
        // 使用新的出库专用分组配置
        List<LocGroupOrder> locGroupAscOrder = slaveProperties.getLocGroupAscOrderOut();
        // 1. 先进行整体阻塞检查(包含连续性 + 双向前方清空校验)
        LockingCheckResultParam checkResult = checkEmptyPlateBlocking(param.getLocNos(), locGroupAscOrder);
        if (!checkResult.isSuccess()) {
            throw new CoolException(checkResult.getErrorMessage());
        }
        // 2. 获取按出库方向排序好的库位列表
        List<LocMast> orderedLocMasts = checkResult.getSortedSelected();
        // 3. 优先级基础参数(数值越大优先级越高)
        double BASE_PRI = 10.0;          // 最低优先级组基准(数值最大)
        double GROUP_STEP = 100.0;       // 组间差距
        double IN_GROUP_STEP = 1.0;      // 组内位置差距
        Date now = new Date();
        // 4. 逐个处理排序后的库位,生成出库任务
        for (int index = orderedLocMasts.size(); index > 0; index--) {
            LocMast locMast = orderedLocMasts.get(index);
            String locNo = locMast.getLocNo();
            // 获取工作号
            int workNo = commonService.getWorkNo(WorkNoType.PAKOUT.type);
            // 获取库位
            LocMast locMast = locMastService.selectById(locNo);
            if (Cools.isEmpty(locMast)) {
                throw new CoolException(locNo+"库位不存在");
            }
            if (!locMast.getLocSts().equals("D")){
                throw new CoolException("所选库位存在状态不为D的库位,库位号:"+locMast.getLocNo()+" 、当前状态:"+locMast.getLocSts()+"-"+locMast.getLocSts$());
            }
            boolean res1 = true;
            if(param.getOutSite()==100){
                res1 = false;
            }
            List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                    .eq("bay1", locMast.getBay1())
                    .eq("lev1",locMast.getLev1())
                    .orderBy("row1",res1));
            for (LocMast locMast1 : locMasts) {
                if (locMast1.getLocNo().equals(locMast.getLocNo())) {
                    break;
                }
                if (locMast1.getLocSts().equals("D") || locMast1.getLocSts().equals("F")) {
                    throw new CoolException(locNo+"库位堵塞,禁止出库");
                }
            }
            // 获取源站
            Wrapper<StaDesc> wrapper = new EntityWrapper<StaDesc>()
                    .eq("type_no", 110)
@@ -527,45 +717,68 @@
            StaDesc staDesc = staDescService.selectOne(wrapper);
            Integer sourceStaNo = staDesc.getCrnStn();
            if (Cools.isEmpty(sourceStaNo)) {
                throw new CoolException("检索源站失败");
                throw new CoolException("检索源站失败,库位:" + locNo);
            }
            Date now = new Date();
            // 保存工作档
            // 计算动态优先级(组优先级 + 组内顺序偏移)
            // 这里假设所有选中的库位在同一个分组(多分组可再细分)
            int groupIndex = locGroupAscOrder.indexOf(
                    locGroupAscOrder.stream()
                            .filter(g -> g.getRowList().contains(locMast.getRow1()))
                            .findFirst()
                            .orElse(null)
            );
            if (groupIndex == -1) {
                throw new CoolException("分组索引异常: " + locNo);
            }
            double groupBasePri = BASE_PRI + (locGroupAscOrder.size() + 1 - groupIndex) * GROUP_STEP;
            // 组内偏移:按出库顺序(index越小越先出,优先级越高 → 数值越大)
            // 如果是 DESC 方向,index 已经是倒序的,所以直接用
            double inGroupOffset = index * IN_GROUP_STEP;
            double ioPri = groupBasePri + inGroupOffset;  // 数值越大优先级越高
            // 生成工作档
            WrkMast wrkMast = new WrkMast();
            wrkMast.setWrkNo(workNo);
            wrkMast.setIoTime(now);
            wrkMast.setWrkSts(0L); // 工作状态:0.待发送
            wrkMast.setIoType(110); // 入出库状态: 110.空板出库
            wrkMast.setIoPri(10D);
            wrkMast.setSourceStaNo(sourceStaNo); // 源站
            wrkMast.setStaNo(param.getOutSite()); // 目标站
            wrkMast.setWrkSts(0L);           // 待发送
            wrkMast.setIoType(110);          // 空板出库
            wrkMast.setIoPri(ioPri);
            wrkMast.setSourceStaNo(sourceStaNo);
            wrkMast.setStaNo(param.getOutSite());
            wrkMast.setCrnNo(locMast.getCrnNo());
            wrkMast.setSourceLocNo(locNo); // 源库位
            wrkMast.setFullPlt("N"); // 满板:Y
            wrkMast.setPicking("N"); // 拣料
            wrkMast.setExitMk("N"); // 退出
            wrkMast.setEmptyMk("Y"); // 空板
            wrkMast.setSourceLocNo(locNo);
            wrkMast.setFullPlt("N");
            wrkMast.setPicking("N");
            wrkMast.setExitMk("N");
            wrkMast.setEmptyMk("Y");
            wrkMast.setLinkMis("N");
            wrkMast.setAppeUser(userId);
            wrkMast.setAppeTime(now);
            wrkMast.setModiUser(userId);
            wrkMast.setModiTime(now);
            boolean res = wrkMastService.insert(wrkMast);
            if (!res) {
                throw new CoolException("保存工作档失败");
            if (!wrkMastService.insert(wrkMast)) {
                throw new CoolException("保存工作档失败,库位:" + locNo);
            }
            // 更新库位状态 D.空板 -> R.出库预约
            if (locMast.getLocSts().equals("D")){
            // 更新库位状态 D → R(出库预约)
            if ("D".equals(locMast.getLocSts())) {
                locMast.setLocSts("R");
                locMast.setModiUser(userId);
                locMast.setModiTime(now);
                if (!locMastService.updateById(locMast)) {
                    throw new CoolException("更新库位状态失败");
                    throw new CoolException("更新库位状态失败,库位:" + locNo);
                }
            } else {
                throw new CoolException("库位状态异常,非空板状态:" + locNo);
            }
        }
    }
    @Override
    @Transactional
    public WrkMast emptyPlateOut(EmptyPlateOutParam param) {