This commit is contained in:
2026-09-15 20:52:02 +08:00
parent 4b671bb0ab
commit 8b19085bc4
24 changed files with 387 additions and 38 deletions
@@ -27,6 +27,15 @@ public class OutboundPdaController {
identity.operatorKey(), taskNo, request));
}
@PostMapping("/{taskNo}/pallets/{palletNo}/pick")
public ApiResponse<DeliveryResponses.PdaTask> pickPallet(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String taskNo, @PathVariable String palletNo,
@Valid @RequestBody DeliveryRequests.PdaPalletPick request) {
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
return ApiResponse.success(service.pdaPickPallet(String.valueOf(identity.overseasWarehouseId()),
identity.operatorKey(), taskNo, palletNo, request));
}
@GetMapping
public ApiResponse<java.util.List<DeliveryResponses.PdaTask>> list(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required=false) String status) {
@@ -89,6 +89,7 @@ public final class DeliveryRequests {
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, List.of());
}
}
public record PdaPalletPick(@NotBlank @Size(max=255) String idempotencyKey) {}
public record PdaComplete(@NotBlank @Size(max=255) String idempotencyKey) {}
public record Cancel(@NotBlank @Size(max=1000) String reason) {}
}
@@ -78,6 +78,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.stream.Collectors;
@Service
public class DeliveryTransferService {
@@ -476,7 +477,7 @@ public class DeliveryTransferService {
}
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(null); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
outbound.setStatus("PICKED"); outbound.setUpdatedAt(LocalDateTime.now()); outboundOrders.updateById(outbound);
updateOutboundPickingStatus(customerId, outbound);
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
return taskResult(outbound, task);
}
@@ -529,10 +530,7 @@ public class DeliveryTransferService {
pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(request.inventoryPositionId());
pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
List<OutboundPalletItem> pickedItems = outboundPallets.findByOutbound(customerId, outbound.getId());
if (!pickedItems.isEmpty() && pickedItems.stream().allMatch(value -> "PICKED".equals(value.getStatus()))) {
outbound.setStatus("PICKED"); outbound.setUpdatedAt(LocalDateTime.now()); outboundOrders.updateById(outbound);
}
updateOutboundPickingStatus(customerId, outbound);
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
return taskResult(outbound, task);
}
@@ -578,14 +576,69 @@ public class DeliveryTransferService {
for (DropshipCarton carton : cartons) { carton.setStatus("SHIPPED"); carton.setOutboundOrderNo(outbound.getOutboundNo()); carton.setVersion((carton.getVersion() == null ? 0 : carton.getVersion()) + 1); carton.setUpdatedAt(LocalDateTime.now()); dropshipCartons.updateById(carton); }
OutboundPdaPick pick = new OutboundPdaPick(); pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setOutboundPalletItemId(item.getId()); pick.setInventoryPositionId(request.inventoryPositionId()); pick.setPackages(request.packages()); pick.setIdempotencyKey(request.idempotencyKey().trim()); pick.setPickedAt(LocalDateTime.now()); pdaPicks.insert(pick);
item.setPickedPackageCount(value(item.getPickedPackageCount()) + request.packages().intValue()); item.setStatus(item.getPickedPackageCount() >= item.getPackageCount() ? "PICKED" : "PARTIAL_PICKED"); outboundPallets.updateById(item);
List<OutboundPalletItem> pickedItems = outboundPallets.findByOutbound(customerId, outbound.getId());
if (!pickedItems.isEmpty() && pickedItems.stream().allMatch(i -> "PICKED".equals(i.getStatus()))) {
outbound.setStatus("PICKED"); outbound.setUpdatedAt(LocalDateTime.now()); outboundOrders.updateById(outbound);
}
updateOutboundPickingStatus(customerId, outbound);
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
return taskResult(outbound, task);
}
@Transactional
public DeliveryResponses.PdaTask pdaPickPallet(String warehouseId, String operatorId, String taskNo,
String palletNo, DeliveryRequests.PdaPalletPick request) {
String boundWarehouseId = required(warehouseId, "PDA warehouse binding is required");
String code = required(taskNo, "PDA task number is required");
String palletCode = required(palletNo, "Pallet number is required");
OutboundPdaTask task = pdaTasks.lockByNoAndWarehouse(code, boundWarehouseId);
if (task == null) throw notFound("PDA task not found in the bound warehouse");
OutboundOrder outbound = outboundOrders.lockByNo(task.getCustomerId(), requireOutbound(task).getOutboundNo());
if (outbound == null || !boundWarehouseId.equals(outbound.getOverseasWarehouseId())) {
throw notFound("Outbound order not found in the bound warehouse");
}
if (!"ONE_CLICK_DROPSHIP".equals(outbound.getOutboundType())) {
throw conflict("整托扫码仅适用于一件代发出库");
}
if (!Set.of("PENDING", "IN_PROGRESS").contains(task.getStatus())) {
throw conflict("PDA task is not available for picking");
}
List<OutboundPalletItem> palletItems = outboundPallets.findByOutbound(task.getCustomerId(), outbound.getId())
.stream().filter(item -> palletCode.equals(item.getPalletNo())).toList();
if (palletItems.isEmpty()) throw notFound("当前出库任务没有该托盘");
if (palletItems.stream().allMatch(item -> value(item.getPickedPackageCount()) >= value(item.getPackageCount()))) {
return taskResult(outbound, task);
}
List<PalletPickPlan> plans = new ArrayList<>();
int sequence = 0;
for (OutboundPalletItem item : palletItems) {
long remaining = value(item.getPackageCount()) - value(item.getPickedPackageCount());
if (remaining <= 0) continue;
Map<Long, List<DeliveryResponses.PdaPickPosition>> positionsById = pdaPickPositions(
task.getCustomerId(), boundWarehouseId, item).stream().collect(Collectors.groupingBy(
DeliveryResponses.PdaPickPosition::inventoryPositionId, LinkedHashMap::new, Collectors.toList()));
for (Map.Entry<Long, List<DeliveryResponses.PdaPickPosition>> entry : positionsById.entrySet()) {
if (remaining <= 0) break;
List<String> marks = entry.getValue().stream().map(DeliveryResponses.PdaPickPosition::cartonMark)
.filter(StringUtils::hasText).distinct().toList();
long available = marks.isEmpty()
? entry.getValue().stream().mapToLong(DeliveryResponses.PdaPickPosition::availablePackages).sum()
: marks.size();
long packages = Math.min(remaining, available);
if (packages <= 0) continue;
List<String> selectedMarks = marks.isEmpty() ? List.of() : marks.stream().limit(packages).toList();
plans.add(new PalletPickPlan(item.getId(), entry.getKey(), packages, selectedMarks, sequence++));
remaining -= packages;
}
if (remaining > 0) throw conflict("托盘可用库存不足,整托拣货未执行:" + palletCode);
}
DeliveryResponses.PdaTask result = taskResult(outbound, task);
for (PalletPickPlan plan : plans) {
String operationId = palletPickOperationId(request.idempotencyKey(), code, palletCode, plan);
result = pdaPick(boundWarehouseId, operatorId, code, new DeliveryRequests.PdaPick(
plan.itemId(), plan.positionId(), plan.packages(), operationId, plan.cartonMarks()));
}
return result;
}
@Transactional
public DeliveryResponses.PdaTask completePda(String warehouseId, String taskNo,
DeliveryRequests.PdaComplete request) {
@@ -909,6 +962,27 @@ public class DeliveryTransferService {
.stream().map(LastlegOrder::getLxCode).filter(StringUtils::hasText)
.map(String::trim).distinct().collect(java.util.stream.Collectors.joining(","));
}
private void updateOutboundPickingStatus(String customerId, OutboundOrder outbound) {
List<OutboundPalletItem> items = outboundPallets.findByOutbound(customerId, outbound.getId());
String nextStatus = !items.isEmpty() && items.stream().allMatch(item -> "PICKED".equals(item.getStatus()))
? "PICKED" : "PICKING";
outbound.setStatus(nextStatus);
outbound.setUpdatedAt(LocalDateTime.now());
outboundOrders.updateById(outbound);
}
private static String palletPickOperationId(String idempotencyKey, String taskNo, String palletNo,
PalletPickPlan plan) {
String source = required(idempotencyKey, "Idempotency key is required") + '\u0000' + taskNo + '\u0000'
+ palletNo + '\u0000' + plan.itemId() + '\u0000' + plan.positionId() + '\u0000' + plan.sequence();
try {
return "PALLET_PICK:" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(source.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException error) {
throw new IllegalStateException("无法生成整托拣货幂等号", error);
}
}
private record PalletPickPlan(Long itemId, Long positionId, long packages,
List<String> cartonMarks, int sequence) {}
private BigDecimal totalWeight(String customerId, List<DispatchItem> items) {
List<Long> palletIds = items.stream().map(DispatchItem::getWarehousePalletId)
.filter(Objects::nonNull).distinct().toList();