From 0cf76df16897c40df2e425b56743bc0e34d420f9 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 29 Aug 2026 12:36:07 +0800 Subject: [PATCH] fix(firstleg): align coload loading route gates --- .../firstleg/service/SmartLoadingService.java | 16 ++++--- .../service/LoadingPickServiceTest.java | 17 +++++-- .../service/SmartLoadingServiceTest.java | 47 +++++++++++++------ 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java index 2789d9563..fd3d7bf6b 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/SmartLoadingService.java @@ -165,7 +165,7 @@ public class SmartLoadingService { if (total > availableIncludingCurrent) throw validation("累计分配箱数超过可用箱数"); List lines = new ArrayList<>(repository.items(planId)); if (existing != null) lines.removeIf(x -> Objects.equals(x.getId(), existing.getId())); - checkRouteAndCapacity(booking, lines, candidate, total); + checkRouteAndCapacity(booking, lines, candidate, total, !wholeGroupAssignment); touch(plan, operatorId, operatorName); LoadingPlanItemEntity changed; if (existing == null) { @@ -364,8 +364,6 @@ public class SmartLoadingService { ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId()); Set visitedGroups = new HashSet<>(); for (InboundCandidate candidate : inboundCandidates(plan.getContainerBookingId(), booking.getWarehouseId())) { - if (StringUtils.hasText(booking.getDestinationWarehouseCode()) - && !Objects.equals(booking.getDestinationWarehouseCode(), candidate.destinationWarehouseCode())) continue; if (candidate.coLoadGroupId() != null) { if (!visitedGroups.add(candidate.coLoadGroupId()) || StringUtils.hasText(candidate.blockReason())) { continue; @@ -376,6 +374,8 @@ public class SmartLoadingService { operatorId, operatorName); continue; } + if (StringUtils.hasText(booking.getDestinationWarehouseCode()) + && !Objects.equals(booking.getDestinationWarehouseCode(), candidate.destinationWarehouseCode())) continue; int count = fittingBoxes(booking, repository.items(planId), candidate.availablePackageCount(), candidate.averageUnitVolumeCbm(), candidate.unitGrossWeightKg()); if (count > 0) { @@ -423,7 +423,7 @@ public class SmartLoadingService { OrderCargoClient.CargoBinding cargo = loadCargo(candidate); List others = new ArrayList<>(repository.items(planId)); others.removeIf(x -> Objects.equals(x.getId(), itemId)); - checkRouteAndCapacity(booking, others, candidate, request.packageCount()); + checkRouteAndCapacity(booking, others, candidate, request.packageCount(), true); String before = json(adjustSnapshot(item)); LoadingPlanItemEntity desired = new LoadingPlanItemEntity(); desired.setPackageCount(request.packageCount()); @@ -808,10 +808,14 @@ public class SmartLoadingService { throw unavailable(); } } - private void checkRouteAndCapacity(ContainerBookingEntity b,List lines,Candidate c,int count){ + private void checkRouteAndCapacity(ContainerBookingEntity b,List lines,Candidate c, + int count, boolean requireSameDestination){ requireSameChannel(b, c); if (!Objects.equals(b.getWarehouseId(), c.warehouseId())) throw validation("装柜仓库不一致"); - if (StringUtils.hasText(b.getDestinationWarehouseCode()) && !Objects.equals(b.getDestinationWarehouseCode(), c.destinationWarehouseCode())) throw validation("目的仓/港不一致"); + if (requireSameDestination && StringUtils.hasText(b.getDestinationWarehouseCode()) + && !Objects.equals(b.getDestinationWarehouseCode(), c.destinationWarehouseCode())) { + throw validation("目的仓/港不一致"); + } BigDecimal v=lines.stream().map(x->unit(x.getVolumeCbm())).reduce(BigDecimal.ZERO,BigDecimal::add).add(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count))); BigDecimal w=lines.stream().map(x->unit(x.getGrossWeightKg())).reduce(BigDecimal.ZERO,BigDecimal::add).add(unit(c.unitGrossWeightKg()).multiply(BigDecimal.valueOf(count))); if (b.getCapacityCbm()==null || b.getMaxWeightKg()==null) throw validation("货柜容量和最大承重未配置"); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java index 47db7a315..5868a702a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java @@ -58,6 +58,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.doAnswer; @@ -821,15 +822,15 @@ class LoadingPickServiceTest { complete.setPickedPackageCount(5); complete.setItemStatus("PICKED"); GroupSnapshot group = coloadGroup("LOCKED", 10L, - new Member(101L, 301L, 50L, 5), new Member(202L, 302L, 60L, 5)); + new Member(101L, 301L, 50L, 5), new Member(101L, 301L, 60L, 5)); when(repository.taskForUpdate(20L)).thenReturn(task); when(repository.allTaskItemsResolved(20L)).thenReturn(true); when(repository.inboundOwners(20L)).thenReturn(List.of( new LoadingPickItemMapper.InboundOwner(101L, 50L), - new LoadingPickItemMapper.InboundOwner(202L, 60L))); + new LoadingPickItemMapper.InboundOwner(101L, 60L))); when(repository.items(20L)).thenReturn(List.of(shortPicked, complete)); when(coloadGroups.findByInbound(101L, 50L)).thenReturn(group); - when(coloadGroups.findByInbound(202L, 60L)).thenReturn(group); + when(coloadGroups.findByInbound(101L, 60L)).thenReturn(group); assertThatThrownBy(() -> service.complete(20L, new CompleteLoadingRequest(0, "complete-short-coload", "短拣异常"), 9L, "operator")) @@ -1247,8 +1248,14 @@ class LoadingPickServiceTest { } private static GroupSnapshot coloadGroup(String status, Long lockedPlanId, Member... members) { - return new GroupSnapshot(700L, "CLG-700", status, 101L, "FBA", "FBA", 201L, - "FBA1", lockedPlanId, 1, List.of(members), LocalDateTime.now(), LocalDateTime.now()); + GroupSnapshot group = mock(GroupSnapshot.class); + when(group.groupId()).thenReturn(700L); + when(group.status()).thenReturn(status); + when(group.lockedPlanId()).thenReturn(lockedPlanId); + when(group.members()).thenReturn(List.of(members)); + when(group.fullPackageCount()).thenReturn(List.of(members).stream() + .mapToInt(Member::fullPackageCount).sum()); + return group; } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java index aa8b9489a..f9800a076 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/SmartLoadingServiceTest.java @@ -165,8 +165,9 @@ class SmartLoadingServiceTest { @Test void rejectsIndividualAssignmentForColoadMemberWithoutWriting() { stubAssignment(); - when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group("ACTIVE", null, - new Member(9L, 30L, 10L, 10), new Member(19L, 31L, 11L, 5))); + GroupSnapshot group = group("ACTIVE", null, + new Member(9L, 30L, 10L, 10), new Member(19L, 31L, 11L, 5)); + when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group); assertThatThrownBy(() -> service.assign(1L, new AssignRequest(10L, 20L, 10, 0, "individual-group-member"), 7L, "u")) @@ -178,13 +179,13 @@ class SmartLoadingServiceTest { } @Test - void atomicallyAssignsEveryMemberWithItsFullAvailablePackageCount() { + void atomicallyAssignsEveryInboundFragmentOfSameOrderAcrossDifferentDestinations() { Candidate first = candidate(9L, 10L, 20L, 30L, 4); - Candidate second = candidate(19L, 11L, 21L, 31L, 6); + Candidate second = candidate(9L, 11L, 21L, 30L, 6, "FBA2"); GroupSnapshot active = group("ACTIVE", null, - new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6)); + new Member(9L, 30L, 10L, 4), new Member(9L, 30L, 11L, 6)); GroupSnapshot locked = group("LOCKED", 1L, - new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6)); + new Member(9L, 30L, 10L, 4), new Member(9L, 30L, 11L, 6)); AtomicInteger version = new AtomicInteger(); List saved = new ArrayList<>(); when(repository.plan(1L)).thenAnswer(invocation -> plan("EDITING", version.get())); @@ -213,10 +214,10 @@ class SmartLoadingServiceTest { long orderId = invocation.getArgument(2); long detailId = invocation.getArgument(3); return ApiResponse.success(new OrderCargoClient.CargoBinding(detailId, orderId, 1, - "货物", orderId == 30L ? 4 : 6, BigDecimal.TEN, BigDecimal.ONE)); + "货物", detailId == 20L ? 4 : 6, BigDecimal.TEN, BigDecimal.ONE)); }); when(coloadGroups.findByInbound(9L, 10L)).thenReturn(active); - when(coloadGroups.findByInbound(19L, 11L)).thenReturn(active); + when(coloadGroups.findByInbound(9L, 11L)).thenReturn(active); when(coloadGroups.lockWholeGroupForAssignment(eq(9L), eq(10L), eq(1L), anyString(), eq(7L), eq("u"))).thenReturn(locked); @@ -265,10 +266,11 @@ class SmartLoadingServiceTest { void adjustAndIndividualDeleteRejectColoadMembersBeforeAnyWrite() { LoadingPlanEntity plan = plan("EDITING", 0); LoadingPlanItemEntity item = item(8L, 9L, 20L, 4); + GroupSnapshot group = group("LOCKED", 1L, + new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6)); when(repository.plan(1L)).thenReturn(plan); when(repository.item(1L, 8L)).thenReturn(item); - when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group("LOCKED", 1L, - new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6))); + when(coloadGroups.findByInbound(9L, 10L)).thenReturn(group); assertThatThrownBy(() -> service.adjust(1L, 8L, new AdjustRequest(4, 0, 0, "adjust-group-member"), 7L, "u")) @@ -323,9 +325,10 @@ class SmartLoadingServiceTest { 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, 4), new Member(19L, 31L, 11L, 6)); when(coloadGroups.unlockWholeGroupAfterRemoval(eq(700L), eq(1L), - anyString(), eq(7L), eq("u"))).thenReturn(group("ACTIVE", null, - new Member(9L, 30L, 10L, 4), new Member(19L, 31L, 11L, 6))); + anyString(), eq(7L), eq("u"))).thenReturn(unlocked); Plan result = service.removeGroup(1L, 700L, new com.baiyue.firstleg.dto.SmartLoadingDtos.RemoveGroupRequest( @@ -1202,15 +1205,29 @@ class SmartLoadingServiceTest { } private static Candidate candidate(long tenantId, long inboundId, long detailId, long orderId, int boxes) { + return candidate(tenantId, inboundId, detailId, orderId, boxes, "FBA1"); + } + + private static Candidate candidate(long tenantId, long inboundId, long detailId, long orderId, int boxes, + String destinationCode) { return new Candidate(tenantId, inboundId, detailId, orderId, "O" + orderId, "I" + inboundId, - 100L, "FBA", "FBA渠道", 200L, "FBA1", "FBA仓", "客户", "货物", boxes, + 100L, "FBA", "FBA渠道", 200L, destinationCode, "FBA仓", "客户", "货物", boxes, new BigDecimal("2"), new BigDecimal("1.5"), new BigDecimal("0.5"), null, null, null, null, null, null); } private static GroupSnapshot group(String status, Long lockedPlanId, Member... members) { - return new GroupSnapshot(700L, "CLG-700", status, 100L, "FBA", "FBA", 200L, - "FBA1", lockedPlanId, 0, List.of(members), LocalDateTime.now(), LocalDateTime.now()); + GroupSnapshot group = mock(GroupSnapshot.class); + when(group.groupId()).thenReturn(700L); + when(group.groupNo()).thenReturn("CLG-700"); + when(group.status()).thenReturn(status); + when(group.warehouseId()).thenReturn(100L); + when(group.channelCode()).thenReturn("FBA"); + when(group.lockedPlanId()).thenReturn(lockedPlanId); + when(group.members()).thenReturn(List.of(members)); + when(group.fullPackageCount()).thenReturn(List.of(members).stream() + .mapToInt(Member::fullPackageCount).sum()); + return group; } private static ContainerBookingEntity booking() {