fix: converge inbound pallets and retries

This commit is contained in:
2026-09-11 00:06:57 +08:00
parent ed12769858
commit 2bd12f8a5f
11 changed files with 65 additions and 15 deletions
+1 -1
View File
@@ -15,7 +15,7 @@
| 内部资源锁定、释放、报废 | `controller/InternalWarehouseResourceLockController.java` |
| PDA 托盘状态流转与补偿 | `controller/InternalPdaPalletTransitionController.java`、`service/WarehousePalletTransitionService.java`、`mapper/WarehousePalletTransitionMapper.java`;内部前缀 `/internal/v1/pda-pallet-transitions` |
| 托盘解析与业务绑定 | `controller/InternalWarehousePalletController.java`、`dto/WarehousePalletResolveResponse.java`、`entity/WarehousePalletBindingEntity.java` |
| 柜级入库计划批量托盘 | `controller/InternalWarehousePalletBatchController.java`、`service/WarehousePalletBatchService.java`、`warehouse-pallet-batch-internal-v1.yaml`;以稳定批次键幂等执行 `ensure-reserved → confirm`,只从可预留真实托盘/库位建立逐项资源锁;Lastleg 本地计划事务失败时仅补偿仍为 `RESERVED` 的未确认资源,已 `CONFIRMED` 批次不得回退,同柜重放不得产生第二批资源 |
| 柜级入库计划批量托盘 | `controller/InternalWarehousePalletBatchController.java`、`service/WarehousePalletBatchService.java`、`warehouse-pallet-batch-internal-v1.yaml`;以稳定批次键幂等执行 `ensure-reserved → confirm`,权威托盘码已存在且资源可预留时必须复用,未提供或不存在时才创建;Lastleg 本地计划事务失败时仅补偿仍为 `RESERVED` 的未确认资源,已 `CONFIRMED` 批次不得回退,同柜重放不得产生第二批资源 |
| 仓储资源服务 | `service/WarehouseResourceService.java`、`WarehouseResourceLockService.java` |
| 内部服务令牌 | `security/InternalServiceTokenGuard.java` |
| 数据库基线与中文注释契约 | `src/main/resources/db/warehouse.sql`、`src/test/java/com/baiyue/warehouse/schema/SchemaChineseCommentContractTest.java` |
@@ -130,6 +130,13 @@ public class WarehouseResourceRepository {
.orderByAsc(WarehousePalletEntity::getId));
}
public WarehousePalletEntity findPublicPalletByCode(long warehouseId, String palletCode) {
return pallets.selectOne(new LambdaQueryWrapper<WarehousePalletEntity>()
.eq(WarehousePalletEntity::getCustomerId, 0L)
.eq(WarehousePalletEntity::getWarehouseId, warehouseId)
.eq(WarehousePalletEntity::getPalletCode, palletCode));
}
public WarehousePalletEntity findPublicPallet(long palletId) {
return pallets.selectOne(new LambdaQueryWrapper<WarehousePalletEntity>()
.eq(WarehousePalletEntity::getCustomerId, 0L).eq(WarehousePalletEntity::getId, palletId));
@@ -88,12 +88,16 @@ public class WarehousePalletBatchService {
SharedReservation shared = isDropshipPallet(requested.palletCode())
? sharedDropship.get(requested.palletCode()) : null;
if (shared == null) {
Long palletId = resources.createPallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE,
normalized.warehouseId(), new PalletRequests.SaveRequest(requested.palletCode(), "WOOD",
AUTO_PALLET_CAPACITY_CBM, AUTO_PALLET_UNIT_PRICE_USD, "IDLE", null, AUTO_PALLET_REMARK,
requested.fbaWarehouseCode()));
WarehousePalletEntity createdPallet = resources.pallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE,
normalized.warehouseId(), palletId);
WarehousePalletEntity createdPallet = resources.reusablePublicPallet(
normalized.warehouseId(), requested.palletCode());
if (createdPallet == null) {
Long palletId = resources.createPallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE,
normalized.warehouseId(), new PalletRequests.SaveRequest(requested.palletCode(), "WOOD",
AUTO_PALLET_CAPACITY_CBM, AUTO_PALLET_UNIT_PRICE_USD, "IDLE", null, AUTO_PALLET_REMARK,
requested.fbaWarehouseCode()));
createdPallet = resources.pallet(WarehouseResourceService.PUBLIC_RESOURCE_SCOPE,
normalized.warehouseId(), palletId);
}
WarehouseResourceLockResponse lock = locks.reserve(new WarehouseResourceLockRequests.ReserveRequest(
requested.customerId(), normalized.warehouseId(), createdPallet.getId(), createdPallet.getLocationId(),
BUSINESS_TYPE, requested.businessKey(), reservationKey(batch, requested.sequenceNo())));
@@ -191,6 +191,12 @@ public class WarehouseResourceService {
return palletReference(pallet);
}
public WarehousePalletEntity reusablePublicPallet(long warehouseId, String palletCode) {
if (!StringUtils.hasText(palletCode)) return null;
requireWarehouseReference(warehouseId);
return repository.findPublicPalletByCode(warehouseId, palletCode.trim());
}
public WarehousePalletResolveResponse resolvePublicPallet(long palletId) {
WarehousePalletEntity pallet = repository.findPublicPallet(palletId);
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "托盘不存在");
@@ -132,6 +132,25 @@ class WarehousePalletBatchServiceTest {
assertThat(response.items()).extracting(value -> value.palletCode()).containsExactly("PAL-301");
}
@Test
void compensatedSourcePalletCodeReusesTheExistingIdlePhysicalPallet() {
when(batches.insertIgnore(any())).thenReturn(1);
WarehousePalletEntity existing = pallet(301L, 401L, "DP-260910-0001");
existing.setStatus("IDLE");
when(resources.reusablePublicPallet(WAREHOUSE, "DP-260910-0001")).thenReturn(existing);
when(locks.reserve(any())).thenReturn(lockResponse(CUSTOMER_A, 501L));
var request = new WarehousePalletBatchRequests.EnsureReserved(WAREHOUSE, "retry-container", List.of(
new WarehousePalletBatchRequests.Item(CUSTOMER_A, "container:dropship-a:1", "PLAN-A", 1,
"STANDARD", null, "DP-260910-0001")));
var response = service.ensureReserved(request);
assertThat(response.items()).extracting(value -> value.palletId()).containsExactly(301L);
assertThat(response.items()).extracting(value -> value.palletCode()).containsExactly("DP-260910-0001");
verify(resources, never()).createPallet(anyLong(), anyLong(), any());
verify(locks).reserve(any());
}
@Test
void concurrentSameRequestReplaysWinningBatchWithoutCreatingResources() {
AtomicReference<WarehousePalletBatchEntity> inserted = new AtomicReference<>();