贴标申请,仓储转运

This commit is contained in:
2026-10-08 15:55:47 +08:00
parent e09f8785e1
commit 16d2d9111a
160 changed files with 5704 additions and 4799 deletions
@@ -258,6 +258,27 @@ public interface DispatchCandidateMapper {
@Param("orderNo") String orderNo,
@Param("relabelNo") String relabelNo);
@Select("<script>SELECT a.customer_id AS customerId,a.lastleg_order_id AS orderId,a.overseas_warehouse_id AS overseasWarehouseId," +
"MIN(ri.source_warehouse_name) AS overseasWarehouseName,a.relabel_no AS orderNo,a.relabel_no AS orderRefs," +
"GROUP_CONCAT(DISTINCT c.source_return_no ORDER BY c.source_return_no SEPARATOR ',') AS inboundCode," +
"c.pallet_code AS palletNo,COUNT(DISTINCT c.id) AS packageCount,'RELABEL' AS sourceStage,1 AS returnCartonSource," +
"GROUP_CONCAT(DISTINCT c.carton_mark ORDER BY c.carton_mark SEPARATOR ',') AS relabelCartonMarks," +
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS fbaWarehouseCode," +
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseCode " +
"FROM ll_relabel_application a JOIN ll_relabel_item ri ON ri.application_id=a.id AND ri.customer_id=a.customer_id " +
"JOIN ll_return_inventory_carton c ON c.id=ri.return_carton_id AND c.customer_id=a.customer_id " +
"AND c.relabel_application_id=a.id AND c.overseas_warehouse_id=a.overseas_warehouse_id " +
"WHERE a.delivery_method='TRUCK' AND a.status='READY_FOR_OUTBOUND' AND a.fulfillment_status='WAIT_PRE_DISPATCH' " +
"AND c.status='RELABEL_PENDING' AND c.pallet_code IS NOT NULL " +
"<if test='customerId != null and customerId != \"\"'>AND a.customer_id=#{customerId} </if>" +
"<if test='relabelNo != null and relabelNo != \"\"'>AND a.relabel_no=#{relabelNo} </if>" +
"<if test='orderNo != null and orderNo != \"\"'>AND a.relabel_no LIKE CONCAT('%',#{orderNo},'%') </if>" +
"AND NOT EXISTS(SELECT 1 FROM ll_dispatch_item di JOIN ll_dispatch_order d ON d.id=di.dispatch_order_id " +
"AND d.customer_id=di.customer_id WHERE di.customer_id=a.customer_id AND di.source_business_no=a.relabel_no AND d.status!='CANCELLED') " +
"GROUP BY a.id,a.customer_id,a.lastleg_order_id,a.overseas_warehouse_id,a.relabel_no,c.pallet_code</script>")
List<DispatchPalletCandidate> nativeReturnRelabelAvailable(@Param("customerId") String customerId,
@Param("orderNo") String orderNo, @Param("relabelNo") String relabelNo);
@org.apache.ibatis.annotations.Update("UPDATE ll_relabel_application SET fulfillment_status='PRE_DISPATCH_CREATED',updated_at=CURRENT_TIMESTAMP(3) " +
"WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} AND fulfillment_status='WAIT_PRE_DISPATCH'")
int markRelabelPreDispatched(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
@@ -44,6 +44,7 @@ public class DispatchPalletCandidate {
private String contactPhone;
private Long returnReceiptId;
private String returnReceiptNo;
private boolean returnCartonSource;
@com.fasterxml.jackson.annotation.JsonProperty("sourceType")
public String sourceType() { return returnReceiptId == null ? "INBOUND_PLAN" : "RETURN_RECEIPT"; }
@@ -8,8 +8,12 @@ import jakarta.validation.constraints.Size;
public final class DispatchPdaRequests {
private DispatchPdaRequests() {}
public record Pick(@NotNull Long positionId, @Positive long packages, @PositiveOrZero long productQuantity,
@NotBlank String idempotencyKey, String remark) {}
public record Pick(Long positionId, @Positive long packages, @PositiveOrZero long productQuantity,
@NotBlank String idempotencyKey, String remark, @Size(max = 255) String cartonMark) {
public Pick(Long positionId, long packages, long productQuantity, String idempotencyKey, String remark) {
this(positionId, packages, productQuantity, idempotencyKey, remark, null);
}
}
public record Depart(@NotBlank String idempotencyKey, String remark) {}
public record CompletePicking(@NotBlank String idempotencyKey) {}
}
@@ -16,7 +16,15 @@ public final class DispatchPdaResponses {
public record Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode,
String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel,
int scheduledPackages,
long pickedPackages, List<Position> positions) {}
long pickedPackages, List<Position> positions, List<ReturnCarton> returnCartons) {
public Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode,
String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel,
int scheduledPackages, long pickedPackages, List<Position> positions) {
this(dispatchItemId, palletNo, destinationWarehouseCode, destinationWarehouseName, fbaWarehouseCode,
warehouseRouteLabel, scheduledPackages, pickedPackages, positions, List.of());
}
}
public record ReturnCarton(Long id, String cartonMark, String status, long productQuantity) {}
public record Position(Long id, Long balanceId, long availablePackages, long allocatedPackages,
long pickedPackages) {}
public record Picked(Long dispatchItemId, Long positionId, long packages, long pickedPackages,
@@ -15,6 +15,7 @@ public class DispatchPdaPick {
private Long taskId;
private Long dispatchItemId;
private Long inventoryPositionId;
private Long returnCartonId;
private Long packages;
private Long productQuantity;
private String idempotencyKey;
@@ -16,6 +16,7 @@ public class DispatchPdaPickOrder {
private Long pickId;
private Long receiptId;
private Long returnReceiptId;
private Long returnCartonId;
private Long orderId;
private Long packages;
private Long productQuantity;
@@ -54,6 +54,7 @@ public class DispatchPdaWorkflowService {
@Resource private WarehouseTransferOrderMapper warehouseTransfers;
@Resource private DispatchPickOrderAttributionService pickAttribution;
@Resource private ObjectMapper objectMapper;
@Resource private com.baiyue.lastleg.dispatch.service.ReturnRelabelDispatchStock returnRelabelStock;
@Transactional(readOnly = true)
public List<DispatchPdaResponses.Task> list(String warehouseId, String status) {
@@ -115,13 +116,23 @@ public class DispatchPdaWorkflowService {
throw notFound("Dispatch pallet item not found");
}
if (!warehouseId.equals(item.getOverseasWarehouseId())) throw conflict("Pallet does not belong to selected warehouse");
InventoryPosition position = positions.selectById(request.positionId());
if (position == null || !customerId.equals(position.getCustomerId())
|| item.getWarehousePalletId() == null || !item.getWarehousePalletId().equals(position.getPalletId())) {
boolean nativeReturn = "RETURN_RELABEL".equals(item.getSourceStage());
if (!nativeReturn && request.positionId() == null) throw conflict("普通入仓拣货必须选择库存仓位");
if (nativeReturn && request.positionId() != null) throw conflict("退货箱拣货请扫描来源箱唛,不使用普通库存仓位");
InventoryPosition position = nativeReturn ? null : positions.selectById(request.positionId());
if (!nativeReturn && (position == null || !customerId.equals(position.getCustomerId())
|| item.getWarehousePalletId() == null || !item.getWarehousePalletId().equals(position.getPalletId()))) {
throw conflict("Inventory position does not belong to this dispatch pallet");
}
long picked = picks.totalItem(customerId, task.getId(), itemId);
if (picked + request.packages() > item.getPackageCount()) throw conflict("Picked packages exceed dispatch pallet packages");
long pickId = IdWorker.getId();
Long returnCartonId = null;
if (nativeReturn) {
pickAttribution.prepareNativeReturn(customerId, item.getOrderId());
returnCartonId = returnRelabelStock.pick(item, request.cartonMark(), request.packages(), request.productQuantity(),
pickId, order.getDispatchNo()).getId();
} else {
pickAttribution.preparePicking(customerId, request.positionId(),
"RELABEL_ORDER".equals(item.getSourceBusinessType()) ? item.getOrderId() : null);
String keyBase = "DISPATCH:" + order.getDispatchNo() + ":" + itemId + ":" + request.idempotencyKey();
@@ -142,12 +153,14 @@ public class DispatchPdaWorkflowService {
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(request.positionId(),
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
keyBase + ":SHIP", request.remark()));
}
DispatchPdaPick pick = new DispatchPdaPick();
pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId());
pick.setId(pickId); pick.setCustomerId(customerId); pick.setTaskId(task.getId()); pick.setReturnCartonId(returnCartonId);
pick.setDispatchItemId(itemId); pick.setInventoryPositionId(request.positionId()); pick.setPackages(request.packages());
pick.setProductQuantity(request.productQuantity()); pick.setIdempotencyKey(request.idempotencyKey());
pick.setPickedAt(LocalDateTime.now()); pick.setCreatedAt(pick.getPickedAt()); picks.insert(pick);
pickAttribution.attribute(customerId, pick,
if (nativeReturn) pickAttribution.attributeNativeReturn(customerId, pick, item.getOrderId());
else pickAttribution.attribute(customerId, pick,
"RELABEL_ORDER".equals(item.getSourceBusinessType()) ? item.getOrderId() : null, item.getReturnReceiptId());
long total = picked + request.packages();
order.setPickedPackageCount((int) (n(order.getPickedPackageCount()) + request.packages()));
@@ -289,6 +302,13 @@ public class DispatchPdaWorkflowService {
private DispatchPdaResponses.Task task(String customerId, DispatchOrder order, DispatchPdaTask task, List<DispatchItem> source) {
List<DispatchPdaResponses.Item> values = new ArrayList<>();
for (DispatchItem item : source) {
if ("RETURN_RELABEL".equals(item.getSourceStage())) {
values.add(new DispatchPdaResponses.Item(item.getId(), item.getPalletNo(), item.getDestinationWarehouseCode(),
item.getDestinationWarehouseName(), item.getFbaWarehouseCode(),
warehouseRouteLabel(item.getFbaWarehouseCode(), item.getDestinationWarehouseCode()), item.getPackageCount(),
picks.totalItem(customerId, task.getId(), item.getId()), List.of(), returnRelabelStock.detail(item)));
continue;
}
List<InventoryPosition> palletPositions = item.getWarehousePalletId() == null ? List.of()
: positions.listPallet(customerId, item.getWarehousePalletId());
boolean relabel = "RELABEL_ORDER".equals(item.getSourceBusinessType());
@@ -162,6 +162,24 @@ public class DispatchPickOrderAttributionService {
}
public record OrderPackages(Long orderId, long packages) {}
/** Use the same order-before-stock lock order as ordinary dispatch picks. */
@org.springframework.transaction.annotation.Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
public void prepareNativeReturn(String customerId, Long orderId) {
LastlegOrder order = orderId == null ? null : orders.lockById(customerId, orderId);
if (order == null || Boolean.TRUE.equals(order.getDetained()))
throw conflict("退货换标履约订单缺失或暂扣");
}
/** Native return boxes have their own stock ledger, but fulfill this relabel tail order. */
public void attributeNativeReturn(String customerId, DispatchPdaPick pick, Long orderId) {
LastlegOrder order = orderId == null ? null : orders.lockById(customerId, orderId);
if (order == null || Boolean.TRUE.equals(order.getDetained())) throw conflict("退货换标履约订单缺失或暂扣");
DispatchPdaPickOrder row = new DispatchPdaPickOrder();
row.setId(IdWorker.getId()); row.setCustomerId(customerId); row.setPickId(pick.getId()); row.setOrderId(orderId);
row.setReturnCartonId(pick.getReturnCartonId());
row.setPackages(pick.getPackages()); row.setProductQuantity(pick.getProductQuantity()); row.setCreatedAt(pick.getPickedAt());
if (attributions.insert(row) != 1 || orders.increasePicked(customerId, orderId, pick.getPackages()) != 1)
throw conflict("退货换标排车拣货归属或箱数不一致");
}
private static long value(Long value) { return value == null ? 0 : value; }
private static long value(Integer value) { return value == null ? 0 : value; }
private static ResponseStatusException conflict(String message) {
@@ -78,6 +78,7 @@ public class DispatchService {
@Resource private com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow;
@Resource private RelabelApplicationMapper relabelApplications;
@Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard relabelBindingGuard;
@Resource private ReturnRelabelDispatchStock returnRelabelStock;
public String resolveCustomerScopeForPlans(List<Long> planIds) {
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
@@ -122,7 +123,9 @@ public class DispatchService {
List<DispatchPalletCandidate> result = "RELABEL".equals(sourceStage) ? List.of() : candidates.available(null, null, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
trimToNull(sourceStage), trimToNull(draftToken));
if (!StringUtils.hasText(sourceStage) || Set.of("RECEIVED", "RELABEL").contains(sourceStage)) {
List<DispatchPalletCandidate> relabel = candidates.relabelAvailable(null, trimToNull(orderNo), null);
List<DispatchPalletCandidate> relabel = java.util.stream.Stream.concat(
candidates.relabelAvailable(null, trimToNull(orderNo), null).stream(),
returnRelabelStock.candidates(null, trimToNull(orderNo), null).stream()).toList();
if (relabel != null && !relabel.isEmpty()) {
result = java.util.stream.Stream.concat(result.stream(), relabel.stream().filter(item ->
!StringUtils.hasText(fbaWarehouseCode) || Objects.toString(item.fbaWarehouseCode(), "")
@@ -567,15 +570,22 @@ public class DispatchService {
@Transactional
private DispatchResponse preArrangeRelabel(String customerId, String operatorId, DispatchRequests.PreArrange request) {
RelabelApplication application = relabelApplications.findNo(customerId, request.relabelNo());
RelabelApplication application = relabelApplications.lockNo(customerId, request.relabelNo());
if (application == null || !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标尚未完成或客户尚未确认,无法进行排车");
}
relabelBindingGuard.requireBound(application);
List<DispatchPalletCandidate> selected = candidates.relabelAvailable(customerId, null, request.relabelNo());
if (!"TRUCK".equals(application.getDeliveryMethod()) || !"WAIT_PRE_DISPATCH".equals(application.getFulfillmentStatus()))
throw new ResponseStatusException(HttpStatus.CONFLICT, "换标单不是待排车状态");
List<DispatchPalletCandidate> selected = java.util.stream.Stream.concat(
candidates.relabelAvailable(customerId, null, request.relabelNo()).stream(),
returnRelabelStock.candidates(customerId, null, request.relabelNo()).stream()).toList();
Set<Long> requested = request.planPalletIds().stream().map(Long::valueOf).collect(Collectors.toSet());
if (selected.stream().anyMatch(DispatchPalletCandidate::returnCartonSource)
&& !requested.equals(selected.stream().map(DispatchPalletCandidate::warehousePalletId).collect(Collectors.toSet())))
throw new ResponseStatusException(HttpStatus.CONFLICT, "退货换标排车须选择本申请全部已绑定托盘");
selected = selected.stream().filter(c -> requested.contains(c.warehousePalletId())).toList();
if (selected.isEmpty() || selected.size() != requested.size()) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单托盘候选已变化,请刷新后重试");
if (selected.isEmpty() || selected.stream().map(DispatchPalletCandidate::warehousePalletId).distinct().count() != requested.size()) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单托盘候选已变化,请刷新后重试");
String warehouse = selected.get(0).overseasWarehouseId();
if (selected.stream().anyMatch(c -> !Objects.equals(warehouse, c.overseasWarehouseId()))) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单只能选择同一海外仓");
LocalDateTime now = LocalDateTime.now();
@@ -584,6 +594,12 @@ public class DispatchService {
dispatch.setRoute(candidateRoute(selected)); dispatch.setFbaAppointmentNo(trimToNull(request.fbaAppointmentNo())); dispatch.setFbaAppointmentStatus(defaultStatus(request.fbaAppointmentStatus())); dispatch.setVehicleAppointmentStatus(defaultStatus(request.vehicleAppointmentStatus())); dispatch.setStatus(PENDING_DEPARTURE); dispatch.setItemCount(selected.size()); dispatch.setPalletCount(selected.size()); dispatch.setPackageCount(selected.stream().map(DispatchPalletCandidate::packageCount).filter(Objects::nonNull).mapToInt(Integer::intValue).sum()); dispatch.setPickedPackageCount(0); dispatch.setShippedPackageCount(0); dispatch.setCreatedBy(trimToNull(operatorId)); dispatch.setCreatedAt(now); dispatch.setUpdatedAt(now); orders.insert(dispatch);
for (DispatchPalletCandidate candidate : selected) { DispatchItem item = new DispatchItem(); item.setId(IdWorker.getId()); item.setDispatchOrderId(dispatch.getId()); item.setCustomerId(customerId); item.setWarehousePalletId(candidate.warehousePalletId()); item.setOrderId(candidate.orderId()); item.setOverseasWarehouseId(candidate.overseasWarehouseId()); item.setOverseasWarehouseName(candidate.overseasWarehouseName()); item.setOrderNo(candidate.orderNo()); item.setOrderRefs(candidate.orderRefs()); item.setInboundCode(candidate.inboundCode()); item.setPalletNo(candidate.palletNo()); item.setDestinationWarehouseCode(candidate.destinationWarehouseCode()); item.setDestinationWarehouseName(candidate.destinationWarehouseName()); item.setFbaWarehouseCode(candidate.fbaWarehouseCode()); item.setPackageCount(Objects.requireNonNullElse(candidate.packageCount(), 0)); item.setGrossWeightKg(candidate.grossWeightKg()); item.setVolumeWeightKg(Objects.requireNonNullElse(candidate.volumeWeightKg(), BigDecimal.ZERO)); item.setVolumeCbm(Objects.requireNonNullElse(candidate.volumeCbm(), BigDecimal.ZERO)); item.setSourceStage("RELABEL"); item.setCartonMarks(candidate.relabelCartonMarks()); item.setSourceBusinessType("RELABEL_ORDER"); item.setSourceBusinessNo(request.relabelNo()); item.setCreatedAt(now); items.insert(item); }
DispatchPdaTask task = new DispatchPdaTask(); task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setDispatchOrderId(dispatch.getId()); task.setTaskNo("DPDA" + task.getId()); task.setStatus("PDA_PENDING"); task.setWarehouseId(warehouse); task.setPushedAt(now); task.setCreatedAt(now); task.setUpdatedAt(now); pdaTasks.insert(task);
for (DispatchItem item : items.findByDispatchOrder(customerId, dispatch.getId())) {
if (selected.stream().anyMatch(c -> c.returnCartonSource() && Objects.equals(c.warehousePalletId(), item.getWarehousePalletId())
&& Objects.equals(c.relabelCartonMarks(), item.getCartonMarks()))) {
item.setSourceStage("RETURN_RELABEL"); items.updateById(item); returnRelabelStock.reserve(item);
}
}
if (candidates.markRelabelPreDispatched(customerId, request.relabelNo()) != 1) throw new ResponseStatusException(HttpStatus.CONFLICT, "换标订单已被其他排车占用");
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
}
@@ -685,6 +701,8 @@ public class DispatchService {
requirePalletEditingAllowed(dispatch);
DispatchItem item = items.lockById(dispatch.getCustomerId(), dispatch.getId(), itemId);
if (item == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch pallet not found");
if ("RETURN_RELABEL".equals(item.getSourceStage()))
throw new ResponseStatusException(HttpStatus.CONFLICT, "退货换标按整单排车,请取消整张排车单后重新排车");
List<DispatchPalletLock> activeLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
.eq(DispatchPalletLock::getCustomerId, dispatch.getCustomerId())
@@ -734,10 +752,17 @@ public class DispatchService {
throw new ResponseStatusException(HttpStatus.CONFLICT, "Only a pending dispatch can be cancelled");
}
String customerId = dispatch.getCustomerId();
if (Objects.requireNonNullElse(dispatch.getPickedPackageCount(), 0) > 0)
throw new ResponseStatusException(HttpStatus.CONFLICT, "排车已开始拣货,不能取消并释放已出库箱");
if (outboundOrders.findByDispatch(customerId, dispatch.getId()) != null)
throw new ResponseStatusException(HttpStatus.CONFLICT, "已生成出库单,请先取消对应出库单再取消排车");
LocalDateTime now = LocalDateTime.now();
dispatch.setStatus(CANCELLED);
dispatch.setUpdatedAt(now);
orders.updateById(dispatch);
for (DispatchItem item : items.findByDispatchOrder(customerId, dispatch.getId())) {
if ("RETURN_RELABEL".equals(item.getSourceStage())) returnRelabelStock.release(item);
}
items.findByDispatchOrder(customerId, dispatch.getId()).stream()
.filter(item -> "RELABEL_ORDER".equals(item.getSourceBusinessType()))
.map(DispatchItem::getSourceBusinessNo).filter(StringUtils::hasText).distinct()
@@ -0,0 +1,85 @@
package com.baiyue.lastleg.dispatch.service;
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaResponses;
import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.*;
/** Native return stock stays in its own ledger; no synthetic InventoryPosition rows. */
@Service
public class ReturnRelabelDispatchStock {
@Resource private DispatchCandidateMapper candidates;
@Resource private ReturnInventoryCartonMapper cartons;
@Resource private PalletPhysicalGateway physical;
@Resource private ReturnInventoryService inventory;
public List<DispatchPalletCandidate> candidates(String customer, String orderNo, String relabelNo) {
List<DispatchPalletCandidate> result = new ArrayList<>();
for (var candidate : candidates.nativeReturnRelabelAvailable(customer, orderNo, relabelNo)) {
var pallet = physical.resolve(candidate.palletNo());
if (pallet == null || !String.valueOf(pallet.warehouseId()).equals(candidate.overseasWarehouseId())
|| !Set.of("IDLE", "OCCUPIED").contains(pallet.status())) continue;
candidate.warehousePalletId(pallet.palletId());
result.add(candidate);
}
return result;
}
public void reserve(DispatchItem item) {
for (String mark : marks(item).stream().sorted().toList()) {
var carton = require(item, mark, "RELABEL_PENDING");
carton.setStatus("OUTBOUND_PENDING"); carton.setUpdatedAt(LocalDateTime.now());
if (cartons.updateById(carton) != 1) throw conflict("退货箱排车预占失败");
}
}
public void release(DispatchItem item) {
for (String mark : marks(item).stream().sorted().toList()) {
var carton = require(item, mark, "OUTBOUND_PENDING");
carton.setStatus("RELABEL_PENDING"); carton.setUpdatedAt(LocalDateTime.now());
if (cartons.updateById(carton) != 1) throw conflict("退货箱排车取消释放失败");
}
}
public ReturnInventoryCarton pick(DispatchItem item, String mark, long packages, long quantity, long pickId, String dispatchNo) {
if (packages != 1 || !marks(item).contains(mark)) throw conflict("请扫描本排车的来源箱唛,每次拣货一箱");
var carton = require(item, mark, "OUTBOUND_PENDING");
if (quantity != carton.getTotalQuantity()) throw conflict("退货箱 SKU 总数量已变化,请刷新后重试");
inventory.shipCarton(item.getCustomerId(), item.getOverseasWarehouseId(), carton.getSourceReturnNo(),
carton.getCartonMark(), dispatchNo, pickId);
return carton;
}
public List<DispatchPdaResponses.ReturnCarton> detail(DispatchItem item) {
return marks(item).stream().map(mark -> {
var carton = cartons.findCustomerMark(item.getCustomerId(), mark);
if (carton == null || !item.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId()))
throw conflict("排车来源退货箱缺失");
return new DispatchPdaResponses.ReturnCarton(carton.getId(), mark, carton.getStatus(), carton.getTotalQuantity());
}).toList();
}
private ReturnInventoryCarton require(DispatchItem item, String mark, String status) {
var carton = cartons.lockMark(mark);
if (carton == null || !item.getCustomerId().equals(carton.getCustomerId())
|| !item.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId())
|| !Objects.equals(item.getPalletNo(), carton.getPalletCode())
|| !Objects.equals(item.getSourceBusinessNo(), carton.getRelabelNo()) || !status.equals(carton.getStatus()))
throw conflict("退货箱已被占用或来源、客户、仓库、托盘不一致");
return carton;
}
private Set<String> marks(DispatchItem item) {
return new LinkedHashSet<>(Arrays.asList(Objects.toString(item.getCartonMarks(), "").split(",")));
}
private ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
}