fix: allocate pallet locations on weight confirmation
This commit is contained in:
@@ -38,6 +38,10 @@ public interface WarehousePalletLocationClient {
|
||||
ApiResponse<BlockResult> confirmBlock(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody BlockConfirmRequest request);
|
||||
|
||||
@PostMapping("/pallet-locations/blocks/expand")
|
||||
ApiResponse<BlockResult> expandBlock(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody BlockExpandRequest request);
|
||||
|
||||
@PostMapping("/pallet-locations/blocks/transfer")
|
||||
ApiResponse<BlockResult> transferBlock(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody BlockTransferRequest request);
|
||||
@@ -51,6 +55,8 @@ public interface WarehousePalletLocationClient {
|
||||
long operatorId, LocalDateTime expiresAt) {}
|
||||
record BlockConfirmRequest(long blockId, long businessId, String idempotencyKey,
|
||||
long operatorId) {}
|
||||
record BlockExpandRequest(long blockId, long businessId, int requiredSlotCount,
|
||||
String idempotencyKey, long operatorId) {}
|
||||
record BlockTransferRequest(long blockId, long sourceBusinessId, long targetBusinessId,
|
||||
String targetInboundCode, String idempotencyKey, long operatorId) {}
|
||||
record BlockReleaseRequest(long blockId, long businessId, String idempotencyKey,
|
||||
|
||||
@@ -67,6 +67,8 @@ public class InboundMeasurementService {
|
||||
private WarehousePalletLocationClient warehousePallets;
|
||||
@Resource
|
||||
private InboundPalletAllocationRepository palletAllocations;
|
||||
@Resource
|
||||
private MeasurementLocationOccupationService locationOccupation;
|
||||
|
||||
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
@@ -296,6 +298,8 @@ public class InboundMeasurementService {
|
||||
request.adjustedWeightValue(), request.adjustedWeightUnit(),
|
||||
request.adjustmentReason(), operatorId, operatorName);
|
||||
} else if (WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) {
|
||||
locationOccupation.ensureForConfirmedWeight(
|
||||
tenantId, inboundId, operatorId, weight.getId(), weight.getVersion());
|
||||
return toWeight(weight);
|
||||
}
|
||||
BigDecimal value = effectiveValue(weight);
|
||||
@@ -324,10 +328,9 @@ public class InboundMeasurementService {
|
||||
weight.setVersion(confirmedVersion);
|
||||
insertRevision(weight, confirmedVersion, "CONFIRM", value, unit, value, unit,
|
||||
grossWeightKg, netWeightKg, null, operatorId, operatorName);
|
||||
if (!palletAllocations.list(tenantId, inboundId).isEmpty()) {
|
||||
palletAllocations.bindNextEmpty(tenantId, inboundId, weight);
|
||||
}
|
||||
recalculateConfirmedWeight(inbound, operatorId);
|
||||
locationOccupation.ensureForConfirmedWeight(
|
||||
tenantId, inboundId, operatorId, weight.getId(), confirmedVersion);
|
||||
return toWeight(weight);
|
||||
}
|
||||
|
||||
|
||||
+87
-8
@@ -23,7 +23,6 @@ import java.util.UUID;
|
||||
@Service
|
||||
public class MeasurementLocationOccupationService {
|
||||
private static final String SOURCE_SERVICE = "firstleg-service";
|
||||
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
|
||||
@Resource
|
||||
private WarehousePalletLocationClient warehouse;
|
||||
@Resource
|
||||
@@ -37,26 +36,43 @@ public class MeasurementLocationOccupationService {
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
public void occupy(long tenantId, long inboundId, long operatorId, long measurementId,
|
||||
int measurementVersion) {
|
||||
public void ensureForConfirmedWeight(long tenantId, long inboundId, long operatorId,
|
||||
long weightResultId, int weightVersion) {
|
||||
InboundOrderEntity order = requireOrder(tenantId, inboundId);
|
||||
long warehouseId = requireWarehouse(order);
|
||||
BigDecimal volume = requireVolume(order);
|
||||
var confirmedWeights = confirmedWeights(tenantId, inboundId);
|
||||
int requiredSlotCount = requiredSlotCount(volume, confirmedWeights.size());
|
||||
int packageCount = requirePackageCount(order);
|
||||
var current = allocations.list(tenantId, inboundId);
|
||||
if (!current.isEmpty()) {
|
||||
long blockId = requireActiveBlock(current, warehouseId);
|
||||
if (current.size() < requiredSlotCount) {
|
||||
String expandKey = "measure-block-expand-" + inboundId + "-" + weightResultId + "-" + weightVersion;
|
||||
WarehousePalletLocationClient.BlockResult expanded = requireSuccess(call(() -> warehouse.expandBlock(
|
||||
internalToken, new WarehousePalletLocationClient.BlockExpandRequest(blockId, inboundId,
|
||||
requiredSlotCount, expandKey, operatorId))), "称重确认后整票库位块扩容失败");
|
||||
requireBlockResult(expanded, requiredSlotCount, "OCCUPIED");
|
||||
if (expanded.blockId() != blockId) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Warehouse扩容返回了不同整票库位块");
|
||||
}
|
||||
requirePreservedSlots(current, expanded);
|
||||
allocations.replaceBlock(tenantId, inboundId, warehouseId, packageCount, volume, expanded);
|
||||
}
|
||||
allocations.bindConfirmedWeights(tenantId, inboundId, confirmedWeights);
|
||||
return;
|
||||
}
|
||||
|
||||
// Warehouse retains released blocks for audit. A compensated attempt therefore needs a new
|
||||
// reservation key, otherwise a retry would replay the released block instead of reserving again.
|
||||
String key = "measure-block-" + inboundId + "-" + measurementId + "-" + measurementVersion
|
||||
String key = "measure-block-" + inboundId + "-" + weightResultId + "-" + weightVersion
|
||||
+ "-" + UUID.randomUUID();
|
||||
WarehousePalletLocationClient.BlockResult reserved = requireSuccess(call(() -> warehouse.reserveBlock(
|
||||
internalToken, new WarehousePalletLocationClient.BlockReserveRequest(warehouseId,
|
||||
SOURCE_SERVICE, inboundId, order.getInboundCode(), requiredSlotCount, key, operatorId,
|
||||
LocalDateTime.now().plus(properties.getReservationTtl())))),
|
||||
"测量完成后库位预占失败");
|
||||
int packageCount = requirePackageCount(order);
|
||||
requireBlockResult(reserved, requiredSlotCount, "RESERVED");
|
||||
allocations.replaceBlock(tenantId, inboundId, warehouseId, packageCount, volume, reserved);
|
||||
allocations.bindConfirmedWeights(tenantId, inboundId, confirmedWeights);
|
||||
try {
|
||||
WarehousePalletLocationClient.BlockResult occupied = requireSuccess(call(() -> warehouse.confirmBlock(
|
||||
internalToken, new WarehousePalletLocationClient.BlockConfirmRequest(reserved.blockId(), inboundId,
|
||||
@@ -75,6 +91,68 @@ public class MeasurementLocationOccupationService {
|
||||
}
|
||||
}
|
||||
|
||||
public void requireOccupied(long tenantId, long inboundId) {
|
||||
InboundOrderEntity order = requireOrder(tenantId, inboundId);
|
||||
long warehouseId = requireWarehouse(order);
|
||||
var confirmedWeights = confirmedWeights(tenantId, inboundId);
|
||||
int requiredSlotCount = requiredSlotCount(requireVolume(order), confirmedWeights.size());
|
||||
var current = allocations.list(tenantId, inboundId);
|
||||
requireActiveBlock(current, warehouseId);
|
||||
if (current.size() < requiredSlotCount) {
|
||||
throw new BizException(ResultCode.CONFLICT, "整票库位块容量不足,请重新确认称重图片");
|
||||
}
|
||||
requireAllWeightsBound(current, confirmedWeights);
|
||||
}
|
||||
|
||||
private long requireActiveBlock(java.util.List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> current,
|
||||
long warehouseId) {
|
||||
if (current.isEmpty() || current.get(0).getBlockId() == null || current.get(0).getBlockId() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "称重图片尚未建立整票库位块");
|
||||
}
|
||||
long blockId = current.get(0).getBlockId();
|
||||
if (current.stream().anyMatch(slot -> !Long.valueOf(blockId).equals(slot.getBlockId())
|
||||
|| !Long.valueOf(warehouseId).equals(slot.getWarehouseId())
|
||||
|| !"CONFIRMED".equals(slot.getAllocationStatus())
|
||||
|| slot.getSlotNo() == null || slot.getSlotNo() <= 0
|
||||
|| slot.getReservationId() == null || slot.getReservationId() <= 0
|
||||
|| slot.getLocationId() == null || slot.getLocationId() <= 0)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "整票库位块快照不完整或状态异常");
|
||||
}
|
||||
java.util.List<Integer> slotNos = current.stream().map(item -> item.getSlotNo()).sorted().toList();
|
||||
if (!slotNos.equals(java.util.stream.IntStream.rangeClosed(1, current.size()).boxed().toList())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "整票库位块槽序不连续");
|
||||
}
|
||||
return blockId;
|
||||
}
|
||||
|
||||
private static void requireAllWeightsBound(
|
||||
java.util.List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> current,
|
||||
java.util.List<InboundWeightResultEntity> confirmedWeights) {
|
||||
java.util.Set<Long> bound = current.stream()
|
||||
.filter(item -> "BOUND".equals(item.getSlotStatus()))
|
||||
.map(item -> item.getWeightResultId()).filter(java.util.Objects::nonNull)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (confirmedWeights.stream().map(InboundWeightResultEntity::getId).anyMatch(id -> !bound.contains(id))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "已确认称重图片未完整绑定整票库位槽");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requirePreservedSlots(
|
||||
java.util.List<com.baiyue.firstleg.entity.InboundPalletAllocationEntity> current,
|
||||
WarehousePalletLocationClient.BlockResult expanded) {
|
||||
java.util.Map<Integer, WarehousePalletLocationClient.BlockResource> bySlot = expanded.resources().stream()
|
||||
.collect(java.util.stream.Collectors.toMap(WarehousePalletLocationClient.BlockResource::slotNo,
|
||||
java.util.function.Function.identity()));
|
||||
if (current.stream().anyMatch(slot -> {
|
||||
WarehousePalletLocationClient.BlockResource resource = bySlot.get(slot.getSlotNo());
|
||||
return resource == null || resource.reservationId() != slot.getReservationId()
|
||||
|| resource.locationId() != slot.getLocationId()
|
||||
|| resource.sharedOverflow() != Boolean.TRUE.equals(slot.getSharedOverflow());
|
||||
})) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Warehouse扩容改变了既有库位槽");
|
||||
}
|
||||
}
|
||||
|
||||
private InboundOrderEntity requireOrder(long tenantId, long inboundId) {
|
||||
InboundOrderEntity order = workflow.findOwned(tenantId, inboundId);
|
||||
if (order == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
||||
@@ -154,7 +232,8 @@ public class MeasurementLocationOccupationService {
|
||||
int requiredSlotCount, String expectedStatus) {
|
||||
if (result.blockId() <= 0 || !expectedStatus.equals(result.blockStatus())
|
||||
|| result.resources() == null || result.resources().size() != requiredSlotCount
|
||||
|| result.resources().stream().anyMatch(resource -> resource.blockId() != result.blockId())) {
|
||||
|| result.resources().stream().anyMatch(resource -> resource.blockId() != result.blockId()
|
||||
|| !expectedStatus.equals(resource.resourceStatus()))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Warehouse返回的整票库位块不完整");
|
||||
}
|
||||
java.util.List<Integer> slotNos = result.resources().stream()
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ public class MeasurementSubmissionService {
|
||||
long tenantId, long inboundId, long operatorId,
|
||||
InboundMeasurementRequests.SubmitRequest request) {
|
||||
var result = measurements.submit(tenantId, inboundId, operatorId, request);
|
||||
locations.occupy(tenantId, inboundId, operatorId, result.measurementId(), result.version());
|
||||
locations.requireOccupied(tenantId, inboundId);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -996,6 +996,9 @@ paths:
|
||||
分别写入 CORRECT 和 CONFIRM 修订记录。输入表示含托盘毛重;服务端统一换算为 KG,
|
||||
使用创建时固化的托盘皮重计算 netWeightKg,且毛重必须大于皮重。
|
||||
入库单 actualGrossWeightKg 为实测毛重;actualNetWeightKg 为扣除托盘皮重后的货物净重,两者不得混用。
|
||||
首张图片确认成功时按实测体积容量与已确认图片数建立并占用整票库位块;后续图片绑定下一个空槽。
|
||||
所需槽数增长时原子扩容原块,既有 blockId、slotNo、reservationId 和 locationId 保持不变,
|
||||
普通连续位不足的新增槽使用同仓共享 A0。确认响应立即返回图片对应的权威块槽绑定。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
@@ -1081,12 +1084,12 @@ paths:
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/submit:
|
||||
post:
|
||||
summary: 提交测量完成
|
||||
description: 测量提交后立即按总体积拆分普通托盘、分配并确认占用库位,再进入客服待审核;无普通空闲位时共享 A0。
|
||||
description: 提交只校验并复用称重图片确认阶段已经 OCCUPIED 且绑定完整的权威整票库位块,然后进入客服待审核;不会首次分配、重建或更换已显示槽位。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
'200': {description: 成功时 processStatus=MEASURE_PENDING、measureReviewStatus=PENDING,且库位已 OCCUPIED}
|
||||
'200': {description: 成功时 processStatus=MEASURE_PENDING、measureReviewStatus=PENDING,且复用的整票库位块保持 OCCUPIED}
|
||||
'403': {description: 无测量提交权限}
|
||||
/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing:
|
||||
get:
|
||||
@@ -1728,7 +1731,7 @@ components:
|
||||
palletLocationGuide:
|
||||
allOf: [{$ref: '#/components/schemas/InboundPalletLocationGuide'}]
|
||||
nullable: true
|
||||
description: 服务端权威的整票上架引导;尚未预留库位时为空,禁止由 weightResults 数量或数组下标推导
|
||||
description: 服务端权威的整票上架引导;首张称重图片确认前为空,确认成功后立即返回并在后续确认时原位扩容,禁止由 weightResults 数量或数组下标推导
|
||||
InboundPalletLocationGuide:
|
||||
type: object
|
||||
required: [blockId, reservedSlotCount, actualPalletCount, reservedEmptySlotCount, slots]
|
||||
|
||||
Reference in New Issue
Block a user