自动化立体仓库 - WMS系统
我们现在讨论一下系统找库位方案, 如何实现,对现有找库位规则进行整改,数据库也要整改
1、要能方便的填写单伸堆垛机或双伸堆垛机的深浅库位配置
2、根据设备状态分配库位,离线设备不分配
3、库位分配要均衡到每一个设备
4、库位高度需要匹配到对应库位信息,低库位能向上兼容
5、空托盘优先放在locType2库位=1的库位,没有这种库位了,允许放到其他库位
6、给入库站点设置有限去那些堆垛机,其次去那些堆垛机,弄成页面可以配置入库站点
7、在系统配置新增优先放前几列的配置,当入库的货物是高频货物时放在前几列
8、组托中会标识该托盘是高频还是低频,如果是高频则从前往后找库位,如果是低频则从后往前找库位
9、找库位时locMast中whsType字段无用
30个文件已修改
2034 ■■■■ 已修改文件
src/main/java/com/zy/api/entity/OrderParams.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/BasDevpController.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/DigitalTwinController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/OpenController.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/WorkController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/BasDevp.java 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/WaitPakin.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/WaitPakinLog.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/BasCrnDepthRuleRuntimePreviewParam.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/CombParam.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/MesToCombParam.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/result/FindLocNoAttributeVo.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/WaitPakinLogMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/DigitalTwinService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/OpenService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/DigitalTwinServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/MobileServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java 434 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/utils/Utils.java 215 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/entity/Parameter.java 3 ●●●●● 补丁 | 查看 | 原始文档 | 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/resources/application.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/BasDevpMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/WaitPakinLogMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/WaitPakinMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basDevp/basDevp.js 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basCrnDepthRule/basCrnDepthRule.html 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basDevp/basDevp_detail.html 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/api/entity/OrderParams.java
@@ -2,6 +2,8 @@
import java.io.Serializable;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.format.annotation.DateTimeFormat;
import com.fasterxml.jackson.annotation.JsonFormat;
@@ -59,6 +61,40 @@
    @ApiModelProperty("WMS ID")
    private String wms_id;
    @ApiModelProperty("客户Id")
    private String customerId;
    @ApiModelProperty("客户名称")
    private String customerName;
    @ApiModelProperty("item编号")
    private String item;
    @JsonProperty("package")
    @JSONField(name = "package")
    @ApiModelProperty("是否散货,0 非散货;1 散货")
    private Integer packageFlag;
    @ApiModelProperty("外库门号")
    private String outDoorNo;
    @ApiModelProperty("车牌号")
    private String plateNo;
    @ApiModelProperty("车次号")
    private String trainNo;
    @ApiModelProperty("货物频次:1=高频,2=低频")
    private Integer freqType;
    @ApiModelProperty("立方数,单位:立方米")
    private Double cubeNumber;
    @ApiModelProperty("集装箱号")
    private String containerNo;
    @ApiModelProperty("teu,按orderId去重统计")
    private Integer teu;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java
@@ -706,19 +706,26 @@
            return R.error("当前目标库位不存在");
        }
        Integer preferredArea = resolveReassignArea(wrkMast, currentLoc);
        if (preferredArea == null) {
        LocTypeDto locTypeDto = buildReassignLocTypeDto(currentLoc);
        List<Integer> areaOrder = buildReassignAreaOrder(wrkMast, currentLoc);
        if (Cools.isEmpty(areaOrder)) {
            return R.error("无法确定任务所属库区");
        }
        List<Integer> candidateCrnNos = buildReassignCandidateCrnNos(preferredArea, wrkMast.getCrnNo());
        if (candidateCrnNos.isEmpty()) {
            return R.error("当前库区没有其他堆垛机可供重分配");
        StartupDto startupDto = null;
        Integer preferredArea = null;
        for (Integer area : areaOrder) {
            List<Integer> candidateCrnNos = buildReassignCandidateCrnNos(area, wrkMast.getCrnNo());
            if (candidateCrnNos.isEmpty()) {
                continue;
            }
            startupDto = commonService.findRun2InboundLocByCandidateCrnNos(
                    wrkMast.getSourceStaNo(), wrkMast.getIoType(), area, candidateCrnNos, locTypeDto);
            if (startupDto != null && !Cools.isEmpty(startupDto.getLocNo())) {
                preferredArea = area;
                break;
            }
        }
        LocTypeDto locTypeDto = buildReassignLocTypeDto(currentLoc);
        StartupDto startupDto = commonService.findRun2InboundLocByCandidateCrnNos(
                wrkMast.getSourceStaNo(), wrkMast.getIoType(), preferredArea, candidateCrnNos, locTypeDto);
        if (startupDto == null || Cools.isEmpty(startupDto.getLocNo())) {
            return R.error("当前库区没有可重新分配的空库位");
        }
@@ -801,15 +808,19 @@
    }
    private Integer resolveReassignArea(WrkMast wrkMast, LocMast currentLoc) {
        Integer stationArea = Utils.getStationStorageArea(wrkMast.getSourceStaNo());
        if (belongsToArea(stationArea, wrkMast.getCrnNo(), currentLoc)) {
            return stationArea;
        List<Integer> stationAreas = Utils.getStationStorageAreas(wrkMast.getSourceStaNo());
        if (!Cools.isEmpty(stationAreas)) {
            for (Integer area : stationAreas) {
                if (belongsToArea(area, wrkMast.getCrnNo(), currentLoc)) {
                    return area;
                }
            }
        }
        Integer fallbackArea = findAreaByCurrentTask(wrkMast.getCrnNo(), currentLoc);
        if (fallbackArea != null) {
            return fallbackArea;
        }
        return stationArea;
        return Utils.getStationStorageArea(wrkMast.getSourceStaNo());
    }
    private Integer findAreaByCurrentTask(Integer currentCrnNo, LocMast currentLoc) {
@@ -821,6 +832,25 @@
        return null;
    }
    private List<Integer> buildReassignAreaOrder(WrkMast wrkMast, LocMast currentLoc) {
        LinkedHashSet<Integer> areaOrder = new LinkedHashSet<>();
        List<Integer> stationAreas = Utils.getStationStorageAreas(wrkMast.getSourceStaNo());
        if (!Cools.isEmpty(stationAreas)) {
            areaOrder.addAll(stationAreas);
        }
        Integer currentArea = findAreaByCurrentTask(wrkMast.getCrnNo(), currentLoc);
        if (currentArea != null) {
            areaOrder.add(currentArea);
        }
        if (areaOrder.isEmpty()) {
            Integer resolvedArea = resolveReassignArea(wrkMast, currentLoc);
            if (resolvedArea != null) {
                areaOrder.add(resolvedArea);
            }
        }
        return new ArrayList<>(areaOrder);
    }
    private boolean belongsToArea(Integer area, Integer currentCrnNo, LocMast currentLoc) {
        if (area == null || area <= 0) {
            return false;
src/main/java/com/zy/asrs/controller/BasDevpController.java
@@ -7,13 +7,16 @@
import com.baomidou.mybatisplus.plugins.Page;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.param.BasDevpInitParam;
import com.zy.asrs.service.BasCrnpService;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.utils.Utils;
import com.zy.common.web.BaseController;
import com.core.annotations.ManagerAuth;
import com.core.common.BaseRes;
import com.core.common.Cools;
import com.core.common.DateUtils;
import com.core.common.R;
import com.core.exception.CoolException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -24,6 +27,8 @@
    @Autowired
    private BasDevpService basDevpService;
    @Autowired
    private BasCrnpService basCrnpService;
    @RequestMapping(value = "/basDevp/init/auth")
    @ManagerAuth(memo = "初始化站点")
@@ -85,6 +90,7 @@
    @RequestMapping(value = "/basDevp/add/auth")
    @ManagerAuth(memo = "站点添加")
    public R add(BasDevp basDevp) {
        normalizeInboundPriority(basDevp, null);
        basDevp.setModiUser(getUserId());
        basDevp.setModiTime(new Date());
        basDevp.setAppeUser(getUserId());
@@ -99,6 +105,11 @@
        if (Cools.isEmpty(basDevp) || null==basDevp.getDevNo()){
            return R.error();
        }
        BasDevp existing = basDevpService.selectById(basDevp.getDevNo());
        if (existing == null) {
            return R.error("站点不存在");
        }
        normalizeInboundPriority(basDevp, existing);
        basDevp.setModiUser(getUserId());
        basDevp.setModiTime(new Date());
        basDevpService.updateById(basDevp);
@@ -155,4 +166,65 @@
        return R.ok();
    }
    private void normalizeInboundPriority(BasDevp basDevp, BasDevp existing) {
        if (basDevp == null) {
            return;
        }
        String firstCsv = resolveCsvValue(basDevp.getInFirstCrnCsv(), existing == null ? null : existing.getInFirstCrnCsv());
        String secondCsv = resolveCsvValue(basDevp.getInSecondCrnCsv(), existing == null ? null : existing.getInSecondCrnCsv());
        List<Integer> firstCrnNos = parseCrnPool("第一优先池", firstCsv);
        List<Integer> secondCrnNos = parseCrnPool("第二优先池", secondCsv);
        Set<Integer> duplicateCrnNos = new LinkedHashSet<>(firstCrnNos);
        duplicateCrnNos.retainAll(secondCrnNos);
        if (!duplicateCrnNos.isEmpty()) {
            throw new CoolException("第一优先池和第二优先池不能重复配置同一台堆垛机:" + duplicateCrnNos);
        }
        basDevp.setInFirstCrnCsv(Utils.normalizeCrnCsv(firstCrnNos));
        basDevp.setInSecondCrnCsv(Utils.normalizeCrnCsv(secondCrnNos));
        if (existing == null) {
            basDevp.setInFirstCrnCurrentNo(null);
            basDevp.setInSecondCrnCurrentNo(null);
            return;
        }
        basDevp.setInFirstCrnCurrentNo(resolveCurrentNo(existing.getInFirstCrnCurrentNo(), firstCrnNos));
        basDevp.setInSecondCrnCurrentNo(resolveCurrentNo(existing.getInSecondCrnCurrentNo(), secondCrnNos));
    }
    private String resolveCsvValue(String incoming, String existing) {
        if (incoming == null) {
            return existing;
        }
        return incoming;
    }
    private List<Integer> parseCrnPool(String label, String csv) {
        List<Integer> crnNos;
        try {
            crnNos = Utils.parseCrnNos(csv);
        } catch (CoolException e) {
            throw new CoolException(label + e.getMessage());
        }
        List<Integer> distinctCrnNos = Utils.distinctCrnNos(crnNos);
        if (crnNos.size() != distinctCrnNos.size()) {
            throw new CoolException(label + "存在重复堆垛机号");
        }
        for (Integer crnNo : distinctCrnNos) {
            if (basCrnpService.selectById(crnNo) == null) {
                throw new CoolException(label + "包含不存在的堆垛机号:" + crnNo);
            }
        }
        return distinctCrnNos;
    }
    private Integer resolveCurrentNo(Integer currentNo, List<Integer> crnNos) {
        if (currentNo == null || Cools.isEmpty(crnNos) || !crnNos.contains(currentNo)) {
            return null;
        }
        return currentNo;
    }
}
src/main/java/com/zy/asrs/controller/DigitalTwinController.java
@@ -142,4 +142,17 @@
        return R.ok(digitalTwinService.getLocalDetal());
    }
    /**
     * 按库位号查询库存明细
     *
     * @param locNo 库位号
     */
    @RequestMapping(value = "/getLocalDetalByLocNo")
    public R getLocalDetalByLocNo(@RequestParam(required = false) String locNo) {
        if (locNo == null || locNo.trim().isEmpty()) {
            return R.error("locNo不能为空");
        }
        return R.ok(digitalTwinService.getLocalDetalByLocNo(locNo));
    }
}
src/main/java/com/zy/asrs/controller/OpenController.java
@@ -25,6 +25,7 @@
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
 * Created by vincent on 2022/4/8
@@ -49,6 +50,8 @@
    private WaitPakinService waitPakinService;
    @Autowired
    private WrkDetlService wrkDetlService;
    @Autowired
    private WrkDetlLogService wrkDetlLogService;
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
@@ -123,6 +126,23 @@
                                             HttpServletRequest request) {
        auth(appkey, param, request);
        return R.ok().add(openService.pakinOrderComplete(param));
    }
    /**
     * 托盘入库历史记录重报ERP
     */
    @PostMapping("/order/pakin/erp/report/v1")
//    @AppAuth(memo = "入库历史重报ERP")
    public synchronized R reportPakinHistoryToErp(@RequestBody(required = false) List<String> barcodes) {
//        auth(appkey, barcodes, request);
        if (Cools.isEmpty(barcodes)) {
            return R.parse(BaseRes.PARAM);
        }
        try {
            return openService.reportPakinHistoryToErp(barcodes);
        } catch (Exception e) {
            return R.error(e.getMessage());
        }
    }
