自动化立体仓库 - WMS系统
#
zwl
11 小时以前 27c184d7af2775a4d647348ee508f4de335df8fb
#
14个文件已修改
1499 ■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/OpenController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/WorkController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/BasDevp.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/OpenService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/WorkServiceImpl.java 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/utils/Utils.java 453 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/entity/Parameter.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/service/CommonService.java 781 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/web/WcsController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/controller/ConfigController.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basDevp/basDevp.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basDevp/basDevp_detail.html 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/config/config_detail.html 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/OpenController.java
@@ -140,6 +140,27 @@
    }
    /**
     * 执行订单出库
     */
    @PostMapping("/order/pakout/execute/default/v1")
    @AppAuth(memo = "执行订单出库")
    public synchronized R pakoutOrderExecute(@RequestHeader(required = false) String appkey,
                                             @RequestBody OpenOrderPakoutExecuteParam param,
                                             HttpServletRequest request) {
        auth(appkey, param, request);
        if (Cools.isEmpty(param)) {
            return R.parse(BaseRes.PARAM);
        }
        if (Cools.isEmpty(param.getOrderId())) {
            return R.error("出库单号[orderId]不能为空");
        }
        if (Cools.isEmpty(param.getExecute())) {
            return R.error("执行动作[execute]不能为空");
        }
        return openService.pakoutOrderExecute(param);
    }
    /**
     * pause out order
     */
    @PostMapping("/order/pakout/pause/default/v1")
@@ -499,3 +520,4 @@
        return R.ok();
    }
}
src/main/java/com/zy/asrs/controller/WorkController.java
@@ -10,6 +10,7 @@
import com.zy.asrs.entity.param.StockOutParam;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.WorkService;
import com.zy.asrs.utils.Utils;
import com.zy.common.model.StartupDto;
import com.zy.common.web.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
@@ -47,6 +48,14 @@
        return R.ok().add(basDevpService.getAvailableEmptyInSite());
    }
    @RequestMapping("/test/station/storage/crn/list")
    @ManagerAuth(memo = "测试站点库区堆垛机顺序")
    public R testStationStorageCrnList(@RequestParam Integer stationId,
                                       @RequestParam(required = false) Integer locType1,
                                       @RequestParam(required = false) String matnr) {
        List<Map<String, Integer>> stationStorageAreaName = Utils.getStationStorageAreaName(stationId, locType1, matnr);
        return R.ok().add(stationStorageAreaName);
    }
    @RequestMapping("/available/take/site")
    @ManagerAuth()
    public R availableTakeSite(){
@@ -185,3 +194,4 @@
    }
}
src/main/java/com/zy/asrs/entity/BasDevp.java
@@ -14,6 +14,7 @@
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
@Data
@TableName("asr_bas_devp")
@@ -140,7 +141,7 @@
    @TableField("io_time")
    private Date ioTime;
    @ApiModelProperty(value= "")
    @ApiModelProperty(value= "绑定库区")
    private String area;
    @ApiModelProperty(value= "")
@@ -226,6 +227,23 @@
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.ioTime);
    }
    public String getArea$() {
        if (Cools.isEmpty(this.area)) {
            return "";
        }
        String normalized = this.area.trim();
        String upper = normalized.toUpperCase(Locale.ROOT);
        if ("1".equals(normalized) || "A".equals(upper) || "A区".equals(upper) || "A库".equals(upper) || "A库区".equals(upper)) {
            return "A库区";
        }
        if ("2".equals(normalized) || "B".equals(upper) || "B区".equals(upper) || "B库".equals(upper) || "B库区".equals(upper)) {
            return "B库区";
        }
        if ("3".equals(normalized) || "C".equals(upper) || "C区".equals(upper) || "C库".equals(upper) || "C库区".equals(upper)) {
            return "C库区";
        }
        return normalized;
    }
    public String getLocType1$() {
        if (null == this.locType1){ return null; }
        switch (this.locType1){
@@ -301,3 +319,4 @@
    }
}
src/main/java/com/zy/asrs/service/OpenService.java
@@ -31,6 +31,11 @@
    R pakoutOrderPause(OpenOrderPakoutPauseParam param);
    /**
     * execute out order
     */
    R pakoutOrderExecute(OpenOrderPakoutExecuteParam param);
    /**
     * 入库单回写
     */
    List<OpenOrderCompeteResult> pakoutOrderComplete(OpenOrderCompleteParam param);
@@ -76,3 +81,4 @@
     */
    R outOrder(OutTaskParam  param);
}
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java
@@ -21,6 +21,8 @@
import com.zy.common.constant.ArmConstant;
import com.zy.common.model.DetlDto;
import com.zy.common.model.LocDetlDto;
import com.zy.common.model.LocDto;
import com.zy.common.model.TaskDto;
import com.zy.common.model.enums.WorkNoType;
import com.zy.common.service.CommonService;
import com.zy.common.utils.HttpHandler;
@@ -92,6 +94,10 @@
    private WrkMastService wrkMastService;
    @Autowired
    private WcsApiService wcsApiService;
    @Autowired
    private WorkService workService;
    @Autowired
    private BasCrnpService basCrnpService;
    @Override
    @Transactional
