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