This commit is contained in:
2026-09-04 10:51:25 +08:00
parent 4ca5a57e6a
commit 7a0e158b51
13 changed files with 561 additions and 30 deletions
@@ -56,6 +56,7 @@ class PlatformSmartLoadingControllerContractTest {
authority("adjust","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,long.class,AdjustRequest.class);
authority("warehouseRemark","platform:firstleg:preload:edit",AuthenticatedUser.class,long.class,long.class,WarehouseRemarkRequest.class);
authority("delete","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,long.class,DeleteRequest.class);
authority("removeUnpicked","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,RemoveUnpickedRequest.class);
}
@Test void openApiHasNoManualCreateOrPushStep() throws Exception {
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
@@ -75,6 +76,7 @@ class PlatformSmartLoadingControllerContractTest {
.contains("SmartLoadingAssignRequest:")
.contains("SmartLoadingAdjustRequest:")
.contains("SmartLoadingDeleteRequest:")
.contains("SmartLoadingRemoveUnpickedRequest:")
.contains("SmartLoadingWarehouseRemarkRequest:")
.contains("/items/{itemId}/warehouse-remark:")
.contains("需要 platform:firstleg:preload:edit 权限")
@@ -83,6 +85,8 @@ class PlatformSmartLoadingControllerContractTest {
.contains("items[].version 是递增后的 itemVersion")
.contains("数量调整或动态增量不会覆盖已有 warehouseRemark")
.contains("柜内总容积和总重量分别允许达到柜型额定值的150%")
.contains("人工分配不限制订单渠道与柜次渠道一致")
.contains("渠道只约束智能推荐")
.contains("canAssign: {type: boolean}")
.contains("canEdit: {type: boolean}")
.contains("pickedPackageCount: {type: integer, minimum: 0}")
@@ -332,16 +332,16 @@ class SmartLoadingServiceTest {
}
@Test
void assignsPendingReviewOrderAsAProvisionalContainerAllocation() {
void manuallyAssignsCrossChannelPendingReviewOrderAsAProvisionalContainerAllocation() {
long customerId = 2092797167699816450L;
long orderId = 2095052002489335810L;
String inboundCode = "EXXHK2609020361";
var pending = new PendingReviewOrderClient.Row(orderId, "FL-361", inboundCode,
customerId, "客户", 100L, "FBA", "FBA渠道", 5,
customerId, "客户", 100L, "SLOW", "慢船", 5,
LocalDateTime.parse("2026-09-02T15:31:36"), "PENDING_REVIEW");
var snapshot = new com.baiyue.firstleg.dto.OrderApprovedInboundCommand(
customerId, orderId, "FL-361", inboundCode, customerId, "客户", 100L, "义乌仓",
null, null, null, "FIRST_LEG", "FBA", "FBA渠道", null, null, null,
null, null, null, "FIRST_LEG", "SLOW", "慢船", null, null, null,
null, null, false, "FBA", 200L, "FBA1", "FBA仓", null, null, null, 5);
var label = new OrderCargoClient.CargoLabelBinding(20L, orderId, 1, "货物", 5,
null, null, null);
@@ -381,7 +381,7 @@ class SmartLoadingServiceTest {
}
@Test
void assignsWaitMeasureInboundUsingDeclaredFactsWithoutTreatingStatusAsAGate() {
void manuallyAssignsCrossChannelWaitMeasureInboundUsingDeclaredFacts() {
long customerId = 9L;
long inboundId = 10L;
long orderId = 30L;
@@ -391,11 +391,11 @@ class SmartLoadingServiceTest {
projection.setOrderId(orderId);
projection.setInboundCode("I10");
projection.setWarehouseId(100L);
projection.setChannelCode("FBA");
projection.setChannelCode("SLOW");
projection.setProcessStatus("WAIT_MEASURE");
var snapshot = new com.baiyue.firstleg.dto.OrderApprovedInboundCommand(
customerId, orderId, "O30", "I10", customerId, "客户", 100L, "义乌仓",
null, null, null, "FIRST_LEG", "FBA", "FBA渠道", null, null, null,
null, null, null, "FIRST_LEG", "SLOW", "慢船", null, null, null,
null, null, false, "FBA", 200L, "FBA1", "FBA仓", null, null, null, 5);
var label = new OrderCargoClient.CargoLabelBinding(20L, orderId, 1, "货物", 5,
null, null, null);
@@ -575,8 +575,8 @@ class SmartLoadingServiceTest {
@Test
void atomicallyAssignsEveryInboundFragmentOfSameOrderAcrossDifferentDestinations() {
Candidate first = candidate(9L, 10L, 20L, 30L, 4);
Candidate second = candidate(9L, 11L, 21L, 30L, 6, "FBA2");
Candidate first = withChannel(candidate(9L, 10L, 20L, 30L, 4), "SLOW", "慢船");
Candidate second = withChannel(candidate(9L, 11L, 21L, 30L, 6, "FBA2"), "SLOW", "慢船");
GroupSnapshot active = group("ACTIVE", null,
new Member(9L, 30L, 10L, "IN-10", 4), new Member(9L, 30L, 11L, "IN-11", 6));
GroupSnapshot locked = group("LOCKED", 1L,
@@ -796,6 +796,94 @@ class SmartLoadingServiceTest {
org.mockito.ArgumentMatchers.startsWith("UNLOCK-"), eq(7L), eq("u"));
}
@Test
void removesAllUnpickedItemsWithOnePlanVersionChange() {
LoadingPlanEntity initial = plan("EDITING", 2);
LoadingPlanEntity after = plan("EDITING", 3);
LoadingPlanItemEntity first = item(8L, 9L, 20L, 4);
LoadingPlanItemEntity second = item(9L, 19L, 21L, 6);
second.setInboundId(11L);
when(repository.plan(1L)).thenReturn(initial, after);
when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenReturn(List.of(first, second), List.of());
when(repository.touch(1L, 2, 7L, "u")).thenReturn(1);
when(repository.remove(eq(1L), anyLong(), eq(0), eq(7L), eq("u"))).thenReturn(1);
Plan result = service.removeUnpicked(1L,
new com.baiyue.firstleg.dto.SmartLoadingDtos.RemoveUnpickedRequest(2, "remove-unpicked"),
7L, "u");
assertThat(result.items()).isEmpty();
verify(repository).touch(1L, 2, 7L, "u");
verify(repository, times(2)).remove(eq(1L), anyLong(), eq(0), eq(7L), eq("u"));
verify(loadingPickService, times(2)).remove(anyLong());
}
@Test
void bulkRemovalKeepsWholeGroupWhenAnyMemberWasPicked() {
LoadingPlanEntity initial = plan("LOADING", 2);
LoadingPlanEntity after = plan("LOADING", 3);
LoadingPlanItemEntity first = item(8L, 9L, 20L, 4);
LoadingPlanItemEntity second = item(9L, 19L, 21L, 6);
second.setInboundId(11L);
LoadingPlanItemEntity standalone = item(10L, 29L, 22L, 3);
standalone.setInboundId(12L);
GroupSnapshot group = group("LOCKED", 1L,
new Member(9L, 30L, 10L, "IN-10", 4), new Member(19L, 31L, 11L, "IN-11", 6));
when(repository.plan(1L)).thenReturn(initial, after);
when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenReturn(List.of(first, second, standalone), List.of(first, second));
when(repository.touch(1L, 2, 7L, "u")).thenReturn(1);
when(repository.remove(1L, 10L, 0, 7L, "u")).thenReturn(1);
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
when(coloadGroups.findByInbound(19L, 11L)).thenReturn(group);
when(loadingPickService.mutationSnapshot(1L)).thenReturn(
new LoadingPickService.MutationSnapshot(77L, "LOADING", 5, Map.of(8L, 1)));
Plan result = service.removeUnpicked(1L,
new com.baiyue.firstleg.dto.SmartLoadingDtos.RemoveUnpickedRequest(2, "keep-picked-group"),
7L, "u");
assertThat(result.items()).extracting(LoadingPlanItemEntity::getId).containsExactly(8L, 9L);
verify(repository).remove(1L, 10L, 0, 7L, "u");
verify(repository, never()).remove(eq(1L), eq(8L), anyInt(), anyLong(), anyString());
verify(repository, never()).remove(eq(1L), eq(9L), anyInt(), anyLong(), anyString());
verify(coloadGroups, never()).unlockWholeGroupAfterRemoval(anyLong(), anyLong(), anyString(), anyLong(), anyString());
}
@Test
void bulkRemovalRemovesAndUnlocksACompletelyUnpickedGroupOnce() {
LoadingPlanEntity initial = plan("EDITING", 2);
LoadingPlanEntity after = plan("EDITING", 3);
LoadingPlanItemEntity first = item(8L, 9L, 20L, 4);
LoadingPlanItemEntity second = item(9L, 19L, 21L, 6);
second.setInboundId(11L);
GroupSnapshot group = group("LOCKED", 1L,
new Member(9L, 30L, 10L, "IN-10", 4), new Member(19L, 31L, 11L, "IN-11", 6));
when(repository.plan(1L)).thenReturn(initial, after);
when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenReturn(List.of(first, second), List.of());
when(repository.activeItemsByInbound(9L, 10L)).thenReturn(List.of(first));
when(repository.activeItemsByInbound(19L, 11L)).thenReturn(List.of(second));
when(repository.touch(1L, 2, 7L, "u")).thenReturn(1);
when(repository.remove(eq(1L), anyLong(), eq(0), eq(7L), eq("u"))).thenReturn(1);
when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group);
when(coloadGroups.findByInbound(19L, 11L)).thenReturn(group);
GroupSnapshot unlocked = group("ACTIVE", null,
new Member(9L, 30L, 10L, "IN-10", 4), new Member(19L, 31L, 11L, "IN-11", 6));
when(coloadGroups.unlockWholeGroupAfterRemoval(eq(700L), eq(1L), anyString(), eq(7L), eq("u")))
.thenReturn(unlocked);
Plan result = service.removeUnpicked(1L,
new com.baiyue.firstleg.dto.SmartLoadingDtos.RemoveUnpickedRequest(2, "remove-unpicked-group"),
7L, "u");
assertThat(result.items()).isEmpty();
verify(repository, times(2)).remove(eq(1L), anyLong(), eq(0), eq(7L), eq("u"));
verify(coloadGroups).unlockWholeGroupAfterRemoval(eq(700L), eq(1L),
org.mockito.ArgumentMatchers.startsWith("UNLOCK-"), eq(7L), eq("u"));
}
@Test
void proportionallyDistributesInboundQuantityWithoutLosingCartons() {
Candidate first = new Candidate(9L, 10L, 20L, 30L, "O", "I", 100L,