src/main/java/com/zy/api/controller/WcsApiController.java
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.core.annotations.ManagerAuth; import com.core.common.R; import com.zy.api.controller.params.ReassignLocParams; import com.zy.api.controller.params.ReceviceTaskParams; import com.zy.api.controller.params.WorkTaskParams; import com.zy.api.service.WcsApiService; @@ -77,4 +78,15 @@ return R.ok().add(waitPakins); } @ApiOperation("WCS申请任务重新分配入库位") @PostMapping("/openapi/reassign/loc") public R reassignInboundLoc(@RequestBody ReassignLocParams params, HttpServletRequest request) { if (Objects.isNull(params)) { return R.error("参数不能为空!!"); } log.info("[reassignInboundLoc] request={}", JSON.toJSONString(params)); request.setAttribute("cache", params); return wcsApiService.reassignInboundLoc(params); } } src/main/java/com/zy/api/controller/params/ReassignLocParams.java
New file @@ -0,0 +1,21 @@ package com.zy.api.controller.params; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; import java.util.List; @Data @ApiModel(value = "ReassignLocParams", description = "wcs reassign inbound loc params") public class ReassignLocParams implements Serializable { private static final long serialVersionUID = 1L; @ApiModelProperty("wms task no") private String taskNo; @ApiModelProperty("available rows, reserved for interface compatibility only") private List<Integer> row; } src/main/java/com/zy/api/service/WcsApiService.java
@@ -1,6 +1,7 @@ package com.zy.api.service; import com.core.common.R; import com.zy.api.controller.params.ReassignLocParams; import com.zy.api.controller.params.ReceviceTaskParams; import com.zy.api.controller.params.StopOutTaskParams; import com.zy.api.controller.params.WorkTaskParams; @@ -54,6 +55,13 @@ R syncDeviceStatusFromWcs(boolean logOnFailure); /** * WCS 申请任务重新分配入库位 * @param params * @return */ R reassignInboundLoc(ReassignLocParams params); /** * batch pause out tasks * @param params * @return src/main/java/com/zy/api/service/impl/WcsApiServiceImpl.java
@@ -7,6 +7,7 @@ import com.core.common.Cools; import com.core.common.R; import com.core.exception.CoolException; import com.zy.api.controller.params.ReassignLocParams; import com.zy.api.controller.params.ReceviceTaskParams; import com.zy.api.controller.params.StopOutTaskParams; import com.zy.api.controller.params.WorkTaskParams; @@ -18,6 +19,8 @@ import com.zy.asrs.service.*; import com.zy.asrs.utils.Utils; import com.zy.common.constant.MesConstant; import com.zy.common.model.LocTypeDto; import com.zy.common.model.StartupDto; import com.zy.common.service.CommonService; import com.zy.common.utils.HttpHandler; import lombok.extern.slf4j.Slf4j; @@ -28,6 +31,7 @@ import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.math.BigDecimal; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -90,6 +94,8 @@ private BasCrnpService basCrnpService; @Autowired private ApiLogService apiLogService; @Autowired private RowLastnoService rowLastnoService; /** @@ -607,6 +613,62 @@ } } @Override @Transactional(rollbackFor = Exception.class) public R reassignInboundLoc(ReassignLocParams params) { if (params == null) { return R.error("参数不能为空!!"); } if (Cools.isEmpty(params.getTaskNo())) { return R.error("任务号不能为空!!"); } WrkMast wrkMast = wrkMastService.selectOne(new EntityWrapper<WrkMast>().eq("wrk_no", params.getTaskNo())); String validateMsg = validateReassignInboundTask(wrkMast); if (!Cools.isEmpty(validateMsg)) { return R.error(validateMsg); } LocMast currentLoc = locMastService.selectById(wrkMast.getLocNo()); if (Cools.isEmpty(currentLoc)) { return R.error("当前目标库位不存在"); } Integer preferredArea = resolveReassignArea(wrkMast, currentLoc); if (preferredArea == null) { return R.error("无法确定任务所属库区"); } List<Integer> candidateCrnNos = buildReassignCandidateCrnNos(preferredArea, wrkMast.getCrnNo()); if (candidateCrnNos.isEmpty()) { return R.error("当前库区没有其他堆垛机可供重分配"); } LocTypeDto locTypeDto = buildReassignLocTypeDto(currentLoc); StartupDto startupDto = commonService.findRun2InboundLocByCandidateCrnNos( wrkMast.getSourceStaNo(), wrkMast.getIoType(), preferredArea, candidateCrnNos, locTypeDto); if (startupDto == null || Cools.isEmpty(startupDto.getLocNo())) { return R.error("当前库区没有可重新分配的空库位"); } LocMast targetLoc = locMastService.selectById(startupDto.getLocNo()); if (Cools.isEmpty(targetLoc)) { throw new CoolException("新目标库位不存在"); } if (!"O".equals(targetLoc.getLocSts())) { throw new CoolException(targetLoc.getLocNo() + "目标库位已被占用"); } Date now = new Date(); updateReassignTargetLoc(targetLoc, wrkMast, currentLoc, now); updateReassignWorkMast(wrkMast, startupDto, now); releaseOldReservedLocIfNeeded(currentLoc, targetLoc.getLocNo(), now); Map<String, Object> result = new LinkedHashMap<>(); result.put("locNo", Utils.WMSLocToWCSLoc(targetLoc.getLocNo())); return R.ok("操作成功").add(result); } private boolean requiresOutboundErpConfirm(WrkMast wrkMast) { Integer ioType = wrkMast == null ? null : wrkMast.getIoType(); return ioType != null && (ioType == 101 || ioType == 103 || ioType == 104 || ioType == 107 || ioType == 110); @@ -643,6 +705,172 @@ return null; } private String validateReassignInboundTask(WrkMast wrkMast) { if (wrkMast == null) { return "任务不存在"; } if (wrkMast.getIoType() == null || (wrkMast.getIoType() != 1 && wrkMast.getIoType() != 10)) { return "当前任务不是入库任务"; } if (!Objects.equals(wrkMast.getWrkSts(), 2L)) { return "当前任务状态不允许重新分配入库位"; } if (wrkMast.getCrnNo() == null) { return "当前任务未分配堆垛机"; } if (Cools.isEmpty(wrkMast.getLocNo())) { return "当前任务未分配目标库位"; } if (wrkMast.getSourceStaNo() == null) { return "当前任务缺少源站信息"; } return null; } private Integer resolveReassignArea(WrkMast wrkMast, LocMast currentLoc) { Integer stationArea = Utils.getStationStorageArea(wrkMast.getSourceStaNo()); if (belongsToArea(stationArea, wrkMast.getCrnNo(), currentLoc)) { return stationArea; } Integer fallbackArea = findAreaByCurrentTask(wrkMast.getCrnNo(), currentLoc); if (fallbackArea != null) { return fallbackArea; } return stationArea; } private Integer findAreaByCurrentTask(Integer currentCrnNo, LocMast currentLoc) { for (int area = 1; area <= 3; area++) { if (belongsToArea(area, currentCrnNo, currentLoc)) { return area; } } return null; } private boolean belongsToArea(Integer area, Integer currentCrnNo, LocMast currentLoc) { if (area == null || area <= 0) { return false; } RowLastno areaRowLastno = rowLastnoService.selectById(area); if (areaRowLastno == null) { return false; } Integer startCrnNo = resolveAreaStartCrnNo(areaRowLastno); Integer endCrnNo = resolveAreaEndCrnNo(areaRowLastno, startCrnNo); if (currentCrnNo != null && currentCrnNo >= startCrnNo && currentCrnNo <= endCrnNo) { return true; } Integer row = currentLoc == null ? null : currentLoc.getRow1(); Integer startRow = areaRowLastno.getsRow(); Integer endRow = areaRowLastno.geteRow(); return row != null && startRow != null && endRow != null && row >= startRow && row <= endRow; } private List<Integer> buildReassignCandidateCrnNos(Integer area, Integer currentCrnNo) { RowLastno areaRowLastno = rowLastnoService.selectById(area); if (areaRowLastno == null) { throw new CoolException("未找到库区轮询规则"); } int startCrnNo = resolveAreaStartCrnNo(areaRowLastno); int endCrnNo = resolveAreaEndCrnNo(areaRowLastno, startCrnNo); if (currentCrnNo == null || currentCrnNo < startCrnNo || currentCrnNo > endCrnNo) { throw new CoolException("当前任务堆垛机不在所属库区范围内"); } List<Integer> candidateCrnNos = new ArrayList<>(); for (int crnNo = currentCrnNo - 1; crnNo >= startCrnNo; crnNo--) { candidateCrnNos.add(crnNo); } for (int crnNo = endCrnNo; crnNo > currentCrnNo; crnNo--) { candidateCrnNos.add(crnNo); } return candidateCrnNos; } private int resolveAreaStartCrnNo(RowLastno areaRowLastno) { if (areaRowLastno.getsCrnNo() != null && areaRowLastno.getsCrnNo() > 0) { return areaRowLastno.getsCrnNo(); } return 1; } private int resolveAreaEndCrnNo(RowLastno areaRowLastno, int startCrnNo) { if (areaRowLastno.geteCrnNo() != null && areaRowLastno.geteCrnNo() >= startCrnNo) { return areaRowLastno.geteCrnNo(); } int crnQty = areaRowLastno.getCrnQty() == null || areaRowLastno.getCrnQty() <= 0 ? 1 : areaRowLastno.getCrnQty(); return startCrnNo + crnQty - 1; } private LocTypeDto buildReassignLocTypeDto(LocMast currentLoc) { LocTypeDto locTypeDto = new LocTypeDto(); if (currentLoc == null) { return locTypeDto; } locTypeDto.setLocType1(normalizeLocType(currentLoc.getLocType1())); locTypeDto.setLocType2(normalizeLocType(currentLoc.getLocType2())); locTypeDto.setLocType3(normalizeLocType(currentLoc.getLocType3())); return locTypeDto; } private Short normalizeLocType(Short locType) { return locType == null || locType <= 0 ? null : locType; } private void updateReassignTargetLoc(LocMast targetLoc, WrkMast wrkMast, LocMast currentLoc, Date now) { targetLoc.setLocSts("S"); targetLoc.setModiUser(WCS_SYNC_USER); targetLoc.setModiTime(now); if (!Cools.isEmpty(wrkMast.getBarcode())) { targetLoc.setBarcode(wrkMast.getBarcode()); } else if (!Cools.isEmpty(currentLoc) && !Cools.isEmpty(currentLoc.getBarcode())) { targetLoc.setBarcode(currentLoc.getBarcode()); } else { targetLoc.setBarcode(""); } if (wrkMast.getScWeight() != null) { targetLoc.setScWeight(wrkMast.getScWeight()); } else if (!Cools.isEmpty(currentLoc) && currentLoc.getScWeight() != null) { targetLoc.setScWeight(currentLoc.getScWeight()); } else { targetLoc.setScWeight(BigDecimal.ZERO); } if (!locMastService.updateById(targetLoc)) { throw new CoolException("改变库位状态失败"); } } private void updateReassignWorkMast(WrkMast wrkMast, StartupDto startupDto, Date now) { wrkMast.setLocNo(startupDto.getLocNo()); wrkMast.setCrnNo(startupDto.getCrnNo()); if (startupDto.getStaNo() != null) { wrkMast.setStaNo(startupDto.getStaNo()); } wrkMast.setWrkSts(2L); wrkMast.setModiUser(WCS_SYNC_USER); wrkMast.setModiTime(now); if (!wrkMastService.updateById(wrkMast)) { throw new CoolException("修改工作档失败"); } } private void releaseOldReservedLocIfNeeded(LocMast currentLoc, String newLocNo, Date now) { if (currentLoc == null || Cools.isEmpty(currentLoc.getLocNo()) || currentLoc.getLocNo().equals(newLocNo)) { return; } if (!"S".equals(currentLoc.getLocSts())) { return; } currentLoc.setLocSts("O"); currentLoc.setBarcode(""); currentLoc.setScWeight(BigDecimal.ZERO); currentLoc.setModiUser(WCS_SYNC_USER); currentLoc.setModiTime(now); if (!locMastService.updateById(currentLoc)) { throw new CoolException("释放原目标库位失败"); } } /** * 按任务类型选择 WCS 接口地址。 * in -> 入库接口 src/main/java/com/zy/asrs/task/AutoFrontLocMoveScheduler.java
New file @@ -0,0 +1,47 @@ package com.zy.asrs.task; import com.core.common.Cools; import com.zy.asrs.task.core.ReturnT; import com.zy.asrs.task.handler.AutoFrontLocMoveHandler; import com.zy.asrs.task.support.AutoFrontLocMoveConfigResolver; import com.zy.asrs.task.support.AutoFrontLocMoveSettings; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Slf4j @Component public class AutoFrontLocMoveScheduler { @Autowired private AutoFrontLocMoveConfigResolver configResolver; @Autowired private AutoFrontLocMoveHandler handler; /** * 由于定时器固定 5 秒触发一次,实际业务执行频率由配置中的 intervalSeconds 控制。 */ private volatile long lastRunAt = 0L; @Scheduled(cron = "0/5 * * * * ? ") public synchronized void execute() { AutoFrontLocMoveSettings settings = configResolver.resolve(); if (settings == null || !settings.isEnabled() || Cools.isEmpty(settings.getRules())) { lastRunAt = 0L; return; } long now = System.currentTimeMillis(); long intervalMillis = Math.max(1, settings.getIntervalSeconds()) * 1000L; if (lastRunAt > 0 && now - lastRunAt < intervalMillis) { return; } lastRunAt = now; ReturnT<String> result = handler.start(settings); if (!result.isSuccess()) { log.warn("前排补货移库执行失败:{}", result.getMsg()); } } } src/main/java/com/zy/asrs/task/handler/AutoFrontLocMoveHandler.java
New file @@ -0,0 +1,238 @@ package com.zy.asrs.task.handler; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.core.common.Cools; import com.zy.asrs.entity.BasCrnp; import com.zy.asrs.entity.LocDetl; import com.zy.asrs.entity.LocMast; import com.zy.asrs.entity.WrkMast; import com.zy.asrs.service.BasCrnpService; import com.zy.asrs.service.LocDetlService; import com.zy.asrs.service.LocMastService; import com.zy.asrs.service.WorkService; import com.zy.asrs.service.WrkMastService; import com.zy.asrs.task.AbstractHandler; import com.zy.asrs.task.core.ReturnT; import com.zy.asrs.task.support.AutoFrontLocMoveSettings; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @Slf4j @Service public class AutoFrontLocMoveHandler extends AbstractHandler<String> { @Autowired private BasCrnpService basCrnpService; @Autowired private WrkMastService wrkMastService; @Autowired private LocMastService locMastService; @Autowired private LocDetlService locDetlService; @Autowired private WorkService workService; public synchronized ReturnT<String> start(AutoFrontLocMoveSettings settings) { if (settings == null || !settings.isEnabled() || Cools.isEmpty(settings.getRules())) { return SUCCESS; } try { // 每次调度按配置逐台堆垛机尝试,单台只成功下发一笔移库,避免瞬时堆积。 for (AutoFrontLocMoveSettings.Rule rule : settings.getRules()) { dispatch(rule, settings.getUserId()); } } catch (Exception e) { log.error("前排补货移库执行失败", e); return FAIL.setMsg(e.getMessage()); } return SUCCESS; } void dispatch(AutoFrontLocMoveSettings.Rule rule, Long userId) { if (rule == null || rule.getCrnNo() == null || Cools.isEmpty(rule.getFrontRowList())) { return; } if (!isCraneAvailable(rule.getCrnNo())) { return; } List<LocMast> targetLocs = loadTargetLocs(rule); if (targetLocs.isEmpty()) { return; } List<LocMast> sourceLocs = loadSourceLocs(rule); if (sourceLocs.isEmpty()) { return; } // 目标库位按前排优先顺序遍历,来源库位按配置顺序或“离前排更远”优先遍历。 for (LocMast targetLoc : targetLocs) { for (LocMast sourceLoc : sourceLocs) { if (!isCompatible(sourceLoc, targetLoc)) { continue; } try { workService.locMove(sourceLoc.getLocNo(), targetLoc.getLocNo(), userId); log.info("前排补货移库已下发,堆垛机{},源库位{},目标库位{}", rule.getCrnNo(), sourceLoc.getLocNo(), targetLoc.getLocNo()); return; } catch (Exception e) { log.warn("前排补货移库下发失败,堆垛机{},源库位{},目标库位{},原因:{}", rule.getCrnNo(), sourceLoc.getLocNo(), targetLoc.getLocNo(), e.getMessage()); } } } } private boolean isCraneAvailable(Integer crnNo) { BasCrnp crnp = basCrnpService.selectById(crnNo); if (Cools.isEmpty(crnp)) { return false; } if ("N".equalsIgnoreCase(crnp.getInEnable()) || "N".equalsIgnoreCase(crnp.getOutEnable())) { return false; } if (crnp.getCrnSts() == null || crnp.getCrnSts() != 3) { return false; } if (crnp.getCrnErr() != null && crnp.getCrnErr() != 0L) { return false; } if (crnp.getTankQty() != null && crnp.getTankQty() == 0) { return false; } EntityWrapper<WrkMast> wrapper = new EntityWrapper<>(); wrapper.eq("crn_no", crnNo); wrapper.last(" and wrk_sts in (2,3,4,11,12)"); List<WrkMast> activeWorks = wrkMastService.selectList(wrapper); // 有执行中/待执行工作时不再追加自动移库,避免与人工或业务任务抢设备。 return Cools.isEmpty(activeWorks); } private List<LocMast> loadTargetLocs(AutoFrontLocMoveSettings.Rule rule) { EntityWrapper<LocMast> wrapper = new EntityWrapper<>(); wrapper.eq("crn_no", rule.getCrnNo()); wrapper.eq("loc_sts", "O"); wrapper.in("row1", rule.getFrontRowList()); List<LocMast> candidates = locMastService.selectList(wrapper); List<LocMast> targetLocs = new ArrayList<>(); if (!Cools.isEmpty(candidates)) { for (LocMast candidate : candidates) { if (isUsableLoc(candidate)) { targetLocs.add(candidate); } } } targetLocs.sort(buildRowComparator(rule.getFrontRowList())); return targetLocs; } private List<LocMast> loadSourceLocs(AutoFrontLocMoveSettings.Rule rule) { EntityWrapper<LocMast> wrapper = new EntityWrapper<>(); wrapper.eq("crn_no", rule.getCrnNo()); wrapper.eq("loc_sts", "F"); if (!Cools.isEmpty(rule.getSourceRowList())) { wrapper.in("row1", rule.getSourceRowList()); } List<LocMast> candidates = locMastService.selectList(wrapper); if (Cools.isEmpty(candidates)) { return new ArrayList<>(); } Set<Integer> frontRows = new HashSet<>(rule.getFrontRowList()); List<LocMast> sourceLocs = new ArrayList<>(); for (LocMast candidate : candidates) { if (!isUsableLoc(candidate)) { continue; } // 未显式配置来源排时,默认只从“非前排”搬货,避免刚补到前排又被搬走。 if (Cools.isEmpty(rule.getSourceRowList()) && frontRows.contains(candidate.getRow1())) { continue; } if (!hasMovableStock(candidate.getLocNo())) { continue; } sourceLocs.add(candidate); } if (!Cools.isEmpty(rule.getSourceRowList())) { sourceLocs.sort(buildRowComparator(rule.getSourceRowList())); } else { sourceLocs.sort(buildDefaultSourceComparator(rule.getFrontRowList())); } return sourceLocs; } private Comparator<LocMast> buildRowComparator(List<Integer> rowOrder) { Map<Integer, Integer> sortIndex = new HashMap<>(); for (int i = 0; i < rowOrder.size(); i++) { sortIndex.put(rowOrder.get(i), i); } return Comparator .comparingInt((LocMast loc) -> sortIndex.getOrDefault(loc.getRow1(), Integer.MAX_VALUE)) .thenComparing(LocMast::getBay1, Comparator.nullsLast(Integer::compareTo)) .thenComparing(LocMast::getLev1, Comparator.nullsLast(Integer::compareTo)) .thenComparing(LocMast::getGro1, Comparator.nullsLast(Integer::compareTo)) .thenComparing(LocMast::getLocNo, Comparator.nullsLast(String::compareTo)); } private Comparator<LocMast> buildDefaultSourceComparator(List<Integer> frontRowOrder) { // 前排是升序时,来源默认从更后排开始;前排是降序时,来源默认从更前排开始。 boolean descending = frontRowOrder.size() > 1 && frontRowOrder.get(0) > frontRowOrder.get(frontRowOrder.size() - 1); Comparator<Integer> rowComparator = descending ? Comparator.reverseOrder() : Integer::compareTo; return Comparator .comparing(LocMast::getRow1, Comparator.nullsLast(rowComparator)) .thenComparing(LocMast::getBay1, Comparator.nullsLast(Integer::compareTo)) .thenComparing(LocMast::getLev1, Comparator.nullsLast(Integer::compareTo)) .thenComparing(LocMast::getGro1, Comparator.nullsLast(Integer::compareTo)) .thenComparing(LocMast::getLocNo, Comparator.nullsLast(String::compareTo)); } private boolean hasMovableStock(String locNo) { List<LocDetl> locDetls = locDetlService.selectList(new EntityWrapper<LocDetl>().eq("loc_no", locNo)); if (Cools.isEmpty(locDetls)) { return false; } // 任何一条库存明细被冻结,都视为该托盘不可被自动策略搬运。 for (LocDetl locDetl : locDetls) { if (locDetl != null && locDetl.getFrozen() != null && locDetl.getFrozen() == 1) { return false; } } return true; } private boolean isUsableLoc(LocMast locMast) { return locMast != null && (locMast.getFrozen() == null || locMast.getFrozen() == 0) && !"N".equalsIgnoreCase(locMast.getOutEnable()); } private boolean isCompatible(LocMast sourceLoc, LocMast targetLoc) { if (sourceLoc == null || targetLoc == null) { return false; } if (sourceLoc.getLocNo() != null && sourceLoc.getLocNo().equals(targetLoc.getLocNo())) { return false; } // 自动移库只做同类库位补位,减少因库位画像差异导致的后续作业风险。 return sameOrBlank(sourceLoc.getWhsType(), targetLoc.getWhsType()) && sameOrBlank(sourceLoc.getLocType1(), targetLoc.getLocType1()) && sameOrBlank(sourceLoc.getLocType2(), targetLoc.getLocType2()) && sameOrBlank(sourceLoc.getLocType3(), targetLoc.getLocType3()); } private boolean sameOrBlank(Object source, Object target) { return source == null || target == null || source.equals(target); } } src/main/java/com/zy/asrs/task/support/AutoFrontLocMoveConfigResolver.java
New file @@ -0,0 +1,141 @@ package com.zy.asrs.task.support; import com.alibaba.fastjson.JSON; import com.core.common.Cools; import com.zy.system.entity.Config; import com.zy.system.service.ConfigService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @Slf4j @Service public class AutoFrontLocMoveConfigResolver { public static final String CONFIG_CODE = "AutoFrontLocMove"; private static final int DEFAULT_INTERVAL_SECONDS = 60; private static final long DEFAULT_USER_ID = 9527L; @Autowired private ConfigService configService; /** * 从 sys_config 读取 JSON 配置。 * 配置缺失、停用或解析失败时,回退成“关闭状态”的默认配置。 */ public AutoFrontLocMoveSettings resolve() { AutoFrontLocMoveSettings settings = defaults(); Config config = configService.selectConfigByCode(CONFIG_CODE); if (!isConfigEnabled(config)) { return settings; } try { AutoFrontLocMoveSettings parsed = JSON.parseObject(config.getValue(), AutoFrontLocMoveSettings.class); if (parsed == null) { return settings; } normalize(parsed); return parsed; } catch (Exception e) { log.error("前排补货移库配置解析失败,code={}, value={}", CONFIG_CODE, config.getValue(), e); return settings; } } AutoFrontLocMoveSettings defaults() { AutoFrontLocMoveSettings settings = new AutoFrontLocMoveSettings(); settings.setEnabled(false); settings.setIntervalSeconds(DEFAULT_INTERVAL_SECONDS); settings.setUserId(DEFAULT_USER_ID); settings.setRules(new ArrayList<>()); return settings; } void normalize(AutoFrontLocMoveSettings settings) { if (settings.getIntervalSeconds() == null || settings.getIntervalSeconds() <= 0) { settings.setIntervalSeconds(DEFAULT_INTERVAL_SECONDS); } if (settings.getUserId() == null || settings.getUserId() <= 0L) { settings.setUserId(DEFAULT_USER_ID); } List<AutoFrontLocMoveSettings.Rule> normalizedRules = new ArrayList<>(); if (!Cools.isEmpty(settings.getRules())) { for (AutoFrontLocMoveSettings.Rule rule : settings.getRules()) { if (rule == null || rule.getCrnNo() == null) { continue; } List<Integer> frontRows = parseRows(rule.getFrontRows()); if (frontRows.isEmpty()) { continue; } // 解析结果预先挂在 rule 上,调度执行时直接使用,避免每次扫描重复拆字符串。 rule.setFrontRowList(frontRows); rule.setSourceRowList(parseRows(rule.getSourceRows())); normalizedRules.add(rule); } } settings.setRules(normalizedRules); if (normalizedRules.isEmpty()) { settings.setEnabled(false); } } List<Integer> parseRows(String rawRows) { LinkedHashSet<Integer> rows = new LinkedHashSet<>(); if (Cools.isEmpty(rawRows)) { return new ArrayList<>(); } String[] parts = rawRows.split(","); for (String part : parts) { String token = part == null ? null : part.trim(); if (Cools.isEmpty(token)) { continue; } if (token.contains("-")) { String[] range = token.split("-"); if (range.length != 2) { continue; } Integer start = safeParseInt(range[0]); Integer end = safeParseInt(range[1]); if (start == null || end == null || start <= 0 || end <= 0) { continue; } // 支持正序/倒序区间,例如 1-3 或 20-18。 int step = start <= end ? 1 : -1; for (int row = start; ; row += step) { rows.add(row); if (row == end) { break; } } continue; } Integer row = safeParseInt(token); if (row != null && row > 0) { rows.add(row); } } return new ArrayList<>(rows); } private boolean isConfigEnabled(Config config) { return config != null && config.getStatus() != null && config.getStatus() == 1 && !Cools.isEmpty(config.getValue()); } private Integer safeParseInt(String raw) { try { return Integer.parseInt(raw.trim()); } catch (Exception ignore) { return null; } } } src/main/java/com/zy/asrs/task/support/AutoFrontLocMoveSettings.java
New file @@ -0,0 +1,61 @@ package com.zy.asrs.task.support; import lombok.Data; import java.io.Serializable; import java.util.ArrayList; import java.util.List; @Data public class AutoFrontLocMoveSettings implements Serializable { private static final long serialVersionUID = 1L; private boolean enabled; /** * 调度执行间隔,单位秒。 */ private Integer intervalSeconds = 60; /** * 自动移库的操作人,默认沿用历史自动任务用户。 */ private Long userId = 9527L; /** * 每台堆垛机对应的前排补货规则。 */ private List<Rule> rules = new ArrayList<>(); @Data public static class Rule implements Serializable { private static final long serialVersionUID = 1L; /** * 堆垛机号。 */ private Integer crnNo; /** * 需要优先补满的排号,支持 1-3,5,8 这种格式,顺序即优先级。 */ private String frontRows; /** * 可作为来源的排号,支持 20-4,2 这种格式;为空时默认取“非前排”的其余排。 */ private String sourceRows; /** * 解析后的前排顺序,运行时使用。 */ private List<Integer> frontRowList = new ArrayList<>(); /** * 解析后的来源排顺序,运行时使用。 */ private List<Integer> sourceRowList = new ArrayList<>(); } } src/main/java/com/zy/common/config/ControllerResAdvice.java
@@ -113,6 +113,7 @@ } return uri.contains("/open/asrs") || uri.contains("/wcs/openapi/report") || uri.contains("/wcs/openapi/reassign/loc") || uri.contains("/rpc/pakin/loc"); } @@ -120,6 +121,9 @@ if (uri != null && uri.contains("/wcs/openapi/report")) { return "WCS回写"; } if (uri != null && uri.contains("/wcs/openapi/reassign/loc")) { return "WCS重分配入库"; } if (uri != null && uri.contains("/rpc/pakin/loc")) { return "WCS入库"; } src/main/java/com/zy/common/service/CommonService.java
@@ -216,6 +216,51 @@ } /** * 供 6.15 重分配接口复用:按外部指定的堆垛机顺序,在指定库区内找新的入库位。 * * 这里不推进 row_lastno 游标,只负责一次性的路径校验 + 设备校验 + 空库位搜索。 */ public StartupDto findRun2InboundLocByCandidateCrnNos(Integer sourceStaNo, Integer staDescId, Integer preferredArea, List<Integer> candidateCrnNos, LocTypeDto locTypeDto) { if (sourceStaNo == null) { throw new CoolException("源站不能为空"); } if (Cools.isEmpty(candidateCrnNos)) { return null; } Integer whsType = Utils.GetWhsType(sourceStaNo); RowLastno defaultRowLastno = rowLastnoService.selectById(whsType); if (Cools.isEmpty(defaultRowLastno)) { throw new CoolException("站点=" + sourceStaNo + " 未查询到对应的库位规则"); } RowLastno searchRowLastno = defaultRowLastno; if (preferredArea != null && preferredArea > 0) { RowLastno areaRowLastno = rowLastnoService.selectById(preferredArea); if (Cools.isEmpty(areaRowLastno)) { throw new CoolException("未找到库区轮询规则"); } searchRowLastno = areaRowLastno; } RowLastnoType rowLastnoType = rowLastnoTypeService.selectById(searchRowLastno.getTypeId()); if (Cools.isEmpty(rowLastnoType)) { throw new CoolException("数据异常,请联系管理员===》库位规则类型未知"); } if (rowLastnoType.getType() != 1 && rowLastnoType.getType() != 2) { throw new CoolException("当前仓库不支持重新分配入库位"); } StartupDto startupDto = new StartupDto(); LocMast locMast = findRun2EmptyLocByCrnNos(searchRowLastno, rowLastnoType, candidateCrnNos, locTypeDto, staDescId, sourceStaNo, startupDto, preferredArea, "reassign-inbound"); if (Cools.isEmpty(locMast) || !"O".equals(locMast.getLocSts())) { return null; } startupDto.setSourceStaNo(sourceStaNo); startupDto.setCrnNo(locMast.getCrnNo()); startupDto.setLocNo(locMast.getLocNo()); return startupDto; } /** * 空托盘识别规则: * 1. 以组托档物料编码 matnr=emptyPallet 为主,不再依赖 ioType=10。 * 2. 保留 staDescId=10 的兼容判断,避免旧链路还未切换时行为突变。 src/main/resources/sql/20260407_auto_front_loc_move_config.sql
New file @@ -0,0 +1,11 @@ IF NOT EXISTS (SELECT 1 FROM sys_config WHERE code = 'AutoFrontLocMove') BEGIN INSERT INTO sys_config (name, code, value, type, status) VALUES ( N'前排补货移库配置', 'AutoFrontLocMove', N'{"enabled":false,"intervalSeconds":60,"userId":9527,"rules":[{"crnNo":1,"frontRows":"1-3","sourceRows":"4-20"},{"crnNo":2,"frontRows":"20-18","sourceRows":"17-1"}]}', 2, 1 ); END; src/test/java/com/zy/asrs/task/handler/AutoFrontLocMoveHandlerTest.java
New file @@ -0,0 +1,100 @@ package com.zy.asrs.task.handler; import com.zy.asrs.entity.BasCrnp; import com.zy.asrs.entity.LocDetl; import com.zy.asrs.entity.LocMast; import com.zy.asrs.service.BasCrnpService; import com.zy.asrs.service.LocDetlService; import com.zy.asrs.service.LocMastService; import com.zy.asrs.service.WorkService; import com.zy.asrs.service.WrkMastService; import com.zy.asrs.task.support.AutoFrontLocMoveSettings; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Arrays; import java.util.Collections; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class AutoFrontLocMoveHandlerTest { @InjectMocks private AutoFrontLocMoveHandler handler; @Mock private BasCrnpService basCrnpService; @Mock private WrkMastService wrkMastService; @Mock private LocMastService locMastService; @Mock private LocDetlService locDetlService; @Mock private WorkService workService; @Test void shouldCreateMoveTaskForFirstFrontEmptyLoc() { AutoFrontLocMoveSettings settings = new AutoFrontLocMoveSettings(); settings.setEnabled(true); settings.setUserId(9527L); AutoFrontLocMoveSettings.Rule rule = new AutoFrontLocMoveSettings.Rule(); rule.setCrnNo(1); rule.setFrontRowList(Arrays.asList(1, 2)); settings.setRules(Collections.singletonList(rule)); BasCrnp basCrnp = new BasCrnp(); basCrnp.setCrnNo(1); basCrnp.setInEnable("Y"); basCrnp.setOutEnable("Y"); basCrnp.setCrnSts(3); basCrnp.setCrnErr(0L); when(basCrnpService.selectById(1)).thenReturn(basCrnp); when(wrkMastService.selectList(any())).thenReturn(Collections.emptyList()); LocMast targetLoc = buildLoc("0101010", 1, "O"); LocMast incompatibleSource = buildLoc("0105010", 5, "F"); incompatibleSource.setWhsType(2L); LocMast compatibleSource = buildLoc("0106010", 6, "F"); when(locMastService.selectList(any())) .thenReturn(Collections.singletonList(targetLoc)) .thenReturn(Arrays.asList(incompatibleSource, compatibleSource)); LocDetl locDetl = new LocDetl(); locDetl.setLocNo(compatibleSource.getLocNo()); locDetl.setFrozen(0); when(locDetlService.selectList(any())).thenReturn(Collections.singletonList(locDetl)); doNothing().when(workService).locMove(compatibleSource.getLocNo(), targetLoc.getLocNo(), 9527L); handler.start(settings); verify(workService).locMove(compatibleSource.getLocNo(), targetLoc.getLocNo(), 9527L); } private LocMast buildLoc(String locNo, Integer row, String locSts) { LocMast locMast = new LocMast(); locMast.setLocNo(locNo); locMast.setCrnNo(1); locMast.setRow1(row); locMast.setBay1(1); locMast.setLev1(1); locMast.setGro1(1); locMast.setWhsType(1L); locMast.setLocType1((short) 1); locMast.setLocType2((short) 1); locMast.setLocType3((short) 1); locMast.setOutEnable("Y"); locMast.setLocSts(locSts); locMast.setFrozen(0); return locMast; } } src/test/java/com/zy/asrs/task/support/AutoFrontLocMoveConfigResolverTest.java
New file @@ -0,0 +1,52 @@ package com.zy.asrs.task.support; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; class AutoFrontLocMoveConfigResolverTest { private final AutoFrontLocMoveConfigResolver resolver = new AutoFrontLocMoveConfigResolver(); @Test void parseRowsShouldKeepConfiguredOrderAndDeduplicate() { assertEquals(Arrays.asList(20, 19, 18, 15, 14, 13, 8), resolver.parseRows("20-18,15-13,8,18")); } @Test void normalizeShouldApplyDefaultsAndDropInvalidRules() { AutoFrontLocMoveSettings settings = new AutoFrontLocMoveSettings(); settings.setEnabled(true); settings.setIntervalSeconds(0); settings.setUserId(0L); AutoFrontLocMoveSettings.Rule invalidRule = new AutoFrontLocMoveSettings.Rule(); invalidRule.setCrnNo(1); AutoFrontLocMoveSettings.Rule validRule = new AutoFrontLocMoveSettings.Rule(); validRule.setCrnNo(2); validRule.setFrontRows("1-3"); validRule.setSourceRows("6-4"); settings.setRules(Arrays.asList(invalidRule, validRule)); resolver.normalize(settings); assertEquals(Integer.valueOf(60), settings.getIntervalSeconds()); assertEquals(Long.valueOf(9527L), settings.getUserId()); assertEquals(1, settings.getRules().size()); assertEquals(Arrays.asList(1, 2, 3), settings.getRules().get(0).getFrontRowList()); assertEquals(Arrays.asList(6, 5, 4), settings.getRules().get(0).getSourceRowList()); AutoFrontLocMoveSettings onlyInvalid = new AutoFrontLocMoveSettings(); onlyInvalid.setEnabled(true); onlyInvalid.setRules(Collections.singletonList(invalidRule)); resolver.normalize(onlyInvalid); assertFalse(onlyInvalid.isEnabled()); } }