fix: allocate pallet locations on weight confirmation
This commit is contained in:
+9
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user