This commit is contained in:
2026-08-14 17:37:59 +08:00
parent 87e919f3c4
commit feced8eb92
40 changed files with 1142 additions and 244 deletions
@@ -0,0 +1,59 @@
package com.baiyue.warehouse.controller;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.PalletRequests;
import com.baiyue.warehouse.service.PalletApplicationService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
class InternalPalletControllerTest {
private static final String TOKEN = "test-internal-token";
private final PalletApplicationService service = mock(PalletApplicationService.class);
private final InternalPalletController controller = new InternalPalletController();
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(controller, "service", service);
ReflectionTestUtils.setField(controller, "internalToken", TOKEN);
controller.initializeToken();
}
@Test
void readsSnapshotFromPlatformScope() {
PalletRequests.Snapshot expected = snapshot(30L, "TP-001");
when(service.snapshot(0L, 20L, 30L)).thenReturn(expected);
assertThat(controller.snapshot(TOKEN, 30L, 20L).data()).isEqualTo(expected);
verify(service).snapshot(0L, 20L, 30L);
}
@Test
void listsAllWarehouseSnapshotsFromPlatformScope() {
List<PalletRequests.Snapshot> expected = List.of(snapshot(30L, "TP-001"), snapshot(31L, "TP-002"));
when(service.snapshots(0L, 20L)).thenReturn(expected);
assertThat(controller.snapshots(TOKEN, 20L).data()).isEqualTo(expected);
verify(service).snapshots(0L, 20L);
}
@Test
void rejectsInvalidInternalToken() {
assertThatThrownBy(() -> controller.snapshots("invalid", 20L)).isInstanceOf(BizException.class);
verifyNoInteractions(service);
}
private static PalletRequests.Snapshot snapshot(long id, String code) {
return new PalletRequests.Snapshot(id, code, "STANDARD", new BigDecimal("1.2"), new BigDecimal("8"));
}
}
@@ -0,0 +1,25 @@
package com.baiyue.warehouse.controller;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class PalletOpenApiContractTest {
@Test
void freezesInternalSnapshotPathsAndFields() throws IOException {
String yaml = Files.readString(Path.of("src/main/resources/openapi/pallet-v1.yaml"));
assertThat(yaml)
.contains("/internal/warehouse/v1/pallets/{palletId}/snapshot:")
.contains("/internal/warehouse/v1/pallets/snapshots:")
.contains("required: [palletId, palletCode, palletType, capacityCbm, tareWeightKg]")
.contains("additionalProperties: false")
.doesNotContain("tenantId:")
.doesNotContain("available:")
.doesNotContain("status:");
}
}
@@ -10,7 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class InitialWarehouseResourceSeedSqlContractTest {
@Test
void initializesAvailablePalletAndLocationsForBothDomesticWarehouses() throws Exception {
void initializesReusablePalletMasterDataAndAvailableLocationsForBothDomesticWarehouses() throws Exception {
assertModuleSeed(Files.readString(Path.of("src", "main", "resources", "db", "phase2-pallet-seed.sql"),
StandardCharsets.UTF_8));
assertUnifiedSeed(Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
@@ -19,6 +19,7 @@ class InitialWarehouseResourceSeedSqlContractTest {
private static void assertModuleSeed(String sql) {
assertSeed(sql, "190000000000000001", "190000000000000002", "190000000000000");
assertThat(sql).doesNotContain("color,status,remark");
}
private static void assertUnifiedSeed(String sql) {
@@ -44,21 +44,29 @@ class WarehouseSchemaContractTest {
void shouldHandleCase04() throws IOException {
String sql = read("phase2-pallet-location-schema.sql");
assertColumns(sql, "wh_pallet", "tenant_id", "warehouse_id", "pallet_code", "pallet_type",
"capacity_cbm", "weight_kg", "color", "status", "remark", "deleted", "version");
assertThat(sql).contains("uk_pallet_tenant_code (tenant_id, pallet_code)");
"capacity_cbm", "weight_kg", "color", "remark", "deleted", "version");
assertThat(tableDefinition(sql, "wh_pallet"))
.contains("uk_pallet_tenant_code (tenant_id, pallet_code)")
.contains("idx_pallet_warehouse (tenant_id, warehouse_id, deleted, pallet_code)")
.doesNotContain("status VARCHAR")
.doesNotContain("AVAILABLE")
.doesNotContain("RESERVED")
.doesNotContain("OCCUPIED");
assertColumns(sql, "wh_location", "tenant_id", "warehouse_id", "zone_code", "zone_name",
"location_code", "location_type", "max_weight_kg", "shared_overflow", "status", "remark", "deleted", "version");
assertColumns(sql, "wh_location_reservation", "idempotency_key", "business_id", "pallet_id",
"location_id", "shared_overflow", "reservation_status", "expires_at",
"active_reservation", "active_pallet_reservation", "version");
"active_reservation", "version");
assertColumns(sql, "wh_location_occupancy", "reservation_id", "business_id", "pallet_id",
"location_id", "shared_overflow", "occupancy_status",
"active_location_occupancy", "active_pallet_occupancy", "version");
"active_location_occupancy", "version");
assertThat(sql).contains("uk_location_tenant_warehouse_code")
.contains("uk_location_active_reservation")
.contains("uk_pallet_active_reservation (tenant_id, pallet_id, active_pallet_reservation)")
.contains("uk_location_active_occupancy")
.contains("uk_pallet_active_occupancy");
.doesNotContain("active_pallet_reservation")
.doesNotContain("active_pallet_occupancy")
.doesNotContain("uk_pallet_active_reservation")
.doesNotContain("uk_pallet_active_occupancy");
}
@Test
@@ -112,6 +120,24 @@ class WarehouseSchemaContractTest {
}
}
@Test
void unifiedSqlShouldConvergePalletsToReusableMasterData() throws IOException {
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
String palletDefinition = tableDefinition(unified, "wh_pallet");
assertThat(palletDefinition)
.contains("idx_pallet_warehouse (tenant_id, warehouse_id, deleted, pallet_code)")
.contains("idx_pallet_type (tenant_id, warehouse_id, pallet_type, capacity_cbm, deleted)")
.doesNotContain("status VARCHAR")
.doesNotContain("idx_pallet_available");
assertThat(unified)
.contains("ALTER TABLE wh_pallet DROP COLUMN status")
.contains("ALTER TABLE wh_location_reservation DROP COLUMN active_pallet_reservation")
.contains("ALTER TABLE wh_location_occupancy DROP COLUMN active_pallet_occupancy")
.contains("托盘动态资源状态或活动唯一约束仍存在")
.contains("库位活动预占或占用唯一约束不完整");
}
private static String read(String file) throws IOException {
return Files.readString(Path.of("src/main/resources/db", file));
}
@@ -133,4 +159,10 @@ class WarehouseSchemaContractTest {
int end = sql.indexOf(' ', start);
return sql.substring(start, end);
}
private static String tableDefinition(String sql, String table) {
int start = sql.indexOf("CREATE TABLE IF NOT EXISTS " + table + " (");
int end = sql.indexOf("ENGINE=InnoDB", start);
return sql.substring(start, end);
}
}
@@ -27,7 +27,7 @@ class PalletApplicationServiceTest {
@Test
void shouldHandleCase01() {
PalletEntity entity = pallet("OCCUPIED");
PalletEntity entity = pallet();
when(repository.findOwned(10001, 20, 30)).thenReturn(entity);
org.assertj.core.api.Assertions.assertThat(service.detail(10001, 20, 30))
.extracting("id", "palletCode").containsExactly(30L, null);
@@ -41,7 +41,7 @@ class PalletApplicationServiceTest {
@Test
void pagesWithoutTenantFilter() {
PalletRequests.PageQuery query = new PalletRequests.PageQuery(1, 20, null, null, null);
when(repository.page(null, query)).thenReturn(new PageResult<>(java.util.List.of(pallet("AVAILABLE")),
when(repository.page(null, query)).thenReturn(new PageResult<>(java.util.List.of(pallet()),
1, 20, 1, 1));
org.assertj.core.api.Assertions.assertThat(service.page(null, query).records())
@@ -63,12 +63,29 @@ class PalletApplicationServiceTest {
.hasMessageContaining("仅支持国内仓");
}
private static PalletEntity pallet(String status) {
@Test
void returnsReusablePalletSnapshotsWithoutResourceStatus() {
WarehouseEntity warehouse = new WarehouseEntity();
warehouse.setWarehouseType("DOMESTIC");
when(warehouseRepository.findOwned(0L, 20L)).thenReturn(warehouse);
PalletEntity entity = pallet();
entity.setPalletCode("TP-001");
entity.setPalletType("STANDARD");
entity.setCapacityCbm(new BigDecimal("1.200000"));
entity.setWeightKg(new BigDecimal("8.000"));
when(repository.findAllByWarehouse(0L, 20L)).thenReturn(java.util.List.of(entity));
org.assertj.core.api.Assertions.assertThat(service.snapshots(0L, 20L)).singleElement().satisfies(snapshot -> {
org.assertj.core.api.Assertions.assertThat(snapshot.palletId()).isEqualTo(30L);
org.assertj.core.api.Assertions.assertThat(snapshot.tareWeightKg()).isEqualByComparingTo("8.000");
});
}
private static PalletEntity pallet() {
PalletEntity entity = new PalletEntity();
entity.setId(30L);
entity.setTenantId(10001L);
entity.setWarehouseId(20L);
entity.setStatus(status);
entity.setVersion(0);
return entity;
}
@@ -52,22 +52,35 @@ class PalletLocationAllocationServiceTest {
}
@Test
void shouldHandleCase01() {
void reusesOnePalletMasterForMultipleAllocations() {
when(locations.findActiveResources(10001, "FIRSTLEG", 88)).thenReturn(List.of());
when(pallets.findAvailable(10001, 20, "木托盘", new BigDecimal("2"), 1))
when(pallets.findReusable(10001, 20, "木托盘", new BigDecimal("2")))
.thenReturn(List.of(pallet(30, "TP-001")));
when(pallets.transitionStatus(10001, 20, 30, List.of("AVAILABLE"), "RESERVED")).thenReturn(0);
when(locations.reserveAvailable(any())).thenReturn(List.of(
new LocationDtos.ReservationResult(101, "LR101", 30, "TP-001", 40, "A-01", false,
LocalDateTime.now().plusMinutes(30)),
new LocationDtos.ReservationResult(102, "LR102", 30, "TP-001", 41, "A-02", false,
LocalDateTime.now().plusMinutes(30))));
assertThatThrownBy(() -> service.reserve(reserve(88))).isInstanceOf(BizException.class);
verify(locations, never()).reserveAvailable(any());
var request = new PalletAllocationDtos.ReserveRequest(10001, 20, "FIRSTLEG", 88, "RK-001", "measure-001",
9001, new BigDecimal("4"), new BigDecimal("2"), "木托盘", LocalDateTime.now().plusMinutes(30), true);
assertThat(service.reserve(request).allocations()).hasSize(2)
.extracting(PalletAllocationDtos.AllocationResult::palletId)
.containsOnly(30L);
org.mockito.ArgumentCaptor<LocationDtos.ReserveCommand> command =
org.mockito.ArgumentCaptor.forClass(LocationDtos.ReserveCommand.class);
verify(locations).reserveAvailable(command.capture());
assertThat(command.getValue().targets()).hasSize(2)
.extracting(LocationDtos.ReservationTarget::palletId)
.containsOnly(30L);
}
@Test
void shouldHandleCase02() {
when(locations.findActiveResources(10001, "FIRSTLEG", 99)).thenReturn(List.of());
when(pallets.findAvailable(10001, 20, "STANDARD", new BigDecimal("2"), 1))
when(pallets.findReusable(10001, 20, "STANDARD", new BigDecimal("2")))
.thenReturn(List.of(pallet(30, "TP-001")));
when(pallets.transitionStatus(10001, 20, 30, List.of("AVAILABLE"), "RESERVED")).thenReturn(1);
when(locations.reserveAvailable(any())).thenReturn(List.of(
new LocationDtos.ReservationResult(101, "LR101", 30, "TP-001", 40, "A0", true,
LocalDateTime.now().plusMinutes(30))));
@@ -77,7 +90,7 @@ class PalletLocationAllocationServiceTest {
var result = service.reserve(request);
assertThat(result.allocations().get(0).locationCode()).isEqualTo("A0");
verify(pallets).findAvailable(10001, 20, "STANDARD", new BigDecimal("2"), 1);
verify(pallets).findReusable(10001, 20, "STANDARD", new BigDecimal("2"));
}
@Test
@@ -86,13 +99,12 @@ class PalletLocationAllocationServiceTest {
101, null, 20, 30, "TP-001", 40, "A-01", false, "RESERVED");
when(locations.findActiveResources(10001, "FIRSTLEG", 88)).thenReturn(List.of(resource));
when(locations.release(any())).thenReturn(List.of(resource));
when(pallets.transitionStatus(10001, 20, 30, List.of("RESERVED"), "AVAILABLE")).thenReturn(1);
List<LocationDtos.ActiveResource> released = service.release(
new PalletAllocationDtos.ReleaseRequest(10001, "FIRSTLEG", 88, 9001, "入库补偿"));
assertThat(released).containsExactly(resource);
verify(pallets).transitionStatus(10001, 20, 30, List.of("RESERVED"), "AVAILABLE");
verifyNoInteractions(pallets);
}
@Test
@@ -117,7 +129,7 @@ class PalletLocationAllocationServiceTest {
assertThat(result.palletCount()).isEqualTo(1);
assertThat(result.allocations().get(0).state()).isEqualTo("RESERVED");
verify(pallets, never()).findAvailable(any(Long.class), any(Long.class), any(), any(), any(Integer.class));
verify(pallets, never()).findReusable(any(Long.class), any(Long.class), any(), any());
}
@Test
@@ -127,8 +139,6 @@ class PalletLocationAllocationServiceTest {
when(locations.findActiveResources(10001L, "FIRSTLEG", 88L)).thenReturn(List.of(resource));
when(locations.confirmOccupied(any())).thenReturn(List.of(new LocationDtos.OccupancyResult(
301L, "OC-301", 101L, 30L, 40L, "A-01", false)));
when(pallets.transitionStatus(10001L, 20L, 30L, List.of("RESERVED"), "OCCUPIED"))
.thenReturn(1);
service.confirm(new PalletAllocationDtos.ConfirmRequest(10001L, 20L, "FIRSTLEG", 88L,
9001L, new BigDecimal("1.0"), new BigDecimal("2.0"), 20));
@@ -153,8 +163,6 @@ class PalletLocationAllocationServiceTest {
"TP-" + index, locationId, "A-" + index, false, "RESERVED"));
occupancyResults.add(new LocationDtos.OccupancyResult(400L + index, "OC-" + index,
reservationId, palletId, locationId, "A-" + index, false));
when(pallets.transitionStatus(10001L, 20L, palletId, List.of("RESERVED"), "OCCUPIED"))
.thenReturn(1);
}
when(locations.findActiveResources(10001L, "FIRSTLEG", 89L)).thenReturn(resources);
when(locations.confirmOccupied(any())).thenReturn(occupancyResults);
@@ -198,8 +206,6 @@ class PalletLocationAllocationServiceTest {
});
when(releaseOperations.lockByKey(anyLong(), anyString(), anyLong(), anyString())).thenReturn(operation);
when(locations.releaseSelectedExactly(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, "pda-release-88-202", 9001L, "PDA partial pick", List.of(40L)));
@@ -209,7 +215,7 @@ class PalletLocationAllocationServiceTest {
assertThat(item.locationId()).isEqualTo(40L);
assertThat(item.allocatedPackageCount()).isEqualTo(4);
});
verify(pallets).transitionStatus(10001L, 20L, 30L, List.of("OCCUPIED"), "AVAILABLE");
verifyNoInteractions(pallets);
verify(releaseOperations).complete(org.mockito.ArgumentMatchers.eq(701L), anyString());
}
@@ -265,14 +271,12 @@ class PalletLocationAllocationServiceTest {
.thenReturn(operation);
when(locations.releaseSelectedReservations(any())).thenReturn(List.of(new LocationDtos.ExactSelectiveReleaseResult(
101L, 201L, 1, 20L, 30L, 40L, "A-01", 8)));
when(pallets.transitionStatus(0L, 20L, 30L, List.of("OCCUPIED"), "AVAILABLE")).thenReturn(1);
when(locations.transferToChildByReservation(any())).thenReturn(List.of(new LocationDtos.TransferResult(
102L, "LR102", 202L, 2, 20L, 31L, "TP-002", 41L, "A-02",
new BigDecimal("1.200000"), 12, "OCCUPIED")));
PalletEntity pallet = pallet(31L, "TP-002");
pallet.setStatus("OCCUPIED");
pallet.setCapacityCbm(new BigDecimal("1.500000"));
when(pallets.findOwned(0L, 20L, 31L)).thenReturn(pallet);
when(pallets.findOwnedIncludingDeleted(0L, 20L, 31L)).thenReturn(pallet);
var result = service.settle(settlementRequest());
@@ -407,9 +411,8 @@ class PalletLocationAllocationServiceTest {
101L, "LR101", 201L, 1, 20L, 30L, "TP-001", 40L, "A-01",
new BigDecimal("1.200000"), 5, "OCCUPIED")));
PalletEntity pallet = pallet(30L, "TP-001");
pallet.setStatus("OCCUPIED");
pallet.setCapacityCbm(new BigDecimal("1.500000"));
when(pallets.findOwned(10001L, 20L, 30L)).thenReturn(pallet);
when(pallets.findOwnedIncludingDeleted(10001L, 20L, 30L)).thenReturn(pallet);
var result = service.transfer(new PalletAllocationDtos.TransferRequest(
10001L, "FIRSTLEG", 88L, 89L, "RK-001-S1", "transfer-88-89", 9001L,
@@ -421,7 +424,6 @@ class PalletLocationAllocationServiceTest {
assertThat(item.remainingPackageCount()).isEqualTo(5);
assertThat(item.state()).isEqualTo("OCCUPIED");
});
verify(pallets, never()).transitionStatus(10001L, 20L, 30L, List.of("OCCUPIED"), "AVAILABLE");
verify(transferOperations).complete(org.mockito.ArgumentMatchers.eq(501L), anyString());
}