二期内容提交
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.warehouse.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformLocationControllerContractTest {
|
||||
@Test
|
||||
void 库位接口路径权限和Swagger说明完整() {
|
||||
assertThat(PlatformLocationController.class.getAnnotation(RequestMapping.class).value())
|
||||
.containsExactly("/api/platform/v1/locations");
|
||||
assertMethod("page", GetMapping.class, "platform:location:list");
|
||||
assertMethod("detail", GetMapping.class, "platform:location:list");
|
||||
assertMethod("available", GetMapping.class, "platform:location:list");
|
||||
assertMethod("create", PostMapping.class, "platform:location:create");
|
||||
assertMethod("update", PutMapping.class, "platform:location:edit");
|
||||
assertMethod("changeStatus", PatchMapping.class, "platform:location:status");
|
||||
assertMethod("delete", DeleteMapping.class, "platform:location:delete");
|
||||
}
|
||||
|
||||
private static void assertMethod(String name, Class<? extends Annotation> mapping, String permission) {
|
||||
Method method = java.util.Arrays.stream(PlatformLocationController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(name))
|
||||
.findFirst().orElseThrow();
|
||||
assertThat(method.getAnnotation(mapping)).isNotNull();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + permission + "')");
|
||||
assertThat(method.getAnnotation(Operation.class).summary()).isNotBlank();
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.baiyue.warehouse.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformPalletControllerContractTest {
|
||||
@Test
|
||||
void 托盘接口路径和按钮权限完整() {
|
||||
assertThat(PlatformPalletController.class.getAnnotation(RequestMapping.class).value())
|
||||
.containsExactly("/api/platform/v1/pallets");
|
||||
assertMethod("page", GetMapping.class, "platform:pallet:list");
|
||||
assertMethod("detail", GetMapping.class, "platform:pallet:list");
|
||||
assertMethod("create", PostMapping.class, "platform:pallet:create");
|
||||
assertMethod("update", PutMapping.class, "platform:pallet:edit");
|
||||
assertMethod("delete", DeleteMapping.class, "platform:pallet:delete");
|
||||
assertThat(Arrays.stream(PlatformPalletController.class.getDeclaredMethods())
|
||||
.noneMatch(method -> method.getName().equals("status"))).isTrue();
|
||||
}
|
||||
|
||||
private static void assertMethod(String name, Class<? extends Annotation> mapping, String authority) {
|
||||
Method method = Arrays.stream(PlatformPalletController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(name)).findFirst().orElseThrow();
|
||||
assertThat(method.getAnnotation(mapping)).isNotNull();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + authority + "')");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.baiyue.warehouse.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LocationMapperContractTest {
|
||||
@Test
|
||||
void 可用库位按库区和库位数字自然顺序临近分配() throws NoSuchMethodException {
|
||||
String selectSql = sql(LocationMapper.class.getMethod(
|
||||
"selectAvailable", long.class, long.class, int.class).getAnnotation(Select.class).value());
|
||||
String lockSql = sql(LocationMapper.class.getMethod(
|
||||
"lockAvailable", long.class, long.class, int.class).getAnnotation(Select.class).value());
|
||||
|
||||
assertNearbyOrder(selectSql);
|
||||
assertNearbyOrder(lockSql);
|
||||
assertThat(lockSql).contains("FOR UPDATE SKIP LOCKED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void A0依靠唯一编码并发创建后加锁复用() throws NoSuchMethodException {
|
||||
Method insert = LocationMapper.class.getMethod(
|
||||
"insertSharedOverflowIfAbsent", long.class, long.class, long.class, long.class);
|
||||
Method lock = LocationMapper.class.getMethod("lockSharedOverflow", long.class, long.class);
|
||||
|
||||
assertThat(sql(insert.getAnnotation(Insert.class).value()))
|
||||
.contains("location_code, location_name")
|
||||
.contains("'A0'")
|
||||
.contains("shared_overflow")
|
||||
.contains("ON DUPLICATE KEY UPDATE id = id");
|
||||
assertThat(sql(lock.getAnnotation(Select.class).value()))
|
||||
.contains("tenant_id = #{tenantId}")
|
||||
.contains("warehouse_id = #{warehouseId}")
|
||||
.contains("location_code = 'A0'")
|
||||
.contains("FOR UPDATE");
|
||||
}
|
||||
|
||||
private static void assertNearbyOrder(String sql) {
|
||||
assertThat(sql).contains("shared_overflow = 0")
|
||||
.contains("ORDER BY l.zone_code")
|
||||
.contains("REGEXP_REPLACE(l.location_code, '[0-9]+$', '')")
|
||||
.contains("REGEXP_SUBSTR(l.location_code, '[0-9]+$')")
|
||||
.contains("AS UNSIGNED");
|
||||
}
|
||||
|
||||
private static String sql(String[] fragments) {
|
||||
return String.join(" ", fragments);
|
||||
}
|
||||
}
|
||||
+37
@@ -40,6 +40,43 @@ class WarehouseSchemaContractTest {
|
||||
assertBaseColumns(sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 二期库位共用表覆盖基础资料预占占用和并发约束() throws IOException {
|
||||
String sql = read("phase2-pallet-location-schema.sql");
|
||||
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");
|
||||
assertColumns(sql, "wh_location_occupancy", "reservation_id", "business_id", "pallet_id",
|
||||
"location_id", "shared_overflow", "occupancy_status",
|
||||
"active_location_occupancy", "active_pallet_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");
|
||||
}
|
||||
|
||||
@Test
|
||||
void A0迁移允许共享库位多占用但仍限制托盘唯一活动占用() throws IOException {
|
||||
String sql = read("phase2-location-overflow-migration.sql");
|
||||
assertThat(sql).contains("'wh_location', 'shared_overflow'")
|
||||
.contains("'wh_location_reservation', 'shared_overflow'")
|
||||
.contains("'wh_location_occupancy', 'shared_overflow'")
|
||||
.contains("reservation_status = 'RESERVED' AND shared_overflow = 0")
|
||||
.contains("occupancy_status = ''OCCUPIED'' AND shared_overflow = 0")
|
||||
.contains("occupancy_status = ''OCCUPIED'' THEN 1")
|
||||
.contains("active_location_occupancy")
|
||||
.contains("active_pallet_occupancy")
|
||||
.contains("active_pallet_reservation")
|
||||
.contains("uk_pallet_active_reservation (tenant_id, pallet_id, active_pallet_reservation)")
|
||||
.contains("uk_location_active_occupancy (tenant_id, location_id, active_location_occupancy)")
|
||||
.contains("uk_pallet_active_occupancy (tenant_id, pallet_id, active_pallet_occupancy)")
|
||||
.contains("FROM information_schema.columns")
|
||||
.doesNotContain("ADD COLUMN IF NOT EXISTS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 既有库增量迁移覆盖全部仓储实体表和列表索引() throws IOException {
|
||||
String sql = read("phase1-warehouse-schema-migration.sql");
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.LocationDtos.SaveRequest;
|
||||
import com.baiyue.warehouse.entity.LocationEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
import com.baiyue.warehouse.repository.LocationRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
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 LocationApplicationServiceTest {
|
||||
@Test
|
||||
void 新增时仓库必须属于当前企业租户() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
LocationApplicationService service = new LocationApplicationService(repository);
|
||||
|
||||
assertThatThrownBy(() -> service.create(10001L, request("AVAILABLE", null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("企业租户");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 同一租户仓库的库位编码必须唯一() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse("ACTIVE"));
|
||||
when(repository.existsCode(10001L, 88L, "A1-01", null)).thenReturn(true);
|
||||
LocationApplicationService service = new LocationApplicationService(repository);
|
||||
|
||||
assertThatThrownBy(() -> service.create(10001L, request("AVAILABLE", null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("编码");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 活动预占或占用库位不能停用() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
LocationEntity location = location("AVAILABLE", 3);
|
||||
when(repository.findOwned(10001L, 88L, 9L)).thenReturn(location);
|
||||
when(repository.hasActiveResource(10001L, 9L)).thenReturn(true);
|
||||
LocationApplicationService service = new LocationApplicationService(repository);
|
||||
|
||||
assertThatThrownBy(() -> service.changeStatus(10001L, 88L, 9L, "DISABLED", 3, 7L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不能停用");
|
||||
verify(repository, never()).changeStatus(10001L, 88L, 9L, "AVAILABLE", "DISABLED", 3, 7L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 删除前必须先停用且没有活动资源() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
when(repository.findOwned(10001L, 88L, 9L)).thenReturn(location("AVAILABLE", 3));
|
||||
LocationApplicationService service = new LocationApplicationService(repository);
|
||||
|
||||
assertThatThrownBy(() -> service.delete(10001L, 88L, 9L, 3, 7L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("先停用");
|
||||
verify(repository, never()).deleteDisabled(10001L, 88L, 9L, 3, 7L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 条件更新失败统一转换为乐观锁冲突() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
when(repository.findOwned(10001L, 88L, 9L)).thenReturn(location("DISABLED", 3));
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse("ACTIVE"));
|
||||
when(repository.changeStatus(10001L, 88L, 9L, "DISABLED", "AVAILABLE", 3, 7L)).thenReturn(0);
|
||||
LocationApplicationService service = new LocationApplicationService(repository);
|
||||
|
||||
assertThatThrownBy(() -> service.changeStatus(10001L, 88L, 9L, "AVAILABLE", 3, 7L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("刷新");
|
||||
}
|
||||
|
||||
private static SaveRequest request(String status, Integer version) {
|
||||
return new SaveRequest(88L, "NORMAL", "常规货", "A1-01", "A1 区 01 位",
|
||||
"PALLET", new BigDecimal("500"), status, "备注", version);
|
||||
}
|
||||
|
||||
private static WarehouseEntity warehouse(String status) {
|
||||
WarehouseEntity warehouse = new WarehouseEntity();
|
||||
warehouse.setId(88L);
|
||||
warehouse.setTenantId(10001L);
|
||||
warehouse.setStatus(status);
|
||||
return warehouse;
|
||||
}
|
||||
|
||||
private static LocationEntity location(String status, int version) {
|
||||
LocationEntity location = new LocationEntity();
|
||||
location.setId(9L);
|
||||
location.setTenantId(10001L);
|
||||
location.setWarehouseId(88L);
|
||||
location.setStatus(status);
|
||||
location.setVersion(version);
|
||||
return location;
|
||||
}
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
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.entity.LocationEntity;
|
||||
import com.baiyue.warehouse.entity.LocationOccupancyEntity;
|
||||
import com.baiyue.warehouse.entity.LocationReservationEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
import com.baiyue.warehouse.repository.LocationRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
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.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class LocationResourceServiceTest {
|
||||
@Test
|
||||
void 普通库位不足时优先分配普通位并将剩余托盘共享到A0() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
when(repository.lockAvailable(10001L, 88L, 2)).thenReturn(List.of(location(9L, "A1")));
|
||||
when(repository.lockOrCreateSharedOverflow(10001L, 88L, 7L)).thenReturn(overflow());
|
||||
when(repository.changeStatus(10001L, 88L, 9L, "AVAILABLE", "RESERVED", 0, 7L)).thenReturn(1);
|
||||
LocationResourceService service = new LocationResourceService(repository);
|
||||
|
||||
var result = service.reserveAvailable(reserveCommand());
|
||||
|
||||
assertThat(result).extracting(item -> item.locationCode()).containsExactly("A1", "A0");
|
||||
assertThat(result).extracting(item -> item.sharedOverflow()).containsExactly(false, true);
|
||||
verify(repository).changeStatus(10001L, 88L, 9L, "AVAILABLE", "RESERVED", 0, 7L);
|
||||
verify(repository, never()).changeStatus(10001L, 88L, 99L, "AVAILABLE", "RESERVED", 0, 7L);
|
||||
verify(repository, times(2)).insertReservation(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 无普通库位时同一请求的所有不足目标复用同一个A0() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
when(repository.lockAvailable(10001L, 88L, 2)).thenReturn(List.of());
|
||||
when(repository.lockOrCreateSharedOverflow(10001L, 88L, 7L)).thenReturn(overflow());
|
||||
LocationResourceService service = new LocationResourceService(repository);
|
||||
|
||||
var result = service.reserveAvailable(reserveCommand());
|
||||
|
||||
assertThat(result).extracting(item -> item.locationId()).containsExactly(99L, 99L);
|
||||
assertThat(result).extracting(item -> item.locationCode()).containsExactly("A0", "A0");
|
||||
verify(repository, never()).changeStatus(anyLong(), anyLong(), anyLong(),
|
||||
any(String.class), any(String.class), anyInt(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 单事务按托盘顺序锁定并条件预占库位() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
when(repository.lockAvailable(10001L, 88L, 2))
|
||||
.thenReturn(List.of(location(9L, "A1"), location(10L, "A2")));
|
||||
when(repository.changeStatus(10001L, 88L, 9L, "AVAILABLE", "RESERVED", 0, 7L)).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, 10L, "AVAILABLE", "RESERVED", 0, 7L)).thenReturn(1);
|
||||
LocationResourceService service = new LocationResourceService(repository);
|
||||
|
||||
var result = service.reserveAvailable(reserveCommand());
|
||||
|
||||
assertThat(result).extracting(item -> item.locationCode()).containsExactly("A1", "A2");
|
||||
ArgumentCaptor<LocationReservationEntity> captor = ArgumentCaptor.forClass(LocationReservationEntity.class);
|
||||
verify(repository, org.mockito.Mockito.times(2)).insertReservation(captor.capture());
|
||||
assertThat(captor.getAllValues()).extracting(LocationReservationEntity::getIdempotencyKey)
|
||||
.containsExactly("inbound-1-p1", "inbound-1-p2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 已确认预占按占用事实幂等返回() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
LocationReservationEntity reservation = reservation("CONFIRMED");
|
||||
when(repository.lockReservation(10001L, 101L)).thenReturn(reservation);
|
||||
LocationOccupancyEntity occupancy = new LocationOccupancyEntity();
|
||||
occupancy.setId(201L);
|
||||
occupancy.setOccupancyNo("LO201");
|
||||
occupancy.setReservationId(101L);
|
||||
occupancy.setPalletId(501L);
|
||||
occupancy.setLocationId(9L);
|
||||
occupancy.setLocationCode("A1");
|
||||
when(repository.findOccupancyByReservation(10001L, 101L)).thenReturn(occupancy);
|
||||
LocationResourceService service = new LocationResourceService(repository);
|
||||
|
||||
var result = service.confirmOccupied(confirmCommand());
|
||||
|
||||
assertThat(result).singleElement().extracting(item -> item.occupancyId()).isEqualTo(201L);
|
||||
verify(repository, never()).insertOccupancy(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 确认占用使用预占归属和库位版本条件更新() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
LocationReservationEntity reservation = reservation("RESERVED");
|
||||
when(repository.lockReservation(10001L, 101L)).thenReturn(reservation);
|
||||
when(repository.findOwned(10001L, 88L, 9L)).thenReturn(location(9L, "A1"));
|
||||
when(repository.changeStatus(10001L, 88L, 9L, "RESERVED", "OCCUPIED", 0, 7L)).thenReturn(1);
|
||||
when(repository.confirmReservation(reservation)).thenReturn(1);
|
||||
LocationResourceService service = new LocationResourceService(repository);
|
||||
|
||||
var result = service.confirmOccupied(confirmCommand());
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
verify(repository).insertOccupancy(any(LocationOccupancyEntity.class));
|
||||
verify(repository).confirmReservation(reservation);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 确认A0占用时保持共享库位AVAILABLE() {
|
||||
LocationRepository repository = mock(LocationRepository.class);
|
||||
when(repository.findWarehouse(10001L, 88L)).thenReturn(warehouse());
|
||||
LocationReservationEntity reservation = reservation("RESERVED");
|
||||
reservation.setLocationId(99L);
|
||||
reservation.setLocationCode("A0");
|
||||
reservation.setSharedOverflow(true);
|
||||
when(repository.lockReservation(10001L, 101L)).thenReturn(reservation);
|
||||
when(repository.findOwned(10001L, 88L, 99L)).thenReturn(overflow());
|
||||
when(repository.confirmReservation(reservation)).thenReturn(1);
|
||||
LocationResourceService service = new LocationResourceService(repository);
|
||||
|
||||
var result = service.confirmOccupied(confirmCommand());
|
||||
|
||||
assertThat(result).singleElement().satisfies(item -> {
|
||||
assertThat(item.locationCode()).isEqualTo("A0");
|
||||
assertThat(item.sharedOverflow()).isTrue();
|
||||
});
|
||||
verify(repository, never()).changeStatus(anyLong(), anyLong(), anyLong(),
|
||||
any(String.class), any(String.class), anyInt(), anyLong());
|
||||
verify(repository).insertOccupancy(any(LocationOccupancyEntity.class));
|
||||
}
|
||||
|
||||
private static ReserveCommand reserveCommand() {
|
||||
return new ReserveCommand(10001L, 88L, "FIRSTLEG", 301L, "RK001", 7L,
|
||||
LocalDateTime.now().plusMinutes(30), List.of(
|
||||
new ReservationTarget(501L, "P1", "inbound-1-p1"),
|
||||
new ReservationTarget(502L, "P2", "inbound-1-p2")));
|
||||
}
|
||||
|
||||
private static ConfirmCommand confirmCommand() {
|
||||
return new ConfirmCommand(10001L, 88L, "FIRSTLEG", 301L, 7L,
|
||||
List.of(new OccupancyTarget(101L, new BigDecimal("1.200000"), 10)));
|
||||
}
|
||||
|
||||
private static WarehouseEntity warehouse() {
|
||||
WarehouseEntity warehouse = new WarehouseEntity();
|
||||
warehouse.setId(88L);
|
||||
warehouse.setTenantId(10001L);
|
||||
warehouse.setStatus("ACTIVE");
|
||||
return warehouse;
|
||||
}
|
||||
|
||||
private static LocationEntity location(long id, String code) {
|
||||
LocationEntity location = new LocationEntity();
|
||||
location.setId(id);
|
||||
location.setTenantId(10001L);
|
||||
location.setWarehouseId(88L);
|
||||
location.setLocationCode(code);
|
||||
location.setStatus("AVAILABLE");
|
||||
location.setSharedOverflow(false);
|
||||
location.setVersion(0);
|
||||
return location;
|
||||
}
|
||||
|
||||
private static LocationEntity overflow() {
|
||||
LocationEntity location = location(99L, "A0");
|
||||
location.setSharedOverflow(true);
|
||||
return location;
|
||||
}
|
||||
|
||||
private static LocationReservationEntity reservation(String status) {
|
||||
LocationReservationEntity reservation = new LocationReservationEntity();
|
||||
reservation.setId(101L);
|
||||
reservation.setTenantId(10001L);
|
||||
reservation.setWarehouseId(88L);
|
||||
reservation.setSourceService("FIRSTLEG");
|
||||
reservation.setBusinessId(301L);
|
||||
reservation.setInboundCode("RK001");
|
||||
reservation.setPalletId(501L);
|
||||
reservation.setPalletCode("P1");
|
||||
reservation.setLocationId(9L);
|
||||
reservation.setLocationCode("A1");
|
||||
reservation.setReservationStatus(status);
|
||||
reservation.setExpiresAt(LocalDateTime.now().plusMinutes(10));
|
||||
reservation.setVersion(0);
|
||||
return reservation;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.entity.PalletEntity;
|
||||
import com.baiyue.warehouse.repository.PalletRepository;
|
||||
import com.baiyue.warehouse.repository.WarehouseRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PalletApplicationServiceTest {
|
||||
private final PalletRepository repository = mock(PalletRepository.class);
|
||||
private final WarehouseRepository warehouseRepository = mock(WarehouseRepository.class);
|
||||
private final PalletApplicationService service = new PalletApplicationService(repository, warehouseRepository);
|
||||
|
||||
@Test
|
||||
void 平台详情不暴露内部状态() {
|
||||
PalletEntity entity = pallet("OCCUPIED");
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 跨仓库托盘不可见() {
|
||||
assertThatThrownBy(() -> service.detail(10001, 21, 30)).isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
private static PalletEntity pallet(String status) {
|
||||
PalletEntity entity = new PalletEntity();
|
||||
entity.setId(30L);
|
||||
entity.setTenantId(10001L);
|
||||
entity.setWarehouseId(20L);
|
||||
entity.setStatus(status);
|
||||
entity.setVersion(0);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.LocationDtos;
|
||||
import com.baiyue.warehouse.dto.PalletAllocationDtos;
|
||||
import com.baiyue.warehouse.entity.PalletEntity;
|
||||
import com.baiyue.warehouse.repository.PalletRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
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.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PalletLocationAllocationServiceTest {
|
||||
private final PalletRepository pallets = mock(PalletRepository.class);
|
||||
private final LocationResourceService locations = mock(LocationResourceService.class);
|
||||
private final PalletLocationAllocationService service =
|
||||
new PalletLocationAllocationService(pallets, new PalletVolumeSplitter(), locations);
|
||||
|
||||
@Test
|
||||
void 并发抢占托盘时整批失败且不预占库位() {
|
||||
when(locations.findActiveResources(10001, "FIRSTLEG", 88)).thenReturn(List.of());
|
||||
when(pallets.findAvailable(10001, 20, "木托盘", new BigDecimal("2"), 1))
|
||||
.thenReturn(List.of(pallet(30, "TP-001")));
|
||||
when(pallets.transitionStatus(10001, 20, 30, List.of("AVAILABLE"), "RESERVED")).thenReturn(0);
|
||||
|
||||
assertThatThrownBy(() -> service.reserve(reserve(88))).isInstanceOf(BizException.class);
|
||||
verify(locations, never()).reserveAvailable(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 未指定类型时默认推荐普通托盘() {
|
||||
when(locations.findActiveResources(10001, "FIRSTLEG", 99)).thenReturn(List.of());
|
||||
when(pallets.findAvailable(10001, 20, "STANDARD", new BigDecimal("2"), 1))
|
||||
.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))));
|
||||
|
||||
var request = new PalletAllocationDtos.ReserveRequest(10001, 20, "FIRSTLEG", 99, "RK-001", "measure-99",
|
||||
9001, new BigDecimal("2"), new BigDecimal("2"), null, LocalDateTime.now().plusMinutes(30), true);
|
||||
var result = service.reserve(request);
|
||||
|
||||
assertThat(result.allocations().get(0).locationCode()).isEqualTo("A0");
|
||||
verify(pallets).findAvailable(10001, 20, "STANDARD", new BigDecimal("2"), 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 释放预占后托盘恢复可用并可再次预占() {
|
||||
LocationDtos.ActiveResource resource = new LocationDtos.ActiveResource(
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 已确认占用禁止补偿释放避免回滚入库事实() {
|
||||
LocationDtos.ActiveResource resource = new LocationDtos.ActiveResource(
|
||||
101, 201L, 20, 30, "TP-001", 40, "A-01", false, "OCCUPIED");
|
||||
when(locations.findActiveResources(10001, "FIRSTLEG", 88)).thenReturn(List.of(resource));
|
||||
|
||||
assertThatThrownBy(() -> service.release(
|
||||
new PalletAllocationDtos.ReleaseRequest(10001, "FIRSTLEG", 88, 9001, "错误补偿")))
|
||||
.isInstanceOf(BizException.class);
|
||||
verify(locations, never()).release(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 相同业务幂等重试直接返回已有预占() {
|
||||
LocationDtos.ActiveResource resource = new LocationDtos.ActiveResource(
|
||||
101, null, 20, 30, "TP-001", 40, "A-01", false, "RESERVED");
|
||||
when(locations.findActiveResources(10001, "FIRSTLEG", 88)).thenReturn(List.of(resource));
|
||||
|
||||
var result = service.reserve(reserve(88));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static PalletEntity pallet(long id, String code) {
|
||||
PalletEntity entity = new PalletEntity();
|
||||
entity.setId(id);
|
||||
entity.setPalletCode(code);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class PalletVolumeSplitterTest {
|
||||
private final PalletVolumeSplitter splitter = new PalletVolumeSplitter();
|
||||
|
||||
@Test
|
||||
void 整除时不多拆托盘() {
|
||||
var result = splitter.split(new BigDecimal("20"), new BigDecimal("2"));
|
||||
assertThat(result.palletCount()).isEqualTo(10);
|
||||
assertThat(result.allocations()).allMatch(item -> item.allocatedVolumeCbm().compareTo(new BigDecimal("2")) == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 非整除时向上拆分并保留尾盘体积() {
|
||||
var result = splitter.split(new BigDecimal("20.1"), new BigDecimal("2"));
|
||||
assertThat(result.palletCount()).isEqualTo(11);
|
||||
assertThat(result.allocations().get(10).allocatedVolumeCbm()).isEqualByComparingTo("0.1");
|
||||
assertThat(result.allocations()).extracting(PalletVolumeSplitter.Allocation::allocationNo)
|
||||
.containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 拒绝空值零值负值和超大拆分() {
|
||||
assertThatThrownBy(() -> splitter.split(null, BigDecimal.ONE)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> splitter.split(BigDecimal.ZERO, BigDecimal.ONE)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> splitter.split(BigDecimal.ONE, BigDecimal.ZERO)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> splitter.split(new BigDecimal("10001"), BigDecimal.ONE)).isInstanceOf(BizException.class);
|
||||
}
|
||||
}
|
||||
+3
@@ -22,6 +22,9 @@ class WarehouseSchemaReadinessServiceTest {
|
||||
assertThat(readiness.ready()).isTrue();
|
||||
assertThat(readiness.status()).isEqualTo("UP");
|
||||
verify(jdbcTemplate).execute(org.mockito.ArgumentMatchers.contains("wh_express_company"));
|
||||
verify(jdbcTemplate).execute(org.mockito.ArgumentMatchers.contains("wh_location_reservation"));
|
||||
verify(jdbcTemplate).execute(org.mockito.ArgumentMatchers.contains("wh_location_occupancy"));
|
||||
verify(jdbcTemplate).execute(org.mockito.ArgumentMatchers.contains("active_location_occupancy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user