提交
This commit is contained in:
+10
@@ -62,6 +62,16 @@ public class InternalPalletLocationController {
|
||||
return ApiResponse.success(service.release(request));
|
||||
}
|
||||
|
||||
@PostMapping("/release-selected")
|
||||
@Operation(summary = "Release selected occupied locations for partial picking",
|
||||
description = "Explicit IDs release only those locations; otherwise the first N allocations are released")
|
||||
public ApiResponse<List<PalletAllocationDtos.SelectiveReleaseResult>> releaseSelected(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody PalletAllocationDtos.SelectiveReleaseRequest request) {
|
||||
verify(token);
|
||||
return ApiResponse.success(service.releaseSelected(request));
|
||||
}
|
||||
|
||||
private void verify(String token) {
|
||||
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
|
||||
if (!MessageDigest.isEqual(expectedToken, supplied)) {
|
||||
|
||||
@@ -138,6 +138,30 @@ public final class LocationDtos {
|
||||
) {
|
||||
}
|
||||
|
||||
public record SelectiveReleaseCommand(
|
||||
@Min(1) long tenantId,
|
||||
@NotBlank @Size(max = 40) String sourceService,
|
||||
@Min(1) long businessId,
|
||||
@Min(1) long operatorId,
|
||||
@NotBlank @Size(max = 500) String reason,
|
||||
@Min(1) int totalPackageCount,
|
||||
@Size(max = 200) List<@Min(1) Long> locationIds,
|
||||
@Min(1) Integer releaseLocationCount
|
||||
) {
|
||||
}
|
||||
|
||||
public record SelectiveReleaseResult(
|
||||
long reservationId,
|
||||
long occupancyId,
|
||||
int allocationNo,
|
||||
long warehouseId,
|
||||
long palletId,
|
||||
long locationId,
|
||||
String locationCode,
|
||||
int releasedPackageCount
|
||||
) {
|
||||
}
|
||||
|
||||
public record ActiveResource(
|
||||
long reservationId,
|
||||
String reservationNo,
|
||||
|
||||
@@ -52,6 +52,33 @@ public final class PalletAllocationDtos {
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases occupied locations for a partial pick. When {@code locationIds} is empty,
|
||||
* the first {@code releaseLocationCount} allocations are selected.
|
||||
*/
|
||||
public record SelectiveReleaseRequest(
|
||||
@Min(1) long tenantId,
|
||||
@NotBlank @Size(max = 40) String sourceService,
|
||||
@Min(1) long businessId,
|
||||
@Min(1) long operatorId,
|
||||
@NotBlank @Size(max = 500) String reason,
|
||||
@Min(1) int totalPackageCount,
|
||||
@Size(max = 200) List<@Min(1) Long> locationIds,
|
||||
@Min(1) Integer releaseLocationCount
|
||||
) {
|
||||
}
|
||||
|
||||
public record SelectiveReleaseResult(
|
||||
long reservationId,
|
||||
long occupancyId,
|
||||
int allocationNo,
|
||||
long palletId,
|
||||
long locationId,
|
||||
String locationCode,
|
||||
int releasedPackageCount
|
||||
) {
|
||||
}
|
||||
|
||||
public record AllocationResult(
|
||||
long reservationId,
|
||||
String reservationNo,
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 库位占用事实 Mapper。 */
|
||||
@Mapper
|
||||
public interface LocationOccupancyMapper extends BaseMapper<LocationOccupancyEntity> {
|
||||
@@ -26,4 +28,15 @@ public interface LocationOccupancyMapper extends BaseMapper<LocationOccupancyEnt
|
||||
""")
|
||||
int release(@Param("tenantId") long tenantId, @Param("id") long id,
|
||||
@Param("version") int version, @Param("reason") String reason);
|
||||
|
||||
@Select("""
|
||||
SELECT * FROM wh_location_occupancy
|
||||
WHERE tenant_id = #{tenantId} AND source_service = #{sourceService}
|
||||
AND business_id = #{businessId} AND occupancy_status = 'OCCUPIED'
|
||||
ORDER BY reservation_id ASC, id ASC
|
||||
FOR UPDATE
|
||||
""")
|
||||
List<LocationOccupancyEntity> lockOccupiedByBusiness(@Param("tenantId") long tenantId,
|
||||
@Param("sourceService") String sourceService,
|
||||
@Param("businessId") long businessId);
|
||||
}
|
||||
|
||||
@@ -143,6 +143,11 @@ public class LocationRepository {
|
||||
.eq(LocationOccupancyEntity::getReservationId, reservationId));
|
||||
}
|
||||
|
||||
public List<LocationOccupancyEntity> lockOccupiedByBusiness(long tenantId, String sourceService,
|
||||
long businessId) {
|
||||
return occupancies.lockOccupiedByBusiness(tenantId, sourceService, businessId);
|
||||
}
|
||||
|
||||
public void insertOccupancy(LocationOccupancyEntity entity) {
|
||||
occupancies.insert(entity);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import com.baiyue.warehouse.dto.LocationDtos.ReleaseCommand;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.ReservationResult;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.ReservationTarget;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.ReserveCommand;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.SelectiveReleaseCommand;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.SelectiveReleaseResult;
|
||||
import com.baiyue.warehouse.entity.LocationEntity;
|
||||
import com.baiyue.warehouse.entity.LocationOccupancyEntity;
|
||||
import com.baiyue.warehouse.entity.LocationReservationEntity;
|
||||
@@ -31,7 +33,6 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -46,6 +47,8 @@ import java.util.Set;
|
||||
public class LocationResourceService {
|
||||
@Resource
|
||||
private LocationRepository repository;
|
||||
@Resource
|
||||
private PickingPackageAllocator packageAllocator;
|
||||
|
||||
public List<LocationEntity> findAvailable(@Min(1) long tenantId, @Min(1) long warehouseId,
|
||||
@Min(1) @Max(200) int limit) {
|
||||
@@ -182,6 +185,66 @@ public class LocationResourceService {
|
||||
return List.copyOf(released);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<SelectiveReleaseResult> releaseSelected(@Valid SelectiveReleaseCommand command) {
|
||||
List<LocationOccupancyEntity> active = repository.lockOccupiedByBusiness(
|
||||
command.tenantId(), command.sourceService(), command.businessId());
|
||||
if (active.isEmpty()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "No occupied locations are available for this inbound order");
|
||||
}
|
||||
requireLimit(active.size());
|
||||
List<Integer> packages = packageAllocator.allocate(command.totalPackageCount(), active.size());
|
||||
Set<Long> requestedIds = command.locationIds() == null
|
||||
? Set.of() : new HashSet<>(command.locationIds());
|
||||
if (requestedIds.size() != (command.locationIds() == null ? 0 : command.locationIds().size())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "Selected location IDs must be unique");
|
||||
}
|
||||
|
||||
List<Integer> selectedIndexes = new ArrayList<>();
|
||||
if (!requestedIds.isEmpty()) {
|
||||
for (int index = 0; index < active.size(); index++) {
|
||||
if (requestedIds.contains(active.get(index).getLocationId())) {
|
||||
selectedIndexes.add(index);
|
||||
}
|
||||
}
|
||||
if (selectedIndexes.size() != requestedIds.size()) {
|
||||
throw new BizException(ResultCode.FORBIDDEN,
|
||||
"A selected location does not belong to the current inbound order");
|
||||
}
|
||||
if (command.releaseLocationCount() != null
|
||||
&& command.releaseLocationCount() != selectedIndexes.size()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST,
|
||||
"releaseLocationCount must match the selected location count");
|
||||
}
|
||||
} else {
|
||||
if (command.releaseLocationCount() == null || command.releaseLocationCount() > active.size()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST,
|
||||
"releaseLocationCount must not exceed the active location count");
|
||||
}
|
||||
for (int index = 0; index < command.releaseLocationCount(); index++) {
|
||||
selectedIndexes.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
List<SelectiveReleaseResult> released = new ArrayList<>(selectedIndexes.size());
|
||||
for (int index : selectedIndexes) {
|
||||
LocationOccupancyEntity occupancy = active.get(index);
|
||||
LocationEntity location = repository.findOwned(command.tenantId(), occupancy.getWarehouseId(),
|
||||
occupancy.getLocationId());
|
||||
if (location == null || repository.releaseOccupancy(occupancy, command.reason()) != 1
|
||||
|| (!Boolean.TRUE.equals(occupancy.getSharedOverflow())
|
||||
&& repository.changeStatus(command.tenantId(), occupancy.getWarehouseId(), occupancy.getLocationId(),
|
||||
"OCCUPIED", "AVAILABLE", location.getVersion(), command.operatorId()) != 1)) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Location occupancy changed concurrently while releasing selected locations");
|
||||
}
|
||||
released.add(new SelectiveReleaseResult(occupancy.getReservationId(), occupancy.getId(), index + 1,
|
||||
occupancy.getWarehouseId(), occupancy.getPalletId(), occupancy.getLocationId(),
|
||||
occupancy.getLocationCode(), packages.get(index)));
|
||||
}
|
||||
return List.copyOf(released);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ActiveResource> findActiveResources(@Min(1) long tenantId,
|
||||
@NotBlank String sourceService,
|
||||
|
||||
+22
@@ -129,6 +129,28 @@ public class PalletLocationAllocationService {
|
||||
return released;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PALLET_LOCATION_RELEASE_SELECTED", resourceType = "INBOUND",
|
||||
resourceId = "#request.businessId", description = "Release selected occupied locations for picking")
|
||||
public List<PalletAllocationDtos.SelectiveReleaseResult> releaseSelected(
|
||||
PalletAllocationDtos.SelectiveReleaseRequest request) {
|
||||
List<LocationDtos.SelectiveReleaseResult> released = locationResourceService.releaseSelected(
|
||||
new LocationDtos.SelectiveReleaseCommand(request.tenantId(), request.sourceService(), request.businessId(),
|
||||
request.operatorId(), request.reason(), request.totalPackageCount(), request.locationIds(),
|
||||
request.releaseLocationCount()));
|
||||
List<PalletAllocationDtos.SelectiveReleaseResult> result = new ArrayList<>(released.size());
|
||||
for (LocationDtos.SelectiveReleaseResult resource : released) {
|
||||
if (palletRepository.transitionStatus(request.tenantId(), resource.warehouseId(), resource.palletId(),
|
||||
List.of("OCCUPIED"), "AVAILABLE") != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Pallet changed concurrently during selected release");
|
||||
}
|
||||
result.add(new PalletAllocationDtos.SelectiveReleaseResult(resource.reservationId(),
|
||||
resource.occupancyId(), resource.allocationNo(), resource.palletId(), resource.locationId(),
|
||||
resource.locationCode(), resource.releasedPackageCount()));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private List<LocationDtos.ActiveResource> active(long tenantId, String sourceService, long businessId) {
|
||||
return new ArrayList<>(locationResourceService.findActiveResources(tenantId, sourceService, businessId));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Allocates cartons by location with a ceiling-sized leading share. */
|
||||
@Component
|
||||
public class PickingPackageAllocator {
|
||||
public List<Integer> allocate(int totalPackageCount, int locationCount) {
|
||||
if (totalPackageCount < 1 || locationCount < 1 || locationCount > 200) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "Package and location counts must be positive");
|
||||
}
|
||||
int share = ((totalPackageCount - 1) / locationCount) + 1;
|
||||
int remaining = totalPackageCount;
|
||||
List<Integer> result = new ArrayList<>(locationCount);
|
||||
for (int index = 0; index < locationCount; index++) {
|
||||
int allocated = Math.min(share, remaining);
|
||||
result.add(allocated);
|
||||
remaining -= allocated;
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
}
|
||||
+100
-1
@@ -4,6 +4,7 @@ import com.baiyue.warehouse.dto.LocationDtos.ConfirmCommand;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.OccupancyTarget;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.ReservationTarget;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.ReserveCommand;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.SelectiveReleaseCommand;
|
||||
import com.baiyue.warehouse.entity.LocationEntity;
|
||||
import com.baiyue.warehouse.entity.LocationOccupancyEntity;
|
||||
import com.baiyue.warehouse.entity.LocationReservationEntity;
|
||||
@@ -20,6 +21,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
@@ -29,7 +31,7 @@ import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(LocationResourceService.class)
|
||||
@SpringJUnitConfig(classes = {LocationResourceService.class, PickingPackageAllocator.class})
|
||||
class LocationResourceServiceTest {
|
||||
@MockBean private LocationRepository repository;
|
||||
@Resource private LocationResourceService service;
|
||||
@@ -138,6 +140,79 @@ class LocationResourceServiceTest {
|
||||
verify(repository).insertOccupancy(any(LocationOccupancyEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReleaseOnlyExplicitlySelectedLocations() {
|
||||
List<LocationOccupancyEntity> active = List.of(
|
||||
occupancy(201L, 101L, 9L, "A1", 501L),
|
||||
occupancy(202L, 102L, 10L, "A2", 502L),
|
||||
occupancy(203L, 103L, 11L, "A3", 503L));
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(active);
|
||||
when(repository.findOwned(10001L, 88L, 10L)).thenReturn(occupiedLocation(10L, "A2"));
|
||||
when(repository.releaseOccupancy(active.get(1), "PDA partial pick")).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, 10L, "OCCUPIED", "AVAILABLE", 0, 7L)).thenReturn(1);
|
||||
|
||||
var result = service.releaseSelected(new SelectiveReleaseCommand(10001L, "FIRSTLEG", 301L, 7L,
|
||||
"PDA partial pick", 10, List.of(10L), null));
|
||||
|
||||
assertThat(result).singleElement().satisfies(item -> {
|
||||
assertThat(item.allocationNo()).isEqualTo(2);
|
||||
assertThat(item.locationId()).isEqualTo(10L);
|
||||
assertThat(item.releasedPackageCount()).isEqualTo(4);
|
||||
});
|
||||
verify(repository, never()).releaseOccupancy(active.get(0), "PDA partial pick");
|
||||
verify(repository, never()).releaseOccupancy(active.get(2), "PDA partial pick");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReleaseFirstNAllocationsWhenLocationsAreNotSpecified() {
|
||||
List<LocationOccupancyEntity> active = List.of(
|
||||
occupancy(201L, 101L, 9L, "A1", 501L),
|
||||
occupancy(202L, 102L, 10L, "A2", 502L),
|
||||
occupancy(203L, 103L, 11L, "A3", 503L));
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(active);
|
||||
for (int index = 0; index < 2; index++) {
|
||||
LocationOccupancyEntity item = active.get(index);
|
||||
when(repository.findOwned(10001L, 88L, item.getLocationId()))
|
||||
.thenReturn(occupiedLocation(item.getLocationId(), item.getLocationCode()));
|
||||
when(repository.releaseOccupancy(item, "PDA default pick")).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, item.getLocationId(), "OCCUPIED", "AVAILABLE", 0, 7L))
|
||||
.thenReturn(1);
|
||||
}
|
||||
|
||||
var result = service.releaseSelected(new SelectiveReleaseCommand(10001L, "FIRSTLEG", 301L, 7L,
|
||||
"PDA default pick", 10, List.of(), 2));
|
||||
|
||||
assertThat(result).extracting(item -> item.allocationNo()).containsExactly(1, 2);
|
||||
assertThat(result).extracting(item -> item.locationCode()).containsExactly("A1", "A2");
|
||||
assertThat(result).extracting(item -> item.releasedPackageCount()).containsExactly(4, 4);
|
||||
verify(repository, never()).releaseOccupancy(active.get(2), "PDA default pick");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectLocationOwnedByAnotherInboundOrder() {
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(
|
||||
occupancy(201L, 101L, 9L, "A1", 501L)));
|
||||
|
||||
assertThatThrownBy(() -> service.releaseSelected(new SelectiveReleaseCommand(
|
||||
10001L, "FIRSTLEG", 301L, 7L, "PDA partial pick", 10, List.of(99L), null)))
|
||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
verify(repository, never()).releaseOccupancy(any(), any(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailOnConcurrentOccupancyUpdate() {
|
||||
LocationOccupancyEntity active = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(active));
|
||||
when(repository.findOwned(10001L, 88L, 9L)).thenReturn(occupiedLocation(9L, "A1"));
|
||||
when(repository.releaseOccupancy(active, "PDA pick")).thenReturn(0);
|
||||
|
||||
assertThatThrownBy(() -> service.releaseSelected(new SelectiveReleaseCommand(
|
||||
10001L, "FIRSTLEG", 301L, 7L, "PDA pick", 10, List.of(), 1)))
|
||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
verify(repository, never()).changeStatus(anyLong(), anyLong(), anyLong(),
|
||||
any(String.class), any(String.class), anyInt(), anyLong());
|
||||
}
|
||||
|
||||
private static ReserveCommand reserveCommand() {
|
||||
return new ReserveCommand(10001L, 88L, "FIRSTLEG", 301L, "RK001", 7L,
|
||||
LocalDateTime.now().plusMinutes(30), List.of(
|
||||
@@ -176,6 +251,30 @@ class LocationResourceServiceTest {
|
||||
return location;
|
||||
}
|
||||
|
||||
private static LocationEntity occupiedLocation(long id, String code) {
|
||||
LocationEntity location = location(id, code);
|
||||
location.setStatus("OCCUPIED");
|
||||
return location;
|
||||
}
|
||||
|
||||
private static LocationOccupancyEntity occupancy(long id, long reservationId, long locationId,
|
||||
String locationCode, long palletId) {
|
||||
LocationOccupancyEntity occupancy = new LocationOccupancyEntity();
|
||||
occupancy.setId(id);
|
||||
occupancy.setTenantId(10001L);
|
||||
occupancy.setReservationId(reservationId);
|
||||
occupancy.setSourceService("FIRSTLEG");
|
||||
occupancy.setBusinessId(301L);
|
||||
occupancy.setWarehouseId(88L);
|
||||
occupancy.setPalletId(palletId);
|
||||
occupancy.setLocationId(locationId);
|
||||
occupancy.setLocationCode(locationCode);
|
||||
occupancy.setSharedOverflow(false);
|
||||
occupancy.setOccupancyStatus("OCCUPIED");
|
||||
occupancy.setVersion(0);
|
||||
return occupancy;
|
||||
}
|
||||
|
||||
private static LocationReservationEntity reservation(String status) {
|
||||
LocationReservationEntity reservation = new LocationReservationEntity();
|
||||
reservation.setId(101L);
|
||||
|
||||
+19
@@ -104,6 +104,25 @@ class PalletLocationAllocationServiceTest {
|
||||
verify(pallets, never()).findAvailable(any(Long.class), any(Long.class), any(), any(), any(Integer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReleaseOnlyPalletsReturnedBySelectedLocationRelease() {
|
||||
var resource = new LocationDtos.SelectiveReleaseResult(
|
||||
102L, 202L, 2, 20L, 30L, 40L, "A-02", 4);
|
||||
when(locations.releaseSelected(any())).thenReturn(List.of(resource));
|
||||
when(pallets.transitionStatus(10001L, 20L, 30L, List.of("OCCUPIED"), "AVAILABLE"))
|
||||
.thenReturn(1);
|
||||
|
||||
var result = service.releaseSelected(new PalletAllocationDtos.SelectiveReleaseRequest(
|
||||
10001L, "FIRSTLEG", 88L, 9001L, "PDA partial pick", 10, List.of(40L), null));
|
||||
|
||||
assertThat(result).singleElement().satisfies(item -> {
|
||||
assertThat(item.allocationNo()).isEqualTo(2);
|
||||
assertThat(item.locationId()).isEqualTo(40L);
|
||||
assertThat(item.releasedPackageCount()).isEqualTo(4);
|
||||
});
|
||||
verify(pallets).transitionStatus(10001L, 20L, 30L, List.of("OCCUPIED"), "AVAILABLE");
|
||||
}
|
||||
|
||||
private static PalletAllocationDtos.ReserveRequest reserve(long businessId) {
|
||||
return new PalletAllocationDtos.ReserveRequest(10001, 20, "FIRSTLEG", businessId, "RK-001", "measure-001",
|
||||
9001, new BigDecimal("2"), new BigDecimal("2"), "木托盘", LocalDateTime.now().plusMinutes(30), true);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PickingPackageAllocatorTest {
|
||||
private final PickingPackageAllocator allocator = new PickingPackageAllocator();
|
||||
|
||||
@Test
|
||||
void shouldUseCeilingShareAndCapFinalLocation() {
|
||||
assertThat(allocator.allocate(10, 3)).containsExactly(4, 4, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleExactDivision() {
|
||||
assertThat(allocator.allocate(12, 3)).containsExactly(4, 4, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNeverAllocateMoreThanRemainingPackages() {
|
||||
assertThat(allocator.allocate(2, 3)).containsExactly(1, 1, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user