@@ -420,6 +426,27 @@
        return R.ok("pause out success").add(result);
    }
    @Override
    @Transactional
    public R pakoutOrderExecute(OpenOrderPakoutExecuteParam param) {
        if (param == null || Cools.isEmpty(param.getOrderId())) {
            throw new CoolException("orderId is empty");
        }
        if (param.getExecute() == null) {
            throw new CoolException("execute is empty");
        }
        if (Objects.equals(param.getExecute(), 1)) {
            return createPakoutTasks(param.getOrderId());
        }
        if (Objects.equals(param.getExecute(), 2)) {
            OpenOrderPakoutPauseParam pauseParam = new OpenOrderPakoutPauseParam();
            pauseParam.setOrderNo(param.getOrderId());
            pauseParam.setReason("OPEN_API_PAUSE");
            return pakoutOrderPause(pauseParam);
        }
        throw new CoolException("execute only supports 1 or 2");
    }
    private List<WrkMast> findActiveOutboundTasks(String orderNo) {
        List<WrkDetl> wrkDetls = wrkDetlService.selectList(new EntityWrapper<WrkDetl>().eq("order_no", orderNo));
        if (wrkDetls == null || wrkDetls.isEmpty()) {
@@ -434,13 +461,116 @@
        }
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>()
                .in("wrk_no", wrkNos)
                .in("io_type", Arrays.asList(101, 108, 110))
                .in("io_type", Arrays.asList(101, 103, 107, 108, 110))
                .lt("wrk_sts", 14L));
        if (wrkMasts == null || wrkMasts.isEmpty()) {
            return Collections.emptyList();
        }
        wrkMasts.sort(Comparator.comparing(WrkMast::getWrkNo));
        return wrkMasts;
    }
    private R createPakoutTasks(String orderNo) {
        Order order = orderService.selectByNo(orderNo);
        if (order == null) {
            throw new CoolException("order not found: " + orderNo);
        }
        if (order.getSettle() != 1L && order.getSettle() != 2L) {
            throw new CoolException("该订单已处理");
        }
        List<OrderDetl> orderDetls = orderDetlService.selectByOrderId(order.getId());
        if (Cools.isEmpty(orderDetls)) {
            throw new CoolException("订单明细为空");
        }
        Set<String> exist = new HashSet<>();
        List<LocDto> locDtos = new ArrayList<>();
        List<String> lackDetails = new ArrayList<>();
        for (OrderDetl orderDetl : orderDetls) {
            double issued = Optional.ofNullable(orderDetl.getAnfme()).orElse(0.0D) - Optional.ofNullable(orderDetl.getWorkQty()).orElse(0.0D);
            if (issued <= 0.0D) {
                continue;
            }
            List<LocDetl> locDetls = locDetlService.queryStockAll(null, exist,
                    orderDetl.getMatnr(), orderDetl.getBatch(), orderDetl.getBrand(),
                    orderDetl.getStandby1(), orderDetl.getStandby2(), orderDetl.getStandby3(),
                    orderDetl.getBoxType1(), orderDetl.getBoxType2());
            for (LocDetl locDetl : locDetls) {
                if (issued <= 0.0D) {
                    break;
                }
                LocMast locMast = locMastService.selectOne(new EntityWrapper<LocMast>().eq("loc_no", locDetl.getLocNo()));
                if (locMast == null) {
                    continue;
                }
                BasCrnp basCrnp = basCrnpService.selectOne(new EntityWrapper<BasCrnp>().eq("crn_no", locMast.getCrnNo()));
                if (basCrnp == null || !"Y".equalsIgnoreCase(basCrnp.getOutEnable())) {
                    continue;
                }
                double allocateQty = issued >= locDetl.getAnfme() ? locDetl.getAnfme() : issued;
                LocDto locDto = new LocDto(locDetl.getLocNo(), locDetl.getMatnr(), locDetl.getMaktx(), locDetl.getBatch(), orderDetl.getOrderNo(), allocateQty);
                locDto.setFrozen(locDetl.getFrozen());
                locDto.setFrozenLoc(locMast.getFrozen());
                locDto.setBrand(orderDetl.getBrand());
                locDto.setStandby1(orderDetl.getStandby1());
                locDto.setStandby2(orderDetl.getStandby2());
                locDto.setStandby3(orderDetl.getStandby3());
                locDto.setBoxType1(orderDetl.getBoxType1());
                locDto.setBoxType2(orderDetl.getBoxType2());
                locDto.setBoxType3(orderDetl.getBoxType3());
                locDto.setStaNos(staDescService.queryOutStaNosByLocNo(locDetl.getLocNo(), allocateQty >= locDetl.getAnfme() ? 101 : 103));
                if (Cools.isEmpty(locDto.getStaNos())) {
                    continue;
                }
                locDtos.add(locDto);
                exist.add(locDetl.getLocNo());
                issued -= allocateQty;
            }
            if (issued > 0.0D) {
                lackDetails.add(buildLackDetail(orderDetl, issued));
            }
        }
        List<LocDto> availableLocDtos = new ArrayList<>();
        for (LocDto locDto : locDtos) {
            if (locDto.getFrozen() != 1 && locDto.getFrozenLoc() != 1) {
                availableLocDtos.add(locDto);
            }
        }
        if (Cools.isEmpty(availableLocDtos)) {
            throw new CoolException(Cools.isEmpty(lackDetails) ? "未生成任何出库任务" : "库存不足");
        }
        List<TaskDto> taskDtos = new ArrayList<>();
        for (LocDto locDto : availableLocDtos) {
            TaskDto taskDto = new TaskDto(locDto.getLocNo(), locDto.getStaNo(), locDto);
            if (TaskDto.has(taskDtos, taskDto)) {
                TaskDto dto = TaskDto.find(taskDtos, taskDto);
                if (dto != null) {
                    dto.getLocDtos().addAll(taskDto.getLocDtos());
                }
            } else {
                taskDtos.add(taskDto);
            }
        }
        for (TaskDto taskDto : taskDtos) {
            BasDevp staNo = basDevpService.checkSiteStatus(taskDto.getStaNo());
            workService.stockOut(staNo, taskDto, 9527L);
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("orderId", orderNo);
        result.put("createdTaskCount", taskDtos.size());
        result.put("allocatedDetailCount", availableLocDtos.size());
        result.put("lackDetailCount", lackDetails.size());
        result.put("lackDetails", lackDetails);
        return R.ok("execute out success").add(result);
    }
    private String buildLackDetail(OrderDetl orderDetl, double lackQty) {
        return orderDetl.getMatnr() + "|batch=" + (orderDetl.getBatch() == null ? "" : orderDetl.getBatch()) + "|lack=" + lackQty;
    }
    private boolean needNotifyWcsStop(WrkMast wrkMast) {
@@ -1253,3 +1383,4 @@
        return R.ok();
    }
}
src/main/java/com/zy/asrs/service/impl/WorkServiceImpl.java
@@ -692,7 +692,12 @@
        // 判断是否是盘点单
        String orderNo = taskDto.getLocDtos().get(0).getOrderNo();
        OrderPakout orderPakout = orderPakOutService.selectByNo(orderNo);
        int ioType = orderPakout.getDocType() == 8 ? 107 : (taskDto.isAll() ? 101 : 103);
        Order order = orderPakout == null ? orderService.selectByNo(orderNo) : null;
        if (orderPakout == null && order == null) {
            throw new CoolException("订单不存在:" + orderNo);
        }
        Long docType = orderPakout != null ? orderPakout.getDocType() : order.getDocType();
        int ioType = Objects.equals(docType, 8L) ? 107 : (taskDto.isAll() ? 101 : 103);
        StaDesc staDesc = staDescService.queryCrnStnAuto(ioType, locMast.getCrnNo(), staNo.getDevNo());
        List<LocMast> list = locMastMapper.selectList(
                new EntityWrapper<LocMast>()
@@ -755,9 +760,14 @@
        // 生成工作档明细
        for (LocDto locDto : taskDto.getLocDtos()) {
            if (locDto.getAnfme()==null || locDto.getAnfme() <= 0.0D) { continue; }
//            OrderDetl orderDetl = orderDetlService.selectItem(locDto.getOrderNo(), locDto.getMatnr(), locDto.getBatch());
            OrderDetl orderDetl = OrderInAndOutUtil.selectItem(Boolean.FALSE, locDto.getOrderNo(), locDto.getMatnr(), locDto.getBatch(),locDto.getBrand()
            OrderDetl orderDetl = orderPakout != null
                    ? OrderInAndOutUtil.selectItem(Boolean.FALSE, locDto.getOrderNo(), locDto.getMatnr(), locDto.getBatch(),locDto.getBrand()
                    ,locDto.getStandby1(),locDto.getStandby2(),locDto.getStandby3(),locDto.getBoxType1(),locDto.getBoxType2(),locDto.getBoxType3())
                    : orderDetlService.selectItem(locDto.getOrderNo(), locDto.getMatnr(), locDto.getBatch(),locDto.getBrand()
                    ,locDto.getStandby1(),locDto.getStandby2(),locDto.getStandby3(),locDto.getBoxType1(),locDto.getBoxType2(),locDto.getBoxType3());
            if (orderDetl == null) {
                throw new CoolException("订单明细不存在:" + locDto.getOrderNo() + ", " + locDto.getMatnr());
            }
//            if (orderDetl == null) {
////                orderDetl = orderDetlService.selectItem(locDto.getOrderNo(), locDto.getMatnr(), null);
//                orderDetl = OrderInAndOutUtil.selectItem(Boolean.FALSE, locDto.getOrderNo(), locDto.getMatnr(), null);
@@ -792,11 +802,22 @@
//                throw new CoolException("修改订单明细数量失败");
//            }
//            orderService.updateSettle(orderDetl.getOrderId(), 2L, userId);
            OrderInAndOutUtil.increaseWorkQty(Boolean.FALSE,orderDetl.getOrderId(), orderDetl.getMatnr(), orderDetl.getBatch(),
                    orderDetl.getBrand(),orderDetl.getStandby1(),orderDetl.getStandby2(),orderDetl.getStandby3(),
                    orderDetl.getBoxType1(),orderDetl.getBoxType2(),orderDetl.getBoxType3()
                    , locDto.getAnfme());
            OrderInAndOutUtil.updateOrder(Boolean.FALSE,orderDetl.getOrderId(), 2L, userId);
            if (orderPakout != null) {
                OrderInAndOutUtil.increaseWorkQty(Boolean.FALSE,orderDetl.getOrderId(), orderDetl.getMatnr(), orderDetl.getBatch(),
                        orderDetl.getBrand(),orderDetl.getStandby1(),orderDetl.getStandby2(),orderDetl.getStandby3(),
                        orderDetl.getBoxType1(),orderDetl.getBoxType2(),orderDetl.getBoxType3()
                        , locDto.getAnfme());
                OrderInAndOutUtil.updateOrder(Boolean.FALSE,orderDetl.getOrderId(), 2L, userId);
            } else {
                if (!orderDetlService.increaseWorkQty(orderDetl.getOrderId(), orderDetl.getMatnr(), orderDetl.getBatch(),
                        orderDetl.getBrand(), orderDetl.getStandby1(), orderDetl.getStandby2(), orderDetl.getStandby3(),
                        orderDetl.getBoxType1(), orderDetl.getBoxType2(), orderDetl.getBoxType3(), locDto.getAnfme())) {
                    throw new CoolException("修改订单明细作业数量失败");
                }
                if (!orderService.updateSettle(orderDetl.getOrderId(), 2L, userId)) {
                    throw new CoolException("修改订单状态失败");
                }
            }
        }
        //修改agv备料区状态
        if(locMastRgv.getLocSts().equals("O")){
@@ -1786,3 +1807,4 @@
    }
}
src/main/java/com/zy/asrs/utils/Utils.java
@@ -5,10 +5,16 @@
import com.core.common.Cools;
import com.core.common.SpringUtils;
import com.core.exception.CoolException;
import com.zy.asrs.entity.BasCrnp;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.LocMast;
import com.zy.asrs.entity.RowLastno;
import com.zy.asrs.service.BasCrnpService;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.LocMastService;
import com.zy.asrs.service.RowLastnoService;
import com.zy.common.CodeBuilder;
import com.zy.common.entity.Parameter;
import com.zy.common.model.LocDetlDto;
import com.zy.common.properties.SlaveProperties;
import com.zy.common.service.CommonService;
@@ -19,7 +25,11 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Locale;
/**
 * Created by vincent on 2020/8/27
@@ -120,6 +130,441 @@
        }
    }
    public static Integer getStationStorageArea(Integer stationId) {
        if (stationId == null || stationId <= 0) {
            return null;
        }
        BasDevpService basDevpService = SpringUtils.getBean(BasDevpService.class);
        BasDevp station = basDevpService.selectById(stationId);
        if (station == null) {
            return null;
        }
        return parseStorageArea(station.getArea());
    }
    /**
     * 生成入库找库位时的堆垛机优先顺序。
     *
     * <p>处理规则:
     * 1. 先根据入库站点查询所属库区。
     * 2. 先提取该库区内的堆垛机,并按可用空库位过滤不可用堆垛机。
     * 3. 若当前库区没有满足条件的空库位,再补充其他库区的堆垛机。
     * 4. 当 {@code locType1 = 1} 时,先返回低库位堆垛机,再把同批堆垛机的高库位追加到后面。
     * 5. 对不存在、故障、不可入以及无空库位的堆垛机直接剔除。
     * 6. 当物料为 {@code emptyPallet} 时,按空板入库优先规则重新排序。
     *
     * <p>返回结果中的每一项格式为:
     * {@code {crnNo: 堆垛机号, locType1: 库位高低类型}}
     *
     * @param stationId 入库站点
     * @param locType1 目标库位高低类型,1=低库位,2=高库位
     * @param matnr 物料编码,传入 {@code emptyPallet} 时使用空板排序规则
     * @return 按优先级排好序的堆垛机列表
     */
    public static List<Map<String, Integer>> getStationStorageAreaName(Integer stationId, Integer locType1, String matnr) {
        List<Map<String, Integer>> result = new ArrayList<>();
        // 先定位入库站点所属库区。
        Integer storageArea = getStationStorageArea(stationId);
        Integer whsType = GetWhsType(stationId);
        if (storageArea == null || whsType == null || whsType <= 0) {
            return result;
        }
        RowLastnoService rowLastnoService = SpringUtils.getBean(RowLastnoService.class);
        RowLastno rowLastno = rowLastnoService.selectById(whsType);
        if (rowLastno == null) {
            return result;
        }
        BasCrnpService basCrnpService = SpringUtils.getBean(BasCrnpService.class);
        LocMastService locMastService = SpringUtils.getBean(LocMastService.class);
        boolean emptyPallet = "emptyPallet".equalsIgnoreCase(matnr);
        // 先取当前库区对应的堆垛机。
        List<Integer> preferredCrnNos = getAreaCrnNos(storageArea, rowLastno);
        List<Integer> preferredAvailableCrnNos = getAvailableCrnNos(preferredCrnNos, locType1, emptyPallet, basCrnpService, locMastService);
        appendCrnLocTypeEntries(result, preferredAvailableCrnNos, locType1, locMastService);
        // 当前库区没有可用容量时,再补充其他库区堆垛机。
        if (!hasAvailableCapacity(preferredCrnNos, locType1, basCrnpService, locMastService)) {
            List<Integer> otherAreaCrnNos = getOtherAreaCrnNos(storageArea, rowLastno);
            List<Integer> otherAvailableCrnNos = getAvailableCrnNos(otherAreaCrnNos, locType1, emptyPallet, basCrnpService, locMastService);
            appendCrnLocTypeEntries(result, otherAvailableCrnNos, locType1, locMastService);
        }
        return result;
    }
    private static void appendCrnLocTypeEntries(List<Map<String, Integer>> result, List<Integer> crnNos, Integer locType1, LocMastService locMastService) {
        Short normalizedLocType1 = normalizeLocType1(locType1);
        if (normalizedLocType1 == null) {
            appendCrnLocTypeEntries(result, crnNos, (short) 1, locMastService);
            appendCrnLocTypeEntries(result, crnNos, (short) 2, locMastService);
            return;
        }
        appendCrnLocTypeEntries(result, crnNos, normalizedLocType1, locMastService);
        if (normalizedLocType1 == 1) {
            appendCrnLocTypeEntries(result, crnNos, (short) 2, locMastService);
        }
    }
    private static void appendCrnLocTypeEntries(List<Map<String, Integer>> result, List<Integer> crnNos, Short targetLocType1, LocMastService locMastService) {
        if (targetLocType1 == null || Cools.isEmpty(crnNos)) {
            return;
        }
        for (Integer crnNo : crnNos) {
            if (!hasAvailableLoc(crnNo, targetLocType1, locMastService) || containsCrnLocType(result, crnNo, targetLocType1)) {
                continue;
            }
            Map<String, Integer> item = new LinkedHashMap<>();
            item.put("crnNo", crnNo);
            item.put("locType1", targetLocType1.intValue());
            result.add(item);
        }
    }
    private static boolean containsCrnLocType(List<Map<String, Integer>> result, Integer crnNo, Short locType1) {
        for (Map<String, Integer> item : result) {
            if (item == null) {
                continue;
            }
            if (crnNo.equals(item.get("crnNo")) && locType1.intValue() == item.get("locType1")) {
                return true;
            }
        }
        return false;
    }
    private static boolean hasAvailableCapacity(List<Integer> crnNos, Integer locType1, BasCrnpService basCrnpService, LocMastService locMastService) {
        return !getAvailableCrnNos(crnNos, locType1, false, basCrnpService, locMastService).isEmpty();
    }
    private static List<Integer> getAvailableCrnNos(List<Integer> candidateCrnNos, Integer locType1, boolean emptyPallet,
                                                     BasCrnpService basCrnpService, LocMastService locMastService) {
        LinkedHashSet<Integer> availableCrnNos = new LinkedHashSet<>();
        if (Cools.isEmpty(candidateCrnNos)) {
            return new ArrayList<>();
        }
        for (Integer crnNo : candidateCrnNos) {
            if (crnNo == null || !basCrnpService.checkSiteError(crnNo, true)) {
                continue;
            }
            if (!hasAvailableLocForRequest(crnNo, locType1, locMastService)) {
                continue;
            }
            availableCrnNos.add(crnNo);
        }
        List<Integer> result = new ArrayList<>(availableCrnNos);
        return result;
    }
    private static int compareEmptyPalletCrn(Integer leftCrnNo, Integer rightCrnNo, BasCrnpService basCrnpService) {
        int leftPriority = getEmptyPalletPriority(basCrnpService.selectById(leftCrnNo));
        int rightPriority = getEmptyPalletPriority(basCrnpService.selectById(rightCrnNo));
        if (leftPriority != rightPriority) {
            return Integer.compare(rightPriority, leftPriority);
        }
        return Integer.compare(leftCrnNo, rightCrnNo);
    }
    private static int getEmptyPalletPriority(BasCrnp basCrnp) {
        if (basCrnp == null) {
            return -1;
        }
        return "Y".equalsIgnoreCase(basCrnp.getEmpIn()) ? 1 : 0;
    }
    private static boolean hasAvailableLocForRequest(Integer crnNo, Integer locType1, LocMastService locMastService) {
        Short normalizedLocType1 = normalizeLocType1(locType1);
        if (normalizedLocType1 == null) {
            return hasAvailableLoc(crnNo, (short) 1, locMastService) || hasAvailableLoc(crnNo, (short) 2, locMastService);
        }
        if (hasAvailableLoc(crnNo, normalizedLocType1, locMastService)) {
            return true;
        }
        return normalizedLocType1 == 1 && hasAvailableLoc(crnNo, (short) 2, locMastService);
    }
    private static boolean hasAvailableLoc(Integer crnNo, Short locType1, LocMastService locMastService) {
        if (crnNo == null || locType1 == null) {
            return false;
        }
        return locMastService.selectCount(new EntityWrapper<LocMast>()
                .eq("crn_no", crnNo)
                .eq("loc_sts", "O")
                .eq("loc_type1", locType1)) > 0;
    }
    private static Short normalizeLocType1(Integer locType1) {
        if (locType1 == null || (locType1 != 1 && locType1 != 2)) {
            return null;
        }
        return locType1.shortValue();
    }
    private static List<Integer> getOtherAreaCrnNos(Integer preferredArea, RowLastno rowLastno) {
        LinkedHashSet<Integer> otherAreaCrnNos = new LinkedHashSet<>();
        for (int area = 1; area <= 3; area++) {
            if (preferredArea != null && preferredArea == area) {
                continue;
            }
            otherAreaCrnNos.addAll(getAreaCrnNos(area, rowLastno));
        }
        if (otherAreaCrnNos.isEmpty()) {
            otherAreaCrnNos.addAll(getAllCrnNos(rowLastno));
            otherAreaCrnNos.removeAll(getAreaCrnNos(preferredArea, rowLastno));
        }
        return new ArrayList<>(otherAreaCrnNos);
    }
    private static List<Integer> getAreaCrnNos(Integer area, RowLastno rowLastno) {
        LinkedHashSet<Integer> crnNos = new LinkedHashSet<>();
        RowLastno areaRowLastno = findAreaRowLastno(area, rowLastno);
        if (areaRowLastno == null) {
            return new ArrayList<>(crnNos);
        }
        Integer startCrnNo = resolveAreaStartCrnNo(areaRowLastno, rowLastno);
        Integer endCrnNo = resolveAreaEndCrnNo(areaRowLastno, rowLastno);
        if (startCrnNo != null && endCrnNo != null && startCrnNo <= endCrnNo) {
            for (int crnNo = startCrnNo; crnNo <= endCrnNo; crnNo++) {
                addAreaCrnNo(crnNos, crnNo, 1, endCrnNo);
            }
            for (int crnNo = areaRowLastno.getsCrnNo(); crnNo <= startCrnNo; crnNo++) {
                addAreaCrnNo(crnNos, crnNo, 1, endCrnNo);
            }
            Integer nextCrnQty = startCrnNo + 1;
            if (areaRowLastno.geteCrnNo() != null && nextCrnQty > areaRowLastno.geteCrnNo()) {
                nextCrnQty = areaRowLastno.getsCrnNo() == null ? 1 : areaRowLastno.getsCrnNo();
            }
            areaRowLastno.setCrnQty(nextCrnQty);
            SpringUtils.getBean(RowLastnoService.class).updateById(areaRowLastno);
        }
        if (crnNos.isEmpty()) {
            crnNos.addAll(getFallbackAreaCrnNos(area, rowLastno));
        }
        return new ArrayList<>(crnNos);
    }
    private static RowLastno findAreaRowLastno(Integer area, RowLastno defaultRowLastno) {
        if (area == null) {
            return defaultRowLastno;
        }
        RowLastnoService rowLastnoService = SpringUtils.getBean(RowLastnoService.class);
        List<RowLastno> typeMatched = rowLastnoService.selectList(new EntityWrapper<RowLastno>()
                .eq("type_id", area));
        if (!Cools.isEmpty(typeMatched)) {
            return typeMatched.get(0);
        }
        List<RowLastno> whsMatched = rowLastnoService.selectList(new EntityWrapper<RowLastno>()
                .eq("whs_type", area));
        if (!Cools.isEmpty(whsMatched)) {
            return whsMatched.get(0);
        }
        return defaultRowLastno;
    }
    private static Integer resolveAreaStartCrnNo(RowLastno areaRowLastno, RowLastno defaultRowLastno) {
        if (areaRowLastno != null && areaRowLastno.getCrnQty() != null && areaRowLastno.getCrnQty() > 0) {
            return areaRowLastno.getCrnQty();
        }
        if (areaRowLastno != null && areaRowLastno.getsCrnNo() != null && areaRowLastno.getsCrnNo() > 0) {
            return areaRowLastno.getsCrnNo();
        }
        if (defaultRowLastno != null && defaultRowLastno.getsCrnNo() != null && defaultRowLastno.getsCrnNo() > 0) {
            return defaultRowLastno.getsCrnNo();
        }
        return 1;
    }
    private static Integer resolveAreaEndCrnNo(RowLastno areaRowLastno, RowLastno defaultRowLastno) {
        if (areaRowLastno != null && areaRowLastno.geteCrnNo() != null && areaRowLastno.geteCrnNo() > 0) {
            return areaRowLastno.geteCrnNo();
        }
        return null;
    }
    private static void addAreaCrnNo(LinkedHashSet<Integer> crnNos, Integer crnNo, Integer startCrnNo, Integer endCrnNo) {
        if (crnNos == null || crnNo == null || startCrnNo == null || endCrnNo == null) {
            return;
        }
        if (crnNo < startCrnNo || crnNo > endCrnNo) {
            return;
        }
        crnNos.add(crnNo);
    }
    private static List<Integer> getAllCrnNos(RowLastno rowLastno) {
        List<Integer> crnNos = new ArrayList<>();
        if (rowLastno == null) {
            return crnNos;
        }
        int startCrnNo = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo();
        int endCrnNo = rowLastno.geteCrnNo() == null ? startCrnNo + ((rowLastno.getCrnQty() == null ? 1 : rowLastno.getCrnQty()) - 1) : rowLastno.geteCrnNo();
        for (int crnNo = startCrnNo; crnNo <= endCrnNo; crnNo++) {
            crnNos.add(crnNo);
        }
        return crnNos;
    }
    private static List<Integer> getFallbackAreaCrnNos(Integer area, RowLastno rowLastno) {
        List<Integer> allCrnNos = getAllCrnNos(rowLastno);
        List<Integer> result = new ArrayList<>();
        if (Cools.isEmpty(allCrnNos) || area == null || area < 1 || area > 3) {
            return result;
        }
        int total = allCrnNos.size();
        int baseSize = total / 3;
        int remainder = total % 3;
        int startIndex = 0;
        for (int currentArea = 1; currentArea < area; currentArea++) {
            startIndex += baseSize + (currentArea <= remainder ? 1 : 0);
        }
        int currentSize = baseSize + (area <= remainder ? 1 : 0);
        int endIndex = Math.min(startIndex + currentSize, total);
        for (int index = startIndex; index < endIndex; index++) {
            result.add(allCrnNos.get(index));
        }
        return result;
    }
    private static List<Integer> mapRowsToCrnNos(RowLastno rowLastno, List<Integer> rows) {
        List<Integer> result = new ArrayList<>();
        if (rowLastno == null || Cools.isEmpty(rows)) {
            return result;
        }
        LinkedHashSet<Integer> orderedCrnNos = new LinkedHashSet<>();
        Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId());
        if (rowSpan == null || rowSpan <= 0) {
            rowSpan = 2;
        }
        int startCrnNo = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo();
        int endCrnNo = rowLastno.geteCrnNo() == null ? startCrnNo + ((rowLastno.getCrnQty() == null ? 1 : rowLastno.getCrnQty()) - 1) : rowLastno.geteCrnNo();
        int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow();
        int endRow = rowLastno.geteRow() == null ? Integer.MAX_VALUE : rowLastno.geteRow();
        for (Integer row : rows) {
            if (row == null || row < startRow || row > endRow) {
                continue;
            }
            int crnNo = startCrnNo + (row - startRow) / rowSpan;
            if (crnNo >= startCrnNo && crnNo <= endCrnNo) {
                orderedCrnNos.add(crnNo);
            }
        }
        result.addAll(orderedCrnNos);
        return result;
    }
    private static Integer getCrnRowSpan(Integer typeId) {
        if (typeId == null) {
            return null;
        }
        switch (typeId) {
            case 1:
                return 4;
            case 2:
                return 2;
            default:
                return null;
        }
    }
    private static String getRun2AreaRowsConfig(Integer area) {
        Parameter parameter = Parameter.get();
        if (parameter == null || area == null) {
            return null;
        }
        switch (area) {
            case 1:
                return parameter.getRun2Area1Rows();
            case 2:
                return parameter.getRun2Area2Rows();
            case 3:
                return parameter.getRun2Area3Rows();
            default:
                return null;
        }
    }
    private static List<Integer> parseAreaRows(String configValue, RowLastno rowLastno) {
        List<Integer> rows = new ArrayList<>();
        if (rowLastno == null || Cools.isEmpty(configValue)) {
            return rows;
        }
        LinkedHashSet<Integer> orderedRows = new LinkedHashSet<>();
        String normalized = configValue.replace(",", ",")
                .replace(";", ";")
                .replace("、", ",")
                .replaceAll("\\s+", "");
        if (normalized.isEmpty()) {
            return rows;
        }
        for (String segment : normalized.split("[,;]")) {
            if (segment == null || segment.isEmpty()) {
                continue;
            }
            if (segment.contains("-")) {
                String[] rangeParts = segment.split("-", 2);
                Integer startRow = safeParseInt(rangeParts[0]);
                Integer endRow = safeParseInt(rangeParts[1]);
                if (startRow == null || endRow == null) {
                    continue;
                }
                int step = startRow <= endRow ? 1 : -1;
                for (int row = startRow; step > 0 ? row <= endRow : row >= endRow; row += step) {
                    addAreaRow(orderedRows, row, rowLastno);
                }
                continue;
            }
            addAreaRow(orderedRows, safeParseInt(segment), rowLastno);
        }
        rows.addAll(orderedRows);
        return rows;
    }
    private static void addAreaRow(LinkedHashSet<Integer> rows, Integer row, RowLastno rowLastno) {
        if (rows == null || row == null || rowLastno == null) {
            return;
        }
        if (row < rowLastno.getsRow() || row > rowLastno.geteRow()) {
            return;
        }
        rows.add(row);
    }
    private static Integer safeParseInt(String value) {
        if (Cools.isEmpty(value)) {
            return null;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    private static Integer parseStorageArea(String area) {
        if (Cools.isEmpty(area)) {
            return null;
        }
        String normalized = area.trim();
        if (normalized.isEmpty()) {
            return null;
        }
        try {
            int areaNo = Integer.parseInt(normalized);
            return areaNo >= 1 && areaNo <= 3 ? areaNo : null;
        } catch (NumberFormatException ignored) {
        }
        String upper = normalized.toUpperCase(Locale.ROOT);
        if ("A".equals(upper) || "A区".equals(upper) || "A库".equals(upper) || "A库区".equals(upper)) {
            return 1;
        }
        if ("B".equals(upper) || "B区".equals(upper) || "B库".equals(upper) || "B库区".equals(upper)) {
            return 2;
        }
        if ("C".equals(upper) || "C区".equals(upper) || "C库".equals(upper) || "C库区".equals(upper)) {
            return 3;
        }
        return null;
    }
    public static String zerofill(String msg, Integer count) {
        if (msg.length() == count) {
            return msg;
@@ -774,3 +1219,11 @@
        return row + "-" + boy + "-" + lev;
    }
}
src/main/java/com/zy/common/entity/Parameter.java
@@ -63,4 +63,22 @@
    // erp上报
    private String erpReport;
    // AGV area A row config
    private String agvArea1Rows;
    // AGV area B row config
    private String agvArea2Rows;
    // AGV area C row config
    private String agvArea3Rows;
    // run2 area A row config
    private String run2Area1Rows;
    // run2 area B row config
    private String run2Area2Rows;
    // run2 area C row config
    private String run2Area3Rows;
}
src/main/java/com/zy/common/service/CommonService.java
@@ -12,6 +12,7 @@
import com.zy.asrs.service.*;
import com.zy.asrs.utils.Utils;
import com.zy.asrs.utils.VersionUtils;
import com.zy.common.entity.Parameter;
import com.zy.common.model.LocTypeDto;
import com.zy.common.model.Shelves;
import com.zy.common.model.StartupDto;
@@ -26,7 +27,9 @@
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
@@ -138,36 +141,596 @@
            Integer whsType = Utils.GetWhsType(sourceStaNo);
            RowLastno rowLastno = rowLastnoService.selectById(whsType);
            RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId());
            if (rowLastnoType.getType() == 2) {
                return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 0, locTypeDto, 0);
            Integer preferredArea = resolvePreferredArea(sourceStaNo, findLocNoAttributeVo);
            if (preferredArea != null) {
                findLocNoAttributeVo.setOutArea(preferredArea);
            }
            /**
             * 库型 1: 标准堆垛机库  2: 平库  3: 穿梭板  4: 四向车  5: AGV  0: 未知
             */
            switch (rowLastnoType.getType()) {
                case 1:
                    return getLocNoRun(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 0, locTypeDto, 0);
                case 2:
                    log.error("站点={} 未查询到对应的规则", sourceStaNo);
                    break;
                    return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 0, locTypeDto, recommendRows, 0);
                case 3:
                    log.error("站点={} 未查询到对应的规则", sourceStaNo);
                    break;
                    return getLocNoRun(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 0, locTypeDto, 0);
                case 4:
                    return getLocNoRun4(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 4, locTypeDto, 0);
                case 5:
                    return getLocNoRun5(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 0, locTypeDto, recommendRows, 0);
                default:
                    log.error("站点={} 未查询到对应的规则", sourceStaNo);
                    break;
                    throw new CoolException("站点=" + sourceStaNo + " 未查询到对应的库位规则");
            }
        } catch (CoolException e) {
            log.error("站点={} 查找库位失败: {}", sourceStaNo, e.getMessage(), e);
            throw e;
        } catch (Exception e) {
            log.error("站点={} 未查询到对应的规则", sourceStaNo);
            log.error("站点={} 查找库位异常", sourceStaNo, e);
            throw new CoolException("站点=" + sourceStaNo + " 查找库位失败");
        }
    }
    private Integer resolvePreferredArea(Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo) {
        BasDevp sourceStation = basDevpService.selectById(sourceStaNo);
        Integer stationArea = parseArea(sourceStation == null ? null : sourceStation.getArea());
        if (stationArea != null) {
            return stationArea;
        }
        Integer requestArea = findLocNoAttributeVo.getOutArea();
        if (requestArea != null && requestArea >= 1 && requestArea <= 3) {
            return requestArea;
        }
        return null;
    }
    private Integer parseArea(String area) {
        if (Cools.isEmpty(area)) {
            return null;
        }
        String normalized = area.trim();
        if (normalized.isEmpty()) {
            return null;
        }
        try {
            int areaNo = Integer.parseInt(normalized);
            return areaNo >= 1 && areaNo <= 3 ? areaNo : null;
        } catch (NumberFormatException ignored) {
        }
        String upper = normalized.toUpperCase(Locale.ROOT);
        if ("A".equals(upper) || "A区".equals(upper) || "A库".equals(upper) || "A库区".equals(upper)) {
            return 1;
        }
        if ("B".equals(upper) || "B区".equals(upper) || "B库".equals(upper) || "B库区".equals(upper)) {
            return 2;
        }
        if ("C".equals(upper) || "C区".equals(upper) || "C库".equals(upper) || "C库区".equals(upper)) {
            return 3;
        }
        return null;
    }
    private String getAgvAreaRowsConfig(Integer area) {
        Parameter parameter = Parameter.get();
        if (parameter == null || area == null) {
            return null;
        }
        switch (area) {
            case 1:
                return parameter.getAgvArea1Rows();
            case 2:
                return parameter.getAgvArea2Rows();
            case 3:
                return parameter.getAgvArea3Rows();
            default:
                return null;
        }
    }
    private List<Integer> getAgvAreaRows(Integer area, RowLastno rowLastno) {
        List<Integer> configuredRows = parseAgvRows(getAgvAreaRowsConfig(area), rowLastno);
        if (!configuredRows.isEmpty()) {
            return configuredRows;
        }
        return getLegacyAgvRows(rowLastno);
    }
    private List<Integer> getAgvFallbackRows(RowLastno rowLastno) {
        LinkedHashSet<Integer> rows = new LinkedHashSet<>();
        for (int area = 1; area <= 3; area++) {
            rows.addAll(parseAgvRows(getAgvAreaRowsConfig(area), rowLastno));
        }
        rows.addAll(getLegacyAgvRows(rowLastno));
        return new ArrayList<>(rows);
    }
    private List<Integer> parseAgvRows(String configValue, RowLastno rowLastno) {
        List<Integer> rows = new ArrayList<>();
        if (rowLastno == null || Cools.isEmpty(configValue)) {
            return rows;
        }
        LinkedHashSet<Integer> orderedRows = new LinkedHashSet<>();
        String normalized = configValue.replace(",", ",")
                .replace(";", ";")
                .replace("、", ",")
                .replaceAll("\\s+", "");
        if (normalized.isEmpty()) {
            return rows;
        }
        for (String segment : normalized.split("[,;]")) {
            if (segment == null || segment.isEmpty()) {
                continue;
            }
            if (segment.contains("-")) {
                String[] rangeParts = segment.split("-", 2);
                Integer startRow = safeParseInt(rangeParts[0]);
                Integer endRow = safeParseInt(rangeParts[1]);
                if (startRow == null || endRow == null) {
                    continue;
                }
                int step = startRow <= endRow ? 1 : -1;
                for (int row = startRow; step > 0 ? row <= endRow : row >= endRow; row += step) {
                    addAgvRow(orderedRows, row, rowLastno);
                }
                continue;
            }
            addAgvRow(orderedRows, safeParseInt(segment), rowLastno);
        }
        rows.addAll(orderedRows);
        return rows;
    }
    private List<Integer> getLegacyAgvRows(RowLastno rowLastno) {
        List<Integer> rows = new ArrayList<>();
        if (rowLastno == null) {
            return rows;
        }
        LinkedHashSet<Integer> orderedRows = new LinkedHashSet<>();
        int startRow = Math.min(38, rowLastno.geteRow());
        int endRow = Math.max(32, rowLastno.getsRow());
        if (startRow >= endRow) {
            for (int row = startRow; row >= endRow; row--) {
                addAgvRow(orderedRows, row, rowLastno);
            }
        } else {
            for (int row = rowLastno.geteRow(); row >= rowLastno.getsRow(); row--) {
                addAgvRow(orderedRows, row, rowLastno);
            }
        }
        rows.addAll(orderedRows);
        return rows;
    }
    private void addAgvRow(LinkedHashSet<Integer> rows, Integer row, RowLastno rowLastno) {
        if (rows == null || row == null || rowLastno == null) {
            return;
        }
        if (row < rowLastno.getsRow() || row > rowLastno.geteRow()) {
            return;
        }
        rows.add(row);
    }
    private Integer safeParseInt(String value) {
        if (Cools.isEmpty(value)) {
            return null;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    private int[] getAgvAreaBayRange(Integer area) {
        if (area == null) {
            return new int[]{1, 19};
        }
        switch (area) {
            case 1:
                return new int[]{1, 12};
            case 2:
                return new int[]{13, 36};
            case 3:
                return new int[]{37, 56};
            default:
                return new int[]{1, 56};
        }
    }
    private LocMast findAgvLocByRows(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> rows,
                                     int startBay, int endBay, int curRow, int nearRow,
                                     LocTypeDto locTypeDto, boolean useDeepCheck) {
        for (Integer row : rows) {
            if (row == null) {
                continue;
            }
            List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                    .eq("row1", row)
                    .ge("bay1", startBay)
                    .le("bay1", endBay)
                    .eq("loc_sts", "O")
                    .eq("loc_type1", locTypeDto.getLocType1())
                    .orderBy("lev1", true)
                    .orderBy("bay1", true));
            for (LocMast candidate : locMasts) {
                if (!VersionUtils.locMoveCheckLocTypeComplete(candidate, locTypeDto)) {
                    continue;
                }
                if (useDeepCheck) {
                    if (!Utils.BooleanWhsTypeStaIoType(rowLastno)) {
                        continue;
                    }
                    LocMast deepLoc = locMastService.selectLocByLocStsPakInO(curRow, nearRow, candidate, rowLastnoType.getType().longValue());
                    if (!Cools.isEmpty(deepLoc) && deepLoc.getRow1() == curRow) {
                        return deepLoc;
                    }
                    continue;
                }
                return candidate;
            }
        }
        return null;
    }
    private String getRun2AreaRowsConfig(Integer area) {
        Parameter parameter = Parameter.get();
        if (parameter == null || area == null) {
            return null;
        }
        String run2Config;
        switch (area) {
            case 1:
                run2Config = parameter.getRun2Area1Rows();
                break;
            case 2:
                run2Config = parameter.getRun2Area2Rows();
                break;
            case 3:
                run2Config = parameter.getRun2Area3Rows();
                break;
            default:
                return null;
        }
        return Cools.isEmpty(run2Config) ? getAgvAreaRowsConfig(area) : run2Config;
    }
    private List<Integer> getRun2AreaRows(Integer area, RowLastno rowLastno) {
        List<Integer> configuredRows = parseAgvRows(getRun2AreaRowsConfig(area), rowLastno);
        if (!configuredRows.isEmpty()) {
            return configuredRows;
        }
        return getLegacyAgvRows(rowLastno);
    }
    private List<Integer> getRun2FallbackRows(RowLastno rowLastno) {
        LinkedHashSet<Integer> rows = new LinkedHashSet<>();
        for (int area = 1; area <= 3; area++) {
            rows.addAll(parseAgvRows(getRun2AreaRowsConfig(area), rowLastno));
        }
        rows.addAll(getLegacyAgvRows(rowLastno));
        return new ArrayList<>(rows);
    }
    private Integer resolveRun2CrnNo(RowLastno rowLastno) {
        if (rowLastno == null) {
            return null;
        }
        Integer currentRow = rowLastno.getCurrentRow();
        Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId());
        if (rowSpan == null || rowSpan <= 0) {
            rowSpan = 2;
        }
        int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow();
        int startCrnNo = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo();
        if (currentRow == null) {
            return startCrnNo;
        }
        int offset = Math.max(currentRow - startRow, 0) / rowSpan;
        int crnNo = startCrnNo + offset;
        Integer endCrnNo = rowLastno.geteCrnNo();
        if (endCrnNo != null && crnNo > endCrnNo) {
            return startCrnNo;
        }
        return crnNo;
    }
    private int getNextRun2CurrentRow(RowLastno rowLastno, int currentRow) {
        Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId());
        if (rowSpan == null || rowSpan <= 0) {
            rowSpan = 2;
        }
        int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow();
        int endRow = rowLastno.geteRow() == null ? currentRow : rowLastno.geteRow();
        int lastStartRow = Math.max(startRow, endRow - rowSpan + 1);
        if (currentRow >= lastStartRow) {
            return startRow;
        }
        return currentRow + rowSpan;
    }
    private List<Integer> getOrderedCrnNos(RowLastno rowLastno, Integer startCrnNo) {
        List<Integer> orderedCrnNos = new ArrayList<>();
        if (rowLastno == null) {
            return orderedCrnNos;
        }
        int start = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo();
        int end = rowLastno.geteCrnNo() == null ? start + rowLastno.getCrnQty() - 1 : rowLastno.geteCrnNo();
        int first = startCrnNo == null ? start : startCrnNo;
        if (first < start || first > end) {
            first = start;
        }
        for (int crnNo = first; crnNo <= end; crnNo++) {
            orderedCrnNos.add(crnNo);
        }
        for (int crnNo = start; crnNo < first; crnNo++) {
            orderedCrnNos.add(crnNo);
        }
        return orderedCrnNos;
    }
    private List<Integer> filterCrnNosByRows(RowLastno rowLastno, List<Integer> orderedCrnNos, List<Integer> rows) {
        if (Cools.isEmpty(rows)) {
            return new ArrayList<>(orderedCrnNos);
        }
        LinkedHashSet<Integer> rowSet = new LinkedHashSet<>(rows);
        List<Integer> result = new ArrayList<>();
        Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId());
        if (rowSpan == null || rowSpan <= 0) {
            rowSpan = 2;
        }
        int startCrnNo = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo();
        int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow();
        int endRow = rowLastno.geteRow() == null ? Integer.MAX_VALUE : rowLastno.geteRow();
        for (Integer crnNo : orderedCrnNos) {
            if (crnNo == null || crnNo < startCrnNo) {
                continue;
            }
            int crnOffset = crnNo - startCrnNo;
            int crnStartRow = startRow + crnOffset * rowSpan;
            for (int row = crnStartRow; row < crnStartRow + rowSpan && row <= endRow; row++) {
                if (rowSet.contains(row)) {
                    result.add(crnNo);
                    break;
                }
            }
        }
        return result;
    }
    private List<Integer> mapRowsToCrnNos(RowLastno rowLastno, List<Integer> rows) {
        List<Integer> result = new ArrayList<>();
        if (rowLastno == null || Cools.isEmpty(rows)) {
            return result;
        }
        LinkedHashSet<Integer> orderedCrnNos = new LinkedHashSet<>();
        Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId());
        if (rowSpan == null || rowSpan <= 0) {
            rowSpan = 2;
        }
        int startCrnNo = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo();
        int endCrnNo = rowLastno.geteCrnNo() == null ? startCrnNo + rowLastno.getCrnQty() - 1 : rowLastno.geteCrnNo();
        int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow();
        int endRow = rowLastno.geteRow() == null ? Integer.MAX_VALUE : rowLastno.geteRow();
        for (Integer row : rows) {
            if (row == null || row < startRow || row > endRow) {
                continue;
            }
            int crnNo = startCrnNo + (row - startRow) / rowSpan;
            if (crnNo >= startCrnNo && crnNo <= endCrnNo) {
                orderedCrnNos.add(crnNo);
            }
        }
        result.addAll(orderedCrnNos);
        return result;
    }
    private Integer resolveTargetStaNo(RowLastno rowLastno, Integer staDescId, Integer sourceStaNo, Integer crnNo) {
        if (!Utils.BooleanWhsTypeSta(rowLastno, staDescId)) {
            return null;
        }
        StaDesc staDesc = staDescService.selectOne(new EntityWrapper<StaDesc>()
                .eq("type_no", staDescId)
                .eq("stn_no", sourceStaNo)
                .eq("crn_no", crnNo));
        if (Cools.isEmpty(staDesc)) {
            log.error("type_no={},stn_no={},crn_no={}", staDescId, sourceStaNo, crnNo);
            return null;
        }
        BasDevp staNo = basDevpService.selectById(staDesc.getCrnStn());
        if (Cools.isEmpty(staNo) || !"Y".equals(staNo.getAutoing())) {
            log.error("目标站{}不可用", staDesc.getCrnStn());
            return null;
        }
        return staNo.getDevNo();
    }
    private void logRun2NoMatch(String stage, Integer sourceStaNo, Integer preferredArea, List<Integer> candidateCrnNos,
                                LocTypeDto locTypeDto, List<Integer> crnErrorCrns, List<Integer> routeBlockedCrns,
                                List<Integer> noEmptyCrns, List<Integer> locTypeBlockedCrns) {
        log.warn("run2 no location. stage={}, sourceStaNo={}, preferredArea={}, candidateCrnNos={}, crnErrorCrns={}, routeBlockedCrns={}, noEmptyCrns={}, locTypeBlockedCrns={}, spec={}",
                stage, sourceStaNo, preferredArea, candidateCrnNos, crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns,
                JSON.toJSONString(locTypeDto));
    }
    private LocMast findRun2EmptyLocByCrnNos(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> candidateCrnNos,
                                             LocTypeDto locTypeDto, Integer staDescId, Integer sourceStaNo, StartupDto startupDto,
                                             Integer preferredArea, String stage) {
        if (Cools.isEmpty(candidateCrnNos)) {
            log.warn("run2 skip empty candidate list. stage={}, sourceStaNo={}, preferredArea={}, spec={}",
                    stage, sourceStaNo, preferredArea, JSON.toJSONString(locTypeDto));
            return null;
        }
        List<Integer> crnErrorCrns = new ArrayList<>();
        List<Integer> routeBlockedCrns = new ArrayList<>();
        List<Integer> noEmptyCrns = new ArrayList<>();
        List<Integer> locTypeBlockedCrns = new ArrayList<>();
        for (Integer candidateCrnNo : candidateCrnNos) {
            if (candidateCrnNo == null || !basCrnpService.checkSiteError(candidateCrnNo, true)) {
                crnErrorCrns.add(candidateCrnNo);
                continue;
            }
            Integer targetStaNo = resolveTargetStaNo(rowLastno, staDescId, sourceStaNo, candidateCrnNo);
            if (Utils.BooleanWhsTypeSta(rowLastno, staDescId) && targetStaNo == null) {
                routeBlockedCrns.add(candidateCrnNo);
                continue;
            }
            Wrapper<LocMast> openWrapper = new EntityWrapper<LocMast>()
                    .eq("crn_no", candidateCrnNo)
                    .eq("loc_sts", "O")
                    .eq("loc_type1", locTypeDto.getLocType1())
                    .orderBy("lev1")
                    .orderBy("bay1");
            LocMast anyOpenLoc = locMastService.selectOne(openWrapper);
            if (Cools.isEmpty(anyOpenLoc)) {
                noEmptyCrns.add(candidateCrnNo);
                continue;
            }
            Wrapper<LocMast> wrapper = new EntityWrapper<LocMast>()
                    .eq("crn_no", candidateCrnNo)
                    .eq("loc_sts", "O")
                    .eq("loc_type1", locTypeDto.getLocType1())
                    .orderBy("lev1")
                    .orderBy("bay1");
            if (locTypeDto != null && locTypeDto.getLocType1() != null) {
                wrapper.eq("loc_type1", locTypeDto.getLocType1());
            }
            LocMast candidateLoc = locMastService.selectOne(wrapper);
            if (Cools.isEmpty(candidateLoc) || (locTypeDto != null && !VersionUtils.locMoveCheckLocTypeComplete(candidateLoc, locTypeDto))) {
                locTypeBlockedCrns.add(candidateCrnNo);
                continue;
            }
            if (targetStaNo != null) {
                startupDto.setStaNo(targetStaNo);
            }
            return candidateLoc;
        }
        logRun2NoMatch(stage, sourceStaNo, preferredArea, candidateCrnNos, locTypeDto, crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        return null;
    }
    private LocMast findRun2EmptyLocByCrnLocTypeEntries(RowLastno rowLastno, RowLastnoType rowLastnoType,
                                                        List<Map<String, Integer>> crnLocTypeEntries, LocTypeDto locTypeDto,
                                                        Integer staDescId, Integer sourceStaNo, StartupDto startupDto,
                                                        Integer preferredArea, String stage) {
        if (Cools.isEmpty(crnLocTypeEntries)) {
            log.warn("run2 skip empty crn-locType list. stage={}, sourceStaNo={}, preferredArea={}, spec={}",
                    stage, sourceStaNo, preferredArea, JSON.toJSONString(locTypeDto));
            return null;
        }
        List<Integer> candidateCrnNos = extractCrnNos(crnLocTypeEntries);
        List<Integer> crnErrorCrns = new ArrayList<>();
        List<Integer> routeBlockedCrns = new ArrayList<>();
        List<Integer> noEmptyCrns = new ArrayList<>();
        List<Integer> locTypeBlockedCrns = new ArrayList<>();
        return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, stage, 0, candidateCrnNos,
                crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
    }
    private LocMast findRun2EmptyLocByCrnLocTypeEntriesRecursively(RowLastno rowLastno, RowLastnoType rowLastnoType,
                                                                   List<Map<String, Integer>> crnLocTypeEntries, LocTypeDto locTypeDto,
                                                                   Integer staDescId, Integer sourceStaNo, StartupDto startupDto,
                                                                   Integer preferredArea, String stage, int index,
                                                                   List<Integer> candidateCrnNos, List<Integer> crnErrorCrns,
                                                                   List<Integer> routeBlockedCrns, List<Integer> noEmptyCrns,
                                                                   List<Integer> locTypeBlockedCrns) {
        if (index >= crnLocTypeEntries.size()) {
            logRun2NoMatch(stage, sourceStaNo, preferredArea, candidateCrnNos, locTypeDto, crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
            return null;
        }
        Map<String, Integer> crnLocTypeEntry = crnLocTypeEntries.get(index);
        Integer candidateCrnNo = crnLocTypeEntry == null ? null : crnLocTypeEntry.get("crnNo");
        Short candidateLocType1 = crnLocTypeEntry == null || crnLocTypeEntry.get("locType1") == null
                ? null : crnLocTypeEntry.get("locType1").shortValue();
        if (candidateCrnNo == null || !basCrnpService.checkSiteError(candidateCrnNo, true)) {
            crnErrorCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        Integer targetStaNo = resolveTargetStaNo(rowLastno, staDescId, sourceStaNo, candidateCrnNo);
        if (Utils.BooleanWhsTypeSta(rowLastno, staDescId) && targetStaNo == null) {
            routeBlockedCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        LocTypeDto searchLocTypeDto = buildRun2SearchLocTypeDto(locTypeDto, candidateLocType1);
        LocMast candidateLoc = findRun2OrderedEmptyLocByCrnLocType(rowLastnoType, candidateCrnNo, candidateLocType1, searchLocTypeDto);
        if (Cools.isEmpty(candidateLoc)) {
            noEmptyCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        if (searchLocTypeDto != null && !VersionUtils.locMoveCheckLocTypeComplete(candidateLoc, searchLocTypeDto)) {
            locTypeBlockedCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        if (targetStaNo != null) {
            startupDto.setStaNo(targetStaNo);
        }
        return candidateLoc;
    }
    private List<Integer> extractCrnNos(List<Map<String, Integer>> crnLocTypeEntries) {
        LinkedHashSet<Integer> orderedCrnNos = new LinkedHashSet<>();
        if (Cools.isEmpty(crnLocTypeEntries)) {
            return new ArrayList<>();
        }
        for (Map<String, Integer> crnLocTypeEntry : crnLocTypeEntries) {
            if (crnLocTypeEntry == null || crnLocTypeEntry.get("crnNo") == null) {
                continue;
            }
            orderedCrnNos.add(crnLocTypeEntry.get("crnNo"));
        }
        return new ArrayList<>(orderedCrnNos);
    }
    private LocTypeDto buildRun2SearchLocTypeDto(LocTypeDto locTypeDto, Short candidateLocType1) {
        if (locTypeDto == null && candidateLocType1 == null) {
            return null;
        }
        LocTypeDto searchLocTypeDto = new LocTypeDto();
        if (locTypeDto != null) {
            searchLocTypeDto.setLocType1(locTypeDto.getLocType1());
            searchLocTypeDto.setLocType2(locTypeDto.getLocType2());
            searchLocTypeDto.setLocType3(locTypeDto.getLocType3());
            searchLocTypeDto.setSiteId(locTypeDto.getSiteId());
        }
        if (candidateLocType1 != null) {
            searchLocTypeDto.setLocType1(candidateLocType1);
        }
        return searchLocTypeDto;
    }
    private LocMast findRun2OrderedEmptyLocByCrnLocType(RowLastnoType rowLastnoType, Integer candidateCrnNo,
                                                        Short candidateLocType1, LocTypeDto locTypeDto) {
        if (candidateCrnNo == null) {
            return null;
        }
        Wrapper<LocMast> wrapper = new EntityWrapper<LocMast>()
                .eq("crn_no", candidateCrnNo)
                .eq("loc_sts", "O");
        if (candidateLocType1 != null) {
            wrapper.eq("loc_type1", candidateLocType1);
        }
        // 单伸堆垛机按层、列递增顺序找第一个空库位。
        if (rowLastnoType != null && rowLastnoType.getType() != null && (rowLastnoType.getType() == 1 || rowLastnoType.getType() == 2)) {
            wrapper.orderBy("lev1", true).orderBy("bay1", true);
        } else {
            wrapper.orderBy("lev1", true).orderBy("bay1", true);
        }
        LocMast candidateLoc = locMastService.selectOne(wrapper);
        if (Cools.isEmpty(candidateLoc)) {
            return null;
        }
        if (locTypeDto != null && !VersionUtils.locMoveCheckLocTypeComplete(candidateLoc, locTypeDto)) {
            return null;
        }
        return candidateLoc;
    }
    private Optional<CrnRowInfo> findAvailableCrnAndNearRow(RowLastno rowLastno, int curRow, int crnNumber, int times,
                                                            FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto,
@@ -719,6 +1282,10 @@
    }
    public StartupDto getLocNoRun2(Integer whsType, Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, Integer moveCrnNo, LocTypeDto locTypeDto, int times) {
        return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, locTypeDto, null, times);
    }
    public StartupDto getLocNoRun2(Integer whsType, Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, Integer moveCrnNo, LocTypeDto locTypeDto, List<Integer> recommendRows, int times) {
        int crnNo = 0;
        int nearRow = 0;
@@ -732,71 +1299,72 @@
        if (Cools.isEmpty(rowLastno)) {
            throw new CoolException("数据异常,请联系管理员===>库位规则未知");
        }
        crnNo = rowLastno.getCurrentRow()/2+1;
        RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId());
        if (Cools.isEmpty(rowLastnoType)) {
            throw new CoolException("数据异常,请联系管理员===》库位规则类型未知");
        }
        int crnNumber = rowLastno.getCrnQty();
        rowCount = crnNumber;
        curRow = rowLastno.getCurrentRow();
        crnNo = resolveRun2CrnNo(rowLastno);
        Integer preferredArea = findLocNoAttributeVo.getOutArea();
        Wrapper<StaDesc> wrapper = null;
        StaDesc staDesc = null;
        BasDevp staNo = null;
        if (Utils.BooleanWhsTypeSta(rowLastno, staDescId)) {
            wrapper = new EntityWrapper<StaDesc>()
                    .eq("type_no", staDescId)
                    .eq("stn_no", sourceStaNo)
                    .eq("crn_no", crnNo);
            staDesc = staDescService.selectOne(wrapper);
            if (Cools.isEmpty(staDesc)) {
                log.error("type_no={},stn_no={},crn_no={}", staDescId, sourceStaNo, crnNo);
                crnNo = 0;
        List<Integer> orderedCrnNos = getOrderedCrnNos(rowLastno, crnNo);
        List<Integer> triedCrnNos = new ArrayList<>();
        List<Integer> recommendCrnNos = mapRowsToCrnNos(rowLastno, recommendRows);
        if (!Cools.isEmpty(recommendCrnNos)) {
            locMast = findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, recommendCrnNos, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "recommend");
            triedCrnNos.addAll(recommendCrnNos);
        }
        if (Cools.isEmpty(locMast)) {
            List<Map<String, Integer>> stationCrnLocTypes = Utils.getStationStorageAreaName(
                    sourceStaNo,
                    locTypeDto == null || locTypeDto.getLocType1() == null ? null : locTypeDto.getLocType1().intValue(),
                    findLocNoAttributeVo == null ? null : findLocNoAttributeVo.getMatnr());
            if (!Cools.isEmpty(stationCrnLocTypes)) {
                locMast = findRun2EmptyLocByCrnLocTypeEntries(rowLastno, rowLastnoType, stationCrnLocTypes,
                        locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "station-priority");
            } else if (preferredArea == null) {
                List<Integer> defaultCrnNos = new ArrayList<>(orderedCrnNos);
                defaultCrnNos.removeAll(triedCrnNos);
                locMast = findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, defaultCrnNos, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "default");
            } else {
                staNo = basDevpService.selectById(staDesc.getCrnStn());
                if (!staNo.getAutoing().equals("Y")) {
                    log.error("目标站{}不可用", staDesc.getCrnStn());
                    crnNo = 0;
                List<Integer> preferredCrnNos = filterCrnNosByRows(rowLastno, orderedCrnNos, getRun2AreaRows(preferredArea, rowLastno));
                preferredCrnNos.removeAll(triedCrnNos);
                locMast = findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, preferredCrnNos, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "preferred-area");
                if (Cools.isEmpty(locMast)) {
                    List<Integer> fallbackCrnNos = filterCrnNosByRows(rowLastno, orderedCrnNos, getRun2FallbackRows(rowLastno));
                    fallbackCrnNos.removeAll(triedCrnNos);
                    fallbackCrnNos.removeAll(preferredCrnNos);
                    locMast = findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, fallbackCrnNos, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "fallback-area");
                }
                startupDto.setStaNo(staNo.getDevNo());
            }
        }
        LocMast locMast1 = locMastService.selectOne(new EntityWrapper<LocMast>()
                .eq("crn_no", crnNo)
                .eq("loc_sts", "O")
                .orderBy("lev1")
                .orderBy("bay1")
                .eq("loc_type1",locTypeDto.getLocType1()));
        if (!Cools.isEmpty(locMast1)) {
            locMast=locMast1;
        if (!Cools.isEmpty(locMast)) {
            crnNo = locMast.getCrnNo();
            nearRow = locMast.getRow1();
        }
        if (curRow==rowLastno.geteRow()-1) {
            curRow = 1;
        }else{
            curRow = curRow + 2;
        if (curRow == 0) {
            curRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow();
        }
        curRow = getNextRun2CurrentRow(rowLastno, curRow);
        rowLastno.setCurrentRow(curRow);
        rowLastnoService.updateById(rowLastno);
        if (!Cools.isEmpty(locMast) && !basCrnpService.checkSiteError(crnNo, true)) {
            locMast = null;
        }
        if (Cools.isEmpty(locMast) || !locMast.getLocSts().equals("O")) {
            if (times < rowCount * 2) {
                times = times + 1;
                return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, locTypeDto, times);
                return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, locTypeDto, recommendRows, times);
            }
            LocTypeDto compatibleLocTypeDto = buildUpwardCompatibleLocTypeDto(locTypeDto);
            if (compatibleLocTypeDto != null) {
                log.warn("locType1 upward compatibility retry. source={}, target={}", JSON.toJSONString(locTypeDto), JSON.toJSONString(compatibleLocTypeDto));
                return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, compatibleLocTypeDto, 0);
                return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, compatibleLocTypeDto, recommendRows, 0);
            }
            log.error("No empty location found. spec={}, times={}", JSON.toJSONString(locTypeDto), times);
            throw new CoolException("\u6ca1\u6709\u7a7a\u5e93\u4f4d");
            log.error("No empty location found. spec={}, times={}, preferredArea={}, nearRow={}", JSON.toJSONString(locTypeDto), times, preferredArea, nearRow);
            throw new CoolException("没有空库位");
        }
        int workNo = getWorkNo(0);
@@ -806,7 +1374,6 @@
        startupDto.setLocNo(locMast.getLocNo());
        return startupDto;
    }
    private LocMast findSingleExtensionEmptyLoc(RowLastno rowLastno, int crnNo, int nearRow, RowLastnoType rowLastnoType, LocTypeDto locTypeDto) {
        for (Integer searchRow : getCrnSearchRows(rowLastno, crnNo, nearRow)) {
            List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
@@ -1359,8 +1926,10 @@
            }
        }
        if (Cools.isEmpty(locMast) && sourceStaNo != 4006) {//si'lou'p四楼盘点选择区域
                 List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
        Integer preferredArea = findLocNoAttributeVo.getOutArea();
        if (Cools.isEmpty(locMast) && preferredArea == null) {
            List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                    .eq("row1", nearRow)
                    .eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue())
                    .orderBy("lev1", true).orderBy("bay1", true)); // 最浅库位
@@ -1377,104 +1946,21 @@
                    }
                }
            }
        } else {
            // 根据 findLocNoAttributeVo.getOutArea() 设置列范围
            int startBay = 1;
            int endBay = 19;
            switch (findLocNoAttributeVo.getOutArea()) {
                case 3:
                    startBay = 15;
                    endBay = 19;
                    break;
                case 2:
                    startBay = 8;
                    endBay = 14;
                    break;
                case 1:
                    startBay = 1;
                    endBay = 8;
                    break;
                default:
                    break;
        } else if (Cools.isEmpty(locMast)) {
            int[] bayRange = getAgvAreaBayRange(preferredArea);
            locMast = findAgvLocByRows(rowLastno, rowLastnoType, getAgvAreaRows(preferredArea, rowLastno),
                    bayRange[0], bayRange[1], curRow, nearRow, locTypeDto, false);
            if (!Cools.isEmpty(locMast)) {
                crnNo = locMast.getCrnNo();
            }
            // 优先从指定列范围查找
            boolean found = false;
            // 按照排号从38到32递减查找,优先查找指定列范围(如1-8、8-14、15-19)
            for (int row = 38; row >= 32; row--) {
                List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                        .eq("row1", row)
                        .ge("bay1", startBay)
                        .le("bay1", endBay)
                        .eq("loc_sts", "O")
                        .eq("whs_type", rowLastnoType.getType().longValue())
                        .orderBy("lev1", true)
                        .orderBy("bay1", true)); // 最浅库位
                for (LocMast locMast1 : locMasts) {
                    if (!VersionUtils.locMoveCheckLocTypeComplete(locMast1, locTypeDto)) {
                        continue;
                    }
                    if(locMast1!= null){
                        locMast = locMast1;
                        found = true;
                        break;
                    }
//                    if (Utils.BooleanWhsTypeStaIoType(rowLastno)) {
//                        // 获取目标库位所在巷道最深空库位
//                        LocMast locMast2 = locMastService.selectLocByLocStsPakInO(curRow, nearRow, locMast1, rowLastnoType.getType().longValue());
//                        if (!Cools.isEmpty(locMast2) && locMast2.getRow1() == curRow) {
//                            locMast = locMast2;
//                            found = true;
//                            break;
//                        }
//                    }
                }
                if (found) {
                    break; // 找到目标库位后跳出循环
                }
            }
            // 如果没有在优先范围内找到合适库位,继续进行全局查找(1-19列)
            if (!found) {
                // 从排号38到32查找所有列(1-19)
                for (int row = 38; row >= 32; row--) {
                    List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                            .eq("row1", row)
                            .ge("bay1", 1)  // 查找1到19列
                            .le("bay1", 19)
                            .eq("loc_sts", "O")
                            .eq("whs_type", rowLastnoType.getType().longValue())
                            .orderBy("lev1", true)
                            .orderBy("bay1", true)); // 最浅库位
                    for (LocMast locMast1 : locMasts) {
                        if (!VersionUtils.locMoveCheckLocTypeComplete(locMast1, locTypeDto)) {
                            continue;
                        }
                        if (Utils.BooleanWhsTypeStaIoType(rowLastno)) {
                            // ???????????????
                            LocMast locMast2 = locMastService.selectLocByLocStsPakInO(curRow, nearRow, locMast1, rowLastnoType.getType().longValue());
                            if (!Cools.isEmpty(locMast2) && locMast2.getRow1() == curRow) {
                                locMast = locMast2;
                                found = true;
                                break;
                            }
                        }
                    }
                    if (found) {
                        break; // ???????????
                    }
            if (Cools.isEmpty(locMast)) {
                locMast = findAgvLocByRows(rowLastno, rowLastnoType, getAgvFallbackRows(rowLastno),
                        1, 19, curRow, nearRow, locTypeDto, true);
                if (!Cools.isEmpty(locMast)) {
                    crnNo = locMast.getCrnNo();
                }
            }
        }
        // Retry search
        if (Cools.isEmpty(locMast) || !locMast.getLocSts().equals("O")) {
            // Scan next aisle first, then retry with upward-compatible locType1.
@@ -1517,3 +2003,4 @@
    }
}
src/main/java/com/zy/common/web/WcsController.java
@@ -79,7 +79,7 @@
            return R.ok(dto1);
        }
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("io_type", 1));
        if (!Cools.isEmpty(wrkMasts)&&wrkMasts.size()>30) {
        if (!Cools.isEmpty(wrkMasts)&&wrkMasts.size()>100) {
            return R.error("限行");
        }
            waitPakins = waitPakinService.selectList(new EntityWrapper<WaitPakin>().eq("zpallet", param.getBarcode()));
src/main/java/com/zy/system/controller/ConfigController.java
@@ -71,8 +71,8 @@
            configService.insert(config);
        } else {
            configService.updateById(config);
            Parameter.reset();
        }
        Parameter.reset();
        return R.ok();
    }
