perf(lastleg): batch live appointment work facts and filter candidates

This commit is contained in:
2026-10-15 19:10:57 +08:00
parent abf4e45e39
commit 097bec33b1
24 changed files with 441 additions and 40 deletions
@@ -62,6 +62,16 @@ class InboundFbaAppointmentServiceTest {
SourceFbaAppointmentRequest request(String key,FbaAppointmentDetails details,String... sources) {return new SourceFbaAppointmentRequest(requestRefs(sources),"100","ONT8",details,key);}
FbaAppointmentResponse create(String key) {return service.createFromSources("operator",request(key,pending(null),"ORDER:1","ORDER:2"));}
@Test void workBatchSnapshotsReadOnlyHeadersAndItemsWithoutRunningRepartitionEligibility() {
var original=create("snapshot");
when(headers.selectBatchIds(List.of(original.id()))).thenReturn(List.copyOf(storedHeaders));
clearInvocations(headers,items,query,sources);
assertEquals(List.of(original),service.batchSnapshots(List.of(original.batchKey())));
verify(headers).selectBatchIds(List.of(original.id()));verify(items).selectList(any());verifyNoMoreInteractions(headers,items);verifyNoInteractions(query,sources);
assertThrows(ResponseStatusException.class,()->service.batchSnapshots(List.of("DISPATCH:1")));
assertThrows(ResponseStatusException.class,()->service.batchSnapshots(List.of("FBA_APPOINTMENT:999")));
}
@Test void sourceRequestKeepsWireShapeAndPassesAllSourceFieldsToResolution() throws Exception {
var json=new ObjectMapper().findAndRegisterModules();
var input=request("source-json",pending(null),"ORDER:1","ORDER:2");
@@ -81,7 +81,9 @@ class InboundPlanPaginationTest {
String sql = String.join("", InboundPlanQueryMapper.class
.getMethod("rows", String.class, InboundPlanRequests.Query.class, long.class, long.class)
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
assertThat(sql).contains("ai2.customer_id=p.customer_id", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
assertThat(sql).contains("ai2.customer_id=p.customer_id", "ai2.fba_warehouse_code=l.fba_warehouse_code",
"a2.replacement_nos_json IS NULL", "ORDER BY ai2.active DESC,a2.created_at DESC,ai2.id DESC LIMIT 1");
assertThat(sql).contains("COUNT(DISTINCT current_item.appointment_id)", "current_item.active=1", "THEN 'MULTIPLE'");
assertThat(sql).contains("p.customer_id=#{customerId}", "o.customer_id=p.customer_id");
assertThat(sql).doesNotContain("a2.customer_id=#{customerId}");
assertThat(sql).contains("di.customer_id=p.customer_id AND di.order_id=l.order_id",
@@ -1,6 +1,8 @@
package com.baiyue.lastleg.inboundplan.work;
import com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationService;
import com.baiyue.lastleg.appointmentplanning.AppointmentCoverageService.SourceRef;
import com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.Resolution;
import com.baiyue.lastleg.appointmentwork.AppointmentWorkCommitmentService;
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
import org.junit.jupiter.api.Test;
@@ -12,9 +14,28 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class AppointmentWorkDeliveryProviderTest {
@Test void establishedFbaBatchesShareSnapshotsDeliveriesAndImpactReads() {
var appointments=mock(InboundFbaAppointmentService.class);var cargo=mock(AppointmentCargoVerificationService.class);
var commitments=mock(AppointmentWorkCommitmentService.class);var deliveries=mock(AppointmentWorkDeliveryMapper.class);var sources=mock(AppointmentWorkSourceMapper.class);
var requests=java.util.stream.IntStream.rangeClosed(1,200).mapToObj(i->new FactRequest("W"+i,Type.DELIVERY,Flow.TRANSFER,"1",null,"FBA_APPOINTMENT:"+i,null,List.of(),List.of(),List.of())).toList();
var keys=requests.stream().map(FactRequest::batchKey).toList();
var snapshots=keys.stream().map(key->{
var snapshot=mock(com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse.class);
when(snapshot.batchKey()).thenReturn(key);when(snapshot.id()).thenReturn(Long.valueOf(key.substring(16)));when(snapshot.appointmentStatus()).thenReturn("PENDING");
return snapshot;
}).toList();
when(appointments.batchSnapshots(keys)).thenReturn(snapshots);
when(commitments.impactTokens(keys)).thenReturn(keys.stream().collect(java.util.stream.Collectors.toMap(k->k,k->k+":0")));
var result=new AppointmentWorkDeliveryProvider(appointments,cargo,commitments,deliveries,sources).read(requests);
assertEquals(200,result.size());assertTrue(result.stream().allMatch(f->f.planExists() && f.executionStatus()==ExecutionStatus.PENDING));
verify(appointments).batchSnapshots(keys);verify(deliveries).batches(keys);verify(commitments).impactTokens(keys);
verifyNoMoreInteractions(appointments,deliveries,commitments);verifyNoInteractions(cargo,sources);
when(appointments.batchSnapshots(keys)).thenReturn(List.of());
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->new AppointmentWorkDeliveryProvider(appointments,cargo,commitments,deliveries,sources).read(requests));
}
@Test void handledPrerequisiteCommitsTheCallerTransactionThroughTheRealCargoProxy() {
var sourceService=mock(com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.class);
when(sourceService.resolve("ORDER:11","C",null)).thenThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"当前货源类型不支持FBA预约"));
when(sourceService.inspectBatch(List.of(new SourceRef("C","ORDER:11",null)))).thenReturn(List.of(new Resolution(new SourceRef("C","ORDER:11",null),null,new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"当前货源类型不支持FBA预约"))));
var cargo=new AppointmentCargoVerificationService(sourceService,mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationMapper.class),new com.fasterxml.jackson.databind.ObjectMapper());
var ds=new org.h2.jdbcx.JdbcDataSource();ds.setURL("jdbc:h2:mem:work-read-tx-"+java.util.UUID.randomUUID()+";DB_CLOSE_DELAY=-1");
var manager=new org.springframework.jdbc.datasource.DataSourceTransactionManager(ds);
@@ -33,11 +54,11 @@ class AppointmentWorkDeliveryProviderTest {
var cargo=mock(AppointmentCargoVerificationService.class);var sources=mock(AppointmentWorkSourceMapper.class);
var provider=new AppointmentWorkDeliveryProvider(mock(InboundFbaAppointmentService.class),cargo,mock(AppointmentWorkCommitmentService.class),mock(AppointmentWorkDeliveryMapper.class),sources);
var request=new FactRequest("W",Type.DELIVERY,Flow.TRANSFER,"1","INBOUND:1",null,null,List.of(),List.of(),List.of(new SourceReference("C","ORDER:11",null)));
when(cargo.inspect("ORDER:11","C",null)).thenThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"接仓点不在启用的美国仓目录中"));
when(cargo.inspectBatch(List.of(new SourceRef("C","ORDER:11",null)))).thenReturn(List.of(new AppointmentCargoVerificationService.Inspection(new SourceRef("C","ORDER:11",null),null,new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"接仓点不在启用的美国仓目录中"))));
var fact=provider.read(List.of(request)).get(0);
assertEquals(ExecutionStatus.PENDING,fact.executionStatus());assertNull(fact.action());assertNull(fact.actual());
assertEquals(List.of("SOURCE_CONFIGURATION"),fact.missingFields());assertEquals(List.of("接仓点不在启用的美国仓目录中"),fact.blockReasons());
doThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE,"服务不可用")).when(cargo).inspect("ORDER:11","C",null);
doThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE,"服务不可用")).when(cargo).inspectBatch(List.of(new SourceRef("C","ORDER:11",null)));
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->provider.read(List.of(request)));
}
@Test void privateCompletionCoversThisDispatchPerSourceWithoutCompletingTheWholeOrder() {
@@ -42,8 +42,31 @@ class AppointmentWorkDomainMapperTest {
assertEquals(0,dispatch.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",1));
var deliveries=session.getMapper(AppointmentWorkDeliveryMapper.class);assertTrue(deliveries.batch("FBA_APPOINTMENT:1").isEmpty());assertTrue(deliveries.customer("1","1").isEmpty());
assertTrue(deliveries.privateBatch("DISPATCH:1").isEmpty());
assertTrue(deliveries.batches(List.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:2")).isEmpty());
var sources=session.getMapper(AppointmentWorkSourceMapper.class);assertTrue(sources.containerSources("1").isEmpty());assertTrue(sources.privateDispatches("1").isEmpty());assertTrue(sources.privateBatch("DISPATCH:1").isEmpty());
var receiving=session.getMapper(AppointmentWorkReceivingMapper.class);assertTrue(receiving.plans("1").isEmpty());assertNull(receiving.session("1"));
var receiving=session.getMapper(AppointmentWorkReceivingMapper.class);assertTrue(receiving.plans(List.of("1")).isEmpty());assertTrue(receiving.sessions(List.of("1")).isEmpty());
try(var sql=session.getConnection().createStatement()) {
sql.execute("INSERT INTO ll_order(id,order_no,customer_id,source_region,source_inbound_id,source_event_id,source_loading_plan_id,source_payload_hash,source_container_booking_id) VALUES(101,'O-1','A','TEST','I-1','E-1','P-1','HASH','1'),(102,'O-2','B','TEST','I-2','E-2','P-2','HASH','2')");
sql.execute("INSERT INTO ll_inbound_plan(id,plan_no,customer_id,source_order_id,container_no,planned_arrival_date) VALUES(201,'IP-1','A',101,'C-1','2026-10-16'),(202,'IP-2','B',102,'C-2','2026-10-17')");
sql.execute("INSERT INTO ll_inbound_plan_line(id,customer_id,plan_id,order_id,inbound_code) VALUES(301,'A',201,101,'IN-1'),(302,'B',202,102,'IN-2'),(303,'WRONG',201,102,'WRONG')");
sql.execute("INSERT INTO ll_receiving_container_session(id,source_container_booking_id,container_fact_hash,container_no,overseas_warehouse_id,overseas_warehouse_code,overseas_warehouse_name,operator_key,status,created_at) VALUES(401,'1','HASH','C-1',1,'W','W','A','COMPLETED','2026-10-15 01:00:00'),(402,'1','HASH','C-1',1,'W','W','A','RECEIVING','2026-10-15 02:00:00'),(403,'1','HASH','C-1',1,'W','W','A','SIGNED','2026-10-15 02:00:00'),(404,'2','HASH','C-2',1,'W','W','B','WAIT_SIGN','2026-10-15 01:00:00')");
}
session.clearCache();
var plans=receiving.plans(List.of("1","2"));assertEquals(2,plans.size());
assertEquals("1",plans.get(0).getSourceContainerBookingId());assertEquals("IP-1",plans.get(0).getPlanNo());assertEquals(java.time.LocalDate.of(2026,10,16),plans.get(0).getPlannedArrivalDate());
assertEquals("2",plans.get(1).getSourceContainerBookingId());
var sourceBatch=sources.containerSourcesBatch(List.of("1","2"));assertEquals(2,sourceBatch.size());
assertEquals("1",sourceBatch.get(0).containerBookingId());assertEquals(sources.containerSources("1").get(0),sourceBatch.get(0).source());
var sessions=receiving.sessions(List.of("1","2"));assertEquals(2,sessions.size());
assertEquals(403L,sessions.stream().filter(s->"1".equals(s.getSourceContainerBookingId())).findFirst().orElseThrow().getId());
try(var sql=session.getConnection().createStatement()) {
sql.execute("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,status) VALUES(501,'A','D-1','PENDING_DEPARTURE'),(502,'B','D-2','CANCELLED'),(503,'B','D-3','PENDING_DEPARTURE')");
sql.execute("INSERT INTO ll_dispatch_appointment_allocation(dispatch_id,dispatch_item_id,plan_pallet_id,batch_key,customer_id,source_key,destination_key,package_count) VALUES(501,601,701,'FBA_APPOINTMENT:1','A','ORDER:101','FBA:ONT8',4),(501,602,702,'FBA_APPOINTMENT:1','B','ORDER:102','FBA:ONT8',6),(502,603,703,'FBA_APPOINTMENT:1','B','ORDER:102','FBA:ONT8',2),(503,604,704,'FBA_APPOINTMENT:2','B','ORDER:102','FBA:ONT8',3)");
}
session.clearCache();
var batchDeliveries=deliveries.batches(List.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:2"));assertEquals(3,batchDeliveries.size());
assertEquals(deliveries.batch("FBA_APPOINTMENT:1"),batchDeliveries.stream().filter(d->d.batchKey().equals("FBA_APPOINTMENT:1")).map(AppointmentWorkDeliveryMapper.BatchDelivery::delivery).toList());
assertEquals(deliveries.batch("FBA_APPOINTMENT:2"),batchDeliveries.stream().filter(d->d.batchKey().equals("FBA_APPOINTMENT:2")).map(AppointmentWorkDeliveryMapper.BatchDelivery::delivery).toList());
var trips=session.getMapper(AppointmentWorkTripMapper.class);assertTrue(trips.batchTrips("FBA_APPOINTMENT:1").isEmpty());assertTrue(trips.batches(1).isEmpty());assertNull(trips.trip(1));
}
}
@@ -13,9 +13,10 @@ import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
class AppointmentWorkReceivingProviderTest {
@Test void receivingIsInProgressWithoutInventingAnActualCompletionAndTargetCarriesRealIdentity() {
var mapper=mock(AppointmentWorkReceivingMapper.class); var provider=new AppointmentWorkReceivingProvider(mapper);
var plan=new InboundPlan(); plan.setId(10L);plan.setPlanNo("IP-REAL");plan.setCustomerId("C");plan.setPlannedArrivalDate(LocalDate.of(2026,10,16));
var plan=new AppointmentWorkReceivingMapper.ContainerPlan(); plan.setSourceContainerBookingId("1");plan.setId(10L);plan.setPlanNo("IP-REAL");plan.setCustomerId("C");plan.setPlannedArrivalDate(LocalDate.of(2026,10,16));
var session=new ReceivingContainerSession();session.setId(2L);session.setStatus("RECEIVING");session.setReceivingAt(LocalDateTime.of(2026,10,15,10,0));
when(mapper.plans("1")).thenReturn(List.of(plan)); when(mapper.session("1")).thenReturn(session);
session.setSourceContainerBookingId("1");
when(mapper.plans(List.of("1"))).thenReturn(List.of(plan)); when(mapper.sessions(List.of("1"))).thenReturn(List.of(session));
var request=new FactRequest("work",Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,"1",null,null,null,List.of(),List.of(),List.of());
var fact=provider.read(List.of(request)).get(0);
assertEquals(ExecutionStatus.IN_PROGRESS,fact.executionStatus()); assertNull(fact.actual());
@@ -23,4 +24,14 @@ class AppointmentWorkReceivingProviderTest {
session.setStatus("COMPLETED"); session.setCompletedAt(LocalDateTime.of(2026,10,16,12,0));
fact=provider.read(List.of(request)).get(0); assertEquals(ExecutionStatus.COMPLETED,fact.executionStatus()); assertEquals(session.getCompletedAt(),fact.actual().localOccurredAt());
}
@Test void twoQueriesCoverTwoHundredContainersAndKeepTheirFactsSeparate() {
var mapper=mock(AppointmentWorkReceivingMapper.class); var provider=new AppointmentWorkReceivingProvider(mapper);
var requests=java.util.stream.IntStream.range(1,201).mapToObj(i->new FactRequest("W"+i,Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,""+i,null,null,null,List.of(),List.of(),List.of())).toList();
var plan=new AppointmentWorkReceivingMapper.ContainerPlan();plan.setSourceContainerBookingId("2");plan.setId(20L);plan.setPlanNo("IP-2");plan.setPlannedArrivalDate(LocalDate.of(2026,10,16));
when(mapper.plans(org.mockito.ArgumentMatchers.anyList())).thenReturn(List.of(plan));
var facts=provider.read(requests);assertEquals(200,facts.size());assertNull(facts.get(0).target());assertEquals(plan.getPlannedArrivalDate(),facts.get(1).target().date());
verify(mapper).plans(requests.stream().map(FactRequest::containerBookingId).toList());verify(mapper).sessions(requests.stream().map(FactRequest::containerBookingId).toList());verifyNoMoreInteractions(mapper);
when(mapper.plans(org.mockito.ArgumentMatchers.anyList())).thenReturn(java.util.Collections.nCopies(1001,plan));
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->provider.read(requests));
}
}