This commit is contained in:
2026-09-04 12:18:36 +08:00
parent 566b07be62
commit b697e4c9f0
51 changed files with 1898 additions and 623 deletions
@@ -4,7 +4,9 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.PalletAllocationDtos;
import com.baiyue.warehouse.dto.LocationDtos;
import com.baiyue.warehouse.dto.LocationBlockDtos;
import com.baiyue.warehouse.service.PalletLocationAllocationService;
import com.baiyue.warehouse.service.LocationBlockService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
@@ -22,11 +24,13 @@ class InternalPalletLocationControllerTest {
private static final String INTERNAL_TOKEN = "test-internal-service-token";
private final PalletLocationAllocationService service = mock(PalletLocationAllocationService.class);
private final LocationBlockService blockService = mock(LocationBlockService.class);
private final InternalPalletLocationController controller = new InternalPalletLocationController();
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(controller, "service", service);
ReflectionTestUtils.setField(controller, "blockService", blockService);
ReflectionTestUtils.setField(controller, "internalToken", INTERNAL_TOKEN);
controller.initializeToken();
}
@@ -78,6 +82,22 @@ class InternalPalletLocationControllerTest {
verify(service).occupiedResources(0L, "firstleg-service", 88L);
}
@Test
void reservesWholeBlockOnlyForInternalCaller() {
LocationBlockDtos.ReserveRequest request = new LocationBlockDtos.ReserveRequest(20L, "firstleg-service",
88L, "RK-001", 3, "reserve-block-1", 9001L, java.time.LocalDateTime.now().plusHours(1));
LocationBlockDtos.BlockResult expected = new LocationBlockDtos.BlockResult(700L, "RESERVED", List.of(
new LocationBlockDtos.Resource(700L, 701L, 40L, "A01", 1, false, "RESERVED")));
when(blockService.reserve(request)).thenReturn(expected);
assertThat(controller.reserveBlock(INTERNAL_TOKEN, request).data()).isEqualTo(expected);
verify(blockService).reserve(request);
assertThatThrownBy(() -> controller.reserveBlock("invalid", request))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.FORBIDDEN);
}
private static PalletAllocationDtos.TransferRequest request() {
return new PalletAllocationDtos.TransferRequest(10001L, "FIRSTLEG", 88L, 89L,
"RK-001-S1", "transfer-88-89", 9001L, List.of(new PalletAllocationDtos.TransferItem(40L, 5)));
@@ -47,6 +47,13 @@ class WarehouseSchemaContractTest {
@Test
void shouldHandleCase04() throws IOException {
String sql = read("phase2-pallet-location-schema.sql");
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_location_block (")
.contains("CREATE TABLE IF NOT EXISTS wh_location_block_slot (")
.contains("UNIQUE KEY uk_location_block_active_normal")
.contains("UNIQUE KEY uk_location_block_active_business")
.contains("block_status IN ('RESERVED','OCCUPIED')")
.contains("resource_status IN ('RESERVED','OCCUPIED')")
.contains("COMMENT='整票库位块槽位;空槽也由当前入仓码独占'");
assertColumns(sql, "wh_pallet", "tenant_id", "warehouse_id", "pallet_code", "pallet_type",
"capacity_cbm", "weight_kg", "color", "remark", "deleted", "version");
assertThat(tableDefinition(sql, "wh_pallet"))
@@ -0,0 +1,176 @@
package com.baiyue.warehouse.service;
import com.baiyue.warehouse.dto.LocationBlockDtos;
import com.baiyue.warehouse.entity.LocationBlockEntity;
import com.baiyue.warehouse.entity.LocationBlockSlotEntity;
import com.baiyue.warehouse.entity.LocationEntity;
import com.baiyue.warehouse.entity.WarehouseEntity;
import com.baiyue.warehouse.mapper.LocationBlockMapper;
import com.baiyue.warehouse.mapper.LocationBlockSlotMapper;
import com.baiyue.warehouse.repository.LocationRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
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 LocationBlockServiceTest {
private final LocationRepository locations = mock(LocationRepository.class);
private final LocationBlockMapper blocks = mock(LocationBlockMapper.class);
private final LocationBlockSlotMapper slots = mock(LocationBlockSlotMapper.class);
private final LocationBlockService service = new LocationBlockService();
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(service, "locations", locations);
ReflectionTestUtils.setField(service, "blocks", blocks);
ReflectionTestUtils.setField(service, "slots", slots);
WarehouseEntity warehouse = new WarehouseEntity();
warehouse.setId(20L);
warehouse.setTenantId(0L);
warehouse.setStatus("ACTIVE");
when(locations.findWarehouse(0L, 20L)).thenReturn(warehouse);
when(blocks.selectOne(any())).thenReturn(null);
when(blocks.insert(any(LocationBlockEntity.class))).thenReturn(1);
when(slots.insert(any(LocationBlockSlotEntity.class))).thenReturn(1);
when(locations.changeStatus(anyLong(), anyLong(), anyLong(), any(), any(), anyInt(), anyLong())).thenReturn(1);
}
@Test
void reservesSameZoneAndPrefixContinuousRunAheadOfEarlierGap() {
when(locations.lockAvailable(0L, 20L, 1000)).thenReturn(List.of(
location(1L, "A", "A01"), location(2L, "A", "A03"),
location(3L, "B", "B01"), location(4L, "B", "B02"), location(5L, "B", "B03")));
LocationBlockDtos.BlockResult result = service.reserve(request(3));
assertThat(result.blockStatus()).isEqualTo("RESERVED");
assertThat(result.resources()).extracting(LocationBlockDtos.Resource::locationCode)
.containsExactly("B01", "B02", "B03");
assertThat(result.resources()).extracting(LocationBlockDtos.Resource::slotNo).containsExactly(1, 2, 3);
assertThat(result.resources()).allSatisfy(item -> {
assertThat(item.blockId()).isEqualTo(result.blockId());
assertThat(item.sharedOverflow()).isFalse();
assertThat(item.resourceStatus()).isEqualTo("RESERVED");
});
verify(locations, never()).lockOrCreateSharedOverflow(anyLong(), anyLong(), anyLong());
}
@Test
void keepsAllOrdinaryLocationsAndUsesSharedA0ForOnlyTheMissingSlots() {
LocationEntity ordinary = location(1L, "A", "A01");
LocationEntity overflow = location(99L, "OVERFLOW", "A0");
overflow.setSharedOverflow(true);
when(locations.lockAvailable(0L, 20L, 1000)).thenReturn(List.of(ordinary));
when(locations.lockOrCreateSharedOverflow(0L, 20L, 9001L)).thenReturn(overflow);
LocationBlockDtos.BlockResult result = service.reserve(request(3));
assertThat(result.resources()).extracting(LocationBlockDtos.Resource::locationCode)
.containsExactly("A01", "A0", "A0");
assertThat(result.resources()).extracting(LocationBlockDtos.Resource::sharedOverflow)
.containsExactly(false, true, true);
verify(locations).changeStatus(0L, 20L, 1L, "AVAILABLE", "RESERVED", 0, 9001L);
verify(locations, never()).changeStatus(eq(0L), eq(20L), eq(99L), any(), any(), anyInt(), anyLong());
}
@Test
void transfersOwnershipWithoutReleasingAnySlot() {
LocationBlockEntity block = block("OCCUPIED", 88L, null);
when(blocks.lockById(700L)).thenReturn(block);
when(blocks.transfer(700L, 88L, 89L, "RK-S1", "transfer-1", 0)).thenReturn(1);
when(slots.lockByBlockId(700L)).thenReturn(List.of(slot(701L, 1, 1L, "A01", false, "OCCUPIED")));
LocationBlockDtos.BlockResult result = service.transfer(
new LocationBlockDtos.TransferRequest(700L, 88L, 89L, "RK-S1", "transfer-1", 9001L));
assertThat(result.blockStatus()).isEqualTo("OCCUPIED");
verify(blocks).transfer(700L, 88L, 89L, "RK-S1", "transfer-1", 0);
verify(slots, never()).changeBlockStatus(anyLong(), any(), any());
verify(locations, never()).changeStatus(anyLong(), anyLong(), anyLong(), any(), any(), anyInt(), anyLong());
}
@Test
void releasesEveryNormalSlotTogetherAndLeavesSharedA0Available() {
LocationBlockEntity block = block("OCCUPIED", 88L, null);
LocationBlockSlotEntity first = slot(701L, 1, 1L, "A01", false, "OCCUPIED");
LocationBlockSlotEntity overflow = slot(702L, 2, 99L, "A0", true, "OCCUPIED");
when(blocks.lockById(700L)).thenReturn(block);
when(slots.lockByBlockId(700L)).thenReturn(List.of(first, overflow));
when(locations.lockOwned(0L, 20L, 1L)).thenReturn(location(1L, "A", "A01", "OCCUPIED"));
when(slots.changeBlockStatus(700L, "OCCUPIED", "RELEASED")).thenReturn(2);
when(blocks.changeStatus(700L, 88L, "OCCUPIED", "RELEASED", "release-1", 0)).thenReturn(1);
LocationBlockDtos.BlockResult result = service.release(new LocationBlockDtos.ReleaseRequest(
700L, 88L, "release-1", 9001L, "ALL_PACKAGES_PICKED"));
assertThat(result.resources()).extracting(LocationBlockDtos.Resource::resourceStatus)
.containsOnly("RELEASED");
verify(locations).changeStatus(0L, 20L, 1L, "OCCUPIED", "AVAILABLE", 0, 9001L);
verify(locations, never()).lockOwned(0L, 20L, 99L);
}
private static LocationBlockDtos.ReserveRequest request(int slots) {
return new LocationBlockDtos.ReserveRequest(20L, "firstleg-service", 88L, "RK-001", slots,
"reserve-1", 9001L, LocalDateTime.now().plusHours(1));
}
private static LocationEntity location(long id, String zone, String code) {
return location(id, zone, code, "AVAILABLE");
}
private static LocationEntity location(long id, String zone, String code, String status) {
LocationEntity entity = new LocationEntity();
entity.setId(id);
entity.setTenantId(0L);
entity.setWarehouseId(20L);
entity.setZoneCode(zone);
entity.setLocationCode(code);
entity.setSharedOverflow(false);
entity.setStatus(status);
entity.setVersion(0);
return entity;
}
private static LocationBlockEntity block(String status, long businessId, String operationKey) {
LocationBlockEntity entity = new LocationBlockEntity();
entity.setId(700L);
entity.setTenantId(0L);
entity.setWarehouseId(20L);
entity.setSourceService("firstleg-service");
entity.setBusinessId(businessId);
entity.setInboundCode("RK-001");
entity.setSlotCount(2);
entity.setBlockStatus(status);
entity.setLastOperationKey(operationKey);
entity.setVersion(0);
return entity;
}
private static LocationBlockSlotEntity slot(long id, int slotNo, long locationId, String code,
boolean overflow, String status) {
LocationBlockSlotEntity entity = new LocationBlockSlotEntity();
entity.setId(id);
entity.setTenantId(0L);
entity.setBlockId(700L);
entity.setSlotNo(slotNo);
entity.setLocationId(locationId);
entity.setLocationCode(code);
entity.setSharedOverflow(overflow);
entity.setResourceStatus(status);
entity.setVersion(0);
return entity;
}
}