fix(firstleg): refresh recommendation capacity
This commit is contained in:
@@ -368,6 +368,13 @@ public class SmartLoadingService {
|
||||
if (!visitedGroups.add(candidate.coLoadGroupId()) || StringUtils.hasText(candidate.blockReason())) {
|
||||
continue;
|
||||
}
|
||||
GroupSnapshot currentGroup = group(candidate.accountId(), candidate.inboundId());
|
||||
if (currentGroup == null || !"ACTIVE".equals(currentGroup.status())
|
||||
|| currentGroup.lockedPlanId() != null
|
||||
|| !groupFits(booking, repository.items(planId), currentGroup,
|
||||
availableCandidates(booking))) {
|
||||
continue;
|
||||
}
|
||||
LoadingPlanEntity current = repository.plan(planId);
|
||||
assignGroup(planId, new AssignGroupRequest(candidate.inboundId(), current.getVersion(),
|
||||
"REC-GRP-" + sha256(operation + '|' + candidate.coLoadGroupId()).substring(0, 48)),
|
||||
@@ -640,8 +647,7 @@ public class SmartLoadingService {
|
||||
if (!"ACTIVE".equals(group.status()) || group.lockedPlanId() != null) {
|
||||
throw conflict("同柜不可拆组已锁定或已消费");
|
||||
}
|
||||
List<Candidate> available = repository.candidates(booking.getWarehouseId()).stream()
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate)).toList();
|
||||
List<Candidate> available = availableCandidates(booking);
|
||||
Map<Long, List<Candidate>> result = new LinkedHashMap<>();
|
||||
for (Member member : group.members()) {
|
||||
if (!repository.activeItemsByInbound(member.tenantId(), member.inboundId()).isEmpty()) {
|
||||
@@ -663,6 +669,11 @@ public class SmartLoadingService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Candidate> availableCandidates(ContainerBookingEntity booking) {
|
||||
return repository.candidates(booking.getWarehouseId()).stream()
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate)).toList();
|
||||
}
|
||||
|
||||
private boolean groupFits(ContainerBookingEntity booking, List<LoadingPlanItemEntity> planItems,
|
||||
GroupSnapshot group, List<Candidate> available) {
|
||||
if (booking.getCapacityCbm() == null || booking.getMaxWeightKg() == null) return false;
|
||||
|
||||
@@ -262,6 +262,87 @@ class SmartLoadingServiceTest {
|
||||
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void recommendRechecksCurrentCapacitySkipsStaleGroupAndContinuesWithLaterCandidate() {
|
||||
Candidate ordinary = candidate(9L, 10L, 20L, 30L, 4);
|
||||
Candidate groupFirst = candidate(9L, 11L, 21L, 31L, 4);
|
||||
Candidate groupSecond = candidate(9L, 12L, 22L, 32L, 4);
|
||||
Candidate later = candidate(9L, 13L, 23L, 33L, 1);
|
||||
GroupSnapshot group = group("ACTIVE", null,
|
||||
new Member(9L, 31L, 11L, 4), new Member(9L, 32L, 12L, 4));
|
||||
ContainerBookingEntity small = booking();
|
||||
small.setCapacityCbm(new BigDecimal("5"));
|
||||
AtomicInteger version = new AtomicInteger();
|
||||
List<LoadingPlanItemEntity> saved = new ArrayList<>();
|
||||
when(repository.plan(1L)).thenAnswer(invocation -> plan("EDITING", version.get()));
|
||||
when(repository.planByBooking(99L)).thenAnswer(invocation -> plan("EDITING", version.get()));
|
||||
when(repository.booking(99L)).thenReturn(small);
|
||||
when(repository.items(1L)).thenAnswer(invocation -> List.copyOf(saved));
|
||||
when(repository.candidates(100L)).thenReturn(List.of(ordinary, groupFirst, groupSecond, later));
|
||||
when(repository.candidate(1L, 100L, 10L, 20L)).thenReturn(ordinary);
|
||||
when(repository.candidate(1L, 100L, 13L, 23L)).thenReturn(later);
|
||||
when(repository.activeItemsByInbound(anyLong(), anyLong(), anyLong())).thenReturn(List.of());
|
||||
when(repository.activeItem(anyLong(), anyLong(), anyLong())).thenReturn(null);
|
||||
when(repository.touch(eq(1L), anyInt(), eq(7L), eq("u"))).thenAnswer(invocation -> {
|
||||
int expected = invocation.getArgument(1);
|
||||
if (expected != version.get()) return 0;
|
||||
version.incrementAndGet();
|
||||
return 1;
|
||||
});
|
||||
doAnswer(invocation -> {
|
||||
LoadingPlanItemEntity item = invocation.getArgument(0);
|
||||
item.setId(800L + saved.size());
|
||||
saved.add(item);
|
||||
return null;
|
||||
}).when(repository).insert(any(LoadingPlanItemEntity.class));
|
||||
when(cargoClient.detail(anyString(), anyLong(), anyLong(), anyLong())).thenAnswer(invocation -> {
|
||||
long orderId = invocation.getArgument(2);
|
||||
long detailId = invocation.getArgument(3);
|
||||
return ApiResponse.success(new OrderCargoClient.CargoBinding(detailId, orderId, 1,
|
||||
"货物", detailId == 23L ? 1 : 4, BigDecimal.TEN, BigDecimal.ONE));
|
||||
});
|
||||
when(coloadGroups.findByInbound(9L, 11L)).thenReturn(group);
|
||||
when(coloadGroups.findByInbound(9L, 12L)).thenReturn(group);
|
||||
|
||||
Plan result = service.recommend(1L,
|
||||
new RecommendRequest(0, "recommend-refresh-group-capacity"), 7L, "u");
|
||||
|
||||
assertThat(result.version()).isEqualTo(2);
|
||||
assertThat(saved).extracting(LoadingPlanItemEntity::getInboundId,
|
||||
LoadingPlanItemEntity::getPackageCount).containsExactly(
|
||||
org.assertj.core.groups.Tuple.tuple(10L, 4),
|
||||
org.assertj.core.groups.Tuple.tuple(13L, 1));
|
||||
verify(coloadGroups, never()).lockWholeGroupForAssignment(anyLong(), anyLong(), anyLong(),
|
||||
anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualGroupAssignmentOverCurrentCapacityStillReturnsA0422() {
|
||||
Candidate first = candidate(9L, 11L, 21L, 31L, 4);
|
||||
Candidate second = candidate(9L, 12L, 22L, 32L, 4);
|
||||
GroupSnapshot group = group("ACTIVE", null,
|
||||
new Member(9L, 31L, 11L, 4), new Member(9L, 32L, 12L, 4));
|
||||
ContainerBookingEntity small = booking();
|
||||
small.setCapacityCbm(new BigDecimal("3"));
|
||||
when(repository.plan(1L)).thenReturn(plan("EDITING", 0));
|
||||
when(repository.planByBooking(99L)).thenReturn(plan("EDITING", 0));
|
||||
when(repository.booking(99L)).thenReturn(small);
|
||||
when(repository.items(1L)).thenReturn(List.of());
|
||||
when(repository.candidates(100L)).thenReturn(List.of(first, second));
|
||||
when(repository.activeItemsByInbound(anyLong(), anyLong())).thenReturn(List.of());
|
||||
when(coloadGroups.findByInbound(9L, 11L)).thenReturn(group);
|
||||
when(coloadGroups.findByInbound(9L, 12L)).thenReturn(group);
|
||||
|
||||
BizException error = catchThrowableOfType(() -> service.assignGroup(1L,
|
||||
new com.baiyue.firstleg.dto.SmartLoadingDtos.AssignGroupRequest(
|
||||
11L, 0, "manual-group-too-large"), 7L, "u"), BizException.class);
|
||||
|
||||
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
assertThat(error).hasMessageContaining("110%上限");
|
||||
verify(coloadGroups, never()).lockWholeGroupForAssignment(anyLong(), anyLong(), anyLong(),
|
||||
anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void adjustAndIndividualDeleteRejectColoadMembersBeforeAnyWrite() {
|
||||
LoadingPlanEntity plan = plan("EDITING", 0);
|
||||
|
||||
Reference in New Issue
Block a user