fix(firstleg): align coload loading route gates

This commit is contained in:
2026-08-29 12:36:07 +08:00
parent ba641754de
commit 0cf76df168
3 changed files with 54 additions and 26 deletions
@@ -165,7 +165,7 @@ public class SmartLoadingService {
if (total > availableIncludingCurrent) throw validation("累计分配箱数超过可用箱数");
List<LoadingPlanItemEntity> 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<Long> 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<LoadingPlanItemEntity> 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<LoadingPlanItemEntity> lines,Candidate c,int count){
private void checkRouteAndCapacity(ContainerBookingEntity b,List<LoadingPlanItemEntity> 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("货柜容量和最大承重未配置");
@@ -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;
}
}
@@ -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<LoadingPlanItemEntity> 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() {