@@ -85,6 +85,7 @@
            }
        }
        configService.insert(config);
        Parameter.reset();
        return R.ok();
    }
@@ -111,6 +112,7 @@
            return R.error();
        }
        configService.deleteBatchIds(Arrays.asList(ids));
        Parameter.reset();
        return R.ok();
    }
src/main/webapp/static/js/basDevp/basDevp.js
@@ -80,6 +80,7 @@
            ,{field: 'locType1$', align: 'center',title: '高低'}
            ,{field: 'barcode', align: 'center',title: '条形码'}
            ,{field: 'inQty', align: 'center',title: '入库暂存'}
            ,{field: 'area$', align: 'center',title: '绑定库区'}
            // ,{field: 'row1', align: 'center',title: ''}
            // ,{field: 'ioTime$', align: 'center',title: ''}
            // ,{field: 'area', align: 'center',title: ''}
@@ -645,3 +646,4 @@
        $("#search").click();
    }
});
src/main/webapp/views/basDevp/basDevp_detail.html
@@ -150,10 +150,10 @@
                <input id="ioTime$" class="layui-input" type="text" autocomplete="off">
            </div>
        </div>
        <div class="layui-inline"  style="width:31%;display: none">
            <label class="layui-form-label">:</label>
        <div class="layui-inline"  style="width:31%;">
            <label class="layui-form-label">绑定库区:</label>
            <div class="layui-input-inline">
                <input id="area" class="layui-input" type="text">
                <input id="area" class="layui-input" type="text" placeholder="空=不限制,支持 1/2/3 或 A/B/C">
            </div>
        </div>
        <div class="layui-inline"  style="width:31%;display: none">
src/main/webapp/views/config/config_detail.html
@@ -69,7 +69,8 @@
        </div>
        <div id="prompt">
            温馨提示:请仔细填写相关信息,<span class="extrude"><span class="not-null">*</span> 为必填选项。</span>
            温馨提示:请仔细填写相关信息,<span class="extrude"><span class="not-null">*</span> 为必填选项。</span><br>
            库区排配置可维护编码:AreaCrnNo1/AreaCrnNo2/AreaCrnNo3值支持 1-28这样格式的堆垛机号
        </div>
    </form>
</div>