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
@@ -145,6 +145,20 @@ public class InboundFbaAppointmentService {
}
return new FbaAppointmentBatchContext(snapshot,List.copyOf(allocations),List.copyOf(actions),reasons.stream().distinct().toList());
}
/** Original persisted appointment facts only; reading work status must not run split/merge eligibility. */
@Transactional(readOnly=true)
public List<FbaAppointmentResponse> batchSnapshots(List<String> keys) {
if(keys==null || keys.isEmpty()) return List.of();
if(keys.size()>200) throw bad("每次最多读取200个预约批次");
var ids=keys.stream().map(key->{
try {long id=Long.parseLong(key.substring("FBA_APPOINTMENT:".length()));if(id>0 && key.equals(batchKey(id))) return id;}
catch(RuntimeException ignored) { }
throw bad("批次关联键无效");
}).distinct().toList();
var rows=appointmentMapper.selectBatchIds(ids);
if(rows.size()!=ids.size()) throw missing();
return responses(rows);
}
@Transactional(isolation=org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
public FbaAppointmentResponse update(String operatorId,String appointmentNo,UpdateFbaAppointmentRequest request) {
if ("CANCELLED".equals(status(request.details().appointmentStatus()))) throw bad("取消预约请使用取消操作并填写实际取消原因");
@@ -16,4 +16,20 @@ public interface AppointmentWorkDeliveryMapper {
record Delivery(String customerId,String sourceKey,String destinationKey,Integer allocatedPackages,String dispatchStatus,String deliveryNo,String status,Integer packages,Integer signedPackages,LocalDateTime signedAt) { }
@Select("SELECT b.customer_id,b.source_key,b.destination_key,b.package_count allocated_packages,d.status dispatch_status,x.delivery_no,x.status,x.package_count packages,x.signed_packages,x.signed_at FROM ll_dispatch_appointment_allocation b JOIN ll_dispatch_order d ON d.id=b.dispatch_id AND d.status<>'CANCELLED' LEFT JOIN ll_delivery_order x ON x.dispatch_order_id=b.dispatch_id AND x.customer_id=b.customer_id AND CONCAT('FBA:',x.destination_warehouse_code)=b.destination_key WHERE b.batch_key=#{batch} ORDER BY b.dispatch_item_id,b.source_key,x.id LIMIT 1001")
List<Delivery> batch(String batch);
record BatchDelivery(String batchKey,String customerId,String sourceKey,String destinationKey,Integer allocatedPackages,String dispatchStatus,String deliveryNo,String status,Integer packages,Integer signedPackages,LocalDateTime signedAt) {
public Delivery delivery() { return new Delivery(customerId,sourceKey,destinationKey,allocatedPackages,dispatchStatus,deliveryNo,status,packages,signedPackages,signedAt); }
}
@Select("""
<script>
SELECT batch_key,customer_id,source_key,destination_key,allocated_packages,dispatch_status,delivery_no,status,packages,signed_packages,signed_at FROM (
SELECT b.batch_key,b.customer_id,b.source_key,b.destination_key,b.package_count allocated_packages,d.status dispatch_status,
x.delivery_no,x.status,x.package_count packages,x.signed_packages,x.signed_at,
ROW_NUMBER() OVER(PARTITION BY b.batch_key ORDER BY b.dispatch_item_id,b.source_key,x.id) work_row
FROM ll_dispatch_appointment_allocation b JOIN ll_dispatch_order d ON d.id=b.dispatch_id AND d.status&lt;&gt;'CANCELLED'
LEFT JOIN ll_delivery_order x ON x.dispatch_order_id=b.dispatch_id AND x.customer_id=b.customer_id AND CONCAT('FBA:',x.destination_warehouse_code)=b.destination_key
WHERE b.batch_key IN <foreach collection='batches' item='batch' open='(' separator=',' close=')'>#{batch}</foreach>
) ranked WHERE work_row &lt;= 1001 ORDER BY batch_key,work_row
</script>
""")
List<BatchDelivery> batches(@Param("batches") List<String> batches);
}
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.work;
import com.baiyue.lastleg.appointmentplanning.*;
import com.baiyue.lastleg.appointmentwork.*;
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
import com.baiyue.lastleg.security.OverseasPermissions;
import org.springframework.stereotype.Component;
import java.util.*;
@@ -22,14 +23,43 @@ public class AppointmentWorkDeliveryProvider implements AppointmentWorkFactGatew
this.appointments=appointments; this.cargo=cargo; this.commitments=commitments; this.deliveries=deliveries; this.sourceMapping=sourceMapping;
}
public boolean supports(Type type,Flow flow) { return type==Type.DELIVERY && flow==Flow.TRANSFER; }
public List<Fact> read(List<FactRequest> requests) { return requests.stream().map(this::read).toList(); }
private Fact read(FactRequest request) {
if(request.batchKey()==null) return unbatched(request);
public List<Fact> read(List<FactRequest> requests) {
var mapped=new HashMap<String,List<AppointmentWorkSourceMapper.Source>>();
var privateSources=new HashMap<String,List<AppointmentWorkSourceMapper.Source>>();
var containers=requests.stream().filter(r->r.batchKey()==null && r.requirementKey()!=null && r.requirementKey().startsWith("INBOUND:")).map(FactRequest::containerBookingId).distinct().toList();
if(!containers.isEmpty()) sourceMapping.containerSourcesBatch(containers).forEach(s->mapped.computeIfAbsent(s.containerBookingId(),ignored->new ArrayList<>()).add(s.source()));
for(var request:requests) if(request.batchKey()==null && request.requirementKey()!=null && request.requirementKey().startsWith("INBOUND:")) {
var sources=mapped.getOrDefault(request.containerBookingId(),List.of());
if(sources.size()>1000) throw unavailable("柜次尾程来源超过完整读取上限");
var related=sources.stream().filter(s->request.requirementKey().equals("INBOUND:"+s.sourceInboundId())).toList();
if(!related.isEmpty() && related.stream().allMatch(s->"PRIVATE".equals(s.destinationType()))) privateSources.put(request.workItemKey(),related);
}
var refs=requests.stream().filter(r->r.batchKey()==null && !privateSources.containsKey(r.workItemKey())).flatMap(r->r.sources().stream())
.map(s->new AppointmentCoverageService.SourceRef(s.customerId(),s.sourceKey(),s.destinationKey())).distinct().toList();
var inspections=new HashMap<AppointmentCoverageService.SourceRef,AppointmentCargoVerificationService.Inspection>();
if(!refs.isEmpty()) cargo.inspectBatch(refs).forEach(i->inspections.put(i.reference(),i));
var batches=requests.stream().map(FactRequest::batchKey).filter(k->k!=null && !k.startsWith("DISPATCH:")).distinct().toList();
var snapshots=new HashMap<String,FbaAppointmentResponse>();
var deliveryRows=new HashMap<String,List<AppointmentWorkDeliveryMapper.Delivery>>();
Map<String,String> impacts=Map.of();
if(!batches.isEmpty()) {
appointments.batchSnapshots(batches).forEach(a->snapshots.put(a.batchKey(),a));
deliveries.batches(batches).forEach(d->deliveryRows.computeIfAbsent(d.batchKey(),ignored->new ArrayList<>()).add(d.delivery()));
impacts=commitments.impactTokens(batches);
}
var context=new BatchFacts(snapshots,deliveryRows,impacts);
return requests.stream().map(r->read(r,privateSources.get(r.workItemKey()),inspections,context)).toList();
}
private record BatchFacts(Map<String,FbaAppointmentResponse> appointments,Map<String,List<AppointmentWorkDeliveryMapper.Delivery>> deliveries,Map<String,String> impacts) { }
private Fact read(FactRequest request,List<AppointmentWorkSourceMapper.Source> privateSources,Map<AppointmentCoverageService.SourceRef,AppointmentCargoVerificationService.Inspection> inspections,BatchFacts context) {
if(request.batchKey()==null) return privateSources!=null?customer(request,privateSources):unbatched(request,inspections);
if(request.batchKey().startsWith("DISPATCH:")) return customerBatch(request);
var context=appointments.batchContext(request.batchKey()); var a=context.appointment();
var a=context.appointments().get(request.batchKey());
var impact=context.impacts().get(request.batchKey());
if(a==null || impact==null) throw unavailable("预约批次事实批量读取不完整");
boolean cancelled=Set.of("CANCELLED","FAILED").contains(a.appointmentStatus());
var target=cancelled?null:target(a); var confirmed=cancelled?null:confirmed(a);
var rows=deliveries.batch(a.batchKey());
var rows=context.deliveries().getOrDefault(a.batchKey(),List.of());
if(rows.size()>1000) throw unavailable("批次派送事实超过完整读取上限");
boolean complete=!rows.isEmpty() && rows.stream().allMatch(d->d.status()!=null && Set.of("DELIVERED","RETURN_PENDING","RETURNED").contains(d.status()) && d.signedAt()!=null && d.packages()!=null && d.packages().equals(d.signedPackages()));
// Every original source allocation must have real dispatch coverage, not just one signed fragment.
@@ -46,25 +76,21 @@ public class AppointmentWorkDeliveryProvider implements AppointmentWorkFactGatew
var action=complete?null:new Action(confirmed==null?ActionCode.RECORD_CONFIRMATION:ActionCode.CHANGE_BOOKING,confirmed==null?"维护原预约并登记确认":"核对或变更原预约",request.workItemKey(),new AppointmentTarget(a.id().toString(),a.batchKey(),sourceRefs));
var references=new ArrayList<BusinessReference>(); references.add(new BusinessReference("FBA_APPOINTMENT",a.batchKey(),a.appointmentNo(),"送仓预约"));
rows.stream().filter(d->d.deliveryNo()!=null).map(d->new BusinessReference("DELIVERY",d.deliveryNo(),d.deliveryNo(),"实际派送")).distinct().forEach(references::add);
return new Fact(request.workItemKey(),digest(a.batchKey()+":"+a.version()+":"+rows),commitments.impactToken(a.batchKey()),true,
return new Fact(request.workItemKey(),digest(a.batchKey()+":"+a.version()+":"+rows),impact,true,
confirmed!=null?PlanStatus.CONFIRMED:target!=null?PlanStatus.TARGET_SET:PlanStatus.UNPLANNED,execution,target,confirmed,actual,a.timezone(),a.appointmentNo(),null,
List.of(ORDER_READ),action==null?List.of():List.of(OverseasPermissions.INBOUND_ORDER_FBA_APPOINTMENT),action,blocks,
actual!=null?List.of("SOURCE_TIMEZONE"):List.of(),List.copyOf(references),null);
}
private Fact unbatched(FactRequest request) {
if(request.requirementKey()!=null && request.requirementKey().startsWith("INBOUND:")) {
var mapped=sourceMapping.containerSources(request.containerBookingId());
if(mapped.size()>1000) throw unavailable("柜次尾程来源超过完整读取上限");
var related=mapped.stream().filter(s->request.requirementKey().equals("INBOUND:"+s.sourceInboundId())).toList();
if(!related.isEmpty() && related.stream().allMatch(s->"PRIVATE".equals(s.destinationType()))) return customer(request,related);
}
private Fact unbatched(FactRequest request,Map<AppointmentCoverageService.SourceRef,AppointmentCargoVerificationService.Inspection> inspections) {
var blocks=new ArrayList<String>(); var missing=new ArrayList<String>(); Action action=null;
var fingerprints=new ArrayList<String>();
if(request.sources().isEmpty()) { blocks.add("尾程货源尚未建立,请先核对整柜来源与入仓计划"); missing.add("LASTLEG_SOURCE"); }
else {
for(var source:request.sources()) {
try {
var context=cargo.inspect(source.sourceKey(),source.customerId(),source.destinationKey());
var inspection=inspections.get(new AppointmentCoverageService.SourceRef(source.customerId(),source.sourceKey(),source.destinationKey()));
if(inspection==null) throw unavailable("预约货源批量读取不完整");
var context=inspection.requireContext();
fingerprints.add(context.toString()); blocks.addAll(context.blockReasons());
} catch(org.springframework.web.server.ResponseStatusException error) {
if(!AppointmentWorkSourceAvailability.unresolved(error)) throw error;
@@ -7,8 +7,32 @@ import java.util.List;
@Mapper
public interface AppointmentWorkReceivingMapper {
@Select("SELECT DISTINCT p.* FROM ll_inbound_plan p JOIN ll_inbound_plan_line l ON l.plan_id=p.id JOIN ll_order o ON o.id=l.order_id AND o.customer_id=l.customer_id WHERE o.source_container_booking_id=#{container} ORDER BY p.id LIMIT 1001")
List<InboundPlan> plans(String container);
@Select("SELECT * FROM ll_receiving_container_session WHERE source_container_booking_id=#{container} ORDER BY created_at DESC,id DESC LIMIT 1")
ReceivingContainerSession session(String container);
class ContainerPlan extends InboundPlan {
private String sourceContainerBookingId;
public String getSourceContainerBookingId() { return sourceContainerBookingId; }
public void setSourceContainerBookingId(String value) { sourceContainerBookingId=value; }
}
@Select("""
<script>
SELECT ranked.* FROM (
SELECT source.*,ROW_NUMBER() OVER(PARTITION BY source_container_booking_id ORDER BY id) work_row FROM (
SELECT DISTINCT p.*,o.source_container_booking_id FROM ll_inbound_plan p
JOIN ll_inbound_plan_line l ON l.plan_id=p.id JOIN ll_order o ON o.id=l.order_id AND o.customer_id=l.customer_id
WHERE o.source_container_booking_id IN
<foreach collection='containers' item='container' open='(' separator=',' close=')'>#{container}</foreach>
) source
) ranked WHERE work_row &lt;= 1001 ORDER BY source_container_booking_id,id
</script>
""")
List<ContainerPlan> plans(@Param("containers") List<String> containers);
@Select("""
<script>
SELECT ranked.* FROM (
SELECT s.*,ROW_NUMBER() OVER(PARTITION BY source_container_booking_id ORDER BY created_at DESC,id DESC) work_row
FROM ll_receiving_container_session s WHERE source_container_booking_id IN
<foreach collection='containers' item='container' open='(' separator=',' close=')'>#{container}</foreach>
) ranked WHERE work_row=1
</script>
""")
List<ReceivingContainerSession> sessions(@Param("containers") List<String> containers);
}
@@ -4,6 +4,7 @@ import com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway;
import com.baiyue.lastleg.security.OverseasPermissions;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.stream.Collectors;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkPolicy.*;
@@ -13,11 +14,15 @@ public class AppointmentWorkReceivingProvider implements AppointmentWorkFactGate
private final AppointmentWorkReceivingMapper mapper;
public AppointmentWorkReceivingProvider(AppointmentWorkReceivingMapper mapper) { this.mapper=mapper; }
public boolean supports(Type type,Flow flow) { return type==Type.WAREHOUSE_RECEIPT && flow==Flow.TRANSFER; }
public List<Fact> read(List<FactRequest> requests) { return requests.stream().map(this::read).toList(); }
private Fact read(FactRequest request) {
var plans=mapper.plans(request.containerBookingId());
public List<Fact> read(List<FactRequest> requests) {
if(requests.isEmpty()) return List.of();
var ids=requests.stream().map(FactRequest::containerBookingId).distinct().toList();
var plans=mapper.plans(ids).stream().collect(Collectors.groupingBy(AppointmentWorkReceivingMapper.ContainerPlan::getSourceContainerBookingId));
var sessions=mapper.sessions(ids).stream().collect(Collectors.toMap(s->s.getSourceContainerBookingId(),s->s));
return requests.stream().map(r->read(r,plans.getOrDefault(r.containerBookingId(),List.of()),sessions.get(r.containerBookingId()))).toList();
}
private Fact read(FactRequest request,List<AppointmentWorkReceivingMapper.ContainerPlan> plans,com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerSession session) {
if(plans.size()>1000) throw unavailable("柜次接仓计划超过完整读取上限");
var session=mapper.session(request.containerBookingId());
var dates=plans.stream().map(p->p.getPlannedArrivalDate()).filter(Objects::nonNull).distinct().sorted().toList();
var target=dates.isEmpty()?null:new Slot(dates.get(0),null,null,null,null);
boolean complete=session!=null && session.getCompletedAt()!=null && Set.of("COMPLETED","COMPLETED_WITH_DIFFERENCE").contains(session.getStatus());
@@ -8,6 +8,20 @@ public interface AppointmentWorkSourceMapper {
record Source(Long id,String customerId,String sourceOrderId,String sourceInboundId,String destinationType,String fbaWarehouseCode,Integer packageCount) { }
@Select("SELECT id,customer_id,source_order_id,source_inbound_id,destination_type,fba_warehouse_code,package_count FROM ll_order WHERE source_container_booking_id=#{container} ORDER BY id LIMIT 1001")
List<Source> containerSources(String container);
record ContainerSource(String containerBookingId,Long id,String customerId,String sourceOrderId,String sourceInboundId,String destinationType,String fbaWarehouseCode,Integer packageCount) {
public Source source() { return new Source(id,customerId,sourceOrderId,sourceInboundId,destinationType,fbaWarehouseCode,packageCount); }
}
@Select("""
<script>
SELECT container_booking_id,id,customer_id,source_order_id,source_inbound_id,destination_type,fba_warehouse_code,package_count FROM (
SELECT source_container_booking_id container_booking_id,id,customer_id,source_order_id,source_inbound_id,destination_type,fba_warehouse_code,package_count,
ROW_NUMBER() OVER(PARTITION BY source_container_booking_id ORDER BY id) work_row
FROM ll_order WHERE source_container_booking_id IN
<foreach collection='containers' item='container' open='(' separator=',' close=')'>#{container}</foreach>
) ranked WHERE work_row &lt;= 1001 ORDER BY container_booking_id,id
</script>
""")
List<ContainerSource> containerSourcesBatch(@Param("containers") List<String> containers);
record DispatchSource(long dispatchId,String dispatchNo,String status,long orderId,String customerId,String sourceKey,String destinationKey,long packages) { }
@Select("SELECT b.dispatch_id,d.dispatch_no,d.status,o.id order_id,b.customer_id,b.source_key,b.destination_key,SUM(b.package_count) packages FROM ll_dispatch_appointment_allocation b JOIN ll_dispatch_order d ON d.id=b.dispatch_id JOIN ll_order o ON b.destination_key=CONCAT('CUSTOMER:',o.id) AND b.customer_id=o.customer_id WHERE b.batch_key=CONCAT('DISPATCH:',d.id) AND o.source_container_booking_id=#{container} GROUP BY b.dispatch_id,d.dispatch_no,d.status,o.id,b.customer_id,b.source_key,b.destination_key ORDER BY b.dispatch_id,o.id LIMIT 1001")
List<DispatchSource> privateDispatches(String container);