perf(firstleg): batch appointment source and execution facts
This commit is contained in:
+25
-10
@@ -7,6 +7,8 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import com.baiyue.firstleg.portschedule.dto.AppointmentWorkSourceDtos.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
@@ -21,7 +23,7 @@ class AppointmentWorkSourceServiceTest {
|
||||
}
|
||||
@Test void completeCursorManifestIncludesUnboundSourcesAndRejectsScopeDrift() {
|
||||
when(mapper.sources()).thenReturn(List.of(source(1,1),source(2,1)));
|
||||
when(mapper.cargo(anyLong())).thenReturn(List.of()); when(facts.establishedFlow(anyLong())).thenReturn("UNKNOWN");
|
||||
when(mapper.cargo(anyList())).thenReturn(List.of()); stubFlows("UNKNOWN");
|
||||
var first=service.discover(null,1);
|
||||
assertFalse(first.complete()); assertNotNull(first.nextCursor());
|
||||
assertNull(first.containers().get(0).departedAt());
|
||||
@@ -33,9 +35,11 @@ class AppointmentWorkSourceServiceTest {
|
||||
}
|
||||
@Test void sourceKeepsCustomerAndPhysicalInboundLineageWithoutDisplayingIdsAsNumbers() {
|
||||
when(mapper.sources()).thenReturn(List.of(source(1,1)));
|
||||
when(mapper.cargo(1)).thenReturn(List.of(new AppointmentWorkSourceMapper.Cargo(8,9,10,"ORDER-10","IN-8","PRIVATE","P","客户仓",12)));
|
||||
when(facts.establishedFlow(1)).thenReturn("DIRECT");
|
||||
when(mapper.cargo(List.of(1L))).thenReturn(List.of(new AppointmentWorkSourceMapper.ContainerCargo(1,8,9,10,"ORDER-10","IN-8","PRIVATE","P","客户仓",12)));
|
||||
stubFlows("DIRECT");
|
||||
var source=service.container(1);var requirement=source.requirements().get(0);
|
||||
assertEquals(service.hash(List.of(source(1,1),List.of(new AppointmentWorkSourceMapper.Cargo(8,9,10,"ORDER-10","IN-8","PRIVATE","P","客户仓",12)))),source.sourceVersion());
|
||||
assertEquals(source,service.discover(null,200).containers().get(0));
|
||||
assertEquals("INBOUND:8",requirement.requirementKey());assertEquals("PRIVATE",requirement.destinationKind());
|
||||
assertTrue(requirement.references().stream().anyMatch(r->r.kind().equals("CUSTOMER") && r.key().equals("9") && r.businessNo()==null));
|
||||
}
|
||||
@@ -44,31 +48,42 @@ class AppointmentWorkSourceServiceTest {
|
||||
assertEquals(401,assertThrows(ResponseStatusException.class,()->guarded.discover(null,null,20)).getStatusCode().value());
|
||||
verifyNoInteractions(mapper);
|
||||
}
|
||||
@Test void missingFlowEvidenceIsNotSilentlyPresentedAsUnknown() {
|
||||
when(mapper.sources()).thenReturn(List.of(source(1,1)));
|
||||
assertEquals(503,assertThrows(ResponseStatusException.class,()->service.container(1)).getStatusCode().value());
|
||||
}
|
||||
@Test void factBatchReadsManifestOnceAndPreservesEachContainerIdentity() {
|
||||
when(mapper.sources()).thenReturn(IntStream.rangeClosed(1,100).mapToObj(id->source(id,1)).toList());
|
||||
when(mapper.cargo(anyLong())).thenReturn(List.of());
|
||||
when(mapper.cargo(anyList())).thenReturn(List.of());
|
||||
stubFlows("UNKNOWN");
|
||||
var fact=mock(Fact.class);
|
||||
when(facts.read(any(),any())).thenAnswer(invocation->{
|
||||
FactRequest request=invocation.getArgument(0); ContainerSource source=invocation.getArgument(1);
|
||||
assertEquals(request.containerBookingId(),source.containerBookingId());
|
||||
return fact;
|
||||
List<FactRequest> requests=invocation.getArgument(0); Map<Long,ContainerSource> sources=invocation.getArgument(1);
|
||||
assertEquals(100,sources.size());
|
||||
requests.forEach(request->assertEquals(request.containerBookingId(),sources.get(Long.parseLong(request.containerBookingId())).containerBookingId()));
|
||||
return requests.stream().map(request->fact).toList();
|
||||
});
|
||||
var requests=IntStream.rangeClosed(1,100).boxed().flatMap(id->List.of(
|
||||
request("PICKUP-"+id,"PICKUP",id),request("RETURN-"+id,"EMPTY_RETURN",id)).stream()).toList();
|
||||
assertEquals(200,service.facts(requests).size());
|
||||
verify(mapper,times(1)).sources();
|
||||
for(long id=1;id<=100;id++) verify(mapper,times(1)).cargo(id);
|
||||
verify(facts,times(200)).read(any(),any());
|
||||
verify(mapper,times(1)).cargo(IntStream.rangeClosed(1,100).mapToObj(Long::valueOf).toList());
|
||||
verify(facts,times(1)).establishedFlows(anyList());
|
||||
verify(facts,times(1)).read(any(),any());
|
||||
}
|
||||
@Test void nextFactBatchReadsFreshManifestAndRejectsRemovedContainer() {
|
||||
when(mapper.sources()).thenReturn(List.of(source(1,1)),List.of());
|
||||
when(mapper.cargo(1)).thenReturn(List.of());
|
||||
when(mapper.cargo(List.of(1L))).thenReturn(List.of());
|
||||
stubFlows("UNKNOWN");
|
||||
var request=request("PICKUP-1","PICKUP",1);
|
||||
service.facts(List.of(request));
|
||||
assertEquals(404,assertThrows(ResponseStatusException.class,()->service.facts(List.of(request))).getStatusCode().value());
|
||||
verify(mapper,times(2)).sources();
|
||||
verify(facts,times(1)).read(any(),any());
|
||||
}
|
||||
private void stubFlows(String flow) {
|
||||
when(facts.establishedFlows(anyList())).thenAnswer(invocation->((List<Long>)invocation.getArgument(0)).stream().collect(Collectors.toMap(id->id,id->flow)));
|
||||
}
|
||||
private FactRequest request(String key,String type,long id) {
|
||||
return new FactRequest(key,type,"TRANSIT",Long.toString(id),null,null,null,List.of(),List.of(),List.of());
|
||||
}
|
||||
|
||||
+25
@@ -8,6 +8,8 @@ import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import java.util.List;
|
||||
import com.baiyue.firstleg.portschedule.mapper.AppointmentWorkSourceMapper;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class DirectDeliveryMapperTest {
|
||||
@@ -20,8 +22,19 @@ class DirectDeliveryMapperTest {
|
||||
s.execute("CREATE TABLE fl_direct_delivery_unloading(container_booking_id BIGINT,inbound_id BIGINT,actual_at TIMESTAMP,timezone VARCHAR,received_by VARCHAR,evidence_no VARCHAR,remark VARCHAR,version INT,idempotency_key VARCHAR,request_hash VARCHAR,created_by BIGINT,created_by_name VARCHAR,PRIMARY KEY(container_booking_id,inbound_id),UNIQUE(container_booking_id,idempotency_key))");
|
||||
s.execute("INSERT INTO fl_container_booking VALUES(1,'ACTIVE',NULL)");s.execute("INSERT INTO fl_loading_plan VALUES(10,1,'COMPLETED')");
|
||||
s.execute("INSERT INTO fl_loading_plan_item VALUES(10,20,'I20','O20','D',3,0),(10,20,'I20','O20','D',4,0),(10,21,'I21','O21','D',5,0),(10,22,'I22','O22','D',100,1)");
|
||||
s.execute("INSERT INTO fl_container_booking VALUES(2,'ACTIVE','2026-01-02 12:00:00'),(3,'ACTIVE',NULL)");
|
||||
s.execute("INSERT INTO fl_loading_plan VALUES(11,2,'COMPLETED'),(12,3,'DRAFT')");
|
||||
s.execute("INSERT INTO fl_loading_plan_item VALUES(11,20,'I20','O20','D',2,0),(12,20,'I20','O20','D',100,0)");
|
||||
s.execute("ALTER TABLE fl_loading_plan_item ADD account_id BIGINT DEFAULT 7");
|
||||
s.execute("ALTER TABLE fl_loading_plan_item ADD order_id BIGINT DEFAULT 8");
|
||||
s.execute("ALTER TABLE fl_loading_plan_item ADD destination_warehouse_code VARCHAR DEFAULT 'D'");
|
||||
s.execute("CREATE TABLE fl_inbound_order(id BIGINT,destination_type VARCHAR)");
|
||||
s.execute("INSERT INTO fl_inbound_order VALUES(20,'PRIVATE'),(21,'AMAZON'),(22,'PRIVATE')");
|
||||
s.execute("CREATE TABLE fl_terminal_drayage_history(container_booking_id BIGINT,action VARCHAR)");
|
||||
s.execute("INSERT INTO fl_terminal_drayage_history VALUES(1,'SELECT_FLEET'),(1,'SCHEDULE_PICKUP'),(1,'SCHEDULE_PICKUP'),(1,'OTHER'),(2,'SELECT_FLEET')");
|
||||
}
|
||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.setEnvironment(new Environment("test",new JdbcTransactionFactory(),ds));config.addMapper(DirectDeliveryMapper.class);
|
||||
config.addMapper(AppointmentWorkSourceMapper.class);config.addMapper(TerminalDrayageMapper.class);
|
||||
try(var session=new MybatisSqlSessionFactoryBuilder().build(config).openSession()) {
|
||||
var mapper=session.getMapper(DirectDeliveryMapper.class);assertEquals(1,mapper.lockContainer(1).id());
|
||||
assertEquals(2,mapper.deliveries(1).size());assertEquals(7,mapper.deliveries(1).get(0).packageCount());
|
||||
@@ -29,6 +42,18 @@ class DirectDeliveryMapperTest {
|
||||
mapper.insert(1,20,at,"America/Los_Angeles","签收人","POD20",null,"key","hash",7,"admin");
|
||||
assertEquals(1,mapper.evidenceCount(1));assertEquals(1,mapper.keyCount(1,"key"));
|
||||
assertEquals(at,mapper.deliveries(1).get(0).actualAt());assertNull(mapper.deliveries(1).get(1).actualAt());
|
||||
var batch=mapper.deliveriesBatch(List.of(1L,2L,3L));
|
||||
assertEquals(3,batch.size());assertEquals(mapper.deliveries(1),batch.stream().filter(r->r.containerBookingId()==1).map(DirectDeliveryMapper.ContainerDelivery::delivery).toList());
|
||||
assertEquals(2,batch.get(2).packageCount());assertNull(batch.get(2).actualAt());
|
||||
assertEquals(3,mapper.containers(List.of(1L,2L,3L)).size());
|
||||
var flows=mapper.flowEvidence(List.of(1L,2L,3L));
|
||||
assertEquals(1,flows.stream().filter(r->r.id()==1).findFirst().orElseThrow().evidenceCount());
|
||||
assertNotNull(flows.stream().filter(r->r.id()==2).findFirst().orElseThrow().warehouseUnloadedAt());
|
||||
var cargo=session.getMapper(AppointmentWorkSourceMapper.class).cargo(List.of(1L,2L,3L));
|
||||
assertEquals(3,cargo.size());assertEquals(7,cargo.get(0).packageCount());assertEquals(1,cargo.get(0).containerBookingId());
|
||||
assertEquals(2,cargo.get(2).containerBookingId());assertEquals(2,cargo.get(2).packageCount());
|
||||
var changes=session.getMapper(TerminalDrayageMapper.class).pickupChanges(List.of(1L,2L,3L));
|
||||
assertEquals(new TerminalDrayageMapper.PickupChanges(1,2,1),changes.stream().filter(r->r.containerBookingId()==1).findFirst().orElseThrow());
|
||||
assertThrows(RuntimeException.class,()->mapper.insert(1,20,at,"America/Los_Angeles","签收人","POD20",null,"another","hash",7,"admin"));
|
||||
session.rollback();
|
||||
}
|
||||
|
||||
+35
-13
@@ -5,11 +5,11 @@ import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerAc
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.terminaldrayage.mapper.*;
|
||||
import com.baiyue.firstleg.terminaldrayage.entity.TerminalDrayageEntity;
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.History;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -22,22 +22,43 @@ class AppointmentWorkTerminalFactsTest {
|
||||
private final LocalDateTime at=LocalDateTime.parse("2026-01-01T12:30:00");
|
||||
private ContainerSource source(){return new ContainerSource("1","C1",null,at,at,"v","UNKNOWN",null,List.of());}
|
||||
private FactRequest request(String type){return new FactRequest("W1",type,"DIRECT","1",null,null,null,List.of(),List.of(),List.of());}
|
||||
private History history(String action){return new History(action,"A","B",1,2,null,7L,"admin",at);}
|
||||
private Fact read(FactRequest request,ContainerSource source) {return adapter.read(List.of(request),Map.of(1L,source)).get(0);}
|
||||
@Test void twoHundredRequestsReadEachFactSetOnceAndKeepMissingFactsVisible() {
|
||||
var ids=java.util.stream.LongStream.rangeClosed(1,100).boxed().toList();
|
||||
var sources=new java.util.HashMap<Long,ContainerSource>();
|
||||
var requests=new java.util.ArrayList<FactRequest>();
|
||||
for(long id:ids) {
|
||||
sources.put(id,new ContainerSource(Long.toString(id),"C"+id,null,at,at,"v","UNKNOWN",null,List.of()));
|
||||
requests.add(new FactRequest("P"+id,"PICKUP","TRANSIT",Long.toString(id),null,null,null,List.of(),List.of(),List.of()));
|
||||
requests.add(new FactRequest("R"+id,"EMPTY_RETURN","TRANSIT",Long.toString(id),null,null,null,List.of(),List.of(),List.of()));
|
||||
}
|
||||
var results=adapter.read(requests,sources);
|
||||
assertEquals(200,results.size());
|
||||
for(int i=0;i<results.size();i++) {assertEquals(requests.get(i).workItemKey(),results.get(i).workItemKey());assertFalse(results.get(i).blockReasons().isEmpty());}
|
||||
verify(nodes).containerActualFacts(ids);verify(terminal).findAll(ids);verify(terminal).pickupChanges(ids);
|
||||
verify(guard).completedUnloading(ids);verifyNoMoreInteractions(nodes,terminal,guard);verifyNoInteractions(direct);
|
||||
}
|
||||
@Test void batchedFlowConflictsStillFailClosed() {
|
||||
when(direct.flowEvidence(List.of(1L,2L))).thenReturn(List.of(new DirectDeliveryMapper.FlowEvidence(1,null,1),new DirectDeliveryMapper.FlowEvidence(2,at,1)));
|
||||
assertEquals(409,assertThrows(org.springframework.web.server.ResponseStatusException.class,()->adapter.establishedFlows(List.of(1L,2L))).getStatusCode().value());
|
||||
}
|
||||
@Test void initialPickupPlanRetainsLocalDateAndDoesNotInventZoneOrReview() {
|
||||
var plan=new TerminalDrayageEntity();plan.setPickupAppointmentAt(at);plan.setFleetSupplierId(4L);plan.setFleetSupplierName("车队");
|
||||
when(terminal.find(1)).thenReturn(plan);
|
||||
when(terminal.histories(1)).thenReturn(List.of(history("SELECT_FLEET"),history("SCHEDULE_PICKUP")));
|
||||
var fact=adapter.read(request("PICKUP"),source());
|
||||
var plan=new TerminalDrayageEntity();plan.setContainerBookingId(1L);plan.setPickupAppointmentAt(at);plan.setFleetSupplierId(4L);plan.setFleetSupplierName("车队");
|
||||
when(terminal.findAll(List.of(1L))).thenReturn(List.of(plan));
|
||||
when(terminal.pickupChanges(List.of(1L))).thenReturn(List.of(new TerminalDrayageMapper.PickupChanges(1,1,1)));
|
||||
var fact=read(request("PICKUP"),source());
|
||||
assertEquals("0:0",fact.impactToken());assertEquals(at.toLocalDate(),fact.confirmed().date());
|
||||
assertNull(fact.timezone());assertTrue(fact.missingFields().contains("SOURCE_TIMEZONE"));
|
||||
when(terminal.histories(1)).thenReturn(List.of(history("SELECT_FLEET"),history("SCHEDULE_PICKUP"),history("SCHEDULE_PICKUP")));
|
||||
assertEquals("1:0",adapter.read(request("PICKUP"),source()).impactToken());
|
||||
var returnRequest=new FactRequest("W2","EMPTY_RETURN","DIRECT","1",null,null,null,List.of(),List.of(),List.of());
|
||||
assertEquals(fact,adapter.read(List.of(request("PICKUP"),returnRequest),Map.of(1L,source())).get(0));
|
||||
when(terminal.pickupChanges(List.of(1L))).thenReturn(List.of(new TerminalDrayageMapper.PickupChanges(1,2,1)));
|
||||
assertEquals("1:0",read(request("PICKUP"),source()).impactToken());
|
||||
}
|
||||
@Test void emptyReturnActualIsCompleteEvenWhenOriginalTimezoneIsUnknownAndDoesNotCopyPickupResponsibility() {
|
||||
var plan=new TerminalDrayageEntity();plan.setFleetSupplierId(4L);plan.setFleetSupplierName("提柜承运人");
|
||||
when(terminal.find(1)).thenReturn(plan);
|
||||
var plan=new TerminalDrayageEntity();plan.setContainerBookingId(1L);plan.setFleetSupplierId(4L);plan.setFleetSupplierName("提柜承运人");
|
||||
when(terminal.findAll(List.of(1L))).thenReturn(List.of(plan));
|
||||
when(nodes.containerActualFacts(List.of(1L))).thenReturn(List.of(new ContainerActualFacts(1L,2L,3,"STOPPED","EMPTY_RETURNED",at,null,null,null,null,at.minusDays(1),at)));
|
||||
var fact=adapter.read(request("EMPTY_RETURN"),source());
|
||||
var fact=read(request("EMPTY_RETURN"),source());
|
||||
assertEquals("COMPLETED",fact.executionStatus());assertNull(fact.actual().occurredAt());assertEquals(at,fact.actual().localOccurredAt());
|
||||
assertNull(fact.responsibility());assertNull(fact.action());assertNull(fact.businessNo());
|
||||
}
|
||||
@@ -47,9 +68,10 @@ class AppointmentWorkTerminalFactsTest {
|
||||
var source=new ContainerSource("1","C1",null,at,at,"v","DIRECT",null,List.of(
|
||||
new Requirement("INBOUND:2",null,null,"PRIVATE","港","A",null,own,List.of(),List.of()),
|
||||
new Requirement("INBOUND:3",null,null,"PRIVATE","港","B",null,other,List.of(),List.of())));
|
||||
when(direct.deliveries(1)).thenReturn(List.of(new DirectDeliveryMapper.Delivery(2,"IN2","O2","A",4,1,at,"America/Los_Angeles","a","P2",null,"k","h")));
|
||||
when(direct.deliveriesBatch(List.of(1L))).thenReturn(List.of(new DirectDeliveryMapper.ContainerDelivery(1,2,"IN2","O2","A",4,1,at,"America/Los_Angeles","a","P2",null,"k","h")));
|
||||
var request=new FactRequest("W1","DELIVERY","DIRECT","1","INBOUND:2",null,null,List.of(),List.of(),List.of());
|
||||
var fact=adapter.read(request,source);
|
||||
var fact=read(request,source);
|
||||
assertEquals(own,fact.references());assertEquals("IN2",fact.businessNo());assertEquals("COMPLETED",fact.executionStatus());
|
||||
assertEquals(fact,adapter.read(List.of(request,new FactRequest("W2","PICKUP","DIRECT","1",null,null,null,List.of(),List.of(),List.of())),Map.of(1L,source)).get(0));
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -9,6 +9,21 @@ import static org.mockito.Mockito.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class UnloadingEvidenceGuardTest {
|
||||
@Test void batchRetainsInactiveMissingPartialAndZonedCompletionRules() {
|
||||
var mapper=mock(DirectDeliveryMapper.class);var guard=new UnloadingEvidenceGuard(mapper);
|
||||
var at=LocalDateTime.parse("2026-10-10T12:00:00");
|
||||
var ids=List.of(1L,2L,3L,4L,5L,6L);
|
||||
when(mapper.containers(ids)).thenReturn(List.of(new DirectDeliveryMapper.Container(1,"ACTIVE",at),
|
||||
new DirectDeliveryMapper.Container(2,"ACTIVE",null),new DirectDeliveryMapper.Container(3,"ACTIVE",null),
|
||||
new DirectDeliveryMapper.Container(4,"CANCELLED",at),new DirectDeliveryMapper.Container(5,"ACTIVE",null)));
|
||||
when(mapper.deliveriesBatch(List.of(2L,3L,5L))).thenReturn(List.of(
|
||||
new DirectDeliveryMapper.ContainerDelivery(2,20,"I20","O20","D",1,1,at,"America/New_York",null,null,null,null,null),
|
||||
new DirectDeliveryMapper.ContainerDelivery(2,21,"I21","O21","D",1,1,at.minusHours(2),"America/Los_Angeles",null,null,null,null,null),
|
||||
new DirectDeliveryMapper.ContainerDelivery(3,30,"I30","O30","D",1,1,at,"America/New_York",null,null,null,null,null),
|
||||
new DirectDeliveryMapper.ContainerDelivery(3,31,"I31","O31","D",1,0,null,null,null,null,null,null,null)));
|
||||
assertEquals(java.util.Map.of(1L,at,2L,at.minusHours(2)),guard.completedUnloading(ids));
|
||||
verify(mapper).containers(ids);verify(mapper).deliveriesBatch(List.of(2L,3L,5L));verifyNoMoreInteractions(mapper);
|
||||
}
|
||||
@Test void requiresEveryPhysicalInboundSourceAndAcceptsLatestActualEvidence() {
|
||||
var mapper=mock(DirectDeliveryMapper.class);var guard=new UnloadingEvidenceGuard(mapper);
|
||||
var container=new DirectDeliveryMapper.Container(1,"ACTIVE",null);
|
||||
|
||||
Reference in New Issue
Block a user