From b6226f1ca6a3a3da644780500bbe153720d3bb4c Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 25 Aug 2026 18:21:39 +0800 Subject: [PATCH] fix: replay completed pallet operations --- .../service/PdaPalletOperationService.java | 21 +++++++++++++++++++ .../PdaPalletOperationServiceTest.java | 20 ++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java index 076d3c66d..35bcdee74 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java @@ -71,6 +71,9 @@ public class PdaPalletOperationService { public PalletOperationResponses.Operation assemble(PdaWarehouseIdentity identity, PalletOperationRequests.Assemble request) { + PalletOperationResponses.Operation replay = replayIfPresent(identity, "PALLET_ASSEMBLE", + request.idempotencyKey(), request); + if (replay != null) return replay; Map pallets = resolvePallets(identity.overseasWarehouseId(), request.target(), request.sources()); PalletPhysicalGateway.PalletReference target = pallets.get(code(request.target().palletCode())); @@ -85,6 +88,9 @@ public class PdaPalletOperationService { public PalletOperationResponses.Operation disassemble(PdaWarehouseIdentity identity, PalletOperationRequests.Disassemble request) { + PalletOperationResponses.Operation replay = replayIfPresent(identity, "PALLET_DISASSEMBLE", + request.idempotencyKey(), request); + if (replay != null) return replay; LinkedHashMap expectations = new LinkedHashMap<>(); expectations.put(code(request.source().palletCode()), request.source()); request.targets().forEach(value -> expectations.put(code(value.target().palletCode()), value.target())); @@ -109,6 +115,9 @@ public class PdaPalletOperationService { public PalletOperationResponses.Operation swap(PdaWarehouseIdentity identity, PalletOperationRequests.Swap request) { + PalletOperationResponses.Operation replay = replayIfPresent(identity, "PALLET_SWAP", + request.idempotencyKey(), request); + if (replay != null) return replay; Map pallets = resolvePallets(identity.overseasWarehouseId(), request.source(), List.of(request.target())); PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode())); @@ -129,6 +138,9 @@ public class PdaPalletOperationService { public PalletOperationResponses.Operation unbind(PdaWarehouseIdentity identity, PalletOperationRequests.Unbind request) { + PalletOperationResponses.Operation replay = replayIfPresent(identity, "PALLET_UNBIND", + request.idempotencyKey(), request); + if (replay != null) return replay; Map pallets = resolvePallets(identity.overseasWarehouseId(), List.of(request.source())); PalletPhysicalGateway.PalletReference source = pallets.get(code(request.source().palletCode())); @@ -413,6 +425,15 @@ public class PdaPalletOperationService { return value; } + private PalletOperationResponses.Operation replayIfPresent(PdaWarehouseIdentity identity, String type, + String key, Object request) { + String normalizedKey = required(key, "幂等键不能为空"); + PdaPalletOperation existing = mapper.findOperation(identity.operatorKey(), normalizedKey); + if (existing == null) return null; + validateExisting(existing, type, hash(request)); + return read(existing).asReplay(); + } + private void saveTransitions(long operationId, List transitions) { newTransaction.executeWithoutResult(status -> { PdaPalletOperation operation = mapper.selectById(operationId); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java index d13c5b887..ed82fcf29 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java @@ -27,6 +27,7 @@ import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.web.server.ResponseStatusException; import java.util.List; +import java.time.LocalDateTime; import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.*; @@ -119,6 +120,25 @@ class PdaPalletOperationServiceTest { assertEquals("PDAOPS-LONG-LOCATION-CODE", ledger.getValue().getWarehouseLocationCode()); } + @Test void exactReplayReturnsStoredResultBeforeValidatingChangedPalletVersions() throws Exception { + var request = new PalletOperationRequests.Swap("idem-replay", + new PalletOperationRequests.PalletExpectation("SOURCE", 1), + new PalletOperationRequests.PalletExpectation("TARGET", 2), "swap"); + PdaPalletOperation existing = new PdaPalletOperation(); existing.setId(500L); + existing.setOperationType("PALLET_SWAP"); existing.setStatus("COMPLETED"); + existing.setRequestHash(ReflectionTestUtils.invokeMethod(service, "hash", request)); + var stored = new com.baiyue.lastleg.palletops.dto.PalletOperationResponses.Operation( + 500L, "PALLET_SWAP", "idem-replay", "COMPLETED", false, LocalDateTime.now(), List.of()); + existing.setResultJson(new ObjectMapper().findAndRegisterModules().writeValueAsString(stored)); + when(mapper.findOperation("7", "idem-replay")).thenReturn(existing); + + var replay = service.swap(identity(), request); + + assertTrue(replay.replayed()); + assertEquals(500L, replay.operationId()); + verifyNoInteractions(physical); + } + private static PdaWarehouseIdentity identity() { return new PdaWarehouseIdentity("7", "picker", 88L); } private static PalletPhysicalGateway.PalletReference pallet(long id, String code, long warehouse, int version) { return new PalletPhysicalGateway.PalletReference(id, code, warehouse, 1000L + id,