diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java index a3be9c524..b88eb00dd 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java @@ -11,6 +11,8 @@ public interface DispatchSourceMapper { long allocatedPackages, long pickedPackages, long version) { } record Receipt(long id, long orderId, long inventoryPositionId, long packages, String inboundCode) { } record ReturnedReceipt(long id,long orderId,long inventoryPositionId,long packages) { } + record PalletReceipt(long palletId,long id,long orderId,long inventoryPositionId,long packages,String inboundCode) { } + record PalletReturnedReceipt(long palletId,long id,long orderId,long inventoryPositionId,long packages) { } record Claim(Long id, Long dispatchId, String draftToken) { } record RelabelBasis(String status,String fulfillmentStatus,LocalDateTime updatedAt,LocalDateTime customerConfirmedAt) { } record NativeApplication(String customerId,String relabelNo) { } @@ -112,6 +114,27 @@ public interface DispatchSourceMapper { @Select("SELECT r.id,r.order_id AS orderId,r.inventory_position_id AS inventoryPositionId,r.received_packages AS packages FROM ll_return_stock_receipt r JOIN ll_inventory_position p ON p.id=r.inventory_position_id AND p.customer_id=r.customer_id WHERE r.customer_id=#{customer} AND p.pallet_id=#{pallet} ORDER BY r.id") List returnedReceipts(@Param("customer")String customer,@Param("pallet")Long pallet); + /** 同客户整页实托一次读齐库存版本,仍保留原仓身份供服务校验。 */ + @Select("") + @Options(useCache=false,flushCache=Options.FlushCachePolicy.TRUE) + List stocks(@Param("customer")String customer,@Param("pallets")List pallets); + + /** 批读保留托盘身份和原实收顺序,拆分后与单托版本依据完全相同。 */ + @Select("") + List palletReceipts(@Param("customer")String customer,@Param("pallets")List pallets); + + /** 退回实收按客户与当前实托位置批读,不混入另一客户的同托记录。 */ + @Select("") + List palletReturnedReceipts(@Param("customer")String customer,@Param("pallets")List pallets); + /** 只使本客户本完整来源键的到期草稿成员失效,物理键由独立物理层管理。 */ @Update("UPDATE ll_dispatch_pallet_lock SET status='EXPIRED',active_key=NULL,updated_at=#{at} " + "WHERE status='ACTIVE' AND expires_at IS NOT NULL AND expires_at<=#{at} AND customer_id=#{customer} AND source_pallet_key=#{key}") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java index 08d3261f6..0e90dcd0b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java @@ -75,6 +75,24 @@ public class DispatchSourceRepository { public List receipts(String customer, Long pallet) { return source.receipts(customer,pallet); } /** 同一实托的退回实收不得从来源证明丢失。 */ public List returnedReceipts(String customer,Long pallet){return source.returnedReceipts(customer,pallet);} + /** 一条来源的库存与实收版本依据;无实托的在途计划三项均为空。 */ + public record SourceFacts(List stock,List receipts, + List returnedReceipts) { } + /** 同客户的候选托盘只用三次查询;拆分顺序与原单托查询一致,不跨请求缓存。 */ + public java.util.Map sourceFacts(String customer,List pallets) { + if(pallets.isEmpty())return java.util.Map.of(); + var stocks=source.stocks(customer,pallets).stream().collect(java.util.stream.Collectors.groupingBy(DispatchSourceMapper.Stock::palletId)); + var receipts=source.palletReceipts(customer,pallets).stream().collect(java.util.stream.Collectors.groupingBy( + DispatchSourceMapper.PalletReceipt::palletId,java.util.stream.Collectors.mapping( + row->new DispatchSourceMapper.Receipt(row.id(),row.orderId(),row.inventoryPositionId(),row.packages(),row.inboundCode()),java.util.stream.Collectors.toList()))); + var returned=source.palletReturnedReceipts(customer,pallets).stream().collect(java.util.stream.Collectors.groupingBy( + DispatchSourceMapper.PalletReturnedReceipt::palletId,java.util.stream.Collectors.mapping( + row->new DispatchSourceMapper.ReturnedReceipt(row.id(),row.orderId(),row.inventoryPositionId(),row.packages()),java.util.stream.Collectors.toList()))); + var result=new java.util.LinkedHashMap(); + for(var pallet:pallets)result.put(pallet,new SourceFacts(List.copyOf(stocks.getOrDefault(pallet,List.of())), + List.copyOf(receipts.getOrDefault(pallet,List.of())),List.copyOf(returned.getOrDefault(pallet,List.of())))); + return java.util.Collections.unmodifiableMap(result); + } /** 释放已到期草稿键。 */ public int expire(String customer, String key, LocalDateTime at) { return source.expire(customer,key,at); } /** 锁定当前活动资源占用。 */ diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java index 7c5c760be..556ef8c14 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java @@ -32,6 +32,7 @@ import static com.baiyue.lastleg.dispatch.port.DispatchContracts.*; /** 真整车创建、加货与查询;客户只属于明细,不属于整车头。 */ @Service public class DispatchAggregateService implements DispatchExecutionPort,DispatchAppointmentAmendmentPort { + private static final org.slf4j.Logger LOG=org.slf4j.LoggerFactory.getLogger(DispatchAggregateService.class); @jakarta.annotation.Resource private DispatchNumberGenerator numbers; @jakarta.annotation.Resource private DispatchAppointmentAllocations appointments; @jakarta.annotation.Resource private DispatchPhysicalClaimService physicalClaims; @@ -149,9 +150,13 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA if(query.planIds()!=null && (query.planIds().size()>500 || query.planIds().stream().anyMatch(id->id==null || id<=0))) throw bad("计划筛选不合法"); var normalized=new DispatchPlanningModels.CandidateQuery(query.warehouseId(),query.planIds(),trim(query.orderNo()), trim(query.destinationWarehouseCode()),trim(query.sourceStage()),trim(query.draftToken())); + long started=System.nanoTime(); var rows=sourceRules.wholeCandidates(actor.operatorId(),normalized); + long candidatesRead=System.nanoTime(); var groups=sourceRules.selectionGroups(rows); + long groupsRead=System.nanoTime(); var batchFacts=appointments.candidates(rows); + long batchesRead=System.nanoTime(); var result=new ArrayList(); for(var row:rows) { SourceType type=Set.of("RELABEL","RETURN_RELABEL").contains(Objects.toString(row.sourceStage(),""))?SourceType.RELABEL: @@ -181,6 +186,12 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA row.overseasWarehouseCode(),row.overseasWarehouseName(),row.orderNo(),row.inboundCode(),row.planNo(),row.palletNo(),row.containerNo(), destination,row.packageCount()==null?0:row.packageCount(),row.grossWeightKg(),row.volumeCbm(),"SOURCE_SUGGESTION",List.copyOf(references),List.copyOf(reasons),group)); } + long finished=System.nanoTime(); + // 慢查询只记录阶段耗时和条数,不记录客户、订单、仓库或草稿凭据。 + if(finished-started>=1_000_000_000L)LOG.warn( + "dispatch_candidates_slow count={} candidatesMs={} groupsMs={} appointmentsMs={} assembleMs={} totalMs={}", + rows.size(),(candidatesRead-started)/1_000_000L,(groupsRead-candidatesRead)/1_000_000L, + (batchesRead-groupsRead)/1_000_000L,(finished-batchesRead)/1_000_000L,(finished-started)/1_000_000L); return new DispatchPlanningModels.CandidateList(List.copyOf(result),Instant.now().truncatedTo(ChronoUnit.MILLIS),true); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java index 58626665d..ff2f8b705 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java @@ -4,6 +4,7 @@ import com.baiyue.lastleg.dispatch.entity.DispatchItem; import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper; import com.baiyue.lastleg.dispatch.mapper.DispatchDraftMapper; import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate; +import com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository.SourceFacts; import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper; import jakarta.annotation.Resource; import org.springframework.http.HttpStatus; @@ -34,16 +35,64 @@ public class DispatchService { /** 同一完整组只解析一次,隐藏的真实成员也随服务器组返回。 */ public Map selectionGroups(List rows) { + var reads=new CandidateReadScope(rows); var result=new LinkedHashMap(); for(var row:rows) { String key=wholeSourceKey(selection(row,null)); if(result.containsKey(key))continue; - var group=sourceGroup(row,false); + var group=sourceGroup(row,false,reads); for(var source:group.view().sources())result.put(source.sourceKey(),group.view()); } return Collections.unmodifiableMap(result); } + /** 仅当前候选事务内批读普通来源;写入和锁内校验继续逐组读取当前事实。 */ + private final class CandidateReadScope { + private final Map> loaded=new HashMap<>(); + private final Map>> available=new HashMap<>(); + private final Map> sourceFacts=new HashMap<>(); + + /** 同页同仓托盘一次读取,避免每一行再次执行完整计划和退回来源投影。 */ + private CandidateReadScope(List rows) { + var pallets=new TreeMap>(); + for(var row:rows)if(row.warehousePalletId()!=null) + pallets.computeIfAbsent(row.overseasWarehouseId(),key->new TreeSet<>()).add(row.warehousePalletId()); + pallets.forEach((warehouse,ids)->read(warehouse,List.copyOf(ids))); + loadFacts(rows); + } + + /** 换标闭包发现页外实托时补读缺失集合,不拿筛选后的页面充当完整组成。 */ + private List read(String warehouse,List pallets) { + var known=loaded.computeIfAbsent(warehouse,key->new HashSet<>()); + var byPallet=available.computeIfAbsent(warehouse,key->new HashMap<>()); + var missing=pallets.stream().filter(id->!known.contains(id)).distinct().sorted().toList(); + if(!missing.isEmpty()) { + var rows=wholeSources.physicalAvailable(warehouse,missing); + for(var row:rows) + byPallet.computeIfAbsent(row.warehousePalletId(),key->new ArrayList<>()).add(row); + loadFacts(rows); + known.addAll(missing); + } + return pallets.stream().flatMap(id->byPallet.getOrDefault(id,List.of()).stream()).toList(); + } + + /** 只补齐本请求尚未读取的客户实托,隐藏组成员也必须有完整版本依据。 */ + private void loadFacts(Collection rows) { + var missing=new TreeMap>(); + for(var row:rows)if(row.warehousePalletId()!=null + && !sourceFacts.getOrDefault(row.customerId(),Map.of()).containsKey(row.warehousePalletId())) + missing.computeIfAbsent(row.customerId(),key->new TreeSet<>()).add(row.warehousePalletId()); + missing.forEach((customer,pallets)->sourceFacts.computeIfAbsent(customer,key->new HashMap<>()) + .putAll(wholeSources.sourceFacts(customer,List.copyOf(pallets)))); + } + + /** 在途计划没有库存身份,不向数据库发出三个必为空的实收查询。 */ + private SourceFacts facts(DispatchPalletCandidate row) { + return row.warehousePalletId()==null?new SourceFacts(List.of(),List.of(),List.of()): + Objects.requireNonNull(sourceFacts.getOrDefault(row.customerId(),Map.of()).get(row.warehousePalletId()),"实托版本依据缺失"); + } + } + /** 草稿单次操作先从实际入口解析完整组,不让前端循环单来源凑整托。 */ public SelectionGroup selectionGroup(String warehouse,DispatchCommands.CargoSelection requested) { var row=readSelection(warehouse,requested);return sourceGroup(row,false).view(); @@ -78,6 +127,11 @@ public class DispatchService { /** 实托→申请→整申请实托求固定点;未证实来源只阻断,不补造第一客户或伪业务号。 */ private SourceGroup sourceGroup(DispatchPalletCandidate seed,boolean locked) { + return sourceGroup(seed,locked,null); + } + + /** 只读候选复用同事务批读,单组和写入路径不复用候选读取作用域。 */ + private SourceGroup sourceGroup(DispatchPalletCandidate seed,boolean locked,CandidateReadScope reads) { var rows=new TreeMap();rows.put(wholeSourceKey(selection(seed,null)),seed); var pallets=new TreeSet();if(seed.warehousePalletId()!=null)pallets.add(seed.warehousePalletId()); var applications=new TreeMap();var loaded=new HashSet();var reasons=new LinkedHashSet(); @@ -111,7 +165,8 @@ public class DispatchService { // 原生退货的资格由其真实箱生产者负责;普通位置提供者明确提示走原生分支。 reasons.removeIf(reason->nativeNos.stream().anyMatch(no->reason.startsWith(no+":"))); if(!pallets.isEmpty()) { - var available=wholeSources.physicalAvailable(seed.overseasWarehouseId(),List.copyOf(pallets)); + var available=reads==null?wholeSources.physicalAvailable(seed.overseasWarehouseId(),List.copyOf(pallets)): + reads.read(seed.overseasWarehouseId(),List.copyOf(pallets)); if(available.size()>1000)reasons.add("实托可用来源超过完整核验上限"); var buckets=available.stream().filter(r->r.packageCount()!=null && r.packageCount()>0).collect(Collectors.groupingBy(r->r.customerId()+":"+r.warehousePalletId(),TreeMap::new,Collectors.toList())); for(var entry:buckets.entrySet()) { @@ -132,7 +187,8 @@ public class DispatchService { reasons.add("实托还有未完整选择或重复计入的真实可用库存来源"); } } - var bases=new TreeMap();for(var entry:rows.entrySet())bases.put(entry.getKey(),baseSourceVersion(entry.getValue(),entry.getKey(),false)); + if(reads!=null)reads.loadFacts(rows.values()); + var bases=new TreeMap();for(var entry:rows.entrySet())bases.put(entry.getKey(),baseSourceVersion(entry.getValue(),entry.getKey(),reads)); String version=wholeHashes.source(Arrays.asList(bases,facts==null?null:facts.compositionVersion(),pallets)); var versions=new TreeMap();bases.forEach((key,basis)->versions.put(key,wholeHashes.source(List.of(basis,version)))); var sources=new ArrayList(); @@ -399,7 +455,7 @@ public class DispatchService { } /** 单来源基础摘要不递归包含组版本,组版本再绑定全部成员基础摘要。 */ - private String baseSourceVersion(DispatchPalletCandidate row,String key,boolean lockStock) { + private String baseSourceVersion(DispatchPalletCandidate row,String key,CandidateReadScope reads) { var facts = new java.util.TreeMap(); facts.put("sourceKey",key); facts.put("customerId",row.customerId()); facts.put("warehouseId",row.overseasWarehouseId()); facts.put("planPalletId",row.planPalletId()); facts.put("returnReceiptId",row.returnReceiptId()); @@ -411,12 +467,15 @@ public class DispatchService { facts.put("grossWeightKg",row.grossWeightKg()); facts.put("volumeCbm",row.volumeCbm()); facts.put("cartonMarks",row.relabelCartonMarks()); if(key.startsWith("RELABEL:")) facts.put("relabelBasis",wholeSources.relabelBasis(key.substring(8,key.lastIndexOf(':')))); - var stock=wholeSources.stock(row.customerId(),row.warehousePalletId(),lockStock); + var basis=row.warehousePalletId()==null?new SourceFacts(List.of(),List.of(),List.of()): + reads!=null?reads.facts(row):new SourceFacts(wholeSources.stock(row.customerId(),row.warehousePalletId(),false), + wholeSources.receipts(row.customerId(),row.warehousePalletId()),wholeSources.returnedReceipts(row.customerId(),row.warehousePalletId())); + var stock=basis.stock(); if(stock.stream().anyMatch(position->!Objects.equals(row.overseasWarehouseId(),position.warehouseId()))) throw new ResponseStatusException(HttpStatus.CONFLICT,"实托库存起运仓不一致,请核对来源"); facts.put("stock",stock); - facts.put("receipts",wholeSources.receipts(row.customerId(),row.warehousePalletId())); - facts.put("returnedReceipts",wholeSources.returnedReceipts(row.customerId(),row.warehousePalletId())); + facts.put("receipts",basis.receipts()); + facts.put("returnedReceipts",basis.returnedReceipts()); return wholeHashes.source(facts); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java index 446d3e332..92e6eda23 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java @@ -13,6 +13,67 @@ import static org.mockito.ArgumentMatchers.*; /** 候选投影在此受控,来源订单/仓/库存版本/实收组成与锁序均经实际MyBatis查询。 */ class DispatchWholeSourcePersistenceTest { + /** 同页普通来源批读不随托盘逐行重复,完整组与版本须和单组权威读取完全一致。 */ + @org.junit.jupiter.params.ParameterizedTest + @org.junit.jupiter.params.provider.ValueSource(booleans={true,false}) + void batchesPageSourceReadsWithoutChangingGroupsOrReusingThemAcrossRequests(boolean sameCustomer) throws Exception { + var fixture=new Sources(sameCustomer);var selections=fixture.selections(); + var first=fixture.rules.selectionGroup("9",selections.get(0)); + var second=fixture.rules.selectionGroup("9",selections.get(1)); + clearInvocations(fixture.sourceMapper,fixture.candidateMapper); + var rows=fixture.f.candidates.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).toList(); + var groups=fixture.f.tx.execute(status->fixture.rules.selectionGroups(rows)); + assertThat(groups.get("PLAN:1")).isEqualTo(first); + assertThat(groups.get("PLAN:2")).isEqualTo(second); + verify(fixture.sourceMapper,times(1)).physicalPlans("9",List.of(501L,502L)); + verify(fixture.sourceMapper,times(1)).physicalReturns("9",List.of(501L,502L)); + if(sameCustomer) { + verify(fixture.candidateMapper,times(1)).readSelected("101",List.of(1L,2L)); + verify(fixture.candidateMapper,never()).readSelected("101",List.of(1L)); + } + var customers=sameCustomer?Map.of("101",List.of(501L,502L)):Map.of("101",List.of(501L),"202",List.of(502L)); + customers.forEach((customer,pallets)->{ + verify(fixture.sourceMapper,times(1)).stocks(customer,pallets); + verify(fixture.sourceMapper,times(1)).palletReceipts(customer,pallets); + verify(fixture.sourceMapper,times(1)).palletReturnedReceipts(customer,pallets); + }); + verify(fixture.sourceMapper,never()).stock(anyString(),anyLong(),anyBoolean()); + verify(fixture.sourceMapper,never()).receipts(anyString(),anyLong()); + verify(fixture.sourceMapper,never()).returnedReceipts(anyString(),anyLong()); + verify(fixture.sourceMapper,never()).physicalPlans("9",List.of(501L)); + fixture.f.db.update("UPDATE ll_inbound_stock_receipt SET received_packages=4 WHERE id=1"); + var refreshed=fixture.f.tx.execute(status->fixture.rules.selectionGroups(rows)); + assertThat(refreshed.get("PLAN:1").groupVersion()).isNotEqualTo(first.groupVersion()); + verify(fixture.sourceMapper,times(2)).physicalPlans("9",List.of(501L,502L)); + } + /** 未实收计划没有实托,整页版本生成不能逐条发空库存和实收查询。 */ + @Test void plannedCandidatesNeedNoPhysicalFactQueries() throws Exception { + var fixture=new Sources(true); + var rows=fixture.f.candidates.values().stream().map(row->row.warehousePalletId(null).sourceStage("PLAN")).toList(); + var expected=new HashMap(); + for(var row:rows)expected.put("PLAN:"+row.planPalletId(),fixture.rules.wholeSourceVersion(row,"PLAN:"+row.planPalletId(),false)); + clearInvocations(fixture.sourceMapper,fixture.candidateMapper); + var groups=fixture.f.tx.execute(status->fixture.rules.selectionGroups(rows)); + for(var entry:expected.entrySet())assertThat(groups.get(entry.getKey()).sources().get(0).selection().sourceVersion()).isEqualTo(entry.getValue()); + verifyNoInteractions(fixture.sourceMapper,fixture.candidateMapper); + } + /** 同实托不同客户的收货依据必须隔离,真实退回实收仍参加摘要且保持原顺序。 */ + @Test void batchFactsMatchIndividualQueriesForSharedPalletCustomersAndReturns() throws Exception { + var fixture=new Sources(); + fixture.f.db.update("INSERT INTO ll_inventory_position VALUES(3,'202',3,501,'PALLET-501','9',2,0,0,0,1)"); + fixture.f.db.update("INSERT INTO ll_inbound_stock_receipt VALUES(3,'202','IN-OTHER',9002,3,2,501)"); + fixture.f.db.update("INSERT INTO ll_return_stock_receipt VALUES(5,'101',1,9001,1,2)"); + fixture.f.db.update("INSERT INTO ll_return_stock_receipt VALUES(6,'202',3,9002,3,1)"); + var repository=new DispatchSourceRepository(fixture.candidateMapper,mock(ReturnDispatchCandidateMapper.class),fixture.sourceMapper); + for(var customer:List.of("101","202")) { + var batch=repository.sourceFacts(customer,List.of(501L,502L)); + for(var pallet:List.of(501L,502L)) { + assertThat(batch.get(pallet).stock()).isEqualTo(repository.stock(customer,pallet,false)); + assertThat(batch.get(pallet).receipts()).isEqualTo(repository.receipts(customer,pallet)); + assertThat(batch.get(pallet).returnedReceipts()).isEqualTo(repository.returnedReceipts(customer,pallet)); + } + } + } /** 同仓多客户先锁齐来源订单,随后稳定锁计划和库存,不以MIN展示订单替代组成。 */ @Test void resolvesMultipleCustomersAndLocksAllSourceOrdersBeforeInventory() throws Exception { var fixture=new Sources();var selections=fixture.selections();clearInvocations(fixture.sourceMapper); @@ -43,10 +104,12 @@ class DispatchWholeSourcePersistenceTest { /** 实际来源表保持独立客户;只有复杂候选投影查询在本用例隔离。 */ private static final class Sources { final DispatchAggregatePersistenceTest.Fixture f=new DispatchAggregatePersistenceTest.Fixture(); - final DispatchSourceMapper sourceMapper;final DispatchService rules=new DispatchService(); + final DispatchSourceMapper sourceMapper;final DispatchCandidateMapper candidateMapper;final DispatchService rules=new DispatchService(); /** 构造真实订单组成和库存版本,不能由候选响应替代这些数据库事实。 */ - Sources() throws Exception { - f.source(1,"101",501,"ONT8",3);f.source(2,"202",502,"LAX9",5); + Sources() throws Exception { this(false); } + /** 同客户多托用于核对候选批读次数,跨客户夹具继续覆盖原隔离与锁序。 */ + Sources(boolean sameCustomer) throws Exception { + f.source(1,"101",501,"ONT8",3);f.source(2,sameCustomer?"101":"202",502,"LAX9",5); f.db.execute("ALTER TABLE ll_order ADD detained INT DEFAULT 0"); f.db.execute("ALTER TABLE ll_order ADD overseas_warehouse_id VARCHAR(64)"); f.db.execute("CREATE TABLE ll_inbound_plan(id BIGINT PRIMARY KEY,customer_id VARCHAR(64),overseas_warehouse_id VARCHAR(64),source_order_id BIGINT)"); @@ -60,9 +123,9 @@ class DispatchWholeSourcePersistenceTest { f.db.update("INSERT INTO ll_inventory_position VALUES(?,?,?,?,?,?,?,0,0,0,1)",id,row.customerId(),id,row.warehousePalletId(),row.palletNo(),"9",row.packageCount()); f.db.update("INSERT INTO ll_inbound_stock_receipt VALUES(?,?,?,?,?,?,?)",id,row.customerId(),row.inboundCode(),row.orderId(),id,row.packageCount(),row.warehousePalletId()); } - var plans=mock(DispatchCandidateMapper.class); + var plans=mock(DispatchCandidateMapper.class);candidateMapper=plans; when(plans.findCustomerScopesByPlanPalletIds(anyList())).thenAnswer(call->{List ids=call.getArgument(0);return List.of(f.candidates.get(ids.get(0)).customerId());}); - when(plans.readSelected(anyString(),anyList())).thenAnswer(call->{List ids=call.getArgument(1);return List.of(f.candidates.get(ids.get(0)));}); + when(plans.readSelected(anyString(),anyList())).thenAnswer(call->{List ids=call.getArgument(1);return ids.stream().map(f.candidates::get).toList();}); sourceMapper=spy(f.session.getMapper(DispatchSourceMapper.class)); var sources=new DispatchSourceRepository(plans,mock(ReturnDispatchCandidateMapper.class),sourceMapper); ReflectionTestUtils.setField(rules,"wholeSources",sources);ReflectionTestUtils.setField(rules,"wholeHashes",new DispatchCommandHasher(f.json));