fix(warehouse-overseas): keep confirmed pallet locks active

This commit is contained in:
2026-09-09 00:11:46 +08:00
parent 93704a24ce
commit cf8053b7ce
4 changed files with 45 additions and 7 deletions
@@ -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);
@@ -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:
@@ -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':");
@@ -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"),