fix: allocate pallet locations on weight confirmation

This commit is contained in:
2026-09-04 13:21:37 +08:00
parent d6956d396c
commit 944ecb2bf3
21 changed files with 581 additions and 77 deletions
@@ -70,6 +70,15 @@ public class InternalPalletLocationController {
return ApiResponse.success(blockService.confirm(request));
}
@PostMapping("/blocks/expand")
@Operation(summary = "原子扩容整票库位块", description = "保留原槽资源标识,新增槽优先紧邻原连续普通位,缺口共享A0")
public ApiResponse<LocationBlockDtos.BlockResult> expandBlock(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody LocationBlockDtos.ExpandRequest request) {
verify(token);
return ApiResponse.success(blockService.expand(request));
}
@PostMapping("/blocks/transfer")
public ApiResponse<LocationBlockDtos.BlockResult> transferBlock(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@@ -35,6 +35,15 @@ public final class LocationBlockDtos {
) {
}
public record ExpandRequest(
@Min(1) long blockId,
@Min(1) long businessId,
@Min(1) @Max(200) int requiredSlotCount,
@NotBlank @Size(max = 128) String idempotencyKey,
@Min(1) long operatorId
) {
}
public record TransferRequest(
@Min(1) long blockId,
@Min(1) long sourceBusinessId,
@@ -31,4 +31,14 @@ public interface LocationBlockMapper extends BaseMapper<LocationBlockEntity> {
int transfer(@Param("id") long id, @Param("sourceBusinessId") long sourceBusinessId,
@Param("targetBusinessId") long targetBusinessId, @Param("targetInboundCode") String targetInboundCode,
@Param("operationKey") String operationKey, @Param("version") int version);
@Update("""
UPDATE wh_location_block SET slot_count=#{slotCount}, last_operation_key=#{operationKey},
updated_at=CURRENT_TIMESTAMP(3), version=version+1
WHERE id=#{id} AND tenant_id=0 AND business_id=#{businessId}
AND block_status IN ('RESERVED','OCCUPIED') AND slot_count=#{previousSlotCount} AND version=#{version}
""")
int expand(@Param("id") long id, @Param("businessId") long businessId,
@Param("previousSlotCount") int previousSlotCount, @Param("slotCount") int slotCount,
@Param("operationKey") String operationKey, @Param("version") int version);
}
@@ -76,7 +76,7 @@ public class LocationBlockService {
"AVAILABLE", "RESERVED", location.getVersion(), request.operatorId()) != 1) {
throw new BizException(ResultCode.CONFLICT, "整票库位块候选被并发占用");
}
LocationBlockSlotEntity slot = slot(blockId, index + 1, location);
LocationBlockSlotEntity slot = slot(blockId, index + 1, location, "RESERVED");
slots.insert(slot);
allocated.add(slot);
}
@@ -86,6 +86,62 @@ public class LocationBlockService {
}
}
@Transactional
public LocationBlockDtos.BlockResult expand(LocationBlockDtos.ExpandRequest request) {
LocationBlockEntity block = lockOwned(request.blockId(), request.businessId());
List<LocationBlockSlotEntity> allocated = new ArrayList<>(slots.lockByBlockId(block.getId()));
if (request.idempotencyKey().equals(block.getLastOperationKey())) {
if (block.getSlotCount() != request.requiredSlotCount() || allocated.size() != block.getSlotCount()) {
throw new BizException(ResultCode.CONFLICT, "整票库位块扩容幂等键已用于不同请求");
}
return result(block, allocated);
}
if (!("RESERVED".equals(block.getBlockStatus()) || "OCCUPIED".equals(block.getBlockStatus()))) {
throw new BizException(ResultCode.CONFLICT, "只有活动整票库位块允许扩容");
}
if (request.requiredSlotCount() <= block.getSlotCount()) {
throw new BizException(ResultCode.BAD_REQUEST, "整票库位块只允许扩容,不允许缩容或无变化提交");
}
if (allocated.size() != block.getSlotCount()) {
throw new BizException(ResultCode.CONFLICT, "整票库位块槽位数量不完整");
}
requireActiveWarehouse(block.getWarehouseId());
int additional = request.requiredSlotCount() - block.getSlotCount();
List<LocationEntity> available = locations.lockAvailable(RESOURCE_TENANT_ID, block.getWarehouseId(), CANDIDATE_LIMIT);
List<LocationEntity> adjacent = selectAdjacentLocations(block, allocated, available, additional);
LocationEntity overflow = adjacent.size() < additional
? locations.lockOrCreateSharedOverflow(RESOURCE_TENANT_ID, block.getWarehouseId(), request.operatorId()) : null;
if (adjacent.size() < additional && overflow == null) {
throw new BizException(ResultCode.CONFLICT, "系统共享溢出位 A0 创建或读取失败");
}
String resourceStatus = block.getBlockStatus();
try {
for (int offset = 0; offset < additional; offset++) {
LocationEntity location = offset < adjacent.size() ? adjacent.get(offset) : overflow;
if (!Boolean.TRUE.equals(location.getSharedOverflow())
&& locations.changeStatus(RESOURCE_TENANT_ID, block.getWarehouseId(), location.getId(),
"AVAILABLE", resourceStatus, location.getVersion(), request.operatorId()) != 1) {
throw new BizException(ResultCode.CONFLICT, "整票库位块扩容候选被并发占用");
}
LocationBlockSlotEntity added = slot(block.getId(), block.getSlotCount() + offset + 1,
location, resourceStatus);
slots.insert(added);
allocated.add(added);
}
if (blocks.expand(block.getId(), request.businessId(), block.getSlotCount(), request.requiredSlotCount(),
request.idempotencyKey(), block.getVersion()) != 1) {
throw new BizException(ResultCode.CONFLICT, "整票库位块扩容时状态发生并发变更");
}
} catch (DataIntegrityViolationException exception) {
throw new BizException(ResultCode.CONFLICT, "整票库位块扩容槽位重复或普通库位已被占用");
}
block.setSlotCount(request.requiredSlotCount());
block.setLastOperationKey(request.idempotencyKey());
return result(block, allocated);
}
@Transactional
public LocationBlockDtos.BlockResult confirm(LocationBlockDtos.ConfirmRequest request) {
LocationBlockEntity block = lockOwned(request.blockId(), request.businessId());
@@ -220,6 +276,54 @@ public class LocationBlockService {
return true;
}
private List<LocationEntity> selectAdjacentLocations(LocationBlockEntity block,
List<LocationBlockSlotEntity> allocated,
List<LocationEntity> available, int required) {
LocationBlockSlotEntity lastNormal = allocated.stream()
.filter(item -> !Boolean.TRUE.equals(item.getSharedOverflow()))
.max(java.util.Comparator.comparingInt(LocationBlockSlotEntity::getSlotNo))
.orElse(null);
if (lastNormal == null) {
return List.of();
}
LocationEntity anchor = locations.lockOwned(RESOURCE_TENANT_ID, block.getWarehouseId(), lastNormal.getLocationId());
if (anchor == null) {
throw new BizException(ResultCode.CONFLICT, "整票库位块连续段锚点不存在");
}
Matcher anchorCode = NUMBERED_CODE.matcher(anchor.getLocationCode());
if (!anchorCode.matches()) {
return List.of();
}
String prefix = anchorCode.group(1);
int expected;
try {
expected = Math.addExact(Integer.parseInt(anchorCode.group(2)), 1);
} catch (ArithmeticException exception) {
return List.of();
}
List<LocationEntity> result = new ArrayList<>(required);
for (int offset = 0; offset < required; offset++) {
int target = expected + offset;
LocationEntity next = available.stream().filter(item -> {
Matcher code = NUMBERED_CODE.matcher(item.getLocationCode());
if (!code.matches() || !Objects.equals(anchor.getZoneCode(), item.getZoneCode())
|| !prefix.equals(code.group(1))) {
return false;
}
try {
return Integer.parseInt(code.group(2)) == target;
} catch (NumberFormatException exception) {
return false;
}
}).findFirst().orElse(null);
if (next == null) {
break;
}
result.add(next);
}
return List.copyOf(result);
}
private static LocationBlockEntity block(long id, LocationBlockDtos.ReserveRequest request) {
LocationBlockEntity entity = new LocationBlockEntity();
entity.setId(id);
@@ -237,7 +341,8 @@ public class LocationBlockService {
return entity;
}
private static LocationBlockSlotEntity slot(long blockId, int slotNo, LocationEntity location) {
private static LocationBlockSlotEntity slot(long blockId, int slotNo, LocationEntity location,
String resourceStatus) {
LocationBlockSlotEntity entity = new LocationBlockSlotEntity();
entity.setId(IdWorker.getId());
entity.setTenantId(RESOURCE_TENANT_ID);
@@ -246,7 +351,7 @@ public class LocationBlockService {
entity.setLocationId(location.getId());
entity.setLocationCode(location.getLocationCode());
entity.setSharedOverflow(Boolean.TRUE.equals(location.getSharedOverflow()));
entity.setResourceStatus("RESERVED");
entity.setResourceStatus(resourceStatus);
entity.setVersion(0);
return entity;
}
@@ -23,6 +23,16 @@ paths:
content: { application/json: { schema: { $ref: '#/components/schemas/ConfirmRequest' } } }
responses:
'200': { $ref: '#/components/responses/BlockResponse' }
/internal/warehouse/v1/pallet-locations/blocks/expand:
post:
summary: 原子扩容整票库位块
description: 仅允许活动块从N扩至更大N;保留原资源标识,新增槽优先紧邻原连续普通位,缺口共享A0。
security: [{ internalServiceToken: [] }]
requestBody:
required: true
content: { application/json: { schema: { $ref: '#/components/schemas/ExpandRequest' } } }
responses:
'200': { $ref: '#/components/responses/BlockResponse' }
/internal/warehouse/v1/pallet-locations/blocks/transfer:
post:
summary: 将整段资源归属转移给余量子入库单
@@ -81,6 +91,15 @@ components:
businessId: { type: integer, format: int64, minimum: 1 }
idempotencyKey: { type: string, maxLength: 128 }
operatorId: { type: integer, format: int64, minimum: 1 }
ExpandRequest:
type: object
required: [blockId, businessId, requiredSlotCount, idempotencyKey, operatorId]
properties:
blockId: { type: integer, format: int64, minimum: 1 }
businessId: { type: integer, format: int64, minimum: 1 }
requiredSlotCount: { type: integer, minimum: 1, maximum: 200 }
idempotencyKey: { type: string, maxLength: 128 }
operatorId: { type: integer, format: int64, minimum: 1 }
TransferRequest:
type: object
required: [blockId, sourceBusinessId, targetBusinessId, targetInboundCode, idempotencyKey, operatorId]