fix(firstleg): refresh recommendation capacity

This commit is contained in:
2026-08-30 16:06:20 +08:00
parent 4581c8a294
commit c9ee319e9d
2 changed files with 94 additions and 2 deletions
@@ -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);