fix(warehouse-overseas): avoid pallet batch gap lock

This commit is contained in:
2026-09-08 23:59:50 +08:00
parent a88a04fd24
commit 93704a24ce
4 changed files with 8 additions and 9 deletions
@@ -51,13 +51,6 @@ public class WarehousePalletBatchService {
public WarehousePalletBatchResponse ensureReserved(WarehousePalletBatchRequests.EnsureReserved request) {
WarehousePalletBatchRequests.EnsureReserved normalized = normalize(request);
String requestHash = hash(normalized);
WarehousePalletBatchEntity existing = batches.lockByKey(normalized.warehouseId(), normalized.batchKey());
if (existing != null) {
requireSameRequest(existing, normalized.warehouseId(), requestHash);
if ("COMPENSATED".equals(existing.getStatus())) return reactivate(existing);
return response(existing, items.lockByBatch(existing.getId()), true);
}
WarehousePalletBatchEntity batch = new WarehousePalletBatchEntity();
batch.setId(IdWorker.getId());
batch.setWarehouseId(normalized.warehouseId());
@@ -15,7 +15,7 @@ paths:
/internal/v1/warehouse-pallet-batches/confirm:
post:
summary: 确认Lastleg已持久化托盘快照并建立业务绑定
description: 仅RESERVED可确认;重复确认返回同一结果,不生成新托盘或资源锁。
description: 仅RESERVED可确认;commandKey幂等作用域为(warehouseId,batchKey,commandKey),无需全局唯一;同作用域同载荷重复确认返回同一结果,不生成新托盘或资源锁。
security: [{ internalServiceToken: [] }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ConfirmRequest' } } } }
responses:
@@ -23,7 +23,7 @@ paths:
/internal/v1/warehouse-pallet-batches/compensate:
post:
summary: 补偿释放Lastleg本地失败对应的本批未确认资源
description: 仅RESERVED且尚未建立业务绑定的本批资源可补偿;CONFIRMED或已被后续占用时返回A0409。
description: 仅RESERVED且尚未建立业务绑定的本批资源可补偿;commandKey幂等作用域为(warehouseId,batchKey,commandKey),无需全局唯一;CONFIRMED、异载荷重放或已被后续占用时返回A0409。
security: [{ internalServiceToken: [] }]
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/CompensateRequest' } } } }
responses:
@@ -33,6 +33,7 @@ class WarehousePalletBatchContractTest {
.contains("/internal/v1/warehouse-pallet-batches/confirm:")
.contains("/internal/v1/warehouse-pallet-batches/compensate:")
.contains("业务冲突仍为HTTP 200且响应体code=A0409")
.contains("commandKey幂等作用域为(warehouseId,batchKey,commandKey),无需全局唯一")
.contains("businessKey: { type: string, minLength: 1, maxLength: 100 }")
.contains("palletCode: { type: string, maxLength: 64 }")
.doesNotContain("'409':");
@@ -19,6 +19,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
@@ -74,6 +75,7 @@ class WarehousePalletBatchServiceTest {
ArgumentCaptor<Long> scope = ArgumentCaptor.forClass(Long.class);
verify(resources, times(2)).createPallet(scope.capture(), anyLong(), any(PalletRequests.SaveRequest.class));
assertThat(scope.getAllValues()).containsOnly(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE);
verify(batches, never()).lockByKey(WAREHOUSE, "container-1");
ArgumentCaptor<WarehouseResourceLockRequests.ReserveRequest> reservations =
ArgumentCaptor.forClass(WarehouseResourceLockRequests.ReserveRequest.class);
verify(locks, times(2)).reserve(reservations.capture());
@@ -100,6 +102,9 @@ class WarehousePalletBatchServiceTest {
assertThat(response.replayed()).isTrue();
assertThat(response.batchId()).isEqualTo(inserted.get().getId());
InOrder order = org.mockito.Mockito.inOrder(batches);
order.verify(batches).insertIgnore(any());
order.verify(batches).lockByKey(WAREHOUSE, "container-1");
verify(resources, never()).createPallet(anyLong(), anyLong(), any());
verify(locks, never()).reserve(any());
}