fix(dispatch): limit appointment prerequisite to inbound cargo

This commit is contained in:
2026-10-22 15:39:33 +08:00
parent 56f2853fb7
commit 86889d7e0e
4 changed files with 18 additions and 12 deletions
@@ -174,10 +174,10 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
var destination=new Destination(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.destinationKey(row.destinationType(),row.destinationWarehouseCode(),
row.fbaWarehouseCode(),row.deliveryAddress(),row.postalCode()),row.destinationType(),row.destinationWarehouseId(),row.destinationWarehouseCode(),
row.destinationWarehouseName(),row.deliveryAddress(),row.postalCode(),row.contactName(),row.contactPhone());
var fact=row.planPalletId()==null?null:batchFacts.get(new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.CandidatePhysical(row.customerId(),row.planPalletId()));
var fact=type!=SourceType.INBOUND_PLAN || row.planPalletId()==null?null:batchFacts.get(new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.CandidatePhysical(row.customerId(),row.planPalletId()));
var references=new ArrayList<BatchReference>();var reasons=new LinkedHashSet<String>();
reasons.addAll(group.blockReasons());
if("FBA".equals(destination.type()) && fact==null)reasons.add("本票FBA货物缺少可核对的正式预约来源,请先确认送仓时间");
if(type==SourceType.INBOUND_PLAN && "FBA".equals(destination.type()) && fact==null)reasons.add("本票FBA货物缺少可核对的正式预约来源,请先确认送仓时间");
if(fact!=null) {
reasons.addAll(fact.blockReasons());
// 来源候选已验证当前草稿拥有者并过滤其他作业占用,不能把本草稿占用再次当禁用。
@@ -859,7 +859,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
for(var source:resolved) {
if(keys.contains(source.sourceKey())) throw conflict("所选来源已在本车,不能重复加入");
var c=source.candidate();
if("FBA".equals(c.destinationType()) && c.planPalletId()==null)throw conflict("本票FBA货物缺少可核对的正式预约来源,请先确认送仓时间");
if(source.selection().sourceType()==SourceType.INBOUND_PLAN && "FBA".equals(c.destinationType()) && c.planPalletId()==null)throw conflict("本票FBA货物缺少可核对的正式预约来源,请先确认送仓时间");
sources.expire(c.customerId(),source.sourceKey(),at);
if(!sources.claims(c.customerId(),source.sourceKey(),at).isEmpty())throw conflict("所选逻辑来源已被有效运输安排占用");
try {if(sources.claim(IdWorker.getId(),c.customerId(),source.sourceKey(),c.planPalletId(),c.warehousePalletId(),c.returnReceiptId(),c.warehousePalletId()==null?null:physical.get(c.warehousePalletId()),row.getId(),actor.operatorId(),at)!=1)throw conflict("来源占用未保存");}
@@ -423,13 +423,14 @@ public class DispatchDraftService implements DispatchPlanningPort {
.map(c->new DraftContainer(c.getId(),c.getSequenceNo(),c.getCapacity(),keys(c))).toList(),selected(row).stream().map(c->projectCandidate(c,batches)).toList(),row.getExpiresAt().toInstant(ZoneOffset.UTC));}
/** 真实客户及计划托盘来自服务器来源,整页一次调用原Provider。 */
private Map<AppointmentWorkDispatchPort.CandidatePhysical,AppointmentWorkDispatchPort.CandidateBatches> readCandidateBatches(List<Candidate> candidates) {
var identities=candidates.stream().filter(c->c.selection().planPalletId()!=null).map(c->new AppointmentWorkDispatchPort.CandidatePhysical(c.customerId(),c.selection().planPalletId())).distinct().toList();
var identities=candidates.stream().filter(c->c.selection().sourceType()==DispatchTypes.SourceType.INBOUND_PLAN && c.selection().planPalletId()!=null).map(c->new AppointmentWorkDispatchPort.CandidatePhysical(c.customerId(),c.selection().planPalletId())).distinct().toList();
var result=new LinkedHashMap<AppointmentWorkDispatchPort.CandidatePhysical,AppointmentWorkDispatchPort.CandidateBatches>();
for(int offset=0;offset<identities.size();offset+=1000)result.putAll(appointmentProvider.candidateBatches(identities.subList(offset,Math.min(offset+1000,identities.size()))));
return result;
}
/** 显式映射原批次版本;本草稿有效占用不作为所选来源自身的阻断理由。 */
private Candidate projectCandidate(Candidate c,Map<AppointmentWorkDispatchPort.CandidatePhysical,AppointmentWorkDispatchPort.CandidateBatches> batches) {
if(c.selection().sourceType()!=DispatchTypes.SourceType.INBOUND_PLAN)return c;
if(c.selection().planPalletId()==null) {
if(!"FBA".equals(c.destination().type()))return c;
return new Candidate(c.selection(),c.sourceKey(),c.sourceVersion(),c.customerId(),c.customerName(),c.warehouseId(),c.warehouseCode(),c.warehouseName(),c.orderNo(),c.inboundCode(),c.planNo(),c.palletNo(),c.containerNo(),c.destination(),c.availablePackages(),c.suggestedWeightKg(),c.suggestedVolumeCbm(),c.measurementSource(),List.of(),List.of("本票FBA货物缺少可核对的正式预约来源,请先确认送仓时间"),c.selectionGroup());
@@ -5974,7 +5974,7 @@ components:
required:
- idempotencyKey
- warehouseId
description: 同一起运仓,允许空单或多客户、多目的仓。FBA货物须先取得覆盖本客户、来源和箱数的正式预约时间才能选入,不能等提交审核才检查。客户和来源归属由服务端解析,临时资源不生成假档案。
description: 同一起运仓,允许空单或多客户、多目的仓。普通入仓FBA货物须先取得覆盖本客户、来源和箱数的正式预约时间才能选入,不能等提交审核才检查;换标和退货独立来源保留原流程。客户和来源归属由服务端解析,临时资源不生成假档案。
DispatchAddCargo:
type: object
x-java-record: DispatchCommands.AddCargo
@@ -129,12 +129,17 @@ class DispatchDraftPersistenceTest {
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock",Integer.class)).isZero();
assertThat(service.drafts(owner).get(0).sources()).isEmpty();
}
/** FBA来源尚不能关联正式预约时不得借其他来源类型绕过确认门禁。 */
@Test void fbaWithoutAuthoritativeAppointmentSourceCannotEnterDraft() {
var state=empty(1);var choice=new DispatchCommands.CargoSelection(SourceType.RELABEL,null,null,"RL-1",1001L,"version-1");choices.put(1L,choice);
/** 本轮门禁限普通入仓,独立换标和退货FBA保留原排车流程。 */
@ParameterizedTest
@org.junit.jupiter.params.provider.EnumSource(value=SourceType.class,names={"RELABEL","RETURN_RECEIPT"})
void independentFbaKeepsExistingDraftFlow(SourceType sourceType) {
var state=empty(1);var choice=sourceType==SourceType.RELABEL
?new DispatchCommands.CargoSelection(sourceType,null,null,"RL-1",1001L,"version-1")
:new DispatchCommands.CargoSelection(sourceType,null,1L,null,1001L,"version-1");choices.put(1L,choice);
doAnswer(call->{var resolved=whole(choice);resolved.candidate().destinationType("FBA");return List.of(resolved);}).when(sources).lockDraftSources(anyString(),anyList());
assertThatThrownBy(()->select(state,0,1)).hasMessageContaining("缺少可核对的正式预约来源");
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock",Integer.class)).isZero();
assertThat(select(state,0,1).sources()).hasSize(1);
assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock",Integer.class)).isOne();
verify(appointmentProvider,never()).candidateBatches(anyList());
}
/** 新实例只依赖数据库命令结果,关闭内存对象也能恢复创建及原布局结果。 */
@Test void restartRecoversOriginalResultsAndRejectsChangedActorVersionOrPayload(){
@@ -540,8 +545,8 @@ class DispatchDraftPersistenceTest {
.returnReceiptId(selection.sourceType()==SourceType.RETURN_RECEIPT?sourceId:null)
.customerId(sourceId%2==0?"102":"101").customerName("客户"+sourceId).overseasWarehouseId("92")
.overseasWarehouseCode("WH").overseasWarehouseName("海外仓").packageCount(10).palletNo("P-"+pallet)
.sourceStage(selection.sourceType()==SourceType.RELABEL?"RELABEL":"RECEIVED").destinationType(selection.sourceType()==SourceType.INBOUND_PLAN?"FBA":"PRIVATE")
.destinationWarehouseCode(destination).fbaWarehouseCode(selection.sourceType()==SourceType.INBOUND_PLAN?destination:null);
.sourceStage(selection.sourceType()==SourceType.RELABEL?"RELABEL":"RECEIVED").destinationType("FBA")
.destinationWarehouseCode(destination).fbaWarehouseCode(destination);
return new DispatchService.WholeSource(row,selection,DispatchService.wholeSourceKey(selection),selection.sourceVersion());
}
/** 受控事实按原来源类型及服务器版本产生完整组,不把组装责任交给前端。 */