perf(firstleg): streamline smart loading assignments
This commit is contained in:
@@ -46,6 +46,7 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
|
||||
}
|
||||
public record WorkbenchContainer(Long bookingId, String bookingNo, String cabinetNo, String containerNo,
|
||||
Long warehouseId, String warehouseName, String channelCode, String channelName, String containerType,
|
||||
BigDecimal capacityCbm, BigDecimal maxWeightKg,
|
||||
Long planId, String planNo, String planStatus, Integer version) {}
|
||||
public record Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
|
||||
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsName, Integer availablePackageCount,
|
||||
|
||||
@@ -252,7 +252,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
|
||||
<script>
|
||||
SELECT b.id bookingId,b.booking_no bookingNo,b.cabinet_no cabinetNo,b.container_no containerNo,
|
||||
b.warehouse_id warehouseId,b.warehouse_name warehouseName,b.channel_code channelCode,
|
||||
b.channel_name channelName,b.container_type containerType,p.id planId,p.plan_no planNo,
|
||||
b.channel_name channelName,b.container_type containerType,b.capacity_cbm capacityCbm,
|
||||
b.max_weight_kg maxWeightKg,p.id planId,p.plan_no planNo,
|
||||
CASE WHEN p.id IS NULL OR NOT EXISTS (SELECT 1 FROM fl_loading_plan_item state_item
|
||||
WHERE state_item.loading_plan_id=p.id AND state_item.deleted=0)
|
||||
THEN 'WAIT_LOADING' ELSE p.plan_status END planStatus,b.version version
|
||||
|
||||
@@ -559,6 +559,12 @@ public class SmartLoadingService {
|
||||
|
||||
private Plan assignInbound(long planId, AssignInboundRequest request,
|
||||
long operatorId, String operatorName, boolean wholeGroupAssignment) {
|
||||
return assignInbound(planId, request, operatorId, operatorName, wholeGroupAssignment, null);
|
||||
}
|
||||
|
||||
private Plan assignInbound(long planId, AssignInboundRequest request,
|
||||
long operatorId, String operatorName, boolean wholeGroupAssignment,
|
||||
List<Candidate> frozenDetails) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("ASSIGN_INBOUND|" + planId + '|' + request.inboundId() + '|'
|
||||
+ request.packageCount() + '|' + request.expectedVersion());
|
||||
@@ -574,8 +580,10 @@ public class SmartLoadingService {
|
||||
}
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
List<Candidate> details = candidates(plan.getContainerBookingId(), booking.getWarehouseId()).stream()
|
||||
.filter(candidate -> Objects.equals(candidate.inboundId(), request.inboundId())).toList();
|
||||
List<Candidate> details = frozenDetails == null
|
||||
? candidates(plan.getContainerBookingId(), booking.getWarehouseId()).stream()
|
||||
.filter(candidate -> Objects.equals(candidate.inboundId(), request.inboundId())).toList()
|
||||
: List.copyOf(frozenDetails);
|
||||
if (details.isEmpty()) throw conflict("入仓码不存在、已分配完或尚未完成入库");
|
||||
Candidate first = details.get(0);
|
||||
if (!wholeGroupAssignment) requireStandalone(first.accountId(), first.inboundId());
|
||||
@@ -708,21 +716,21 @@ public class SmartLoadingService {
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion(), operatorId, operatorName);
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
Candidate selected = candidates(plan.getContainerBookingId(), booking.getWarehouseId()).stream()
|
||||
.filter(candidate -> Objects.equals(candidate.inboundId(), request.inboundId()))
|
||||
.findFirst().orElseThrow(() -> conflict("同柜不可拆组成员当前不可分配"));
|
||||
GroupSnapshot discovered = requiredGroup(selected.accountId(), selected.inboundId());
|
||||
InboundOrderEntity selected = repository.inbound(request.inboundId());
|
||||
if (selected == null) throw conflict("同柜不可拆组成员当前不可分配");
|
||||
GroupSnapshot discovered = requiredGroup(selected.getTenantId(), selected.getId());
|
||||
Map<Long, List<Candidate>> memberCandidates = requireWholeGroupCandidates(
|
||||
planId, booking, discovered, repository.items(planId));
|
||||
GroupSnapshot locked = coloadGroups.lockWholeGroupForAssignment(selected.accountId(),
|
||||
selected.inboundId(), planId, "LOCK-" + sha256(operation).substring(0, 48),
|
||||
GroupSnapshot locked = coloadGroups.lockWholeGroupForAssignment(selected.getTenantId(),
|
||||
selected.getId(), planId, "LOCK-" + sha256(operation).substring(0, 48),
|
||||
operatorId, operatorName);
|
||||
requireSameGroup(discovered, locked, planId);
|
||||
for (Member member : locked.members()) {
|
||||
LoadingPlanEntity current = repository.plan(planId);
|
||||
assignInbound(planId, new AssignInboundRequest(member.inboundId(), member.fullPackageCount(),
|
||||
current.getVersion(), "GRP-INB-" + sha256(operation + '|' + member.inboundId())
|
||||
.substring(0, 48)), operatorId, operatorName, true);
|
||||
.substring(0, 48)), operatorId, operatorName, true,
|
||||
memberCandidates.get(member.inboundId()));
|
||||
}
|
||||
LoadingPlanEntity assigned = repository.plan(planId);
|
||||
history(planId, null, "ADD_COLOAD_GROUP", plan.getPlanStatus(), assigned.getPlanStatus(), operation,
|
||||
|
||||
@@ -2082,7 +2082,7 @@ components:
|
||||
SmartLoadingWorkbenchContainer:
|
||||
type: object
|
||||
required: [bookingId, bookingNo, cabinetNo, warehouseId, warehouseName, channelCode,
|
||||
channelName, containerType, planStatus, version]
|
||||
channelName, containerType, capacityCbm, maxWeightKg, planStatus, version]
|
||||
properties:
|
||||
bookingId: {type: string, pattern: '^[1-9][0-9]*$', description: containerBookingId 技术关联}
|
||||
bookingNo: {type: string, description: 货柜登记业务号}
|
||||
@@ -2093,6 +2093,8 @@ components:
|
||||
channelCode: {type: string}
|
||||
channelName: {type: string}
|
||||
containerType: {type: string}
|
||||
capacityCbm: {type: number, nullable: true, description: 柜型基础容积;装柜上限按正式规则计算}
|
||||
maxWeightKg: {type: number, nullable: true, description: 柜型基础载重;装柜上限按正式规则计算}
|
||||
planId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
planNo: {type: string, nullable: true}
|
||||
planStatus: {type: string, enum: [WAIT_LOADING, EDITING, LOADING]}
|
||||
|
||||
+2
@@ -21,6 +21,8 @@ class PlatformSmartLoadingControllerContractTest {
|
||||
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
|
||||
assertThat(yaml).contains("/api/platform/v1/first-leg/smart-loading/containers:")
|
||||
.contains("SmartLoadingWorkbenchContainerPageEnvelope:")
|
||||
.contains("capacityCbm: {type: number, nullable: true, description: 柜型基础容积;装柜上限按正式规则计算}")
|
||||
.contains("maxWeightKg: {type: number, nullable: true, description: 柜型基础载重;装柜上限按正式规则计算}")
|
||||
.contains("只返回 WAIT_LOADING、EDITING 或 LOADING");
|
||||
}
|
||||
@Test void openApiFreezesGlobalPagedInboundCandidates() throws Exception {
|
||||
|
||||
@@ -441,6 +441,7 @@ class SmartLoadingServiceTest {
|
||||
void pagesWorkbenchContainersWithTheSameGlobalFilters() {
|
||||
WorkbenchContainer container = new WorkbenchContainer(99L, "CB-1", "CAB-1", null,
|
||||
100L, "义乌仓", "MATSON_CLX_TRUCK", "美森正班海卡", "40HC",
|
||||
new BigDecimal("68"), new BigDecimal("28000"),
|
||||
1L, "LP-1", "EDITING", 2);
|
||||
WorkbenchContainerQuery query = new WorkbenchContainerQuery(List.of("MATSON_CLX"), 100L,
|
||||
9L, "IN-10", LocalDate.parse("2026-09-01"), LocalDate.parse("2026-09-30"),
|
||||
@@ -585,6 +586,7 @@ class SmartLoadingServiceTest {
|
||||
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(booking());
|
||||
when(repository.inbound(10L)).thenReturn(inbound(9L, 10L));
|
||||
when(repository.candidates(100L)).thenReturn(List.of(first, second));
|
||||
when(repository.candidate(1L, 100L, 10L, 20L)).thenReturn(first);
|
||||
when(repository.candidate(1L, 100L, 11L, 21L)).thenReturn(second);
|
||||
@@ -628,6 +630,7 @@ class SmartLoadingServiceTest {
|
||||
org.mockito.ArgumentMatchers.startsWith("LOCK-"), eq(7L), eq("u"));
|
||||
verify(loadingModelBridge, times(2)).bindIncrementalAllocation(eq(1L), anyLong(), anyLong(),
|
||||
eq(7L), eq("u"), anyString());
|
||||
verify(repository, times(1)).candidates(100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -721,6 +724,7 @@ class SmartLoadingServiceTest {
|
||||
when(repository.plan(1L)).thenReturn(plan("EDITING", 0));
|
||||
when(repository.planByBooking(99L)).thenReturn(plan("EDITING", 0));
|
||||
when(repository.booking(99L)).thenReturn(small);
|
||||
when(repository.inbound(11L)).thenReturn(inbound(9L, 11L));
|
||||
when(repository.items(1L)).thenReturn(List.of());
|
||||
when(repository.candidates(100L)).thenReturn(List.of(first, second));
|
||||
when(repository.activeItemsByInbound(anyLong(), anyLong())).thenReturn(List.of());
|
||||
@@ -1457,6 +1461,13 @@ class SmartLoadingServiceTest {
|
||||
return candidate(9L, 10L, 20L, 30L, boxes);
|
||||
}
|
||||
|
||||
private static InboundOrderEntity inbound(long tenantId, long inboundId) {
|
||||
InboundOrderEntity value = new InboundOrderEntity();
|
||||
value.setTenantId(tenantId);
|
||||
value.setId(inboundId);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Candidate candidate(long tenantId, long inboundId, long detailId, long orderId, int boxes) {
|
||||
return candidate(tenantId, inboundId, detailId, orderId, boxes, "FBA1");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user