From cf8053b7ce0437b22d53f6be25720a4aec3373a4 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 00:11:46 +0800 Subject: [PATCH] fix(warehouse-overseas): keep confirmed pallet locks active --- .../service/WarehousePalletBatchService.java | 3 -- .../warehouse-pallet-batch-internal-v1.yaml | 2 +- .../WarehousePalletBatchContractTest.java | 1 + .../WarehousePalletBatchServiceTest.java | 46 +++++++++++++++++-- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletBatchService.java b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletBatchService.java index fb26a5cba..d649a00c0 100644 --- a/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletBatchService.java +++ b/warehouse-overseas-service/src/main/java/com/baiyue/warehouse/service/WarehousePalletBatchService.java @@ -148,9 +148,6 @@ public class WarehousePalletBatchService { } else if (!Objects.equals(binding.getPalletId(), item.getPalletId())) { throw conflict("Inbound plan pallet business key is already bound to another pallet"); } - if (lockRecords.close(lock, "CONFIRMED") != 1) { - throw conflict("Inbound plan pallet reservation changed concurrently"); - } item.setStatus("CONFIRMED"); item.setUpdatedAt(LocalDateTime.now()); items.updateById(item); diff --git a/warehouse-overseas-service/src/main/resources/openapi/warehouse-pallet-batch-internal-v1.yaml b/warehouse-overseas-service/src/main/resources/openapi/warehouse-pallet-batch-internal-v1.yaml index 717b6f3dc..5b239e44c 100644 --- a/warehouse-overseas-service/src/main/resources/openapi/warehouse-pallet-batch-internal-v1.yaml +++ b/warehouse-overseas-service/src/main/resources/openapi/warehouse-pallet-batch-internal-v1.yaml @@ -15,7 +15,7 @@ paths: /internal/v1/warehouse-pallet-batches/confirm: post: summary: 确认Lastleg已持久化托盘快照并建立业务绑定 - description: 仅RESERVED可确认;commandKey幂等作用域为(warehouseId,batchKey,commandKey),无需全局唯一;同作用域同载荷重复确认返回同一结果,不生成新托盘或资源锁。 + description: 仅RESERVED可确认;commandKey幂等作用域为(warehouseId,batchKey,commandKey),无需全局唯一;同作用域同载荷重复确认返回同一结果,不生成新托盘或资源锁。确认只固化批次和业务绑定,资源锁保持ACTIVE供后续真实履约release或scrap。 security: [{ internalServiceToken: [] }] requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ConfirmRequest' } } } } responses: diff --git a/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/schema/WarehousePalletBatchContractTest.java b/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/schema/WarehousePalletBatchContractTest.java index a2dcdbed7..9d252a6f5 100644 --- a/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/schema/WarehousePalletBatchContractTest.java +++ b/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/schema/WarehousePalletBatchContractTest.java @@ -34,6 +34,7 @@ class WarehousePalletBatchContractTest { .contains("/internal/v1/warehouse-pallet-batches/compensate:") .contains("业务冲突仍为HTTP 200且响应体code=A0409") .contains("commandKey幂等作用域为(warehouseId,batchKey,commandKey),无需全局唯一") + .contains("资源锁保持ACTIVE供后续真实履约release或scrap") .contains("businessKey: { type: string, minLength: 1, maxLength: 100 }") .contains("palletCode: { type: string, maxLength: 64 }") .doesNotContain("'409':"); diff --git a/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/service/WarehousePalletBatchServiceTest.java b/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/service/WarehousePalletBatchServiceTest.java index 59f33897e..49157c4a2 100644 --- a/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/service/WarehousePalletBatchServiceTest.java +++ b/warehouse-overseas-service/src/test/java/com/baiyue/warehouse/service/WarehousePalletBatchServiceTest.java @@ -8,6 +8,7 @@ import com.baiyue.warehouse.dto.WarehouseResourceLockResponse; import com.baiyue.warehouse.entity.WarehousePalletBatchEntity; import com.baiyue.warehouse.entity.WarehousePalletBatchItemEntity; import com.baiyue.warehouse.entity.WarehousePalletBindingEntity; +import com.baiyue.warehouse.entity.WarehouseLocationEntity; import com.baiyue.warehouse.entity.WarehousePalletEntity; import com.baiyue.warehouse.entity.WarehouseResourceLockEntity; import com.baiyue.warehouse.mapper.WarehousePalletBatchItemMapper; @@ -143,7 +144,6 @@ class WarehousePalletBatchServiceTest { when(items.lockByBatch(11L)).thenReturn(batchItems); when(lockRecords.find(CUSTOMER_A, 501L)).thenReturn(lock(CUSTOMER_A, batchItems.get(0))); when(lockRecords.find(CUSTOMER_B, 502L)).thenReturn(lock(CUSTOMER_B, batchItems.get(1))); - when(lockRecords.close(any(), org.mockito.ArgumentMatchers.eq("CONFIRMED"))).thenReturn(1); var response = service.confirm(new WarehousePalletBatchRequests.Confirm(WAREHOUSE, "container-1", "confirm-1")); @@ -155,6 +155,7 @@ class WarehousePalletBatchServiceTest { verify(repository, times(2)).insertPalletBinding(bindings.capture()); assertThat(bindings.getAllValues()).extracting(WarehousePalletBindingEntity::getCustomerId) .containsExactly(CUSTOMER_A, CUSTOMER_B); + verify(lockRecords, never()).close(any(), org.mockito.ArgumentMatchers.eq("CONFIRMED")); } @Test @@ -164,7 +165,6 @@ class WarehousePalletBatchServiceTest { when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch); when(items.lockByBatch(11L)).thenReturn(List.of(item)); when(lockRecords.find(CUSTOMER_A, 501L)).thenReturn(lock(CUSTOMER_A, item)); - when(lockRecords.close(any(), org.mockito.ArgumentMatchers.eq("CONFIRMED"))).thenReturn(1); var command = new WarehousePalletBatchRequests.Confirm(WAREHOUSE, "container-1", "confirm-1"); service.confirm(command); @@ -182,7 +182,6 @@ class WarehousePalletBatchServiceTest { when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch); when(items.lockByBatch(11L)).thenReturn(List.of(item)); when(lockRecords.find(CUSTOMER_A, 501L)).thenReturn(lock(CUSTOMER_A, item)); - when(lockRecords.close(any(), org.mockito.ArgumentMatchers.eq("CONFIRMED"))).thenReturn(1); service.confirm(new WarehousePalletBatchRequests.Confirm(WAREHOUSE, "container-1", "confirm-1")); assertThatThrownBy(() -> service.confirm( @@ -191,6 +190,16 @@ class WarehousePalletBatchServiceTest { .hasMessageContaining("different payload"); } + @Test + void confirmedBatchKeepsLockActiveForSubsequentPhysicalRelease() { + assertPhysicalLifecycleAfterConfirm("RELEASED"); + } + + @Test + void confirmedBatchKeepsLockActiveForSubsequentPhysicalScrap() { + assertPhysicalLifecycleAfterConfirm("SCRAPPED"); + } + @Test void compensationReleasesOnlyThisBatchUsingEachCustomerScope() { WarehousePalletBatchEntity batch = batch(11L, "RESERVED", 1); @@ -322,6 +331,37 @@ class WarehousePalletBatchServiceTest { }); } + private void assertPhysicalLifecycleAfterConfirm(String terminalStatus) { + WarehousePalletBatchEntity batch = batch(11L, "RESERVED", 1); + WarehousePalletBatchItemEntity item = item(21L, CUSTOMER_A, 301L, 401L, 501L, 1, "RESERVED"); + WarehouseResourceLockEntity active = lock(CUSTOMER_A, item); + when(batches.lockByKey(WAREHOUSE, "container-1")).thenReturn(batch); + when(items.lockByBatch(11L)).thenReturn(List.of(item)); + when(lockRecords.find(CUSTOMER_A, 501L)).thenReturn(active); + + service.confirm(new WarehousePalletBatchRequests.Confirm(WAREHOUSE, "container-1", "confirm-1")); + + assertThat(batch.getStatus()).isEqualTo("CONFIRMED"); + assertThat(item.getStatus()).isEqualTo("CONFIRMED"); + assertThat(active.getStatus()).isEqualTo("ACTIVE"); + verify(lockRecords, never()).close(active, "CONFIRMED"); + + WarehouseLocationEntity virtual = new WarehouseLocationEntity(); + virtual.setLocationType("VIRTUAL"); + virtual.setStatus("ACTIVE"); + when(lockRecords.findLocation(CUSTOMER_A, WAREHOUSE, 401L)).thenReturn(virtual); + when(lockRecords.close(active, terminalStatus)).thenReturn(1); + WarehouseResourceLockService physicalLifecycle = new WarehouseResourceLockService(); + ReflectionTestUtils.setField(physicalLifecycle, "repository", lockRecords); + if ("RELEASED".equals(terminalStatus)) { + when(lockRecords.releasePalletAtVirtualLocation(active)).thenReturn(1); + assertThat(physicalLifecycle.release(CUSTOMER_A, 501L).status()).isEqualTo("RELEASED"); + } else { + when(lockRecords.scrapPalletAtVirtualLocation(active)).thenReturn(1); + assertThat(physicalLifecycle.scrap(CUSTOMER_A, 501L).status()).isEqualTo("SCRAPPED"); + } + } + private static WarehousePalletBatchRequests.EnsureReserved request() { return new WarehousePalletBatchRequests.EnsureReserved(WAREHOUSE, "container-1", List.of( new WarehousePalletBatchRequests.Item(CUSTOMER_B, "container:plan-b:2", "PLAN-B", 2, "STANDARD", "ONT8"),