@@ -846,12 +866,130 @@
        }
        List<WrkDetl> wrkDetls = wrkDetlService.selectByWrkNo(Integer.valueOf(param.getTaskNo()));
        String costTime = resolveCostTime(wrkMast, wrkDetls);
        HashMap<String, Object> map = new HashMap<>();
        map.put("taskNo", param.getTaskNo());
        map.put("ioType", wrkMast.getIoType());
        map.put("costTime", costTime);
        map.put("wrkDetls", wrkDetls);
        return R.ok().add(map);
    }
    private String resolveCostTime(WrkMast wrkMast, List<WrkDetl> wrkDetls) {
        Date createTime = resolveOrderCreateTime(resolveOrderNo(wrkMast, wrkDetls));
        if (createTime == null) {
            createTime = resolveTaskCreateTime(wrkMast);
        }
        if (createTime == null) {
            return "0";
        }
        long minutes = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - createTime.getTime());
        if (minutes < 0L) {
            minutes = 0L;
        }
        return String.valueOf(minutes);
    }
    private String resolveOrderNo(WrkMast wrkMast, List<WrkDetl> wrkDetls) {
        if (wrkMast != null && !Cools.isEmpty(wrkMast.getUserNo())) {
            return wrkMast.getUserNo();
        }
        if (Cools.isEmpty(wrkDetls)) {
            return null;
        }
        for (WrkDetl wrkDetl : wrkDetls) {
            if (wrkDetl != null && !Cools.isEmpty(wrkDetl.getOrderNo())) {
                return wrkDetl.getOrderNo();
            }
        }
        return null;
    }
    private Date resolveOrderCreateTime(String orderNo) {
        if (Cools.isEmpty(orderNo)) {
            return null;
        }
        Date historyCreateTime = minDate(
                minCreateTime(wrkMastLogService.selectList(new EntityWrapper<WrkMastLog>().eq("user_no", orderNo))),
                minCreateTime(wrkDetlLogService.selectList(new EntityWrapper<WrkDetlLog>().eq("order_no", orderNo)))
        );
        if (historyCreateTime != null) {
            return historyCreateTime;
        }
        return minDate(
                minCreateTime(wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("user_no", orderNo))),
                minCreateTime(wrkDetlService.selectList(new EntityWrapper<WrkDetl>().eq("order_no", orderNo)))
        );
    }
    private Date minDate(Date first, Date second) {
        if (first == null) {
            return second;
        }
        if (second == null) {
            return first;
        }
        return first.before(second) ? first : second;
    }
    private Date resolveTaskCreateTime(WrkMast wrkMast) {
        if (wrkMast == null) {
            return null;
        }
        return firstDate(wrkMast.getAppeTime(), wrkMast.getIoTime(), wrkMast.getModiTime());
    }
    private Date firstDate(Date... dates) {
        if (dates == null || dates.length == 0) {
            return null;
        }
        for (Date date : dates) {
            if (date != null) {
                return date;
            }
        }
        return null;
    }
    private Date minCreateTime(List<?> list) {
        if (Cools.isEmpty(list)) {
            return null;
        }
        Date min = null;
        for (Object item : list) {
            Date createTime = resolveCreateTime(item);
            if (createTime == null) {
                continue;
            }
            if (min == null || createTime.before(min)) {
                min = createTime;
            }
        }
        return min;
    }
    private Date resolveCreateTime(Object item) {
        if (item instanceof WrkMast) {
            WrkMast wrkMast = (WrkMast) item;
            return firstDate(wrkMast.getAppeTime(), wrkMast.getIoTime(), wrkMast.getModiTime());
        }
        if (item instanceof WrkMastLog) {
            WrkMastLog wrkMastLog = (WrkMastLog) item;
            return firstDate(wrkMastLog.getAppeTime(), wrkMastLog.getIoTime(), wrkMastLog.getModiTime());
        }
        if (item instanceof WrkDetl) {
            WrkDetl wrkDetl = (WrkDetl) item;
            return firstDate(wrkDetl.getAppeTime(), wrkDetl.getIoTime(), wrkDetl.getModiTime());
        }
        if (item instanceof WrkDetlLog) {
            WrkDetlLog wrkDetlLog = (WrkDetlLog) item;
            return firstDate(wrkDetlLog.getAppeTime(), wrkDetlLog.getIoTime(), wrkDetlLog.getModiTime());
        }
        return null;
    }
}
src/main/java/com/zy/asrs/controller/WorkController.java
@@ -49,7 +49,7 @@
    }
    @RequestMapping("/test/station/storage/crn/list")
    @ManagerAuth(memo = "测试站点库区堆垛机顺序")
    @ManagerAuth(memo = "测试站点入库优先堆垛机顺序")
    public R testStationStorageCrnList(@RequestParam Integer stationId,
                                       @RequestParam(required = false) Integer locType1,
                                       @RequestParam(required = false) String matnr) {
src/main/java/com/zy/asrs/entity/BasDevp.java
@@ -8,12 +8,14 @@
import com.zy.system.service.UserService;
import com.core.common.Cools;
import com.core.common.SpringUtils;
import com.zy.asrs.utils.Utils;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
@Data
@@ -141,8 +143,24 @@
    @TableField("io_time")
    private Date ioTime;
    @ApiModelProperty(value= "绑定库区")
    @ApiModelProperty(value= "绑定库区(仅管理用途)")
    private String area;
    @ApiModelProperty(value= "入库第一优先堆垛机,CSV")
    @TableField("in_first_crn_csv")
    private String inFirstCrnCsv;
    @ApiModelProperty(value= "入库第二优先堆垛机,CSV")
    @TableField("in_second_crn_csv")
    private String inSecondCrnCsv;
    @ApiModelProperty(value= "入库第一优先池当前堆垛机号")
    @TableField("in_first_crn_current_no")
    private Integer inFirstCrnCurrentNo;
    @ApiModelProperty(value= "入库第二优先池当前堆垛机号")
    @TableField("in_second_crn_current_no")
    private Integer inSecondCrnCurrentNo;
    @ApiModelProperty(value= "")
    @TableField("in_ok")
@@ -231,18 +249,18 @@
        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库区";
        List<Integer> areas = Utils.parseStorageAreas(this.area);
        if (!Cools.isEmpty(areas)) {
            StringBuilder builder = new StringBuilder();
            for (Integer areaNo : areas) {
                if (builder.length() > 0) {
                    builder.append("、");
                }
                builder.append(Utils.formatStorageArea(areaNo));
            }
            return builder.toString();
        }
        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;
        return this.area.trim();
    }
    public String getLocType1$() {
        if (null == this.locType1){ return null; }
src/main/java/com/zy/asrs/entity/WaitPakin.java
@@ -43,6 +43,10 @@
    @ApiModelProperty(value= "建议入库区域:堆垛机号")
    private String origin;
    @ApiModelProperty(value= "货物频次:1=高频,2=低频")
    @TableField("freq_type")
    private Integer freqType;
    @ApiModelProperty(value= "库位编码,若有,则存储到指定库位。为后续分区预留")
    private String manu;
src/main/java/com/zy/asrs/entity/WaitPakinLog.java
@@ -73,6 +73,10 @@
    @ApiModelProperty(value= "产地")
    private String origin;
    @ApiModelProperty(value= "货物频次:1=高频,2=低频")
    @TableField("freq_type")
    private Integer freqType;
    @ApiModelProperty(value= "厂家")
    private String manu;
src/main/java/com/zy/asrs/entity/param/BasCrnDepthRuleRuntimePreviewParam.java
@@ -13,6 +13,8 @@
    private String matnr;
    private Integer freqType;
    private Short locType1;
    private Short locType2;
src/main/java/com/zy/asrs/entity/param/CombParam.java
@@ -26,6 +26,11 @@
        private String orderNo;
        /**
         * 货物频次:1=高频,2=低频
         */
        private Integer freqType;
        // 物料编号
        private String matnr = "";
src/main/java/com/zy/asrs/entity/param/MesToCombParam.java
@@ -25,6 +25,10 @@
    private String batchId;
    //货物状态,枚举:0 正常;1 异常;
    private Integer status;
    //货物频次,1=高频,2=低频
    private Integer freqType;
    //是否满板,0 空板;1 满板;2 半板;
    private Integer full;
    //建议入库区域,枚举,1 A库;2 B库;3 C库;或某个巷道;
src/main/java/com/zy/asrs/entity/result/FindLocNoAttributeVo.java
@@ -5,6 +5,8 @@
import com.zy.asrs.entity.WrkDetl;
import com.zy.asrs.entity.param.FullStoreParam;
import java.util.List;
public class FindLocNoAttributeVo {
    private String matnr = "";
    private String specs = "";
@@ -18,6 +20,8 @@
    private String boxType2 = "1";
    private String boxType3 = "1";
    private Integer outArea;
    private List<Integer> outAreas;
    private Integer freqType;
    public FindLocNoAttributeVo() {
    }
@@ -82,6 +86,7 @@
        this.boxType1 = waitPakin.getBoxType1();
        this.boxType2 = waitPakin.getBoxType2();
        this.boxType3 = waitPakin.getBoxType3();
        this.freqType = waitPakin.getFreqType();
    }
    public FindLocNoAttributeVo(FullStoreParam.MatCodeStore matCodeStore) {
@@ -208,6 +213,22 @@
        this.outArea = outArea;
    }
    public List<Integer> getOutAreas() {
        return outAreas;
    }
    public void setOutAreas(List<Integer> outAreas) {
        this.outAreas = outAreas;
    }
    public Integer getFreqType() {
        return freqType;
    }
    public void setFreqType(Integer freqType) {
        this.freqType = freqType;
    }
    public boolean beSimilar(LocDetl locDetl){
        return (this.matnr.equals(locDetl.getMatnr())
src/main/java/com/zy/asrs/mapper/WaitPakinLogMapper.java
@@ -4,13 +4,15 @@
import com.zy.asrs.entity.WaitPakinLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface WaitPakinLogMapper extends BaseMapper<WaitPakinLog> {
    @Insert("insert into cust_wait_pakin_log select * from cust_wait_pakin where zpallet=#{zpallet}")
    int save(String zpallet);
    @Insert("insert into cust_wait_pakin_log (zpallet, anfme, loc_no, matnr, maktx, batch, order_no, origin, freq_type, manu, specs, model, color, brand, unit, price, sku, units, barcode, manu_date, item_num, safe_qty, weight, man_length, volume, three_code, supp, supp_code, be_batch, dead_time, dead_warn, source, inspect, danger, status, io_status, modi_time, modi_user, appe_time, appe_user, memo, standby1, standby2, standby3, box_type1, box_type2, box_type3) " +
            "select zpallet, anfme, loc_no, matnr, maktx, batch, order_no, origin, freq_type, manu, specs, model, color, brand, unit, price, sku, units, barcode, manu_date, item_num, safe_qty, weight, man_length, volume, three_code, supp, supp_code, be_batch, dead_time, dead_warn, source, inspect, danger, status, io_status, modi_time, modi_user, appe_time, appe_user, memo, standby1, standby2, standby3, box_type1, box_type2, box_type3 from cust_wait_pakin where zpallet=#{zpallet}")
    int save(@Param("zpallet") String zpallet);
}
src/main/java/com/zy/asrs/service/DigitalTwinService.java
@@ -80,5 +80,7 @@
    List<Map<String, Object>> getLocalDetal();
    List<Map<String, Object>> getLocalDetalByLocNo(String locNo);
    Map<String, Object> getLocInfo();
}
src/main/java/com/zy/asrs/service/OpenService.java
@@ -45,6 +45,11 @@
     */
    List<StockVo> queryStock();
    /**
     * 按托盘码批量重报入库历史到 ERP。
     */
    R reportPakinHistoryToErp(List<String> barcodes);
    // ------------------------------------------
    // 打包上线
src/main/java/com/zy/asrs/service/impl/DigitalTwinServiceImpl.java
@@ -445,6 +445,34 @@
        return result;
    }
    @Override
    public List<Map<String, Object>> getLocalDetalByLocNo(String locNo) {
        if (locNo == null || locNo.trim().isEmpty()) {
            return Collections.emptyList();
        }
        List<LocDetl> locDetls = locDetlService.selectList(new EntityWrapper<LocDetl>()
                .eq("loc_no", locNo.trim())
                .orderBy("appe_time", true));
        List<Map<String, Object>> result = new ArrayList<>();
        for (LocDetl locDetl : locDetls) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("locNo", locDetl.getLocNo());
            item.put("zpallet", locDetl.getZpallet());
            item.put("matnr", locDetl.getMatnr());
            item.put("maktx", locDetl.getMaktx());
            item.put("specs", locDetl.getSpecs());
            item.put("batch", locDetl.getBatch());
            item.put("anfme", locDetl.getAnfme());
            item.put("orderNo", locDetl.getOrderNo());
            item.put("frozen", locDetl.getFrozen());
            result.add(item);
        }
        return result;
    }
    public Map<String, Object> getLocInfo() {
        List<LocMast> LocMasts = locMastMapper.selectList(new EntityWrapper<>());
        Map<String, Object> result = new HashMap<>();
src/main/java/com/zy/asrs/service/impl/MobileServiceImpl.java
@@ -574,6 +574,7 @@
        }
        Date now = new Date();
        Integer freqType = resolveCombFreqType(param.getCombMats());
        boolean allEmpty = true;
        if (param.getCombMats() != null) {
            for (CombParam.CombMat mat : param.getCombMats()) {
@@ -616,6 +617,7 @@
                waitPakin.setZpallet(param.getBarcode());   // 托盘码
                waitPakin.setIoStatus("N");     // 入出状态
                waitPakin.setAnfme(detlDto.getAnfme());  // 数量
                waitPakin.setFreqType(freqType);
                waitPakin.setStatus("Y");    // 状态
                waitPakin.setAppeUser(userId);
                waitPakin.setAppeTime(now);
@@ -681,6 +683,7 @@
                waitPakin.setZpallet(param.getBarcode());   // 托盘码
                waitPakin.setIoStatus("N");     // 入出状态
                waitPakin.setAnfme(detlDto.getAnfme());  // 数量
                waitPakin.setFreqType(freqType);
                waitPakin.setStatus("Y");    // 状态
                waitPakin.setAppeUser(userId);
                waitPakin.setAppeTime(now);
@@ -704,6 +707,26 @@
//            orderService.updateSettle(order.getId(), 2L, userId);
            OrderInAndOutUtil.updateOrder(Boolean.TRUE,order.getId(), 2L, userId);
        }
    }
    private Integer resolveCombFreqType(List<CombParam.CombMat> combMats) {
        Integer resolvedFreqType = null;
        if (Cools.isEmpty(combMats)) {
            return null;
        }
        for (CombParam.CombMat combMat : combMats) {
            if (combMat == null || combMat.getFreqType() == null || combMat.getFreqType() <= 0) {
                continue;
            }
            if (resolvedFreqType == null) {
                resolvedFreqType = combMat.getFreqType();
                continue;
            }
            if (!resolvedFreqType.equals(combMat.getFreqType())) {
                throw new CoolException("组托货物频次不一致");
            }
        }
        return resolvedFreqType;
    }
@@ -1056,6 +1079,7 @@
            throw new CoolException(param.getBarcode() + "数据正在进行入库");
        }
        Date now = new Date();
        Integer freqType = resolveCombFreqType(param.getCombMats());
        boolean packDown = Parameter.get().getPackDown().equals("true");
