Merge remote-tracking branch 'origin/test' into dev
This commit is contained in:
-10
@@ -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());
|
||||
@@ -155,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);
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@ paths:
|
||||
/internal/v1/warehouse-pallet-batches/confirm:
|
||||
post:
|
||||
summary: 确认Lastleg已持久化托盘快照并建立业务绑定
|
||||
description: 仅RESERVED可确认;重复确认返回同一结果,不生成新托盘或资源锁。
|
||||
description: 仅RESERVED可确认;commandKey幂等作用域为(warehouseId,batchKey,commandKey),无需全局唯一;同作用域同载荷重复确认返回同一结果,不生成新托盘或资源锁。确认只固化批次和业务绑定,资源锁保持ACTIVE供后续真实履约release或scrap。
|
||||
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:
|
||||
|
||||
+2
@@ -33,6 +33,8 @@ 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("资源锁保持ACTIVE供后续真实履约release或scrap")
|
||||
.contains("businessKey: { type: string, minLength: 1, maxLength: 100 }")
|
||||
.contains("palletCode: { type: string, maxLength: 64 }")
|
||||
.doesNotContain("'409':");
|
||||
|
||||
+48
-3
@@ -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;
|
||||
@@ -19,6 +20,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 +76,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 +103,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());
|
||||
}
|
||||
@@ -138,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"));
|
||||
|
||||
@@ -150,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
|
||||
@@ -159,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);
|
||||
@@ -177,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(
|
||||
@@ -186,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);
|
||||
@@ -317,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"),
|
||||
|
||||
Reference in New Issue
Block a user