package com.zy.common.service; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.baomidou.mybatisplus.mapper.Wrapper; import com.core.common.Arith; 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; import com.zy.common.properties.SlaveProperties; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; /** * 货架核心功能 * Created by vincent on 2020/6/11 */ @Slf4j @Service public class CommonService { private static final int MIN_SPARE_LOC_COUNT = 2; private static class Run2AreaSearchResult { private final LocMast locMast; private final RowLastno rowLastno; private final List runnableCrnNos; /** * @param locMast 命中的空库位 * @param rowLastno 命中库区对应的轮询游标记录 * @param runnableCrnNos 当前库区可参与轮询的堆垛机顺序 */ private Run2AreaSearchResult(LocMast locMast, RowLastno rowLastno, List runnableCrnNos) { this.locMast = locMast; this.rowLastno = rowLastno; this.runnableCrnNos = runnableCrnNos; } } private static class Run2SearchResult { private final LocMast locMast; private final RowLastno rowLastno; private final List runnableCrnNos; private Run2SearchResult(LocMast locMast, RowLastno rowLastno, List 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 private WrkLastnoService wrkLastnoService; @Autowired private RowLastnoService rowLastnoService; @Autowired private RowLastnoTypeService rowLastnoTypeService; @Autowired private BasCrnpService basCrnpService; @Autowired private StaDescService staDescService; @Autowired private BasDevpService basDevpService; @Autowired private LocMastService locMastService; @Autowired private LocDetlService locDetlService; @Autowired private SlaveProperties slaveProperties; @Autowired private WrkDetlService wrkDetlService; @Autowired private BasCrnDepthRuleService basCrnDepthRuleService; /** * 生成工作号 * * @param wrkMk * @return workNo(工作号) */ public int getWorkNo(Integer wrkMk) { WrkLastno wrkLastno = wrkLastnoService.selectById(wrkMk); if (Cools.isEmpty(wrkLastno)) { throw new CoolException("数据异常,请联系管理员"); } int workNo = wrkLastno.getWrkNo(); int sNo = wrkLastno.getSNo(); int eNo = wrkLastno.getENo(); workNo = workNo >= eNo ? sNo : workNo + 1; while (true) { WrkMast wrkMast = wrkMastService.selectById(workNo); if (null != wrkMast) { workNo = workNo >= eNo ? sNo : workNo + 1; } else { break; } } // 修改序号记录 if (workNo > 0) { wrkLastno.setWrkNo(workNo); wrkLastnoService.updateById(wrkLastno); } // 检验 if (workNo == 0) { throw new CoolException("生成工作号失败,请联系管理员"); } else { if (wrkMastService.selectById(workNo) != null) { throw new CoolException("生成工作号" + workNo + "在工作档中已存在"); } } return workNo; } //拆盘机处空板扫码,驱动托盘向码垛位,不入库 @Transactional public StartupDto getScanBarcodeEmptyBoard() { StartupDto startupDto = new StartupDto(); Integer staNo = 0; if (wrkMastService.selectCount(new EntityWrapper().eq("io_type", 201).eq("staNo", 216)) < 2) { staNo = 216; } if (wrkMastService.selectCount(new EntityWrapper().eq("io_type", 201).eq("staNo", 220)) < 2) { staNo = 220; } startupDto.setStaNo(staNo); return startupDto; } /** * 检索库位号 * * @param staDescId 路径ID * @param sourceStaNo 源站 * @param findLocNoAttributeVo 属性 * @param locTypeDto 类型 * @return locNo 检索到的库位号 */ @Transactional public StartupDto getLocNo(Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto) { return getLocNo(staDescId, sourceStaNo, findLocNoAttributeVo, locTypeDto, null); } @Transactional public StartupDto getLocNo(Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto, List recommendRows) { try { locTypeDto = normalizeLocTypeDto(staDescId, findLocNoAttributeVo, locTypeDto); Integer whsType = Utils.GetWhsType(sourceStaNo); RowLastno rowLastno = rowLastnoService.selectById(whsType); RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId()); Integer preferredArea = resolvePreferredArea(sourceStaNo, findLocNoAttributeVo); if (preferredArea != null) { findLocNoAttributeVo.setOutArea(preferredArea); } /** * 库型 1: 标准堆垛机库 2: 平库 3: 穿梭板 4: 四向车 5: AGV 0: 未知 */ switch (rowLastnoType.getType()) { case 1: case 2: return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 0, locTypeDto, recommendRows, 0); case 3: return getLocNoRun(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 0, locTypeDto, 0); case 4: return getLocNoRun4(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 4, locTypeDto, 0); case 5: return getLocNoRun5(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, 0, locTypeDto, recommendRows, 0); default: throw new CoolException("站点=" + sourceStaNo + " 未查询到对应的库位规则"); } } catch (CoolException e) { log.error("站点={} 查找库位失败: {}", sourceStaNo, e.getMessage(), e); throw e; } catch (Exception e) { log.error("站点={} 查找库位异常", sourceStaNo, e); throw new CoolException("站点=" + sourceStaNo + " 查找库位失败"); } } /** * 空托盘识别规则: * 1. 以组托档物料编码 matnr=emptyPallet 为主,不再依赖 ioType=10。 * 2. 保留 staDescId=10 的兼容判断,避免旧链路还未切换时行为突变。 */ private boolean isEmptyPalletRequest(Integer staDescId, FindLocNoAttributeVo findLocNoAttributeVo) { if (findLocNoAttributeVo != null && "emptyPallet".equalsIgnoreCase(findLocNoAttributeVo.getMatnr())) { return true; } return staDescId != null && staDescId == 10; } /** * 统一整理入库规格,避免不同入口传入的 locType 不一致。 * * 空托盘的库位策略有两段: * 1. 首轮只限制 loc_type2=1,表示优先找窄库位。 * 2. loc_type1 高度信息必须保留,后续再按低位向高位兼容。 * * 非空托盘只保留 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)) { normalizedLocTypeDto.setLocType2(null); normalizedLocTypeDto.setLocType3(null); return normalizedLocTypeDto; } if (findLocNoAttributeVo != null && Cools.isEmpty(findLocNoAttributeVo.getMatnr())) { findLocNoAttributeVo.setMatnr("emptyPallet"); } 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 buildEmptyPalletSearchLocTypes(LocTypeDto locTypeDto) { LinkedHashSet searchLocTypes = new LinkedHashSet(); 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(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 applyLocTypeFilters(Wrapper wrapper, LocTypeDto locTypeDto, boolean includeLocType1) { if (wrapper == null || locTypeDto == null) { return wrapper; } 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()); // } return wrapper; } /** * 解析本次找位应优先使用的库区,站点绑定优先于接口传参。 */ private Integer resolvePreferredArea(Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo) { BasDevp sourceStation = basDevpService.selectById(sourceStaNo); Integer stationArea = parseArea(sourceStation == null ? null : sourceStation.getArea()); if (stationArea != null) { return stationArea; } Integer requestArea = findLocNoAttributeVo.getOutArea(); if (requestArea != null && requestArea >= 1 && requestArea <= 3) { return requestArea; } return null; } /** * 把站点维护的库区值统一解析成 1/2/3。 */ private Integer parseArea(String area) { if (Cools.isEmpty(area)) { return null; } String normalized = area.trim(); if (normalized.isEmpty()) { return null; } try { int areaNo = Integer.parseInt(normalized); return areaNo >= 1 && areaNo <= 3 ? areaNo : null; } catch (NumberFormatException ignored) { } String upper = normalized.toUpperCase(Locale.ROOT); if ("A".equals(upper) || "A区".equals(upper) || "A库".equals(upper) || "A库区".equals(upper)) { return 1; } if ("B".equals(upper) || "B区".equals(upper) || "B库".equals(upper) || "B库区".equals(upper)) { return 2; } if ("C".equals(upper) || "C区".equals(upper) || "C库".equals(upper) || "C库区".equals(upper)) { return 3; } return null; } /** * 读取 AGV 各库区对应的排配置。 */ private String getAgvAreaRowsConfig(Integer area) { Parameter parameter = Parameter.get(); if (parameter == null || area == null) { return null; } switch (area) { case 1: return parameter.getAgvArea1Rows(); case 2: return parameter.getAgvArea2Rows(); case 3: return parameter.getAgvArea3Rows(); default: return null; } } /** * 解析 AGV 指定库区的排顺序,缺配置时回退旧默认排。 */ private List getAgvAreaRows(Integer area, RowLastno rowLastno) { List configuredRows = parseAgvRows(getAgvAreaRowsConfig(area), rowLastno); if (!configuredRows.isEmpty()) { return configuredRows; } return getLegacyAgvRows(rowLastno); } /** * 汇总 AGV 所有库区的回退排顺序。 */ private List getAgvFallbackRows(RowLastno rowLastno) { LinkedHashSet rows = new LinkedHashSet<>(); for (int area = 1; area <= 3; area++) { rows.addAll(parseAgvRows(getAgvAreaRowsConfig(area), rowLastno)); } rows.addAll(getLegacyAgvRows(rowLastno)); return new ArrayList<>(rows); } /** * 把 AGV 库区排配置解析成有序排号列表。 */ private List parseAgvRows(String configValue, RowLastno rowLastno) { List rows = new ArrayList<>(); if (rowLastno == null || Cools.isEmpty(configValue)) { return rows; } LinkedHashSet orderedRows = new LinkedHashSet<>(); String normalized = configValue.replace(",", ",") .replace(";", ";") .replace("、", ",") .replaceAll("\\s+", ""); if (normalized.isEmpty()) { return rows; } for (String segment : normalized.split("[,;]")) { if (segment == null || segment.isEmpty()) { continue; } if (segment.contains("-")) { String[] rangeParts = segment.split("-", 2); Integer startRow = safeParseInt(rangeParts[0]); Integer endRow = safeParseInt(rangeParts[1]); if (startRow == null || endRow == null) { continue; } int step = startRow <= endRow ? 1 : -1; for (int row = startRow; step > 0 ? row <= endRow : row >= endRow; row += step) { addAgvRow(orderedRows, row, rowLastno); } continue; } addAgvRow(orderedRows, safeParseInt(segment), rowLastno); } rows.addAll(orderedRows); return rows; } /** * 返回 AGV 旧逻辑使用的默认排顺序。 */ private List getLegacyAgvRows(RowLastno rowLastno) { List rows = new ArrayList<>(); if (rowLastno == null) { return rows; } LinkedHashSet orderedRows = new LinkedHashSet<>(); int startRow = Math.min(38, rowLastno.geteRow()); int endRow = Math.max(32, rowLastno.getsRow()); if (startRow >= endRow) { for (int row = startRow; row >= endRow; row--) { addAgvRow(orderedRows, row, rowLastno); } } else { for (int row = rowLastno.geteRow(); row >= rowLastno.getsRow(); row--) { addAgvRow(orderedRows, row, rowLastno); } } rows.addAll(orderedRows); return rows; } /** * 仅在排号落在仓库范围内时追加到 AGV 排列表。 */ private void addAgvRow(LinkedHashSet rows, Integer row, RowLastno rowLastno) { if (rows == null || row == null || rowLastno == null) { return; } if (row < rowLastno.getsRow() || row > rowLastno.geteRow()) { return; } rows.add(row); } /** * 安全解析整数配置,失败时返回 null。 */ private Integer safeParseInt(String value) { if (Cools.isEmpty(value)) { return null; } try { return Integer.parseInt(value.trim()); } catch (NumberFormatException ignored) { return null; } } /** * 返回 AGV 各库区对应的 bay 范围。 */ private int[] getAgvAreaBayRange(Integer area) { if (area == null) { return new int[]{1, 19}; } switch (area) { case 1: return new int[]{1, 12}; case 2: return new int[]{13, 36}; case 3: return new int[]{37, 56}; default: return new int[]{1, 56}; } } /** * 按指定排和 bay 范围顺序搜索 AGV 可用库位。 */ private LocMast findAgvLocByRows(RowLastno rowLastno, RowLastnoType rowLastnoType, List rows, int startBay, int endBay, int curRow, int nearRow, LocTypeDto locTypeDto, boolean useDeepCheck) { for (Integer row : rows) { if (row == null) { continue; } Wrapper wrapper = new EntityWrapper() .eq("row1", row) .ge("bay1", startBay) .le("bay1", endBay) .eq("loc_sts", "O"); applyLocTypeFilters(wrapper, locTypeDto, true); wrapper.orderBy("lev1", true).orderBy("bay1", true); List locMasts = locMastService.selectList(wrapper); for (LocMast candidate : locMasts) { if (!VersionUtils.locMoveCheckLocTypeComplete(candidate, locTypeDto)) { continue; } if (useDeepCheck) { if (!Utils.BooleanWhsTypeStaIoType(rowLastno)) { continue; } LocMast deepLoc = locMastService.selectLocByLocStsPakInO(curRow, nearRow, candidate, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(deepLoc) && deepLoc.getRow1() == curRow) { return deepLoc; } continue; } return candidate; } } return null; } /** * 读取 run2 各库区对应的排配置,缺失时复用 AGV 库区配置。 */ private String getRun2AreaRowsConfig(Integer area) { Parameter parameter = Parameter.get(); if (parameter == null || area == null) { return null; } String run2Config; switch (area) { case 1: run2Config = parameter.getRun2Area1Rows(); break; case 2: run2Config = parameter.getRun2Area2Rows(); break; case 3: run2Config = parameter.getRun2Area3Rows(); break; default: return null; } return Cools.isEmpty(run2Config) ? getAgvAreaRowsConfig(area) : run2Config; } /** * 解析 run2 指定库区的排顺序,缺配置时回退旧默认排。 */ private List getRun2AreaRows(Integer area, RowLastno rowLastno) { List configuredRows = parseAgvRows(getRun2AreaRowsConfig(area), rowLastno); if (!configuredRows.isEmpty()) { return configuredRows; } return getLegacyAgvRows(rowLastno); } /** * 汇总 run2 其它库区的回退排顺序。 */ private List getRun2FallbackRows(RowLastno rowLastno) { LinkedHashSet rows = new LinkedHashSet<>(); for (int area = 1; area <= 3; area++) { rows.addAll(parseAgvRows(getRun2AreaRowsConfig(area), rowLastno)); } rows.addAll(getLegacyAgvRows(rowLastno)); return new ArrayList<>(rows); } /** * 解析 run2 的起始堆垛机号,优先使用 currentCrnNo。 */ private Integer resolveRun2CrnNo(RowLastno rowLastno) { if (rowLastno == null) { return null; } 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 = getRun2StartCrnNo(rowLastno); if (currentRow == null || currentRow <= 0) { return startCrnNo; } int offset = Math.max(currentRow - startRow, 0) / rowSpan; int crnNo = startCrnNo + offset; 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 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) { rowSpan = 2; } int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow(); int endRow = rowLastno.geteRow() == null ? currentRow : rowLastno.geteRow(); int lastStartRow = Math.max(startRow, endRow - rowSpan + 1); if (currentRow >= lastStartRow) { return startRow; } return currentRow + rowSpan; } /** * 从指定起点开始生成完整的堆垛机轮询顺序。 */ private List getOrderedCrnNos(RowLastno rowLastno, Integer startCrnNo) { List orderedCrnNos = new ArrayList<>(); if (rowLastno == null) { return orderedCrnNos; } int start = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo(); int end = rowLastno.geteCrnNo() == null ? start + rowLastno.getCrnQty() - 1 : rowLastno.geteCrnNo(); int first = startCrnNo == null ? start : startCrnNo; if (first < start || first > end) { first = start; } for (int crnNo = first; crnNo <= end; crnNo++) { orderedCrnNos.add(crnNo); } for (int crnNo = start; crnNo < first; crnNo++) { orderedCrnNos.add(crnNo); } return orderedCrnNos; } /** * 优先按 s_crn_no/e_crn_no 反推真实堆垛机数量。 * * 之前有逻辑错误把 asr_row_lastno.crn_qty 当成轮询游标写回, * 导致“堆垛机数量”字段被污染。后续找位不能再直接信任 crn_qty, * 否则会少扫堆垛机,表现出来就是任务长期偏向某几台堆垛机。 */ private int resolveCrnCount(RowLastno rowLastno) { if (rowLastno == null) { return 0; } if (rowLastno.getsCrnNo() != null && rowLastno.geteCrnNo() != null && rowLastno.geteCrnNo() >= rowLastno.getsCrnNo()) { return rowLastno.geteCrnNo() - rowLastno.getsCrnNo() + 1; } if (rowLastno.getCrnQty() != null && rowLastno.getCrnQty() > 0) { return rowLastno.getCrnQty(); } Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId()); if (rowSpan != null && rowSpan > 0 && rowLastno.getsRow() != null && rowLastno.geteRow() != null && rowLastno.geteRow() >= rowLastno.getsRow()) { return (rowLastno.geteRow() - rowLastno.getsRow() + 1 + rowSpan - 1) / rowSpan; } return 0; } private Integer getCrnStartRow(RowLastno rowLastno, Integer crnNo) { if (rowLastno == null || crnNo == null) { return null; } Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId()); if (rowSpan == null || rowSpan <= 0) { return null; } int startCrnNo = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo(); int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow(); if (crnNo < startCrnNo) { return null; } return startRow + (crnNo - startCrnNo) * rowSpan; } /** * 按“可入 + 自动连线 + 无故障”判断堆垛机是否在线可分配。 */ 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: * 普通入库必须同时满足设备可入、设备无故障、并且当前源站到该堆垛机存在有效目标站路径。 * * routeRequired=false: * 空托盘跨库区找位时,只校验设备主档、故障和可入状态,不把 sta_desc 路径当成拦截条件。 * 这样做是为了先满足“能找到库位”,目标站路径由后续主数据补齐。 */ private boolean canRun2CrnAcceptPakin(RowLastno rowLastno, Integer staDescId, Integer sourceStaNo, Integer crnNo) { return canRun2CrnAcceptPakin(rowLastno, staDescId, sourceStaNo, crnNo, true); } private boolean canRun2CrnAcceptPakin(RowLastno rowLastno, Integer staDescId, Integer sourceStaNo, Integer crnNo, boolean routeRequired) { if (!isCrnActive(crnNo)) { return false; } if (!routeRequired) { return true; } if (!Utils.BooleanWhsTypeSta(rowLastno, staDescId)) { return true; } StaDesc staDesc = staDescService.selectOne(new EntityWrapper() .eq("type_no", staDescId) .eq("stn_no", sourceStaNo) .eq("crn_no", crnNo)); return !Cools.isEmpty(staDesc) && !Cools.isEmpty(staDesc.getCrnStn()); } /** * 按既定轮询顺序过滤出真正可参与本次找位的堆垛机列表。 */ private List getOrderedRunnableRun2CrnNos(RowLastno rowLastno, Integer staDescId, Integer sourceStaNo, List orderedCrnNos) { return getOrderedRunnableRun2CrnNos(rowLastno, staDescId, sourceStaNo, orderedCrnNos, true); } private List getOrderedRunnableRun2CrnNos(RowLastno rowLastno, Integer staDescId, Integer sourceStaNo, List orderedCrnNos, boolean routeRequired) { List runnableCrnNos = new ArrayList<>(); if (Cools.isEmpty(orderedCrnNos)) { return runnableCrnNos; } for (Integer candidateCrnNo : orderedCrnNos) { if (canRun2CrnAcceptPakin(rowLastno, staDescId, sourceStaNo, candidateCrnNo, routeRequired)) { runnableCrnNos.add(candidateCrnNo); } } return runnableCrnNos; } /** * 根据本次命中的堆垛机,把轮询游标推进到下一台可参与轮询的堆垛机。 */ private int getNextRun2CurrentRow(RowLastno rowLastno, List runnableCrnNos, Integer selectedCrnNo, int currentRow) { if (Cools.isEmpty(runnableCrnNos) || selectedCrnNo == null) { return getNextRun2CurrentRow(rowLastno, currentRow); } int index = runnableCrnNos.indexOf(selectedCrnNo); if (index < 0) { return getNextRun2CurrentRow(rowLastno, currentRow); } Integer nextCrnNo = runnableCrnNos.get((index + 1) % runnableCrnNos.size()); Integer nextRow = getCrnStartRow(rowLastno, nextCrnNo); return nextRow == null ? getNextRun2CurrentRow(rowLastno, currentRow) : nextRow; } private Run2Cursor getNextRun2Cursor(RowLastno rowLastno, List 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 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); } /** * 构造空托盘跨库区搜索顺序: * 先当前库区,再依次补足其它库区,避免重复。 */ private List buildAreaSearchOrder(Integer preferredArea) { LinkedHashSet areaOrder = new LinkedHashSet<>(); if (preferredArea != null && preferredArea >= 1 && preferredArea <= 3) { areaOrder.add(preferredArea); } for (int area = 1; area <= 3; area++) { areaOrder.add(area); } return new ArrayList<>(areaOrder); } /** * 预览 run2 当前会参与的库区、堆垛机顺序和深浅排画像,不落任务档。 */ public Map 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 result = new HashMap(); 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 orderedCrnNos = getOrderedCrnNos(rowLastno, resolveRun2CrnNo(rowLastno)); List runnableCrnNos = getOrderedRunnableRun2CrnNos(rowLastno, param.getStaDescId(), param.getSourceStaNo(), orderedCrnNos); result.put("orderedCrnNos", orderedCrnNos); result.put("runnableCrnNos", runnableCrnNos); if (emptyPalletRequest) { List areaSearchOrder = buildAreaSearchOrder(preferredArea); List> searchStages = new ArrayList>(); for (LocTypeDto stageLocTypeDto : buildEmptyPalletSearchLocTypes(locTypeDto)) { List> areaPreviews = new ArrayList>(); for (Integer area : areaSearchOrder) { RowLastno areaRowLastno = getAreaRowLastno(area, rowLastno); RowLastnoType areaRowLastnoType = rowLastnoTypeService.selectById(areaRowLastno.getTypeId()); List areaOrderedCrnNos = getOrderedCrnNos(areaRowLastno, resolveRun2CrnNo(areaRowLastno)); List areaRunnableCrnNos = getOrderedRunnableRun2CrnNos(areaRowLastno, param.getStaDescId(), param.getSourceStaNo(), areaOrderedCrnNos, false); Map areaItem = new HashMap(); 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 stageItem = new HashMap(); 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 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> buildRun2ProfilePreview(RowLastno rowLastno, RowLastnoType rowLastnoType, List crnNos, Integer staDescId, Integer sourceStaNo, LocTypeDto locTypeDto) { List> profiles = new ArrayList>(); if (Cools.isEmpty(crnNos)) { return profiles; } for (Integer crnNo : crnNos) { CrnDepthRuleProfile profile = basCrnDepthRuleService.resolveProfile(rowLastno, crnNo, getCrnStartRow(rowLastno, crnNo)); Map item = new HashMap(); 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; } /** * 根据库区取该库区对应的轮询记录。 * 当前库里 1/2/3 库区正好复用了 asr_row_lastno 的 whs_type 主键,因此这里直接按 area 取。 * 如果缺主数据,就回退到当前源站所在仓的 rowLastno,避免直接空指针。 */ private RowLastno getAreaRowLastno(Integer area, RowLastno defaultRowLastno) { if (area != null && area > 0) { RowLastno areaRowLastno = rowLastnoService.selectById(area); if (!Cools.isEmpty(areaRowLastno)) { return areaRowLastno; } } return defaultRowLastno; } /** * 空托盘 run2 专用搜索链路。 * * 执行顺序: * 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, String stageCode) { for (Integer area : buildAreaSearchOrder(preferredArea)) { RowLastno areaRowLastno = getAreaRowLastno(area, defaultRowLastno); if (Cools.isEmpty(areaRowLastno)) { continue; } RowLastnoType areaRowLastnoType = rowLastnoTypeService.selectById(areaRowLastno.getTypeId()); if (Cools.isEmpty(areaRowLastnoType)) { continue; } Integer areaStartCrnNo = resolveRun2CrnNo(areaRowLastno); List orderedAreaCrnNos = getOrderedCrnNos(areaRowLastno, areaStartCrnNo); // 空托盘跨库区时只筛设备主档和故障状态,不强依赖 sta_desc。 List runnableAreaCrnNos = getOrderedRunnableRun2CrnNos(areaRowLastno, staDescId, sourceStaNo, orderedAreaCrnNos, false); if (Cools.isEmpty(runnableAreaCrnNos)) { continue; } LocMast locMast = findRun2EmptyLocByCrnNos(areaRowLastno, areaRowLastnoType, runnableAreaCrnNos, locTypeDto, staDescId, sourceStaNo, startupDto, area, stageCode + "-area-" + area, false); if (!Cools.isEmpty(locMast)) { return new Run2AreaSearchResult(locMast, areaRowLastno, runnableAreaCrnNos); } } return null; } /** * 空托盘命中库位后,按命中库区回写该库区自己的轮询游标。 * 这样 A/B/C 三个库区会分别维护各自的“下一台堆垛机”,不会互相覆盖。 */ private void advanceEmptyPalletRun2Cursor(Run2AreaSearchResult searchResult, LocMast locMast) { if (searchResult == null || searchResult.rowLastno == null || locMast == null) { return; } RowLastno updateRowLastno = searchResult.rowLastno; int currentRow = updateRowLastno.getCurrentRow() == null ? 0 : updateRowLastno.getCurrentRow(); updateRun2Cursor(updateRowLastno, searchResult.runnableCrnNos, locMast.getCrnNo(), currentRow); } /** * 普通物料 run2 找位主流程。 * * 执行顺序: * 1. 先看站点是否配置了“堆垛机 + 库位类型”优先级。 * 2. 没配库区时,直接按当前 run2 轮询顺序找位。 * 3. 配了库区时,只在当前库区查找,普通托盘不跨库区兜底。 */ private Run2SearchResult findNormalRun2Loc(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer sourceStaNo, Integer staDescId, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto, StartupDto startupDto, Integer preferredArea, List orderedCrnNos) { List> stationCrnLocTypes = Utils.getStationStorageAreaName( sourceStaNo, locTypeDto == null || locTypeDto.getLocType1() == null ? null : locTypeDto.getLocType1().intValue(), findLocNoAttributeVo == null ? null : findLocNoAttributeVo.getMatnr()); if (!Cools.isEmpty(stationCrnLocTypes)) { // 站点优先级只是“优先尝试”,没有命中时必须继续走默认/库区回退, // 否则会把“优先候选无位”误判成“整仓无位”。 LocMast locMast = findRun2EmptyLocByCrnLocTypeEntries(rowLastno, rowLastnoType, stationCrnLocTypes, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "station-priority"); if (!Cools.isEmpty(locMast)) { return new Run2SearchResult(locMast, rowLastno, getOrderedRunnableRun2CrnNos(rowLastno, staDescId, sourceStaNo, extractCrnNos(stationCrnLocTypes))); } } List candidateCrnNos; if (preferredArea == null) { candidateCrnNos = new ArrayList<>(orderedCrnNos); } else { candidateCrnNos = filterCrnNosByRows(rowLastno, orderedCrnNos, getRun2AreaRows(preferredArea, rowLastno)); } List 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) { updateRun2Cursor(rowLastno, null, null, curRow); } /** * 普通物料游标优先按“本次真正可用的堆垛机集合”推进。 * * 这样即使库区定义里存在不存在的堆垛机,或者路径主数据只覆盖部分堆垛机, * 满板任务也会在真实可作业的堆垛机之间轮询,不会因为理论堆垛机号的空洞而长期回落到同一台。 */ private void advanceNormalRun2Cursor(RowLastno rowLastno, int curRow, List runnableCrnNos, Integer selectedCrnNo) { updateRun2Cursor(rowLastno, runnableCrnNos, selectedCrnNo, curRow); } /** * 根据排范围把整仓堆垛机顺序裁剪为某个库区内的堆垛机集合。 */ private List filterCrnNosByRows(RowLastno rowLastno, List orderedCrnNos, List rows) { if (Cools.isEmpty(rows)) { return new ArrayList<>(orderedCrnNos); } LinkedHashSet rowSet = new LinkedHashSet<>(rows); List result = new ArrayList<>(); Integer rowSpan = getCrnRowSpan(rowLastno.getTypeId()); if (rowSpan == null || rowSpan <= 0) { rowSpan = 2; } int startCrnNo = rowLastno.getsCrnNo() == null ? 1 : rowLastno.getsCrnNo(); int startRow = rowLastno.getsRow() == null ? 1 : rowLastno.getsRow(); int endRow = rowLastno.geteRow() == null ? Integer.MAX_VALUE : rowLastno.geteRow(); for (Integer crnNo : orderedCrnNos) { if (crnNo == null || crnNo < startCrnNo) { continue; } int crnOffset = crnNo - startCrnNo; int crnStartRow = startRow + crnOffset * rowSpan; for (int row = crnStartRow; row < crnStartRow + rowSpan && row <= endRow; row++) { if (rowSet.contains(row)) { result.add(crnNo); break; } } } return result; } /** * 解析当前源站到目标堆垛机的入库目标站号。 */ private Integer resolveTargetStaNo(RowLastno rowLastno, Integer staDescId, Integer sourceStaNo, Integer crnNo) { if (!Utils.BooleanWhsTypeSta(rowLastno, staDescId)) { return null; } StaDesc staDesc = staDescService.selectOne(new EntityWrapper() .eq("type_no", staDescId) .eq("stn_no", sourceStaNo) .eq("crn_no", crnNo)); if (Cools.isEmpty(staDesc)) { log.error("type_no={},stn_no={},crn_no={}", staDescId, sourceStaNo, crnNo); return null; } BasDevp staNo = basDevpService.selectById(staDesc.getCrnStn()); if (Cools.isEmpty(staNo)) { log.error("目标站{}不存在", staDesc.getCrnStn()); return null; } return staNo.getDevNo(); } private void logRun2NoMatch(String stage, Integer sourceStaNo, Integer preferredArea, List candidateCrnNos, LocTypeDto locTypeDto, List crnErrorCrns, List routeBlockedCrns, List noEmptyCrns, List locTypeBlockedCrns) { log.warn("run2 no location. stage={}, sourceStaNo={}, preferredArea={}, candidateCrnNos={}, crnErrorCrns={}, routeBlockedCrns={}, noEmptyCrns={}, locTypeBlockedCrns={}, spec={}", stage, sourceStaNo, preferredArea, candidateCrnNos, crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns, JSON.toJSONString(locTypeDto)); } /** * 按给定堆垛机顺序依次找空库位。 * * 这里同时承担三类过滤: * 1. 设备侧过滤:堆垛机故障或不存在时直接跳过。 * 2. 路径侧过滤:当前源站到该堆垛机目标站无路径时跳过。 * 3. 库位侧过滤:无空库位或库位规格不匹配时跳过。 * * 对空托盘来说,candidateCrnNos 由 run2 的轮询顺序生成,因此天然具备“均分到每个堆垛机”的效果。 */ private LocMast findRun2EmptyLocByCrnNos(RowLastno rowLastno, RowLastnoType rowLastnoType, List candidateCrnNos, LocTypeDto locTypeDto, Integer staDescId, Integer sourceStaNo, StartupDto startupDto, Integer preferredArea, String stage) { return findRun2EmptyLocByCrnNos(rowLastno, rowLastnoType, candidateCrnNos, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, stage, true); } private LocMast findRun2EmptyLocByCrnNos(RowLastno rowLastno, RowLastnoType rowLastnoType, List candidateCrnNos, LocTypeDto locTypeDto, Integer staDescId, Integer sourceStaNo, StartupDto startupDto, Integer preferredArea, String stage, boolean routeRequired) { if (Cools.isEmpty(candidateCrnNos)) { log.warn("run2 skip empty candidate list. stage={}, sourceStaNo={}, preferredArea={}, spec={}", stage, sourceStaNo, preferredArea, JSON.toJSONString(locTypeDto)); return null; } List crnErrorCrns = new ArrayList<>(); List routeBlockedCrns = new ArrayList<>(); List noEmptyCrns = new ArrayList<>(); List locTypeBlockedCrns = new ArrayList<>(); 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 candidateCrnNos, LocTypeDto locTypeDto, Integer staDescId, Integer sourceStaNo, StartupDto startupDto, Integer preferredArea, String stage, boolean routeRequired, int index, List crnErrorCrns, List routeBlockedCrns, List noEmptyCrns, List 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> crnLocTypeEntries, LocTypeDto locTypeDto, Integer staDescId, Integer sourceStaNo, StartupDto startupDto, Integer preferredArea, String stage) { if (Cools.isEmpty(crnLocTypeEntries)) { log.warn("run2 skip empty crn-locType list. stage={}, sourceStaNo={}, preferredArea={}, spec={}", stage, sourceStaNo, preferredArea, JSON.toJSONString(locTypeDto)); return null; } List candidateCrnNos = extractCrnNos(crnLocTypeEntries); List crnErrorCrns = new ArrayList<>(); List routeBlockedCrns = new ArrayList<>(); List noEmptyCrns = new ArrayList<>(); List locTypeBlockedCrns = new ArrayList<>(); return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, stage, 0, candidateCrnNos, crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns); } private LocMast findRun2EmptyLocByCrnLocTypeEntriesRecursively(RowLastno rowLastno, RowLastnoType rowLastnoType, List> crnLocTypeEntries, LocTypeDto locTypeDto, Integer staDescId, Integer sourceStaNo, StartupDto startupDto, Integer preferredArea, String stage, int index, List candidateCrnNos, List crnErrorCrns, List routeBlockedCrns, List noEmptyCrns, List locTypeBlockedCrns) { if (index >= crnLocTypeEntries.size()) { logRun2NoMatch(stage, sourceStaNo, preferredArea, candidateCrnNos, locTypeDto, crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns); return null; } Map crnLocTypeEntry = crnLocTypeEntries.get(index); Integer candidateCrnNo = crnLocTypeEntry == null ? null : crnLocTypeEntry.get("crnNo"); Short candidateLocType1 = crnLocTypeEntry == null || crnLocTypeEntry.get("locType1") == null ? null : crnLocTypeEntry.get("locType1").shortValue(); if (!isCrnActive(candidateCrnNo)) { crnErrorCrns.add(candidateCrnNo); return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos, crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns); } Integer targetStaNo = resolveTargetStaNo(rowLastno, staDescId, sourceStaNo, candidateCrnNo); if (Utils.BooleanWhsTypeSta(rowLastno, staDescId) && targetStaNo == null) { routeBlockedCrns.add(candidateCrnNo); return findRun2EmptyLocByCrnLocTypeEntriesRecursively(rowLastno, rowLastnoType, crnLocTypeEntries, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, stage, index + 1, candidateCrnNos, crnErrorCrns, routeBlockedCrns, noEmptyCrns, locTypeBlockedCrns); } LocTypeDto searchLocTypeDto = buildRun2SearchLocTypeDto(locTypeDto, candidateLocType1); Integer preferredNearRow = getCrnStartRow(rowLastno, candidateCrnNo); LocMast candidateLoc = findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, candidateCrnNo, preferredNearRow, searchLocTypeDto); if (Cools.isEmpty(candidateLoc)) { 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); } if (targetStaNo != null) { startupDto.setStaNo(targetStaNo); } return candidateLoc; } /** * 从站点优先级配置中抽取不重复的堆垛机顺序。 */ private List extractCrnNos(List> crnLocTypeEntries) { LinkedHashSet orderedCrnNos = new LinkedHashSet<>(); if (Cools.isEmpty(crnLocTypeEntries)) { return new ArrayList<>(); } for (Map crnLocTypeEntry : crnLocTypeEntries) { if (crnLocTypeEntry == null || crnLocTypeEntry.get("crnNo") == null) { continue; } orderedCrnNos.add(crnLocTypeEntry.get("crnNo")); } return new ArrayList<>(orderedCrnNos); } /** * 用站点优先级里的 locType1 覆盖本次查询的高度条件。 */ private LocTypeDto buildRun2SearchLocTypeDto(LocTypeDto locTypeDto, Short candidateLocType1) { if (locTypeDto == null && candidateLocType1 == null) { return null; } LocTypeDto searchLocTypeDto = new LocTypeDto(); if (locTypeDto != null) { searchLocTypeDto.setLocType1(locTypeDto.getLocType1()); searchLocTypeDto.setLocType2(locTypeDto.getLocType2()); searchLocTypeDto.setLocType3(locTypeDto.getLocType3()); searchLocTypeDto.setSiteId(locTypeDto.getSiteId()); } if (candidateLocType1 != null) { searchLocTypeDto.setLocType1(candidateLocType1); } return searchLocTypeDto; } /** * 按站点优先配置直接查某台堆垛机上的第一个可用空库位。 */ private LocMast findRun2OrderedEmptyLocByCrnLocType(RowLastnoType rowLastnoType, Integer candidateCrnNo, Short candidateLocType1, LocTypeDto locTypeDto) { if (candidateCrnNo == null) { return null; } Wrapper wrapper = new EntityWrapper() .eq("crn_no", candidateCrnNo) .eq("loc_sts", "O"); if (candidateLocType1 != null) { wrapper.eq("loc_type1", candidateLocType1); } applyLocTypeFilters(wrapper, locTypeDto, false); // 单伸堆垛机按层、列递增顺序找第一个空库位。 if (rowLastnoType != null && rowLastnoType.getType() != null && (rowLastnoType.getType() == 1 || rowLastnoType.getType() == 2)) { wrapper.orderBy("lev1", true).orderBy("bay1", true); } else { wrapper.orderBy("lev1", true).orderBy("bay1", true); } LocMast candidateLoc = locMastService.selectOne(wrapper); if (Cools.isEmpty(candidateLoc)) { return null; } if (locTypeDto != null && !VersionUtils.locMoveCheckLocTypeComplete(candidateLoc, locTypeDto)) { return null; } return candidateLoc; } private Optional findAvailableCrnAndNearRow(RowLastno rowLastno, int curRow, int crnNumber, int times, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto, RowLastnoType rowLastnoType) { int attempt = times; while (attempt < crnNumber * 4) { int[] params = Utils.LocNecessaryParameters(rowLastno, curRow, crnNumber); curRow = params[1]; int crnNo = params[2]; // if (!basCrnpService.checkSiteError(crnNo, true)) { // attempt++; // continue; // } int rowCount = params[0]; int nearRow = params[3]; // 只取数量判断,避免拉整 list int availableLocCount = locMastService.selectCount(new EntityWrapper() .eq("row1", nearRow) .eq("loc_sts", "O") .eq("whs_type", 1)); int crnCountO = wrkMastService.selectCount(new EntityWrapper() .eq("crn_no", crnNo).le("io_type", 100)); if (availableLocCount - crnCountO <= 2) { // 可以提成常量,比如 MIN_SPARE_SLOTS = 2 log.error("{}号堆垛机没有空库位!!! 尺寸规格: {}, 轮询次数:{}", crnNo, JSON.toJSONString(locTypeDto), attempt); attempt++; continue; } return Optional.of(new CrnRowInfo(crnNo, nearRow, curRow, rowCount, attempt)); } return Optional.empty(); } private Optional findBalancedCrnAndNearRow(RowLastno rowLastno, int curRow, int crnNumber, int times, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto, RowLastnoType rowLastnoType) { int maxScanTimes = Math.max(crnNumber * 2, 1); int scanCurRow = curRow; Map availableLocCountCache = new HashMap<>(); Map crnTaskCountCache = new HashMap<>(); CrnRowInfo bestInfo = null; int bestTaskCount = Integer.MAX_VALUE; int bestSpareLocCount = Integer.MIN_VALUE; int bestOffset = Integer.MAX_VALUE; CrnRowInfo fallbackInfo = null; int fallbackTaskCount = Integer.MAX_VALUE; int fallbackAvailableLocCount = Integer.MIN_VALUE; int fallbackOffset = Integer.MAX_VALUE; for (int attempt = 0; attempt < maxScanTimes; attempt++) { int[] params = Utils.LocNecessaryParameters(rowLastno, scanCurRow, crnNumber); scanCurRow = params[1]; int rowCount = params[0]; int crnNo = params[2]; int nearRow = params[3]; if (attempt < times) { continue; } int availableLocCount = availableLocCountCache.computeIfAbsent(crnNo, key -> countAvailableLocForCrn(rowLastno, rowLastnoType, key, nearRow)); int crnTaskCount = crnTaskCountCache.computeIfAbsent(crnNo, key -> wrkMastService.selectCount(new EntityWrapper() .eq("crn_no", key) .le("io_type", 100))); int spareLocCount = availableLocCount - crnTaskCount; int offset = attempt - times; if (availableLocCount > 0 && isBetterCrnCandidate(crnTaskCount, availableLocCount, offset, fallbackTaskCount, fallbackAvailableLocCount, fallbackOffset)) { fallbackInfo = new CrnRowInfo(crnNo, nearRow, scanCurRow, rowCount, attempt); fallbackTaskCount = crnTaskCount; fallbackAvailableLocCount = availableLocCount; fallbackOffset = offset; } if (spareLocCount <= MIN_SPARE_LOC_COUNT) { log.warn("{}号堆垛机可用空库位余量不足,降级候选继续保留。尺寸规格:{},轮询次数:{}", crnNo, JSON.toJSONString(locTypeDto), attempt); continue; } if (isBetterCrnCandidate(crnTaskCount, spareLocCount, offset, bestTaskCount, bestSpareLocCount, bestOffset)) { bestInfo = new CrnRowInfo(crnNo, nearRow, scanCurRow, rowCount, attempt); bestTaskCount = crnTaskCount; bestSpareLocCount = spareLocCount; bestOffset = offset; } } if (bestInfo != null) { return Optional.of(bestInfo); } if (fallbackInfo != null) { log.warn("堆垛机均衡分配未找到满足余量阈值的候选,降级使用仍有空位的堆垛机: crnNo={}", fallbackInfo.getCrnNo()); } return Optional.ofNullable(fallbackInfo); } private boolean isBetterCrnCandidate(int taskCount, int spareLocCount, int offset, int bestTaskCount, int bestSpareLocCount, int bestOffset) { if (taskCount != bestTaskCount) { return taskCount < bestTaskCount; } if (spareLocCount != bestSpareLocCount) { return spareLocCount > bestSpareLocCount; } return offset < bestOffset; } /** * 推导库位主档查询时应使用的 whsType。 */ private Long resolveLocWhsType(RowLastno rowLastno, RowLastnoType rowLastnoType) { if (rowLastno != null && rowLastno.getWhsType() != null) { return rowLastno.getWhsType().longValue(); } if (rowLastnoType != null && rowLastnoType.getType() != null) { return rowLastnoType.getType().longValue(); } return null; } /** * 判断库位是否满足本次规格约束。 */ 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 findOpenLocsByRow(RowLastno rowLastno, RowLastnoType rowLastnoType, Integer row, Integer crnNo, LocTypeDto locTypeDto, boolean singleExtension) { List result = new ArrayList(); if (row == null) { return result; } Wrapper wrapper = new EntityWrapper() // .eq("row1", row) .eq("loc_sts", "O"); if (crnNo != null) { wrapper.eq("crn_no", crnNo); } applyLocTypeFilters(wrapper, locTypeDto, true); if (singleExtension) { wrapper.orderBy("lev1", true).orderBy("bay1", true); } else { wrapper.orderBy("lev1", true).orderBy("bay1", true); } List 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 wrapper = new EntityWrapper(); 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 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 processedShallowRows = new LinkedHashSet(); 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 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 processedShallowRows = new LinkedHashSet(); 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 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 findBalancedSingleExtensionCrnAndNearRow(RowLastno rowLastno, int curRow, int crnNumber, int times, LocTypeDto locTypeDto, RowLastnoType rowLastnoType) { int maxScanTimes = Math.max(crnNumber * 2, 1); int scanCurRow = curRow; Map availableLocCountCache = new HashMap<>(); Map crnTaskCountCache = new HashMap<>(); CrnRowInfo bestInfo = null; int bestTaskCount = Integer.MAX_VALUE; int bestSpareLocCount = Integer.MIN_VALUE; int bestOffset = Integer.MAX_VALUE; CrnRowInfo fallbackInfo = null; int fallbackTaskCount = Integer.MAX_VALUE; int fallbackAvailableLocCount = Integer.MIN_VALUE; int fallbackOffset = Integer.MAX_VALUE; for (int attempt = 0; attempt < maxScanTimes; attempt++) { int[] params = Utils.LocNecessaryParameters(rowLastno, scanCurRow, crnNumber); scanCurRow = params[1]; int rowCount = params[0]; int crnNo = params[2]; int nearRow = params[3]; if (attempt < times) { continue; } int availableLocCount = availableLocCountCache.computeIfAbsent(crnNo, key -> countAvailableSingleExtensionLocForCrn(rowLastno, rowLastnoType, key, nearRow, locTypeDto)); int crnTaskCount = crnTaskCountCache.computeIfAbsent(crnNo, key -> wrkMastService.selectCount(new EntityWrapper() .eq("crn_no", key) .le("io_type", 100))); int spareLocCount = availableLocCount - crnTaskCount; int offset = attempt - times; if (availableLocCount > 0 && isBetterCrnCandidate(crnTaskCount, availableLocCount, offset, fallbackTaskCount, fallbackAvailableLocCount, fallbackOffset)) { fallbackInfo = new CrnRowInfo(crnNo, nearRow, scanCurRow, rowCount, attempt); fallbackTaskCount = crnTaskCount; fallbackAvailableLocCount = availableLocCount; fallbackOffset = offset; } if (spareLocCount <= MIN_SPARE_LOC_COUNT) { continue; } if (isBetterCrnCandidate(crnTaskCount, spareLocCount, offset, bestTaskCount, bestSpareLocCount, bestOffset)) { bestInfo = new CrnRowInfo(crnNo, nearRow, scanCurRow, rowCount, attempt); bestTaskCount = crnTaskCount; bestSpareLocCount = spareLocCount; bestOffset = offset; } } if (bestInfo != null) { return Optional.of(bestInfo); } return Optional.ofNullable(fallbackInfo); } /** * 返回某台堆垛机本次找位应扫描的排顺序。 */ private List getCrnSearchRows(RowLastno rowLastno, int crnNo, int preferredNearRow) { CrnDepthRuleProfile profile = basCrnDepthRuleService.resolveProfile(rowLastno, crnNo, preferredNearRow); if (profile == null || Cools.isEmpty(profile.getSearchRows())) { return new ArrayList(); } return new ArrayList(profile.getSearchRows()); } /** * 按库型返回每台堆垛机占用的排跨度。 */ private Integer getCrnRowSpan(Integer typeId) { if (typeId == null) { return null; } switch (typeId) { case 1: return 4; case 2: return 2; default: return null; } } /** * 向搜索排列表追加一个合法且不重复的排号。 */ private void addSearchRow(List searchRows, Integer row, RowLastno rowLastno) { if (row == null) { return; } if (row < rowLastno.getsRow() || row > rowLastno.geteRow()) { return; } if (!searchRows.contains(row)) { searchRows.add(row); } } /** * run/run2 标准堆垛机统一的空库位查询入口。 */ private LocMast findStandardEmptyLoc(RowLastno rowLastno, RowLastnoType rowLastnoType, int crnNo, int nearRow, LocTypeDto locTypeDto) { return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto); } /** * 构造只提升 locType1 的向上兼容规格。 */ private LocTypeDto buildUpwardCompatibleLocTypeDto(LocTypeDto locTypeDto) { if (locTypeDto == null || locTypeDto.getLocType1() == null || locTypeDto.getLocType1() >= 2) { return null; } LocTypeDto compatibleLocTypeDto = new LocTypeDto(); compatibleLocTypeDto.setLocType1((short) (locTypeDto.getLocType1() + 1)); compatibleLocTypeDto.setLocType2(locTypeDto.getLocType2()); compatibleLocTypeDto.setLocType3(locTypeDto.getLocType3()); compatibleLocTypeDto.setSiteId(locTypeDto.getSiteId()); return compatibleLocTypeDto; } /** * 统一封装找库位失败后的兼容重试顺序。 * * 兼容规则固定为: * 只允许 loc_type1 低位向高位兼容,loc_type2/loc_type3 不参与满托找位。 */ private LocTypeDto buildRetryCompatibleLocTypeDto(Integer staDescId, FindLocNoAttributeVo findLocNoAttributeVo, LocTypeDto locTypeDto) { return buildUpwardCompatibleLocTypeDto(locTypeDto); } /** * 检索库位号 * * @param whsType 类型 1:双深式货架 * @param staDescId 路径ID * @param sourceStaNo 源站 * @param findLocNoAttributeVo 属性 * @param moveCrnNo 源 * @param locTypeDto 类型 * @param times 轮询次数 * @return locNo 检索到的库位号 */ @Transactional public StartupDto getLocNoRun(Integer whsType, Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, Integer moveCrnNo, LocTypeDto locTypeDto, int times) { // 初始化参数 int crnNo = 0; //堆垛机号 int nearRow = 0; //最浅库位排 int curRow = 0; //最深库位排 int rowCount = 0; //轮询轮次 LocMast locMast = null; // 目标库位 StartupDto startupDto = new StartupDto(); RowLastno rowLastno = rowLastnoService.selectById(whsType); if (Cools.isEmpty(rowLastno)) { throw new CoolException("数据异常,请联系管理员===>库位规则未知"); } RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId()); if (Cools.isEmpty(rowLastnoType)) { throw new CoolException("数据异常,请联系管理员===》库位规则类型未知"); } int sRow = rowLastno.getsRow(); int eRow = rowLastno.geteRow(); int crnNumber = resolveCrnCount(rowLastno); // ===============>>>> 开始执行 curRow = rowLastno.getCurrentRow(); if (!Cools.isEmpty(moveCrnNo) && moveCrnNo != 0) { crnNumber = moveCrnNo; if (times == 0) { curRow = moveCrnNo * 4 - 1; } else { curRow = moveCrnNo * 4 - 2; } } //此程序用于优化堆垛机异常时的运行时间 Optional infoOpt = findBalancedCrnAndNearRow(rowLastno, curRow, crnNumber, times, findLocNoAttributeVo, locTypeDto, rowLastnoType); if (!infoOpt.isPresent()) { infoOpt = findAvailableCrnAndNearRow(rowLastno, curRow, crnNumber, times, findLocNoAttributeVo, locTypeDto, rowLastnoType); } if (!infoOpt.isPresent()) { throw new CoolException("无可用堆垛机"); } CrnRowInfo info = infoOpt.get(); crnNo = info.getCrnNo(); nearRow = info.getNearRow(); curRow = info.getCurRow(); rowCount = info.getRowCount(); times = info.getTimes(); boolean signRule1 = false; boolean signRule2 = false; if (Utils.BooleanWhsTypeStaIoType(rowLastno)) { // 靠近摆放规则 --- 同天同规格物料 //分离版 if (!Cools.isEmpty(findLocNoAttributeVo.getMatnr()) && staDescId == 1) { signRule1 = true; } // 靠近摆放规则 --- 同天同规格物料 //互通版 if (!Cools.isEmpty(findLocNoAttributeVo.getMatnr()) && staDescId == 1) { signRule2 = true; } if (!Cools.isEmpty(findLocNoAttributeVo.getMatnr()) && (staDescId == 11 || staDescId == 111)) { signRule1 = true; } } if (signRule1) { if (nearRow != curRow) { List locMasts = locMastService.selectList(new EntityWrapper() .eq("row1", nearRow).eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue())); for (LocMast locMast1 : locMasts) { //获取巷道 // List groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,locMast1.getLocNo(), curRow>nearRow); // LocMast locMastGro = locMastService.selectById(wrkMast.getLocNo()); //获取目标库位所在巷道最浅非空库位 LocMast locMastF = locMastService.selectLocByLocStsPakInF(curRow, nearRow, locMast1, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(locMastF) && locMastF.getLocSts().equals("F")) { LocDetl locDetl = locDetlService.selectOne(new EntityWrapper().eq("loc_no", locMastF.getLocNo())); if (!Cools.isEmpty(locDetl) && findLocNoAttributeVo.beSimilar(locDetl)) { //获取目标库位所在巷道最深空库位 locMast = locMastService.selectLocByLocStsPakInO(curRow, nearRow, locMast1, rowLastnoType.getType().longValue()); break; } } } } } else if (signRule2) { List locNos = locDetlService.getSameDetlToday(findLocNoAttributeVo.getMatnr(), sRow, eRow); for (String locNo : locNos) { if (Utils.isShallowLoc(slaveProperties, locNo)) { continue; } String shallowLocNo = Utils.getShallowLoc(slaveProperties, locNo); // 检测目标库位是否为空库位 LocMast shallowLoc = locMastService.selectById(shallowLocNo); if (shallowLoc != null && shallowLoc.getLocSts().equals("O")) { if (VersionUtils.locMoveCheckLocTypeComplete(shallowLoc, locTypeDto)) { if (basCrnpService.checkSiteError(shallowLoc.getCrnNo(), true)) { locMast = shallowLoc; crnNo = locMast.getCrnNo(); break; } } } } } // 靠近摆放规则 --- 空托 //互通版 if (staDescId == 10 && Utils.BooleanWhsTypeStaIoType(rowLastno)) { List locMasts = locMastService.selectList(new EntityWrapper() .eq("loc_sts", "D").ge("row1", sRow).le("row1", eRow).eq("whs_type", rowLastnoType.getType().longValue())); if (!locMasts.isEmpty()) { for (LocMast loc : locMasts) { if (Utils.isShallowLoc(slaveProperties, loc.getLocNo())) { continue; } String shallowLocNo = Utils.getShallowLoc(slaveProperties, loc.getLocNo()); // 检测目标库位是否为空库位 LocMast shallowLoc = locMastService.selectById(shallowLocNo); if (shallowLoc != null && shallowLoc.getLocSts().equals("O")) { if (VersionUtils.locMoveCheckLocTypeComplete(shallowLoc, locTypeDto)) { if (basCrnpService.checkSiteError(shallowLoc.getCrnNo(), true)) { locMast = shallowLoc; crnNo = locMast.getCrnNo(); break; } } } } } } Wrapper wrapper = null; StaDesc staDesc = null; BasDevp staNo = null; if (Utils.BooleanWhsTypeSta(rowLastno, staDescId)) { // 获取目标站 wrapper = new EntityWrapper() .eq("type_no", staDescId) .eq("stn_no", sourceStaNo) .eq("crn_no", crnNo); staDesc = staDescService.selectOne(wrapper); if (Cools.isEmpty(staDesc)) { log.error("type_no={},stn_no={},crn_no={}", staDescId, sourceStaNo, crnNo); // throw new CoolException("入库路径不存在"); crnNo = 0; } else { staNo = basDevpService.selectById(staDesc.getCrnStn()); if (!staNo.getAutoing().equals("Y")) { log.error("目标站" + staDesc.getCrnStn() + "不可用"); // throw new CoolException("目标站"+staDesc.getCrnStn()+"不可用"); crnNo = 0; } startupDto.setStaNo(staNo.getDevNo()); } // 更新库位排号 if (Cools.isEmpty(locMast)) { rowLastno.setCurrentRow(curRow); rowLastnoService.updateById(rowLastno); } } // Search empty location ==============================>> if (staDescId == 10 && Cools.isEmpty(locMast) && crnNo != 0) { locMast = findStandardEmptyLoc(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto); } if (Cools.isEmpty(locMast) && crnNo != 0) { locMast = findStandardEmptyLoc(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto); } if (!Cools.isEmpty(locMast) && !basCrnpService.checkSiteError(crnNo, true)) { locMast = null; } // Retry search if (Cools.isEmpty(locMast) || !locMast.getLocSts().equals("O")) { // Scan next aisle first, then retry with upward-compatible locType1. if (times < rowCount * 2) { times = times + 1; return getLocNoRun(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, locTypeDto, times); } LocTypeDto compatibleLocTypeDto = buildRetryCompatibleLocTypeDto(staDescId, findLocNoAttributeVo, locTypeDto); if (compatibleLocTypeDto != null) { log.warn("locType compatibility retry. source={}, target={}", JSON.toJSONString(locTypeDto), JSON.toJSONString(compatibleLocTypeDto)); return getLocNoRun(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, compatibleLocTypeDto, 0); } log.error("No empty location found. spec={}, times={}", JSON.toJSONString(locTypeDto), times); throw new CoolException("\u6ca1\u6709\u7a7a\u5e93\u4f4d"); } String locNo = locMast.getLocNo(); // 生成工作号 int workNo = getWorkNo(0); // 返回dto startupDto.setWorkNo(workNo); startupDto.setCrnNo(crnNo); startupDto.setSourceStaNo(sourceStaNo); startupDto.setLocNo(locNo); return startupDto; } public StartupDto getLocNoRun2(Integer whsType, Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, Integer moveCrnNo, LocTypeDto locTypeDto, int times) { return getLocNoRun2(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, locTypeDto, null, times); } /** * run2 入库找位主流程。 * * 当前方法只保留“组织流程”和“统一收口”的职责,具体策略拆成独立方法: * 1. 普通物料:按 row_lastno 自身轮询顺序 -> 站点优先库区/堆垛机 -> 其它库区。 * 2. 空托盘:优先库区 loc_type2=1 -> 其它库区 loc_type2=1 -> loc_type1=2 兼容。 * 3. 命中库位后分别回写普通物料游标或空托盘库区游标。 * * WCS 传入的推荐排不再参与 run2 选位,避免上游 row 参数把任务重新绑回固定堆垛机。 */ public StartupDto getLocNoRun2(Integer whsType, Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, Integer moveCrnNo, LocTypeDto locTypeDto, List recommendRows, int times) { int crnNo = 0; int nearRow = 0; LocMast locMast = null; StartupDto startupDto = new StartupDto(); RowLastno rowLastno = rowLastnoService.selectById(whsType); if (Cools.isEmpty(rowLastno)) { throw new CoolException("数据异常,请联系管理员===>库位规则未知"); } RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId()); if (Cools.isEmpty(rowLastnoType)) { throw new CoolException("数据异常,请联系管理员===》库位规则类型未知"); } 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 orderedCrnNos = getOrderedCrnNos(rowLastno, crnNo); List orderedRunnableCrnNos = getOrderedRunnableRun2CrnNos(rowLastno, staDescId, sourceStaNo, orderedCrnNos); if (emptyPalletRequest) { emptyPalletAreaSearchResult = findEmptyPalletRun2Loc(rowLastno, staDescId, sourceStaNo, startupDto, preferredArea, locTypeDto); if (!Cools.isEmpty(emptyPalletAreaSearchResult)) { locMast = emptyPalletAreaSearchResult.locMast; } } else { normalRun2SearchResult = findNormalRun2Loc(rowLastno, rowLastnoType, sourceStaNo, staDescId, findLocNoAttributeVo, locTypeDto, startupDto, preferredArea, orderedCrnNos); if (normalRun2SearchResult != null) { locMast = normalRun2SearchResult.locMast; } } if (!Cools.isEmpty(locMast)) { crnNo = locMast.getCrnNo(); nearRow = locMast.getRow1(); } if (emptyPalletRequest) { advanceEmptyPalletRun2Cursor(emptyPalletAreaSearchResult, locMast); } else if (!Cools.isEmpty(locMast)) { List 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) { 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={}, preferredArea={}, nearRow={}", JSON.toJSONString(locTypeDto), preferredArea, nearRow); throw new CoolException("没有空库位"); } int workNo = getWorkNo(0); startupDto.setWorkNo(workNo); startupDto.setCrnNo(crnNo); startupDto.setSourceStaNo(sourceStaNo); startupDto.setLocNo(locMast.getLocNo()); return startupDto; } /** * 单伸堆垛机复用统一画像算法查询空库位。 */ private LocMast findSingleExtensionEmptyLoc(RowLastno rowLastno, int crnNo, int nearRow, RowLastnoType rowLastnoType, LocTypeDto locTypeDto) { return findConfiguredEmptyLocForCrn(rowLastno, rowLastnoType, crnNo, nearRow, locTypeDto); } public StartupDto getLocNoRun4(Integer whsType, Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, Integer moveCrnNo, LocTypeDto locTypeDto, int times) { // 初始化参数 int crnNo = 0; //堆垛机号 int nearRow = 0; //最浅库位排 int curRow = 0; //最深库位排 int rowCount = 0; //轮询轮次 LocMast locMast = null; // 目标库位 StartupDto startupDto = new StartupDto(); RowLastno rowLastno = rowLastnoService.selectById(whsType); if (Cools.isEmpty(rowLastno)) { throw new CoolException("数据异常,请联系管理员===>库位规则未知"); } RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId()); if (Cools.isEmpty(rowLastnoType)) { throw new CoolException("数据异常,请联系管理员===》库位规则类型未知"); } int sRow = rowLastno.getsRow(); int eRow = rowLastno.geteRow(); int crnNumber = resolveCrnCount(rowLastno); // ===============>>>> 开始执行 curRow = rowLastno.getCurrentRow(); if (!Cools.isEmpty(moveCrnNo) && moveCrnNo != 0) { crnNumber = moveCrnNo; // if (times==0){ // curRow = moveCrnNo*4-1; // }else { // curRow = moveCrnNo*4-2; // } } //此程序用于优化堆垛机异常时的运行时间 for (int i = times; i < crnNumber * 2; i++) { int[] locNecessaryParameters = Utils.LocNecessaryParameters(rowLastno, curRow, crnNumber); rowCount = locNecessaryParameters[0]; curRow = locNecessaryParameters[1]; crnNo = locNecessaryParameters[2]; nearRow = locNecessaryParameters[3]; List locMasts = locMastService.selectList(new EntityWrapper() .eq("crn_no", crnNo).eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue())); if (locMasts.size() <= 5) { nearRow = 0; times++; continue; } break; } if (crnNo == 0) { throw new CoolException("无可用库位"); } // 相似工作档案 --- 同天同规格物料 if (!Cools.isEmpty(findLocNoAttributeVo.getMatnr()) && (staDescId == 1 || staDescId == 11 || staDescId == 111)) { //查询相似工作档案 List wrkMasts = wrkMastService.selectWrkMastWrkDetl(staDescId, findLocNoAttributeVo, crnNo); int nearbay = 0; //相似工作档案 目标库位列 int nearlev = 0; //相似工作档案 目标库位层 for (WrkMast wrkMast : wrkMasts) { int curRowW = curRow; //相似工作档案 最深库位排 int nearRowW = nearRow; //相似工作档案 最浅库位排 if (Cools.isEmpty(wrkMast.getLocNo())) { continue; } //目标排为最外层排 if (Utils.getRow(wrkMast.getLocNo()) == nearRow) { continue; } //起始站不一致 if (!wrkMast.getSourceStaNo().equals(sourceStaNo)) { continue; } //相同列、层过滤 if (Utils.getBay(wrkMast.getLocNo()) == nearbay && Utils.getLev(wrkMast.getLocNo()) == nearlev) { continue; } else { nearbay = Utils.getBay(wrkMast.getLocNo()); nearlev = Utils.getLev(wrkMast.getLocNo()); } //获取目标库位所在巷道并排序 // List groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,wrkMast.getLocNo(), curRow>nearRow); LocMast locMastGro = locMastService.selectById(wrkMast.getLocNo()); for (int i = 0; i < crnNumber * 2; i++) { if (!(Utils.getRow(locMastGro.getLocNo()) > nearRowW && Utils.getRow(locMastGro.getLocNo()) <= curRowW) && !(Utils.getRow(locMastGro.getLocNo()) < nearRowW && Utils.getRow(locMastGro.getLocNo()) >= curRowW)) { int[] locNecessaryParameters = Utils.LocNecessaryParameters(rowLastno, curRowW, crnNumber); curRowW = locNecessaryParameters[1]; nearRowW = locNecessaryParameters[3]; } else { break; } } //获取目标库位所在巷道最浅非空库位 LocMast locMast2 = locMastService.selectLocByLocStsPakInF(curRowW, nearRowW, locMastGro, rowLastnoType.getType().longValue()); //目标库位所在巷道最浅非空库位存在&&非最外侧库位&&入库状态 if (!Cools.isEmpty(locMast2) && Utils.getRow(locMast2.getLocNo()) != nearRowW && (locMast2.getLocSts().equals("S") || locMast2.getLocSts().equals("Q"))) { //获取库存明细 WrkDetl wrkDetl = wrkDetlService.selectOne(new EntityWrapper().eq("wrk_no", wrkMast.getWrkNo())); //判断同规格物料 if (!Cools.isEmpty(wrkDetl) && findLocNoAttributeVo.beSimilar(wrkDetl)) { int row2 = 0; if (Utils.getRow(locMast2.getLocNo()) > nearRowW) { row2 = Utils.getRow(locMast2.getLocNo()) - 1; } else { row2 = Utils.getRow(locMast2.getLocNo()) + 1; } String targetLocNo = zerofill(String.valueOf(row2), 2) + locMast2.getLocNo().substring(2); locMast = locMastService.selectOne(new EntityWrapper().eq("loc_no", targetLocNo).eq("loc_sts", "O")); if (Cools.isEmpty(locMast)) { continue; } break; } } } } // 相似工作档 --- 空托 if (Cools.isEmpty(locMast) && staDescId == 10) { List wrkMasts = wrkMastService.selectList(new EntityWrapper().eq("io_type", 10).eq("crn_no", crnNo).eq("whs_type", rowLastnoType.getType().longValue())); int nearbay = 0; int nearlev = 0; for (WrkMast wrkMast : wrkMasts) { int curRowW = curRow; //相似工作档案 最深库位排 int nearRowW = nearRow; //相似工作档案 最浅库位排 if (Cools.isEmpty(wrkMast.getLocNo())) { continue; } if (Utils.getRow(wrkMast.getLocNo()) == nearRow) { continue; } //起始站不一致 if (!wrkMast.getSourceStaNo().equals(sourceStaNo)) { continue; } if (Utils.getBay(wrkMast.getLocNo()) == nearbay && Utils.getLev(wrkMast.getLocNo()) == nearlev) { continue; } else { nearbay = Utils.getBay(wrkMast.getLocNo()); nearlev = Utils.getLev(wrkMast.getLocNo()); } // List groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,wrkMast.getLocNo(), curRow>nearRow); LocMast locMastGro = locMastService.selectById(wrkMast.getLocNo()); for (int i = 0; i < crnNumber * 2; i++) { if (!(Utils.getRow(locMastGro.getLocNo()) > nearRowW && Utils.getRow(locMastGro.getLocNo()) <= curRowW) && !(Utils.getRow(locMastGro.getLocNo()) < nearRowW && Utils.getRow(locMastGro.getLocNo()) >= curRowW)) { int[] locNecessaryParameters = Utils.LocNecessaryParameters(rowLastno, curRowW, crnNumber); curRowW = locNecessaryParameters[1]; nearRowW = locNecessaryParameters[3]; } else { break; } } LocMast locMast2 = locMastService.selectLocByLocStsPakInF(curRowW, nearRowW, locMastGro, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(locMast2) && Utils.getRow(locMast2.getLocNo()) != nearRowW && locMast2.getLocSts().equals("S")) { int row2 = 0; if (Utils.getRow(locMast2.getLocNo()) > nearRowW) { row2 = Utils.getRow(locMast2.getLocNo()) - 1; } else { row2 = Utils.getRow(locMast2.getLocNo()) + 1; } String targetLocNo = zerofill(String.valueOf(row2), 2) + locMast2.getLocNo().substring(2); locMast = locMastService.selectOne(new EntityWrapper().eq("loc_no", targetLocNo).eq("loc_sts", "O")); if (Cools.isEmpty(locMast)) { continue; } break; } } } boolean signRule1 = false; boolean signRule2 = false; if (Utils.BooleanWhsTypeStaIoType(rowLastno)) { // 靠近摆放规则 --- 同天同规格物料 //分离版 if (!Cools.isEmpty(findLocNoAttributeVo.getMatnr()) && staDescId == 1) { // signRule1 = true; } // 靠近摆放规则 --- 同天同规格物料 //互通版 if (!Cools.isEmpty(findLocNoAttributeVo.getMatnr()) && staDescId == 1) { signRule2 = true; } if (!Cools.isEmpty(findLocNoAttributeVo.getMatnr()) && (staDescId == 11 || staDescId == 111)) { signRule1 = true; } } if (signRule1) { if (nearRow != curRow) { List locMasts = locMastService.selectList(new EntityWrapper() .eq("row1", nearRow).eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue())); for (LocMast locMast1 : locMasts) { //获取巷道 // List groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,locMast1.getLocNo(), curRow>nearRow); //获取目标库位所在巷道最浅非空库位 LocMast locMastF = locMastService.selectLocByLocStsPakInF(curRow, nearRow, locMast1, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(locMastF) && locMastF.getLocSts().equals("F")) { LocDetl locDetl = locDetlService.selectOne(new EntityWrapper().eq("loc_no", locMastF.getLocNo())); if (!Cools.isEmpty(locDetl) && findLocNoAttributeVo.beSimilar(locDetl)) { //获取目标库位所在巷道最深空库位 locMast = locMastService.selectLocByLocStsPakInO(curRow, nearRow, locMast1, rowLastnoType.getType().longValue()); break; } } } } } else if (signRule2) { List locNos = locDetlService.getSameDetlToday(findLocNoAttributeVo.getMatnr(), sRow, eRow); int nearbay = 0; int nearlev = 0; for (String locNo : locNos) { int curRowW = curRow; //相似工作档案 最深库位排 int nearRowW = nearRow; //相似工作档案 最浅库位排 if (Cools.isEmpty(locNo)) { continue; } if (Utils.getRow(locNo) == nearRow) { continue; } if (Utils.getBay(locNo) == nearbay && Utils.getLev(locNo) == nearlev) { continue; } else { nearbay = Utils.getBay(locNo); nearlev = Utils.getLev(locNo); } // List groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,wrkMast.getLocNo(), curRow>nearRow); LocMast locMastGro = locMastService.selectById(locNo); for (int i = 0; i < crnNumber * 2; i++) { if (!(Utils.getRow(locMastGro.getLocNo()) > nearRowW && Utils.getRow(locMastGro.getLocNo()) <= curRowW) && !(Utils.getRow(locMastGro.getLocNo()) < nearRowW && Utils.getRow(locMastGro.getLocNo()) >= curRowW)) { int[] locNecessaryParameters = Utils.LocNecessaryParameters(rowLastno, curRowW, crnNumber); curRowW = locNecessaryParameters[1]; nearRowW = locNecessaryParameters[3]; } else { break; } } LocMast locMast2 = locMastService.selectLocByLocStsPakInF(curRowW, nearRowW, locMastGro, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(locMast2) && Utils.getRow(locMast2.getLocNo()) != nearRowW && locMast2.getLocSts().equals("S")) { int row2 = 0; if (Utils.getRow(locMast2.getLocNo()) > nearRowW) { row2 = Utils.getRow(locMast2.getLocNo()) - 1; } else { row2 = Utils.getRow(locMast2.getLocNo()) + 1; } String targetLocNo = zerofill(String.valueOf(row2), 2) + locMast2.getLocNo().substring(2); locMast = locMastService.selectOne(new EntityWrapper().eq("loc_no", targetLocNo).eq("loc_sts", "O")); if (Cools.isEmpty(locMast)) { continue; } break; } } } // // 靠近摆放规则 --- 空托 //分离版 // if (staDescId == 10 && Utils.BooleanWhsTypeStaIoType(rowLastno)) { // List locMasts = locMastService.selectList(new EntityWrapper().eq("row1", nearRow).eq("loc_sts", "O")); // for (LocMast locMast1:locMasts){ // //获取巷道 //// List groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,locMast1.getLocNo(), curRow>nearRow); // //获取目标库位所在巷道最浅非空库位 // LocMast locMastF = locMastService.selectLocByLocStsPakInF(curRow,nearRow,locMast1,rowLastnoType.getType().longValue()); // if (!Cools.isEmpty(locMastF) && locMastF.getLocSts().equals("D")){ // //获取目标库位所在巷道最浅非空库位 // locMast = locMastService.selectLocByLocStsPakInO(curRow,nearRow,locMast1,rowLastnoType.getType().longValue()); // break; // } // } // } // 靠近摆放规则 --- 空托 //互通版 if (staDescId == 10 && Utils.BooleanWhsTypeStaIoType(rowLastno)) { List locMasts = locMastService.selectList(new EntityWrapper().eq("loc_sts", "D").ge("row1", sRow).le("row1", eRow)); int nearbay = 0; int nearlev = 0; for (LocMast locMastSign : locMasts) { int curRowW = curRow; //相似工作档案 最深库位排 int nearRowW = nearRow; //相似工作档案 最浅库位排 if (Cools.isEmpty(locMastSign.getLocNo())) { continue; } if (Utils.getRow(locMastSign.getLocNo()) == nearRow) { continue; } if (Utils.getBay(locMastSign.getLocNo()) == nearbay && Utils.getLev(locMastSign.getLocNo()) == nearlev) { continue; } else { nearbay = Utils.getBay(locMastSign.getLocNo()); nearlev = Utils.getLev(locMastSign.getLocNo()); } // List groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,wrkMast.getLocNo(), curRow>nearRow); LocMast locMastGro = locMastService.selectById(locMastSign.getLocNo()); for (int i = 0; i < crnNumber * 2; i++) { if (!(Utils.getRow(locMastGro.getLocNo()) > nearRowW && Utils.getRow(locMastGro.getLocNo()) <= curRowW) && !(Utils.getRow(locMastGro.getLocNo()) < nearRowW && Utils.getRow(locMastGro.getLocNo()) >= curRowW)) { int[] locNecessaryParameters = Utils.LocNecessaryParameters(rowLastno, curRowW, crnNumber); curRowW = locNecessaryParameters[1]; nearRowW = locNecessaryParameters[3]; } else { break; } } LocMast locMast2 = locMastService.selectLocByLocStsPakInF(curRowW, nearRowW, locMastGro, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(locMast2) && Utils.getRow(locMast2.getLocNo()) != nearRowW && locMast2.getLocSts().equals("S")) { int row2 = 0; if (Utils.getRow(locMast2.getLocNo()) > nearRowW) { row2 = Utils.getRow(locMast2.getLocNo()) - 1; } else { row2 = Utils.getRow(locMast2.getLocNo()) + 1; } String targetLocNo = zerofill(String.valueOf(row2), 2) + locMast2.getLocNo().substring(2); locMast = locMastService.selectOne(new EntityWrapper().eq("loc_no", targetLocNo).eq("loc_sts", "O")); if (Cools.isEmpty(locMast)) { continue; } break; } } } Wrapper wrapper = null; StaDesc staDesc = null; BasDevp staNo = null; if (Utils.BooleanWhsTypeSta(rowLastno, staDescId)) { // 获取目标站 wrapper = new EntityWrapper() .eq("type_no", staDescId) .eq("stn_no", sourceStaNo) .eq("crn_no", crnNo); staDesc = staDescService.selectOne(wrapper); if (Cools.isEmpty(staDesc)) { log.error("入库路径不存在:type_no={},stn_no={},crn_no={}", staDescId, sourceStaNo, crnNo); crnNo = 0; } else { staNo = basDevpService.selectById(staDesc.getCrnStn()); if (!staNo.getAutoing().equals("Y")) { log.error("目标站" + staDesc.getCrnStn() + "不可用"); crnNo = 0; } startupDto.setStaNo(staNo.getDevNo()); } } // 更新库位排号 if (Utils.BooleanWhsTypeSta(rowLastno, staDescId) && Cools.isEmpty(locMast)) { rowLastno.setCurrentRow(curRow); rowLastnoService.updateById(rowLastno); } // 开始查找库位 ==============================>> // 1.按规则查找库位 if (Cools.isEmpty(locMast) && crnNo != 0) { List locMasts = locMastService.selectList(new EntityWrapper() .eq("row1", nearRow) .eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue()) .orderBy("lev1", true).orderBy("bay1", true));//最浅库位 for (LocMast locMast1 : locMasts) { if (!VersionUtils.locMoveCheckLocTypeComplete(locMast1, locTypeDto)) { continue; } if (Utils.BooleanWhsTypeStaIoType(rowLastno)) { //获取目标库位所在巷道最深空库位 LocMast locMast2 = locMastService.selectLocByLocStsPakInO(curRow, nearRow, locMast1, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(locMast2) && locMast2.getBay1() == curRow) { locMast = locMast2; break; } } } //未找到 允许混料 if (Cools.isEmpty(locMast) && Utils.BooleanWhsTypeStaIoType(rowLastno)) { for (LocMast locMast1 : locMasts) { if (!VersionUtils.locMoveCheckLocTypeComplete(locMast1, locTypeDto)) { continue; } if (Utils.BooleanWhsTypeStaIoType(rowLastno)) { // ????????????? // List groupOutsideLocCrn = Utils.getGroupOutLocCrn(curRow,nearRow,locMast1.getLocNo(), curRow>nearRow); // ???????????????? LocMast locMast2 = locMastService.selectLocByLocStsPakInF(curRow, nearRow, locMast1, rowLastnoType.getType().longValue()); if (Cools.isEmpty(locMast2)) { LocMast locMast3 = locMastService.selectLocByLocStsPakInO(curRow, nearRow, locMast1, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(locMast3)) { locMast = locMast3; break; } } else { if ((locMast2.getLocSts().equals("F") && staDescId == 1) || (locMast2.getLocSts().equals("D") && staDescId == 10)) { LocMast locMast3 = locMastService.selectLocByLocStsPakInO(curRow, nearRow, locMast1, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(locMast3)) { locMast = locMast3; break; } } } } } } } // Retry search if (Cools.isEmpty(locMast) || !locMast.getLocSts().equals("O")) { // Scan next aisle first, then retry with upward-compatible locType1. if (times < rowCount * 2) { times = times + 1; return getLocNoRun4(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, locTypeDto, times); } LocTypeDto compatibleLocTypeDto = buildRetryCompatibleLocTypeDto(staDescId, findLocNoAttributeVo, locTypeDto); if (compatibleLocTypeDto != null) { log.warn("locType compatibility retry. source={}, target={}", JSON.toJSONString(locTypeDto), JSON.toJSONString(compatibleLocTypeDto)); return getLocNoRun4(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, compatibleLocTypeDto, 0); } log.error("No empty location found. spec={}, times={}", JSON.toJSONString(locTypeDto), times); throw new CoolException("\u6ca1\u6709\u7a7a\u5e93\u4f4d"); } String locNo = locMast.getLocNo(); // 生成工作号 int workNo = getWorkNo(0); // 返回dto startupDto.setWorkNo(workNo); startupDto.setCrnNo(crnNo); startupDto.setSourceStaNo(sourceStaNo); startupDto.setLocNo(locNo); return startupDto; } public StartupDto getLocNoRun5(Integer whsType, Integer staDescId, Integer sourceStaNo, FindLocNoAttributeVo findLocNoAttributeVo, Integer moveCrnNo, LocTypeDto locTypeDto, List recommendRows, int times) { // WCS 传入的推荐排不再参与 AGV/平库选位,统一按库位排号自身轮询逻辑找位。 // 初始化参数 int crnNo = 0; //堆垛机号 int nearRow = 0; //最浅库位排 int curRow = 0; //最深库位排 int rowCount = 0; //轮询轮次 LocMast locMast = null; // 目标库位 StartupDto startupDto = new StartupDto(); RowLastno rowLastno = rowLastnoService.selectById(whsType); if (Cools.isEmpty(rowLastno)) { throw new CoolException("数据异常,请联系管理员===>库位规则未知"); } RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(rowLastno.getTypeId()); if (Cools.isEmpty(rowLastnoType)) { throw new CoolException("数据异常,请联系管理员===》库位规则类型未知"); } int sRow = rowLastno.getsRow(); int eRow = rowLastno.geteRow(); int crnNumber = eRow - sRow + 1; // ===============>>>> 开始执行 curRow = rowLastno.getCurrentRow(); if (!Cools.isEmpty(moveCrnNo) && moveCrnNo != 0) { crnNumber = moveCrnNo; } //此程序用于优化堆垛机异常时的运行时间 int[] locNecessaryParameters = Utils.LocNecessaryParameters(rowLastno, curRow, crnNumber); curRow = locNecessaryParameters[1]; crnNo = 6; rowCount = locNecessaryParameters[0]; nearRow = locNecessaryParameters[3]; Wrapper wrapper = null; StaDesc staDesc = null; BasDevp staNo = null; // if (Utils.BooleanWhsTypeSta(rowLastno, staDescId)) { // 获取目标站 // wrapper = new EntityWrapper() // .eq("type_no", staDescId) // .eq("stn_no", sourceStaNo) // .eq("crn_no", crnNo); // staDesc = staDescService.selectOne(wrapper); // if (Cools.isEmpty(staDesc)) { // log.error("type_no={},stn_no={},crn_no={}", staDescId, sourceStaNo, crnNo); //// throw new CoolException("入库路径不存在"); // crnNo = 0; // } // else { // staNo = basDevpService.selectById(staDesc.getCrnStn()); // if (!staNo.getAutoing().equals("Y")) { // log.error("目标站" + staDesc.getCrnStn() + "不可用"); //// throw new CoolException("目标站"+staDesc.getCrnStn()+"不可用"); // crnNo = 0; // } // startupDto.setStaNo(staNo.getDevNo()); // } // } // 更新库位排号 if (Utils.BooleanWhsTypeSta(rowLastno, staDescId) && Cools.isEmpty(locMast)) { rowLastno.setCurrentRow(curRow); rowLastnoService.updateById(rowLastno); } // 开始查找库位 ==============================>> Integer preferredArea = findLocNoAttributeVo.getOutArea(); if (Cools.isEmpty(locMast) && preferredArea == null) { List locMasts = locMastService.selectList(new EntityWrapper() .eq("row1", nearRow) .eq("loc_sts", "O").eq("whs_type", rowLastnoType.getType().longValue()) .orderBy("lev1", true).orderBy("bay1", true)); // 最浅库位 for (LocMast locMast1 : locMasts) { if (!VersionUtils.locMoveCheckLocTypeComplete(locMast1, locTypeDto)) { continue; } if (Utils.BooleanWhsTypeStaIoType(rowLastno)) { // 获取目标库位所在巷道最深空库位 LocMast locMast2 = locMastService.selectLocByLocStsPakInO(curRow, nearRow, locMast1, rowLastnoType.getType().longValue()); if (!Cools.isEmpty(locMast2) && locMast2.getRow1() == curRow) { locMast = locMast2; break; } } } } else if (Cools.isEmpty(locMast)) { int[] bayRange = getAgvAreaBayRange(preferredArea); locMast = findAgvLocByRows(rowLastno, rowLastnoType, getAgvAreaRows(preferredArea, rowLastno), bayRange[0], bayRange[1], curRow, nearRow, locTypeDto, false); if (!Cools.isEmpty(locMast)) { crnNo = locMast.getCrnNo(); } if (Cools.isEmpty(locMast)) { locMast = findAgvLocByRows(rowLastno, rowLastnoType, getAgvFallbackRows(rowLastno), 1, 19, curRow, nearRow, locTypeDto, true); if (!Cools.isEmpty(locMast)) { crnNo = locMast.getCrnNo(); } } } // Retry search if (Cools.isEmpty(locMast) || !locMast.getLocSts().equals("O")) { // Scan next aisle first, then retry with upward-compatible locType1. if (times < rowCount * 2) { times = times + 1; return getLocNoRun5(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, locTypeDto, recommendRows, times); } LocTypeDto compatibleLocTypeDto = buildRetryCompatibleLocTypeDto(staDescId, findLocNoAttributeVo, locTypeDto); if (compatibleLocTypeDto != null) { log.warn("locType compatibility retry. source={}, target={}", JSON.toJSONString(locTypeDto), JSON.toJSONString(compatibleLocTypeDto)); return getLocNoRun5(whsType, staDescId, sourceStaNo, findLocNoAttributeVo, moveCrnNo, compatibleLocTypeDto, recommendRows, 0); } log.error("No empty location found. spec={}, times={}", JSON.toJSONString(locTypeDto), times); throw new CoolException("\u6ca1\u6709\u7a7a\u5e93\u4f4d"); } String locNo = locMast.getLocNo(); // 生成工作号 int workNo = getWorkNo(0); // 返回dto startupDto.setWorkNo(workNo); startupDto.setCrnNo(crnNo); startupDto.setSourceStaNo(sourceStaNo); startupDto.setLocNo(locNo); return startupDto; } public static String zerofill(String msg, Integer count) { if (msg.length() == count) { return msg; } else if (msg.length() > count) { return msg.substring(0, 16); } else { StringBuilder msgBuilder = new StringBuilder(msg); for (int i = 0; i < count - msg.length(); ++i) { msgBuilder.insert(0, "0"); } return msgBuilder.toString(); } } }