This commit is contained in:
2026-08-13 19:19:29 +08:00
parent cca1bc002e
commit 808fabaf41
11 changed files with 176 additions and 37 deletions
@@ -114,6 +114,9 @@ public class ContainerService {
if (containerNo == null) return;
LoadingPlanEntity plan = smartLoadingRepository.planByBooking(booking.getId());
if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return;
// The PDA task and its item versions are stable work identities. A later transport-number
// correction only changes the booking display value and must not resynchronize the task.
if (loadingPickRepository.taskByPlan(plan.getId()) != null) return;
loadingPickService.begin(plan.getId(), containerNo, operatorId, operatorName,
smartLoadingRepository.items(plan.getId()));
}
@@ -423,8 +423,21 @@ public class LoadingPickService {
if (item.getPickedPackageCount() > planItem.getPackageCount()) {
throw conflict("已拣货箱数不能低于计划箱数");
}
int pickedPackageCount = count(item.getPickedPackageCount());
String itemStatus = pickedPackageCount == 0 ? "PENDING"
: pickedPackageCount == planItem.getPackageCount() ? "PICKED" : "PICKING";
if (Objects.equals(item.getPickTaskId(), taskId)
&& Objects.equals(item.getInboundId(), planItem.getInboundId())
&& Objects.equals(item.getInboundDetailId(), planItem.getInboundDetailId())
&& Objects.equals(item.getPlannedPackageCount(), planItem.getPackageCount())
&& Objects.equals(item.getItemStatus(), itemStatus)) {
return;
}
item.setPickTaskId(taskId);
item.setInboundId(planItem.getInboundId());
item.setInboundDetailId(planItem.getInboundDetailId());
item.setPlannedPackageCount(planItem.getPackageCount());
item.setItemStatus(item.getPickedPackageCount().equals(planItem.getPackageCount()) ? "PICKED" : "PICKING");
item.setItemStatus(itemStatus);
repository.update(item);
}
@@ -128,7 +128,7 @@ components:
loadingPlanId: {type: integer, format: int64}
containerBookingId: {type: integer, format: int64}
cabinetNo: {type: string}
currentContainerNo: {type: string, nullable: true, description: Mutable transport display field; never task identity}
currentContainerNo: {type: string, nullable: true, description: Mutable transport display field; changing it never resynchronizes the task or item versions and it is never task identity}
taskStatus: {type: string, enum: [LOADING, COMPLETED]}
workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW, COMPLETED]}
version: {type: integer, minimum: 0}
@@ -28,6 +28,7 @@ class PdaConvergenceOpenApiContractTest {
.contains("BatchRequest")
.contains("inventorySnapshotToken")
.contains("InventorySnapshot")
.contains("changing it never resynchronizes the task or item versions")
.contains("expectedTaskVersion")
.contains("CompleteLoadingRequest");
}
@@ -148,6 +148,27 @@ class ContainerServiceTest {
org.mockito.ArgumentMatchers.eq("admin"), any());
}
@Test
@DisplayName("PDA任务创建后替换真实柜号不重同步任务明细")
void doesNotResynchronizeExistingPdaTaskWhenContainerNumberChanges() {
ContainerBookingEntity current = booking("EDITING", 2);
LoadingPlanEntity plan = new LoadingPlanEntity();
plan.setId(31L);
plan.setPlanStatus("EDITING");
plan.setPreloadPushedAt(LocalDateTime.of(2026, 8, 11, 20, 0));
when(repository.find(11L)).thenReturn(current);
when(repository.updateTransport(11L, "MSCU7654321", "V002", 2, 9L, "admin"))
.thenReturn(1);
when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan);
when(loadingPickRepository.taskByPlan(31L)).thenReturn(new LoadingPickTaskEntity());
service.updateTransport(11L, 9L, "admin",
new TransportUpdateRequest("MSCU7654321", "V002", 2));
verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), any(), any());
verify(smartLoadingRepository, never()).items(31L);
}
@Test
@DisplayName("PDA任务创建后禁止清空真实集装箱号")
void rejectsClearingContainerNumberAfterPdaTaskStarts() {
@@ -16,6 +16,7 @@ import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import jakarta.annotation.Resource;
@@ -352,6 +353,69 @@ class LoadingPickServiceTest {
verify(repository, never()).touchTask(any(Long.class), any(Integer.class));
}
@Test
void staleTaskVersionDoesNotDriftItemAndFollowingValidBatchSucceeds() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity item = item();
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.task(20L)).thenReturn(task);
when(repository.assignment(20L)).thenReturn(assignment());
when(repository.items(20L)).thenReturn(List.of(item));
when(repository.items(20L, 101L)).thenReturn(List.of(item));
when(repository.pick(30L, 2, "MANUAL", "[]", 0, 101L)).thenReturn(1);
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
when(repository.touchTask(20L, 0)).thenReturn(1);
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
BatchItemRequest line = new BatchItemRequest(
30L, "PICK", 2, null, "MANUAL", List.of(), 0, null);
assertThatThrownBy(() -> service.batch(20L, "batch-stale", new BatchRequest(
101L, 10L, 99L, "CSLU1234567", 1, snapshotToken, List.of(line)), 9L, "operator"))
.isInstanceOf(BizException.class)
.hasMessageContaining("任务版本已变化");
assertThat(item.getVersion()).isZero();
assertThat(item.getPickedPackageCount()).isZero();
service.batch(20L, "batch-valid", new BatchRequest(
101L, 10L, 99L, "CSLU1234567", 0, snapshotToken, List.of(line)), 9L, "operator");
verify(repository, times(1)).pick(30L, 2, "MANUAL", "[]", 0, 101L);
verify(repository, times(1)).touchTask(20L, 0);
}
@Test
void repeatedTaskBeginDoesNotChangePendingItemBeforeRejectedStaleBatch() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity pending = item();
var planItem = new LoadingPlanItemEntity();
planItem.setId(40L);
planItem.setInboundId(50L);
planItem.setInboundDetailId(60L);
planItem.setPackageCount(2);
when(repository.taskByPlan(10L)).thenReturn(task);
when(repository.itemByPlanItem(40L)).thenReturn(pending);
when(repository.taskForUpdate(20L)).thenReturn(task);
service.begin(10L, "CSLU1234567", 9L, "operator", List.of(planItem));
BatchRequest staleRequest = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 1,
"stale-token", List.of(
new BatchItemRequest(30L, "PICK", 2, null, "AUTO", null, 0, null)));
assertThatThrownBy(() -> service.batch(20L, "batch-stale-after-resync", staleRequest,
9L, "operator")).isInstanceOf(BizException.class);
assertThat(pending.getVersion()).isZero();
assertThat(pending.getPickedPackageCount()).isZero();
assertThat(pending.getItemStatus()).isEqualTo("PENDING");
verify(repository, never()).update(any(LoadingPickItemEntity.class));
verify(repository, never()).pick(any(Long.class), any(Integer.class), any(), any(),
any(Integer.class), any(Long.class));
verify(repository, never()).touchTask(any(Long.class), any(Integer.class));
}
private void preparePick(LoadingPickTaskEntity task, LoadingPickItemEntity item, String operationId) {
when(repository.history(operationId)).thenReturn(null);
when(repository.task(20L)).thenReturn(task);