fix: align PDA inventory with pallet location blocks

This commit is contained in:
2026-09-04 12:28:47 +08:00
parent b697e4c9f0
commit 8099343fca
7 changed files with 153 additions and 72 deletions
@@ -46,13 +46,6 @@ public interface WarehousePalletLocationClient {
ApiResponse<BlockResult> releaseBlock(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody BlockReleaseRequest request);
@GetMapping("/pallet-locations/occupied-resources")
ApiResponse<List<OccupiedResource>> occupiedResources(
@RequestHeader("X-Internal-Service-Token") String token,
@RequestParam("tenantId") long tenantId,
@RequestParam("sourceService") String sourceService,
@RequestParam("businessId") long businessId);
record BlockReserveRequest(long warehouseId, String sourceService, long businessId,
String inboundCode, int requiredSlotCount, String idempotencyKey,
long operatorId, LocalDateTime expiresAt) {}
@@ -65,9 +58,6 @@ public interface WarehousePalletLocationClient {
record BlockResource(long blockId, long reservationId, long locationId, String locationCode,
int slotNo, boolean sharedOverflow, String resourceStatus) {}
record BlockResult(long blockId, String blockStatus, List<BlockResource> resources) {}
record OccupiedResource(long reservationId, long occupancyId, int allocationNo, long warehouseId,
long palletId, long locationId, String locationCode, boolean sharedOverflow,
int allocatedPackageCount) {}
record PalletSnapshot(long palletId, String palletCode, String palletType,
BigDecimal capacityCbm, BigDecimal tareWeightKg) {}
}
@@ -92,6 +92,7 @@ public final class LoadingPickDtos {
public record PickItem(Long id, Long loadingPlanItemId, Long tenantId, Long inboundId, Long inboundDetailId,
String orderNo, String inboundCode, String cargoName, List<String> boxCodes,
List<InventoryResource> inventoryResources,
Integer plannedPackageCount, Integer pickedPackageCount,
String itemStatus, Integer version) {
}
@@ -131,9 +132,10 @@ public final class LoadingPickDtos {
Integer itemCount, Integer pendingItemCount) {
}
public record InventoryResource(Long reservationId, Long occupancyId, Long palletId,
public record InventoryResource(Long blockId, Integer slotNo, Long reservationId,
Long weightResultId, Integer palletSequence, Long palletId,
Long locationId, String locationCode, Boolean sharedOverflow,
Integer availablePackageCount) {
String slotStatus, Integer availablePackageCount) {
}
public record InboundInventory(Long inboundId, Integer availablePackageCount,
@@ -1,9 +1,7 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.WarehousePalletLocationClient;
import com.baiyue.firstleg.dto.LoadingPickDtos.PickItem;
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest;
@@ -34,6 +32,7 @@ import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
import com.baiyue.firstleg.loading.service.LoadingDeliveryService;
@@ -46,12 +45,11 @@ import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.InboundPalletAllocationRepository;
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import jakarta.annotation.Resource;
import feign.FeignException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@@ -80,8 +78,6 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
@Service
public class LoadingPickService {
private static final Logger log = LoggerFactory.getLogger(LoadingPickService.class);
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
private static final String SOURCE_SERVICE = "firstleg-service";
@Resource
private LoadingPickRepository repository;
@Resource
@@ -107,9 +103,7 @@ public class LoadingPickService {
@Resource
private PdaPickSettlementService settlementService;
@Resource
private WarehousePalletLocationClient warehouse;
@Value("${baiyue.security.internal-service-token:test-internal-service-token}")
private String internalToken;
private InboundPalletAllocationRepository palletAllocations;
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
@Transactional
@@ -906,14 +900,19 @@ public class LoadingPickService {
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
Map<Long, List<CartonLabelRow>> cartonsByItem = repository.cartonLabels(task.getId()).stream()
.collect(Collectors.groupingBy(CartonLabelRow::getPickItemId));
Map<Long, Long> tenantByInbound = tenantByInbound(task.getId());
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(
booking.getWarehouseId(), source, tenantByInbound);
List<PickItem> items = source.stream().map(item -> {
List<CartonLabelRow> cartons = cartonsByItem.getOrDefault(item.getId(), List.of());
CartonLabelRow first = cartons.isEmpty() ? null : cartons.get(0);
return new PickItem(item.getId(), item.getLoadingPlanItemId(), first == null ? null : first.getTenantId(),
Long tenantId = first == null ? tenantByInbound.get(item.getInboundId()) : first.getTenantId();
return new PickItem(item.getId(), item.getLoadingPlanItemId(), tenantId,
item.getInboundId(),
item.getInboundDetailId(), first == null ? null : first.getOrderNo(),
first == null ? null : first.getInboundCode(), first == null ? null : first.getCargoName(),
cartons.stream().map(CartonLabelRow::getBoxCode).toList(), item.getPlannedPackageCount(),
cartons.stream().map(CartonLabelRow::getBoxCode).toList(),
inventoryByInbound.getOrDefault(item.getInboundId(), List.of()), item.getPlannedPackageCount(),
item.getPickedPackageCount(), item.getItemStatus(), item.getVersion());
}).toList();
LoadingPickAssignmentEntity assigned = repository.assignment(task.getId());
@@ -1036,17 +1035,11 @@ public class LoadingPickService {
private InventorySnapshot buildInventorySnapshot(LoadingPickTaskEntity task, long warehouseId,
List<LoadingPickItemEntity> items) {
Map<Long, List<InventoryResource>> inventoryByInbound = inventoryByInbound(
warehouseId, items, tenantByInbound(task.getId()));
List<InboundInventory> inbounds = items.stream().map(LoadingPickItemEntity::getInboundId)
.filter(Objects::nonNull).distinct().sorted().map(inboundId -> {
List<WarehousePalletLocationClient.OccupiedResource> current = occupiedResources(inboundId);
List<InventoryResource> resources = current.stream()
.filter(resource -> resource.warehouseId() == warehouseId)
.sorted(Comparator.comparingLong(WarehousePalletLocationClient.OccupiedResource::reservationId)
.thenComparingLong(WarehousePalletLocationClient.OccupiedResource::occupancyId))
.map(resource -> new InventoryResource(resource.reservationId(), resource.occupancyId(),
resource.palletId(), resource.locationId(), resource.locationCode(),
resource.sharedOverflow(), resource.allocatedPackageCount()))
.toList();
List<InventoryResource> resources = inventoryByInbound.getOrDefault(inboundId, List.of());
int available = resources.stream().mapToInt(InventoryResource::availablePackageCount).sum();
return new InboundInventory(inboundId, available, resources);
}).toList();
@@ -1057,20 +1050,51 @@ public class LoadingPickService {
LocalDateTime.now(), token, inbounds);
}
private List<WarehousePalletLocationClient.OccupiedResource> occupiedResources(long inboundId) {
ApiResponse<List<WarehousePalletLocationClient.OccupiedResource>> response;
try {
response = warehouse.occupiedResources(internalToken, PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID,
SOURCE_SERVICE, inboundId);
} catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储实时库存查询失败,请刷新后重试");
private Map<Long, Long> tenantByInbound(long taskId) {
return repository.inboundOwners(taskId).stream().collect(Collectors.toMap(
LoadingPickItemMapper.InboundOwner::inboundId,
LoadingPickItemMapper.InboundOwner::tenantId,
(left, right) -> left));
}
private Map<Long, List<InventoryResource>> inventoryByInbound(long warehouseId,
List<LoadingPickItemEntity> items,
Map<Long, Long> tenantByInbound) {
Map<Long, List<InventoryResource>> result = new HashMap<>();
for (Long inboundId : items.stream().map(LoadingPickItemEntity::getInboundId)
.filter(Objects::nonNull).distinct().toList()) {
Long tenantId = tenantByInbound.get(inboundId);
if (tenantId == null) throw conflict("PDA任务缺少入库单客户归属");
List<InventoryResource> resources = palletAllocations.list(tenantId, inboundId).stream()
.filter(resource -> Objects.equals(resource.getWarehouseId(), warehouseId))
.filter(resource -> "CONFIRMED".equals(resource.getAllocationStatus()))
.sorted(Comparator.comparing(InboundPalletAllocationEntity::getBlockId,
Comparator.nullsLast(Long::compareTo))
.thenComparing(InboundPalletAllocationEntity::getSlotNo,
Comparator.nullsLast(Integer::compareTo)))
.map(LoadingPickService::inventoryResource)
.toList();
result.put(inboundId, resources);
}
if (response == null || !response.successful() || response.data() == null) {
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
throw new BizException(code, response == null || response.message() == null
? "仓储实时库存查询失败,请刷新后重试" : response.message());
return result;
}
private static InventoryResource inventoryResource(InboundPalletAllocationEntity resource) {
if (resource.getBlockId() == null || resource.getSlotNo() == null
|| resource.getReservationId() == null || resource.getLocationId() == null) {
throw conflict("入库单库位块快照不完整,请重新确认测量入库");
}
return response.data();
return new InventoryResource(resource.getBlockId(), resource.getSlotNo(),
resource.getReservationId(), resource.getWeightResultId(), resource.getPalletSequence(),
resource.getPalletId(), resource.getLocationId(), resource.getLocationCode(),
Boolean.TRUE.equals(resource.getSharedOverflow()), resource.getSlotStatus(), available(resource));
}
private static int available(InboundPalletAllocationEntity resource) {
int allocated = count(resource.getAllocatedPackageCount());
int picked = count(resource.getPickedPackageCount());
if (picked > allocated) throw conflict("整票库位槽已拣箱数超过分配箱数");
return allocated - picked;
}
private static String inventoryToken(LoadingPickTaskEntity task, long warehouseId,
@@ -1080,8 +1104,9 @@ public class LoadingPickService {
for (InboundInventory inbound : inbounds) {
canonical.append('|').append(inbound.inboundId()).append('|').append(inbound.availablePackageCount());
for (InventoryResource resource : inbound.resources()) {
canonical.append('|').append(resource.reservationId()).append('|').append(resource.occupancyId())
.append('|').append(resource.locationId()).append('|').append(resource.availablePackageCount());
canonical.append('|').append(resource.blockId()).append('|').append(resource.slotNo())
.append('|').append(resource.reservationId()).append('|').append(resource.locationId())
.append('|').append(resource.availablePackageCount());
}
}
return hash(canonical.toString());
@@ -201,7 +201,7 @@ components:
items: {$ref: '#/components/schemas/PickItem'}
PickItem:
type: object
required: [id, loadingPlanItemId, tenantId, inboundId, inboundDetailId, boxCodes, plannedPackageCount, pickedPackageCount, itemStatus, version]
required: [id, loadingPlanItemId, tenantId, inboundId, inboundDetailId, boxCodes, inventoryResources, plannedPackageCount, pickedPackageCount, itemStatus, version]
properties:
id: {type: integer, format: int64}
loadingPlanItemId: {type: integer, format: int64}
@@ -215,6 +215,10 @@ components:
type: array
description: Stable carton label codes; one successful scan represents one carton
items: {type: string}
inventoryResources:
type: array
description: Current authoritative whole-block slot snapshots for this inbound; never projected from legacy occupancy rows
items: {$ref: '#/components/schemas/InventoryResource'}
plannedPackageCount: {type: integer, minimum: 1}
pickedPackageCount: {type: integer, minimum: 0}
itemStatus: {type: string, enum: [PENDING, PICKING, PICKED, PARTIAL, SKIPPED]}
@@ -334,12 +338,16 @@ components:
items: {$ref: '#/components/schemas/InventoryResource'}
InventoryResource:
type: object
required: [reservationId, occupancyId, palletId, locationId, sharedOverflow, availablePackageCount]
required: [blockId, slotNo, reservationId, locationId, sharedOverflow, slotStatus, availablePackageCount]
properties:
blockId: {type: integer, format: int64}
slotNo: {type: integer, minimum: 1}
reservationId: {type: integer, format: int64}
occupancyId: {type: integer, format: int64}
palletId: {type: integer, format: int64}
weightResultId: {type: integer, format: int64, nullable: true}
palletSequence: {type: integer, minimum: 1, nullable: true}
palletId: {type: integer, format: int64, nullable: true}
locationId: {type: integer, format: int64}
locationCode: {type: string, nullable: true}
sharedOverflow: {type: boolean}
availablePackageCount: {type: integer, minimum: 1}
slotStatus: {type: string, enum: [RESERVED_EMPTY, BOUND]}
availablePackageCount: {type: integer, minimum: 0}