perf(dispatch): batch candidate stock and receipt version reads
This commit is contained in:
+68
-5
@@ -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<String,String>();
|
||||
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<Long> ids=call.getArgument(0);return List.of(f.candidates.get(ids.get(0)).customerId());});
|
||||
when(plans.readSelected(anyString(),anyList())).thenAnswer(call->{List<Long> ids=call.getArgument(1);return List.of(f.candidates.get(ids.get(0)));});
|
||||
when(plans.readSelected(anyString(),anyList())).thenAnswer(call->{List<Long> 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));
|
||||
|
||||
Reference in New Issue
Block a user