自动化立体仓库 - WMS系统
zwl
3 天以前 2acfc2d2a0e956910c51bd996f443b3cb9bd3dc9
优化找库位规则
16个文件已添加
10个文件已修改
3427 ■■■■■ 已修改文件
pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/BasCrnDepthRuleController.java 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/BasCrnDepthRule.java 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/RowLastno.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/BasCrnDepthRuleRuntimePreviewParam.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/BasCrnDepthRuleTemplateParam.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/BasCrnDepthRuleMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/BasCrnDepthRuleService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/BasCrnDepthRuleServiceImpl.java 471 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/BasCrnpServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/utils/Utils.java 52 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/model/CrnDepthRuleProfile.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/service/CommonService.java 1090 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/web/WcsController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/BasCrnDepthRuleMapper.xml 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260321_bas_crn_depth_rule.sql 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260322_row_lastno_current_crn_no.sql 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basCrnDepthRule/basCrnDepthRule.js 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/basDevp/basDevp.js 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/rowLastno/rowLastno.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basCrnDepthRule/basCrnDepthRule.html 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/basDevp/basDevp_detail.html 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/rowLastno/rowLastno_detail.html 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/asrs/service/impl/BasCrnDepthRuleServiceImplTest.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/common/service/CommonServiceLocTypeStrategyTest.java 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/test/java/com/zy/common/service/CommonServiceRun2AllocationTest.java 329 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -117,6 +117,11 @@
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
src/main/java/com/zy/asrs/controller/BasCrnDepthRuleController.java
New file
@@ -0,0 +1,214 @@
package com.zy.asrs.controller;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import com.baomidou.mybatisplus.plugins.Page;
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.zy.asrs.entity.BasCrnDepthRule;
import com.zy.asrs.entity.param.BasCrnDepthRuleRuntimePreviewParam;
import com.zy.asrs.entity.param.BasCrnDepthRuleTemplateParam;
import com.zy.asrs.service.BasCrnDepthRuleService;
import com.zy.common.service.CommonService;
import com.zy.common.web.BaseController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
public class BasCrnDepthRuleController extends BaseController {
    @Autowired
    private BasCrnDepthRuleService basCrnDepthRuleService;
    @Autowired
    private CommonService commonService;
    /**
     * 查询单条堆垛机深浅规则。
     */
    @RequestMapping(value = "/basCrnDepthRule/{id}/auth")
    @ManagerAuth
    public R get(@PathVariable("id") Long id) {
        return R.ok(basCrnDepthRuleService.selectById(id));
    }
    /**
     * 分页查询堆垛机深浅规则列表。
     */
    @RequestMapping(value = "/basCrnDepthRule/list/auth")
    @ManagerAuth
    public R list(@RequestParam(defaultValue = "1") Integer curr,
                  @RequestParam(defaultValue = "10") Integer limit,
                  @RequestParam(required = false) String orderByField,
                  @RequestParam(required = false) String orderByType,
                  @RequestParam(required = false) String condition,
                  @RequestParam Map<String, Object> param) {
        EntityWrapper<BasCrnDepthRule> wrapper = new EntityWrapper<BasCrnDepthRule>();
        excludeTrash(param);
        convert(param, wrapper);
        allLike(BasCrnDepthRule.class, param.keySet(), wrapper, condition);
        if (!Cools.isEmpty(orderByField)) {
            wrapper.orderBy(humpToLine(orderByField), "asc".equals(orderByType));
        } else {
            wrapper.orderBy("whs_type", true).orderBy("crn_no", true);
        }
        return R.ok(basCrnDepthRuleService.selectPage(new Page<BasCrnDepthRule>(curr, limit), wrapper));
    }
    /**
     * 把前端查询参数转换成 MyBatis Plus 的模糊/时间范围条件。
     */
    private <T> void convert(Map<String, Object> map, EntityWrapper<T> wrapper) {
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String val = String.valueOf(entry.getValue());
            if (val.contains(RANGE_TIME_LINK)) {
                String[] dates = val.split(RANGE_TIME_LINK);
                wrapper.ge(entry.getKey(), DateUtils.convert(dates[0]));
                wrapper.le(entry.getKey(), DateUtils.convert(dates[1]));
            } else {
                wrapper.like(entry.getKey(), val);
            }
        }
    }
    /**
     * 新增一条堆垛机深浅规则。
     */
    @RequestMapping(value = "/basCrnDepthRule/add/auth")
    @ManagerAuth(memo = "堆垛机深浅规则添加")
    public R add(BasCrnDepthRule basCrnDepthRule) {
        basCrnDepthRuleService.validateRule(basCrnDepthRule);
        Date now = new Date();
        basCrnDepthRule.setCreateBy(getUserId());
        basCrnDepthRule.setCreateTime(now);
        basCrnDepthRule.setUpdateBy(getUserId());
        basCrnDepthRule.setUpdateTime(now);
        basCrnDepthRuleService.insert(basCrnDepthRule);
        return R.ok("保存成功");
    }
    /**
     * 修改已有堆垛机深浅规则,并保留创建审计字段。
     */
    @RequestMapping(value = "/basCrnDepthRule/update/auth")
    @ManagerAuth(memo = "堆垛机深浅规则修改")
    public R update(BasCrnDepthRule basCrnDepthRule) {
        if (Cools.isEmpty(basCrnDepthRule) || basCrnDepthRule.getId() == null) {
            return R.error();
        }
        basCrnDepthRuleService.validateRule(basCrnDepthRule);
        BasCrnDepthRule dbRule = basCrnDepthRuleService.selectById(basCrnDepthRule.getId());
        if (dbRule == null) {
            return R.error("规则不存在");
        }
        basCrnDepthRule.setCreateBy(dbRule.getCreateBy());
        basCrnDepthRule.setCreateTime(dbRule.getCreateTime());
        basCrnDepthRule.setUpdateBy(getUserId());
        basCrnDepthRule.setUpdateTime(new Date());
        basCrnDepthRuleService.updateById(basCrnDepthRule);
        return R.ok("修改完成");
    }
    /**
     * 批量删除堆垛机深浅规则。
     */
    @RequestMapping(value = "/basCrnDepthRule/delete/auth")
    @ManagerAuth(memo = "堆垛机深浅规则删除")
    public R delete(@RequestParam String param) {
        List<BasCrnDepthRule> list = JSONArray.parseArray(param, BasCrnDepthRule.class);
        if (Cools.isEmpty(list)) {
            return R.error();
        }
        for (BasCrnDepthRule entity : list) {
            if (entity != null && entity.getId() != null) {
                basCrnDepthRuleService.deleteById(entity.getId());
            }
        }
        return R.ok();
    }
    /**
     * 导出当前查询条件命中的规则数据。
     */
    @RequestMapping(value = "/basCrnDepthRule/export/auth")
    @ManagerAuth(memo = "堆垛机深浅规则导出")
    public R export(@RequestBody JSONObject param) {
        List<String> fields = JSONObject.parseArray(param.getJSONArray("fields").toJSONString(), String.class);
        EntityWrapper<BasCrnDepthRule> wrapper = new EntityWrapper<BasCrnDepthRule>();
        Map<String, Object> map = excludeTrash(param.getJSONObject("basCrnDepthRule"));
        convert(map, wrapper);
        List<BasCrnDepthRule> list = basCrnDepthRuleService.selectList(wrapper);
        return R.ok(exportSupport(list, fields));
    }
    /**
     * 按模板预览某仓库范围内的默认深浅规则。
     */
    @RequestMapping(value = "/basCrnDepthRule/templatePreview/auth")
    @ManagerAuth(memo = "堆垛机深浅规则模板预览")
    public R templatePreview(BasCrnDepthRuleTemplateParam param) {
        return R.ok(basCrnDepthRuleService.previewTemplate(param));
    }
    /**
     * 按模板批量生成或覆盖某仓库的堆垛机深浅规则。
     */
    @RequestMapping(value = "/basCrnDepthRule/templateGenerate/auth")
    @ManagerAuth(memo = "堆垛机深浅规则模板生成")
    public R templateGenerate(BasCrnDepthRuleTemplateParam param) {
        basCrnDepthRuleService.saveTemplate(param, getUserId());
        return R.ok("模板生成完成");
    }
    /**
     * 预览某站点当前的库区、设备和深浅排参与顺序。
     */
    @RequestMapping(value = "/basCrnDepthRule/runtimePreview/auth")
    @ManagerAuth(memo = "堆垛机深浅规则运行预览")
    public R runtimePreview(BasCrnDepthRuleRuntimePreviewParam param) {
        return R.ok(commonService.previewRun2Allocation(param));
    }
    /**
     * 为前端自动完成提供规则主键查询。
     */
    @RequestMapping(value = "/basCrnDepthRuleQuery/auth")
    @ManagerAuth
    public R query(String condition) {
        EntityWrapper<BasCrnDepthRule> wrapper = new EntityWrapper<BasCrnDepthRule>();
        wrapper.like("crn_no", condition);
        Page<BasCrnDepthRule> page = basCrnDepthRuleService.selectPage(new Page<BasCrnDepthRule>(0, 10), wrapper);
        List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
        for (BasCrnDepthRule rule : page.getRecords()) {
            Map<String, Object> map = new HashMap<String, Object>();
            map.put("id", rule.getId());
            map.put("value", rule.getWhsType() + "-" + rule.getCrnNo());
            result.add(map);
        }
        return R.ok(result);
    }
    /**
     * 检查某个字段值是否已存在,用于前端重复校验。
     */
    @RequestMapping(value = "/basCrnDepthRule/check/column/auth")
    @ManagerAuth
    public R query(@RequestBody JSONObject param) {
        Wrapper<BasCrnDepthRule> wrapper = new EntityWrapper<BasCrnDepthRule>()
                .eq(humpToLine(String.valueOf(param.get("key"))), param.get("val"));
        if (null != basCrnDepthRuleService.selectOne(wrapper)) {
            return R.parse(BaseRes.REPEAT).add(getComment(BasCrnDepthRule.class, String.valueOf(param.get("key"))));
        }
        return R.ok();
    }
}
src/main/java/com/zy/asrs/entity/BasCrnDepthRule.java
New file
@@ -0,0 +1,146 @@
package com.zy.asrs.entity;
import com.baomidou.mybatisplus.annotations.TableField;
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.annotations.TableName;
import com.baomidou.mybatisplus.enums.IdType;
import com.core.common.Cools;
import com.core.common.SpringUtils;
import com.zy.asrs.service.BasWhsService;
import com.zy.system.entity.User;
import com.zy.system.service.UserService;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
@Data
@TableName("asr_bas_crn_depth_rule")
public class BasCrnDepthRule implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "仓库类型")
    @TableField("whs_type")
    private Integer whsType;
    @ApiModelProperty(value = "堆垛机号")
    @TableField("crn_no")
    private Integer crnNo;
    @ApiModelProperty(value = "布局类型 1:单伸 2:双伸")
    @TableField("layout_type")
    private Integer layoutType;
    @ApiModelProperty(value = "搜索排顺序CSV")
    @TableField("search_rows_csv")
    private String searchRowsCsv;
    @ApiModelProperty(value = "浅库位排CSV")
    @TableField("shallow_rows_csv")
    private String shallowRowsCsv;
    @ApiModelProperty(value = "深库位排CSV")
    @TableField("deep_rows_csv")
    private String deepRowsCsv;
    @ApiModelProperty(value = "启用状态 1:启用 0:禁用")
    private Integer enabled;
    @ApiModelProperty(value = "备注")
    private String memo;
    @ApiModelProperty(value = "创建人员")
    @TableField("create_by")
    private Long createBy;
    @ApiModelProperty(value = "创建时间")
    @TableField("create_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    @ApiModelProperty(value = "修改人员")
    @TableField("update_by")
    private Long updateBy;
    @ApiModelProperty(value = "修改时间")
    @TableField("update_time")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
    public String getWhsType$() {
        BasWhsService service = SpringUtils.getBean(BasWhsService.class);
        BasWhs basWhs = service.selectById(this.whsType);
        if (!Cools.isEmpty(basWhs)) {
            return String.valueOf(basWhs.getWhsDesc());
        }
        return null;
    }
    public String getLayoutType$() {
        if (this.layoutType == null) {
            return null;
        }
        switch (this.layoutType) {
            case 1:
                return "单伸";
            case 2:
                return "双伸";
            default:
                return String.valueOf(this.layoutType);
        }
    }
    public String getEnabled$() {
        if (this.enabled == null) {
            return null;
        }
        switch (this.enabled) {
            case 1:
                return "启用";
            case 0:
                return "禁用";
            default:
                return String.valueOf(this.enabled);
        }
    }
    public String getCreateBy$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.selectById(this.createBy);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getCreateTime$() {
        if (Cools.isEmpty(this.createTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
    }
    public String getUpdateBy$() {
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.selectById(this.updateBy);
        if (!Cools.isEmpty(user)) {
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getUpdateTime$() {
        if (Cools.isEmpty(this.updateTime)) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.updateTime);
    }
}
src/main/java/com/zy/asrs/entity/RowLastno.java
@@ -44,6 +44,13 @@
    private Integer currentRow;
    /**
     * 当前堆垛机号
     */
    @ApiModelProperty(value= "当前堆垛机号")
    @TableField("current_crn_no")
    private Integer currentCrnNo;
    /**
     * 起始排号
     */
    @ApiModelProperty(value= "起始排号")
@@ -226,6 +233,14 @@
        this.currentRow = currentRow;
    }
    public Integer getCurrentCrnNo() {
        return currentCrnNo;
    }
    public void setCurrentCrnNo(Integer currentCrnNo) {
        this.currentCrnNo = currentCrnNo;
    }
    public Integer getsRow() {
        return sRow;
    }
src/main/java/com/zy/asrs/entity/param/BasCrnDepthRuleRuntimePreviewParam.java
New file
@@ -0,0 +1,21 @@
package com.zy.asrs.entity.param;
import lombok.Data;
@Data
public class BasCrnDepthRuleRuntimePreviewParam {
    private Integer staDescId;
    private Integer sourceStaNo;
    private Integer outArea;
    private String matnr;
    private Short locType1;
    private Short locType2;
    private Short locType3;
}
src/main/java/com/zy/asrs/entity/param/BasCrnDepthRuleTemplateParam.java
New file
@@ -0,0 +1,17 @@
package com.zy.asrs.entity.param;
import lombok.Data;
@Data
public class BasCrnDepthRuleTemplateParam {
    private Integer whsType;
    private Integer startCrnNo;
    private Integer endCrnNo;
    private Integer enabled;
    private String memo;
}
src/main/java/com/zy/asrs/mapper/BasCrnDepthRuleMapper.java
New file
@@ -0,0 +1,11 @@
package com.zy.asrs.mapper;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.zy.asrs.entity.BasCrnDepthRule;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface BasCrnDepthRuleMapper extends BaseMapper<BasCrnDepthRule> {
}
src/main/java/com/zy/asrs/service/BasCrnDepthRuleService.java
New file
@@ -0,0 +1,22 @@
package com.zy.asrs.service;
import com.baomidou.mybatisplus.service.IService;
import com.zy.asrs.entity.BasCrnDepthRule;
import com.zy.asrs.entity.RowLastno;
import com.zy.asrs.entity.param.BasCrnDepthRuleTemplateParam;
import com.zy.common.model.CrnDepthRuleProfile;
import java.util.List;
public interface BasCrnDepthRuleService extends IService<BasCrnDepthRule> {
    void validateRule(BasCrnDepthRule rule);
    BasCrnDepthRule findEnabledRule(Integer whsType, Integer crnNo);
    CrnDepthRuleProfile resolveProfile(RowLastno rowLastno, Integer crnNo, Integer preferredNearRow);
    List<BasCrnDepthRule> previewTemplate(BasCrnDepthRuleTemplateParam param);
    void saveTemplate(BasCrnDepthRuleTemplateParam param, Long userId);
}
src/main/java/com/zy/asrs/service/impl/BasCrnDepthRuleServiceImpl.java
New file
@@ -0,0 +1,471 @@
package com.zy.asrs.service.impl;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.core.common.Cools;
import com.core.exception.CoolException;
import com.zy.asrs.entity.BasCrnDepthRule;
import com.zy.asrs.entity.RowLastno;
import com.zy.asrs.entity.param.BasCrnDepthRuleTemplateParam;
import com.zy.asrs.mapper.BasCrnDepthRuleMapper;
import com.zy.asrs.service.BasCrnDepthRuleService;
import com.zy.asrs.service.RowLastnoService;
import com.zy.asrs.utils.Utils;
import com.zy.common.model.CrnDepthRuleProfile;
import com.zy.common.properties.SlaveProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
@Service("basCrnDepthRuleService")
public class BasCrnDepthRuleServiceImpl extends ServiceImpl<BasCrnDepthRuleMapper, BasCrnDepthRule> implements BasCrnDepthRuleService {
    private static final String PROFILE_SOURCE_CONFIG = "CONFIG";
    private static final String PROFILE_SOURCE_LEGACY = "LEGACY";
    @Autowired
    private RowLastnoService rowLastnoService;
    @Autowired
    private SlaveProperties slaveProperties;
    /**
     * 校验并标准化单条堆垛机深浅规则。
     */
    @Override
    public void validateRule(BasCrnDepthRule rule) {
        if (rule == null || rule.getWhsType() == null || rule.getCrnNo() == null) {
            throw new CoolException("仓库和堆垛机不能为空");
        }
        if (!Integer.valueOf(1).equals(rule.getLayoutType()) && !Integer.valueOf(2).equals(rule.getLayoutType())) {
            throw new CoolException("布局类型只允许 1=单伸 或 2=双伸");
        }
        List<Integer> searchRows = parseRows(rule.getSearchRowsCsv());
        List<Integer> shallowRows = parseRows(rule.getShallowRowsCsv());
        List<Integer> deepRows = parseRows(rule.getDeepRowsCsv());
        if (searchRows.isEmpty()) {
            throw new CoolException("搜索排顺序不能为空");
        }
        if (Integer.valueOf(1).equals(rule.getLayoutType())) {
            if (shallowRows.isEmpty()) {
                shallowRows = new ArrayList<Integer>(searchRows);
            }
            if (!deepRows.isEmpty()) {
                throw new CoolException("单伸规则不允许填写深库位排");
            }
        } else {
            if (shallowRows.isEmpty()) {
                throw new CoolException("双伸规则必须填写浅库位排");
            }
            if (deepRows.isEmpty()) {
                throw new CoolException("双伸规则必须填写深库位排");
            }
        }
        rule.setSearchRowsCsv(joinRows(searchRows));
        rule.setShallowRowsCsv(joinRows(shallowRows));
        rule.setDeepRowsCsv(joinRows(deepRows));
        if (rule.getEnabled() == null) {
            rule.setEnabled(1);
        }
    }
    /**
     * 查询某仓库某堆垛机当前启用的深浅规则。
     */
    @Override
    public BasCrnDepthRule findEnabledRule(Integer whsType, Integer crnNo) {
        if (whsType == null || crnNo == null) {
            return null;
        }
        return this.selectOne(new EntityWrapper<BasCrnDepthRule>()
                .eq("whs_type", whsType)
                .eq("crn_no", crnNo)
                .eq("enabled", 1));
    }
    /**
     * 解析运行时使用的深浅排画像,优先走配置,缺失时回退旧逻辑。
     */
    @Override
    public CrnDepthRuleProfile resolveProfile(RowLastno rowLastno, Integer crnNo, Integer preferredNearRow) {
        if (rowLastno == null || crnNo == null) {
            return new CrnDepthRuleProfile();
        }
        BasCrnDepthRule rule = findEnabledRule(rowLastno.getWhsType(), crnNo);
        if (!Cools.isEmpty(rule)) {
            return buildProfileFromRule(rule);
        }
        return buildLegacyProfile(rowLastno, crnNo, preferredNearRow);
    }
    /**
     * 按旧逻辑预览一批堆垛机的默认深浅规则模板。
     */
    @Override
    public List<BasCrnDepthRule> previewTemplate(BasCrnDepthRuleTemplateParam param) {
        if (param == null || param.getWhsType() == null) {
            throw new CoolException("仓库不能为空");
        }
        RowLastno rowLastno = rowLastnoService.selectById(param.getWhsType());
        if (Cools.isEmpty(rowLastno)) {
            throw new CoolException("未找到仓库对应的轮询规则");
        }
        int startCrnNo = param.getStartCrnNo() == null ? defaultStartCrnNo(rowLastno) : param.getStartCrnNo();
        int endCrnNo = param.getEndCrnNo() == null ? defaultEndCrnNo(rowLastno) : param.getEndCrnNo();
        if (startCrnNo <= 0 || endCrnNo < startCrnNo) {
            throw new CoolException("堆垛机范围错误");
        }
        List<BasCrnDepthRule> rules = new ArrayList<BasCrnDepthRule>();
        for (int crnNo = startCrnNo; crnNo <= endCrnNo; crnNo++) {
            CrnDepthRuleProfile profile = buildLegacyProfile(rowLastno, crnNo, null);
            BasCrnDepthRule rule = new BasCrnDepthRule();
            rule.setWhsType(param.getWhsType());
            rule.setCrnNo(crnNo);
            rule.setLayoutType(profile.getLayoutType());
            rule.setSearchRowsCsv(joinRows(profile.getSearchRows()));
            rule.setShallowRowsCsv(joinRows(profile.getShallowRows()));
            rule.setDeepRowsCsv(joinRows(profile.getDeepRows()));
            rule.setEnabled(param.getEnabled() == null ? 1 : param.getEnabled());
            rule.setMemo(param.getMemo());
            rules.add(rule);
        }
        return rules;
    }
    /**
     * 按模板批量保存规则,已存在则覆盖,不存在则新增。
     */
    @Override
    public void saveTemplate(BasCrnDepthRuleTemplateParam param, Long userId) {
        List<BasCrnDepthRule> previewRules = previewTemplate(param);
        Date now = new Date();
        for (BasCrnDepthRule previewRule : previewRules) {
            validateRule(previewRule);
            BasCrnDepthRule exists = this.selectOne(new EntityWrapper<BasCrnDepthRule>()
                    .eq("whs_type", previewRule.getWhsType())
                    .eq("crn_no", previewRule.getCrnNo()));
            if (exists == null) {
                previewRule.setCreateBy(userId);
                previewRule.setCreateTime(now);
                previewRule.setUpdateBy(userId);
                previewRule.setUpdateTime(now);
                this.insert(previewRule);
                continue;
            }
            previewRule.setId(exists.getId());
            previewRule.setCreateBy(exists.getCreateBy());
            previewRule.setCreateTime(exists.getCreateTime());
            previewRule.setUpdateBy(userId);
            previewRule.setUpdateTime(now);
            this.updateById(previewRule);
        }
    }
    /**
     * 把数据库规则转换成统一的运行时画像对象。
     */
    private CrnDepthRuleProfile buildProfileFromRule(BasCrnDepthRule rule) {
        CrnDepthRuleProfile profile = new CrnDepthRuleProfile();
        profile.setWhsType(rule.getWhsType());
        profile.setCrnNo(rule.getCrnNo());
        profile.setLayoutType(rule.getLayoutType());
        profile.setSource(PROFILE_SOURCE_CONFIG);
        profile.setSearchRows(normalizeRows(parseRows(rule.getSearchRowsCsv())));
        profile.setShallowRows(normalizeRows(parseRows(rule.getShallowRowsCsv())));
        profile.setDeepRows(normalizeRows(parseRows(rule.getDeepRowsCsv())));
        if (profile.getSearchRows().isEmpty()) {
            if (!profile.getShallowRows().isEmpty()) {
                profile.setSearchRows(new ArrayList<Integer>(profile.getShallowRows()));
            } else {
                profile.setSearchRows(new ArrayList<Integer>(profile.getDeepRows()));
            }
        }
        if (profile.isSingleExtension() && profile.getShallowRows().isEmpty()) {
            profile.setShallowRows(new ArrayList<Integer>(profile.getSearchRows()));
        }
        buildPairRows(profile);
        return profile;
    }
    /**
     * 用旧的 row_lastno + slaveProperties 规则构建运行时画像。
     */
    private CrnDepthRuleProfile buildLegacyProfile(RowLastno rowLastno, Integer crnNo, Integer preferredNearRow) {
        CrnDepthRuleProfile profile = new CrnDepthRuleProfile();
        profile.setWhsType(rowLastno.getWhsType());
        profile.setCrnNo(crnNo);
        profile.setLayoutType(resolveLegacyLayoutType(rowLastno));
        profile.setSource(PROFILE_SOURCE_LEGACY);
        profile.setSearchRows(getLegacySearchRows(rowLastno, crnNo, preferredNearRow));
        if (profile.isSingleExtension()) {
            profile.setShallowRows(new ArrayList<Integer>(profile.getSearchRows()));
            profile.setDeepRows(new ArrayList<Integer>());
            return profile;
        }
        List<Integer> shallowRows = new ArrayList<Integer>();
        List<Integer> deepRows = new ArrayList<Integer>();
        for (Integer searchRow : profile.getSearchRows()) {
            if (searchRow == null) {
                continue;
            }
            if (Utils.isShallowLoc(slaveProperties, searchRow)) {
                shallowRows.add(searchRow);
                Integer deepRow = safeGetLegacyDeepRow(rowLastno, searchRow);
                if (deepRow != null) {
                    deepRows.add(deepRow);
                }
            } else {
                deepRows.add(searchRow);
            }
        }
        profile.setShallowRows(normalizeRows(shallowRows));
        profile.setDeepRows(normalizeRows(deepRows));
        buildPairRows(profile);
        return profile;
    }
    /**
     * 建立浅排与深排之间的对应关系,供双伸选位时配对使用。
     */
    private void buildPairRows(CrnDepthRuleProfile profile) {
        if (profile == null || !profile.isDoubleExtension()) {
            return;
        }
        for (Integer shallowRow : profile.getShallowRows()) {
            Integer deepRow = findNearestAdjacentRow(shallowRow, profile.getDeepRows());
            if (deepRow == null) {
                continue;
            }
            profile.getShallowToDeepRow().put(shallowRow, deepRow);
            profile.getDeepToShallowRow().put(deepRow, shallowRow);
        }
    }
    /**
     * 在候选排中找到与当前排距离最近的配对排。
     */
    private Integer findNearestAdjacentRow(Integer currentRow, List<Integer> candidateRows) {
        if (currentRow == null || Cools.isEmpty(candidateRows)) {
            return null;
        }
        Integer best = null;
        int bestDistance = Integer.MAX_VALUE;
        for (Integer candidateRow : candidateRows) {
            if (candidateRow == null) {
                continue;
            }
            int distance = Math.abs(candidateRow - currentRow);
            if (distance < bestDistance) {
                bestDistance = distance;
                best = candidateRow;
            }
            if (distance == 1) {
                return candidateRow;
            }
        }
        return best;
    }
    /**
     * 根据旧库型规则推断单伸/双伸布局类型。
     */
    private Integer resolveLegacyLayoutType(RowLastno rowLastno) {
        if (rowLastno == null) {
            return 1;
        }
        if (rowLastno.getTypeId() != null && rowLastno.getTypeId() == 2) {
            return 1;
        }
        return 2;
    }
    /**
     * 按旧逻辑推导某台堆垛机的搜索排顺序。
     */
    private List<Integer> getLegacySearchRows(RowLastno rowLastno, Integer crnNo, Integer preferredNearRow) {
        List<Integer> searchRows = new ArrayList<Integer>();
        if (rowLastno == null || crnNo == null) {
            return searchRows;
        }
        addRow(searchRows, preferredNearRow, rowLastno);
        int rowSpan = getLegacyRowSpan(rowLastno.getTypeId());
        int startCrnNo = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo();
        int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow();
        int crnOffset = crnNo - startCrnNo;
        if (crnOffset < 0) {
            return searchRows;
        }
        int crnStartRow = startRow + crnOffset * rowSpan;
        if (rowLastno.getTypeId() != null && rowLastno.getTypeId() == 2) {
            addRow(searchRows, crnStartRow, rowLastno);
            addRow(searchRows, crnStartRow + 1, rowLastno);
            return searchRows;
        }
        addRow(searchRows, crnStartRow + 1, rowLastno);
        addRow(searchRows, crnStartRow + 2, rowLastno);
        if (searchRows.isEmpty()) {
            for (int row = crnStartRow; row < crnStartRow + rowSpan; row++) {
                addRow(searchRows, row, rowLastno);
            }
        }
        return searchRows;
    }
    /**
     * 由旧浅排规则推断对应的深排。
     */
    private Integer safeGetLegacyDeepRow(RowLastno rowLastno, Integer shallowRow) {
        if (rowLastno == null || shallowRow == null) {
            return null;
        }
        try {
            String shallowLocNo = Utils.zerofill(String.valueOf(shallowRow), 2) + "00101";
            Integer deepRow = Utils.getRow(Utils.getDeepLoc(slaveProperties, shallowLocNo));
            if (deepRow < rowLastno.getsRow() || deepRow > rowLastno.geteRow()) {
                return null;
            }
            return deepRow;
        } catch (Exception ignored) {
            return null;
        }
    }
    /**
     * 取模板生成时的默认起始堆垛机号。
     */
    private Integer defaultStartCrnNo(RowLastno rowLastno) {
        return rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo();
    }
    /**
     * 取模板生成时的默认结束堆垛机号。
     */
    private Integer defaultEndCrnNo(RowLastno rowLastno) {
        if (rowLastno.geteCrnNo() != null && rowLastno.geteCrnNo() >= defaultStartCrnNo(rowLastno)) {
            return rowLastno.geteCrnNo();
        }
        if (rowLastno.getCrnQty() != null && rowLastno.getCrnQty() > 0) {
            return defaultStartCrnNo(rowLastno) + rowLastno.getCrnQty() - 1;
        }
        return defaultStartCrnNo(rowLastno);
    }
    /**
     * 按旧库型规则返回每台堆垛机占用的排跨度。
     */
    private int getLegacyRowSpan(Integer typeId) {
        if (typeId != null && typeId == 2) {
            return 2;
        }
        return 4;
    }
    /**
     * 向搜索排列表追加一个合法且不重复的排号。
     */
    private void addRow(List<Integer> searchRows, Integer row, RowLastno rowLastno) {
        if (row == null || rowLastno == null) {
            return;
        }
        if (row < rowLastno.getsRow() || row > rowLastno.geteRow()) {
            return;
        }
        if (!searchRows.contains(row)) {
            searchRows.add(row);
        }
    }
    /**
     * 去重并过滤非法排号。
     */
    private List<Integer> normalizeRows(List<Integer> rows) {
        LinkedHashSet<Integer> normalizedRows = new LinkedHashSet<Integer>();
        if (Cools.isEmpty(rows)) {
            return new ArrayList<Integer>();
        }
        for (Integer row : rows) {
            if (row != null && row > 0) {
                normalizedRows.add(row);
            }
        }
        return new ArrayList<Integer>(normalizedRows);
    }
    /**
     * 把 CSV/范围表达式解析成排号列表。
     */
    private List<Integer> parseRows(String rowsCsv) {
        List<Integer> rows = new ArrayList<Integer>();
        if (Cools.isEmpty(rowsCsv)) {
            return rows;
        }
        String normalized = rowsCsv.replace(",", ",")
                .replace(";", ";")
                .replace("、", ",")
                .replaceAll("\\s+", "");
        if (normalized.isEmpty()) {
            return rows;
        }
        String[] segments = normalized.split("[,;]");
        for (String segment : segments) {
            if (Cools.isEmpty(segment)) {
                continue;
            }
            if (segment.contains("-")) {
                String[] rangeParts = segment.split("-", 2);
                Integer startRow = safeParseInt(rangeParts[0]);
                Integer endRow = safeParseInt(rangeParts[1]);
                if (startRow == null || endRow == null) {
                    continue;
                }
                int step = startRow <= endRow ? 1 : -1;
                for (int row = startRow; step > 0 ? row <= endRow : row >= endRow; row += step) {
                    rows.add(row);
                }
                continue;
            }
            Integer row = safeParseInt(segment);
            if (row != null) {
                rows.add(row);
            }
        }
        return normalizeRows(rows);
    }
    /**
     * 安全地把字符串转换为整数,失败时返回 null。
     */
    private Integer safeParseInt(String value) {
        if (Cools.isEmpty(value)) {
            return null;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    /**
     * 把排号列表序列化成逗号分隔字符串。
     */
    private String joinRows(List<Integer> rows) {
        if (Cools.isEmpty(rows)) {
            return null;
        }
        StringBuilder builder = new StringBuilder();
        for (Integer row : rows) {
            if (row == null) {
                continue;
            }
            if (builder.length() > 0) {
                builder.append(",");
            }
            builder.append(row);
        }
        return builder.length() == 0 ? null : builder.toString();
    }
}
src/main/java/com/zy/asrs/service/impl/BasCrnpServiceImpl.java
@@ -22,6 +22,9 @@
    @Autowired
    private BasDevpService basDevpService;
    /**
     * 检查堆垛机基础可用状态,不满足时直接抛出业务异常。
     */
    @Override
    public BasCrnp checkSiteStatus(Integer crnId) {
        BasCrnp crnp = this.selectById(crnId);
@@ -37,6 +40,9 @@
        return crnp;
    }
    /**
     * 统一校验堆垛机是否可参与入库/出库分配。
     */
    @Override
    public boolean checkSiteError(Integer crnNo, boolean pakin) {
        BasCrnp crnp = this.selectById(crnNo);
@@ -44,15 +50,13 @@
//            log.error("{}号堆垛机不存在", crnNo);
            return false;
        }
        if (crnp.getCrnErr() != null && crnp.getCrnSts() != 3){
        if (crnp.getCrnSts() == null || crnp.getCrnSts() != 3) {
            log.error("{}号堆垛机非自动连线状态,无法作业!", crnNo);
            return false;
        }
        if (crnp.getCrnErr() != null) {
            if (crnp.getCrnErr() != 0) {
                log.error("{}号堆垛机异常,异常码{}", crnNo, crnp.getCrnErr());
                return false;
            }
        if (crnp.getCrnErr() != null && crnp.getCrnErr() != 0) {
            log.error("{}号堆垛机异常,异常码{}", crnNo, crnp.getCrnErr());
            return false;
        }
        if (pakin) {
src/main/java/com/zy/asrs/utils/Utils.java
@@ -149,9 +149,8 @@
     * <p>处理规则:
     * 1. 先根据入库站点查询所属库区。
     * 2. 先提取该库区内的堆垛机,并按可用空库位过滤不可用堆垛机。
     * 3. 若当前库区没有满足条件的空库位,再补充其他库区的堆垛机。
     * 4. 当 {@code locType1 = 1} 时,先返回低库位堆垛机,再把同批堆垛机的高库位追加到后面。
     * 5. 对不存在、故障、不可入以及无空库位的堆垛机直接剔除。
     * 3. 当 {@code locType1 = 1} 时,先返回低库位堆垛机,再把同批堆垛机的高库位追加到后面。
     * 4. 对不存在、故障、不可入以及无空库位的堆垛机直接剔除。
     *
     * <p>这里是只读排序工具,不再写回 {@code asr_row_lastno.crn_qty}。
     * {@code crn_qty} 在主数据里表示“堆垛机数量”,不能再被拿来当轮询游标,否则会把整仓配置写坏。
@@ -185,35 +184,29 @@
        // 先取当前库区对应的堆垛机。
        List<Integer> preferredCrnNos = getAreaCrnNos(storageArea, rowLastno);
        List<Integer> preferredAvailableCrnNos = getAvailableCrnNos(preferredCrnNos, locType1, emptyPallet, basCrnpService, locMastService);
        appendCrnLocTypeEntries(result, preferredAvailableCrnNos, locType1, locMastService);
        appendCrnLocTypeEntries(result, preferredAvailableCrnNos, locType1, emptyPallet, locMastService);
        // 当前库区没有可用容量时,再补充其他库区堆垛机。
        if (!hasAvailableCapacity(preferredCrnNos, locType1, basCrnpService, locMastService)) {
            List<Integer> otherAreaCrnNos = getOtherAreaCrnNos(storageArea, rowLastno);
            List<Integer> otherAvailableCrnNos = getAvailableCrnNos(otherAreaCrnNos, locType1, emptyPallet, basCrnpService, locMastService);
            appendCrnLocTypeEntries(result, otherAvailableCrnNos, locType1, locMastService);
        }
        return result;
    }
    private static void appendCrnLocTypeEntries(List<Map<String, Integer>> result, List<Integer> crnNos, Integer locType1, LocMastService locMastService) {
    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, locMastService);
            appendCrnLocTypeEntries(result, crnNos, (short) 2, locMastService);
            appendCrnLocTypeEntries(result, crnNos, (short) 1, emptyPallet, locMastService);
            appendCrnLocTypeEntries(result, crnNos, (short) 2, emptyPallet, locMastService);
            return;
        }
        appendCrnLocTypeEntries(result, crnNos, normalizedLocType1, locMastService);
        appendCrnLocTypeEntries(result, crnNos, normalizedLocType1, emptyPallet, locMastService);
        if (normalizedLocType1 == 1) {
            appendCrnLocTypeEntries(result, crnNos, (short) 2, locMastService);
            appendCrnLocTypeEntries(result, crnNos, (short) 2, emptyPallet, locMastService);
        }
    }
    private static void appendCrnLocTypeEntries(List<Map<String, Integer>> result, List<Integer> crnNos, Short targetLocType1, LocMastService locMastService) {
    private static void appendCrnLocTypeEntries(List<Map<String, Integer>> result, List<Integer> crnNos, Short targetLocType1, boolean emptyPallet, LocMastService locMastService) {
        if (targetLocType1 == null || Cools.isEmpty(crnNos)) {
            return;
        }
        for (Integer crnNo : crnNos) {
            if (!hasAvailableLoc(crnNo, targetLocType1, locMastService) || containsCrnLocType(result, crnNo, targetLocType1)) {
            if (!hasAvailableLoc(crnNo, targetLocType1, emptyPallet, locMastService) || containsCrnLocType(result, crnNo, targetLocType1)) {
                continue;
            }
            Map<String, Integer> item = new LinkedHashMap<>();
@@ -249,7 +242,7 @@
            if (crnNo == null || !basCrnpService.checkSiteError(crnNo, true)) {
                continue;
            }
            if (!hasAvailableLocForRequest(crnNo, locType1, locMastService)) {
            if (!hasAvailableLocForRequest(crnNo, locType1, emptyPallet, locMastService)) {
                continue;
            }
            availableCrnNos.add(crnNo);
@@ -274,25 +267,30 @@
        return "Y".equalsIgnoreCase(basCrnp.getEmpIn()) ? 1 : 0;
    }
    private static boolean hasAvailableLocForRequest(Integer crnNo, Integer locType1, LocMastService locMastService) {
    private static boolean hasAvailableLocForRequest(Integer crnNo, Integer locType1, boolean emptyPallet, LocMastService locMastService) {
        Short normalizedLocType1 = normalizeLocType1(locType1);
        if (normalizedLocType1 == null) {
            return hasAvailableLoc(crnNo, (short) 1, locMastService) || hasAvailableLoc(crnNo, (short) 2, locMastService);
            return hasAvailableLoc(crnNo, (short) 1, emptyPallet, locMastService)
                    || hasAvailableLoc(crnNo, (short) 2, emptyPallet, locMastService);
        }
        if (hasAvailableLoc(crnNo, normalizedLocType1, locMastService)) {
        if (hasAvailableLoc(crnNo, normalizedLocType1, emptyPallet, locMastService)) {
            return true;
        }
        return normalizedLocType1 == 1 && hasAvailableLoc(crnNo, (short) 2, locMastService);
        return normalizedLocType1 == 1 && hasAvailableLoc(crnNo, (short) 2, emptyPallet, locMastService);
    }
    private static boolean hasAvailableLoc(Integer crnNo, Short locType1, LocMastService locMastService) {
    private static boolean hasAvailableLoc(Integer crnNo, Short locType1, boolean emptyPallet, LocMastService locMastService) {
        if (crnNo == null || locType1 == null) {
            return false;
        }
        return locMastService.selectCount(new EntityWrapper<LocMast>()
                .eq("crn_no", crnNo)
                .eq("loc_sts", "O")
                .eq("loc_type1", locType1)) > 0;
        EntityWrapper<LocMast> wrapper = new EntityWrapper<LocMast>();
        wrapper.eq("crn_no", crnNo);
        wrapper.eq("loc_sts", "O");
        wrapper.eq("loc_type1", locType1);
        if (!emptyPallet) {
            wrapper.ne("loc_type2", 1);
        }
        return locMastService.selectCount(wrapper) > 0;
    }
    private static Short normalizeLocType1(Integer locType1) {
src/main/java/com/zy/common/model/CrnDepthRuleProfile.java
New file
@@ -0,0 +1,54 @@
package com.zy.common.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Data
public class CrnDepthRuleProfile {
    private Integer whsType;
    private Integer crnNo;
    private Integer layoutType;
    private String source;
    private List<Integer> searchRows = new ArrayList<Integer>();
    private List<Integer> shallowRows = new ArrayList<Integer>();
    private List<Integer> deepRows = new ArrayList<Integer>();
    private Map<Integer, Integer> shallowToDeepRow = new LinkedHashMap<Integer, Integer>();
    private Map<Integer, Integer> deepToShallowRow = new LinkedHashMap<Integer, Integer>();
    public boolean isSingleExtension() {
        return Integer.valueOf(1).equals(this.layoutType);
    }
    public boolean isDoubleExtension() {
        return Integer.valueOf(2).equals(this.layoutType);
    }
    public boolean isShallowRow(Integer row) {
        return row != null && this.shallowRows.contains(row);
    }
    public boolean isDeepRow(Integer row) {
        return row != null && this.deepRows.contains(row);
    }
    public Integer getPairedDeepRow(Integer shallowRow) {
        return this.shallowToDeepRow.get(shallowRow);
    }
    public Integer getPairedShallowRow(Integer deepRow) {
        return this.deepToShallowRow.get(deepRow);
    }
}
src/main/java/com/zy/common/service/CommonService.java
@@ -7,12 +7,14 @@
import com.core.common.Cools;
import com.core.exception.CoolException;
import com.zy.asrs.entity.*;
import com.zy.asrs.entity.param.BasCrnDepthRuleRuntimePreviewParam;
import com.zy.asrs.entity.result.FindLocNoAttributeVo;
import com.zy.asrs.entity.result.KeyValueVo;
import com.zy.asrs.service.*;
import com.zy.asrs.utils.Utils;
import com.zy.asrs.utils.VersionUtils;
import com.zy.common.entity.Parameter;
import com.zy.common.model.CrnDepthRuleProfile;
import com.zy.common.model.LocTypeDto;
import com.zy.common.model.Shelves;
import com.zy.common.model.StartupDto;
@@ -59,6 +61,28 @@
        }
    }
    private static class Run2SearchResult {
        private final LocMast locMast;
        private final RowLastno rowLastno;
        private final List<Integer> runnableCrnNos;
        private Run2SearchResult(LocMast locMast, RowLastno rowLastno, List<Integer> runnableCrnNos) {
            this.locMast = locMast;
            this.rowLastno = rowLastno;
            this.runnableCrnNos = runnableCrnNos;
        }
    }
    private static class Run2Cursor {
        private final int currentRow;
        private final Integer currentCrnNo;
        private Run2Cursor(int currentRow, Integer currentCrnNo) {
            this.currentRow = currentRow;
            this.currentCrnNo = currentCrnNo;
        }
    }
    @Autowired
    private WrkMastService wrkMastService;
    @Autowired
@@ -81,6 +105,8 @@
    private SlaveProperties slaveProperties;
    @Autowired
    private WrkDetlService wrkDetlService;
    @Autowired
    private BasCrnDepthRuleService basCrnDepthRuleService;
    /**
     * 生成工作号
@@ -206,25 +232,91 @@
     *
     * 空托盘的库位策略有两段:
     * 1. 首轮只限制 loc_type2=1,表示优先找窄库位。
     * 2. 首轮不限制 loc_type1,高低位都允许参与搜索。
     * 2. loc_type1 高度信息必须保留,后续再按低位向高位兼容。
     *
     * 这样做的原因是现场口径已经改成“先找窄库位”,而不是“先找低位窄库位”。
     * 因此这里会主动清空 locType1,防止被站点默认值带成低位优先。
     * 非空托盘只保留 loc_type1,满托找位不再使用 loc_type2/loc_type3 过滤。
     */
    private LocTypeDto normalizeLocTypeDto(Integer staDescId, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto) {
        LocTypeDto normalizedLocTypeDto = locTypeDto == null ? new LocTypeDto() : locTypeDto;
        if (!isEmptyPalletRequest(staDescId, findLocNoAttributeVo)) {
            return locTypeDto;
            normalizedLocTypeDto.setLocType2(null);
            normalizedLocTypeDto.setLocType3(null);
            return normalizedLocTypeDto;
        }
        if (findLocNoAttributeVo != null && Cools.isEmpty(findLocNoAttributeVo.getMatnr())) {
            findLocNoAttributeVo.setMatnr("emptyPallet");
        }
        LocTypeDto normalizedLocTypeDto = locTypeDto == null ? new LocTypeDto() : locTypeDto;
        // 空托盘首轮不限制高低位,只保留“窄库位优先”的约束。
        normalizedLocTypeDto.setLocType1(null);
        normalizedLocTypeDto.setLocType2((short) 1);
        return normalizedLocTypeDto;
    }
    private LocTypeDto copyLocTypeDto(LocTypeDto locTypeDto) {
        if (locTypeDto == null) {
            return null;
        }
        LocTypeDto copied = new LocTypeDto();
        copied.setLocType1(locTypeDto.getLocType1());
        copied.setLocType2(locTypeDto.getLocType2());
        copied.setLocType3(locTypeDto.getLocType3());
        copied.setSiteId(locTypeDto.getSiteId());
        return copied;
    }
    /**
     * 空托盘固定按 4 段式找位:
     * 1. 严格高度 + narrow
     * 2. 严格高度 + any locType2
     * 3. 向上兼容高度 + narrow
     * 4. 向上兼容高度 + any locType2
     */
    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);
            }
        }
        return new ArrayList<LocTypeDto>(searchLocTypes);
    }
    private String buildEmptyPalletStageCode(LocTypeDto baseLocTypeDto, LocTypeDto stageLocTypeDto) {
        boolean compatibleHeight = baseLocTypeDto != null
                && baseLocTypeDto.getLocType1() != null
                && stageLocTypeDto != null
                && stageLocTypeDto.getLocType1() != null
                && !baseLocTypeDto.getLocType1().equals(stageLocTypeDto.getLocType1());
        boolean narrowOnly = stageLocTypeDto != null
                && stageLocTypeDto.getLocType2() != null
                && stageLocTypeDto.getLocType2() == 1;
        return (compatibleHeight ? "compatible-height" : "strict-height")
                + "-"
                + (narrowOnly ? "narrow-only" : "all-locType2");
    }
    /**
     * 判断当前规格是否属于满托找位。
     *
     * 满托只参考 loc_type1,但仍需显式排除 loc_type2=1 的窄库位。
     */
    private boolean isFullPalletLocTypeSearch(LocTypeDto locTypeDto) {
        return locTypeDto != null && locTypeDto.getLocType2() == null && locTypeDto.getLocType3() == null;
    }
    /**
     * 把 locType 条件追加到库位查询条件里。
     */
    private Wrapper<LocMast> applyLocTypeFilters(Wrapper<LocMast> wrapper, LocTypeDto locTypeDto, boolean includeLocType1) {
        if (wrapper == null || locTypeDto == null) {
            return wrapper;
@@ -232,15 +324,21 @@
        if (includeLocType1 && locTypeDto.getLocType1() != null && locTypeDto.getLocType1() > 0) {
            wrapper.eq("loc_type1", locTypeDto.getLocType1());
        }
        if (isFullPalletLocTypeSearch(locTypeDto)) {
            wrapper.eq("loc_type2", 0);
        }
        if (locTypeDto.getLocType2() != null && locTypeDto.getLocType2() > 0) {
            wrapper.eq("loc_type2", locTypeDto.getLocType2());
        }
        if (locTypeDto.getLocType3() != null && locTypeDto.getLocType3() > 0) {
            wrapper.eq("loc_type3", locTypeDto.getLocType3());
        }
//        if (locTypeDto.getLocType3() != null && locTypeDto.getLocType3() > 0) {
//            wrapper.eq("loc_type3", locTypeDto.getLocType3());
//        }
        return wrapper;
    }
    /**
     * 解析本次找位应优先使用的库区,站点绑定优先于接口传参。
     */
    private Integer resolvePreferredArea(Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo) {
        BasDevp sourceStation = basDevpService.selectById(sourceStaNo);
        Integer stationArea = parseArea(sourceStation == null ? null : sourceStation.getArea());
@@ -254,6 +352,9 @@
        return null;
    }
    /**
     * 把站点维护的库区值统一解析成 1/2/3。
     */
    private Integer parseArea(String area) {
        if (Cools.isEmpty(area)) {
            return null;
@@ -280,6 +381,9 @@
        return null;
    }
    /**
     * 读取 AGV 各库区对应的排配置。
     */
    private String getAgvAreaRowsConfig(Integer area) {
        Parameter parameter = Parameter.get();
        if (parameter == null || area == null) {
@@ -297,6 +401,9 @@
        }
    }
    /**
     * 解析 AGV 指定库区的排顺序,缺配置时回退旧默认排。
     */
    private List<Integer> getAgvAreaRows(Integer area, RowLastno rowLastno) {
        List<Integer> configuredRows = parseAgvRows(getAgvAreaRowsConfig(area), rowLastno);
        if (!configuredRows.isEmpty()) {
@@ -305,6 +412,9 @@
        return getLegacyAgvRows(rowLastno);
    }
    /**
     * 汇总 AGV 所有库区的回退排顺序。
     */
    private List<Integer> getAgvFallbackRows(RowLastno rowLastno) {
        LinkedHashSet<Integer> rows = new LinkedHashSet<>();
        for (int area = 1; area <= 3; area++) {
@@ -314,6 +424,9 @@
        return new ArrayList<>(rows);
    }
    /**
     * 把 AGV 库区排配置解析成有序排号列表。
     */
    private List<Integer> parseAgvRows(String configValue, RowLastno rowLastno) {
        List<Integer> rows = new ArrayList<>();
        if (rowLastno == null || Cools.isEmpty(configValue)) {
@@ -350,6 +463,9 @@
        return rows;
    }
    /**
     * 返回 AGV 旧逻辑使用的默认排顺序。
     */
    private List<Integer> getLegacyAgvRows(RowLastno rowLastno) {
        List<Integer> rows = new ArrayList<>();
        if (rowLastno == null) {
@@ -371,6 +487,9 @@
        return rows;
    }
    /**
     * 仅在排号落在仓库范围内时追加到 AGV 排列表。
     */
    private void addAgvRow(LinkedHashSet<Integer> rows, Integer row, RowLastno rowLastno) {
        if (rows == null || row == null || rowLastno == null) {
            return;
@@ -381,6 +500,9 @@
        rows.add(row);
    }
    /**
     * 安全解析整数配置,失败时返回 null。
     */
    private Integer safeParseInt(String value) {
        if (Cools.isEmpty(value)) {
            return null;
@@ -392,6 +514,9 @@
        }
    }
    /**
     * 返回 AGV 各库区对应的 bay 范围。
     */
    private int[] getAgvAreaBayRange(Integer area) {
        if (area == null) {
            return new int[]{1, 19};
@@ -408,6 +533,9 @@
        }
    }
    /**
     * 按指定排和 bay 范围顺序搜索 AGV 可用库位。
     */
    private LocMast findAgvLocByRows(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> rows,
                                     int startBay, int endBay, int curRow, int nearRow,
                                     LocTypeDto locTypeDto, boolean useDeepCheck) {
@@ -442,6 +570,10 @@
        }
        return null;
    }
    /**
     * 读取 run2 各库区对应的排配置,缺失时复用 AGV 库区配置。
     */
    private String getRun2AreaRowsConfig(Integer area) {
        Parameter parameter = Parameter.get();
        if (parameter == null || area == null) {
@@ -464,6 +596,9 @@
        return Cools.isEmpty(run2Config) ? getAgvAreaRowsConfig(area) : run2Config;
    }
    /**
     * 解析 run2 指定库区的排顺序,缺配置时回退旧默认排。
     */
    private List<Integer> getRun2AreaRows(Integer area, RowLastno rowLastno) {
        List<Integer> configuredRows = parseAgvRows(getRun2AreaRowsConfig(area), rowLastno);
        if (!configuredRows.isEmpty()) {
@@ -472,6 +607,9 @@
        return getLegacyAgvRows(rowLastno);
    }
    /**
     * 汇总 run2 其它库区的回退排顺序。
     */
    private List<Integer> getRun2FallbackRows(RowLastno rowLastno) {
        LinkedHashSet<Integer> rows = new LinkedHashSet<>();
        for (int area = 1; area <= 3; area++) {
@@ -481,29 +619,103 @@
        return new ArrayList<>(rows);
    }
    /**
     * 解析 run2 的起始堆垛机号,优先使用 currentCrnNo。
     */
    private Integer resolveRun2CrnNo(RowLastno rowLastno) {
        if (rowLastno == null) {
            return null;
        }
        Integer currentRow = rowLastno.getCurrentRow();
        Integer startCrnNo = getRun2StartCrnNo(rowLastno);
        Integer endCrnNo = getRun2EndCrnNo(rowLastno);
        Integer currentCrnNo = rowLastno.getCurrentCrnNo();
        if (currentCrnNo == null || currentCrnNo <= 0 || currentCrnNo < startCrnNo) {
            return startCrnNo;
        }
        if (endCrnNo != null && currentCrnNo > endCrnNo) {
            return startCrnNo;
        }
        return currentCrnNo;
    }
    private Integer getRun2StartCrnNo(RowLastno rowLastno) {
        if (rowLastno == null || rowLastno.getsCrnNo() == null) {
            return 1;
        }
        return rowLastno.getsCrnNo();
    }
    private Integer getRun2EndCrnNo(RowLastno rowLastno) {
        if (rowLastno == null) {
            return null;
        }
        Integer startCrnNo = getRun2StartCrnNo(rowLastno);
        if (rowLastno.geteCrnNo() != null && rowLastno.geteCrnNo() >= startCrnNo) {
            return rowLastno.geteCrnNo();
        }
        int crnCount = resolveCrnCount(rowLastno);
        if (crnCount <= 0) {
            return startCrnNo;
        }
        return startCrnNo + crnCount - 1;
    }
    /**
     * 兼容旧 currentRow 游标,把排号换算成对应的堆垛机号。
     */
    private Integer resolveRun2CrnNoByCurrentRow(RowLastno rowLastno, Integer currentRow) {
        if (rowLastno == null) {
            return null;
        }
        Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId());
        if (rowSpan == null || rowSpan <= 0) {
            rowSpan = 2;
        }
        int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow();
        int startCrnNo = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo();
        if (currentRow == null) {
        int startCrnNo = getRun2StartCrnNo(rowLastno);
        if (currentRow == null || currentRow <= 0) {
            return startCrnNo;
        }
        int offset = Math.max(currentRow - startRow, 0) / rowSpan;
        int crnNo = startCrnNo + offset;
        Integer endCrnNo = rowLastno.geteCrnNo();
        Integer endCrnNo = getRun2EndCrnNo(rowLastno);
        if (endCrnNo != null && crnNo > endCrnNo) {
            return startCrnNo;
        }
        return crnNo;
    }
    private Integer getNextSequentialRun2CrnNo(RowLastno rowLastno, Integer currentCrnNo) {
        if (rowLastno == null) {
            return null;
        }
        Integer startCrnNo = getRun2StartCrnNo(rowLastno);
        Integer endCrnNo = getRun2EndCrnNo(rowLastno);
        if (currentCrnNo == null || currentCrnNo < startCrnNo) {
            return startCrnNo;
        }
        if (endCrnNo != null && currentCrnNo >= endCrnNo) {
            return startCrnNo;
        }
        return currentCrnNo + 1;
    }
    /**
     * 解析 run2 下一轮应从哪台堆垛机开始。
     */
    private Integer getNextRun2CurrentCrnNo(RowLastno rowLastno, List<Integer> runnableCrnNos, Integer selectedCrnNo) {
        if (!Cools.isEmpty(runnableCrnNos) && selectedCrnNo != null) {
            int index = runnableCrnNos.indexOf(selectedCrnNo);
            if (index >= 0) {
                return runnableCrnNos.get((index + 1) % runnableCrnNos.size());
            }
        }
        return getNextSequentialRun2CrnNo(rowLastno, resolveRun2CrnNo(rowLastno));
    }
    /**
     * 在整仓轮询模式下推进到下一台堆垛机对应的起始排。
     */
    private int getNextRun2CurrentRow(RowLastno rowLastno, int currentRow) {
        Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId());
        if (rowSpan == null || rowSpan <= 0) {
@@ -518,6 +730,9 @@
        return currentRow + rowSpan;
    }
    /**
     * 从指定起点开始生成完整的堆垛机轮询顺序。
     */
    private List<Integer> getOrderedCrnNos(RowLastno rowLastno, Integer startCrnNo) {
        List<Integer> orderedCrnNos = new ArrayList<>();
        if (rowLastno == null) {
@@ -579,6 +794,26 @@
    }
    /**
     * 按“可入 + 自动连线 + 无故障”判断堆垛机是否在线可分配。
     */
    private boolean isCrnActive(Integer crnNo) {
        if (crnNo == null) {
            return false;
        }
        BasCrnp basCrnp = basCrnpService.selectById(crnNo);
        if (Cools.isEmpty(basCrnp)) {
            return false;
        }
        if (!"Y".equalsIgnoreCase(basCrnp.getInEnable())) {
            return false;
        }
        if (basCrnp.getCrnSts() == null || basCrnp.getCrnSts() != 3) {
            return false;
        }
        return basCrnp.getCrnErr() == null || basCrnp.getCrnErr() == 0;
    }
    /**
     * 判断某台堆垛机是否可以参与 run2 入库找位。
     *
     * routeRequired=true:
@@ -593,20 +828,7 @@
    }
    private boolean canRun2CrnAcceptPakin(RowLastno rowLastno, Integer staDescId, Integer sourceStaNo, Integer crnNo, boolean routeRequired) {
        if (crnNo == null) {
            return false;
        }
        BasCrnp basCrnp = basCrnpService.selectById(crnNo);
        if (Cools.isEmpty(basCrnp)) {
            return false;
        }
        if (!"Y".equals(basCrnp.getInEnable())) {
            return false;
        }
        if (basCrnp.getCrnSts() != null && basCrnp.getCrnSts() != 3) {
            return false;
        }
        if (basCrnp.getCrnErr() != null && basCrnp.getCrnErr() != 0) {
        if (!isCrnActive(crnNo)) {
            return false;
        }
        if (!routeRequired) {
@@ -619,11 +841,7 @@
                .eq("type_no", staDescId)
                .eq("stn_no", sourceStaNo)
                .eq("crn_no", crnNo));
        if (Cools.isEmpty(staDesc)) {
            return false;
        }
        BasDevp targetSta = basDevpService.selectById(staDesc.getCrnStn());
        return !Cools.isEmpty(targetSta) && "Y".equals(targetSta.getAutoing());
        return !Cools.isEmpty(staDesc) && !Cools.isEmpty(staDesc.getCrnStn());
    }
    /**
@@ -662,6 +880,33 @@
        return nextRow == null ? getNextRun2CurrentRow(rowLastno, currentRow) : nextRow;
    }
    private Run2Cursor getNextRun2Cursor(RowLastno rowLastno, List<Integer> runnableCrnNos, Integer selectedCrnNo, int currentRow) {
        if (rowLastno == null) {
            return null;
        }
        Integer nextCrnNo = getNextRun2CurrentCrnNo(rowLastno, runnableCrnNos, selectedCrnNo);
        Integer nextRow = nextCrnNo == null ? null : getCrnStartRow(rowLastno, nextCrnNo);
        if (nextRow == null) {
            int baseRow = currentRow == 0 ? (rowLastno.getsRow() == null ? 1 : rowLastno.getsRow()) : currentRow;
            nextRow = getNextRun2CurrentRow(rowLastno, baseRow);
            nextCrnNo = resolveRun2CrnNoByCurrentRow(rowLastno, nextRow);
        }
        return new Run2Cursor(nextRow, nextCrnNo);
    }
    private void updateRun2Cursor(RowLastno rowLastno, List<Integer> runnableCrnNos, Integer selectedCrnNo, int currentRow) {
        if (rowLastno == null) {
            return;
        }
        Run2Cursor nextCursor = getNextRun2Cursor(rowLastno, runnableCrnNos, selectedCrnNo, currentRow);
        if (nextCursor == null) {
            return;
        }
        rowLastno.setCurrentRow(nextCursor.currentRow);
        rowLastno.setCurrentCrnNo(nextCursor.currentCrnNo);
        rowLastnoService.updateById(rowLastno);
    }
    /**
     * 构造空托盘跨库区搜索顺序:
     * 先当前库区,再依次补足其它库区,避免重复。
@@ -675,6 +920,124 @@
            areaOrder.add(area);
        }
        return new ArrayList<>(areaOrder);
    }
    /**
     * 预览 run2 当前会参与的库区、堆垛机顺序和深浅排画像,不落任务档。
     */
    public Map<String, Object> previewRun2Allocation(BasCrnDepthRuleRuntimePreviewParam param) {
        if (param == null || param.getStaDescId() == null || param.getSourceStaNo() == null) {
            throw new CoolException("路径ID和源站不能为空");
        }
        FindLocNoAttributeVo findLocNoAttributeVo = new FindLocNoAttributeVo();
        findLocNoAttributeVo.setMatnr(param.getMatnr());
        findLocNoAttributeVo.setOutArea(param.getOutArea());
        LocTypeDto locTypeDto = new LocTypeDto();
        locTypeDto.setLocType1(param.getLocType1());
        locTypeDto.setLocType2(param.getLocType2());
        locTypeDto.setLocType3(param.getLocType3());
        locTypeDto = normalizeLocTypeDto(param.getStaDescId(), findLocNoAttributeVo, locTypeDto);
        Integer whsType = Utils.GetWhsType(param.getSourceStaNo());
        RowLastno rowLastno = rowLastnoService.selectById(whsType);
        if (Cools.isEmpty(rowLastno)) {
            throw new CoolException("未找到仓库轮询规则");
        }
        RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId());
        Integer preferredArea = resolvePreferredArea(param.getSourceStaNo(), findLocNoAttributeVo);
        boolean emptyPalletRequest = isEmptyPalletRequest(param.getStaDescId(), findLocNoAttributeVo);
        Map<String, Object> result = new HashMap<String, Object>();
        result.put("whsType", whsType);
        result.put("preferredArea", preferredArea);
        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()));
        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);
        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);
                }
                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);
            }
            result.put("areaSearchOrder", areaSearchOrder);
            result.put("searchStages", searchStages);
            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");
        return result;
    }
    /**
     * 组装某批堆垛机的运行时画像预览数据。
     */
    private List<Map<String, Object>> buildRun2ProfilePreview(RowLastno rowLastno, RowLastnoType rowLastnoType, List<Integer> crnNos,
                                                              Integer staDescId, Integer sourceStaNo, LocTypeDto locTypeDto) {
        List<Map<String, Object>> profiles = new ArrayList<Map<String, Object>>();
        if (Cools.isEmpty(crnNos)) {
            return profiles;
        }
        for (Integer crnNo : crnNos) {
            CrnDepthRuleProfile profile = basCrnDepthRuleService.resolveProfile(rowLastno, crnNo, getCrnStartRow(rowLastno, crnNo));
            Map<String, Object> item = new HashMap<String, Object>();
            item.put("crnNo", crnNo);
            item.put("active", isCrnActive(crnNo));
            item.put("targetStaNo", resolveTargetStaNo(rowLastno, staDescId, sourceStaNo, crnNo));
            item.put("layoutType", profile == null ? null : profile.getLayoutType());
            item.put("source", profile == null ? null : profile.getSource());
            item.put("searchRows", profile == null ? null : profile.getSearchRows());
            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);
            item.put("firstMatchLocNo", firstMatchLoc == null ? null : firstMatchLoc.getLocNo());
            item.put("assignableLocCount", countAssignableLocForCrn(rowLastno, rowLastnoType, crnNo,
                    getCrnStartRow(rowLastno, crnNo) == null ? 0 : getCrnStartRow(rowLastno, crnNo), locTypeDto));
            profiles.add(item);
        }
        return profiles;
    }
    /**
@@ -696,15 +1059,29 @@
     * 空托盘 run2 专用搜索链路。
     *
     * 执行顺序:
     * 1. 先按站点绑定库区找 loc_type2=1。
     * 2. 当前库区没有,再按其它库区继续找 loc_type2=1。
     * 1. 先按固定规格阶段构造 4 段式 locType 回退顺序。
     * 2. 每个规格阶段都按“当前库区 -> 其它库区”的顺序搜索。
     * 3. 每个库区内部都按该库区自己的 rowLastno/currentRow 做轮询均分。
     *
     * 这里故意不复用普通 run2 的“推荐排 -> 当前库区排 -> 其它排”逻辑,
     * 因为空托盘的业务口径已经切换成“按库区找堆垛机”,不是按推荐排找巷道。
     */
    private Run2AreaSearchResult findEmptyPalletRun2Loc(RowLastno defaultRowLastno, Integer staDescId, Integer sourceStaNo,
                                                        StartupDto startupDto, Integer preferredArea, LocTypeDto locTypeDto) {
        for (LocTypeDto stageLocTypeDto : buildEmptyPalletSearchLocTypes(locTypeDto)) {
            String stageCode = buildEmptyPalletStageCode(locTypeDto, stageLocTypeDto);
            Run2AreaSearchResult searchResult = findEmptyPalletRun2AreaLoc(defaultRowLastno, staDescId, sourceStaNo,
                    startupDto, preferredArea, stageLocTypeDto, stageCode);
            if (!Cools.isEmpty(searchResult) && !Cools.isEmpty(searchResult.locMast)) {
                return searchResult;
            }
        }
        return null;
    }
    private Run2AreaSearchResult findEmptyPalletRun2AreaLoc(RowLastno defaultRowLastno, Integer staDescId, Integer sourceStaNo,
                                                            StartupDto startupDto, Integer preferredArea, LocTypeDto locTypeDto) {
                                                            StartupDto startupDto, Integer preferredArea, LocTypeDto locTypeDto,
                                                            String stageCode) {
        for (Integer area : buildAreaSearchOrder(preferredArea)) {
            RowLastno areaRowLastno = getAreaRowLastno(area, defaultRowLastno);
            if (Cools.isEmpty(areaRowLastno)) {
@@ -718,14 +1095,13 @@
            List<Integer> orderedAreaCrnNos = getOrderedCrnNos(areaRowLastno, areaStartCrnNo);
            // 空托盘跨库区时只筛设备主档和故障状态,不强依赖 sta_desc。
            List<Integer> runnableAreaCrnNos = getOrderedRunnableRun2CrnNos(areaRowLastno, staDescId, sourceStaNo, orderedAreaCrnNos, false);
            List<Integer> candidateCrnNos = Cools.isEmpty(runnableAreaCrnNos) ? orderedAreaCrnNos : runnableAreaCrnNos;
            if (Cools.isEmpty(candidateCrnNos)) {
            if (Cools.isEmpty(runnableAreaCrnNos)) {
                continue;
            }
            LocMast locMast = findRun2EmptyLocByCrnNos(areaRowLastno, areaRowLastnoType, candidateCrnNos, locTypeDto,
                    staDescId, sourceStaNo, startupDto, area, "empty-pallet-area-" + area, false);
            LocMast locMast = findRun2EmptyLocByCrnNos(areaRowLastno, areaRowLastnoType, runnableAreaCrnNos, locTypeDto,
                    staDescId, sourceStaNo, startupDto, area, stageCode + "-area-" + area, false);
            if (!Cools.isEmpty(locMast)) {
                return new Run2AreaSearchResult(locMast, areaRowLastno, candidateCrnNos);
                return new Run2AreaSearchResult(locMast, areaRowLastno, runnableAreaCrnNos);
            }
        }
        return null;
@@ -740,12 +1116,8 @@
            return;
        }
        RowLastno updateRowLastno = searchResult.rowLastno;
        int updateCurRow = updateRowLastno.getCurrentRow() == null || updateRowLastno.getCurrentRow() == 0
                ? (updateRowLastno.getsRow() == null ? 1 : updateRowLastno.getsRow())
                : updateRowLastno.getCurrentRow();
        updateCurRow = getNextRun2CurrentRow(updateRowLastno, searchResult.runnableCrnNos, locMast.getCrnNo(), updateCurRow);
        updateRowLastno.setCurrentRow(updateCurRow);
        rowLastnoService.updateById(updateRowLastno);
        int currentRow = updateRowLastno.getCurrentRow() == null ? 0 : updateRowLastno.getCurrentRow();
        updateRun2Cursor(updateRowLastno, searchResult.runnableCrnNos, locMast.getCrnNo(), currentRow);
    }
    /**
@@ -754,12 +1126,11 @@
     * 执行顺序:
     * 1. 先看站点是否配置了“堆垛机 + 库位类型”优先级。
     * 2. 没配库区时,直接按当前 run2 轮询顺序找位。
     * 3. 配了库区时,先在当前库区找,再回退到其它库区。
     * 3. 配了库区时,只在当前库区查找,普通托盘不跨库区兜底。
     */
    private LocMast findNormalRun2Loc(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer sourceStaNo,
                                      Integer staDescId, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto,
                                      StartupDto startupDto, Integer preferredArea, List<Integer> orderedCrnNos,
                                      List<Integer> triedCrnNos) {
    private Run2SearchResult findNormalRun2Loc(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer sourceStaNo,
                                               Integer staDescId, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto,
                                               StartupDto startupDto, Integer preferredArea, List<Integer> orderedCrnNos) {
        List<Map<String, Integer>> stationCrnLocTypes = Utils.getStationStorageAreaName(
                sourceStaNo,
                locTypeDto == null || locTypeDto.getLocType1() == null ? null : locTypeDto.getLocType1().intValue(),
@@ -770,34 +1141,27 @@
            LocMast locMast = findRun2EmptyLocByCrnLocTypeEntries(rowLastno, rowLastnoType, stationCrnLocTypes,
                    locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "station-priority");
            if (!Cools.isEmpty(locMast)) {
                return locMast;
                return new Run2SearchResult(locMast, rowLastno,
                        getOrderedRunnableRun2CrnNos(rowLastno, staDescId, sourceStaNo, extractCrnNos(stationCrnLocTypes)));
            }
        }
        List<Integer> candidateCrnNos;
        if (preferredArea == null) {
            List<Integer> defaultCrnNos = new ArrayList<>(orderedCrnNos);
            defaultCrnNos.removeAll(triedCrnNos);
            return findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, defaultCrnNos, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, "default");
            candidateCrnNos = new ArrayList<>(orderedCrnNos);
        } else {
            candidateCrnNos = filterCrnNosByRows(rowLastno, orderedCrnNos, getRun2AreaRows(preferredArea, rowLastno));
        }
        List<Integer> preferredCrnNos = filterCrnNosByRows(rowLastno, orderedCrnNos, getRun2AreaRows(preferredArea, rowLastno));
        preferredCrnNos.removeAll(triedCrnNos);
        LocMast locMast = findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, preferredCrnNos, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, "preferred-area");
        if (!Cools.isEmpty(locMast)) {
            return locMast;
        }
        List<Integer> fallbackCrnNos = filterCrnNosByRows(rowLastno, orderedCrnNos, getRun2FallbackRows(rowLastno));
        fallbackCrnNos.removeAll(triedCrnNos);
        fallbackCrnNos.removeAll(preferredCrnNos);
        return findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, fallbackCrnNos, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, "fallback-area");
        List<Integer> runnableCrnNos = getOrderedRunnableRun2CrnNos(rowLastno, staDescId, sourceStaNo, candidateCrnNos);
        LocMast locMast = findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, preferredArea == null ? "default" : "preferred-area");
        return new Run2SearchResult(locMast, rowLastno, runnableCrnNos);
    }
    /**
     * 普通物料命中库位后,沿用 run2 原有的全仓轮询游标推进方式。
     */
    private void advanceNormalRun2Cursor(RowLastno rowLastno, int curRow) {
        advanceNormalRun2Cursor(rowLastno, curRow, null, null);
        updateRun2Cursor(rowLastno, null, null, curRow);
    }
    /**
@@ -807,19 +1171,12 @@
     * 满板任务也会在真实可作业的堆垛机之间轮询,不会因为理论堆垛机号的空洞而长期回落到同一台。
     */
    private void advanceNormalRun2Cursor(RowLastno rowLastno, int curRow, List<Integer> runnableCrnNos, Integer selectedCrnNo) {
        if (rowLastno == null) {
            return;
        }
        int updateCurRow = curRow == 0 ? (rowLastno.getsRow() == null ? 1 : rowLastno.getsRow()) : curRow;
        if (!Cools.isEmpty(runnableCrnNos) && selectedCrnNo != null) {
            updateCurRow = getNextRun2CurrentRow(rowLastno, runnableCrnNos, selectedCrnNo, updateCurRow);
        } else {
            updateCurRow = getNextRun2CurrentRow(rowLastno, updateCurRow);
        }
        rowLastno.setCurrentRow(updateCurRow);
        rowLastnoService.updateById(rowLastno);
        updateRun2Cursor(rowLastno, runnableCrnNos, selectedCrnNo, curRow);
    }
    /**
     * 根据排范围把整仓堆垛机顺序裁剪为某个库区内的堆垛机集合。
     */
    private List<Integer> filterCrnNosByRows(RowLastno rowLastno, List<Integer> orderedCrnNos, List<Integer> rows) {
        if (Cools.isEmpty(rows)) {
            return new ArrayList<>(orderedCrnNos);
@@ -849,6 +1206,9 @@
        return result;
    }
    /**
     * 解析当前源站到目标堆垛机的入库目标站号。
     */
    private Integer resolveTargetStaNo(RowLastno rowLastno, Integer staDescId, Integer sourceStaNo, Integer crnNo) {
        if (!Utils.BooleanWhsTypeSta(rowLastno, staDescId)) {
            return null;
@@ -862,8 +1222,8 @@
            return null;
        }
        BasDevp staNo = basDevpService.selectById(staDesc.getCrnStn());
        if (Cools.isEmpty(staNo) || !"Y".equals(staNo.getAutoing())) {
            log.error("目标站{}不可用", staDesc.getCrnStn());
        if (Cools.isEmpty(staNo)) {
            log.error("目标站{}不存在", staDesc.getCrnStn());
            return null;
        }
        return staNo.getDevNo();
@@ -906,44 +1266,60 @@
        List<Integer> routeBlockedCrns = new ArrayList<>();
        List<Integer> noEmptyCrns = new ArrayList<>();
        List<Integer> locTypeBlockedCrns = new ArrayList<>();
        for (Integer candidateCrnNo : candidateCrnNos) {
            if (candidateCrnNo == null || !basCrnpService.checkSiteError(candidateCrnNo, true)) {
                crnErrorCrns.add(candidateCrnNo);
                continue;
            }
            Integer targetStaNo = resolveTargetStaNo(rowLastno, staDescId, sourceStaNo, candidateCrnNo);
            if (routeRequired && Utils.BooleanWhsTypeSta(rowLastno, staDescId) && targetStaNo == null) {
                routeBlockedCrns.add(candidateCrnNo);
                continue;
            }
            Wrapper<LocMast> openWrapper = new EntityWrapper<LocMast>()
                    .eq("crn_no", candidateCrnNo)
                    .eq("loc_sts", "O");
            applyLocTypeFilters(openWrapper, locTypeDto, true);
            openWrapper.orderBy("lev1").orderBy("bay1");
            LocMast anyOpenLoc = locMastService.selectOne(openWrapper);
            if (Cools.isEmpty(anyOpenLoc)) {
                noEmptyCrns.add(candidateCrnNo);
                continue;
            }
            Wrapper<LocMast> wrapper = new EntityWrapper<LocMast>()
                    .eq("crn_no", candidateCrnNo)
                    .eq("loc_sts", "O");
            applyLocTypeFilters(wrapper, locTypeDto, true);
            wrapper.orderBy("lev1").orderBy("bay1");
            LocMast candidateLoc = locMastService.selectOne(wrapper);
            if (Cools.isEmpty(candidateLoc) || (locTypeDto != null && !VersionUtils.locMoveCheckLocTypeComplete(candidateLoc, locTypeDto))) {
                locTypeBlockedCrns.add(candidateCrnNo);
                continue;
            }
            if (targetStaNo != null) {
                startupDto.setStaNo(targetStaNo);
            }
            return candidateLoc;
        }
        logRun2NoMatch(stage, sourceStaNo, preferredArea, candidateCrnNos, locTypeDto, crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        return null;
        return findRun2EmptyLocByCrnNosRecursively(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                staDescId, sourceStaNo, startupDto, preferredArea, 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,
                                                        List<Integer> crnErrorCrns, List<Integer> routeBlockedCrns,
                                                        List<Integer> noEmptyCrns, List<Integer> locTypeBlockedCrns) {
        if (index >= candidateCrnNos.size()) {
            logRun2NoMatch(stage, sourceStaNo, preferredArea, candidateCrnNos, locTypeDto,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
            return null;
        }
        Integer candidateCrnNo = candidateCrnNos.get(index);
        if (!isCrnActive(candidateCrnNo)) {
            crnErrorCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnNosRecursively(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, 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,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        Integer preferredNearRow = getCrnStartRow(rowLastno, candidateCrnNo);
        LocMast candidateLoc = findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, candidateCrnNo,
                preferredNearRow, locTypeDto);
        if (Cools.isEmpty(candidateLoc)) {
            int availableLocCount = countAssignableLocForCrn(rowLastno, rowLastnoType, candidateCrnNo,
                    preferredNearRow == null ? 0 : preferredNearRow, locTypeDto);
            if (availableLocCount <= 0) {
                noEmptyCrns.add(candidateCrnNo);
            } else {
                locTypeBlockedCrns.add(candidateCrnNo);
            }
            return findRun2EmptyLocByCrnNosRecursively(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, routeRequired, index + 1,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        if (targetStaNo != null) {
            startupDto.setStaNo(targetStaNo);
        }
        return candidateLoc;
    }
    /**
     * 按站点配置的“堆垛机 + 高低类型”优先级找位,并套用统一均衡策略。
     */
    private LocMast findRun2EmptyLocByCrnLocTypeEntries(RowLastno rowLastno, RowLastnoType rowLastnoType,
                                                        List<Map<String, Integer>> crnLocTypeEntries, LocTypeDto locTypeDto,
                                                        Integer staDescId, Integer sourceStaNo, StartupDto startupDto,
@@ -978,7 +1354,7 @@
        Integer candidateCrnNo = crnLocTypeEntry == null ? null : crnLocTypeEntry.get("crnNo");
        Short candidateLocType1 = crnLocTypeEntry == null || crnLocTypeEntry.get("locType1") == null
                ? null : crnLocTypeEntry.get("locType1").shortValue();
        if (candidateCrnNo == null || !basCrnpService.checkSiteError(candidateCrnNo, true)) {
        if (!isCrnActive(candidateCrnNo)) {
            crnErrorCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos,
@@ -992,15 +1368,17 @@
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        LocTypeDto searchLocTypeDto = buildRun2SearchLocTypeDto(locTypeDto, candidateLocType1);
        LocMast candidateLoc = findRun2OrderedEmptyLocByCrnLocType(rowLastnoType, candidateCrnNo, candidateLocType1, searchLocTypeDto);
        Integer preferredNearRow = getCrnStartRow(rowLastno, candidateCrnNo);
        LocMast candidateLoc = findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, candidateCrnNo,
                preferredNearRow, searchLocTypeDto);
        if (Cools.isEmpty(candidateLoc)) {
            noEmptyCrns.add(candidateCrnNo);
            return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
        }
        if (searchLocTypeDto != null && !VersionUtils.locMoveCheckLocTypeComplete(candidateLoc, searchLocTypeDto)) {
            locTypeBlockedCrns.add(candidateCrnNo);
            int availableLocCount = countAssignableLocForCrn(rowLastno, rowLastnoType, candidateCrnNo,
                    preferredNearRow == null ? 0 : preferredNearRow, searchLocTypeDto);
            if (availableLocCount <= 0) {
                noEmptyCrns.add(candidateCrnNo);
            } else {
                locTypeBlockedCrns.add(candidateCrnNo);
            }
            return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto,
                    staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos,
                    crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns);
@@ -1011,6 +1389,9 @@
        return candidateLoc;
    }
    /**
     * 从站点优先级配置中抽取不重复的堆垛机顺序。
     */
    private List<Integer> extractCrnNos(List<Map<String, Integer>> crnLocTypeEntries) {
        LinkedHashSet<Integer> orderedCrnNos = new LinkedHashSet<>();
        if (Cools.isEmpty(crnLocTypeEntries)) {
@@ -1025,6 +1406,9 @@
        return new ArrayList<>(orderedCrnNos);
    }
    /**
     * 用站点优先级里的 locType1 覆盖本次查询的高度条件。
     */
    private LocTypeDto buildRun2SearchLocTypeDto(LocTypeDto locTypeDto, Short candidateLocType1) {
        if (locTypeDto == null && candidateLocType1 == null) {
            return null;
@@ -1042,6 +1426,9 @@
        return searchLocTypeDto;
    }
    /**
     * 按站点优先配置直接查某台堆垛机上的第一个可用空库位。
     */
    private LocMast findRun2OrderedEmptyLocByCrnLocType(RowLastnoType rowLastnoType, Integer candidateCrnNo,
                                                        Short candidateLocType1, LocTypeDto locTypeDto) {
        if (candidateCrnNo == null) {
@@ -1179,27 +1566,252 @@
        return offset < bestOffset;
    }
    private int countAvailableLocForCrn(RowLastno rowLastno, RowLastnoType rowLastnoType, int crnNo, int preferredNearRow) {
        List<Integer> searchRows = getCrnSearchRows(rowLastno, crnNo, preferredNearRow);
        if (searchRows.isEmpty()) {
            return 0;
    /**
     * 推导库位主档查询时应使用的 whsType。
     */
    private Long resolveLocWhsType(RowLastno rowLastno, RowLastnoType rowLastnoType) {
        if (rowLastno != null && rowLastno.getWhsType() != null) {
            return rowLastno.getWhsType().longValue();
        }
        return locMastService.selectCount(new EntityWrapper<LocMast>()
                .in("row1", searchRows)
                .eq("loc_sts", "O")
                .eq("whs_type", rowLastnoType.getType().longValue()));
        if (rowLastnoType != null && rowLastnoType.getType() != null) {
            return rowLastnoType.getType().longValue();
        }
        return null;
    }
    private int countAvailableSingleExtensionLocForCrn(RowLastno rowLastno, RowLastnoType rowLastnoType, int crnNo, int preferredNearRow, LocTypeDto locTypeDto) {
        List<Integer> searchRows = getCrnSearchRows(rowLastno, crnNo, preferredNearRow);
        if (searchRows.isEmpty()) {
            return 0;
    /**
     * 判断库位是否满足本次规格约束。
     */
    private boolean matchesLocType(LocMast locMast, LocTypeDto locTypeDto) {
        if (locMast != null && isFullPalletLocTypeSearch(locTypeDto) && locMast.getLocType2() != null
                && locMast.getLocType2() == 1) {
            return false;
        }
        return locTypeDto == null || VersionUtils.locMoveCheckLocTypeComplete(locMast, locTypeDto);
    }
    /**
     * 查询某一排上的所有空库位,并按单伸/双伸策略排序。
     */
    private List<LocMast> findOpenLocsByRow(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer row,
                                            Integer crnNo, LocTypeDto locTypeDto, boolean singleExtension) {
        List<LocMast> result = new ArrayList<LocMast>();
        if (row == null) {
            return result;
        }
        Wrapper<LocMast> wrapper = new EntityWrapper<LocMast>()
                .in("row1", searchRows)
//                .eq("row1", row)
                .eq("loc_sts", "O");
        if (crnNo != null) {
            wrapper.eq("crn_no", crnNo);
        }
        applyLocTypeFilters(wrapper, locTypeDto, true);
        return locMastService.selectCount(wrapper);
        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) {
            if (matchesLocType(locMast, locTypeDto)) {
                result.add(locMast);
            }
        }
        return result;
    }
    /**
     * 按排、列、层精确定位某个库位位置上的状态记录。
     */
    private LocMast findLocByPosition(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer crnNo,
                                      Integer row, Integer bay, Integer lev, String... statuses) {
        if (row == null || bay == null || lev == null) {
            return null;
        }
        EntityWrapper<LocMast> wrapper = new EntityWrapper<LocMast>();
        wrapper.eq("row1", row);
        wrapper.eq("bay1", bay);
        wrapper.eq("lev1", lev);
        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]);
            } else {
                wrapper.in("loc_sts", statuses);
            }
        }
        return locMastService.selectOne(wrapper);
    }
    /**
     * 在一对浅排/深排之间选择真正可投放的目标库位。
     */
    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);
        if (Cools.isEmpty(shallowOpenLocs)) {
            return null;
        }
        if (deepRow == null) {
            return shallowOpenLocs.get(0);
        }
        for (LocMast shallowLoc : shallowOpenLocs) {
            LocMast deepOpenLoc = findLocByPosition(rowLastno, rowLastnoType, crnNo, deepRow, shallowLoc.getBay1(), shallowLoc.getLev1(), "O");
            if (!Cools.isEmpty(deepOpenLoc) && matchesLocType(deepOpenLoc, locTypeDto)) {
                return deepOpenLoc;
            }
        }
        for (LocMast shallowLoc : shallowOpenLocs) {
            LocMast deepBlockingLoc = findLocByPosition(rowLastno, rowLastnoType, crnNo, deepRow, shallowLoc.getBay1(), shallowLoc.getLev1(), "F", "D");
            if (!Cools.isEmpty(deepBlockingLoc)) {
                return shallowLoc;
            }
            if (findLocByPosition(rowLastno, rowLastnoType, crnNo, deepRow, shallowLoc.getBay1(), shallowLoc.getLev1()) == null) {
                return shallowLoc;
            }
        }
        return null;
    }
    /**
     * 按某台堆垛机的深浅排画像搜索第一个可分配空库位。
     */
    private LocMast findConfiguredEmptyLocForCrn(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer crnNo,
                                                 Integer preferredNearRow, LocTypeDto locTypeDto) {
        if (rowLastno == null || crnNo == null) {
            return null;
        }
        CrnDepthRuleProfile profile = basCrnDepthRuleService.resolveProfile(rowLastno, crnNo, preferredNearRow);
        if (profile == null || Cools.isEmpty(profile.getSearchRows())) {
            return null;
        }
        LinkedHashSet<Integer> processedShallowRows = new LinkedHashSet<Integer>();
        boolean singleExtension = profile.isSingleExtension();
        for (Integer searchRow : profile.getSearchRows()) {
            if (searchRow == null) {
                continue;
            }
            if (!singleExtension) {
                if (profile.isShallowRow(searchRow)) {
                    if (!processedShallowRows.add(searchRow)) {
                        continue;
                    }
                    LocMast candidateLoc = findPairAssignableLoc(rowLastno, rowLastnoType, crnNo, searchRow,
                            profile.getPairedDeepRow(searchRow), locTypeDto);
                    if (!Cools.isEmpty(candidateLoc)) {
                        return candidateLoc;
                    }
                    continue;
                }
                if (profile.isDeepRow(searchRow)) {
                    Integer shallowRow = profile.getPairedShallowRow(searchRow);
                    if (shallowRow != null) {
                        if (!processedShallowRows.add(shallowRow)) {
                            continue;
                        }
                        LocMast candidateLoc = findPairAssignableLoc(rowLastno, rowLastnoType, crnNo, shallowRow,
                                searchRow, locTypeDto);
                        if (!Cools.isEmpty(candidateLoc)) {
                            return candidateLoc;
                        }
                        continue;
                    }
                }
            }
            List<LocMast> locMasts = findOpenLocsByRow(rowLastno, rowLastnoType, searchRow, crnNo, locTypeDto, singleExtension);
            if (!Cools.isEmpty(locMasts)) {
                return locMasts.get(0);
            }
        }
        return null;
    }
    /**
     * 统计某台堆垛机当前画像下可参与分配的空库位数量。
     */
    private int countAssignableLocForCrn(RowLastno rowLastno, RowLastnoType rowLastnoType, int crnNo, int preferredNearRow, LocTypeDto locTypeDto) {
        CrnDepthRuleProfile profile = basCrnDepthRuleService.resolveProfile(rowLastno, crnNo, preferredNearRow);
        if (profile == null || Cools.isEmpty(profile.getSearchRows())) {
            return 0;
        }
        int count = 0;
        LinkedHashSet<Integer> processedShallowRows = new LinkedHashSet<Integer>();
        boolean singleExtension = profile.isSingleExtension();
        for (Integer searchRow : profile.getSearchRows()) {
            if (searchRow == null) {
                continue;
            }
            if (!singleExtension) {
                if (profile.isShallowRow(searchRow)) {
                    if (!processedShallowRows.add(searchRow)) {
                        continue;
                    }
                    count += countAssignablePairLocs(rowLastno, rowLastnoType, crnNo, searchRow,
                            profile.getPairedDeepRow(searchRow), locTypeDto);
                    continue;
                }
                if (profile.isDeepRow(searchRow)) {
                    Integer shallowRow = profile.getPairedShallowRow(searchRow);
                    if (shallowRow != null) {
                        if (!processedShallowRows.add(shallowRow)) {
                            continue;
                        }
                        count += countAssignablePairLocs(rowLastno, rowLastnoType, crnNo, shallowRow, searchRow, locTypeDto);
                        continue;
                    }
                }
            }
            count += findOpenLocsByRow(rowLastno, rowLastnoType, searchRow, crnNo, locTypeDto, singleExtension).size();
        }
        return count;
    }
    /**
     * 统计一对浅排/深排上的可分配库位数量。
     */
    private int countAssignablePairLocs(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer crnNo,
                                        Integer shallowRow, Integer deepRow, LocTypeDto locTypeDto) {
        List<LocMast> shallowOpenLocs = findOpenLocsByRow(rowLastno, rowLastnoType, shallowRow, crnNo, locTypeDto, false);
        if (Cools.isEmpty(shallowOpenLocs)) {
            return 0;
        }
        if (deepRow == null) {
            return shallowOpenLocs.size();
        }
        int count = 0;
        for (LocMast shallowLoc : shallowOpenLocs) {
            LocMast deepOpenLoc = findLocByPosition(rowLastno, rowLastnoType, crnNo, deepRow, shallowLoc.getBay1(), shallowLoc.getLev1(), "O");
            if (!Cools.isEmpty(deepOpenLoc) && matchesLocType(deepOpenLoc, locTypeDto)) {
                count++;
                continue;
            }
            LocMast deepBlockingLoc = findLocByPosition(rowLastno, rowLastnoType, crnNo, deepRow, shallowLoc.getBay1(), shallowLoc.getLev1(), "F", "D");
            if (!Cools.isEmpty(deepBlockingLoc) ||
                    findLocByPosition(rowLastno, rowLastnoType, crnNo, deepRow, shallowLoc.getBay1(), shallowLoc.getLev1()) == null) {
                count++;
            }
        }
        return count;
    }
    /**
     * 统计某台堆垛机所有可用空库位数量,不附带规格过滤。
     */
    private int countAvailableLocForCrn(RowLastno rowLastno, RowLastnoType rowLastnoType, int crnNo, int preferredNearRow) {
        return countAssignableLocForCrn(rowLastno, rowLastnoType, crnNo, preferredNearRow, null);
    }
    /**
     * 统计某台单伸堆垛机在当前规格约束下的可用空库位数量。
     */
    private int countAvailableSingleExtensionLocForCrn(RowLastno rowLastno, RowLastnoType rowLastnoType, int crnNo, int preferredNearRow, LocTypeDto locTypeDto) {
        return countAssignableLocForCrn(rowLastno, rowLastnoType, crnNo, preferredNearRow, locTypeDto);
    }
    private Optional<CrnRowInfo> findBalancedSingleExtensionCrnAndNearRow(RowLastno rowLastno, int curRow, int crnNumber, int times,
@@ -1263,35 +1875,20 @@
        return Optional.ofNullable(fallbackInfo);
    }
    /**
     * 返回某台堆垛机本次找位应扫描的排顺序。
     */
    private List<Integer> getCrnSearchRows(RowLastno rowLastno, int crnNo, int preferredNearRow) {
        List<Integer> searchRows = new ArrayList<>();
        addSearchRow(searchRows, preferredNearRow, rowLastno);
        Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId());
        if (rowSpan == null) {
            return searchRows;
        CrnDepthRuleProfile profile = basCrnDepthRuleService.resolveProfile(rowLastno, crnNo, preferredNearRow);
        if (profile == null || Cools.isEmpty(profile.getSearchRows())) {
            return new ArrayList<Integer>();
        }
        int crnOffset = crnNo - rowLastno.getsCrnNo();
        if (crnOffset < 0) {
            return searchRows;
        }
        int startRow = rowLastno.getsRow() + crnOffset * rowSpan;
        switch (rowLastno.getTypeId()) {
            case 1:
                addSearchRow(searchRows, startRow + 1, rowLastno);
                addSearchRow(searchRows, startRow + 2, rowLastno);
                break;
            case 2:
                addSearchRow(searchRows, startRow, rowLastno);
                addSearchRow(searchRows, startRow + 1, rowLastno);
                break;
            default:
                break;
        }
        return searchRows;
        return new ArrayList<Integer>(profile.getSearchRows());
    }
    /**
     * 按库型返回每台堆垛机占用的排跨度。
     */
    private Integer getCrnRowSpan(Integer typeId) {
        if (typeId == null) {
            return null;
@@ -1306,6 +1903,9 @@
        }
    }
    /**
     * 向搜索排列表追加一个合法且不重复的排号。
     */
    private void addSearchRow(List<Integer> searchRows, Integer row, RowLastno rowLastno) {
        if (row == null) {
            return;
@@ -1318,67 +1918,16 @@
        }
    }
    /**
     * run/run2 标准堆垛机统一的空库位查询入口。
     */
    private LocMast findStandardEmptyLoc(RowLastno rowLastno, RowLastnoType rowLastnoType, int crnNo, int nearRow, LocTypeDto locTypeDto) {
        for (Integer searchRow : getCrnSearchRows(rowLastno, crnNo, nearRow)) {
            List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                    .eq("row1", searchRow)
                    .eq("loc_sts", "O")
                    .eq("whs_type", rowLastnoType.getType().longValue())
                    .orderBy("lev1", true)
                    .orderBy("bay1", true));
            for (LocMast locMast1 : locMasts) {
                if (!VersionUtils.locMoveCheckLocTypeComplete(locMast1, locTypeDto)) {
                    continue;
                }
                if (Utils.BooleanWhsTypeStaIoType(rowLastno)) {
                    String shallowLoc = Utils.getDeepLoc(slaveProperties, locMast1.getLocNo());
                    LocMast locMast2 = locMastService.selectOne(new EntityWrapper<LocMast>()
                            .eq("loc_no", shallowLoc)
                            .eq("loc_sts", "O")
                            .eq("whs_type", rowLastnoType.getType().longValue()));
                    if (!Cools.isEmpty(locMast2)) {
                        return locMast2;
                    }
                } else if (!Cools.isEmpty(locMast1)) {
                    return locMast1;
                }
            }
            if (!Utils.BooleanWhsTypeStaIoType(rowLastno)) {
                continue;
            }
            for (LocMast locMast1 : locMasts) {
                if (!VersionUtils.locMoveCheckLocTypeComplete(locMast1, locTypeDto)) {
                    continue;
                }
                String shallowLoc = Utils.getDeepLoc(slaveProperties, locMast1.getLocNo());
                LocMast locMast2 = locMastService.selectOne(new EntityWrapper<LocMast>()
                        .eq("loc_no", shallowLoc)
                        .eq("loc_sts", "O")
                        .eq("whs_type", rowLastnoType.getType().longValue()));
                if (!Cools.isEmpty(locMast2)) {
                    return locMast2;
                }
                locMast2 = locMastService.selectOne(new EntityWrapper<LocMast>()
                        .eq("loc_no", shallowLoc)
                        .eq("loc_sts", "F")
                        .eq("whs_type", rowLastnoType.getType().longValue()));
                if (!Cools.isEmpty(locMast2)) {
                    return locMast1;
                }
                locMast2 = locMastService.selectOne(new EntityWrapper<LocMast>()
                        .eq("loc_no", shallowLoc)
                        .eq("loc_sts", "D")
                        .eq("whs_type", rowLastnoType.getType().longValue()));
                if (!Cools.isEmpty(locMast2)) {
                    return locMast1;
                }
            }
        }
        return null;
        return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto);
    }
    /**
     * 构造只提升 locType1 的向上兼容规格。
     */
    private LocTypeDto buildUpwardCompatibleLocTypeDto(LocTypeDto locTypeDto) {
        if (locTypeDto == null || locTypeDto.getLocType1() == null || locTypeDto.getLocType1() >= 2) {
            return null;
@@ -1392,40 +1941,12 @@
    }
    /**
     * 空托盘兼容回退规则:
     * 当首轮 loc_type2=1 没有命中空库位时,允许退化到高位空库位 loc_type1=2。
     *
     * 注意这里不会继续保留 locType2=1。
     * 也就是说第二轮是“高位优先兜底”,而不是“高位窄库位兜底”。
     * 这是按照现场最新口径实现的:窄库位优先,窄库位没有时再找高位空库位。
     */
    private LocTypeDto buildEmptyPalletCompatibleLocTypeDto(LocTypeDto locTypeDto) {
        if (locTypeDto == null || locTypeDto.getLocType2() == null || locTypeDto.getLocType2() != 1) {
            return null;
        }
        LocTypeDto compatibleLocTypeDto = new LocTypeDto();
        compatibleLocTypeDto.setLocType1((short) 2);
        compatibleLocTypeDto.setLocType3(locTypeDto.getLocType3());
        compatibleLocTypeDto.setSiteId(locTypeDto.getSiteId());
        return compatibleLocTypeDto;
    }
    /**
     * 统一封装找库位失败后的兼容重试顺序。
     *
     * 空托盘:
     * 先按 loc_type2=1 查找,失败后退到 loc_type1=2。
     *
     * 非空托盘:
     * 维持原规则,低位失败后再向高位兼容。
     * 兼容规则固定为:
     * 只允许 loc_type1 低位向高位兼容,loc_type2/loc_type3 不参与满托找位。
     */
    private LocTypeDto buildRetryCompatibleLocTypeDto(Integer staDescId, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto) {
        if (isEmptyPalletRequest(staDescId, findLocNoAttributeVo)) {
            LocTypeDto emptyPalletCompatibleLocTypeDto = buildEmptyPalletCompatibleLocTypeDto(locTypeDto);
            if (emptyPalletCompatibleLocTypeDto != null) {
                return emptyPalletCompatibleLocTypeDto;
            }
        }
        return buildUpwardCompatibleLocTypeDto(locTypeDto);
    }
@@ -1672,8 +2193,6 @@
        int crnNo = 0;
        int nearRow = 0;
        int curRow = 0;
        int rowCount = 0;
        LocMast locMast = null;
        StartupDto startupDto = new StartupDto();
@@ -1686,30 +2205,26 @@
        if (Cools.isEmpty(rowLastnoType)) {
            throw new CoolException("数据异常,请联系管理员===》库位规则类型未知");
        }
        int crnNumber = resolveCrnCount(rowLastno);
        rowCount = crnNumber;
        curRow = rowLastno.getCurrentRow();
        int curRow = rowLastno.getCurrentRow() == null ? 0 : rowLastno.getCurrentRow();
        crnNo = resolveRun2CrnNo(rowLastno);
        Integer preferredArea = findLocNoAttributeVo.getOutArea();
        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);
        List<Integer> triedCrnNos = new ArrayList<>();
        if (emptyPalletRequest) {
            // 空托盘单独按库区轮询:
            // 1. 当前库区先找 loc_type2=1
            // 2. 当前库区没有,再找其他库区 loc_type2=1
            // 3. 全部 narrow 都没有时,再退到 loc_type1=2
            emptyPalletAreaSearchResult = findEmptyPalletRun2AreaLoc(rowLastno, staDescId, sourceStaNo, startupDto, preferredArea, locTypeDto);
            emptyPalletAreaSearchResult = findEmptyPalletRun2Loc(rowLastno, staDescId, sourceStaNo, startupDto, preferredArea, locTypeDto);
            if (!Cools.isEmpty(emptyPalletAreaSearchResult)) {
                locMast = emptyPalletAreaSearchResult.locMast;
            }
        } else {
            locMast = findNormalRun2Loc(rowLastno, rowLastnoType, sourceStaNo, staDescId, findLocNoAttributeVo,
                    locTypeDto, startupDto, preferredArea, orderedCrnNos, triedCrnNos);
            normalRun2SearchResult = findNormalRun2Loc(rowLastno, rowLastnoType, sourceStaNo, staDescId, findLocNoAttributeVo,
                    locTypeDto, startupDto, preferredArea, orderedCrnNos);
            if (normalRun2SearchResult != null) {
                locMast = normalRun2SearchResult.locMast;
            }
        }
        if (!Cools.isEmpty(locMast)) {
@@ -1718,22 +2233,25 @@
        }
        if (emptyPalletRequest) {
            advanceEmptyPalletRun2Cursor(emptyPalletAreaSearchResult, locMast);
        } else {
            advanceNormalRun2Cursor(rowLastno, curRow, orderedRunnableCrnNos, locMast == null ? null : locMast.getCrnNo());
        } else if (!Cools.isEmpty(locMast)) {
            List<Integer> cursorCrnNos = normalRun2SearchResult == null || Cools.isEmpty(normalRun2SearchResult.runnableCrnNos)
                    ? orderedRunnableCrnNos
                    : normalRun2SearchResult.runnableCrnNos;
            advanceNormalRun2Cursor(rowLastno, curRow, cursorCrnNos, locMast.getCrnNo());
        }
        if (Cools.isEmpty(locMast) || !locMast.getLocSts().equals("O")) {
            if (!emptyPalletRequest && times < rowCount * 2) {
                times = times + 1;
                return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, locTypeDto, recommendRows, times);
            if (emptyPalletRequest) {
                log.error("No empty location found. spec={}, preferredArea={}, nearRow={}",
                        JSON.toJSONString(locTypeDto), preferredArea, nearRow);
                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={}, times={}, preferredArea={}, nearRow={}", JSON.toJSONString(locTypeDto), times, preferredArea, nearRow);
            log.error("No empty location found. spec={}, preferredArea={}, nearRow={}", JSON.toJSONString(locTypeDto), preferredArea, nearRow);
            throw new CoolException("没有空库位");
        }
@@ -1744,21 +2262,11 @@
        startupDto.setLocNo(locMast.getLocNo());
        return startupDto;
    }
    /**
     * 单伸堆垛机复用统一画像算法查询空库位。
     */
    private LocMast findSingleExtensionEmptyLoc(RowLastno rowLastno, int crnNo, int nearRow, RowLastnoType rowLastnoType, LocTypeDto locTypeDto) {
        for (Integer searchRow : getCrnSearchRows(rowLastno, crnNo, nearRow)) {
            List<LocMast> locMasts = locMastService.selectList(new EntityWrapper<LocMast>()
                    .eq("row1", searchRow)
                    .eq("loc_sts", "O")
                    .eq("whs_type", rowLastnoType.getType().longValue())
                    .orderBy("bay1", true)
                    .orderBy("lev1", true));
            for (LocMast locMast : locMasts) {
                if (VersionUtils.locMoveCheckLocTypeComplete(locMast, locTypeDto)) {
                    return locMast;
                }
            }
        }
        return null;
        return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto);
    }
    public StartupDto getLocNoRun4(Integer whsType, Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, Integer moveCrnNo, LocTypeDto locTypeDto, int times) {
src/main/java/com/zy/common/web/WcsController.java
@@ -81,10 +81,10 @@
//            dto1.setTaskPri((int) Math.round(wrkMast1.getIoPri()));
            return R.ok(dto1);
        }
        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("io_type", 1));
        if (!Cools.isEmpty(wrkMasts) && wrkMasts.size() > 10) {
            return R.error("限行");
        }
//        List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>().eq("io_type", 1));
//        if (!Cools.isEmpty(wrkMasts) && wrkMasts.size() > 10) {
//            return R.error("限行");
//        }
        waitPakins = waitPakinService.selectList(new EntityWrapper<WaitPakin>().eq("zpallet", param.getBarcode()));
        if (Cools.isEmpty(waitPakins)) {
            return R.error("请先添加入库通知档");
@@ -126,6 +126,9 @@
        BasDevp sourceStaNo = basDevpService.checkSiteStatus(param.getSourceStaNo(), true);
        sourceStaNo.setLocType1(param.getLocType1());
        LocTypeDto locTypeDto = new LocTypeDto(sourceStaNo);
        if (waitPakins.get(0).getMatnr().equals("emptyPallet")) {
            locTypeDto.setLocType2((short) 1);
        }
        StartupDto dto = startupFullPutStore(param.getSourceStaNo(), param.getBarcode(), locTypeDto, waitPakins);
        log.info("WCS入库接口返参:{},托盘码:{}", dto, param.getBarcode());
src/main/resources/mapper/BasCrnDepthRuleMapper.xml
New file
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zy.asrs.mapper.BasCrnDepthRuleMapper">
    <resultMap id="BaseResultMap" type="com.zy.asrs.entity.BasCrnDepthRule">
        <id column="id" property="id"/>
        <result column="whs_type" property="whsType"/>
        <result column="crn_no" property="crnNo"/>
        <result column="layout_type" property="layoutType"/>
        <result column="search_rows_csv" property="searchRowsCsv"/>
        <result column="shallow_rows_csv" property="shallowRowsCsv"/>
        <result column="deep_rows_csv" property="deepRowsCsv"/>
        <result column="enabled" property="enabled"/>
        <result column="memo" property="memo"/>
        <result column="create_by" property="createBy"/>
        <result column="create_time" property="createTime"/>
        <result column="update_by" property="updateBy"/>
        <result column="update_time" property="updateTime"/>
    </resultMap>
</mapper>
src/main/resources/sql/20260321_bas_crn_depth_rule.sql
New file
@@ -0,0 +1,30 @@
IF NOT EXISTS (
    SELECT 1
    FROM sys.objects
    WHERE object_id = OBJECT_ID(N'[dbo].[asr_bas_crn_depth_rule]')
      AND type = N'U'
)
BEGIN
    CREATE TABLE [dbo].[asr_bas_crn_depth_rule] (
        [id] BIGINT IDENTITY(1,1) NOT NULL,
        [whs_type] INT NOT NULL,
        [crn_no] INT NOT NULL,
        [layout_type] INT NOT NULL,
        [search_rows_csv] VARCHAR(255) NULL,
        [shallow_rows_csv] VARCHAR(255) NULL,
        [deep_rows_csv] VARCHAR(255) NULL,
        [enabled] INT NOT NULL CONSTRAINT [DF_asr_bas_crn_depth_rule_enabled] DEFAULT ((1)),
        [memo] VARCHAR(255) NULL,
        [create_by] BIGINT NULL,
        [create_time] DATETIME NULL,
        [update_by] BIGINT NULL,
        [update_time] DATETIME NULL,
        CONSTRAINT [PK_asr_bas_crn_depth_rule] PRIMARY KEY CLUSTERED ([id] ASC),
        CONSTRAINT [UK_asr_bas_crn_depth_rule_whs_crn] UNIQUE NONCLUSTERED ([whs_type] ASC, [crn_no] ASC)
    );
    EXEC sys.sp_addextendedproperty
        @name = N'MS_Description', @value = N'堆垛机深浅库位规则',
        @level0type = N'SCHEMA', @level0name = N'dbo',
        @level1type = N'TABLE',  @level1name = N'asr_bas_crn_depth_rule';
END;
src/main/resources/sql/20260322_row_lastno_current_crn_no.sql
New file
@@ -0,0 +1,11 @@
IF COL_LENGTH('dbo.asr_row_lastno', 'current_crn_no') IS NULL
BEGIN
    ALTER TABLE [dbo].[asr_row_lastno]
        ADD [current_crn_no] INT NULL;
    EXEC sys.sp_addextendedproperty
        @name = N'MS_Description', @value = N'当前堆垛机号',
        @level0type = N'SCHEMA', @level0name = N'dbo',
        @level1type = N'TABLE',  @level1name = N'asr_row_lastno',
        @level2type = N'COLUMN', @level2name = N'current_crn_no';
END;
src/main/webapp/static/js/basCrnDepthRule/basCrnDepthRule.js
New file
@@ -0,0 +1,304 @@
var pageCurr;
layui.config({
    base: baseUrl + "/static/layui/lay/modules/"
}).use(['table', 'form', 'admin'], function () {
    var table = layui.table;
    var $ = layui.jquery;
    var layer = layui.layer;
    var form = layui.form;
    var admin = layui.admin;
    tableIns = table.render({
        elem: '#basCrnDepthRule',
        headers: {token: localStorage.getItem('token')},
        url: baseUrl + '/basCrnDepthRule/list/auth',
        page: true,
        limit: 15,
        limits: [15, 30, 50, 100, 200, 500],
        toolbar: '#toolbar',
        cellMinWidth: 80,
        height: 'full-120',
        cols: [[
            {type: 'checkbox'}
            , {field: 'whsType', align: 'center', title: '仓库'}
            , {field: 'crnNo', align: 'center', title: '堆垛机'}
            , {field: 'layoutType$', align: 'center', title: '布局'}
            , {field: 'searchRowsCsv', align: 'center', title: '搜索排'}
            , {field: 'shallowRowsCsv', align: 'center', title: '浅排'}
            , {field: 'deepRowsCsv', align: 'center', title: '深排'}
            , {field: 'enabled$', align: 'center', title: '启用'}
            , {field: 'memo', align: 'center', title: '备注'}
            , {field: 'updateBy$', align: 'center', title: '修改人', hide: true}
            , {field: 'updateTime$', align: 'center', title: '修改时间', hide: true}
            , {fixed: 'right', title: '操作', align: 'center', toolbar: '#operate', width: 120}
        ]],
        request: {
            pageName: 'curr',
            pageSize: 'limit'
        },
        parseData: function (res) {
            return {
                code: res.code,
                msg: res.msg,
                count: res.data.total,
                data: res.data.records
            };
        },
        response: {
            statusCode: 200
        },
        done: function (res, curr) {
            if (res.code === 403) {
                top.location.href = baseUrl + "/";
            }
            pageCurr = curr;
            limit();
        }
    });
    table.on('sort(basCrnDepthRule)', function (obj) {
        var searchData = collectSearchData();
        searchData.orderByField = obj.field;
        searchData.orderByType = obj.type;
        tableIns.reload({
            where: searchData,
            page: {curr: 1}
        });
    });
    table.on('toolbar(basCrnDepthRule)', function (obj) {
        var checkStatus = table.checkStatus(obj.config.id).data;
        switch (obj.event) {
            case 'addData':
                showEditModel();
                break;
            case 'deleteData':
                if (checkStatus.length === 0) {
                    layer.msg('请选择要删除的数据', {icon: 2});
                    return;
                }
                del(checkStatus);
                break;
            case 'exportData':
                exportData(obj);
                break;
            case 'templatePreview':
                openTemplateDialog(false);
                break;
            case 'templateGenerate':
                openTemplateDialog(true);
                break;
            case 'runtimePreview':
                openRuntimeDialog();
                break;
        }
    });
    table.on('tool(basCrnDepthRule)', function (obj) {
        var data = obj.data;
        switch (obj.event) {
            case 'edit':
                showEditModel(data);
                break;
            case 'del':
                del([data]);
                break;
        }
    });
    function collectSearchData() {
        var searchData = {};
        $.each($('#search-box [name]').serializeArray(), function () {
            searchData[this.name] = this.value;
        });
        return searchData;
    }
    function showEditModel(mData) {
        admin.open({
            type: 1,
            area: '900px',
            title: (mData ? '修改' : '新增') + '堆垛机深浅规则',
            content: $('#editDialog').html(),
            success: function (layero, dIndex) {
                form.val('detail', mData || {enabled: 1, layoutType: 2});
                form.render('select');
                form.on('submit(editSubmit)', function (data) {
                    $.ajax({
                        url: baseUrl + '/basCrnDepthRule/' + (mData ? 'update' : 'add') + '/auth',
                        headers: {'token': localStorage.getItem('token')},
                        data: data.field,
                        method: 'POST',
                        success: function (res) {
                            if (res.code === 200) {
                                layer.close(dIndex);
                                layer.msg(res.msg, {icon: 1});
                                tableReload();
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + "/";
                            } else {
                                layer.msg(res.msg, {icon: 2});
                            }
                        }
                    });
                    return false;
                });
                $(layero).children('.layui-layer-content').css('overflow', 'visible');
            }
        });
    }
    function openTemplateDialog(generate) {
        admin.open({
            type: 1,
            area: '520px',
            title: generate ? '模板生成' : '模板预览',
            content: $('#templateDialog').html(),
            success: function (layero, dIndex) {
                form.val('templateForm', {enabled: 1});
                form.render('select');
                form.on('submit(templateSubmit)', function (data) {
                    $.ajax({
                        url: baseUrl + '/basCrnDepthRule/' + (generate ? 'templateGenerate' : 'templatePreview') + '/auth',
                        headers: {'token': localStorage.getItem('token')},
                        data: data.field,
                        method: 'POST',
                        success: function (res) {
                            if (res.code === 200) {
                                if (generate) {
                                    layer.close(dIndex);
                                    layer.msg(res.msg, {icon: 1});
                                    tableReload();
                                    return;
                                }
                                layer.alert('<pre style="white-space: pre-wrap;max-height: 480px;overflow:auto;">' +
                                    JSON.stringify(res.data, null, 2) + '</pre>', {
                                    title: '模板预览'
                                });
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + "/";
                            } else {
                                layer.msg(res.msg, {icon: 2});
                            }
                        }
                    });
                    return false;
                });
                $(layero).children('.layui-layer-content').css('overflow', 'visible');
            }
        });
    }
    function openRuntimeDialog() {
        admin.open({
            type: 1,
            area: '560px',
            title: '运行预览',
            content: $('#runtimeDialog').html(),
            success: function (layero) {
                form.on('submit(runtimeSubmit)', function (data) {
                    $.ajax({
                        url: baseUrl + '/basCrnDepthRule/runtimePreview/auth',
                        headers: {'token': localStorage.getItem('token')},
                        data: data.field,
                        method: 'POST',
                        success: function (res) {
                            if (res.code === 200) {
                                layer.alert('<pre style="white-space: pre-wrap;max-height: 520px;overflow:auto;">' +
                                    JSON.stringify(res.data, null, 2) + '</pre>', {
                                    title: '运行预览结果'
                                });
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + "/";
                            } else {
                                layer.msg(res.msg, {icon: 2});
                            }
                        }
                    });
                    return false;
                });
                $(layero).children('.layui-layer-content').css('overflow', 'visible');
            }
        });
    }
    function exportData(obj) {
        admin.confirm('确定导出Excel吗', {shadeClose: true}, function () {
            var titles = [];
            var fields = [];
            obj.config.cols[0].map(function (col) {
                if (col.type === 'normal' && col.hide === false && col.toolbar == null) {
                    titles.push(col.title);
                    fields.push(col.field);
                }
            });
            var param = {
                basCrnDepthRule: collectSearchData(),
                fields: fields
            };
            $.ajax({
                url: baseUrl + '/basCrnDepthRule/export/auth',
                headers: {'token': localStorage.getItem('token')},
                data: JSON.stringify(param),
                dataType: 'json',
                contentType: 'application/json;charset=UTF-8',
                method: 'POST',
                success: function (res) {
                    if (res.code === 200) {
                        table.exportFile(titles, res.data, 'xls');
                    } else if (res.code === 403) {
                        top.location.href = baseUrl + "/";
                    } else {
                        layer.msg(res.msg, {icon: 2});
                    }
                }
            });
        });
    }
    function del(rows) {
        layer.confirm('确定要删除选中数据吗?', {skin: 'layui-layer-admin', shade: .1}, function (i) {
            layer.close(i);
            $.ajax({
                url: baseUrl + '/basCrnDepthRule/delete/auth',
                headers: {'token': localStorage.getItem('token')},
                data: {param: JSON.stringify(rows)},
                method: 'POST',
                success: function (res) {
                    if (res.code === 200) {
                        layer.msg(res.msg, {icon: 1});
                        tableReload();
                    } else if (res.code === 403) {
                        top.location.href = baseUrl + "/";
                    } else {
                        layer.msg(res.msg, {icon: 2});
                    }
                }
            });
        });
    }
    form.on('submit(search)', function () {
        pageCurr = 1;
        tableReload();
        return false;
    });
    form.on('submit(reset)', function () {
        pageCurr = 1;
        clearFormVal($('#search-box'));
        tableReload();
        return false;
    });
});
function tableReload() {
    var searchData = {};
    $.each($('#search-box [name]').serializeArray(), function () {
        searchData[this.name] = this.value;
    });
    tableIns.reload({
        where: searchData,
        page: {curr: pageCurr || 1}
    });
}
src/main/webapp/static/js/basDevp/basDevp.js
@@ -596,9 +596,13 @@
function setFormVal(el, data, showImg) {
    for (var val in data) {
        var find = el.find(":input[id='" + val + "']");
        var currentVal = data[val];
        if (val === 'area') {
            currentVal = normalizeAreaValue(currentVal);
        }
        if (find[0]!=null){
            if (find[0].type === 'checkbox'){
                if (data[val]==='Y'){
                if (currentVal==='Y'){
                    find.attr("checked","checked");
                    find.val('Y');
                } else {
@@ -608,13 +612,13 @@
                continue;
            }
        }
        find.val(data[val]);
        find.val(currentVal);
        if (showImg){
            var next = find.next();
            if (next.get(0)){
                if (next.get(0).localName === "img") {
                    find.hide();
                    next.attr("src", data[val]);
                    next.attr("src", currentVal);
                    next.show();
                }
            }
@@ -622,6 +626,23 @@
    }
}
function normalizeAreaValue(value) {
    if (value === undefined || value === null) {
        return value;
    }
    var normalized = String(value).replace(/\s+/g, '').toUpperCase();
    if (normalized === '1' || normalized === 'A' || normalized === 'A区' || normalized === 'A库' || normalized === 'A库区') {
        return 'A';
    }
    if (normalized === '2' || normalized === 'B' || normalized === 'B区' || normalized === 'B库' || normalized === 'B库区') {
        return 'B';
    }
    if (normalized === '3' || normalized === 'C' || normalized === 'C区' || normalized === 'C库' || normalized === 'C库区') {
        return 'C';
    }
    return value;
}
function clearFormVal(el) {
    $(':input', el)
        .val('')
src/main/webapp/static/js/rowLastno/rowLastno.js
@@ -25,6 +25,7 @@
            ,{field: 'wrkMk', align: 'center',title: '当前工作号', hide:false}
            ,{field: 'sRow', align: 'center',title: '起始排号', hide:true}
            ,{field: 'currentRow', align: 'center',title: '当前排号', style: 'color: #AA3130;font-weight: bold', hide:false}
            ,{field: 'currentCrnNo', align: 'center',title: '当前堆垛机号', style: 'color: #AA3130;font-weight: bold', hide:false}
            ,{field: 'eRow', align: 'center',title: '终止排号', hide:true}
            ,{field: 'sCrnNo', align: 'center',title: '起始堆垛机号', hide:true}
            ,{field: 'eCrnNo', align: 'center',title: '终止堆垛机号', hide:true}
@@ -332,6 +333,7 @@
            whsType: $('#whsType').val(),
            wrkMk: $('#wrkMk').val(),
            currentRow: $('#currentRow').val(),
            currentCrnNo: $('#currentCrnNo').val(),
            sRow: $('#sRow').val(),
            eRow: $('#eRow').val(),
            crnQty: $('#crnQty').val(),
src/main/webapp/views/basCrnDepthRule/basCrnDepthRule.html
New file
@@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title></title>
    <meta content="webkit" name="renderer">
    <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
    <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
    <link href="../../static/layui/css/layui.css" media="all" rel="stylesheet">
    <link href="../../static/css/admin.css?v=318" media="all" rel="stylesheet">
    <link href="../../static/css/cool.css" media="all" rel="stylesheet">
</head>
<body>
<div class="layui-fluid">
    <div class="layui-card">
        <div class="layui-card-body">
            <div class="layui-form toolbar" id="search-box">
                <div class="layui-form-item">
                    <div class="layui-inline">
                        <div class="layui-input-inline">
                            <input autocomplete="off" class="layui-input" name="whsType" placeholder="仓库类型" type="number">
                        </div>
                    </div>
                    <div class="layui-inline">
                        <div class="layui-input-inline">
                            <input autocomplete="off" class="layui-input" name="crnNo" placeholder="堆垛机号" type="number">
                        </div>
                    </div>
                    <div class="layui-inline">&emsp;
                        <button class="layui-btn icon-btn" lay-filter="search" lay-submit>
                            <i class="layui-icon">&#xe615;</i>搜索
                        </button>
                        <button class="layui-btn icon-btn" lay-filter="reset" lay-submit>
                            <i class="layui-icon">&#xe666;</i>重置
                        </button>
                    </div>
                </div>
            </div>
            <table class="layui-hide" id="basCrnDepthRule" lay-filter="basCrnDepthRule"></table>
        </div>
    </div>
</div>
<script id="toolbar" type="text/html">
    <div class="layui-btn-container">
        <button class="layui-btn layui-btn-sm" lay-event="addData">新增</button>
        <button class="layui-btn layui-btn-sm" lay-event="templatePreview">模板预览</button>
        <button class="layui-btn layui-btn-sm" lay-event="templateGenerate">模板生成</button>
        <button class="layui-btn layui-btn-sm" lay-event="runtimePreview">运行预览</button>
        <button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="deleteData">删除</button>
        <button class="layui-btn layui-btn-primary layui-btn-sm" lay-event="exportData" style="float: right">导出</button>
    </div>
</script>
<script id="operate" type="text/html">
    <a class="layui-btn layui-btn-primary layui-btn-xs" lay-event="edit">修改</a>
    <a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<script id="editDialog" type="text/html">
    <form id="detail" lay-filter="detail" class="layui-form admin-form model-form">
        <input name="id" type="hidden">
        <div class="layui-row">
            <div class="layui-col-md6">
                <div class="layui-form-item">
                    <label class="layui-form-label layui-form-required">仓库</label>
                    <div class="layui-input-block">
                        <input class="layui-input" lay-verify="required|number" name="whsType" placeholder="请输入仓库类型" type="number">
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label layui-form-required">堆垛机</label>
                    <div class="layui-input-block">
                        <input class="layui-input" lay-verify="required|number" name="crnNo" placeholder="请输入堆垛机号" type="number">
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label layui-form-required">布局</label>
                    <div class="layui-input-block">
                        <select lay-verify="required" name="layoutType">
                            <option value="">请选择</option>
                            <option value="1">单伸</option>
                            <option value="2">双伸</option>
                        </select>
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label layui-form-required">搜索排</label>
                    <div class="layui-input-block">
                        <input class="layui-input" lay-verify="required" name="searchRowsCsv" placeholder="例: 2,3 或 2-3">
                    </div>
                </div>
            </div>
            <div class="layui-col-md6">
                <div class="layui-form-item">
                    <label class="layui-form-label">浅排</label>
                    <div class="layui-input-block">
                        <input class="layui-input" name="shallowRowsCsv" placeholder="单伸可留空,双伸必填">
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label">深排</label>
                    <div class="layui-input-block">
                        <input class="layui-input" name="deepRowsCsv" placeholder="双伸必填">
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label">启用</label>
                    <div class="layui-input-block">
                        <select name="enabled">
                            <option value="1">启用</option>
                            <option value="0">禁用</option>
                        </select>
                    </div>
                </div>
                <div class="layui-form-item">
                    <label class="layui-form-label">备注</label>
                    <div class="layui-input-block">
                        <textarea class="layui-textarea" name="memo" placeholder="备注"></textarea>
                    </div>
                </div>
            </div>
        </div>
        <hr class="layui-bg-gray">
        <div class="layui-form-item text-right">
            <button class="layui-btn" lay-filter="editSubmit" lay-submit="">保存</button>
            <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">取消</button>
        </div>
    </form>
</script>
<script id="templateDialog" type="text/html">
    <form id="templateForm" lay-filter="templateForm" class="layui-form admin-form model-form">
        <div class="layui-form-item">
            <label class="layui-form-label layui-form-required">仓库</label>
            <div class="layui-input-block">
                <input class="layui-input" lay-verify="required|number" name="whsType" placeholder="请输入仓库类型" type="number">
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label">开始堆垛机</label>
            <div class="layui-input-block">
                <input class="layui-input" name="startCrnNo" placeholder="可留空" type="number">
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label">结束堆垛机</label>
            <div class="layui-input-block">
                <input class="layui-input" name="endCrnNo" placeholder="可留空" type="number">
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label">启用</label>
            <div class="layui-input-block">
                <select name="enabled">
                    <option value="1">启用</option>
                    <option value="0">禁用</option>
                </select>
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label">备注</label>
            <div class="layui-input-block">
                <textarea class="layui-textarea" name="memo" placeholder="模板备注"></textarea>
            </div>
        </div>
        <div class="layui-form-item text-right">
            <button class="layui-btn" lay-filter="templateSubmit" lay-submit="">确定</button>
            <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">取消</button>
        </div>
    </form>
</script>
<script id="runtimeDialog" type="text/html">
    <form id="runtimeForm" lay-filter="runtimeForm" class="layui-form admin-form model-form">
        <div class="layui-form-item">
            <label class="layui-form-label layui-form-required">路径ID</label>
            <div class="layui-input-block">
                <input class="layui-input" lay-verify="required|number" name="staDescId" placeholder="请输入路径ID" type="number">
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label layui-form-required">源站</label>
            <div class="layui-input-block">
                <input class="layui-input" lay-verify="required|number" name="sourceStaNo" placeholder="请输入源站号" type="number">
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label">请求库区</label>
            <div class="layui-input-block">
                <input class="layui-input" name="outArea" placeholder="1/2/3" type="number">
            </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 表示空托盘">
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label">高低/宽窄/轻重</label>
            <div class="layui-input-block">
                <div class="layui-row layui-col-space10">
                    <div class="layui-col-xs4"><input class="layui-input" name="locType1" placeholder="locType1" type="number"></div>
                    <div class="layui-col-xs4"><input class="layui-input" name="locType2" placeholder="locType2" type="number"></div>
                    <div class="layui-col-xs4"><input class="layui-input" name="locType3" placeholder="locType3" type="number"></div>
                </div>
            </div>
        </div>
        <div class="layui-form-item text-right">
            <button class="layui-btn" lay-filter="runtimeSubmit" lay-submit="">预览</button>
            <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">取消</button>
        </div>
    </form>
</script>
<script src="../../static/js/jquery/jquery-3.3.1.min.js" type="text/javascript"></script>
<script charset="utf-8" src="../../static/layui/layui.js" type="text/javascript"></script>
<script charset="utf-8" src="../../static/js/common.js" type="text/javascript"></script>
<script charset="utf-8" src="../../static/js/cool.js" type="text/javascript"></script>
<script charset="utf-8" src="../../static/js/basCrnDepthRule/basCrnDepthRule.js" type="text/javascript"></script>
</body>
</html>
src/main/webapp/views/basDevp/basDevp_detail.html
@@ -153,7 +153,12 @@
        <div class="layui-inline"  style="width:31%;">
            <label class="layui-form-label">绑定库区:</label>
            <div class="layui-input-inline">
                <input id="area" class="layui-input" type="text" placeholder="空=不限制,支持 1/2/3 或 A/B/C">
                <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>
        </div>
        <div class="layui-inline"  style="width:31%;display: none">
src/main/webapp/views/rowLastno/rowLastno_detail.html
@@ -42,6 +42,12 @@
            </div>
        </div>
        <div class="layui-inline"  style="width:31%;">
            <label class="layui-form-label">当前堆垛机号:</label>
            <div class="layui-input-inline">
                <input id="currentCrnNo" class="layui-input" type="text">
            </div>
        </div>
        <div class="layui-inline"  style="width:31%;">
            <label class="layui-form-label">起始排号:</label>
            <div class="layui-input-inline">
                <input id="sRow" class="layui-input" type="text">
src/test/java/com/zy/asrs/service/impl/BasCrnDepthRuleServiceImplTest.java
New file
@@ -0,0 +1,157 @@
package com.zy.asrs.service.impl;
import com.zy.asrs.entity.BasCrnDepthRule;
import com.zy.asrs.entity.RowLastno;
import com.zy.asrs.entity.param.BasCrnDepthRuleTemplateParam;
import com.zy.asrs.service.RowLastnoService;
import com.zy.common.model.CrnDepthRuleProfile;
import com.zy.common.properties.SlaveProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class BasCrnDepthRuleServiceImplTest {
    @Mock
    private RowLastnoService rowLastnoService;
    private BasCrnDepthRuleServiceImpl service;
    @BeforeEach
    void setUp() {
        service = spy(new BasCrnDepthRuleServiceImpl());
        ReflectionTestUtils.setField(service, "rowLastnoService", rowLastnoService);
        ReflectionTestUtils.setField(service, "slaveProperties", buildSlaveProperties());
    }
    @Test
    void validateRule_forSingleExtension_fillsShallowRowsAndNormalizesCsv() {
        BasCrnDepthRule rule = new BasCrnDepthRule();
        rule.setWhsType(1);
        rule.setCrnNo(1);
        rule.setLayoutType(1);
        rule.setSearchRowsCsv("1, 2,2");
        service.validateRule(rule);
        assertEquals("1,2", rule.getSearchRowsCsv());
        assertEquals("1,2", rule.getShallowRowsCsv());
        assertEquals(null, rule.getDeepRowsCsv());
        assertEquals(Integer.valueOf(1), rule.getEnabled());
    }
    @Test
    void validateRule_forSingleExtension_rejectsDeepRows() {
        BasCrnDepthRule rule = new BasCrnDepthRule();
        rule.setWhsType(1);
        rule.setCrnNo(1);
        rule.setLayoutType(1);
        rule.setSearchRowsCsv("1,2");
        rule.setDeepRowsCsv("3");
        assertThrows(RuntimeException.class, () -> service.validateRule(rule));
    }
    @Test
    void validateRule_forDoubleExtension_requiresBothShallowAndDeepRows() {
        BasCrnDepthRule rule = new BasCrnDepthRule();
        rule.setWhsType(1);
        rule.setCrnNo(1);
        rule.setLayoutType(2);
        rule.setSearchRowsCsv("2,1");
        rule.setShallowRowsCsv("2");
        assertThrows(RuntimeException.class, () -> service.validateRule(rule));
    }
    @Test
    void resolveProfile_prefersConfiguredRule() {
        RowLastno rowLastno = new RowLastno();
        rowLastno.setWhsType(1);
        BasCrnDepthRule rule = new BasCrnDepthRule();
        rule.setWhsType(1);
        rule.setCrnNo(2);
        rule.setLayoutType(2);
        rule.setSearchRowsCsv("6,5");
        rule.setShallowRowsCsv("6");
        rule.setDeepRowsCsv("5");
        doReturn(rule).when(service).findEnabledRule(1, 2);
        CrnDepthRuleProfile profile = service.resolveProfile(rowLastno, 2, 6);
        assertEquals(Integer.valueOf(2), profile.getLayoutType());
        assertEquals(Arrays.asList(6, 5), profile.getSearchRows());
        assertEquals(Arrays.asList(6), profile.getShallowRows());
        assertEquals(Arrays.asList(5), profile.getDeepRows());
        assertEquals(Integer.valueOf(5), profile.getPairedDeepRow(6));
    }
    @Test
    void previewTemplate_buildsLegacyRowsForSingleExtensionWarehouse() {
        RowLastno rowLastno = new RowLastno();
        rowLastno.setWhsType(1);
        rowLastno.setsCrnNo(1);
        rowLastno.seteCrnNo(2);
        rowLastno.setsRow(1);
        rowLastno.seteRow(4);
        rowLastno.setTypeId(2);
        when(rowLastnoService.selectById(1)).thenReturn(rowLastno);
        BasCrnDepthRuleTemplateParam param = new BasCrnDepthRuleTemplateParam();
        param.setWhsType(1);
        List<BasCrnDepthRule> preview = service.previewTemplate(param);
        assertEquals(2, preview.size());
        assertEquals(Integer.valueOf(1), preview.get(0).getLayoutType());
        assertEquals("1,2", preview.get(0).getSearchRowsCsv());
        assertEquals("3,4", preview.get(1).getSearchRowsCsv());
    }
    @Test
    void previewTemplate_buildsLegacyRowsForDoubleExtensionWarehouse() {
        RowLastno rowLastno = new RowLastno();
        rowLastno.setWhsType(1);
        rowLastno.setsCrnNo(1);
        rowLastno.seteCrnNo(1);
        rowLastno.setsRow(1);
        rowLastno.seteRow(4);
        rowLastno.setTypeId(1);
        when(rowLastnoService.selectById(1)).thenReturn(rowLastno);
        BasCrnDepthRuleTemplateParam param = new BasCrnDepthRuleTemplateParam();
        param.setWhsType(1);
        List<BasCrnDepthRule> preview = service.previewTemplate(param);
        assertEquals(1, preview.size());
        assertEquals("2,3", preview.get(0).getSearchRowsCsv());
        assertTrue(preview.get(0).getShallowRowsCsv().contains("2"));
        assertTrue(preview.get(0).getDeepRowsCsv().contains("1"));
    }
    private SlaveProperties buildSlaveProperties() {
        SlaveProperties slaveProperties = new SlaveProperties();
        slaveProperties.setDoubleDeep(true);
        slaveProperties.setDoubleLocs(Arrays.asList(1, 4, 5, 8));
        slaveProperties.setDoubleLocsLeft(Arrays.asList(1, 5));
        slaveProperties.setDoubleLocsRight(Arrays.asList(4, 8));
        slaveProperties.setGroupCount(4);
        return slaveProperties;
    }
}
src/test/java/com/zy/common/service/CommonServiceLocTypeStrategyTest.java
New file
@@ -0,0 +1,165 @@
package com.zy.common.service;
import com.zy.asrs.entity.LocMast;
import com.zy.asrs.entity.result.FindLocNoAttributeVo;
import com.zy.common.model.LocTypeDto;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CommonServiceLocTypeStrategyTest {
    private final CommonService commonService = new CommonService();
    @Test
    void normalizeLocTypeDto_forFullPallet_ignoresLocType2AndLocType3() {
        FindLocNoAttributeVo attributeVo = new FindLocNoAttributeVo();
        attributeVo.setMatnr("MAT-001");
        LocTypeDto locTypeDto = new LocTypeDto();
        locTypeDto.setLocType1((short) 1);
        locTypeDto.setLocType2((short) 1);
        locTypeDto.setLocType3((short) 2);
        LocTypeDto normalized = ReflectionTestUtils.invokeMethod(
                commonService, "normalizeLocTypeDto", 1, attributeVo, locTypeDto);
        assertEquals(Short.valueOf((short) 1), normalized.getLocType1());
        org.junit.jupiter.api.Assertions.assertNull(normalized.getLocType2());
        org.junit.jupiter.api.Assertions.assertNull(normalized.getLocType3());
    }
    @Test
    void normalizeLocTypeDto_forEmptyPallet_prefersNarrowAndKeepsHeight() {
        FindLocNoAttributeVo attributeVo = new FindLocNoAttributeVo();
        attributeVo.setMatnr("emptyPallet");
        LocTypeDto locTypeDto = new LocTypeDto();
        locTypeDto.setLocType1((short) 1);
        locTypeDto.setLocType3((short) 2);
        LocTypeDto normalized = ReflectionTestUtils.invokeMethod(
                commonService, "normalizeLocTypeDto", 10, attributeVo, locTypeDto);
        assertEquals(Short.valueOf((short) 1), normalized.getLocType1());
        assertEquals(Short.valueOf((short) 1), normalized.getLocType2());
        assertEquals(Short.valueOf((short) 2), normalized.getLocType3());
    }
    @Test
    void buildRetryCompatibleLocTypeDto_forEmptyPallet_onlyRaisesHeightAndKeepsOtherLocTypes() {
        FindLocNoAttributeVo attributeVo = new FindLocNoAttributeVo();
        attributeVo.setMatnr("emptyPallet");
        LocTypeDto locTypeDto = new LocTypeDto();
        locTypeDto.setLocType1((short) 1);
        locTypeDto.setLocType2((short) 1);
        locTypeDto.setLocType3((short) 2);
        LocTypeDto compatible = ReflectionTestUtils.invokeMethod(
                commonService, "buildRetryCompatibleLocTypeDto", 10, attributeVo, locTypeDto);
        assertEquals(Short.valueOf((short) 2), compatible.getLocType1());
        assertEquals(Short.valueOf((short) 1), compatible.getLocType2());
        assertEquals(Short.valueOf((short) 2), compatible.getLocType3());
    }
    @Test
    void buildRetryCompatibleLocTypeDto_forFullPallet_onlyRaisesHeight() {
        FindLocNoAttributeVo attributeVo = new FindLocNoAttributeVo();
        attributeVo.setMatnr("MAT-001");
        LocTypeDto locTypeDto = new LocTypeDto();
        locTypeDto.setLocType1((short) 1);
        locTypeDto.setLocType2((short) 2);
        locTypeDto.setLocType3((short) 2);
        LocTypeDto normalized = ReflectionTestUtils.invokeMethod(
                commonService, "normalizeLocTypeDto", 1, attributeVo, locTypeDto);
        LocTypeDto compatible = ReflectionTestUtils.invokeMethod(
                commonService, "buildRetryCompatibleLocTypeDto", 1, attributeVo, normalized);
        assertEquals(Short.valueOf((short) 2), compatible.getLocType1());
        org.junit.jupiter.api.Assertions.assertNull(compatible.getLocType2());
        org.junit.jupiter.api.Assertions.assertNull(compatible.getLocType3());
    }
    @Test
    void wcsFullPalletPayload_whenLowLocExhausted_shouldRetryHighLocWithoutLocType2OrLocType3() {
        FindLocNoAttributeVo attributeVo = new FindLocNoAttributeVo();
        attributeVo.setMatnr("MAT-80000001");
        LocTypeDto requestLocType = new LocTypeDto();
        requestLocType.setLocType1((short) 1);
        requestLocType.setLocType2((short) 2);
        requestLocType.setLocType3((short) 2);
        LocTypeDto normalized = ReflectionTestUtils.invokeMethod(
                commonService, "normalizeLocTypeDto", 1, attributeVo, requestLocType);
        LocTypeDto compatible = ReflectionTestUtils.invokeMethod(
                commonService, "buildRetryCompatibleLocTypeDto", 1, attributeVo, normalized);
        assertEquals(Short.valueOf((short) 2), compatible.getLocType1());
        org.junit.jupiter.api.Assertions.assertNull(compatible.getLocType2());
        org.junit.jupiter.api.Assertions.assertNull(compatible.getLocType3());
    }
    @Test
    void matchesLocType_forFullPallet_rejectsNarrowLocType2() {
        FindLocNoAttributeVo attributeVo = new FindLocNoAttributeVo();
        attributeVo.setMatnr("MAT-001");
        LocTypeDto requestLocType = new LocTypeDto();
        requestLocType.setLocType1((short) 1);
        LocTypeDto normalized = ReflectionTestUtils.invokeMethod(
                commonService, "normalizeLocTypeDto", 1, attributeVo, requestLocType);
        LocMast narrowLoc = new LocMast();
        narrowLoc.setLocType1((short) 1);
        narrowLoc.setLocType2((short) 1);
        Boolean matched = ReflectionTestUtils.invokeMethod(
                commonService, "matchesLocType", narrowLoc, normalized);
        assertFalse(Boolean.TRUE.equals(matched));
    }
    @Test
    void matchesLocType_forFullPallet_acceptsNonNarrowLocType2() {
        FindLocNoAttributeVo attributeVo = new FindLocNoAttributeVo();
        attributeVo.setMatnr("MAT-001");
        LocTypeDto requestLocType = new LocTypeDto();
        requestLocType.setLocType1((short) 1);
        LocTypeDto normalized = ReflectionTestUtils.invokeMethod(
                commonService, "normalizeLocTypeDto", 1, attributeVo, requestLocType);
        LocMast wideLoc = new LocMast();
        wideLoc.setLocType1((short) 1);
        wideLoc.setLocType2((short) 2);
        Boolean matched = ReflectionTestUtils.invokeMethod(
                commonService, "matchesLocType", wideLoc, normalized);
        assertTrue(Boolean.TRUE.equals(matched));
    }
    @Test
    @SuppressWarnings("unchecked")
    void buildEmptyPalletSearchLocTypes_forLowLoc_returnsConfiguredFallbackOrder() {
        LocTypeDto locTypeDto = new LocTypeDto();
        locTypeDto.setLocType1((short) 1);
        locTypeDto.setLocType2((short) 1);
        locTypeDto.setLocType3((short) 2);
        List<LocTypeDto> stages = ReflectionTestUtils.invokeMethod(
                commonService, "buildEmptyPalletSearchLocTypes", locTypeDto);
        assertEquals(4, stages.size());
        assertEquals(Short.valueOf((short) 1), stages.get(0).getLocType1());
        assertEquals(Short.valueOf((short) 1), stages.get(0).getLocType2());
        assertEquals(Short.valueOf((short) 1), stages.get(1).getLocType1());
        org.junit.jupiter.api.Assertions.assertNull(stages.get(1).getLocType2());
        assertEquals(Short.valueOf((short) 2), stages.get(2).getLocType1());
        assertEquals(Short.valueOf((short) 1), stages.get(2).getLocType2());
        assertEquals(Short.valueOf((short) 2), stages.get(3).getLocType1());
        org.junit.jupiter.api.Assertions.assertNull(stages.get(3).getLocType2());
        assertEquals(Short.valueOf((short) 2), stages.get(3).getLocType3());
    }
}
src/test/java/com/zy/common/service/CommonServiceRun2AllocationTest.java
New file
@@ -0,0 +1,329 @@
package com.zy.common.service;
import com.baomidou.mybatisplus.mapper.Wrapper;
import com.zy.asrs.entity.BasCrnp;
import com.zy.asrs.entity.LocMast;
import com.zy.asrs.entity.RowLastno;
import com.zy.asrs.entity.RowLastnoType;
import com.zy.asrs.service.BasCrnDepthRuleService;
import com.zy.asrs.service.BasCrnpService;
import com.zy.asrs.service.BasDevpService;
import com.zy.asrs.service.LocMastService;
import com.zy.asrs.service.RowLastnoService;
import com.zy.asrs.service.StaDescService;
import com.zy.common.model.CrnDepthRuleProfile;
import com.zy.common.model.LocTypeDto;
import com.zy.common.model.StartupDto;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CommonServiceRun2AllocationTest {
    @Mock
    private BasCrnpService basCrnpService;
    @Mock
    private StaDescService staDescService;
    @Mock
    private BasDevpService basDevpService;
    @Mock
    private LocMastService locMastService;
    @Mock
    private BasCrnDepthRuleService basCrnDepthRuleService;
    @Mock
    private RowLastnoService rowLastnoService;
    private CommonService commonService;
    @BeforeEach
    void setUp() {
        commonService = new CommonService();
        ReflectionTestUtils.setField(commonService, "basCrnpService", basCrnpService);
        ReflectionTestUtils.setField(commonService, "staDescService", staDescService);
        ReflectionTestUtils.setField(commonService, "basDevpService", basDevpService);
        ReflectionTestUtils.setField(commonService, "locMastService", locMastService);
        ReflectionTestUtils.setField(commonService, "basCrnDepthRuleService", basCrnDepthRuleService);
        ReflectionTestUtils.setField(commonService, "rowLastnoService", rowLastnoService);
    }
    @Test
    void findRun2EmptyLocByCrnNos_shouldStopAtFirstMatchingCraneInOrder() {
        RowLastno rowLastno = singleExtensionRowLastno(1, 2, 1, 4);
        RowLastnoType rowLastnoType = standardRowLastnoType();
        StartupDto startupDto = new StartupDto();
        LocTypeDto locTypeDto = fullLocType((short) 1);
        LocMast firstCraneLoc = openLoc("0200101", 2, 2, 1, 1);
        when(basCrnpService.selectById(2)).thenReturn(activeCrn(2));
        when(basCrnDepthRuleService.resolveProfile(eq(rowLastno), eq(2), any()))
                .thenReturn(singleExtensionProfile(1, 2));
        when(locMastService.selectList(any())).thenReturn(Collections.singletonList(firstCraneLoc));
        LocMast result = ReflectionTestUtils.invokeMethod(commonService, "findRun2EmptyLocByCrnNos",
                rowLastno, rowLastnoType, Arrays.asList(2, 1), locTypeDto,
                1, 100, startupDto, 1, "run2-order", false);
        assertEquals("0200101", result.getLocNo());
        assertEquals(Integer.valueOf(2), result.getCrnNo());
        verify(locMastService, times(1)).selectList(any());
    }
    @Test
    void findRun2EmptyLocByCrnNos_shouldSkipInactiveCrane() {
        RowLastno rowLastno = singleExtensionRowLastno(1, 2, 1, 4);
        RowLastnoType rowLastnoType = standardRowLastnoType();
        StartupDto startupDto = new StartupDto();
        LocTypeDto locTypeDto = fullLocType((short) 1);
        LocMast secondCraneLoc = openLoc("0400101", 1, 4, 1, 1);
        when(basCrnpService.selectById(2)).thenReturn(inactiveCrn(2));
        when(basCrnpService.selectById(1)).thenReturn(activeCrn(1));
        when(basCrnDepthRuleService.resolveProfile(eq(rowLastno), eq(1), any()))
                .thenReturn(singleExtensionProfile(1, 4));
        when(locMastService.selectList(any())).thenReturn(Collections.singletonList(secondCraneLoc));
        LocMast result = ReflectionTestUtils.invokeMethod(commonService, "findRun2EmptyLocByCrnNos",
                rowLastno, rowLastnoType, Arrays.asList(2, 1), locTypeDto,
                1, 100, startupDto, 1, "run2-skip-offline", false);
        assertEquals("0400101", result.getLocNo());
        assertEquals(Integer.valueOf(1), result.getCrnNo());
        verify(locMastService, times(1)).selectList(any());
    }
    @Test
    void findOpenLocsByRow_forSingleExtension_shouldOrderByLevThenBay() {
        RowLastno rowLastno = singleExtensionRowLastno(1, 1, 1, 2);
        RowLastnoType rowLastnoType = standardRowLastnoType();
        when(locMastService.selectList(any())).thenReturn(Collections.emptyList());
        ReflectionTestUtils.invokeMethod(commonService, "findOpenLocsByRow",
                rowLastno, rowLastnoType, 1, 1, fullLocType((short) 1), true);
        @SuppressWarnings("rawtypes")
        ArgumentCaptor<Wrapper> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(locMastService).selectList(wrapperCaptor.capture());
        String sqlSegment = wrapperCaptor.getValue().getSqlSegment().toLowerCase();
        assertTrue(sqlSegment.contains("order by"));
        assertTrue(sqlSegment.indexOf("lev1") < sqlSegment.indexOf("bay1"));
    }
    @Test
    void findConfiguredEmptyLocForCrn_forDoubleExtension_shouldPreferDeepOpenLoc() {
        RowLastno rowLastno = doubleExtensionRowLastno();
        RowLastnoType rowLastnoType = standardRowLastnoType();
        LocMast shallowLoc = openLoc("0100101", 1, 1, 1, 1);
        LocMast deepOpenLoc = openLoc("0200101", 1, 2, 1, 1);
        when(basCrnDepthRuleService.resolveProfile(eq(rowLastno), eq(1), any()))
                .thenReturn(doubleExtensionProfile());
        when(locMastService.selectList(any())).thenReturn(Collections.singletonList(shallowLoc));
        when(locMastService.selectOne(any())).thenReturn(deepOpenLoc);
        LocMast result = ReflectionTestUtils.invokeMethod(commonService, "findConfiguredEmptyLocForCrn",
                rowLastno, rowLastnoType, 1, 1, fullLocType((short) 1));
        assertEquals("0200101", result.getLocNo());
    }
    @Test
    void findConfiguredEmptyLocForCrn_forDoubleExtension_shouldFallbackToShallowWhenDeepBlocked() {
        RowLastno rowLastno = doubleExtensionRowLastno();
        RowLastnoType rowLastnoType = standardRowLastnoType();
        LocMast shallowLoc = openLoc("0100101", 1, 1, 1, 1);
        LocMast deepBlockedLoc = blockedLoc("0200101", 1, 2, 1, 1, "F");
        when(basCrnDepthRuleService.resolveProfile(eq(rowLastno), eq(1), any()))
                .thenReturn(doubleExtensionProfile());
        when(locMastService.selectList(any())).thenReturn(Collections.singletonList(shallowLoc));
        when(locMastService.selectOne(any())).thenReturn(null, deepBlockedLoc);
        LocMast result = ReflectionTestUtils.invokeMethod(commonService, "findConfiguredEmptyLocForCrn",
                rowLastno, rowLastnoType, 1, 1, fullLocType((short) 1));
        assertEquals("0100101", result.getLocNo());
    }
    @Test
    void getNextRun2CurrentRow_shouldAdvanceWithinRunnableCrnsOnly() {
        RowLastno rowLastno = singleExtensionRowLastno(1, 4, 1, 8);
        Integer nextRow = ReflectionTestUtils.invokeMethod(commonService, "getNextRun2CurrentRow",
                rowLastno, Arrays.asList(2, 4), 2, 3);
        assertEquals(Integer.valueOf(7), nextRow);
    }
    @Test
    void resolveRun2CrnNo_shouldPreferCurrentCrnNoOverCurrentRow() {
        RowLastno rowLastno = singleExtensionRowLastno(1, 4, 1, 8);
        rowLastno.setCurrentRow(7);
        rowLastno.setCurrentCrnNo(2);
        Integer currentCrnNo = ReflectionTestUtils.invokeMethod(commonService, "resolveRun2CrnNo", rowLastno);
        assertEquals(Integer.valueOf(2), currentCrnNo);
    }
    @Test
    void resolveRun2CrnNo_shouldFallbackToStartCrnNoWhenCurrentCrnNoIsInvalid() {
        RowLastno rowLastno = singleExtensionRowLastno(2, 4, 3, 8);
        rowLastno.setCurrentCrnNo(null);
        assertEquals(Integer.valueOf(2),
                ReflectionTestUtils.invokeMethod(commonService, "resolveRun2CrnNo", rowLastno));
        rowLastno.setCurrentCrnNo(0);
        assertEquals(Integer.valueOf(2),
                ReflectionTestUtils.invokeMethod(commonService, "resolveRun2CrnNo", rowLastno));
        rowLastno.setCurrentCrnNo(5);
        assertEquals(Integer.valueOf(2),
                ReflectionTestUtils.invokeMethod(commonService, "resolveRun2CrnNo", rowLastno));
    }
    @Test
    void advanceNormalRun2Cursor_shouldUpdateCurrentRowAndCurrentCrnNoTogether() {
        RowLastno rowLastno = singleExtensionRowLastno(1, 4, 1, 8);
        rowLastno.setCurrentRow(1);
        rowLastno.setCurrentCrnNo(1);
        ReflectionTestUtils.invokeMethod(commonService, "advanceNormalRun2Cursor",
                rowLastno, 1, Arrays.asList(1, 3), 1);
        assertEquals(Integer.valueOf(5), rowLastno.getCurrentRow());
        assertEquals(Integer.valueOf(3), rowLastno.getCurrentCrnNo());
        verify(rowLastnoService).updateById(rowLastno);
    }
    @Test
    void advanceEmptyPalletRun2Cursor_shouldUpdateCurrentRowAndCurrentCrnNoTogether() throws Exception {
        RowLastno rowLastno = singleExtensionRowLastno(1, 4, 1, 8);
        rowLastno.setCurrentRow(5);
        rowLastno.setCurrentCrnNo(3);
        LocMast locMast = openLoc("0500101", 3, 5, 1, 1);
        Object searchResult = run2AreaSearchResult(locMast, rowLastno, Arrays.asList(1, 3));
        ReflectionTestUtils.invokeMethod(commonService, "advanceEmptyPalletRun2Cursor", searchResult, locMast);
        assertEquals(Integer.valueOf(1), rowLastno.getCurrentRow());
        assertEquals(Integer.valueOf(1), rowLastno.getCurrentCrnNo());
        verify(rowLastnoService).updateById(rowLastno);
    }
    private RowLastno singleExtensionRowLastno(int startCrnNo, int endCrnNo, int startRow, int endRow) {
        RowLastno rowLastno = new RowLastno();
        rowLastno.setWhsType(1);
        rowLastno.setsCrnNo(startCrnNo);
        rowLastno.seteCrnNo(endCrnNo);
        rowLastno.setsRow(startRow);
        rowLastno.seteRow(endRow);
        rowLastno.setTypeId(2);
        return rowLastno;
    }
    private RowLastno doubleExtensionRowLastno() {
        RowLastno rowLastno = new RowLastno();
        rowLastno.setWhsType(1);
        rowLastno.setsCrnNo(1);
        rowLastno.seteCrnNo(1);
        rowLastno.setsRow(1);
        rowLastno.seteRow(2);
        rowLastno.setTypeId(1);
        return rowLastno;
    }
    private RowLastnoType standardRowLastnoType() {
        RowLastnoType rowLastnoType = new RowLastnoType();
        rowLastnoType.setType(1);
        return rowLastnoType;
    }
    private Object run2AreaSearchResult(LocMast locMast, RowLastno rowLastno, List<Integer> runnableCrnNos) throws Exception {
        Class<?> clazz = Class.forName("com.zy.common.service.CommonService$Run2AreaSearchResult");
        Constructor<?> constructor = clazz.getDeclaredConstructor(LocMast.class, RowLastno.class, List.class);
        constructor.setAccessible(true);
        return constructor.newInstance(locMast, rowLastno, runnableCrnNos);
    }
    private CrnDepthRuleProfile singleExtensionProfile(int shallowRow, int searchRow) {
        CrnDepthRuleProfile profile = new CrnDepthRuleProfile();
        profile.setLayoutType(1);
        profile.setSearchRows(Collections.singletonList(searchRow));
        profile.setShallowRows(Collections.singletonList(shallowRow));
        return profile;
    }
    private CrnDepthRuleProfile doubleExtensionProfile() {
        CrnDepthRuleProfile profile = new CrnDepthRuleProfile();
        profile.setLayoutType(2);
        profile.setSearchRows(Arrays.asList(1, 2));
        profile.setShallowRows(Collections.singletonList(1));
        profile.setDeepRows(Collections.singletonList(2));
        profile.getShallowToDeepRow().put(1, 2);
        profile.getDeepToShallowRow().put(2, 1);
        return profile;
    }
    private BasCrnp activeCrn(int crnNo) {
        BasCrnp basCrnp = new BasCrnp();
        basCrnp.setCrnNo(crnNo);
        basCrnp.setInEnable("Y");
        basCrnp.setOutEnable("Y");
        basCrnp.setCrnSts(3);
        basCrnp.setCrnErr(0L);
        return basCrnp;
    }
    private BasCrnp inactiveCrn(int crnNo) {
        BasCrnp basCrnp = activeCrn(crnNo);
        basCrnp.setInEnable("N");
        return basCrnp;
    }
    private LocTypeDto fullLocType(short locType1) {
        LocTypeDto locTypeDto = new LocTypeDto();
        locTypeDto.setLocType1(locType1);
        return locTypeDto;
    }
    private LocMast openLoc(String locNo, int crnNo, int row, int bay, int lev) {
        LocMast locMast = new LocMast();
        locMast.setLocNo(locNo);
        locMast.setCrnNo(crnNo);
        locMast.setWhsType(1L);
        locMast.setRow1(row);
        locMast.setBay1(bay);
        locMast.setLev1(lev);
        locMast.setLocSts("O");
        locMast.setLocType1((short) 1);
        locMast.setLocType2((short) 2);
        return locMast;
    }
    private LocMast blockedLoc(String locNo, int crnNo, int row, int bay, int lev, String locSts) {
        LocMast locMast = openLoc(locNo, crnNo, row, bay, lev);
        locMast.setLocSts(locSts);
        return locMast;
    }
}