@@ -1189,6 +1213,7 @@
                waitPakin.setZpallet(param.getBarcode());   // 托盘码
                waitPakin.setIoStatus("N");     // 入出状态
                waitPakin.setAnfme(detlDto.getAnfme());  // 数量
                waitPakin.setFreqType(freqType);
                waitPakin.setStatus("Y");    // 状态
                waitPakin.setAppeUser(userId);
                waitPakin.setAppeTime(now);
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java
@@ -19,6 +19,7 @@
import com.zy.asrs.utils.Utils;
import com.zy.common.constant.AgvConstant;
import com.zy.common.constant.ArmConstant;
import com.zy.common.entity.Parameter;
import com.zy.common.model.DetlDto;
import com.zy.common.model.LocDetlDto;
import com.zy.common.model.LocDto;
@@ -33,6 +34,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
@@ -42,6 +45,15 @@
@Slf4j
@Service
public class OpenServiceImpl implements OpenService {
    private static final Map<Integer, BigDecimal> INBOUND_WEIGHT_FACTOR_BY_SOURCE_STA;
    private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    static {
        Map<Integer, BigDecimal> factorMap = new HashMap<>();
        factorMap.put(112, new BigDecimal("0.98"));
        INBOUND_WEIGHT_FACTOR_BY_SOURCE_STA = Collections.unmodifiableMap(factorMap);
    }
    @Autowired
    private OrderService orderService;
@@ -89,12 +101,22 @@
    private String stationAddress;
    @Value("${erp.address.URL:}")
    private String erpUrl;
    @Value("${erp.switch.ErpReportOld}")
    private boolean erpReportOld;
    @Value("${erp.address.Inaddress:}")
    private String erpInAddress;
    @Value("${erp.address.OutErroraddress:}")
    private String erpOutErrorAddress;
    @Autowired
    private WaitPakinService waitPakinService;
    @Autowired
    private WaitPakinLogService waitPakinLogService;
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
    private WrkMastLogService wrkMastLogService;
    @Autowired
    private WrkDetlLogService wrkDetlLogService;
    @Autowired
    private WcsApiService wcsApiService;
    @Autowired
@@ -659,6 +681,117 @@
    @Transactional
    public List<StockVo> queryStock() {
        return locDetlService.queryStockTotal();
    }
    @Override
    public R reportPakinHistoryToErp(List<String> barcodes) {
        List<String> normalizedBarcodes = normalizeBarcodes(barcodes);
        if (normalizedBarcodes.isEmpty()) {
            return R.error("托盘码集合不能为空");
        }
        if (!isErpReportEnabled()) {
            return R.error("ERP reporting is disabled");
        }
        if (Cools.isEmpty(erpInAddress)) {
            return R.error("ERP入库上报地址未配置");
        }
        Map<String, WrkMastLog> latestInboundLogMap = loadLatestInboundHistoryLogMap(normalizedBarcodes);
        Map<Integer, List<WrkDetlLog>> wrkDetlLogMap = loadWrkDetlLogMap(latestInboundLogMap.values());
        Map<String, WaitPakinLog> waitPakinLogMap = loadWaitPakinLogMap(normalizedBarcodes);
        String requestUrl = buildErpInboundRequestUrl();
        List<Map<String, Object>> rows = new ArrayList<>();
        int successCount = 0;
        int failCount = 0;
        for (String barcode : normalizedBarcodes) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("barcode", barcode);
            WrkMastLog wrkMastLog = latestInboundLogMap.get(barcode);
            if (wrkMastLog == null) {
                row.put("success", false);
                row.put("message", "未找到最新入库历史记录");
                rows.add(row);
                failCount++;
                continue;
            }
            WaitPakinLog waitPakinLog = waitPakinLogMap.get(barcode);
            List<WrkDetlLog> wrkDetlLogs = wrkDetlLogMap.getOrDefault(wrkMastLog.getWrkNo(), Collections.emptyList());
            ErpPakinReportParam param = buildInboundErpParam(barcode, wrkMastLog, wrkDetlLogs, waitPakinLog);
            if (Cools.isEmpty(param.getPalletId())) {
                row.put("success", false);
                row.put("message", "托盘码缺少上报字段[palletId]");
                rows.add(row);
                failCount++;
                continue;
            }
            if (Cools.isEmpty(param.getLocId())) {
                row.put("success", false);
                row.put("message", "托盘码缺少上报字段[locId]");
                rows.add(row);
                failCount++;
                continue;
            }
            String request = JSON.toJSONString(param);
            String response = "";
            boolean success = false;
            String errorMsg = null;
            try {
                response = new HttpHandler.Builder()
                        .setUri(erpUrl)
                        .setPath(erpInAddress)
                        .setJson(request)
                        .build()
                        .doPost();
                success = isErpCallSuccess(response);
                if (!success) {
                    errorMsg = extractErpCallError(response);
                }
            } catch (Exception e) {
                errorMsg = e.getMessage();
            } finally {
                try {
                    apiLogService.save(
                            "Inbound ERP Report",
                            requestUrl,
                            null,
                            "127.0.0.1",
                            request,
                            response,
                            success,
                            "barcode=" + barcode + ",workNo=" + wrkMastLog.getWrkNo()
                    );
                } catch (Exception logEx) {
                    log.error("save inbound erp api log failed", logEx);
                }
            }
            row.put("workNo", wrkMastLog.getWrkNo());
            Integer sourceStaNo = resolveInboundSourceStaNo(wrkMastLog);
            row.put("sourceStaNo", sourceStaNo);
            row.put("weightFactor", resolveInboundWeightFactor(sourceStaNo));
            row.put("erpWeight", param.getWeight());
            row.put("success", success);
            row.put("message", success ? "OK" : errorMsg);
            rows.add(row);
            if (success) {
                successCount++;
            } else {
                failCount++;
            }
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("total", normalizedBarcodes.size());
        result.put("successCount", successCount);
        result.put("failCount", failCount);
        result.put("rows", rows);
        return R.ok().add(result);
    }
    @Override
@@ -1337,6 +1470,7 @@
        waitPakin.setIoStatus("N");     // 入出状态
        waitPakin.setAnfme(param.getAnfme());  // 数量
        waitPakin.setFreqType(param.getFreqType());
        waitPakin.setStatus("Y");    // 状态
        waitPakin.setAppeUser(9995L);
        waitPakin.setAppeTime(now);
@@ -1662,6 +1796,306 @@
        return response;
    }
    private boolean isErpReportEnabled() {
        if (!erpReportOld) {
            return false;
        }
        String erpReport = Parameter.get().getErpReport();
        return Cools.isEmpty(erpReport) || "true".equalsIgnoreCase(erpReport);
    }
    private List<String> normalizeBarcodes(List<String> barcodes) {
        if (barcodes == null || barcodes.isEmpty()) {
            return Collections.emptyList();
        }
        LinkedHashSet<String> normalized = new LinkedHashSet<>();
        for (String barcode : barcodes) {
            if (barcode == null) {
                continue;
            }
            String value = barcode.trim();
            if (!value.isEmpty()) {
                normalized.add(value);
            }
        }
        return new ArrayList<>(normalized);
    }
    private Map<String, WrkMastLog> loadLatestInboundHistoryLogMap(List<String> barcodes) {
        Map<String, WrkMastLog> latestInboundLogMap = new LinkedHashMap<>();
        if (barcodes == null || barcodes.isEmpty()) {
            return latestInboundLogMap;
        }
        List<WrkMastLog> wrkMastLogs = wrkMastLogService.selectList(new EntityWrapper<WrkMastLog>()
                .in("barcode", barcodes)
                .orderBy("barcode", true)
                .orderBy("io_time", false)
                .orderBy("wrk_no", false));
        if (Cools.isEmpty(wrkMastLogs)) {
            return latestInboundLogMap;
        }
        for (WrkMastLog wrkMastLog : wrkMastLogs) {
            if (wrkMastLog == null || Cools.isEmpty(wrkMastLog.getBarcode())) {
                continue;
            }
            if (!isInboundHistoryLog(wrkMastLog.getIoType())) {
                continue;
            }
            latestInboundLogMap.putIfAbsent(wrkMastLog.getBarcode(), wrkMastLog);
        }
        return latestInboundLogMap;
    }
    private boolean isInboundHistoryLog(Integer ioType) {
        if (ioType == null) {
            return false;
        }
        // 历史表里既有入库完成,也有库存调整;这里只取真正的入库类记录。
        return ioType < 19 || ioType == 53 || ioType == 54 || ioType == 57;
    }
    private Map<Integer, List<WrkDetlLog>> loadWrkDetlLogMap(Collection<WrkMastLog> wrkMastLogs) {
        Map<Integer, List<WrkDetlLog>> wrkDetlLogMap = new HashMap<>();
        if (wrkMastLogs == null || wrkMastLogs.isEmpty()) {
            return wrkDetlLogMap;
        }
        LinkedHashSet<Integer> wrkNos = new LinkedHashSet<>();
        for (WrkMastLog wrkMastLog : wrkMastLogs) {
            if (wrkMastLog != null && wrkMastLog.getWrkNo() != null) {
                wrkNos.add(wrkMastLog.getWrkNo());
            }
        }
        if (wrkNos.isEmpty()) {
            return wrkDetlLogMap;
        }
        List<WrkDetlLog> wrkDetlLogs = wrkDetlLogService.selectList(new EntityWrapper<WrkDetlLog>().in("wrk_no", wrkNos));
        if (Cools.isEmpty(wrkDetlLogs)) {
            return wrkDetlLogMap;
        }
        for (WrkDetlLog wrkDetlLog : wrkDetlLogs) {
            if (wrkDetlLog == null || wrkDetlLog.getWrkNo() == null) {
                continue;
            }
            wrkDetlLogMap.computeIfAbsent(wrkDetlLog.getWrkNo(), k -> new ArrayList<>()).add(wrkDetlLog);
        }
        return wrkDetlLogMap;
    }
    private Map<String, WaitPakinLog> loadWaitPakinLogMap(List<String> barcodes) {
        Map<String, WaitPakinLog> waitPakinLogMap = new LinkedHashMap<>();
        if (barcodes == null || barcodes.isEmpty()) {
            return waitPakinLogMap;
        }
        List<WaitPakinLog> waitPakinLogs = waitPakinLogService.selectList(new EntityWrapper<WaitPakinLog>()
                .in("zpallet", barcodes)
                .orderBy("zpallet", true)
                .orderBy("appe_time", false)
                .orderBy("modi_time", false));
        if (Cools.isEmpty(waitPakinLogs)) {
            return waitPakinLogMap;
        }
        for (WaitPakinLog waitPakinLog : waitPakinLogs) {
            if (waitPakinLog == null || Cools.isEmpty(waitPakinLog.getZpallet())) {
                continue;
            }
            waitPakinLogMap.putIfAbsent(waitPakinLog.getZpallet(), waitPakinLog);
        }
        return waitPakinLogMap;
    }
    private ErpPakinReportParam buildInboundErpParam(String barcode,
                                                     WrkMastLog wrkMastLog,
                                                     List<WrkDetlLog> wrkDetlLogs,
                                                     WaitPakinLog waitPakinLog) {
        ErpPakinReportParam param = new ErpPakinReportParam();
        param.setPalletId(resolvePalletId(barcode, wrkMastLog, wrkDetlLogs, waitPakinLog));
        param.setAnfme(resolveInboundAnfme(wrkDetlLogs, waitPakinLog));
        param.setLocId(transformInboundLocId(resolveInboundLocNo(wrkMastLog, waitPakinLog)));
        param.setWeight(resolveInboundWeight(wrkMastLog, waitPakinLog));
        param.setCreateTime(formatDate(resolveInboundCreateTime(wrkMastLog)));
        param.setBizNo(resolveInboundBizNo(wrkDetlLogs, waitPakinLog));
        param.setStartTime(formatDate(resolveInboundStartTime(wrkMastLog, waitPakinLog)));
        return param;
    }
    private String resolvePalletId(String barcode,
                                   WrkMastLog wrkMastLog,
                                   List<WrkDetlLog> wrkDetlLogs,
                                   WaitPakinLog waitPakinLog) {
        if (wrkMastLog != null && !Cools.isEmpty(wrkMastLog.getBarcode())) {
            return wrkMastLog.getBarcode();
        }
        if (wrkDetlLogs != null) {
            for (WrkDetlLog wrkDetlLog : wrkDetlLogs) {
                if (wrkDetlLog != null && !Cools.isEmpty(wrkDetlLog.getZpallet())) {
                    return wrkDetlLog.getZpallet();
                }
            }
        }
        if (waitPakinLog != null && !Cools.isEmpty(waitPakinLog.getZpallet())) {
            return waitPakinLog.getZpallet();
        }
        return barcode;
    }
    private Double resolveInboundAnfme(List<WrkDetlLog> wrkDetlLogs, WaitPakinLog waitPakinLog) {
        double total = 0D;
        boolean hasDetail = false;
        if (wrkDetlLogs != null) {
            for (WrkDetlLog wrkDetlLog : wrkDetlLogs) {
                if (wrkDetlLog == null || wrkDetlLog.getAnfme() == null) {
                    continue;
                }
                total += wrkDetlLog.getAnfme();
                hasDetail = true;
            }
        }
        if (hasDetail) {
            return total;
        }
        if (waitPakinLog != null && waitPakinLog.getAnfme() != null) {
            return waitPakinLog.getAnfme();
        }
        return total;
    }
    private BigDecimal resolveInboundWeight(WrkMastLog wrkMastLog, WaitPakinLog waitPakinLog) {
        BigDecimal baseWeight = BigDecimal.ZERO;
        if (wrkMastLog != null && wrkMastLog.getScWeight() != null) {
            baseWeight = BigDecimal.valueOf(wrkMastLog.getScWeight());
        } else if (waitPakinLog != null && waitPakinLog.getWeight() != null) {
            baseWeight = BigDecimal.valueOf(waitPakinLog.getWeight());
        }
        Integer sourceStaNo = resolveInboundSourceStaNo(wrkMastLog);
        return baseWeight.multiply(resolveInboundWeightFactor(sourceStaNo));
    }
    private Integer resolveInboundSourceStaNo(WrkMastLog wrkMastLog) {
        if (wrkMastLog == null) {
            return null;
        }
        if (wrkMastLog.getSourceStaNo() != null) {
            return wrkMastLog.getSourceStaNo();
        }
        return wrkMastLog.getStaNo();
    }
    private BigDecimal resolveInboundWeightFactor(Integer sourceStaNo) {
        if (sourceStaNo == null) {
            return BigDecimal.ONE;
        }
        BigDecimal factor = INBOUND_WEIGHT_FACTOR_BY_SOURCE_STA.get(sourceStaNo);
        return factor == null ? BigDecimal.ONE : factor;
    }
    private String resolveInboundLocNo(WrkMastLog wrkMastLog, WaitPakinLog waitPakinLog) {
        if (wrkMastLog != null && !Cools.isEmpty(wrkMastLog.getLocNo())) {
            return wrkMastLog.getLocNo();
        }
        if (waitPakinLog != null && !Cools.isEmpty(waitPakinLog.getLocNo())) {
            return waitPakinLog.getLocNo();
        }
        return null;
    }
    private Date resolveInboundCreateTime(WrkMastLog wrkMastLog) {
        if (wrkMastLog == null) {
            return new Date();
        }
        if (wrkMastLog.getCrnEndTime() != null) {
            return wrkMastLog.getCrnEndTime();
        }
        if (wrkMastLog.getModiTime() != null) {
            return wrkMastLog.getModiTime();
        }
        if (wrkMastLog.getIoTime() != null) {
            return wrkMastLog.getIoTime();
        }
        return new Date();
    }
    private Date resolveInboundStartTime(WrkMastLog wrkMastLog, WaitPakinLog waitPakinLog) {
        if (waitPakinLog != null && waitPakinLog.getAppeTime() != null) {
            return waitPakinLog.getAppeTime();
        }
        if (wrkMastLog == null) {
            return new Date();
        }
        if (wrkMastLog.getAppeTime() != null) {
            return wrkMastLog.getAppeTime();
        }
        if (wrkMastLog.getIoTime() != null) {
            return wrkMastLog.getIoTime();
        }
        if (wrkMastLog.getModiTime() != null) {
            return wrkMastLog.getModiTime();
        }
        return new Date();
    }
    private String resolveInboundBizNo(List<WrkDetlLog> wrkDetlLogs, WaitPakinLog waitPakinLog) {
        if (wrkDetlLogs != null) {
            for (WrkDetlLog wrkDetlLog : wrkDetlLogs) {
                if (wrkDetlLog != null && !Cools.isEmpty(wrkDetlLog.getThreeCode())) {
                    return wrkDetlLog.getThreeCode();
                }
            }
        }
        if (waitPakinLog != null && !Cools.isEmpty(waitPakinLog.getThreeCode())) {
            return waitPakinLog.getThreeCode();
        }
        return null;
    }
    private String transformInboundLocId(String locId) {
        if (Cools.isEmpty(locId)) {
            return null;
        }
        String trimmed = locId.trim();
        if (trimmed.length() < 7) {
            return trimmed;
        }
        String row = trimmed.substring(0, 2);
        String col = trimmed.substring(2, 5);
        String lev = trimmed.substring(5, 7);
        try {
            int rowNo = Integer.parseInt(row);
            if (rowNo >= 37) {
                row = "C" + row;
            } else if (rowNo >= 13) {
                row = "B" + row;
            } else {
                row = "A" + row;
            }
        } catch (Exception ignore) {
            return trimmed;
        }
        return row + "-" + col + "-" + lev;
    }
    private String formatDate(Date date) {
        if (date == null) {
            return null;
        }
        return new SimpleDateFormat(DATE_TIME_PATTERN).format(date);
    }
    private String buildErpInboundRequestUrl() {
        if (Cools.isEmpty(erpUrl)) {
            return erpInAddress;
        }
        if (erpInAddress == null) {
            return erpUrl;
        }
        if (erpUrl.endsWith("/") && erpInAddress.startsWith("/")) {
            return erpUrl + erpInAddress.substring(1);
        }
        if (!erpUrl.endsWith("/") && !erpInAddress.startsWith("/")) {
            return erpUrl + "/" + erpInAddress;
        }
        return erpUrl + erpInAddress;
    }
    private String buildErpOutErrorRequestUrl() {
        if (Cools.isEmpty(erpUrl)) {
            return erpOutErrorAddress;
src/main/java/com/zy/asrs/utils/Utils.java
@@ -29,6 +29,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Locale;
/**
@@ -131,98 +132,220 @@
    }
    public static Integer getStationStorageArea(Integer stationId) {
    public static List<Integer> getStationStorageAreas(Integer stationId) {
        if (stationId == null || stationId <= 0) {
            return null;
            return new ArrayList<>();
        }
        BasDevpService basDevpService = SpringUtils.getBean(BasDevpService.class);
        BasDevp station = basDevpService.selectById(stationId);
        if (station == null) {
            return new ArrayList<>();
        }
        return parseStorageAreas(station.getArea());
    }
    public static Integer getStationStorageArea(Integer stationId) {
        List<Integer> storageAreas = getStationStorageAreas(stationId);
        if (Cools.isEmpty(storageAreas)) {
            return null;
        }
        return parseStorageArea(station.getArea());
        return storageAreas.get(0);
    }
    /**
     * 生成入库找库位时的堆垛机优先顺序。
     *
     * <p>处理规则:
     * 1. 先根据入库站点查询所属库区。
     * 2. 先提取该库区内的堆垛机,并按可用空库位过滤不可用堆垛机。
     * 3. 当 {@code locType1 = 1} 时,先返回低库位堆垛机,再把同批堆垛机的高库位追加到后面。
     * 4. 对不存在、故障、不可入以及无空库位的堆垛机直接剔除。
     *
     * <p>这里是只读排序工具,不再写回 {@code asr_row_lastno.crn_qty}。
     * {@code crn_qty} 在主数据里表示“堆垛机数量”,不能再被拿来当轮询游标,否则会把整仓配置写坏。
     * <p>当前语义已经切换为“站点第一优先池 + 第二优先池”的配置预览,
     * 不再沿用旧的 area 顺序。
     *
     * <p>返回结果中的每一项格式为:
     * {@code {crnNo: 堆垛机号, locType1: 库位高低类型}}
     * {@code {pool: 优先池编号, seq: 池内顺序, crnNo: 堆垛机号}}
     *
     * @param stationId 入库站点
     * @param locType1  目标库位高低类型,1=低库位,2=高库位
     * @param matnr     物料编码,传入 {@code emptyPallet} 时使用空板排序规则
     * @return 按优先级排好序的堆垛机列表
     * @param locType1  保留兼容参数,当前不参与排序
     * @param matnr     保留兼容参数,当前不参与排序
     * @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) {
        BasDevpService basDevpService = SpringUtils.getBean(BasDevpService.class);
        BasDevp station = basDevpService.selectById(stationId);
        if (station == null) {
            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, emptyPallet, locMastService);
        appendCrnPoolEntries(result, 1, distinctCrnNos(station.getInFirstCrnCsv()));
        appendCrnPoolEntries(result, 2, distinctCrnNos(station.getInSecondCrnCsv()));
        return result;
    }
    private static void appendCrnLocTypeEntries(List<Map<String, Integer>> result, List<Integer> crnNos, Integer locType1, boolean emptyPallet, LocMastService locMastService) {
        Short normalizedLocType1 = normalizeLocType1(locType1);
        if (normalizedLocType1 == null) {
            appendCrnLocTypeEntries(result, crnNos, (short) 1, emptyPallet, locMastService);
            appendCrnLocTypeEntries(result, crnNos, (short) 2, emptyPallet, locMastService);
    public static List<Integer> parseCrnNos(String csv) {
        List<Integer> crnNos = new ArrayList<>();
        if (Cools.isEmpty(csv)) {
            return crnNos;
        }
        String normalized = csv.replace(",", ",")
                .replace(";", ",")
                .replace("、", ",")
                .replaceAll("\\s+", "");
        if (normalized.isEmpty()) {
            return crnNos;
        }
        for (String segment : normalized.split("[,;]")) {
            if (segment == null || segment.isEmpty()) {
                continue;
            }
            Integer crnNo = safeParseInt(segment);
            if (crnNo == null || crnNo <= 0) {
                throw new CoolException("堆垛机号格式错误:" + segment);
            }
            crnNos.add(crnNo);
        }
        return crnNos;
    }
    public static List<Integer> distinctCrnNos(String csv) {
        return distinctCrnNos(parseCrnNos(csv));
    }
    public static List<Integer> distinctCrnNos(List<Integer> crnNos) {
        List<Integer> result = new ArrayList<>();
        if (Cools.isEmpty(crnNos)) {
            return result;
        }
        LinkedHashSet<Integer> orderedCrnNos = new LinkedHashSet<>();
        for (Integer crnNo : crnNos) {
            if (crnNo == null || crnNo <= 0) {
                continue;
            }
            orderedCrnNos.add(crnNo);
        }
        result.addAll(orderedCrnNos);
        return result;
    }
    public static String normalizeCrnCsv(String csv) {
        return normalizeCrnCsv(parseCrnNos(csv));
    }
    public static String normalizeCrnCsv(List<Integer> crnNos) {
        return joinCrnNos(distinctCrnNos(crnNos));
    }
    public static String joinCrnNos(List<Integer> crnNos) {
        if (Cools.isEmpty(crnNos)) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        for (Integer crnNo : crnNos) {
            if (crnNo == null || crnNo <= 0) {
                continue;
            }
            if (builder.length() > 0) {
                builder.append(",");
            }
            builder.append(crnNo);
        }
        return builder.toString();
    }
    private static void appendCrnPoolEntries(List<Map<String, Integer>> result, int pool, List<Integer> crnNos) {
        if (result == null || Cools.isEmpty(crnNos)) {
            return;
        }
        appendCrnLocTypeEntries(result, crnNos, normalizedLocType1, emptyPallet, locMastService);
        if (normalizedLocType1 == 1) {
            appendCrnLocTypeEntries(result, crnNos, (short) 2, emptyPallet, locMastService);
        int seq = 1;
        for (Integer crnNo : crnNos) {
            if (crnNo == null || crnNo <= 0) {
                continue;
            }
            Map<String, Integer> item = new LinkedHashMap<>();
            item.put("pool", pool);
            item.put("seq", seq++);
            item.put("crnNo", crnNo);
            result.add(item);
        }
    }
    private static void appendCrnLocTypeEntries(List<Map<String, Integer>> result, List<Integer> crnNos, Short targetLocType1, boolean emptyPallet, LocMastService locMastService) {
    public static List<Integer> parseStorageAreas(String area) {
        List<Integer> areas = new ArrayList<>();
        if (Cools.isEmpty(area)) {
            return areas;
        }
        LinkedHashSet<Integer> orderedAreas = new LinkedHashSet<>();
        String normalized = area.replace(",", ",")
                .replace(";", ";")
                .replace("、", ",")
                .replaceAll("\\s+", "");
        if (normalized.isEmpty()) {
            return areas;
        }
        for (String segment : normalized.split("[,;]")) {
            if (segment == null || segment.isEmpty()) {
                continue;
            }
            Integer areaNo = parseStorageArea(segment);
            if (areaNo != null) {
                orderedAreas.add(areaNo);
            }
        }
        areas.addAll(orderedAreas);
        return areas;
    }
    public static String formatStorageArea(Integer area) {
        if (area == null) {
            return "";
        }
        switch (area) {
            case 1:
                return "A库区";
            case 2:
                return "B库区";
            case 3:
                return "C库区";
            default:
                return String.valueOf(area);
        }
    }
    private static void appendCrnLocTypeEntries(List<Map<String, Integer>> result, List<Integer> crnNos, Integer locType1,
                                                boolean emptyPallet, LocMastService locMastService, Integer area) {
        Short normalizedLocType1 = normalizeLocType1(locType1);
        if (normalizedLocType1 == null) {
            appendCrnLocTypeEntries(result, crnNos, (short) 1, emptyPallet, locMastService, area);
            appendCrnLocTypeEntries(result, crnNos, (short) 2, emptyPallet, locMastService, area);
            return;
        }
        appendCrnLocTypeEntries(result, crnNos, normalizedLocType1, emptyPallet, locMastService, area);
        if (normalizedLocType1 == 1) {
            appendCrnLocTypeEntries(result, crnNos, (short) 2, emptyPallet, locMastService, area);
        }
    }
    private static void appendCrnLocTypeEntries(List<Map<String, Integer>> result, List<Integer> crnNos, Short targetLocType1,
                                                boolean emptyPallet, LocMastService locMastService, Integer area) {
        if (targetLocType1 == null || Cools.isEmpty(crnNos)) {
            return;
        }
        for (Integer crnNo : crnNos) {
            if (!hasAvailableLoc(crnNo, targetLocType1, emptyPallet, locMastService) || containsCrnLocType(result, crnNo, targetLocType1)) {
            if (!hasAvailableLoc(crnNo, targetLocType1, emptyPallet, locMastService) || containsCrnLocType(result, area, crnNo, targetLocType1)) {
                continue;
            }
            Map<String, Integer> item = new LinkedHashMap<>();
            item.put("area", area);
            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) {
    private static boolean containsCrnLocType(List<Map<String, Integer>> result, Integer area, 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")) {
            if (crnNo.equals(item.get("crnNo"))
                    && locType1.intValue() == item.get("locType1")
                    && Objects.equals(area, item.get("area"))) {
                return true;
            }
        }
src/main/java/com/zy/common/entity/Parameter.java
@@ -81,4 +81,7 @@
    // run2 area C row config
    private String run2Area3Rows;
    // 高频货物优先放前几列,0 表示关闭
    private String highFreqFrontBayCount;
}
src/main/java/com/zy/common/service/CommonService.java
@@ -28,11 +28,14 @@
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
/**
@@ -185,6 +188,8 @@
            Integer whsType = Utils.GetWhsType(sourceStaNo);
            RowLastno rowLastno = rowLastnoService.selectById(whsType);
            RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId());
            List<Integer> stationAreas = Utils.getStationStorageAreas(sourceStaNo);
            findLocNoAttributeVo.setOutAreas(stationAreas);
            Integer preferredArea = resolvePreferredArea(sourceStaNo, findLocNoAttributeVo);
            if (preferredArea != null) {
                findLocNoAttributeVo.setOutArea(preferredArea);
@@ -250,7 +255,7 @@
        }
        StartupDto startupDto = new StartupDto();
        LocMast locMast = findRun2EmptyLocByCrnNos(searchRowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, "reassign-inbound");
                staDescId, sourceStaNo, startupDto, preferredArea, null, "reassign-inbound");
        if (Cools.isEmpty(locMast) || !"O".equals(locMast.getLocSts())) {
            return null;
        }
@@ -310,30 +315,37 @@
    /**
     * 空托盘固定按 4 段式找位:
     * 1. 严格高度 + narrow
     * 2. 严格高度 + any locType2
     * 3. 向上兼容高度 + narrow
     * 4. 向上兼容高度 + any locType2
     * 2. 向上兼容高度 + narrow
     * 3. 严格高度 + open
     * 4. 向上兼容高度 + open
     */
    private List<LocTypeDto> buildEmptyPalletSearchLocTypes(LocTypeDto locTypeDto) {
        LinkedHashSet<LocTypeDto> searchLocTypes = new LinkedHashSet<LocTypeDto>();
        LocTypeDto narrowStrictLocType = copyLocTypeDto(locTypeDto == null ? new LocTypeDto() : locTypeDto);
        if (narrowStrictLocType != null) {
            narrowStrictLocType.setLocType2((short) 1);
            searchLocTypes.add(narrowStrictLocType);
            LocTypeDto openStrictLocType = copyLocTypeDto(narrowStrictLocType);
            openStrictLocType.setLocType2(null);
            searchLocTypes.add(openStrictLocType);
            LocTypeDto narrowCompatibleLocType = buildUpwardCompatibleLocTypeDto(narrowStrictLocType);
            if (narrowCompatibleLocType != null) {
                narrowCompatibleLocType.setLocType2((short) 1);
                searchLocTypes.add(narrowCompatibleLocType);
                LocTypeDto openCompatibleLocType = copyLocTypeDto(narrowCompatibleLocType);
                openCompatibleLocType.setLocType2(null);
                searchLocTypes.add(openCompatibleLocType);
            }
        List<LocTypeDto> searchLocTypes = new ArrayList<LocTypeDto>();
        LocTypeDto baseLocTypeDto = copyLocTypeDto(locTypeDto == null ? new LocTypeDto() : locTypeDto);
        if (baseLocTypeDto == null) {
            return searchLocTypes;
        }
        return new ArrayList<LocTypeDto>(searchLocTypes);
        LocTypeDto narrowStrictLocType = copyLocTypeDto(baseLocTypeDto);
        narrowStrictLocType.setLocType2((short) 1);
        searchLocTypes.add(narrowStrictLocType);
        LocTypeDto narrowCompatibleLocType = buildUpwardCompatibleLocTypeDto(narrowStrictLocType);
        if (narrowCompatibleLocType != null) {
            narrowCompatibleLocType.setLocType2((short) 1);
            searchLocTypes.add(narrowCompatibleLocType);
        }
        LocTypeDto openStrictLocType = copyLocTypeDto(baseLocTypeDto);
        openStrictLocType.setLocType2((short) 0);
        searchLocTypes.add(openStrictLocType);
        LocTypeDto openCompatibleLocType = buildUpwardCompatibleLocTypeDto(openStrictLocType);
        if (openCompatibleLocType != null) {
            openCompatibleLocType.setLocType2((short) 0);
            searchLocTypes.add(openCompatibleLocType);
        }
        return searchLocTypes;
    }
    private String buildEmptyPalletStageCode(LocTypeDto baseLocTypeDto, LocTypeDto stageLocTypeDto) {
@@ -385,45 +397,19 @@
     * 解析本次找位应优先使用的库区,站点绑定优先于接口传参。
     */
    private Integer resolvePreferredArea(Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo) {
        BasDevp sourceStation = basDevpService.selectById(sourceStaNo);
        Integer stationArea = parseArea(sourceStation == null ? null : sourceStation.getArea());
        Integer stationArea = Utils.getStationStorageArea(sourceStaNo);
        if (stationArea != null) {
            return stationArea;
        }
        Integer requestArea = findLocNoAttributeVo.getOutArea();
        if (requestArea != null && requestArea >= 1 && requestArea <= 3) {
        if (isValidArea(requestArea)) {
            return requestArea;
        }
        return null;
    }
    /**
     * 把站点维护的库区值统一解析成 1/2/3。
     */
    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 boolean isValidArea(Integer area) {
        return area != null && area >= 1 && area <= 3;
    }
    /**
@@ -584,6 +570,12 @@
    private LocMast findAgvLocByRows(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> rows,
                                     int startBay, int endBay, int curRow, int nearRow,
                                     LocTypeDto locTypeDto, boolean useDeepCheck) {
        return findAgvLocByRows(rowLastno, rowLastnoType, rows, startBay, endBay, curRow, nearRow, locTypeDto, null, useDeepCheck);
    }
    private LocMast findAgvLocByRows(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> rows,
                                     int startBay, int endBay, int curRow, int nearRow,
                                     LocTypeDto locTypeDto, FindLocNoAttributeVo findLocNoAttributeVo, boolean useDeepCheck) {
        for (Integer row : rows) {
            if (row == null) {
                continue;
@@ -596,7 +588,8 @@
            applyLocTypeFilters(wrapper, locTypeDto, true);
            wrapper.orderBy("lev1", true).orderBy("bay1", true);
            List<LocMast> locMasts = locMastService.selectList(wrapper);
            for (LocMast candidate : locMasts) {
            List<LocMast> sortedLocMasts = sortLocCandidates(locMasts, findLocNoAttributeVo);
            for (LocMast candidate : sortedLocMasts) {
                if (!VersionUtils.locMoveCheckLocTypeComplete(candidate, locTypeDto)) {
                    continue;
                }
@@ -953,13 +946,236 @@
    }
    /**
     * 构造空托盘跨库区搜索顺序:
     * 先当前库区,再依次补足其它库区,避免重复。
     * 读取站点配置的优先池堆垛机号并做去重。
     */
    private List<Integer> buildAreaSearchOrder(Integer preferredArea) {
    private List<Integer> loadPriorityCrnNos(String csv) {
        return Utils.distinctCrnNos(csv);
    }
    /**
     * 从候选堆垛机池中移除已经出现在排除列表里的堆垛机,保持原始顺序不变。
     */
    private List<Integer> excludePriorityCrnNos(List<Integer> crnNos, List<Integer> excludedCrnNos) {
        List<Integer> result = new ArrayList<Integer>();
        if (Cools.isEmpty(crnNos)) {
            return result;
        }
        LinkedHashSet<Integer> excludedCrnNoSet = new LinkedHashSet<Integer>(Utils.distinctCrnNos(excludedCrnNos));
        for (Integer crnNo : Utils.distinctCrnNos(crnNos)) {
            if (crnNo == null || excludedCrnNoSet.contains(crnNo)) {
                continue;
            }
            result.add(crnNo);
        }
        return result;
    }
    /**
     * 从当前游标的下一台堆垛机开始轮转。
     */
    private List<Integer> rotatePriorityCrnNos(List<Integer> crnNos, Integer currentCrnNo) {
        List<Integer> orderedCrnNos = Utils.distinctCrnNos(crnNos);
        if (Cools.isEmpty(orderedCrnNos) || currentCrnNo == null) {
            return orderedCrnNos;
        }
        int currentIndex = orderedCrnNos.indexOf(currentCrnNo);
        if (currentIndex < 0) {
            return orderedCrnNos;
        }
        List<Integer> rotatedCrnNos = new ArrayList<>();
        for (int index = currentIndex + 1; index < orderedCrnNos.size(); index++) {
            rotatedCrnNos.add(orderedCrnNos.get(index));
        }
        for (int index = 0; index <= currentIndex; index++) {
            rotatedCrnNos.add(orderedCrnNos.get(index));
        }
        return rotatedCrnNos;
    }
    /**
     * 按第一优先池 -> 第二优先池的顺序查找可用库位。
     */
    private LocMast findRun2PriorityLocInPools(BasDevp station, RowLastno rowLastno, RowLastnoType rowLastnoType,
                                                Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo,
                                                LocTypeDto locTypeDto, StartupDto startupDto, boolean emptyPalletRequest) {
        if (station == null) {
            return null;
        }
        List<Integer> firstPoolCrnNos = loadPriorityCrnNos(station.getInFirstCrnCsv());
        List<Integer> secondPoolCrnNos = excludePriorityCrnNos(loadPriorityCrnNos(station.getInSecondCrnCsv()), firstPoolCrnNos);
        if (Cools.isEmpty(firstPoolCrnNos) && Cools.isEmpty(secondPoolCrnNos)) {
            throw new CoolException("站点=" + station.getDevNo() + " 未配置入库优先堆垛机");
        }
        if (emptyPalletRequest) {
            for (int poolNo = 1; poolNo <= 2; poolNo++) {
                List<Integer> poolCrnNos = poolNo == 1 ? firstPoolCrnNos : secondPoolCrnNos;
                Integer currentCrnNo = poolNo == 1 ? station.getInFirstCrnCurrentNo() : station.getInSecondCrnCurrentNo();
                for (LocTypeDto searchLocTypeDto : buildEmptyPalletSearchLocTypes(locTypeDto)) {
                    LocMast locMast = findRun2PriorityLocInPool(rowLastno, rowLastnoType, station, poolCrnNos, currentCrnNo,
                            poolNo, searchLocTypeDto, staDescId, sourceStaNo, findLocNoAttributeVo, startupDto, true);
                    if (locMast != null) {
                        return locMast;
                    }
                }
            }
            return null;
        }
        LocMast locMast = findRun2PriorityLocInPoolWithCompatibility(rowLastno, rowLastnoType, station, firstPoolCrnNos,
                station.getInFirstCrnCurrentNo(), 1, locTypeDto, staDescId, sourceStaNo, findLocNoAttributeVo, startupDto, false);
        if (locMast != null) {
            return locMast;
        }
        return findRun2PriorityLocInPoolWithCompatibility(rowLastno, rowLastnoType, station, secondPoolCrnNos,
                station.getInSecondCrnCurrentNo(), 2, locTypeDto, staDescId, sourceStaNo, findLocNoAttributeVo, startupDto, false);
    }
    /**
     * 在单个优先池内按轮转顺序找位。
     */
    private LocMast findRun2PriorityLocInPool(RowLastno rowLastno, RowLastnoType rowLastnoType, BasDevp station,
                                              List<Integer> crnNos, Integer currentCrnNo, int poolNo, LocTypeDto locTypeDto,
                                              Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo,
                                              StartupDto startupDto) {
        return findRun2PriorityLocInPool(rowLastno, rowLastnoType, station, crnNos, currentCrnNo, poolNo, locTypeDto,
                staDescId, sourceStaNo, findLocNoAttributeVo, startupDto, false);
    }
    private LocMast findRun2PriorityLocInPool(RowLastno rowLastno, RowLastnoType rowLastnoType, BasDevp station,
                                              List<Integer> crnNos, Integer currentCrnNo, int poolNo, LocTypeDto locTypeDto,
                                              Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo,
                                              StartupDto startupDto, boolean ignoreFreqType) {
        if (Cools.isEmpty(crnNos)) {
            return null;
        }
        List<Integer> rotatedCrnNos = rotatePriorityCrnNos(crnNos, currentCrnNo);
        if (Cools.isEmpty(rotatedCrnNos)) {
            return null;
        }
        for (Integer candidateCrnNo : rotatedCrnNos) {
            if (candidateCrnNo == null || !basCrnpService.checkSiteError(candidateCrnNo, true)) {
                continue;
            }
            Integer targetStaNo = resolveTargetStaNo(rowLastno, staDescId, sourceStaNo, candidateCrnNo);
            if (Utils.BooleanWhsTypeSta(rowLastno, staDescId) && targetStaNo == null) {
                continue;
            }
            Integer preferredNearRow = getCrnStartRow(rowLastno, candidateCrnNo);
            LocMast candidateLoc = findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, candidateCrnNo,
                    preferredNearRow, locTypeDto, findLocNoAttributeVo, ignoreFreqType);
            if (Cools.isEmpty(candidateLoc)) {
                continue;
            }
            if (!updatePriorityCursor(station, poolNo, currentCrnNo, candidateCrnNo)) {
                throw new CoolException("站点=" + station.getDevNo() + " 优先池轮转更新失败,请重试");
            }
            if (targetStaNo != null) {
                startupDto.setStaNo(targetStaNo);
            }
            return candidateLoc;
        }
        return null;
    }
    /**
     * 在单个优先池内先按当前规格找位,失败后再做一次 locType1 向上兼容。
     */
    private LocMast findRun2PriorityLocInPoolWithCompatibility(RowLastno rowLastno, RowLastnoType rowLastnoType,
                                                                BasDevp station, List<Integer> crnNos, Integer currentCrnNo,
                                                                int poolNo, LocTypeDto locTypeDto, Integer staDescId,
                                                                Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo,
                                                                StartupDto startupDto, boolean ignoreFreqType) {
        LocMast locMast = findRun2PriorityLocInPool(rowLastno, rowLastnoType, station, crnNos, currentCrnNo, poolNo,
                locTypeDto, staDescId, sourceStaNo, findLocNoAttributeVo, startupDto, ignoreFreqType);
        if (locMast != null) {
            return locMast;
        }
        LocTypeDto compatibleLocTypeDto = buildRetryCompatibleLocTypeDto(staDescId, findLocNoAttributeVo, locTypeDto);
        if (compatibleLocTypeDto == null) {
            return null;
        }
        return findRun2PriorityLocInPool(rowLastno, rowLastnoType, station, crnNos, currentCrnNo, poolNo,
                compatibleLocTypeDto, staDescId, sourceStaNo, findLocNoAttributeVo, startupDto, ignoreFreqType);
    }
    /**
     * 以乐观方式回写优先池游标。
     */
    private boolean updatePriorityCursor(BasDevp station, int poolNo, Integer expectedCurrentNo, Integer selectedCrnNo) {
        if (station == null || station.getDevNo() == null || selectedCrnNo == null) {
            return false;
        }
        BasDevp updateStation = new BasDevp();
        String cursorColumn;
        if (poolNo == 1) {
            updateStation.setInFirstCrnCurrentNo(selectedCrnNo);
            cursorColumn = "in_first_crn_current_no";
        } else {
            updateStation.setInSecondCrnCurrentNo(selectedCrnNo);
            cursorColumn = "in_second_crn_current_no";
        }
        EntityWrapper<BasDevp> wrapper = new EntityWrapper<>();
        wrapper.eq("dev_no", station.getDevNo());
        if (expectedCurrentNo == null) {
            wrapper.isNull(cursorColumn);
        } else {
            wrapper.eq(cursorColumn, expectedCurrentNo);
        }
        if (basDevpService.update(updateStation, wrapper)) {
            return true;
        }
        BasDevp latestStation = basDevpService.selectById(station.getDevNo());
        if (latestStation == null) {
            return false;
        }
        Integer latestCurrentNo = poolNo == 1 ? latestStation.getInFirstCrnCurrentNo() : latestStation.getInSecondCrnCurrentNo();
        return Objects.equals(latestCurrentNo, selectedCrnNo);
    }
    /**
     * 组装优先池预览数据。
     */
    private Map<String, Object> buildPriorityPoolPreview(RowLastno rowLastno, RowLastnoType rowLastnoType, int poolNo,
                                                          List<Integer> crnNos, Integer currentCrnNo, Integer staDescId,
                                                          Integer sourceStaNo, LocTypeDto locTypeDto,
                                                          FindLocNoAttributeVo findLocNoAttributeVo) {
        return buildPriorityPoolPreview(rowLastno, rowLastnoType, poolNo, crnNos, currentCrnNo, staDescId, sourceStaNo,
                locTypeDto, findLocNoAttributeVo, false);
    }
    private Map<String, Object> buildPriorityPoolPreview(RowLastno rowLastno, RowLastnoType rowLastnoType, int poolNo,
                                                          List<Integer> crnNos, Integer currentCrnNo, Integer staDescId,
                                                          Integer sourceStaNo, LocTypeDto locTypeDto,
                                                          FindLocNoAttributeVo findLocNoAttributeVo, boolean ignoreFreqType) {
        Map<String, Object> item = new HashMap<String, Object>();
        List<Integer> configuredCrnNos = Utils.distinctCrnNos(crnNos);
        List<Integer> rotatedCrnNos = rotatePriorityCrnNos(configuredCrnNos, currentCrnNo);
        item.put("poolNo", poolNo);
        item.put("currentCrnNo", currentCrnNo);
        item.put("configuredCrnNos", configuredCrnNos);
        item.put("rotatedCrnNos", rotatedCrnNos);
        item.put("runnableCrnNos", getOrderedRunnableRun2CrnNos(rowLastno, staDescId, sourceStaNo, rotatedCrnNos));
        item.put("profiles", buildRun2ProfilePreview(rowLastno, rowLastnoType, rotatedCrnNos, staDescId, sourceStaNo,
                locTypeDto, findLocNoAttributeVo, ignoreFreqType));
        return item;
    }
    /**
     * 构造空托盘跨库区搜索顺序:
     * 先站点绑定库区,再补请求库区,最后回退全仓库区,避免重复。
     */
    private List<Integer> buildAreaSearchOrder(List<Integer> preferredAreas, Integer requestArea) {
        LinkedHashSet<Integer> areaOrder = new LinkedHashSet<>();
        if (preferredArea != null && preferredArea >= 1 && preferredArea <= 3) {
            areaOrder.add(preferredArea);
        if (!Cools.isEmpty(preferredAreas)) {
            for (Integer area : preferredAreas) {
                if (isValidArea(area)) {
                    areaOrder.add(area);
                }
            }
        }
        if (isValidArea(requestArea)) {
            areaOrder.add(requestArea);
        }
        for (int area = 1; area <= 3; area++) {
            areaOrder.add(area);
@@ -968,7 +1184,7 @@
    }
    /**
     * 预览 run2 当前会参与的库区、堆垛机顺序和深浅排画像,不落任务档。
     * 预览 run2 当前会参与的优先池、堆垛机顺序和深浅排画像,不落任务档。
     */
    public Map<String, Object> previewRun2Allocation(BasCrnDepthRuleRuntimePreviewParam param) {
        if (param == null || param.getStaDescId() == null || param.getSourceStaNo() == null) {
@@ -977,6 +1193,7 @@
        FindLocNoAttributeVo findLocNoAttributeVo = new FindLocNoAttributeVo();
        findLocNoAttributeVo.setMatnr(param.getMatnr());
        findLocNoAttributeVo.setOutArea(param.getOutArea());
        findLocNoAttributeVo.setFreqType(param.getFreqType());
        LocTypeDto locTypeDto = new LocTypeDto();
        locTypeDto.setLocType1(param.getLocType1());
@@ -990,68 +1207,66 @@
            throw new CoolException("未找到仓库轮询规则");
        }
        RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId());
        Integer preferredArea = resolvePreferredArea(param.getSourceStaNo(), findLocNoAttributeVo);
        List<Integer> stationAreas = Utils.getStationStorageAreas(param.getSourceStaNo());
        boolean emptyPalletRequest = isEmptyPalletRequest(param.getStaDescId(), findLocNoAttributeVo);
        BasDevp station = basDevpService.selectById(param.getSourceStaNo());
        if (Cools.isEmpty(station)) {
            throw new CoolException("站点=" + param.getSourceStaNo() + " 未配置入库优先堆垛机");
        }
        List<Integer> firstPoolCrnNos = loadPriorityCrnNos(station.getInFirstCrnCsv());
        List<Integer> secondPoolCrnNos = excludePriorityCrnNos(loadPriorityCrnNos(station.getInSecondCrnCsv()), firstPoolCrnNos);
        Map<String, Object> result = new HashMap<String, Object>();
        result.put("whsType", whsType);
        result.put("preferredArea", preferredArea);
        result.put("preferredArea", findLocNoAttributeVo.getOutArea());
        result.put("preferredAreas", stationAreas);
        result.put("emptyPallet", emptyPalletRequest);
        result.put("locType", locTypeDto);
        result.put("stationPriorityEntries", Utils.getStationStorageAreaName(
                param.getSourceStaNo(),
                locTypeDto == null || locTypeDto.getLocType1() == null ? null : locTypeDto.getLocType1().intValue(),
                findLocNoAttributeVo.getMatnr()));
        result.put("firstPriorityCrnNos", firstPoolCrnNos);
        result.put("secondPriorityCrnNos", secondPoolCrnNos);
        result.put("firstPriorityCurrentNo", station.getInFirstCrnCurrentNo());
        result.put("secondPriorityCurrentNo", station.getInSecondCrnCurrentNo());
        result.put("firstPriorityRotatedCrnNos", rotatePriorityCrnNos(firstPoolCrnNos, station.getInFirstCrnCurrentNo()));
        result.put("secondPriorityRotatedCrnNos", rotatePriorityCrnNos(secondPoolCrnNos, station.getInSecondCrnCurrentNo()));
        List<Integer> orderedCrnNos = getOrderedCrnNos(rowLastno, resolveRun2CrnNo(rowLastno));
        List<Integer> runnableCrnNos = getOrderedRunnableRun2CrnNos(rowLastno, param.getStaDescId(), param.getSourceStaNo(), orderedCrnNos);
        result.put("orderedCrnNos", orderedCrnNos);
        result.put("runnableCrnNos", runnableCrnNos);
        List<Map<String, Object>> poolPreviews = new ArrayList<Map<String, Object>>();
        if (emptyPalletRequest) {
            List<Integer> areaSearchOrder = buildAreaSearchOrder(preferredArea);
            List<Map<String, Object>> searchStages = new ArrayList<Map<String, Object>>();
            for (LocTypeDto stageLocTypeDto : buildEmptyPalletSearchLocTypes(locTypeDto)) {
                List<Map<String, Object>> areaPreviews = new ArrayList<Map<String, Object>>();
                for (Integer area : areaSearchOrder) {
                    RowLastno areaRowLastno = getAreaRowLastno(area, rowLastno);
                    RowLastnoType areaRowLastnoType = rowLastnoTypeService.selectById(areaRowLastno.getTypeId());
                    List<Integer> areaOrderedCrnNos = getOrderedCrnNos(areaRowLastno, resolveRun2CrnNo(areaRowLastno));
                    List<Integer> areaRunnableCrnNos = getOrderedRunnableRun2CrnNos(areaRowLastno, param.getStaDescId(),
                            param.getSourceStaNo(), areaOrderedCrnNos, false);
                    Map<String, Object> areaItem = new HashMap<String, Object>();
                    areaItem.put("area", area);
                    areaItem.put("orderedCrnNos", areaOrderedCrnNos);
                    areaItem.put("runnableCrnNos", areaRunnableCrnNos);
                    areaItem.put("profiles", buildRun2ProfilePreview(areaRowLastno, areaRowLastnoType, areaOrderedCrnNos,
                            param.getStaDescId(), param.getSourceStaNo(), stageLocTypeDto));
                    areaPreviews.add(areaItem);
            for (int poolNo = 1; poolNo <= 2; poolNo++) {
                List<Integer> poolCrnNos = poolNo == 1 ? firstPoolCrnNos : secondPoolCrnNos;
                Integer currentCrnNo = poolNo == 1 ? station.getInFirstCrnCurrentNo() : station.getInSecondCrnCurrentNo();
                Map<String, Object> poolPreview = buildPriorityPoolPreview(rowLastno, rowLastnoType, poolNo, poolCrnNos,
                        currentCrnNo, param.getStaDescId(), param.getSourceStaNo(), locTypeDto, findLocNoAttributeVo, true);
                List<Map<String, Object>> stagePreviews = new ArrayList<Map<String, Object>>();
                for (LocTypeDto stageLocTypeDto : buildEmptyPalletSearchLocTypes(locTypeDto)) {
                    Map<String, Object> stagePreview = buildPriorityPoolPreview(rowLastno, rowLastnoType, poolNo, poolCrnNos,
                            currentCrnNo, param.getStaDescId(), param.getSourceStaNo(), stageLocTypeDto, findLocNoAttributeVo, true);
                    stagePreview.put("stageCode", buildEmptyPalletStageCode(locTypeDto, stageLocTypeDto));
                    stagePreview.put("locType", stageLocTypeDto);
                    stagePreviews.add(stagePreview);
                }
                Map<String, Object> stageItem = new HashMap<String, Object>();
                stageItem.put("stageCode", buildEmptyPalletStageCode(locTypeDto, stageLocTypeDto));
                stageItem.put("locType", stageLocTypeDto);
                stageItem.put("areaSearchOrder", areaSearchOrder);
                stageItem.put("areaPreviews", areaPreviews);
                searchStages.add(stageItem);
                poolPreview.put("stagePreviews", stagePreviews);
                poolPreviews.add(poolPreview);
            }
            result.put("areaSearchOrder", areaSearchOrder);
            result.put("searchStages", searchStages);
            result.put("poolPreviews", poolPreviews);
            return result;
        }
        if (preferredArea != null) {
            List<Integer> preferredCrnNos = filterCrnNosByRows(rowLastno, orderedCrnNos, getRun2AreaRows(preferredArea, rowLastno));
            result.put("candidateCrnNos", preferredCrnNos);
            result.put("profiles", buildRun2ProfilePreview(rowLastno, rowLastnoType, preferredCrnNos,
                    param.getStaDescId(), param.getSourceStaNo(), locTypeDto));
            result.put("areaMode", "preferred-area-only");
            return result;
        }
        result.put("candidateCrnNos", orderedCrnNos);
        result.put("profiles", buildRun2ProfilePreview(rowLastno, rowLastnoType, orderedCrnNos,
                param.getStaDescId(), param.getSourceStaNo(), locTypeDto));
        result.put("areaMode", "warehouse-round-robin");
        poolPreviews.add(buildPriorityPoolPreview(rowLastno, rowLastnoType, 1, firstPoolCrnNos,
                station.getInFirstCrnCurrentNo(), param.getStaDescId(), param.getSourceStaNo(), locTypeDto,
                findLocNoAttributeVo));
        poolPreviews.add(buildPriorityPoolPreview(rowLastno, rowLastnoType, 2, secondPoolCrnNos,
                station.getInSecondCrnCurrentNo(), param.getStaDescId(), param.getSourceStaNo(), locTypeDto,
                findLocNoAttributeVo));
        result.put("poolPreviews", poolPreviews);
        return result;
    }
@@ -1059,7 +1274,14 @@
     * 组装某批堆垛机的运行时画像预览数据。
     */
    private List<Map<String, Object>> buildRun2ProfilePreview(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> crnNos,
                                                              Integer staDescId, Integer sourceStaNo, LocTypeDto locTypeDto) {
                                                              Integer staDescId, Integer sourceStaNo, LocTypeDto locTypeDto,
                                                              FindLocNoAttributeVo findLocNoAttributeVo) {
        return buildRun2ProfilePreview(rowLastno, rowLastnoType, crnNos, staDescId, sourceStaNo, locTypeDto, findLocNoAttributeVo, false);
    }
    private List<Map<String, Object>> buildRun2ProfilePreview(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> crnNos,
                                                              Integer staDescId, Integer sourceStaNo, LocTypeDto locTypeDto,
                                                              FindLocNoAttributeVo findLocNoAttributeVo, boolean ignoreFreqType) {
        List<Map<String, Object>> profiles = new ArrayList<Map<String, Object>>();
        if (Cools.isEmpty(crnNos)) {
            return profiles;
@@ -1076,7 +1298,7 @@
            item.put("shallowRows", profile == null ? null : profile.getShallowRows());
            item.put("deepRows", profile == null ? null : profile.getDeepRows());
            LocMast firstMatchLoc = findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo,
                    getCrnStartRow(rowLastno, crnNo), locTypeDto);
                    getCrnStartRow(rowLastno, crnNo), locTypeDto, findLocNoAttributeVo, ignoreFreqType);
            item.put("firstMatchLocNo", firstMatchLoc == null ? null : firstMatchLoc.getLocNo());
            item.put("assignableLocCount", countAssignableLocForCrn(rowLastno, rowLastnoType, crnNo,
                    getCrnStartRow(rowLastno, crnNo) == null ? 0 : getCrnStartRow(rowLastno, crnNo), locTypeDto));
@@ -1112,11 +1334,12 @@
     * 因为空托盘的业务口径已经切换成“按库区找堆垛机”,不是按推荐排找巷道。
     */
    private Run2AreaSearchResult findEmptyPalletRun2Loc(RowLastno defaultRowLastno, Integer staDescId, Integer sourceStaNo,
                                                        StartupDto startupDto, Integer preferredArea, LocTypeDto locTypeDto) {
                                                        StartupDto startupDto, FindLocNoAttributeVo findLocNoAttributeVo,
                                                        LocTypeDto locTypeDto) {
        for (LocTypeDto stageLocTypeDto : buildEmptyPalletSearchLocTypes(locTypeDto)) {
            String stageCode = buildEmptyPalletStageCode(locTypeDto, stageLocTypeDto);
            Run2AreaSearchResult searchResult = findEmptyPalletRun2AreaLoc(defaultRowLastno, staDescId, sourceStaNo,
                    startupDto, preferredArea, stageLocTypeDto, stageCode);
                    startupDto, findLocNoAttributeVo, stageLocTypeDto, stageCode);
            if (!Cools.isEmpty(searchResult) && !Cools.isEmpty(searchResult.locMast)) {
                return searchResult;
            }
@@ -1125,9 +1348,12 @@
    }
    private Run2AreaSearchResult findEmptyPalletRun2AreaLoc(RowLastno defaultRowLastno, Integer staDescId, Integer sourceStaNo,
                                                            StartupDto startupDto, Integer preferredArea, LocTypeDto locTypeDto,
                                                            StartupDto startupDto, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto,
                                                            String stageCode) {
        for (Integer area : buildAreaSearchOrder(preferredArea)) {
        List<Integer> areaSearchOrder = buildAreaSearchOrder(
                findLocNoAttributeVo == null ? null : findLocNoAttributeVo.getOutAreas(),
                findLocNoAttributeVo == null ? null : findLocNoAttributeVo.getOutArea());
        for (Integer area : areaSearchOrder) {
            RowLastno areaRowLastno = getAreaRowLastno(area, defaultRowLastno);
            if (Cools.isEmpty(areaRowLastno)) {
                continue;
@@ -1144,7 +1370,7 @@
                continue;
            }
            LocMast locMast = findRun2EmptyLocByCrnNos(areaRowLastno, areaRowLastnoType, runnableAreaCrnNos, locTypeDto,
                    staDescId, sourceStaNo, startupDto, area, stageCode + "-area-" + area, false);
                    staDescId, sourceStaNo, startupDto, area, findLocNoAttributeVo, stageCode + "-area-" + area, false);
            if (!Cools.isEmpty(locMast)) {
                return new Run2AreaSearchResult(locMast, areaRowLastno, runnableAreaCrnNos);
            }
@@ -1184,7 +1410,7 @@
            // 站点优先级只是“优先尝试”,没有命中时必须继续走默认/库区回退,
            // 否则会把“优先候选无位”误判成“整仓无位”。
            LocMast locMast = findRun2EmptyLocByCrnLocTypeEntries(rowLastno, rowLastnoType, stationCrnLocTypes,
                    locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "station-priority");
                    locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "station-priority", findLocNoAttributeVo);
            if (!Cools.isEmpty(locMast)) {
                return new Run2SearchResult(locMast, rowLastno,
                        getOrderedRunnableRun2CrnNos(rowLastno, staDescId, sourceStaNo, extractCrnNos(stationCrnLocTypes)));
@@ -1198,7 +1424,8 @@
        }
        List<Integer> runnableCrnNos = getOrderedRunnableRun2CrnNos(rowLastno, staDescId, sourceStaNo, candidateCrnNos);
        LocMast locMast = findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, preferredArea == null ? "default" : "preferred-area");
                staDescId, sourceStaNo, startupDto, preferredArea, findLocNoAttributeVo,
                preferredArea == null ? "default" : "preferred-area");
        return new Run2SearchResult(locMast, rowLastno, runnableCrnNos);
    }
@@ -1296,12 +1523,19 @@
                                             LocTypeDto locTypeDto, Integer staDescId, Integer sourceStaNo, StartupDto startupDto,
                                             Integer preferredArea, String stage) {
        return findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, stage, true);
                staDescId, sourceStaNo, startupDto, preferredArea, null, stage, true);
    }
    private LocMast findRun2EmptyLocByCrnNos(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> candidateCrnNos,
                                             LocTypeDto locTypeDto, Integer staDescId, Integer sourceStaNo, StartupDto startupDto,
                                             Integer preferredArea, String stage, boolean routeRequired) {
                                             Integer preferredArea, FindLocNoAttributeVo findLocNoAttributeVo, String stage) {
        return findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, findLocNoAttributeVo, stage, true);
    }
    private LocMast findRun2EmptyLocByCrnNos(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> candidateCrnNos,
                                             LocTypeDto locTypeDto, Integer staDescId, Integer sourceStaNo, StartupDto startupDto,
                                             Integer preferredArea, FindLocNoAttributeVo findLocNoAttributeVo, String stage, boolean routeRequired) {
        if (Cools.isEmpty(candidateCrnNos)) {
            log.warn("run2 skip empty candidate list. stage={}, sourceStaNo={}, preferredArea={}, spec={}",
                    stage, sourceStaNo, preferredArea, JSON.toJSONString(locTypeDto));
@@ -1312,14 +1546,15 @@
        List<Integer> noEmptyCrns = new ArrayList<>();
        List<Integer> locTypeBlockedCrns = new ArrayList<>();
        return findRun2EmptyLocByCrnNosRecursively(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, stage, routeRequired, 0,
                staDescId, sourceStaNo, startupDto, preferredArea, findLocNoAttributeVo, stage, routeRequired, 0,
                crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
    }
    private LocMast findRun2EmptyLocByCrnNosRecursively(RowLastno rowLastno, RowLastnoType rowLastnoType,
                                                        List<Integer> candidateCrnNos, LocTypeDto locTypeDto,
                                                        Integer staDescId, Integer sourceStaNo, StartupDto startupDto,
                                                        Integer preferredArea, String stage, boolean routeRequired, int index,
                                                        Integer preferredArea, FindLocNoAttributeVo findLocNoAttributeVo,
                                                        String stage, boolean routeRequired, int index,
                                                        List<Integer> crnErrorCrns, List<Integer> routeBlockedCrns,
                                                        List<Integer> noEmptyCrns, List<Integer> locTypeBlockedCrns) {
        if (index >= candidateCrnNos.size()) {
@@ -1331,19 +1566,19 @@
        if (!isCrnActive(candidateCrnNo)) {
            crnErrorCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnNosRecursively(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, routeRequired, index + 1,
                    staDescId, sourceStaNo, startupDto, preferredArea, findLocNoAttributeVo, stage, routeRequired, index + 1,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        Integer targetStaNo = routeRequired ? resolveTargetStaNo(rowLastno, staDescId, sourceStaNo, candidateCrnNo) : null;
        if (routeRequired && Utils.BooleanWhsTypeSta(rowLastno, staDescId) && targetStaNo == null) {
            routeBlockedCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnNosRecursively(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, routeRequired, index + 1,
                    staDescId, sourceStaNo, startupDto, preferredArea, findLocNoAttributeVo, stage, routeRequired, index + 1,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        Integer preferredNearRow = getCrnStartRow(rowLastno, candidateCrnNo);
        LocMast candidateLoc = findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, candidateCrnNo,
                preferredNearRow, locTypeDto);
                preferredNearRow, locTypeDto, findLocNoAttributeVo);
        if (Cools.isEmpty(candidateLoc)) {
            int availableLocCount = countAssignableLocForCrn(rowLastno, rowLastnoType, candidateCrnNo,
                    preferredNearRow == null ? 0 : preferredNearRow, locTypeDto);
@@ -1353,7 +1588,7 @@
                locTypeBlockedCrns.add(candidateCrnNo);
            }
            return findRun2EmptyLocByCrnNosRecursively(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, routeRequired, index + 1,
                    staDescId, sourceStaNo, startupDto, preferredArea, findLocNoAttributeVo, stage, routeRequired, index + 1,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        if (targetStaNo != null) {
@@ -1368,7 +1603,7 @@
    private LocMast findRun2EmptyLocByCrnLocTypeEntries(RowLastno rowLastno, RowLastnoType rowLastnoType,
                                                        List<Map<String, Integer>> crnLocTypeEntries, LocTypeDto locTypeDto,
                                                        Integer staDescId, Integer sourceStaNo, StartupDto startupDto,
                                                        Integer preferredArea, String stage) {
                                                        Integer preferredArea, String stage, FindLocNoAttributeVo findLocNoAttributeVo) {
        if (Cools.isEmpty(crnLocTypeEntries)) {
            log.warn("run2 skip empty crn-locType list. stage={}, sourceStaNo={}, preferredArea={}, spec={}",
                    stage, sourceStaNo, preferredArea, JSON.toJSONString(locTypeDto));
@@ -1380,14 +1615,14 @@
        List<Integer> noEmptyCrns = new ArrayList<>();
        List<Integer> locTypeBlockedCrns = new ArrayList<>();
        return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, stage, 0, candidateCrnNos,
                staDescId, sourceStaNo, startupDto, preferredArea, stage, findLocNoAttributeVo, 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,
                                                                   Integer preferredArea, String stage, FindLocNoAttributeVo findLocNoAttributeVo, int index,
                                                                   List<Integer> candidateCrnNos, List<Integer> crnErrorCrns,
                                                                   List<Integer> routeBlockedCrns, List<Integer> noEmptyCrns,
                                                                   List<Integer> locTypeBlockedCrns) {
@@ -1402,20 +1637,20 @@
        if (!isCrnActive(candidateCrnNo)) {
            crnErrorCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, findLocNoAttributeVo, 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,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, findLocNoAttributeVo, index + 1, candidateCrnNos,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        LocTypeDto searchLocTypeDto = buildRun2SearchLocTypeDto(locTypeDto, candidateLocType1);
        Integer preferredNearRow = getCrnStartRow(rowLastno, candidateCrnNo);
        LocMast candidateLoc = findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, candidateCrnNo,
                preferredNearRow, searchLocTypeDto);
                preferredNearRow, searchLocTypeDto, findLocNoAttributeVo);
        if (Cools.isEmpty(candidateLoc)) {
            int availableLocCount = countAssignableLocForCrn(rowLastno, rowLastnoType, candidateCrnNo,
                    preferredNearRow == null ? 0 : preferredNearRow, searchLocTypeDto);
@@ -1425,7 +1660,7 @@
                locTypeBlockedCrns.add(candidateCrnNo);
            }
            return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, findLocNoAttributeVo, index + 1, candidateCrnNos,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        if (targetStaNo != null) {
@@ -1476,6 +1711,12 @@
     */
    private LocMast findRun2OrderedEmptyLocByCrnLocType(RowLastnoType rowLastnoType, Integer candidateCrnNo,
                                                        Short candidateLocType1, LocTypeDto locTypeDto) {
        return findRun2OrderedEmptyLocByCrnLocType(rowLastnoType, candidateCrnNo, candidateLocType1, locTypeDto, null);
    }
    private LocMast findRun2OrderedEmptyLocByCrnLocType(RowLastnoType rowLastnoType, Integer candidateCrnNo,
                                                        Short candidateLocType1, LocTypeDto locTypeDto,
                                                        FindLocNoAttributeVo findLocNoAttributeVo) {
        if (candidateCrnNo == null) {
            return null;
        }
@@ -1486,20 +1727,18 @@
            wrapper.eq("loc_type1", candidateLocType1);
        }
        applyLocTypeFilters(wrapper, locTypeDto, false);
        // 单伸堆垛机按层、列递增顺序找第一个空库位。
        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);
        List<LocMast> locMasts = locMastService.selectList(wrapper);
        List<LocMast> sortedLocMasts = sortLocCandidates(locMasts, findLocNoAttributeVo);
        for (LocMast candidateLoc : sortedLocMasts) {
            if (candidateLoc == null) {
                continue;
            }
            if (locTypeDto != null && !VersionUtils.locMoveCheckLocTypeComplete(candidateLoc, locTypeDto)) {
                continue;
            }
            return candidateLoc;
        }
        LocMast candidateLoc = locMastService.selectOne(wrapper);
        if (Cools.isEmpty(candidateLoc)) {
            return null;
        }
        if (locTypeDto != null && !VersionUtils.locMoveCheckLocTypeComplete(candidateLoc, locTypeDto)) {
            return null;
        }
        return candidateLoc;
        return null;
    }
    private Optional<CrnRowInfo> findAvailableCrnAndNearRow(RowLastno rowLastno, int curRow, int crnNumber, int times,
@@ -1636,33 +1875,115 @@
    }
    /**
     * 查询某一排上的所有空库位,并按单伸/双伸策略排序。
     * 查询某一排上的所有空库位,并按单伸/双伸策略与频次排序。
     */
    private List<LocMast> findOpenLocsByRow(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer row,
                                            Integer crnNo, LocTypeDto locTypeDto, boolean singleExtension) {
        return findOpenLocsByRow(rowLastno, rowLastnoType, row, crnNo, locTypeDto, null, singleExtension, false);
    }
    private List<LocMast> findOpenLocsByRow(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer row,
                                            Integer crnNo, LocTypeDto locTypeDto, FindLocNoAttributeVo findLocNoAttributeVo,
                                            boolean singleExtension) {
        return findOpenLocsByRow(rowLastno, rowLastnoType, row, crnNo, locTypeDto, findLocNoAttributeVo, singleExtension, false);
    }
    private List<LocMast> findOpenLocsByRow(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer row,
                                            Integer crnNo, LocTypeDto locTypeDto, FindLocNoAttributeVo findLocNoAttributeVo,
                                            boolean singleExtension, boolean ignoreFreqType) {
        List<LocMast> result = new ArrayList<LocMast>();
        if (row == null) {
            return result;
        }
        Wrapper<LocMast> wrapper = new EntityWrapper<LocMast>()
//                .eq("row1", row)
                .eq("loc_sts", "O");
        if (crnNo != null) {
            wrapper.eq("crn_no", crnNo);
        }
        applyLocTypeFilters(wrapper, locTypeDto, true);
        if (singleExtension) {
            wrapper.orderBy("lev1", true).orderBy("bay1", true);
        } else {
            wrapper.orderBy("lev1", true).orderBy("bay1", true);
        }
        List<LocMast> locMasts = locMastService.selectList(wrapper);
        for (LocMast locMast : locMasts) {
        List<LocMast> sortedLocMasts = sortLocCandidates(locMasts, findLocNoAttributeVo, ignoreFreqType);
        for (LocMast locMast : sortedLocMasts) {
            if (matchesLocType(locMast, locTypeDto)) {
                result.add(locMast);
            }
        }
        return result;
    }
    private List<LocMast> sortLocCandidates(List<LocMast> locMasts, FindLocNoAttributeVo findLocNoAttributeVo) {
        return sortLocCandidates(locMasts, findLocNoAttributeVo, false);
    }
    private List<LocMast> sortLocCandidates(List<LocMast> locMasts, FindLocNoAttributeVo findLocNoAttributeVo, boolean ignoreFreqType) {
        Integer freqType = ignoreFreqType ? null : (findLocNoAttributeVo == null ? null : findLocNoAttributeVo.getFreqType());
        return sortLocCandidates(locMasts, freqType, getHighFreqFrontBayCount());
    }
    private List<LocMast> sortLocCandidates(List<LocMast> locMasts, Integer freqType, Integer frontBayCount) {
        List<LocMast> result = new ArrayList<LocMast>();
        if (Cools.isEmpty(locMasts)) {
            return result;
        }
        result.addAll(locMasts);
        Integer normalizedFreqType = normalizeFreqType(freqType);
        if (normalizedFreqType == null) {
            result.sort(Comparator
                    .comparing(LocMast::getLev1, Comparator.nullsLast(Integer::compareTo))
                    .thenComparing(LocMast::getBay1, Comparator.nullsLast(Integer::compareTo))
                    .thenComparing(LocMast::getLocNo, Comparator.nullsLast(String::compareTo)));
            return result;
        }
        if (Objects.equals(normalizedFreqType, 2)) {
            result.sort(Comparator
                    .comparing(LocMast::getLev1, Comparator.nullsLast(Integer::compareTo))
                    .thenComparing(LocMast::getBay1, Comparator.nullsLast(Comparator.reverseOrder()))
                    .thenComparing(LocMast::getLocNo, Comparator.nullsLast(String::compareTo)));
            return result;
        }
        int normalizedFrontBayCount = frontBayCount == null ? 0 : frontBayCount;
        if (normalizedFrontBayCount > 0) {
            result.sort(Comparator
                    .comparingInt((LocMast loc) -> resolveFrontBayGroup(loc, normalizedFrontBayCount))
                    .thenComparing(LocMast::getBay1, Comparator.nullsLast(Integer::compareTo))
                    .thenComparing(LocMast::getLev1, Comparator.nullsLast(Integer::compareTo))
                    .thenComparing(LocMast::getLocNo, Comparator.nullsLast(String::compareTo)));
        } else {
            result.sort(Comparator
                    .comparing(LocMast::getBay1, Comparator.nullsLast(Integer::compareTo))
                    .thenComparing(LocMast::getLev1, Comparator.nullsLast(Integer::compareTo))
                    .thenComparing(LocMast::getLocNo, Comparator.nullsLast(String::compareTo)));
        }
        return result;
    }
    private int resolveFrontBayGroup(LocMast locMast, int frontBayCount) {
        if (locMast == null || locMast.getBay1() == null || locMast.getBay1() <= 0) {
            return 1;
        }
        return locMast.getBay1() <= frontBayCount ? 0 : 1;
    }
    private Integer normalizeFreqType(Integer freqType) {
        if (freqType == null || (freqType != 1 && freqType != 2)) {
            return null;
        }
        return freqType;
    }
    private int getHighFreqFrontBayCount() {
        Parameter parameter = Parameter.get();
        if (parameter == null || Cools.isEmpty(parameter.getHighFreqFrontBayCount())) {
            return 0;
        }
        Integer parsedCount = safeParseInt(parameter.getHighFreqFrontBayCount());
        if (parsedCount == null || parsedCount <= 0) {
            return 0;
        }
        return parsedCount;
    }
    /**
@@ -1680,10 +2001,6 @@
        if (crnNo != null) {
            wrapper.eq("crn_no", crnNo);
        }
        Long whsType = resolveLocWhsType(rowLastno, rowLastnoType);
        if (whsType != null) {
            wrapper.eq("whs_type", whsType);
        }
        if (statuses != null && statuses.length > 0) {
            if (statuses.length == 1) {
                wrapper.eq("loc_sts", statuses[0]);
@@ -1699,7 +2016,13 @@
     */
    private LocMast findPairAssignableLoc(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer crnNo,
                                          Integer shallowRow, Integer deepRow, LocTypeDto locTypeDto) {
        List<LocMast> shallowOpenLocs = findOpenLocsByRow(rowLastno, rowLastnoType, shallowRow, crnNo, locTypeDto, false);
        return findPairAssignableLoc(rowLastno, rowLastnoType, crnNo, shallowRow, deepRow, locTypeDto, null);
    }
    private LocMast findPairAssignableLoc(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer crnNo,
                                          Integer shallowRow, Integer deepRow, LocTypeDto locTypeDto,
                                          FindLocNoAttributeVo findLocNoAttributeVo) {
        List<LocMast> shallowOpenLocs = findOpenLocsByRow(rowLastno, rowLastnoType, shallowRow, crnNo, locTypeDto, findLocNoAttributeVo, false);
        if (Cools.isEmpty(shallowOpenLocs)) {
            return null;
        }
@@ -1728,7 +2051,18 @@
     * 按某台堆垛机的深浅排画像搜索第一个可分配空库位。
     */
    private LocMast findConfiguredEmptyLocForCrn(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer crnNo,
                                                 Integer preferredNearRow, LocTypeDto locTypeDto) {
                                                  Integer preferredNearRow, LocTypeDto locTypeDto) {
        return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, preferredNearRow, locTypeDto, null, false);
    }
    private LocMast findConfiguredEmptyLocForCrn(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer crnNo,
                                                  Integer preferredNearRow, LocTypeDto locTypeDto, FindLocNoAttributeVo findLocNoAttributeVo) {
        return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, preferredNearRow, locTypeDto, findLocNoAttributeVo, false);
    }
    private LocMast findConfiguredEmptyLocForCrn(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer crnNo,
                                                 Integer preferredNearRow, LocTypeDto locTypeDto, FindLocNoAttributeVo findLocNoAttributeVo,
                                                 boolean ignoreFreqType) {
        if (rowLastno == null || crnNo == null) {
            return null;
        }
@@ -1748,7 +2082,7 @@
                        continue;
                    }
                    LocMast candidateLoc = findPairAssignableLoc(rowLastno, rowLastnoType, crnNo, searchRow,
                            profile.getPairedDeepRow(searchRow), locTypeDto);
                            profile.getPairedDeepRow(searchRow), locTypeDto, findLocNoAttributeVo);
                    if (!Cools.isEmpty(candidateLoc)) {
                        return candidateLoc;
                    }
@@ -1761,7 +2095,7 @@
                            continue;
                        }
                        LocMast candidateLoc = findPairAssignableLoc(rowLastno, rowLastnoType, crnNo, shallowRow,
                                searchRow, locTypeDto);
                                searchRow, locTypeDto, findLocNoAttributeVo);
                        if (!Cools.isEmpty(candidateLoc)) {
                            return candidateLoc;
                        }
@@ -1769,7 +2103,7 @@
                    }
                }
            }
            List<LocMast> locMasts = findOpenLocsByRow(rowLastno, rowLastnoType, searchRow, crnNo, locTypeDto, singleExtension);
            List<LocMast> locMasts = findOpenLocsByRow(rowLastno, rowLastnoType, searchRow, crnNo, locTypeDto, findLocNoAttributeVo, singleExtension, ignoreFreqType);
            if (!Cools.isEmpty(locMasts)) {
                return locMasts.get(0);
            }
@@ -1967,7 +2301,12 @@
     * run/run2 标准堆垛机统一的空库位查询入口。
     */
    private LocMast findStandardEmptyLoc(RowLastno rowLastno, RowLastnoType rowLastnoType, int crnNo, int nearRow, LocTypeDto locTypeDto) {
        return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto);
        return findStandardEmptyLoc(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto, null);
    }
    private LocMast findStandardEmptyLoc(RowLastno rowLastno, RowLastnoType rowLastnoType, int crnNo, int nearRow,
                                         LocTypeDto locTypeDto, FindLocNoAttributeVo findLocNoAttributeVo) {
        return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto, findLocNoAttributeVo);
    }
    /**
@@ -2083,7 +2422,7 @@
        if (signRule1) {
            if (nearRow != curRow) {
                List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                        .eq("row1", nearRow).eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue()));
                        .eq("row1", nearRow).eq("loc_sts", "O"));
                for (LocMast locMast1 : locMasts) {
                    //获取巷道
//                    List<String> groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,locMast1.getLocNo(), curRow>nearRow);
@@ -2125,7 +2464,7 @@
        // 靠近摆放规则 --- 空托 //互通版
        if (staDescId == 10 && Utils.BooleanWhsTypeStaIoType(rowLastno)) {
            List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                    .eq("loc_sts", "D").ge("row1", sRow).le("row1", eRow).eq("whs_type", rowLastnoType.getType().longValue()));
                    .eq("loc_sts", "D").ge("row1", sRow).le("row1", eRow));
            if (!locMasts.isEmpty()) {
                for (LocMast loc : locMasts) {
                    if (Utils.isShallowLoc(slaveProperties, loc.getLocNo())) {
@@ -2180,13 +2519,13 @@
        // Search empty location ==============================>>
        if (staDescId == 10 && Cools.isEmpty(locMast) && crnNo != 0) {
            locMast = findStandardEmptyLoc(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto);
            locMast = findStandardEmptyLoc(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto, findLocNoAttributeVo);
        }
        if (Cools.isEmpty(locMast) && crnNo != 0) {
            locMast = findStandardEmptyLoc(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto);
            locMast = findStandardEmptyLoc(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto, findLocNoAttributeVo);
        }
        if (!Cools.isEmpty(locMast) && !basCrnpService.checkSiteError(crnNo, true)) {
@@ -2228,21 +2567,15 @@
     * run2 入库找位主流程。
     *
     * 当前方法只保留“组织流程”和“统一收口”的职责,具体策略拆成独立方法:
     * 1. 普通物料:按 row_lastno 自身轮询顺序 -> 站点优先库区/堆垛机 -> 其它库区。
     * 2. 空托盘:优先库区 loc_type2=1 -> 其它库区 loc_type2=1 -> loc_type1=2 兼容。
     * 3. 命中库位后分别回写普通物料游标或空托盘库区游标。
     *
     * WCS 传入的推荐排不再参与 run2 选位,避免上游 row 参数把任务重新绑回固定堆垛机。
     * 1. 先按站点第一优先池找位,再找第二优先池。
     * 2. 池内按 current_no 轮转,从下一台堆垛机开始平均分配。
     * 3. 空托盘先按 loc_type2=1 搜索,同池无结果再允许其它库位。
     * 4. 低库位可向上兼容,兼容重试仍保持两层优先池顺序。
     */
    @Transactional
    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;
        LocMast locMast = null;
        StartupDto startupDto = new StartupDto();
        RowLastno rowLastno = rowLastnoService.selectById(whsType);
        if (Cools.isEmpty(rowLastno)) {
            throw new CoolException("数据异常,请联系管理员===>库位规则未知");
        }
@@ -2250,59 +2583,33 @@
        if (Cools.isEmpty(rowLastnoType)) {
            throw new CoolException("数据异常,请联系管理员===》库位规则类型未知");
        }
        int curRow = rowLastno.getCurrentRow() == null ? 0 : rowLastno.getCurrentRow();
        crnNo = resolveRun2CrnNo(rowLastno);
        Integer preferredArea = findLocNoAttributeVo.getOutArea();
        BasDevp station = basDevpService.selectById(sourceStaNo);
        if (Cools.isEmpty(station)) {
            throw new CoolException("站点=" + sourceStaNo + " 未配置入库优先堆垛机");
        }
        List<Integer> firstPoolCrnNos = loadPriorityCrnNos(station.getInFirstCrnCsv());
        List<Integer> secondPoolCrnNos = loadPriorityCrnNos(station.getInSecondCrnCsv());
        if (Cools.isEmpty(firstPoolCrnNos) && Cools.isEmpty(secondPoolCrnNos)) {
            throw new CoolException("站点=" + sourceStaNo + " 未配置入库优先堆垛机");
        }
        boolean emptyPalletRequest = isEmptyPalletRequest(staDescId, findLocNoAttributeVo);
        Run2AreaSearchResult emptyPalletAreaSearchResult = null;
        Run2SearchResult normalRun2SearchResult = null;
        List<Integer> orderedCrnNos = getOrderedCrnNos(rowLastno, crnNo);
        List<Integer> orderedRunnableCrnNos = getOrderedRunnableRun2CrnNos(rowLastno, staDescId, sourceStaNo, orderedCrnNos);
        if (emptyPalletRequest) {
            emptyPalletAreaSearchResult = findEmptyPalletRun2Loc(rowLastno, staDescId, sourceStaNo, startupDto, preferredArea, locTypeDto);
            if (!Cools.isEmpty(emptyPalletAreaSearchResult)) {
                locMast = emptyPalletAreaSearchResult.locMast;
            }
        } else {
            normalRun2SearchResult = findNormalRun2Loc(rowLastno, rowLastnoType, sourceStaNo, staDescId, findLocNoAttributeVo,
                    locTypeDto, startupDto, preferredArea, orderedCrnNos);
            if (normalRun2SearchResult != null) {
                locMast = normalRun2SearchResult.locMast;
            }
        }
        if (!Cools.isEmpty(locMast)) {
            crnNo = locMast.getCrnNo();
            nearRow = locMast.getRow1();
        }
        if (emptyPalletRequest) {
            advanceEmptyPalletRun2Cursor(emptyPalletAreaSearchResult, locMast);
        } else if (!Cools.isEmpty(locMast)) {
            List<Integer> cursorCrnNos = normalRun2SearchResult == null || Cools.isEmpty(normalRun2SearchResult.runnableCrnNos)
                    ? orderedRunnableCrnNos
                    : normalRun2SearchResult.runnableCrnNos;
            advanceNormalRun2Cursor(rowLastno, curRow, cursorCrnNos, locMast.getCrnNo());
        }
        LocMast locMast = findRun2PriorityLocInPools(station, rowLastno, rowLastnoType, staDescId, sourceStaNo,
                findLocNoAttributeVo, locTypeDto, startupDto, emptyPalletRequest);
        if (Cools.isEmpty(locMast) || !locMast.getLocSts().equals("O")) {
            if (emptyPalletRequest) {
                log.error("No empty location found. spec={}, preferredArea={}, nearRow={}",
                        JSON.toJSONString(locTypeDto), preferredArea, nearRow);
                log.error("No empty location found. spec={}, station={}, firstPool={}, secondPool={}",
                        JSON.toJSONString(locTypeDto), sourceStaNo, JSON.toJSONString(firstPoolCrnNos),
                        JSON.toJSONString(secondPoolCrnNos));
                throw new CoolException("没有空库位");
            }
            LocTypeDto compatibleLocTypeDto = buildRetryCompatibleLocTypeDto(staDescId, findLocNoAttributeVo, locTypeDto);
            if (compatibleLocTypeDto != null) {
                log.warn("locType compatibility retry. source={}, target={}", JSON.toJSONString(locTypeDto), JSON.toJSONString(compatibleLocTypeDto));
                return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, compatibleLocTypeDto, recommendRows, 0);
            }
            log.error("No empty location found. spec={}, preferredArea={}, nearRow={}", JSON.toJSONString(locTypeDto), preferredArea, nearRow);
            log.error("No empty location found. spec={}, station={}, firstPool={}, secondPool={}",
                    JSON.toJSONString(locTypeDto), sourceStaNo, JSON.toJSONString(firstPoolCrnNos), JSON.toJSONString(secondPoolCrnNos));
            throw new CoolException("没有空库位");
        }
        int workNo = getWorkNo(0);
        startupDto.setWorkNo(workNo);
        startupDto.setCrnNo(crnNo);
        startupDto.setCrnNo(locMast.getCrnNo());
        startupDto.setSourceStaNo(sourceStaNo);
        startupDto.setLocNo(locMast.getLocNo());
        return startupDto;
@@ -2311,7 +2618,12 @@
     * 单伸堆垛机复用统一画像算法查询空库位。
     */
    private LocMast findSingleExtensionEmptyLoc(RowLastno rowLastno, int crnNo, int nearRow, RowLastnoType rowLastnoType, LocTypeDto locTypeDto) {
        return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto);
        return findSingleExtensionEmptyLoc(rowLastno, crnNo, nearRow, rowLastnoType, locTypeDto, null);
    }
    private LocMast findSingleExtensionEmptyLoc(RowLastno rowLastno, int crnNo, int nearRow, RowLastnoType rowLastnoType,
                                                LocTypeDto locTypeDto, FindLocNoAttributeVo findLocNoAttributeVo) {
        return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto, findLocNoAttributeVo);
    }
    public StartupDto getLocNoRun4(Integer whsType, Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, Integer moveCrnNo, LocTypeDto locTypeDto, int times) {
@@ -2356,7 +2668,7 @@
            crnNo = locNecessaryParameters[2];
            nearRow = locNecessaryParameters[3];
            List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                    .eq("crn_no", crnNo).eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue()));
                    .eq("crn_no", crnNo).eq("loc_sts", "O"));
            if (locMasts.size() <= 5) {
                nearRow = 0;
                times++;
@@ -2517,7 +2829,7 @@
        if (signRule1) {
            if (nearRow != curRow) {
                List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                        .eq("row1", nearRow).eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue()));
                        .eq("row1", nearRow).eq("loc_sts", "O"));
                for (LocMast locMast1 : locMasts) {
                    //获取巷道
//                    List<String> groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,locMast1.getLocNo(), curRow>nearRow);
@@ -2688,7 +3000,7 @@
        if (Cools.isEmpty(locMast) && crnNo != 0) {
            List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                    .eq("row1", nearRow)
                    .eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue())
                    .eq("loc_sts", "O")
                    .orderBy("lev1", true).orderBy("bay1", true));//最浅库位
            for (LocMast locMast1 : locMasts) {
                if (!VersionUtils.locMoveCheckLocTypeComplete(locMast1, locTypeDto)) {
@@ -2836,36 +3148,27 @@
        // 开始查找库位 ==============================>>
        Integer preferredArea = findLocNoAttributeVo.getOutArea();
        Integer preferredArea = findLocNoAttributeVo == null ? null : findLocNoAttributeVo.getOutArea();
        List<Integer> areaSearchOrder = buildAreaSearchOrder(findLocNoAttributeVo == null ? null : findLocNoAttributeVo.getOutAreas(), preferredArea);
        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)); // 最浅库位
            for (LocMast locMast1 : locMasts) {
                if (!VersionUtils.locMoveCheckLocTypeComplete(locMast1, locTypeDto)) {
        if (Cools.isEmpty(locMast)) {
            for (Integer area : areaSearchOrder) {
                int[] bayRange = getAgvAreaBayRange(area);
                List<Integer> areaRows = getAgvAreaRows(area, rowLastno);
                if (Cools.isEmpty(areaRows)) {
                    continue;
                }
                if (Utils.BooleanWhsTypeStaIoType(rowLastno)) {
                    // 获取目标库位所在巷道最深空库位
                    LocMast locMast2 = locMastService.selectLocByLocStsPakInO(curRow, nearRow, locMast1, rowLastnoType.getType().longValue());
                    if (!Cools.isEmpty(locMast2) && locMast2.getRow1() == curRow) {
                        locMast = locMast2;
                        break;
                    }
                locMast = findAgvLocByRows(rowLastno, rowLastnoType, areaRows,
                        bayRange[0], bayRange[1], curRow, nearRow, locTypeDto, findLocNoAttributeVo, false);
                if (!Cools.isEmpty(locMast)) {
                    crnNo = locMast.getCrnNo();
                    preferredArea = area;
                    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();
            }
            if (Cools.isEmpty(locMast)) {
                locMast = findAgvLocByRows(rowLastno, rowLastnoType, getAgvFallbackRows(rowLastno),
                        1, 19, curRow, nearRow, locTypeDto, true);
                        1, 19, curRow, nearRow, locTypeDto, findLocNoAttributeVo, true);
                if (!Cools.isEmpty(locMast)) {
                    crnNo = locMast.getCrnNo();
                }
src/main/java/com/zy/common/web/WcsController.java
@@ -137,7 +137,7 @@
            // 源站点状态检测
            BasDevp sourceStaNoEntity = basDevpService.checkSiteStatus(param.getSourceStaNo(), true);
            sourceStaNoEntity.setLocType1(param.getLocType1());
            sourceStaNoEntity.setLocType1((short) (param.getLocType1()!=1?2:1));
            LocTypeDto locTypeDto = new LocTypeDto(sourceStaNoEntity);
            if (waitPakins.get(0).getMatnr().equals("emptyPallet")) {
                locTypeDto.setLocType2((short) 1);
src/main/resources/application.yml
@@ -99,9 +99,9 @@
    #出库上报
    Outaddress: /api/Service/OutPalletCompleted
    #出库异常上报
    OutErroraddress: /api/Service/OutPalletAbnormal
    OutErroraddress: /api/Service/SubmitOutPalletMsg
    #出库任务锁定上报
    OutLockaddress: /api/Service/OutPalletLocked
    OutLockaddress: /api/Service/LockOutOrderPallet
#wcs任务下发
wcs:
src/main/resources/mapper/BasDevpMapper.xml
@@ -27,6 +27,10 @@
        <result column="row1" property="row1" />
        <result column="io_time" property="ioTime" />
        <result column="area" property="area" />
        <result column="in_first_crn_csv" property="inFirstCrnCsv" />
        <result column="in_second_crn_csv" property="inSecondCrnCsv" />
        <result column="in_first_crn_current_no" property="inFirstCrnCurrentNo" />
        <result column="in_second_crn_current_no" property="inSecondCrnCurrentNo" />
        <result column="in_ok" property="inOk" />
        <result column="out_ok" property="outOk" />
        <result column="loc_type1" property="locType1" />
src/main/resources/mapper/WaitPakinLogMapper.xml
@@ -11,6 +11,7 @@
        <result column="maktx" property="maktx" />
        <result column="batch" property="batch" />
        <result column="order_no" property="orderNo" />
        <result column="freq_type" property="freqType" />
        <result column="specs" property="specs" />
        <result column="model" property="model" />
src/main/resources/mapper/WaitPakinMapper.xml
@@ -11,6 +11,7 @@
        <result column="maktx" property="maktx" />
        <result column="batch" property="batch" />
        <result column="order_no" property="orderNo" />
        <result column="freq_type" property="freqType" />
        <result column="specs" property="specs" />
        <result column="model" property="model" />
src/main/webapp/static/js/basDevp/basDevp.js
@@ -81,6 +81,8 @@
            ,{field: 'barcode', align: 'center',title: '条形码'}
            ,{field: 'inQty', align: 'center',title: '入库暂存'}
            ,{field: 'area$', align: 'center',title: '绑定库区'}
            ,{field: 'inFirstCrnCsv', align: 'center',title: '第一优先池'}
            ,{field: 'inSecondCrnCsv', align: 'center',title: '第二优先池'}
            // ,{field: 'row1', align: 'center',title: ''}
            // ,{field: 'ioTime$', align: 'center',title: ''}
            // ,{field: 'area', align: 'center',title: ''}
@@ -483,7 +485,11 @@
            inQty: $('#inQty').val(),
            row1: $('#row1').val(),
            ioTime: top.strToDate($('#ioTime\\$').val()),
            area: $('#area').val(),
            area: getAreaSubmitValue(),
            inFirstCrnCsv: $('#inFirstCrnCsv').val(),
            inSecondCrnCsv: $('#inSecondCrnCsv').val(),
            inFirstCrnCurrentNo: $('#inFirstCrnCurrentNo').val(),
            inSecondCrnCurrentNo: $('#inSecondCrnCurrentNo').val(),
            inOk: $('#inOk').val(),
            outOk: $('#outOk').val(),
            modiUser: $('#modiUser').val(),
@@ -527,6 +533,9 @@
        } else {
            $(el).val('N');
        }
    });
    form.on('checkbox(areaCheckbox)', function () {
        $('#area').val(getAreaSubmitValue());
    });
    // 搜索栏搜索事件
@@ -598,7 +607,11 @@
        var find = el.find(":input[id='" + val + "']");
        var currentVal = data[val];
        if (val === 'area') {
            currentVal = normalizeAreaValue(currentVal);
            setAreaValues(currentVal);
            if (find[0] != null) {
                find.val(normalizeAreaValues(currentVal).join(','));
            }
            continue;
        }
        if (find[0]!=null){
            if (find[0].type === 'checkbox'){
@@ -609,6 +622,9 @@
                    find.remove("checked");
                    find.val('N');
                }
                continue;
            } else if (find[0].type === 'select-multiple') {
                find.val(currentVal || []);
                continue;
            }
        }
@@ -643,11 +659,68 @@
    return value;
}
function normalizeAreaValues(value) {
    if (value === undefined || value === null || value === '') {
        return [];
    }
    var values = Object.prototype.toString.call(value) === '[object Array]'
        ? value
        : String(value).replace(/[,;、]/g, ',').split(/[,;]+/);
    var result = [];
    for (var i = 0; i < values.length; i++) {
        var normalized = normalizeAreaValue(values[i]);
        if (normalized === undefined || normalized === null || normalized === '') {
            continue;
        }
        if (result.indexOf(normalized) < 0) {
            result.push(normalized);
        }
    }
    return result;
}
function setAreaValues(value) {
    var values = normalizeAreaValues(value);
    $('#area').val(values.join(','));
    $('#areaBox input[type="checkbox"][name="areaOption"]').each(function () {
        var checked = values.indexOf(this.value) >= 0;
        $(this).prop('checked', checked);
        if (checked) {
            $(this).attr('checked', 'checked');
        } else {
            $(this).removeAttr('checked');
        }
    });
}
function getAreaValues() {
    var values = [];
    $('#areaBox input[type="checkbox"][name="areaOption"]').each(function () {
        if (this.checked && values.indexOf(this.value) < 0) {
            values.push(this.value);
        }
    });
    return values;
}
function getAreaSubmitValue() {
    var values = getAreaValues();
    $('#area').val(values.join(','));
    return values.join(',');
}
function clearFormVal(el) {
    $(':input', el)
        .val('')
        .removeAttr('checked')
        .removeAttr('selected');
    $(':input', el).each(function () {
        if (this.type === 'checkbox' || this.type === 'radio') {
            $(this).prop('checked', false).removeAttr('checked');
            return;
        }
        if (this.tagName === 'SELECT' && this.multiple) {
            $(this).val([]).find('option').prop('selected', false);
            return;
        }
        $(this).val('');
    });
}
function detailScreen(index) {
src/main/webapp/views/basCrnDepthRule/basCrnDepthRule.html
@@ -192,6 +192,16 @@
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label">货物频次</label>
            <div class="layui-input-block">
                <select class="layui-input" lay-ignore name="freqType">
                    <option value="">默认</option>
                    <option value="1">高频</option>
                    <option value="2">低频</option>
                </select>
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label">物料编码</label>
            <div class="layui-input-block">
                <input class="layui-input" name="matnr" placeholder="emptyPallet 表示空托盘">
src/main/webapp/views/basDevp/basDevp_detail.html
@@ -150,15 +150,28 @@
                <input id="ioTime$" class="layui-input" type="text" autocomplete="off">
            </div>
        </div>
        <div class="layui-inline"  style="width:31%;">
        <div class="layui-inline" style="width:94%;">
            <label class="layui-form-label">绑定库区:</label>
            <div class="layui-input-inline">
                <select id="area" class="layui-input">
                    <option value="">不限制</option>
                    <option value="A">A库区</option>
                    <option value="B">B库区</option>
                    <option value="C">C库区</option>
                </select>
            <div class="layui-input-block" id="areaBox" style="margin-left: 110px; min-height: 38px;">
                <input id="area" type="hidden">
                <input type="checkbox" name="areaOption" value="A" title="A库区" lay-skin="primary" lay-filter="areaCheckbox">
                <input type="checkbox" name="areaOption" value="B" title="B库区" lay-skin="primary" lay-filter="areaCheckbox">
                <input type="checkbox" name="areaOption" value="C" title="C库区" lay-skin="primary" lay-filter="areaCheckbox">
                <div class="layui-form-mid layui-word-aux" style="padding-left: 0;">仅用于站点绑定,不参与找位排序</div>
            </div>
        </div>
        <div class="layui-inline" style="width:94%;">
            <label class="layui-form-label">第一优先池:</label>
            <div class="layui-input-block" style="margin-left: 110px; min-height: 38px;">
                <input id="inFirstCrnCsv" class="layui-input" type="text" placeholder="请输入堆垛机号,按顺序填写,如 1,2,3">
                <input id="inFirstCrnCurrentNo" type="hidden">
            </div>
        </div>
        <div class="layui-inline" style="width:94%;">
            <label class="layui-form-label">第二优先池:</label>
            <div class="layui-input-block" style="margin-left: 110px; min-height: 38px;">
                <input id="inSecondCrnCsv" class="layui-input" type="text" placeholder="请输入堆垛机号,按顺序填写,如 4,5,6">
                <input id="inSecondCrnCurrentNo" type="hidden">
            </div>
        </div>
        <div class="layui-inline"  style="width:31%;display: none">