diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/domain/DenseLanePosition.java b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/domain/DenseLanePosition.java new file mode 100644 index 0000000..604d948 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/domain/DenseLanePosition.java @@ -0,0 +1,51 @@ +package com.ruoyi.out.dense.domain; + +/** + * 密集库通道货位及其几何信息。 + */ +public class DenseLanePosition +{ + private String cargoSpaceId; + private String cargoSpaceName; + private String warehouseId; + private String groupCoding; + private String cargoArea; + private Long thePlatoon; + private Long theColumn; + private Long theFloor; + private String status; + private String locked; + private String trayCode; + private String directionType; + private Long minColumn; + private Long maxColumn; + + public String getCargoSpaceId() { return cargoSpaceId; } + public void setCargoSpaceId(String cargoSpaceId) { this.cargoSpaceId = cargoSpaceId; } + public String getCargoSpaceName() { return cargoSpaceName; } + public void setCargoSpaceName(String cargoSpaceName) { this.cargoSpaceName = cargoSpaceName; } + public String getWarehouseId() { return warehouseId; } + public void setWarehouseId(String warehouseId) { this.warehouseId = warehouseId; } + public String getGroupCoding() { return groupCoding; } + public void setGroupCoding(String groupCoding) { this.groupCoding = groupCoding; } + public String getCargoArea() { return cargoArea; } + public void setCargoArea(String cargoArea) { this.cargoArea = cargoArea; } + public Long getThePlatoon() { return thePlatoon; } + public void setThePlatoon(Long thePlatoon) { this.thePlatoon = thePlatoon; } + public Long getTheColumn() { return theColumn; } + public void setTheColumn(Long theColumn) { this.theColumn = theColumn; } + public Long getTheFloor() { return theFloor; } + public void setTheFloor(Long theFloor) { this.theFloor = theFloor; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getLocked() { return locked; } + public void setLocked(String locked) { this.locked = locked; } + public String getTrayCode() { return trayCode; } + public void setTrayCode(String trayCode) { this.trayCode = trayCode; } + public String getDirectionType() { return directionType; } + public void setDirectionType(String directionType) { this.directionType = directionType; } + public Long getMinColumn() { return minColumn; } + public void setMinColumn(Long minColumn) { this.minColumn = minColumn; } + public Long getMaxColumn() { return maxColumn; } + public void setMaxColumn(Long maxColumn) { this.maxColumn = maxColumn; } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/domain/DenseRelocationCandidate.java b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/domain/DenseRelocationCandidate.java new file mode 100644 index 0000000..86355f5 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/domain/DenseRelocationCandidate.java @@ -0,0 +1,17 @@ +package com.ruoyi.out.dense.domain; + +import com.ruoyi.bas.cargospace.domain.BasCargoSpace; + +/** + * 密集库移库源托盘与候选目标货位。 + */ +public class DenseRelocationCandidate +{ + private DenseLanePosition source; + private BasCargoSpace target; + + public DenseLanePosition getSource() { return source; } + public void setSource(DenseLanePosition source) { this.source = source; } + public BasCargoSpace getTarget() { return target; } + public void setTarget(BasCargoSpace target) { this.target = target; } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/domain/RelocationTargetQuery.java b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/domain/RelocationTargetQuery.java new file mode 100644 index 0000000..2600204 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/domain/RelocationTargetQuery.java @@ -0,0 +1,42 @@ +package com.ruoyi.out.dense.domain; + +/** + * 同层异通道移库目标查询条件。 + */ +public class RelocationTargetQuery +{ + private String warehouseId; + private Long theFloor; + private String sourceGroupCoding; + private String targetGroupCoding; + private String trayCode; + private String cargoSpaceType; + private Long height; + private String storageMode; + private String projectNo; + private String productId; + private String productName; + + public String getWarehouseId() { return warehouseId; } + public void setWarehouseId(String warehouseId) { this.warehouseId = warehouseId; } + public Long getTheFloor() { return theFloor; } + public void setTheFloor(Long theFloor) { this.theFloor = theFloor; } + public String getSourceGroupCoding() { return sourceGroupCoding; } + public void setSourceGroupCoding(String sourceGroupCoding) { this.sourceGroupCoding = sourceGroupCoding; } + public String getTargetGroupCoding() { return targetGroupCoding; } + public void setTargetGroupCoding(String targetGroupCoding) { this.targetGroupCoding = targetGroupCoding; } + public String getTrayCode() { return trayCode; } + public void setTrayCode(String trayCode) { this.trayCode = trayCode; } + public String getCargoSpaceType() { return cargoSpaceType; } + public void setCargoSpaceType(String cargoSpaceType) { this.cargoSpaceType = cargoSpaceType; } + public Long getHeight() { return height; } + public void setHeight(Long height) { this.height = height; } + public String getStorageMode() { return storageMode; } + public void setStorageMode(String storageMode) { this.storageMode = storageMode; } + public String getProjectNo() { return projectNo; } + public void setProjectNo(String projectNo) { this.projectNo = projectNo; } + public String getProductId() { return productId; } + public void setProductId(String productId) { this.productId = productId; } + public String getProductName() { return productName; } + public void setProductName(String productName) { this.productName = productName; } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/mapper/DenseLaneMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/mapper/DenseLaneMapper.java index c071044..8164f71 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/mapper/DenseLaneMapper.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/mapper/DenseLaneMapper.java @@ -1,5 +1,9 @@ package com.ruoyi.out.dense.mapper; +import java.util.List; +import com.ruoyi.bas.cargospace.domain.BasCargoSpace; +import com.ruoyi.out.dense.domain.DenseLanePosition; +import com.ruoyi.out.dense.domain.RelocationTargetQuery; import org.apache.ibatis.annotations.Param; /** @@ -8,4 +12,12 @@ import org.apache.ibatis.annotations.Param; public interface DenseLaneMapper { Integer isCurrentOutermost(@Param("cargoSpaceId") String cargoSpaceId); + + DenseLanePosition selectPosition(@Param("cargoSpaceId") String cargoSpaceId); + + List selectOccupiedLanePositions(DenseLanePosition source); + + List selectBlockersForUpdate(@Param("cargoSpaceId") String cargoSpaceId); + + BasCargoSpace selectRelocationTargetForUpdate(RelocationTargetQuery query); } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/IDenseLaneService.java b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/IDenseLaneService.java index fa2e5e9..2dd06fe 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/IDenseLaneService.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/IDenseLaneService.java @@ -1,7 +1,11 @@ package com.ruoyi.out.dense.service; +import java.util.List; +import com.ruoyi.bas.cargospace.domain.BasCargoSpace; import com.ruoyi.in.job.domain.MydoJob; +import com.ruoyi.out.dense.domain.DenseLanePosition; import com.ruoyi.out.dense.domain.DispatchEligibility; +import com.ruoyi.out.dense.domain.RelocationTargetQuery; /** * 密集库通道服务。 @@ -9,4 +13,10 @@ import com.ruoyi.out.dense.domain.DispatchEligibility; public interface IDenseLaneService { DispatchEligibility checkDispatchEligibility(MydoJob job); + + List orderOutermostFirst(List positions); + + List selectBlockersForUpdate(String cargoSpaceId); + + BasCargoSpace selectRelocationTargetForUpdate(RelocationTargetQuery query); } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/impl/DenseLaneServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/impl/DenseLaneServiceImpl.java index 37a4240..28aa226 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/impl/DenseLaneServiceImpl.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/dense/service/impl/DenseLaneServiceImpl.java @@ -1,8 +1,15 @@ package com.ruoyi.out.dense.service.impl; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Collections; +import java.util.List; +import com.ruoyi.bas.cargospace.domain.BasCargoSpace; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.in.job.domain.MydoJob; +import com.ruoyi.out.dense.domain.DenseLanePosition; import com.ruoyi.out.dense.domain.DispatchEligibility; +import com.ruoyi.out.dense.domain.RelocationTargetQuery; import com.ruoyi.out.dense.mapper.DenseLaneMapper; import com.ruoyi.out.dense.service.IDenseLaneService; import org.springframework.stereotype.Service; @@ -32,10 +39,66 @@ public class DenseLaneServiceImpl implements IDenseLaneService if (!StringUtils.hasText(job.getCargoSpaceId())) { return DispatchEligibility.blocked("03仓出库作业未分配源货位"); } - Integer outermost = denseLaneMapper.isCurrentOutermost(job.getCargoSpaceId()); - if (Integer.valueOf(1).equals(outermost)) { + DenseLanePosition source = denseLaneMapper.selectPosition(job.getCargoSpaceId()); + if (source == null) { + return DispatchEligibility.blocked("出库作业源货位不存在"); + } + if (!StringUtils.hasText(source.getGroupCoding())) { + return DispatchEligibility.allowed(); + } + List ordered = orderOutermostFirst( + denseLaneMapper.selectOccupiedLanePositions(source)); + if (ordered.isEmpty() || job.getCargoSpaceId().equals(ordered.get(0).getCargoSpaceId())) { return DispatchEligibility.allowed(); } return DispatchEligibility.blocked("当前货位不是密集库通道最外侧货位"); } + + @Override + public List orderOutermostFirst(List positions) + { + if (positions == null || positions.isEmpty()) { + return Collections.emptyList(); + } + List ordered = new ArrayList<>(positions); + String direction = StringUtils.defaultIfEmpty(ordered.get(0).getDirectionType(), "1"); + Comparator comparator; + if ("2".equals(direction)) { + comparator = Comparator.comparing(DenseLanePosition::getTheColumn, + Comparator.nullsLast(Long::compareTo)); + } else if ("3".equals(direction)) { + comparator = Comparator + .comparingLong(this::distanceToNearestBoundary) + .thenComparing(DenseLanePosition::getTheColumn, + Comparator.nullsLast(Long::compareTo)); + } else { + comparator = Comparator.comparing(DenseLanePosition::getTheColumn, + Comparator.nullsLast(Long::compareTo)).reversed(); + } + ordered.sort(comparator); + return ordered; + } + + @Override + public List selectBlockersForUpdate(String cargoSpaceId) + { + List blockers = denseLaneMapper.selectBlockersForUpdate(cargoSpaceId); + return blockers == null ? Collections.emptyList() : blockers; + } + + @Override + public BasCargoSpace selectRelocationTargetForUpdate(RelocationTargetQuery query) + { + return denseLaneMapper.selectRelocationTargetForUpdate(query); + } + + private long distanceToNearestBoundary(DenseLanePosition position) + { + if (position.getTheColumn() == null || position.getMinColumn() == null + || position.getMaxColumn() == null) { + return Long.MAX_VALUE; + } + return Math.min(position.getTheColumn() - position.getMinColumn(), + position.getMaxColumn() - position.getTheColumn()); + } } diff --git a/ruoyi-admin/src/main/resources/mapper/out/DenseLaneMapper.xml b/ruoyi-admin/src/main/resources/mapper/out/DenseLaneMapper.xml index 0ef4e04..af5e0bb 100644 --- a/ruoyi-admin/src/main/resources/mapper/out/DenseLaneMapper.xml +++ b/ruoyi-admin/src/main/resources/mapper/out/DenseLaneMapper.xml @@ -4,6 +4,221 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> + + + + + + + + + + + + + + + + + + + + + + + + + "); + + assertThat(sql) + .contains("status in ('2', '3', '4')") + .contains("tray_code is not null") + .contains("from bas_cargo_space lane_space") + .contains("min(lane_space.the_column) as min_column") + .contains("max(lane_space.the_column) as max_column") + .contains("direction_type"); + } + + @Test + void blockerQueryLocksRowsOrdersOuterToInnerAndSkipsActiveJobs() throws Exception + { + String xml = Files.readString(resolveMapperXml()); + String sql = section(xml, ""); + + assertThat(sql) + .containsIgnoringCase("UPDLOCK") + .containsIgnoringCase("HOLDLOCK") + .contains("warehouse_id = #{warehouseId}") + .contains("the_floor = #{theFloor}") + .contains("group_coding != #{sourceGroupCoding}") + .contains("status = '1'") + .contains("isnull(target_space.locked, 'N') = 'N'") + .contains("target_space.tray_code is null") + .contains("target_cargo_space_id") + .contains("not exists"); + } + + private Path resolveMapperXml() + { + return Stream.of( + Path.of("src/main/resources/mapper/out/DenseLaneMapper.xml"), + Path.of("ruoyi-admin/src/main/resources/mapper/out/DenseLaneMapper.xml")) + .filter(Files::exists) + .findFirst() + .orElseThrow(); + } + + private String section(String text, String startMarker, String endMarker) + { + int start = text.indexOf(startMarker); + assertThat(start).isGreaterThanOrEqualTo(0); + int end = text.indexOf(endMarker, start); + assertThat(end).isGreaterThan(start); + return text.substring(start, end); + } +} diff --git a/ruoyi-admin/src/test/java/com/ruoyi/out/dense/service/impl/DenseLaneServiceImplTest.java b/ruoyi-admin/src/test/java/com/ruoyi/out/dense/service/impl/DenseLaneServiceImplTest.java new file mode 100644 index 0000000..2e1bc18 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/out/dense/service/impl/DenseLaneServiceImplTest.java @@ -0,0 +1,116 @@ +package com.ruoyi.out.dense.service.impl; + +import java.util.List; +import com.ruoyi.in.job.domain.MydoJob; +import com.ruoyi.out.dense.domain.DenseLanePosition; +import com.ruoyi.out.dense.mapper.DenseLaneMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class DenseLaneServiceImplTest +{ + private DenseLaneMapper mapper; + private DenseLaneServiceImpl service; + + @BeforeEach + void setUp() + { + mapper = mock(DenseLaneMapper.class); + service = new DenseLaneServiceImpl(mapper); + } + + @Test + void directionOneUsesMaximumOccupiedColumnAsOutermost() + { + DenseLanePosition source = position("CS-8", 8L, "1", 1L, 10L); + DenseLanePosition outermost = position("CS-10", 10L, "1", 1L, 10L); + when(mapper.selectPosition("CS-8")).thenReturn(source); + when(mapper.selectPosition("CS-10")).thenReturn(outermost); + when(mapper.selectOccupiedLanePositions(source)).thenReturn(List.of( + position("CS-2", 2L, "1", 1L, 10L), + source, + outermost)); + when(mapper.selectOccupiedLanePositions(outermost)).thenReturn(List.of( + position("CS-2", 2L, "1", 1L, 10L), + source, + outermost)); + + assertThat(service.checkDispatchEligibility(job("03", "CS-8")).isEligible()).isFalse(); + assertThat(service.checkDispatchEligibility(job("03", "CS-10")).isEligible()).isTrue(); + } + + @Test + void directionTwoUsesMinimumOccupiedColumnAsOutermost() + { + DenseLanePosition source = position("CS-2", 2L, "2", 1L, 10L); + when(mapper.selectPosition("CS-2")).thenReturn(source); + when(mapper.selectOccupiedLanePositions(source)).thenReturn(List.of( + position("CS-2", 2L, "2", 1L, 10L), + position("CS-7", 7L, "2", 1L, 10L))); + + assertThat(service.checkDispatchEligibility(job("03", "CS-2")).isEligible()).isTrue(); + } + + @Test + void directionThreeUsesNearestBoundaryThenSmallerColumn() + { + List ordered = service.orderOutermostFirst(List.of( + position("CS-6", 6L, "3", 1L, 10L), + position("CS-10", 10L, "3", 1L, 10L), + position("CS-1", 1L, "3", 1L, 10L), + position("CS-5", 5L, "3", 1L, 10L))); + + assertThat(ordered).extracting(DenseLanePosition::getCargoSpaceId) + .containsExactly("CS-1", "CS-10", "CS-5", "CS-6"); + } + + @Test + void nonWarehouse03BypassesDenseLookup() + { + assertThat(service.checkDispatchEligibility(job("02", "CS-1")).isEligible()).isTrue(); + verify(mapper, never()).selectPosition("CS-1"); + } + + @Test + void blockerQueryReturnsOnlyUnassignedOuterTrays() + { + List blockers = List.of( + position("CS-9", 9L, "1", 1L, 10L), + position("CS-8", 8L, "1", 1L, 10L)); + when(mapper.selectBlockersForUpdate("CS-7")).thenReturn(blockers); + + assertThat(service.selectBlockersForUpdate("CS-7")) + .extracting(DenseLanePosition::getCargoSpaceId) + .containsExactly("CS-9", "CS-8"); + } + + private MydoJob job(String warehouseId, String cargoSpaceId) + { + MydoJob job = new MydoJob(); + job.setWarehouseId(warehouseId); + job.setCargoSpaceId(cargoSpaceId); + return job; + } + + private DenseLanePosition position(String cargoSpaceId, Long column, String direction, + Long minColumn, Long maxColumn) + { + DenseLanePosition position = new DenseLanePosition(); + position.setCargoSpaceId(cargoSpaceId); + position.setWarehouseId("03"); + position.setGroupCoding("G-01-01"); + position.setTheFloor(1L); + position.setTheColumn(column); + position.setDirectionType(direction); + position.setMinColumn(minColumn); + position.setMaxColumn(maxColumn); + position.setTrayCode("T-" + cargoSpaceId); + return position; + } +}