fix(firstleg): retry chained remainder settlement
This commit is contained in:
@@ -169,6 +169,13 @@ public class PdaPickSettlementService {
|
|||||||
int sequence = splits.nextSplitSequence(parent.tenantId(), rootInboundId);
|
int sequence = splits.nextSplitSequence(parent.tenantId(), rootInboundId);
|
||||||
String remainderChildCode = childPackageCount > 0
|
String remainderChildCode = childPackageCount > 0
|
||||||
? childInboundCode(root.inboundCode(), sequence) : null;
|
? childInboundCode(root.inboundCode(), sequence) : null;
|
||||||
|
// Consume the current slice before inserting its successor so the single-active-remainder
|
||||||
|
// constraint is released. A local failure rolls this back; the Order split is idempotent on retry.
|
||||||
|
requireAffected(splits.markSourceLoaded(parent.tenantId(), parentInboundId, pickedPackageCount,
|
||||||
|
operatorId, sourceVersion), "来源分片状态");
|
||||||
|
if (parentInboundId != rootInboundId) {
|
||||||
|
requireAffected(splits.touchRootSplit(parent.tenantId(), rootInboundId, operatorId, rootVersion), "根单拆分状态");
|
||||||
|
}
|
||||||
if (remainderChildId != 0L) {
|
if (remainderChildId != 0L) {
|
||||||
requireAffected(splits.cloneChildWithPurpose(parent.tenantId(), parentInboundId, rootInboundId, remainderChildId,
|
requireAffected(splits.cloneChildWithPurpose(parent.tenantId(), parentInboundId, rootInboundId, remainderChildId,
|
||||||
orderSplit.remainderChild().orderId(), orderSplit.remainderChild().orderNo(),
|
orderSplit.remainderChild().orderId(), orderSplit.remainderChild().orderNo(),
|
||||||
@@ -198,11 +205,6 @@ public class PdaPickSettlementService {
|
|||||||
line.remainingPackageCount()), "子入库测量明细");
|
line.remainingPackageCount()), "子入库测量明细");
|
||||||
lineNo++;
|
lineNo++;
|
||||||
}
|
}
|
||||||
requireAffected(splits.markSourceLoaded(parent.tenantId(), parentInboundId, pickedPackageCount,
|
|
||||||
operatorId, sourceVersion), "来源分片状态");
|
|
||||||
if (parentInboundId != rootInboundId) {
|
|
||||||
requireAffected(splits.touchRootSplit(parent.tenantId(), rootInboundId, operatorId, rootVersion), "根单拆分状态");
|
|
||||||
}
|
|
||||||
requireAffected(splits.applyBatch(parent.tenantId(), batchId), "拆分批次生效");
|
requireAffected(splits.applyBatch(parent.tenantId(), batchId), "拆分批次生效");
|
||||||
for (MeasurementSplitLine line : measurementLines) {
|
for (MeasurementSplitLine line : measurementLines) {
|
||||||
requireAffected(splits.updateMeasurementItemForLoaded(parent.tenantId(),
|
requireAffected(splits.updateMeasurementItemForLoaded(parent.tenantId(),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
|
|||||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.InOrder;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.test.context.TestPropertySource;
|
import org.springframework.test.context.TestPropertySource;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||||
@@ -27,6 +28,7 @@ import static org.mockito.ArgumentMatchers.anyLong;
|
|||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
@@ -243,6 +245,51 @@ class PdaPickSettlementServiceTest {
|
|||||||
anyLong(), anyString(), anyString(), anyInt(), anyInt(), anyString(), anyLong(), anyString());
|
anyLong(), anyString(), anyString(), anyInt(), anyInt(), anyString(), anyLong(), anyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void consumesSecondSliceBeforeCreatingSingleNextRemainderFromRootSequence() {
|
||||||
|
LoadingPickItemEntity item = item(12L, 22L, 32L, 42L, 12, 5, "PARTIAL");
|
||||||
|
resolvedChild(item, 32L, 42L, 62L, 52L, 12, 2);
|
||||||
|
when(splits.nextSplitSequence(10001L, 31L)).thenReturn(2);
|
||||||
|
childWrites(32L, 42L, 62L, 52L, 5, 2);
|
||||||
|
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||||
|
new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 7)))));
|
||||||
|
|
||||||
|
service.settle(1L, 9L, "operator");
|
||||||
|
|
||||||
|
InOrder order = inOrder(splits);
|
||||||
|
order.verify(splits).markSourceLoaded(10001L, 32L, 5, 9L, 2);
|
||||||
|
order.verify(splits).touchRootSplit(10001L, 31L, 9L, 4);
|
||||||
|
order.verify(splits).cloneChildWithPurpose(eq(10001L), eq(32L), eq(31L), anyLong(),
|
||||||
|
eq(1002L), eq("ORDER-REMAINDER"), eq("IN-001-2"), eq(7), eq(2),
|
||||||
|
eq("REMAINDER"), eq(9L), eq("operator"));
|
||||||
|
verify(splits).insertBatchWithChildren(anyLong(), eq(10001L), eq(31L), eq("PDA-SPLIT-1-32"),
|
||||||
|
eq(32L), eq("pda-task-1"), eq(4), eq(12),
|
||||||
|
org.mockito.ArgumentMatchers.isNull(), anyLong(), eq(5), eq(7), eq(701L), eq(801L),
|
||||||
|
anyString(), eq(9L), eq("operator"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void retryAfterRemoteOrderSuccessCanFinishLocalThirdSliceWithoutNestedCode() {
|
||||||
|
LoadingPickItemEntity item = item(13L, 23L, 33L, 43L, 9, 4, "PARTIAL");
|
||||||
|
resolvedChild(item, 33L, 43L, 63L, 53L, 9, 3);
|
||||||
|
when(splits.nextSplitSequence(10001L, 31L)).thenReturn(3);
|
||||||
|
childWrites(33L, 43L, 63L, 53L, 4, 3);
|
||||||
|
when(splits.markSourceLoaded(10001L, 33L, 4, 9L, 3)).thenReturn(0, 1);
|
||||||
|
when(warehouse.settle(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||||
|
new WarehousePalletLocationClient.SettlementResult(List.of(), List.of(transfer(1, 501L, 5)))));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.settle(1L, 9L, "operator"))
|
||||||
|
.hasMessageContaining("来源分片状态已被并发修改");
|
||||||
|
service.settle(1L, 9L, "operator");
|
||||||
|
|
||||||
|
verify(orderSplits, times(2)).split(eq("internal-token"), eq(9L),
|
||||||
|
org.mockito.ArgumentMatchers.argThat(request -> request.idempotencyKey().equals("pda-task-1")
|
||||||
|
&& request.operationId().equals("PDA-SPLIT-1-33")));
|
||||||
|
verify(splits).cloneChildWithPurpose(eq(10001L), eq(33L), eq(31L), anyLong(),
|
||||||
|
eq(1002L), eq("ORDER-REMAINDER"), eq("IN-001-3"), eq(5), eq(3),
|
||||||
|
eq("REMAINDER"), eq(9L), eq("operator"));
|
||||||
|
}
|
||||||
|
|
||||||
private void resolved(List<LoadingPickItemEntity> items) {
|
private void resolved(List<LoadingPickItemEntity> items) {
|
||||||
when(picks.allTaskItemsResolved(1L)).thenReturn(true);
|
when(picks.allTaskItemsResolved(1L)).thenReturn(true);
|
||||||
when(picks.items(1L)).thenReturn(items);
|
when(picks.items(1L)).thenReturn(items);
|
||||||
@@ -255,6 +302,47 @@ class PdaPickSettlementServiceTest {
|
|||||||
stubOrderSplitSuccess();
|
stubOrderSplitSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void resolvedChild(LoadingPickItemEntity item, long inboundId, long detailId,
|
||||||
|
long measurementId, long measurementItemId,
|
||||||
|
int packageCount, int sourceVersion) {
|
||||||
|
resolved(List.of(item));
|
||||||
|
when(splits.lineage(inboundId)).thenReturn(new InboundSplitSettlementMapper.Lineage(
|
||||||
|
inboundId, 10001L, "CHILD", 31L, "REMAINDER", "NOT_SPLIT"));
|
||||||
|
when(splits.lockParent(31L)).thenReturn(new ParentRow(31L, 10001L, 901L, "ORDER-001",
|
||||||
|
"IN-001", "MASTER", 31L, "LOADING", 5, 5, "FULLY_SPLIT", 4));
|
||||||
|
when(splits.lockParent(inboundId)).thenReturn(new ParentRow(inboundId, 10001L, 902L,
|
||||||
|
"ORDER-REMAINDER", "IN-001-" + (sourceVersion - 1), "CHILD", 31L,
|
||||||
|
"INBOUNDED", packageCount, packageCount, "NOT_SPLIT", sourceVersion));
|
||||||
|
when(splits.lockLatestApprovedItemsForInbound(10001L, inboundId))
|
||||||
|
.thenReturn(List.of(new MeasurementItemRow(measurementItemId, measurementId, detailId, packageCount)));
|
||||||
|
when(allocations.lockConfirmed(10001L, inboundId)).thenAnswer(invocation ->
|
||||||
|
List.of(allocation(1L, 1, 501L, packageCount)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void childWrites(long inboundId, long detailId, long measurementId,
|
||||||
|
long measurementItemId, int pickedPackageCount, int sourceVersion) {
|
||||||
|
when(splits.cloneChildWithPurpose(eq(10001L), eq(inboundId), eq(31L), anyLong(), anyLong(),
|
||||||
|
anyString(), anyString(), anyInt(), anyInt(), eq("REMAINDER"), eq(9L), eq("operator"))).thenReturn(1);
|
||||||
|
when(splits.insertBatchWithChildren(anyLong(), eq(10001L), eq(31L), anyString(), eq(inboundId),
|
||||||
|
anyString(), eq(4), anyInt(), org.mockito.ArgumentMatchers.isNull(), anyLong(), anyInt(), anyInt(),
|
||||||
|
eq(701L), eq(801L), anyString(), eq(9L), eq("operator"))).thenReturn(1);
|
||||||
|
when(splits.cloneMeasurement(eq(10001L), eq(inboundId), eq(measurementId), anyLong(), anyLong(),
|
||||||
|
anyLong(), anyInt(), eq(9L))).thenReturn(1);
|
||||||
|
when(splits.insertSplitItem(anyLong(), eq(10001L), anyLong(), eq(31L), anyLong(), anyInt(),
|
||||||
|
eq(detailId), eq(measurementId), eq(measurementItemId), anyInt())).thenReturn(1);
|
||||||
|
when(splits.cloneMeasurementItem(eq(10001L), eq(measurementId), eq(measurementItemId), anyLong(),
|
||||||
|
anyLong(), anyLong(), anyInt(), anyInt())).thenReturn(1);
|
||||||
|
when(splits.markSourceLoaded(10001L, inboundId, pickedPackageCount, 9L, sourceVersion)).thenReturn(1);
|
||||||
|
when(splits.touchRootSplit(10001L, 31L, 9L, 4)).thenReturn(1);
|
||||||
|
when(splits.applyBatch(eq(10001L), anyLong())).thenReturn(1);
|
||||||
|
when(splits.updateMeasurementItemForLoaded(eq(10001L), eq(measurementItemId), anyInt())).thenReturn(1);
|
||||||
|
when(splits.updateMeasurementForLoaded(eq(10001L), eq(inboundId), eq(measurementId), anyInt())).thenReturn(1);
|
||||||
|
when(splits.normalizeChildMetrics(eq(10001L), anyLong(), anyLong())).thenReturn(1);
|
||||||
|
when(splits.insertHistory(anyLong(), eq(10001L), anyLong(), anyString(), anyString(), eq(9L),
|
||||||
|
eq("operator"), anyString())).thenReturn(1);
|
||||||
|
when(splits.convergePartialPlanItem(anyLong(), eq(9L), eq("operator"))).thenReturn(1);
|
||||||
|
}
|
||||||
|
|
||||||
private void stubOrderSplitSuccess() {
|
private void stubOrderSplitSuccess() {
|
||||||
when(orderSplits.split(eq("internal-token"), eq(9L), any())).thenAnswer(invocation -> {
|
when(orderSplits.split(eq("internal-token"), eq(9L), any())).thenAnswer(invocation -> {
|
||||||
OrderFulfillmentSplitClient.CreateRequest request = invocation.getArgument(2);
|
OrderFulfillmentSplitClient.CreateRequest request = invocation.getArgument(2);
|
||||||
|
|||||||
Reference in New Issue
Block a user