优化
This commit is contained in:
@@ -25,6 +25,10 @@ public class LoadingRepository {
|
||||
return inbounds.find(tenantId, inboundId);
|
||||
}
|
||||
|
||||
public LoadingInboundMapper.Snapshot findInboundForUpdate(long tenantId, long inboundId) {
|
||||
return inbounds.findForUpdate(tenantId, inboundId);
|
||||
}
|
||||
|
||||
public int completeLoading(long tenantId, long inboundId, long operatorId, int expectedVersion) {
|
||||
return inbounds.completeLoading(tenantId, inboundId, operatorId, expectedVersion);
|
||||
}
|
||||
|
||||
+18
@@ -121,6 +121,24 @@ public class LoadingDeliveryService {
|
||||
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, inboundId)));
|
||||
}
|
||||
|
||||
/**
|
||||
* PDA settlement may increment the source version and may create a loaded fulfillment slice.
|
||||
* Resolve and lock the canonical completion row after settlement, then retain optimistic locking
|
||||
* with that current server version instead of reusing the pre-settlement task snapshot.
|
||||
*/
|
||||
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
|
||||
long operatorId, String operatorName,
|
||||
String key, String remark) {
|
||||
CompletionTarget target = repository.completionTarget(tenantId, inboundId);
|
||||
long completionInboundId = target == null ? inboundId : target.completionInboundId();
|
||||
Snapshot current = repository.findInboundForUpdate(tenantId, completionInboundId);
|
||||
if (current == null || current.version() == null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "PDA结算后的实装履约分片不存在,请刷新后重试");
|
||||
}
|
||||
return completeLoading(tenantId, inboundId, operatorId, operatorName, key,
|
||||
new CompleteLoadingRequest(current.version(), remark));
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only preflight used before PDA settlement. The current plan may still be LOADING here;
|
||||
* every external/business hard gate must pass before the PDA task is mutated.
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||
import com.baiyue.firstleg.loading.service.LoadingDeliveryService;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
@@ -59,7 +58,6 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -453,12 +451,10 @@ public class LoadingPickService {
|
||||
.thenComparing(LoadingPickItemMapper.InboundOwner::inboundId))
|
||||
.toList();
|
||||
if (owners.isEmpty()) throw conflict("PDA任务未关联有效入库单");
|
||||
Map<LoadingPickItemMapper.InboundOwner, LoadingInboundMapper.Snapshot> inboundSnapshots = new LinkedHashMap<>();
|
||||
for (LoadingPickItemMapper.InboundOwner owner : owners) {
|
||||
LoadingInboundMapper.Snapshot snapshot = repository.inboundForUpdate(owner.tenantId(), owner.inboundId());
|
||||
if (snapshot == null) throw conflict("PDA任务关联入库单不存在");
|
||||
loadingDeliveryService.requirePdaCompletionReady(owner.tenantId(), owner.inboundId());
|
||||
inboundSnapshots.put(owner, snapshot);
|
||||
}
|
||||
truckAppointmentService.completeLoadingFromPda(task.getContainerBookingId(), taskId,
|
||||
operatorId, operatorName);
|
||||
@@ -472,12 +468,9 @@ public class LoadingPickService {
|
||||
insertOperation(taskId, operationId, requestHash, "COMPLETE_LOADING", operatorId, operatorName);
|
||||
loadingListService.finishFromPda(task.getLoadingPlanId(), taskId, operatorId, operatorName);
|
||||
repository.syncInboundProjections(taskId, operatorId, operatorName);
|
||||
for (Map.Entry<LoadingPickItemMapper.InboundOwner, LoadingInboundMapper.Snapshot> entry
|
||||
: inboundSnapshots.entrySet()) {
|
||||
LoadingPickItemMapper.InboundOwner owner = entry.getKey();
|
||||
loadingDeliveryService.completeLoading(owner.tenantId(), owner.inboundId(), operatorId,
|
||||
operatorName, "PDA-" + operationId + '-' + owner.inboundId(),
|
||||
new LoadingRequests.CompleteLoadingRequest(entry.getValue().version(), remark));
|
||||
for (LoadingPickItemMapper.InboundOwner owner : owners) {
|
||||
loadingDeliveryService.completeAfterPdaSettlement(owner.tenantId(), owner.inboundId(), operatorId,
|
||||
operatorName, "PDA-" + operationId + '-' + owner.inboundId(), remark);
|
||||
}
|
||||
return view(requiredTask(taskId), null);
|
||||
}
|
||||
|
||||
+78
@@ -178,6 +178,62 @@ class LoadingDeliveryServiceTest {
|
||||
verify(repository, never()).completeLoading(anyLong(), anyLong(), anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaFinalizeUsesSourceVersionIncrementedByFullSettlement() {
|
||||
readyForPdaFinalize();
|
||||
CompletionTarget settled = new CompletionTarget(20L, null, 20L,
|
||||
"LOADING", 7, "LOADING", 7);
|
||||
when(repository.completionTarget(10, 20)).thenReturn(settled);
|
||||
when(repository.findInboundForUpdate(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 7));
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 7));
|
||||
when(repository.completeLoadedChild(10, 20, 7, 7)).thenReturn(1);
|
||||
|
||||
LoadingActionResponse result = service.completeAfterPdaSettlement(
|
||||
10, 20, 7, "admin", "pda-full", "完成");
|
||||
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADED");
|
||||
verify(repository).findInboundForUpdate(10, 20);
|
||||
verify(repository).completeLoadedChild(10, 20, 7, 7);
|
||||
verify(repository, never()).completeLoadedChild(10, 20, 7, 6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaFinalizeLocksAndUsesLoadedSliceVersionAfterPartialSettlement() {
|
||||
readyForPdaFinalize();
|
||||
CompletionTarget settled = new CompletionTarget(20L, 21L, 20L,
|
||||
"LOADING", 7, "LOADING", 2);
|
||||
when(repository.completionTarget(10, 20)).thenReturn(settled);
|
||||
when(repository.findInboundForUpdate(10, 21)).thenReturn(new Snapshot(21L, 10L, "LOADING", 2));
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 7));
|
||||
when(repository.completeLoadedChild(10, 20, 7, 2)).thenReturn(1);
|
||||
|
||||
LoadingActionResponse result = service.completeAfterPdaSettlement(
|
||||
10, 20, 7, "admin", "pda-partial", "完成");
|
||||
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADED");
|
||||
verify(repository).findInboundForUpdate(10, 21);
|
||||
verify(repository).completeLoadedChild(10, 20, 7, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaFinalizeKeepsOptimisticConflictForRealConcurrentChange() {
|
||||
readyForPdaFinalize();
|
||||
CompletionTarget settled = new CompletionTarget(20L, null, 20L,
|
||||
"LOADING", 7, "LOADING", 7);
|
||||
when(repository.completionTarget(10, 20)).thenReturn(settled);
|
||||
when(repository.findInboundForUpdate(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 7));
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 7));
|
||||
when(repository.completeLoadedChild(10, 20, 7, 7)).thenReturn(0);
|
||||
|
||||
assertThatThrownBy(() -> service.completeAfterPdaSettlement(
|
||||
10, 20, 7, "admin", "pda-concurrent", "完成"))
|
||||
.hasMessageContaining("数据已被其他人修改");
|
||||
|
||||
verify(histories, never()).insert(any(InboundStatusHistoryEntity.class));
|
||||
verify(repository, never()).completeOperation(anyLong(), anyLong(), anyLong(),
|
||||
anyString(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completesSingleRemainderSourceOnceAndReplaysSameKeyWithoutRepeatingSideEffects() {
|
||||
AtomicInteger claims = new AtomicInteger();
|
||||
@@ -359,6 +415,28 @@ class LoadingDeliveryServiceTest {
|
||||
return value;
|
||||
}
|
||||
|
||||
private void readyForPdaFinalize() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(true);
|
||||
LoadingPickItemMapper.InboundProgress progress = mock(LoadingPickItemMapper.InboundProgress.class);
|
||||
when(progress.getExpectedCount()).thenReturn(5);
|
||||
when(progress.getPickedCount()).thenReturn(5);
|
||||
when(progress.getTaskCount()).thenReturn(1);
|
||||
when(progress.getCompletedTaskCount()).thenReturn(1);
|
||||
when(progress.getUnresolvedCount()).thenReturn(0);
|
||||
when(loadingPicks.inboundProgress(10, 20)).thenReturn(progress);
|
||||
when(repository.listEvidence(10, 20)).thenReturn(List.of(
|
||||
evidence("PHOTO_STEP_1", 1L, null), evidence("PHOTO_STEP_2", 2L, null),
|
||||
evidence("PHOTO_STEP_3", 3L, null), evidence("PHOTO_STEP_4", 4L, null),
|
||||
evidence("SEAL", 5L, "LOCK-001")));
|
||||
when(files.validate(eq("token"), eq(10L), eq(20L), anyLong())).thenAnswer(invocation -> {
|
||||
long fileId = invocation.getArgument(3);
|
||||
String type = fileId == 5L ? "SEAL" : "PHOTO_STEP_" + fileId;
|
||||
return ApiResponse.success(new LoadingEvidenceFileClient.FileMetadata(
|
||||
fileId, type.toLowerCase() + ".png", "LOADING_" + type, "image/png"));
|
||||
});
|
||||
}
|
||||
|
||||
private static LoadingOperationEntity successfulOperation(String requestHash, String resultJson) {
|
||||
LoadingOperationEntity value = new LoadingOperationEntity();
|
||||
value.setRequestHash(requestHash);
|
||||
|
||||
@@ -486,12 +486,18 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualCompletionSettlesResolvedItemsAndCompletesPlan() {
|
||||
void manualCompletionSettlesResolvedItemsCompletesPlanAndReplaysSameOperation() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
LoadingPickItemEntity item = item();
|
||||
AtomicReference<LoadingPickOperationEntity> operation = new AtomicReference<>();
|
||||
item.setPlannedPackageCount(5);
|
||||
item.setPickedPackageCount(3);
|
||||
item.setItemStatus("PARTIAL");
|
||||
when(repository.operation("complete-1")).thenAnswer(invocation -> operation.get());
|
||||
doAnswer(invocation -> {
|
||||
operation.set(invocation.getArgument(0));
|
||||
return null;
|
||||
}).when(repository).insert(any(LoadingPickOperationEntity.class));
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.task(20L)).thenReturn(task);
|
||||
when(repository.items(20L)).thenReturn(List.of(item));
|
||||
@@ -505,6 +511,7 @@ class LoadingPickServiceTest {
|
||||
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
|
||||
|
||||
service.complete(20L, new CompleteLoadingRequest(0, "complete-1", "checked"), 9L, "operator");
|
||||
service.complete(20L, new CompleteLoadingRequest(0, "complete-1", "checked"), 9L, "operator");
|
||||
|
||||
verify(repository).insert(any(LoadingPickOperationEntity.class));
|
||||
verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
|
||||
@@ -512,9 +519,8 @@ class LoadingPickServiceTest {
|
||||
verify(repository).syncInboundProjections(20L, 9L, "operator");
|
||||
verify(loadingDeliveryService).requirePdaCompletionReady(101L, 50L);
|
||||
verify(truckAppointmentService).completeLoadingFromPda(99L, 20L, 9L, "operator");
|
||||
verify(loadingDeliveryService).completeLoading(eq(101L), eq(50L), eq(9L), eq("operator"),
|
||||
eq("PDA-complete-1-50"), argThat(value -> value.expectedVersion() == 7
|
||||
&& "checked".equals(value.remark())));
|
||||
verify(loadingDeliveryService).completeAfterPdaSettlement(101L, 50L, 9L, "operator",
|
||||
"PDA-complete-1-50", "checked");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -538,7 +544,7 @@ class LoadingPickServiceTest {
|
||||
verify(settlementService, never()).settle(anyLong(), anyLong(), anyString());
|
||||
verify(repository, never()).approve(anyLong(), anyLong(), anyString(), any());
|
||||
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingDeliveryService, never()).completeLoading(anyLong(), anyLong(), anyLong(),
|
||||
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(anyLong(), anyLong(), anyLong(),
|
||||
anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user