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
@@ -35,6 +35,9 @@ class AppointmentWorkPersistenceTest {
assertEquals("运营甲",store.replay(state.getWorkItemKey(),"same-key").getOperatorName());
assertThrows(RuntimeException.class,()->store.history(state,new Actor("2","运营乙",ACTOR.permissions()),"SAVE_FOLLOWUP","其他人重复键","same-key","other",Map.of("version",2)));
assertEquals(1,store.history(state.getWorkItemKey()).size());
mapper.insertCommitment("FBA_APPOINTMENT:1","{}");mapper.updateCommitment("FBA_APPOINTMENT:1","{}",3);
var tokens=new AppointmentWorkCommitmentService(mapper,new ObjectMapper()).impactTokens(List.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:2"));
assertEquals(Map.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:1:3","FBA_APPOINTMENT:2","FBA_APPOINTMENT:2:0"),tokens);
}
}
@Test void transactionRollbackCannotLeaveHalfGeneratedRoute() throws Exception {
@@ -12,6 +12,60 @@ import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
class AppointmentWorkQueryServiceTest {
@Test void followTodayPageOneSizeEightDoesNotReadUnrelatedRuntimeFactsOrPaginateBeforeCompletionChecks() {
var f=new WorkTestFixture();
for(int index=0;index<1000;index++) f.add(f.state("unrelated-"+index,Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,null));
var expected=new HashSet<String>();
var prior=f.state("due-1",Type.PICKUP,Flow.TRANSFER,null);f.add(prior);expected.add(prior.getWorkItemKey());
for(int index=1;index<=10;index++) {
var state=f.state("due-"+index,Type.DELIVERY,Flow.TRANSFER,"inbound");state.setFollowupDate(LocalDate.of(2026,10,14));
if(index==1) state.setPrerequisites(List.of(prior.getWorkItemKey()));
if(index==10) f.facts.put(state.getWorkItemKey(),fact(state,null,null,ExecutionStatus.COMPLETED,new Actual(Instant.parse("2026-10-14T12:00:00Z"),null,"America/Los_Angeles")));
f.add(state);expected.add(state.getWorkItemKey());
}
var future=f.state("future",Type.DELIVERY,Flow.TRANSFER,"inbound");future.setFollowupDate(LocalDate.of(2027,1,1));f.add(future);
// Exact parameters from /items?view=FOLLOW_TODAY&page=1&size=8.
var page=f.queries.items(View.FOLLOW_TODAY,1,8,null,null,null,null,null,null,ACTOR);
assertEquals(9,page.total());assertEquals(8,page.records().size());assertEquals(2,page.pages());
assertTrue(page.records().stream().allMatch(i->i.executionStatus()==ExecutionStatus.PENDING));
verify(f.gateway).facts(argThat(states->states.stream().map(AppointmentWorkState::getWorkItemKey).collect(java.util.stream.Collectors.toSet()).equals(expected)));
}
@Test void followTodayPreservesAuthoritativeDateLineZonesAndUndatedPendingHandoffs() {
var f=new WorkTestFixture();
var tomorrow=LocalDate.of(2026,10,16);
var east=f.state("east",Type.DELIVERY,Flow.TRANSFER,"inbound");east.setFollowupDate(tomorrow);f.add(east);
var west=f.state("west",Type.DELIVERY,Flow.TRANSFER,"inbound");west.setFollowupDate(tomorrow);f.add(west);
// Stored timezone is Los Angeles; the live source is already on the following local day.
f.facts.put(east.getWorkItemKey(),fact(east,new Slot(tomorrow,null,null,null,"Pacific/Kiritimati"),null,ExecutionStatus.PENDING,null));
var handoff=f.state("return",Type.EMPTY_RETURN,Flow.TRANSFER,null);
var owner=new Responsibility(ResponsibilityKind.USER,"1","运营甲");
handoff.setReturnHandoff(new ReturnHandoff(1,owner,owner,HandoffStatus.PENDING_ACCEPTANCE,"交接"));f.add(handoff);
var query=new AppointmentWorkQueryService(f.store,f.gateway,Clock.fixed(Instant.parse("2026-10-15T12:00:00Z"),ZoneOffset.UTC));
var page=query.items(View.FOLLOW_TODAY,1,8,null,null,null,null,null,null,ACTOR);
assertEquals(Set.of(east.getWorkItemKey(),handoff.getWorkItemKey()),page.records().stream().map(WorkItem::workItemKey).collect(java.util.stream.Collectors.toSet()));
}
@Test void calendarLoadsOnlyRequestedTypesButUsesCurrentAuthorityDatesAndStillChecksScope() {
var f=new WorkTestFixture();
var receipt=f.state("1",Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,null);receipt.setTarget(slot("2025-01-01"));f.add(receipt);
var pickup=f.state("1",Type.PICKUP,Flow.TRANSFER,null);f.add(pickup);
receipt.setPrerequisites(List.of(pickup.getWorkItemKey()));f.store.save(receipt);
f.facts.put(receipt.getWorkItemKey(),fact(receipt,slot("2026-10-16"),null,ExecutionStatus.PENDING,null));
assertEquals(1,f.queries.calendar(LocalDate.of(2026,10,1),LocalDate.of(2026,10,31),Flow.TRANSFER,Set.of(Type.WAREHOUSE_RECEIPT),ACTOR).total());
verify(f.gateway).facts(argThat(states->states.size()==1 && states.get(0).getWorkItemKey().equals(receipt.getWorkItemKey())));
verify(f.gateway).discover(null,1);
var reader=new Actor("2","无原业务权限",Set.of(LIST));
assertEquals(0,f.queries.calendar(LocalDate.of(2026,10,1),LocalDate.of(2026,10,31),null,Set.of(Type.WAREHOUSE_RECEIPT),reader).total());
}
@Test void filteredItemsLoadPrerequisiteClosureWithoutIncludingUnrelatedContainers() {
var f=new WorkTestFixture();
var pickup=f.state("1",Type.PICKUP,Flow.TRANSFER,null);f.add(pickup);
var retired=f.state("1",Type.ROUTE,Flow.UNKNOWN,null);retired.setRetired(true);retired.setReplacementKeys(List.of(pickup.getWorkItemKey()));f.add(retired);
var receipt=f.state("1",Type.WAREHOUSE_RECEIPT,Flow.TRANSFER,null);receipt.setPrerequisites(List.of(retired.getWorkItemKey()));receipt.setObservedFingerprint("old");f.add(receipt);
f.add(f.state("2",Type.DELIVERY,Flow.TRANSFER,"other"));
var items=f.queries.items(View.ALL,1,20,null,null,Set.of(Type.WAREHOUSE_RECEIPT),"1",null,null,ACTOR);
assertEquals(1,items.total());assertEquals(pickup.getWorkItemKey(),items.records().get(0).nextAction().targetWorkItemKey());
verify(f.gateway).facts(argThat(states->states.stream().map(AppointmentWorkState::getWorkItemKey).collect(java.util.stream.Collectors.toSet()).equals(Set.of(pickup.getWorkItemKey(),receipt.getWorkItemKey()))));
}
@Test void globalSummaryRetainsUnknownRoutesAndReturnAfterDeliveryAcrossCalendarFilters() {
var f=new WorkTestFixture(); var route=f.state("1",Type.ROUTE,Flow.UNKNOWN,null); route.setFollowupDate(LocalDate.of(2026,10,14)); f.add(route);
var returns=f.state("2",Type.EMPTY_RETURN,Flow.DIRECT,null); returns.setTarget(slot("2026-09-01")); f.add(returns);