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
@@ -16,6 +16,17 @@ public interface AppointmentCargoVerificationMapper extends BaseMapper<Appointme
@Select("SELECT * FROM ll_appointment_cargo_verification WHERE customer_id=#{customerId} AND source_key=#{sourceKey} ORDER BY version DESC LIMIT 1")
AppointmentCargoVerification latest(@Param("customerId") String customerId,@Param("sourceKey") String sourceKey);
@Select("""
<script>
SELECT ranked.* FROM (
SELECT v.*,ROW_NUMBER() OVER(PARTITION BY customer_id,source_key ORDER BY version DESC) work_row
FROM ll_appointment_cargo_verification v WHERE
<foreach collection='refs' item='ref' separator=' OR '>(customer_id=#{ref.customerId} AND source_key=#{ref.sourceKey})</foreach>
) ranked WHERE work_row=1
</script>
""")
java.util.List<AppointmentCargoVerification> latestBatch(@Param("refs") java.util.List<AppointmentCoverageService.SourceRef> refs);
@Select("SELECT * FROM ll_appointment_cargo_verification WHERE customer_id=#{customerId} AND source_key=#{sourceKey} AND command_key=#{key}")
AppointmentCargoVerification command(@Param("customerId") String customerId,@Param("sourceKey") String sourceKey,@Param("key") String key);
@@ -27,4 +38,15 @@ public interface AppointmentCargoVerificationMapper extends BaseMapper<Appointme
WHERE customer_id=#{customerId} AND source_key=#{sourceKey} AND active=1
""")
Allocation allocated(@Param("customerId") String customerId,@Param("sourceKey") String sourceKey);
record SourceAllocation(String customerId,String sourceKey,long packages,long unknownItems) { }
@Select("""
<script>
SELECT customer_id,source_key,COALESCE(SUM(package_count),0) packages,COUNT(*)-COUNT(package_count) unknown_items
FROM ll_inbound_fba_appointment_item WHERE active=1 AND (
<foreach collection='refs' item='ref' separator=' OR '>(customer_id=#{ref.customerId} AND source_key=#{ref.sourceKey})</foreach>
) GROUP BY customer_id,source_key
</script>
""")
java.util.List<SourceAllocation> allocatedBatch(@Param("refs") java.util.List<AppointmentCoverageService.SourceRef> refs);
}
@@ -47,10 +47,36 @@ public class AppointmentCargoVerificationService {
var source=sources.resolve(sourceKey,customerId,destinationKey);
return inspect(source);
}
public record Inspection(AppointmentCoverageService.SourceRef reference,Context context,ResponseStatusException error) {
public Context requireContext() { if(error!=null) throw error; return context; }
}
/** Shares source and warehouse reads within this call only; never caches mutable business facts across requests. */
public List<Inspection> inspectBatch(List<AppointmentCoverageService.SourceRef> refs) {
var result=new ArrayList<Inspection>();
for(int start=0;start<refs.size();start+=100) {
var batch=refs.subList(start,Math.min(start+100,refs.size()));
var resolved=sources.inspectBatch(batch);
var valid=resolved.stream().filter(r->r.error()==null).map(AppointmentSourceService.Resolution::reference).toList();
var latest=new HashMap<List<String>,AppointmentCargoVerification>();
var allocations=new HashMap<List<String>,AppointmentCargoVerificationMapper.Allocation>();
if(!valid.isEmpty()) {
mapper.latestBatch(valid).forEach(r->latest.put(List.of(r.getCustomerId(),r.getSourceKey()),r));
mapper.allocatedBatch(valid).forEach(r->allocations.put(List.of(r.customerId(),r.sourceKey()),new AppointmentCargoVerificationMapper.Allocation(r.packages(),r.unknownItems())));
}
for(var resolution:resolved) {
var key=List.of(resolution.reference().customerId(),resolution.reference().sourceKey());
result.add(new Inspection(resolution.reference(),resolution.error()==null?inspect(resolution.source(),latest.get(key),allocations.getOrDefault(key,new AppointmentCargoVerificationMapper.Allocation(0,0))):null,resolution.error()));
}
}
return List.copyOf(result);
}
/** Uses an already-authorized batched source resolution; never repeats warehouse HTTP lookups per row. */
public Context inspect(AppointmentSourceService.Source source) {
var row=mapper.latest(source.customerId(),source.sourceKey());
var allocation=mapper.allocated(source.customerId(),source.sourceKey());
return inspect(source,row,allocation);
}
private Context inspect(AppointmentSourceService.Source source,AppointmentCargoVerification row,AppointmentCargoVerificationMapper.Allocation allocation) {
boolean changed=row!=null && changed(row,source);
Integer total=row==null?source.packages():row.getPackageCount();
var reasons=new ArrayList<>(source.blockReasons());
@@ -60,12 +60,22 @@ public class AppointmentSourceService {
}
/** One catalog request and batched source reads, including mixed first-leg and external bookings. */
public List<Source> resolveBatch(List<AppointmentCoverageService.SourceRef> requested) {
return resolveBatch(requested,true).stream().map(Resolution::requireSource).toList();
}
/** Read-only inspection keeps each source's prerequisite failure separate; infrastructure errors still escape. */
public record Resolution(AppointmentCoverageService.SourceRef reference,Source source,ResponseStatusException error) {
public Source requireSource() { if(error!=null) throw error; return source; }
}
public List<Resolution> inspectBatch(List<AppointmentCoverageService.SourceRef> requested) {
return resolveBatch(requested,false);
}
private List<Resolution> resolveBatch(List<AppointmentCoverageService.SourceRef> requested,boolean strict) {
if(requested==null || requested.isEmpty() || requested.size()>100) throw bad("每批请选择1至100票真实货源");
var keys=new LinkedHashSet<String>(); var orderIds=new LinkedHashSet<Long>();
var bookingRefs=new java.util.ArrayList<AppointmentExternalSourceGateway.SourceRef>();
for(var ref:requested) {
if(ref==null || blank(ref.sourceKey()) || ref.sourceKey().length()>160 || blank(ref.customerId()) || ref.customerId().length()>128) throw bad("请选择完整的真实货源");
if(!keys.add(ref.sourceKey())) throw bad("同一来源货物不能重复选择");
if(!keys.add(ref.sourceKey()) && strict) throw bad("同一来源货物不能重复选择");
if(ref.sourceKey().startsWith("ORDER:")) orderIds.add(orderId(ref.sourceKey()));
else if(ref.sourceKey().startsWith("BOOKING:") && !blank(ref.sourceKey().substring(8))) bookingRefs.add(new AppointmentExternalSourceGateway.SourceRef(ref.customerId(),ref.sourceKey().substring(8)));
else throw bad("不支持的货源类型");
@@ -77,17 +87,28 @@ public class AppointmentSourceService {
if(booking.orderId()!=null) orderIds.add(booking.orderId());
}
var orderRows=orderIds.isEmpty()?List.<com.baiyue.lastleg.entity.LastlegOrder>of():orders.selectBatchIds(orderIds);
var directOrders=requested.stream().filter(ref->ref.sourceKey().startsWith("ORDER:")).map(ref->new AppointmentExternalSourceGateway.OrderRef(ref.customerId(),orderId(ref.sourceKey()))).toList();
if(!directOrders.isEmpty() && !external.resolveOrders(directOrders).isEmpty()) throw bad("该货物来自外接登记,请使用原登记来源继续预约");
var directOrders=requested.stream().filter(ref->ref.sourceKey().startsWith("ORDER:")).map(ref->new AppointmentExternalSourceGateway.OrderRef(ref.customerId(),orderId(ref.sourceKey()))).distinct().toList();
var externalOrders=directOrders.isEmpty()?Set.<AppointmentExternalSourceGateway.OrderRef>of():external.resolveOrders(directOrders).stream()
.map(s->new AppointmentExternalSourceGateway.OrderRef(s.customerId(),s.orderId())).collect(Collectors.toSet());
if(strict && !externalOrders.isEmpty()) throw bad("该货物来自外接登记,请使用原登记来源继续预约");
var orderMap=orderRows.stream().collect(Collectors.toMap(com.baiyue.lastleg.entity.LastlegOrder::getId,Function.identity()));
var transferRefs=orderRows.stream().filter(o->"WAREHOUSE_TRANSFER".equals(o.getOrderType())).map(o->new AppointmentCoverageMapper.OrderRef(o.getCustomerId(),o.getId())).toList();
var destinations=transferRefs.isEmpty()?Map.<Long,AppointmentCoverageMapper.Source>of():destinationFacts.sources(transferRefs).stream().collect(Collectors.toMap(AppointmentCoverageMapper.Source::orderId,Function.identity()));
var containerIds=orderRows.stream().filter(o->"FIRST_LEG".equals(o.getOrderType())).map(com.baiyue.lastleg.entity.LastlegOrder::getSourceContainerBookingId).filter(Objects::nonNull).distinct().toList();
var gates=containerIds.isEmpty()?Map.<String,Boolean>of():shipping.selectBatchIds(containerIds).stream().collect(Collectors.toMap(g->g.getContainerBookingId(),g->g.getNodeSequence()!=null && g.getNodeSequence()>=40));
var catalog=catalog();
var result=requested.stream().map(ref->resolve(ref.sourceKey(),ref.customerId(),ref.destinationKey(),orderMap,bookingMap,gates,catalog,destinations)).toList();
var result=requested.stream().map(ref->{
try {
if(ref.sourceKey().startsWith("ORDER:") && externalOrders.contains(new AppointmentExternalSourceGateway.OrderRef(ref.customerId(),orderId(ref.sourceKey()))))
throw bad("该货物来自外接登记,请使用原登记来源继续预约");
return new Resolution(ref,resolve(ref.sourceKey(),ref.customerId(),ref.destinationKey(),orderMap,bookingMap,gates,catalog,destinations),null);
} catch(ResponseStatusException error) {
if(strict || error.getStatusCode().value()>=500) throw error;
return new Resolution(ref,null,error);
}
}).toList();
var materialized=new java.util.HashSet<Long>();
for(var source:result) if(source.orderId()!=null && !materialized.add(source.orderId())) throw bad("同一货物存在重复来源,请保留原登记来源");
if(strict) for(var resolution:result) { var source=resolution.requireSource(); if(source.orderId()!=null && !materialized.add(source.orderId())) throw bad("同一货物存在重复来源,请保留原登记来源"); }
return result;
}
private Source resolve(String sourceKey,String customerId,String destinationKey,