贴标申请,仓储转运

This commit is contained in:
2026-10-10 13:48:25 +08:00
parent 721d92ac7b
commit 12a5093b41
26 changed files with 1347 additions and 47 deletions
@@ -80,7 +80,13 @@ public final class DeliveryRequests {
@Size(max=1000) String remark,
@NotBlank @Size(max=255) String idempotencyKey,
@Size(max=128) String skuCode,
@jakarta.validation.constraints.Pattern(regexp = "BOX|PALLET") String unitType) {
@jakarta.validation.constraints.Pattern(regexp = "BOX|PALLET") String unitType,
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = WholePieceQuantityDeserializer.class)
@jakarta.validation.constraints.Positive Long quantity) {
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
List<String> cartonMarks, String remark, String idempotencyKey, String skuCode, String unitType) {
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, skuCode, unitType, null);
}
public ReturnOutbound(String recipient, String overseasWarehouseId, String returnNo,
List<String> cartonMarks, String remark, String idempotencyKey) {
this(recipient, overseasWarehouseId, returnNo, cartonMarks, remark, idempotencyKey, null, null);
@@ -114,7 +120,13 @@ public final class DeliveryRequests {
public record PdaPick(@NotNull Long outboundPalletItemId, Long inventoryPositionId,
@NotNull @Min(1) Long packages, @NotBlank @Size(max=255) String idempotencyKey,
@Size(max=500) List<@NotBlank @Size(max=255) String> cartonMarks,
@Size(max=128) String skuCode) {
@Size(max=128) String skuCode,
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = WholePieceQuantityDeserializer.class)
@jakarta.validation.constraints.Positive Long productQuantity) {
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages, String idempotencyKey,
List<String> cartonMarks, String skuCode) {
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, cartonMarks, skuCode, null);
}
public PdaPick(Long outboundPalletItemId, Long inventoryPositionId, Long packages, String idempotencyKey) {
this(outboundPalletItemId, inventoryPositionId, packages, idempotencyKey, List.of(), null);
}
@@ -92,7 +92,7 @@ public final class DeliveryResponses {
String skuCode, String inboundCode, long availablePackages,
long pickedPackages) {}
public record PdaTaskItem(Long outboundPalletItemId, String palletNo, String skuCode, String inboundCode,
int packageCount, int pickedPackageCount, String status,
List<PdaPickPosition> positions) {}
int packageCount, int pickedPackageCount, String status,
List<PdaPickPosition> positions, Long returnSkuQuantity) {}
public record PdaTaskDetail(PdaTask task, List<PdaTaskItem> items) {}
}
@@ -0,0 +1,16 @@
package com.baiyue.lastleg.delivery.dto;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
/** Piece quantities must not silently truncate a fractional JSON number. */
public final class WholePieceQuantityDeserializer extends JsonDeserializer<Long> {
@Override public Long deserialize(JsonParser parser, DeserializationContext context) throws IOException {
if (parser.currentToken() != JsonToken.VALUE_NUMBER_INT)
return (Long) context.handleUnexpectedToken(Long.class, parser);
return parser.getLongValue();
}
}
@@ -28,6 +28,8 @@ public class OutboundPalletItem {
private Integer pickedPackageCount;
private String status;
private String skuCode;
/** Immutable piece quantity for a selected single-SKU return source. Null means legacy whole-carton work. */
private Long returnSkuQuantity;
private String cartonMarks;
private String orderRefs;
private String inboundCode;
@@ -499,6 +499,8 @@ public class DeliveryTransferService {
OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
String warehouseId = request.overseasWarehouseId().trim();
if (request.quantity() != null && (!StringUtils.hasText(request.skuCode()) || request.quantity() <= 0
|| "PALLET".equals(request.unitType()))) throw conflict("按件数出库必须选择单SKU,不能同时整托出库");
List<String> marks = request.cartonMarks().stream().map(String::trim).filter(StringUtils::hasText).distinct().toList();
if (marks.size() != request.cartonMarks().size()) throw conflict("箱唛不能为空且不可重复");
LocalDateTime now = LocalDateTime.now();
@@ -514,6 +516,8 @@ public class DeliveryTransferService {
existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
if (existing != null) return replayReturnOutbound(customerId, existing, requestHash);
for (ReturnInventoryCarton carton : locked) {
if ("PARCEL".equals(carton.getSourceUnitType()) && !StringUtils.hasText(request.skuCode()))
throw conflict("包裹散件只能按 SKU 出库,不能按箱号或托盘出库");
com.baiyue.lastleg.returnreceipt.service.ReturnCartonCountGuard.requireConfirmed(carton);
if (!"AVAILABLE".equals(carton.getStatus())) throw conflict("退货箱已被其他作业占用:" + carton.getCartonMark());
var contents = returnCartonItems.lockCarton(carton.getId());
@@ -521,12 +525,22 @@ public class DeliveryTransferService {
throw conflict("退货箱缺少明细或已被合箱计划锁定:" + carton.getCartonMark());
if (StringUtils.hasText(request.skuCode()) && (contents.size() != 1
|| !request.skuCode().trim().equals(contents.get(0).getSkuCode())))
throw conflict("单SKU出库只支持指定SKU的散件库存");
if (StringUtils.hasText(request.skuCode()) && !"PARCEL".equals(carton.getSourceUnitType()))
throw conflict("已装箱库存请按箱唛出库");
throw conflict("单SKU出库只支持箱内仅有指定SKU的库存;混合箱请先拆箱");
if ("PALLET".equals(request.unitType()) && !StringUtils.hasText(carton.getPalletCode()))
throw conflict("整托出库必须存在托盘绑定");
}
Map<String, Long> skuQuantities = new LinkedHashMap<>();
if (StringUtils.hasText(request.skuCode())) {
long available = locked.stream().mapToLong(c -> value(c.getTotalQuantity())).sum();
long remaining = request.quantity() == null ? available : request.quantity();
if (remaining > available) throw conflict("出库件数超过所选来源库存数量");
for (ReturnInventoryCarton carton : locked) {
long selected = Math.min(remaining, value(carton.getTotalQuantity()));
if (selected > 0) skuQuantities.put(carton.getCartonMark(), selected);
remaining -= selected;
}
locked = locked.stream().filter(c -> skuQuantities.containsKey(c.getCartonMark())).toList();
}
if ("PALLET".equals(request.unitType())) {
String pallet = locked.get(0).getPalletCode();
if (locked.stream().anyMatch(c -> !Objects.equals(pallet, c.getPalletCode()))) throw conflict("整托出库只能选择同一托盘");
@@ -539,7 +553,7 @@ public class DeliveryTransferService {
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
outbound.setOutboundNo(orderNoGenerator.next("RETURN_OUTBOUND", warehouseId, outbound.getId()));
outbound.setOutboundType("RETURN_OUTBOUND"); outbound.setStatus("WAIT_PICK");
outbound.setRecipientInfo(request.recipient().trim()); outbound.setPackageCount(marks.size()); outbound.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
outbound.setRecipientInfo(request.recipient().trim()); outbound.setPackageCount(locked.size()); outbound.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
outbound.setOverseasWarehouseId(warehouseId); outbound.setIdempotencyKey(request.idempotencyKey().trim());
outbound.setRequestHash(requestHash);
outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now); outboundOrders.insert(outbound);
@@ -547,7 +561,9 @@ public class DeliveryTransferService {
OutboundPalletItem item = new OutboundPalletItem(); item.setId(IdWorker.getId()); item.setCustomerId(customerId);
item.setOutboundOrderId(outbound.getId()); item.setPackageCount(1); item.setPickedPackageCount(0); item.setStatus("WAIT_PICK");
item.setCartonMarks(carton.getCartonMark()); item.setInboundCode(carton.getSourceReturnNo());
item.setSkuCode(trim(request.skuCode()));
// Packed stock is still picked by its physical carton mark; loose parcels use SKU scanning.
item.setSkuCode("PARCEL".equals(carton.getSourceUnitType()) ? trim(request.skuCode()) : null);
item.setReturnSkuQuantity(skuQuantities.get(carton.getCartonMark()));
item.setPalletNo("PALLET".equals(request.unitType()) ? carton.getPalletCode() : null);
item.setCreatedAt(now); outboundPallets.insert(item);
carton.setStatus("OUTBOUND_PENDING"); carton.setUpdatedAt(now); returnCartons.updateById(carton);
@@ -557,7 +573,7 @@ public class DeliveryTransferService {
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
delivery.setOverseasWarehouseId(warehouseId); delivery.setDeliveryGroupKey("RETURN:" + request.returnNo().trim());
delivery.setStatus("WAIT_DELIVERY"); delivery.setDeliveryAddress(request.recipient().trim());
delivery.setPackageCount(marks.size()); delivery.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
delivery.setPackageCount(locked.size()); delivery.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
delivery.setRemark(trim(request.remark())); delivery.setIdempotencyKey(childKey(request.idempotencyKey(), "RETURN-DELIVERY"));
delivery.setVersion(0L); delivery.setCreatedBy(operatorId); delivery.setCreatedAt(now); delivery.setUpdatedAt(now);
deliveryOrders.insert(delivery);
@@ -822,7 +838,10 @@ public class DeliveryTransferService {
requireReturnScan(outbound, item, request);
if (value(item.getPickedPackageCount()) > 0) throw conflict("退货箱不可重复出库");
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);
returnStock.shipCarton(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(), outbound.getOutboundNo(), pick.getId());
if (item.getReturnSkuQuantity() != null)
returnStock.shipCartonQuantity(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(),
outbound.getOutboundNo(), pick.getId(), item.getReturnSkuQuantity());
else returnStock.shipCarton(customerId, warehouseId, item.getInboundCode(), item.getCartonMarks(), outbound.getOutboundNo(), pick.getId());
item.setPickedPackageCount(request.packages().intValue()); item.setStatus("PICKED"); outboundPallets.updateById(item);
updateOutboundPickingStatus(customerId, outbound);
task.setStatus("IN_PROGRESS"); task.setUpdatedAt(LocalDateTime.now()); pdaTasks.updateById(task);
@@ -1287,7 +1306,7 @@ public class DeliveryTransferService {
value(item.getPickedPackageCount()), item.getStatus(),
("RETURN_OUTBOUND".equals(outbound.getOutboundType())
|| ("RELABEL_OUTBOUND".equals(outbound.getOutboundType()) && item.getWarehousePalletId() == null)) ? returnPickPositions(outbound, task, item)
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item))).toList();
: pdaPickPositions(task.getCustomerId(), boundWarehouseId, item), item.getReturnSkuQuantity())).toList();
return new DeliveryResponses.PdaTaskDetail(taskResult(outbound, task), items);
}
@@ -1326,6 +1345,8 @@ public class DeliveryTransferService {
|| !outbound.getCustomerId().equals(item.getCustomerId())) throw notFound("退货出库明细不存在");
if (request.inventoryPositionId() != null || !Long.valueOf(1).equals(request.packages()) || value(item.getPackageCount()) != 1)
throw conflict("退货出库须逐箱或逐SKU扫描,无需库存仓位");
if (item.getReturnSkuQuantity() != null && !item.getReturnSkuQuantity().equals(request.productQuantity()))
throw conflict("实际出库件数必须等于本明细计划件数,请核对数量");
if (StringUtils.hasText(item.getSkuCode())) {
if (!Objects.equals(item.getSkuCode(), trim(request.skuCode()))) throw conflict("请扫描指定的退货SKU");
} else if (request.cartonMarks() == null || request.cartonMarks().size() != 1