This commit is contained in:
2026-10-21 17:22:19 +08:00
parent 0c6c55f6f1
commit 0976b6ef0d
23 changed files with 577 additions and 286 deletions
@@ -3,8 +3,9 @@ package com.baiyue.lastleg.dispatch.service;
import com.baiyue.lastleg.dispatch.mapper.*;
import com.baiyue.lastleg.fleet.ownedbusiness.service.*;
import com.baiyue.lastleg.fleet.ownedbusiness.gateway.OwnedBusinessFileGateway;
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.BindPhoto;
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.PhotoSlot;
import com.baiyue.lastleg.fleet.ownedbusiness.port.DispatchLoadingPhotoPort.BindPhoto;
import com.baiyue.lastleg.fleet.ownedbusiness.port.DispatchLoadingPhotoPort.Documents;
import com.baiyue.lastleg.fleet.ownedbusiness.port.DispatchLoadingPhotoPort.Slot;
import com.baiyue.lastleg.fleet.ownedbusiness.mapper.*;
import com.baiyue.lastleg.fleet.ownedbusiness.repository.OwnedBusinessRepository;
import java.nio.file.*;
@@ -42,7 +43,7 @@ class DispatchLoadingDeparturePersistenceTest {
assertThat(f.f.db.queryForObject("SELECT COUNT(*) FROM ll_delivery_order",Long.class)).isZero();
var fifth=f.bind(4,new Actor("picker",ActorMode.PDA,"9"));assertThat(fifth.stage()).isEqualTo("DELIVERING");
assertThat(fifth.complete()).isTrue();assertThat(fifth.photos()).hasSize(5);
var replayPhoto=f.f.tx.execute(s->f.documents.bind(new Actor("picker",ActorMode.PDA,"9"),f.number,PhotoSlot.RECEIPT,new BindPhoto("4","photo-bind-4","1004")));
var replayPhoto=f.f.tx.execute(s->f.documents.bind(new Actor("picker",ActorMode.PDA,"9"),f.number,Slot.RECEIPT,new BindPhoto("4","photo-bind-4","1004")));
assertThat(replayPhoto).isEqualTo(fifth);
assertThat(result.counts().shippedPackageCount()).isZero();assertThat(result.result()).isInstanceOf(DispatchResults.Loaded.class);
assertThat(f.f.db.queryForList("SELECT status FROM ll_outbound_order",String.class)).containsExactly("OUTBOUNDED","OUTBOUNDED");
@@ -103,13 +104,16 @@ class DispatchLoadingDeparturePersistenceTest {
verify(f.delivery,never()).prepareForDeparture(any(),any(),anyString());verify(f.sourceWorkflow,never()).departWholeSources(any(),any(),any(),any());
}
/** 资料上传必须在装车后且属于绑定仓,平台只读权限不能保存照片。 */
/** 装车中可上传,装车前与跨仓拒绝,平台只读权限不能保存照片。 */
@Test void loadingDocumentsEnforceStageWarehouseAndWritePermission() throws Exception {
authorize();var f=new Fixture();
assertThatThrownBy(()->f.documents.get(f.f.actor,f.number)).hasMessageContaining("装车完成后");
assertThat(f.documents.get(f.f.actor,f.number).stage()).isEqualTo("LOADING");
f.f.db.update("UPDATE ll_dispatch_order SET stage='BEFORE_LOADING'");
assertThatThrownBy(()->f.documents.get(f.f.actor,f.number)).hasMessageContaining("进入装车中后");
f.f.db.update("UPDATE ll_dispatch_order SET stage='LOADING'");
f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("complete")));
assertThatThrownBy(()->f.documents.get(new Actor("picker",ActorMode.PDA,"99"),f.number)).hasMessageContaining("当前绑定仓");
assertThatThrownBy(()->f.f.tx.execute(s->f.documents.bind(f.f.actor,f.number,PhotoSlot.FUEL_START,new BindPhoto("0","wrong-slot","1000")))).hasMessageContaining("装车照片框");
assertThatThrownBy(()->f.f.tx.execute(s->f.documents.bind(f.f.actor,f.number,null,new BindPhoto("0","wrong-slot","1000")))).hasMessageContaining("装车照片框");
SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("reader","","platform:overseas-warehouse:dispatch:list"));
assertThat(f.documents.get(f.f.actor,f.number).editable()).isFalse();
assertThatThrownBy(()->f.bind(0,f.f.actor)).hasMessageContaining("无装车资料操作权限");
@@ -124,7 +128,7 @@ class DispatchLoadingDeparturePersistenceTest {
var results=new ArrayList<Future<Boolean>>();
for(String key:List.of("admin-last","pda-last"))results.add(workers.submit(()->{
authorize();try {start.await(5,TimeUnit.SECONDS);var actor=key.startsWith("pda")?new Actor("picker",ActorMode.PDA,"9"):f.f.actor;
f.f.tx.execute(s->f.documents.bind(actor,f.number,PhotoSlot.RECEIPT,new BindPhoto("4",key,"1004")));return true;
f.f.tx.execute(s->f.documents.bind(actor,f.number,Slot.RECEIPT,new BindPhoto("4",key,"1004")));return true;
}catch(com.baiyue.common.core.exception.BizException expected){return false;}
finally {SecurityContextHolder.clearContext();}
}));
@@ -135,6 +139,50 @@ class DispatchLoadingDeparturePersistenceTest {
assertThat(f.f.repository.find(f.number).getStage()).isEqualTo("DELIVERING");
}
/** 资料先齐不能提前发车;确认装车后直接派送,原键返回相同最终结果。 */
@Test void photosReadyBeforeLoadingConfirmationDepartAndReplayOnce() throws Exception {
authorize();var f=new Fixture();
for(int i=0;i<5;i++)assertThat(f.bind(i,f.f.actor).stage()).isEqualTo("LOADING");
verify(f.delivery,never()).prepareForDeparture(any(),any(),anyString());
var result=f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("ready-complete")));
assertThat(result.action()).isEqualTo(Action.LOADING_COMPLETE);
assertThat(result.stage()).isEqualTo(Stage.DELIVERING);assertThat(result.counts().shippedPackageCount()).isEqualTo(8);
assertThat(result.result()).isInstanceOf(DispatchResults.Loaded.class);
var replay=f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("ready-complete")));
assertThat(replay.replayed()).isTrue();assertThat(replay.stage()).isEqualTo(Stage.DELIVERING);
assertThat(replay.recordVersion()).isEqualTo(result.recordVersion());assertThat(replay.result()).isEqualTo(result.result());
assertThat(f.f.tx.execute(s->f.f.service.command(f.f.actor,f.number,"ready-complete")).stage()).isEqualTo(Stage.DELIVERING);
verify(f.delivery,times(1)).prepareForDeparture(any(),any(),anyString());
verify(f.sourceWorkflow,times(1)).departWholeSources(any(),any(),any(),any());
}
/** 资料先齐时派送失败回滚本次装车确认,五张已保存照片保留供原请求重试。 */
@Test void readyDocumentsSurviveFailedLoadingAndOriginalRequestCanRetry() throws Exception {
authorize();var f=new Fixture();for(int i=0;i<5;i++)f.bind(i,f.f.actor);
doThrow(new IllegalStateException("模拟发运失败")).when(f.sourceWorkflow).departWholeSources(any(),any(),any(),any());
assertThatThrownBy(()->f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("retry-ready")))).hasMessageContaining("模拟发运失败");
assertThat(f.f.repository.find(f.number).getStage()).isEqualTo("LOADING");
assertThat(f.documents.get(f.f.actor,f.number).complete()).isTrue();
assertThat(f.f.db.queryForObject("SELECT COUNT(*) FROM ll_delivery_order",Long.class)).isZero();
assertThat(f.f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_command WHERE idempotency_key='retry-ready'",Long.class)).isZero();
doNothing().when(f.sourceWorkflow).departWholeSources(any(),any(),any(),any());
assertThat(f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("retry-ready"))).stage()).isEqualTo(Stage.DELIVERING);
}
/** 最后一张照片与装车确认同时提交,不论谁先拿父锁均只发车一次。 */
@Test void lastPhotoAndLoadingConfirmationCanFinishConcurrently() throws Exception {
authorize();var f=new Fixture();for(int i=0;i<4;i++)f.bind(i,f.f.actor);
var start=new CountDownLatch(1);var workers=Executors.newFixedThreadPool(2);
try {
var load=workers.submit(()->f.tryComplete(start,f.f.actor,"concurrent-load"));
var photo=workers.submit(()->{authorize();try {start.await(5,TimeUnit.SECONDS);return f.bind(4,new Actor("picker",ActorMode.PDA,"9"));}finally{SecurityContextHolder.clearContext();}});
start.countDown();assertThat(load.get(25,TimeUnit.SECONDS)).isTrue();assertThat(photo.get(25,TimeUnit.SECONDS)).isNotNull();
} finally {workers.shutdownNow();}
assertThat(f.f.repository.find(f.number).getStage()).isEqualTo("DELIVERING");
verify(f.delivery,times(1)).prepareForDeparture(any(),any(),anyString());
verify(f.sourceWorkflow,times(1)).departWholeSources(any(),any(),any(),any());
}
/** 配置资料与装车权限及PDA既有角色,不授予人工派送、发车权限。 */
private static void authorize(){SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("operator","",
"platform:overseas-warehouse:dispatch:list","platform:overseas-warehouse:dispatch:edit","platform:overseas-warehouse:dispatch:loading-complete","lastleg:pda:operate","ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR"));}
@@ -176,7 +224,6 @@ class DispatchLoadingDeparturePersistenceTest {
var hashes=new DispatchCommandHasher(f.json);
var sources=new DispatchSourceRepository(mock(DispatchCandidateMapper.class),mock(ReturnDispatchCandidateMapper.class),f.session.getMapper(DispatchSourceMapper.class));
var departure=new DispatchDepartureService(f.service,f.repository,sources,delivery,sourceWorkflow,f.fleet,ledger,hashes,f.json);
service=new DispatchLoadingDepartureService(f.service,delivery,departure,ledger,hashes);
f.session.getConfiguration().addMapper(OwnedBusinessMapper.class);
String schema=Files.readString(Path.of("src/main/resources/db/lastleg.sql"));
for(String suffix:List.of("record","command","history")) {
@@ -187,10 +234,12 @@ class DispatchLoadingDeparturePersistenceTest {
var records=new OwnedBusinessRepository(f.session.getMapper(OwnedBusinessMapper.class),mock(OwnedBusinessQueryMapper.class));
var codec=new OwnedBusinessSupport(f.json);var files=mock(OwnedBusinessFileGateway.class);
when(files.metadata(anyLong(),any(),anyLong())).thenAnswer(call->new OwnedBusinessFileGateway.Evidence(call.getArgument(2),"loading.png","image/png",12,"2026-10-21T00:00:00Z"));
documents=new DispatchLoadingDocumentService(f.service,service,records,new OwnedBusinessCommandService(records,codec),new OwnedBusinessProjection(codec),codec,files);
var photos=new DispatchLoadingPhotoAdapter(records,new OwnedBusinessCommandService(records,codec),new OwnedBusinessProjection(codec),codec,files);
service=new DispatchLoadingDepartureService(f.service,delivery,departure,ledger,hashes,photos);
documents=new DispatchLoadingDocumentService(f.service,service,photos);
}
/** 每张照片保留独立版本与幂等键,复用真实附加记录保存事务。 */
DispatchLoadingDocumentService.Documents bind(int index,Actor actor) {
Documents bind(int index,Actor actor) {
return f.tx.execute(s->documents.bind(actor,number,DispatchLoadingDocumentService.SLOTS.get(index),new BindPhoto(Integer.toString(index),"photo-bind-"+index,Long.toString(1000L+index))));
}
/** 每次请求沿用最初真实测量和版本,便于验证竞争及原键恢复。 */
@@ -23,6 +23,34 @@ import static org.mockito.Mockito.*;
/** 根版本、幂等、真实客户作业与扫码持久化;库存/归属应用命令在此明确模拟,另由原域直接测试验证。 */
class DispatchPickingPersistenceTest {
/** 预排两托10+10、实收9+11,首次扫码同步真实数量并保留原审核,原键重放不重复扣货。 */
@Test void receivedPalletCountsReplaceEstimatesAtomically() throws Exception {
var fixture=new Picking(true);fixture.receivedPlans(9,11);
var firstRequest=new DispatchCommands.Pick(fixture.version,"received-nine",11L,9,0,null,null);
var first=fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.item,firstRequest));
assertThat(fixture.f.db.queryForList("SELECT package_count FROM ll_dispatch_item ORDER BY customer_id",Integer.class)).containsExactly(9,11);
assertThat(fixture.f.db.queryForObject("SELECT package_count FROM ll_dispatch_order",Integer.class)).isEqualTo(20);
assertThat(fixture.f.db.queryForObject("SELECT review_state FROM ll_dispatch_order",String.class)).isEqualTo("APPROVED");
var replay=fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.item,firstRequest));
assertThat(replay.replayed()).isTrue();
var second=fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.secondItem,
new DispatchCommands.Pick(first.recordVersion(),"received-eleven",12L,11,0,null,null)));
assertThat(((DispatchResults.Picked)second.result()).pickingReconciled()).isTrue();
verify(fixture.inventory,times(2)).ship(anyString(),eq("picker"),any());
assertThat(fixture.f.db.queryForObject("SELECT SUM(available_packages) FROM ll_inventory_position",Long.class)).isZero();
}
/** 实收绑定后扣货失败必须同时回滚数量、绑定、总数和原审核。 */
@Test void receivedQuantityAndBindingRollBackWhenStockWriteFails() throws Exception {
var fixture=new Picking(true);fixture.receivedPlans(8,11);
doThrow(new IllegalStateException("库存命令失败")).when(fixture.inventory).ship(anyString(),anyString(),any());
assertThatThrownBy(()->fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.item,
new DispatchCommands.Pick(fixture.version,"receipt-rollback",11L,8,0,null,null)))).isInstanceOf(IllegalStateException.class);
assertThat(fixture.f.db.queryForList("SELECT package_count FROM ll_dispatch_item ORDER BY customer_id",Integer.class)).containsExactly(10,10);
assertThat(fixture.f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_item WHERE warehouse_pallet_id IS NOT NULL",Long.class)).isZero();
assertThat(fixture.f.db.queryForObject("SELECT package_count FROM ll_dispatch_order",Integer.class)).isEqualTo(20);
assertThat(fixture.f.db.queryForObject("SELECT record_version FROM ll_dispatch_order",Long.class)).isEqualTo(fixture.version);
}
/** 原键在后续版本仍恢复原结果,不能重复调用SHIP或创建第二条真实扫码。 */
@Test void sameKeyShipsOnceAndLastPickDoesNotConfirmLoading() throws Exception {
var fixture=new Picking();var request=new DispatchCommands.Pick(fixture.version,"pda-pick-original",11L,3,6,null,"实际扫码");
@@ -68,6 +96,7 @@ class DispatchPickingPersistenceTest {
final DispatchAggregatePersistenceTest.Fixture f=new DispatchAggregatePersistenceTest.Fixture();
final InventoryCommandService inventory=mock(InventoryCommandService.class);final DispatchPickOrderAttributionService attribution=mock(DispatchPickOrderAttributionService.class);
final DispatchPickingService service;final String number;final long item,version;final Long secondItem;
final DispatchSourceRepository sources;
final DispatchContracts.Actor actor=new DispatchContracts.Actor("picker",ActorMode.PDA,"9");
/** 建立真实客户来源、作业表、扫码表与可以参与回滚的库存行。 */
Picking() throws Exception {this(false);}
@@ -94,10 +123,32 @@ class DispatchPickingPersistenceTest {
doAnswer(call->{com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move move=call.getArgument(2);f.db.update("UPDATE ll_inventory_position SET available_packages=available_packages-?,version=version+1 WHERE id=? AND customer_id=?",move.packages(),move.positionId(),call.getArgument(0));return null;}).when(inventory).ship(anyString(),eq("picker"),any());
doAnswer(call->{String customer=call.getArgument(0);DispatchPdaPick pick=call.getArgument(1);long order="101".equals(customer)?9001L:9002L;long receipt="101".equals(customer)?1L:2L;f.db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,order_id,receipt_id,packages,product_quantity)VALUES(?,?,?,?,?,?,?)",pick.getId(),customer,pick.getId(),order,receipt,pick.getPackages(),pick.getProductQuantity());return List.of(new DispatchPickOrderAttributionService.OrderPackages(order,pick.getPackages()));})
.when(attribution).attribute(anyString(),any(DispatchPdaPick.class),isNull());
var sources=new DispatchSourceRepository(mock(DispatchCandidateMapper.class),mock(ReturnDispatchCandidateMapper.class),f.session.getMapper(DispatchSourceMapper.class));
sources=spy(new DispatchSourceRepository(mock(DispatchCandidateMapper.class),mock(ReturnDispatchCandidateMapper.class),f.session.getMapper(DispatchSourceMapper.class)));
var commands=new DispatchCommandLedger(new DispatchCommandRepository(f.session.getMapper(DispatchCommandMapper.class)),new DispatchCommandCodec(f.json));
service=new DispatchPickingService(f.service,f.repository,sources,new DispatchPickingRepository(session.getMapper(DispatchPdaTaskMapper.class),session.getMapper(DispatchPdaPickMapper.class),session.getMapper(InventoryPositionMapper.class),session.getMapper(DispatchItemMapper.class)),attribution,inventory,mock(ReturnRelabelDispatchStock.class),commands,new DispatchCommandHasher(f.json),f.json);
for(var field:List.of("physicalClaims","relabelSources","sourceRules"))org.springframework.test.util.ReflectionTestUtils.setField(service,field,org.springframework.test.util.ReflectionTestUtils.getField(f.service,field));
org.springframework.test.util.ReflectionTestUtils.setField(service,"fleet",f.fleet);
}
/** 来源查询在此隔离,实收同步仍使用真实主单、明细、库存及车次事务。 */
void receivedPlans(int first,int second) {
f.db.update("UPDATE ll_dispatch_item SET warehouse_pallet_id=NULL,source_stage='PLAN',package_count=10");
f.db.update("UPDATE ll_dispatch_order SET package_count=20");
var rules=(DispatchService)org.springframework.test.util.ReflectionTestUtils.getField(service,"sourceRules");
for(int n=1;n<=2;n++) {
long plan=n,pallet=500+n;String customer=n==1?"101":"102";int count=n==1?first:second;
f.candidates.get(plan).packageCount(count);
f.db.update("UPDATE ll_inventory_position SET available_packages=? WHERE id=?",count,10+n);
doReturn(List.of(f.candidates.get(plan))).when(sources).selectedPlan(customer,plan);
doReturn(List.of(9000+plan)).when(sources).sourceOrders(customer,null,plan);
doReturn(List.of(9000+plan)).when(sources).sourceOrders(customer,pallet,plan);
doReturn(plan).when(sources).lockPlan(customer,plan);
doReturn(1).when(sources).materialize(anyLong(),eq(customer),eq(plan),eq(pallet),anyLong(),any());
}
when(rules.selectionGroup(anyString(),any())).thenAnswer(call->{
var selected=(DispatchCommands.CargoSelection)call.getArgument(1);long plan=selected.planPalletId();var row=f.candidates.get(plan);
return new DispatchPlanningModels.SelectionGroup("group","version",List.of(new DispatchPlanningModels.SelectionGroupSource(
selected,"PLAN:"+plan,row.customerId(),row.customerName(),"INBOUND_PLAN","PLAN-"+plan,row.packageCount())),List.of(),true,List.of());
});
}
/** 使用实际实体属性建最小测试表,不修改或声称验证正式DDL。 */
private static String sqlType(Class<?> type){return type==Long.class?"BIGINT":type==Integer.class?"INT":type==LocalDateTime.class?"TIMESTAMP(3)":"VARCHAR(20000)";}
@@ -28,15 +28,23 @@ class DispatchPlanPdaReadPersistenceTest {
verify(f.sources,never()).materialize(anyLong(),anyString(),anyLong(),anyLong(),anyLong(),any());
verify(f.rules,never()).lockExecutionSources(anyString(),anyList());
}
/** 在途、预留托盘和实收数量漂移均不成为可拣库存。 */
/** 在途和来源身份变化均不成为可拣库存。 */
@Test void unreceivedOrChangedPlanHasNoPositions() throws Exception {
var f=new Fixture();var row=f.f.candidates.get(1L);
row.sourceStage("PLAN");assertThat(f.read().items().get(0).positions()).isEmpty();
row.sourceStage("RECEIVED");row.packageCount(2);assertThat(f.read().items().get(0).positions()).isEmpty();
row.packageCount(3);row.overseasWarehouseId("99");assertThat(f.read().items().get(0).positions()).isEmpty();
row.sourceStage("RECEIVED");row.overseasWarehouseId("99");assertThat(f.read().items().get(0).positions()).isEmpty();
row.overseasWarehouseId("9");row.customerId("other");assertThat(f.read().items().get(0).positions()).isEmpty();
verify(f.sources,never()).stock(anyString(),anyLong(),anyBoolean());
}
/** 自动预分配的10+10不约束实收9+11;查询给出实际数量且不改数据库。 */
@Test void plannedTenAndTenProjectsReceivedNineAndEleven() throws Exception {
var f=new Fixture(true);var before=f.dbState();
var task=f.read();
assertThat(task.items()).extracting(DispatchViews.PdaItem::scheduledPackages).containsExactly(9L,11L);
assertThat(task.items()).allSatisfy(i->assertThat(i.positions()).isNotEmpty());
assertThat(task.counts().packageCount()).isEqualTo(20);
assertThat(f.dbState()).isEqualTo(before);
}
/** 原计划不能吸收新的订单或未经原车审核的同托来源。 */
@Test void changedOrdersOrAdditionalPalletSourceBlocksFirstPick() throws Exception {
var f=new Fixture();
@@ -65,9 +73,13 @@ class DispatchPlanPdaReadPersistenceTest {
final DispatchService rules=mock(DispatchService.class);
final DispatchPdaQueryService queries;final String number;
/** 原车已审核,只清除自然收货尚未补写的实托字段。 */
Fixture() throws Exception {
var selection=f.source(1,"101",501,"ONT8",3);
var created=f.tx.execute(s->f.service.create(f.actor,f.configured("plan-pda-read",List.of(selection))));
Fixture() throws Exception {this(false);}
/** 同一客户两托先预排再实收,模拟计划拆分与真实入托数量不同。 */
Fixture(boolean twoPallets) throws Exception {
var selection=f.source(1,"101",501,"ONT8",twoPallets?10:3);
var selections=new java.util.ArrayList<DispatchCommands.CargoSelection>();selections.add(selection);
if(twoPallets)selections.add(f.source(2,"101",502,"ONT8",10));
var created=f.tx.execute(s->f.service.create(f.actor,f.configured("plan-pda-read",selections)));
f.approve(created);number=created.dispatchNo();
f.db.update("UPDATE ll_dispatch_item SET warehouse_pallet_id=NULL,source_stage='PLAN'");
f.db.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR(64),balance_id BIGINT,pallet_id BIGINT,overseas_warehouse_id VARCHAR(64),available_packages BIGINT,hold_packages BIGINT,allocated_packages BIGINT,picked_packages BIGINT)");
@@ -82,6 +94,21 @@ class DispatchPlanPdaReadPersistenceTest {
when(rules.selectionGroup(anyString(),any())).thenReturn(new DispatchPlanningModels.SelectionGroup(
"group","version",List.of(new DispatchPlanningModels.SelectionGroupSource(
selection,"PLAN:1","101","测试客户","INBOUND_PLAN","PLAN-1",3)),List.of(),true,List.of()));
if(twoPallets) {
for(int n=1;n<=2;n++) {
long plan=n,pallet=500+n;int count=n==1?9:11;
f.candidates.get(plan).packageCount(count);
when(sources.selectedPlan("101",plan)).thenReturn(List.of(f.candidates.get(plan)));
when(sources.sourceOrders("101",null,plan)).thenReturn(List.of(9000+plan));
when(sources.sourceOrders("101",pallet,plan)).thenReturn(List.of(9000+plan));
when(sources.stock("101",pallet,false)).thenReturn(List.of(new Stock(10+n,20+n,pallet,"9",count,0,0,0,1)));
}
when(rules.selectionGroup(anyString(),any())).thenAnswer(call->{
var selected=(DispatchCommands.CargoSelection)call.getArgument(1);long plan=selected.planPalletId();
return new DispatchPlanningModels.SelectionGroup("group","version",List.of(new DispatchPlanningModels.SelectionGroupSource(
selected,"PLAN:"+plan,"101","测试客户","INBOUND_PLAN","PLAN-"+plan,plan==1?9:11)),List.of(),true,List.of());
});
}
}
/** 读取正式PDA入口。 */
DispatchViews.PdaTask read(){return f.tx.execute(s->queries.task(new DispatchContracts.Actor("picker",ActorMode.PDA,"9"),number));}