From 42679d4d548be05b83a8c1dd8cfc80edb0e7c11a Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 15 Oct 2026 17:28:22 +0800 Subject: [PATCH] fix(appointment): retain unbookable cargo as blocked work --- .../work/AppointmentWorkDeliveryProvider.java | 11 ++++++++--- .../AppointmentWorkRequirementProvider.java | 12 +++++++++++- .../work/AppointmentWorkSourceAvailability.java | 15 +++++++++++++++ .../AppointmentWorkDeliveryProviderTest.java | 11 +++++++++++ .../AppointmentWorkRequirementProviderTest.java | 17 +++++++++++++++++ 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkSourceAvailability.java diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProvider.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProvider.java index ae617b403..a469dc093 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProvider.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProvider.java @@ -63,11 +63,16 @@ public class AppointmentWorkDeliveryProvider implements AppointmentWorkFactGatew if(request.sources().isEmpty()) { blocks.add("尾程货源尚未建立,请先核对整柜来源与入仓计划"); missing.add("LASTLEG_SOURCE"); } else { for(var source:request.sources()) { - var context=cargo.get(source.sourceKey(),source.customerId(),source.destinationKey()); - fingerprints.add(context.toString()); blocks.addAll(context.blockReasons()); + try { + var context=cargo.get(source.sourceKey(),source.customerId(),source.destinationKey()); + fingerprints.add(context.toString()); blocks.addAll(context.blockReasons()); + } catch(org.springframework.web.server.ResponseStatusException error) { + if(!AppointmentWorkSourceAvailability.unresolved(error)) throw error; + fingerprints.add(source+":"+error.getReason());blocks.add(error.getReason());missing.add("SOURCE_CONFIGURATION"); + } } var first=request.sources().get(0); - action=new Action(ActionCode.VERIFY_CARGO,"核实货物并建立预约",request.workItemKey(),new CargoTarget(first)); + if(missing.isEmpty()) action=new Action(ActionCode.VERIFY_CARGO,"核实货物并建立预约",request.workItemKey(),new CargoTarget(first)); } return new Fact(request.workItemKey(),digest(request.requirementKey()+":"+fingerprints),"UNBATCHED",false,PlanStatus.UNPLANNED,ExecutionStatus.PENDING, null,null,null,null,null,null,List.of(ORDER_READ),action==null?List.of():List.of(OverseasPermissions.INBOUND_ORDER_FBA_APPOINTMENT),action, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java index 2f4a114ed..c57e22880 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java @@ -36,7 +36,17 @@ public class AppointmentWorkRequirementProvider implements AppointmentWorkFactGa if("CUSTOMER".equals(requirement.destinationKind())) { customer(requirement,sourceOrders,privateDispatches,result);continue; } if(!"FBA".equals(requirement.destinationKind())) { result.put(requirement.requirementKey(),requirement); continue; } var refs=canonical(sourceOrders.stream().map(s->new SourceReference(s.customerId(),"ORDER:"+s.id(),null)).toList()); - var resolved=sources.resolveBatch(refs); + List resolved; + try { resolved=sources.resolveBatch(refs); } + catch(org.springframework.web.server.ResponseStatusException error) { + if(!AppointmentWorkSourceAvailability.unresolved(error)) throw error; + // Keep the complete physical obligation and its canonical source identities. The fact + // provider reports the original prerequisite and exposes no booking action. + result.put(requirement.requirementKey(),new Requirement(requirement.requirementKey(),null,null, + requirement.destinationKind(),requirement.originName(),requirement.destinationName(),requirement.timezone(), + requirement.references(),List.of(),refs.stream().map(r->new SourceReference(r.customerId(),r.sourceKey(),r.destinationKey())).toList())); + continue; + } var complete=resolved.stream().map(s->new AppointmentCoverageService.SourceRef(s.customerId(),s.sourceKey(),s.destinationKey())).toList(); var history=appointments.relatedBatchSnapshots(complete); var active=history.stream().filter(AppointmentWorkRequirementProvider::active).toList(); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkSourceAvailability.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkSourceAvailability.java new file mode 100644 index 000000000..b6f21c826 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkSourceAvailability.java @@ -0,0 +1,15 @@ +package com.baiyue.lastleg.inboundplan.work; + +import java.util.Set; +import org.springframework.web.server.ResponseStatusException; + +/** An unbookable physical source remains visible work; transport and integrity errors still fail the scan. */ +final class AppointmentWorkSourceAvailability { + private AppointmentWorkSourceAvailability() { } + private static final Set PREREQUISITES=Set.of( + "当前货源类型不支持FBA预约", "货物尚无已开船或后续实际船节点", "当前货源不是FBA送仓货物", + "接仓点不在启用的美国仓目录中", "目的仓不在启用的美国FBA目录中", "目的地已变化,请返回原业务重新核对"); + static boolean unresolved(ResponseStatusException error) { + return error.getStatusCode().value()==400 && error.getReason()!=null && PREREQUISITES.contains(error.getReason()); + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProviderTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProviderTest.java index fc1bc82f8..2e7fbaee2 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProviderTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProviderTest.java @@ -12,6 +12,17 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class AppointmentWorkDeliveryProviderTest { + @Test void knownSourcePrerequisiteIsVisibleAndCannotOfferBookingButOutagesStillFail() { + 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.get("ORDER:11","C",null)).thenThrow(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).get("ORDER:11","C",null); + assertThrows(org.springframework.web.server.ResponseStatusException.class,()->provider.read(List.of(request))); + } @Test void privateCompletionCoversThisDispatchPerSourceWithoutCompletingTheWholeOrder() { var appointments=mock(InboundFbaAppointmentService.class);var deliveries=mock(AppointmentWorkDeliveryMapper.class);var sources=mock(AppointmentWorkSourceMapper.class); var provider=new AppointmentWorkDeliveryProvider(appointments,mock(AppointmentCargoVerificationService.class),mock(AppointmentWorkCommitmentService.class),deliveries,sources); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java index 4e3d15f89..74fe24081 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java @@ -11,6 +11,23 @@ import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*; import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*; class AppointmentWorkRequirementProviderTest { + @Test void unbookableSourceKeepsItsPhysicalRequirementWhileAuthorityFailuresStillAbort() { + var sources=mock(AppointmentSourceService.class);var cargo=mock(AppointmentCargoVerificationService.class); + var appointments=mock(InboundFbaAppointmentService.class);var mapping=mock(AppointmentWorkSourceMapper.class); + var provider=new AppointmentWorkRequirementProvider(sources,cargo,appointments,mapping); + var requirement=new Requirement("INBOUND:1",null,null,"FBA",null,"ONT8",null,List.of(),List.of(),List.of()); + var container=new ContainerSource("1","CNT",null,null,LocalDateTime.parse("2026-10-15T11:00:00"),"1",Flow.TRANSFER,null,List.of(requirement)); + when(mapping.containerSources("1")).thenReturn(List.of(new AppointmentWorkSourceMapper.Source(11L,"C","10","1","FBA","ONT8",4))); + var refs=List.of(new AppointmentCoverageService.SourceRef("C","ORDER:11",null)); + when(sources.canonicalOrders(anyList())).thenReturn(refs); + when(sources.resolveBatch(refs)).thenThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"当前货源类型不支持FBA预约")); + var actual=provider.requirements(container,Flow.TRANSFER); + assertEquals(1,actual.size());assertEquals("INBOUND:1",actual.get(0).requirementKey()); + assertEquals(List.of(new SourceReference("C","ORDER:11",null)),actual.get(0).sources()); + verifyNoInteractions(cargo,appointments); + doThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE,"仓库目录暂不可用,请稍后重新核对")).when(sources).resolveBatch(refs); + assertThrows(org.springframework.web.server.ResponseStatusException.class,()->provider.requirements(container,Flow.TRANSFER)); + } @Test void privateDispatchKeepsPartialRemainderAndRestoresCancelledQuantity() { var sources=mock(AppointmentSourceService.class);var cargo=mock(AppointmentCargoVerificationService.class);var appointments=mock(InboundFbaAppointmentService.class);var mapping=mock(AppointmentWorkSourceMapper.class); var provider=new AppointmentWorkRequirementProvider(sources,cargo,appointments,mapping);