feat(lastleg): redispatch returned cargo and reconcile source signatures
This commit is contained in:
+3
-3
@@ -75,7 +75,7 @@ public class SmartDispatchController {
|
||||
@Valid @RequestBody DispatchRequests.PreArrange request) {
|
||||
requirePlatform(user);
|
||||
String scopedCustomer = request.relabelNo() == null || request.relabelNo().isBlank()
|
||||
? service.resolveCustomerScopeForPallets(request.planPalletIds())
|
||||
? service.resolveCustomerScopeForSources(request.planPalletIds(), request.returnReceiptIds())
|
||||
: service.resolveCustomerScopeForRelabel(request.relabelNo());
|
||||
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
return ApiResponse.success(service.preArrange(scopedCustomer, operatorId, request));
|
||||
@@ -88,7 +88,7 @@ public class SmartDispatchController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody DispatchRequests.DraftPallet request) {
|
||||
requirePlatform(user);
|
||||
String customerId = service.resolveCustomerScopeForPallets(List.of(request.planPalletId()));
|
||||
String customerId = service.resolveCustomerScopeForSources(request.planPalletId() == null ? List.of() : List.of(request.planPalletId()), request.returnReceiptId() == null ? List.of() : List.of(request.returnReceiptId()));
|
||||
service.lockDraftPallet(customerId, operatorId(user), request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
@@ -113,7 +113,7 @@ public class SmartDispatchController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody DispatchRequests.DraftPallet request) {
|
||||
requirePlatform(user);
|
||||
String customerId = service.resolveCustomerScopeForPallets(List.of(request.planPalletId()));
|
||||
String customerId = service.resolveCustomerScopeForSources(request.planPalletId() == null ? List.of() : List.of(request.planPalletId()), request.returnReceiptId() == null ? List.of() : List.of(request.returnReceiptId()));
|
||||
service.unlockDraftPallet(customerId, operatorId(user), request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
+19
-3
@@ -19,12 +19,21 @@ public final class DispatchRequests {
|
||||
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||
}
|
||||
|
||||
public record PreArrange(@NotEmpty @Size(max = 500) List<@NotNull Long> planPalletIds,
|
||||
public record PreArrange(@Size(max = 500) List<@NotNull Long> planPalletIds,
|
||||
@Size(max = 64) String fbaAppointmentNo,
|
||||
@Size(max = 32) String fbaAppointmentStatus,
|
||||
@Size(max = 32) String vehicleAppointmentStatus,
|
||||
@Size(max = 64) String draftToken,
|
||||
@Size(max = 64) String relabelNo) {
|
||||
@Size(max = 64) String relabelNo,
|
||||
@Size(max = 500) List<@NotNull Long> returnReceiptIds) {
|
||||
public PreArrange {
|
||||
planPalletIds = planPalletIds == null ? List.of() : List.copyOf(planPalletIds);
|
||||
returnReceiptIds = returnReceiptIds == null ? List.of() : List.copyOf(returnReceiptIds);
|
||||
}
|
||||
public PreArrange(List<Long> planPalletIds, String fbaAppointmentNo, String fbaAppointmentStatus,
|
||||
String vehicleAppointmentStatus, String draftToken, String relabelNo) {
|
||||
this(planPalletIds, fbaAppointmentNo, fbaAppointmentStatus, vehicleAppointmentStatus, draftToken, relabelNo, List.of());
|
||||
}
|
||||
public PreArrange(List<Long> planPalletIds, String fbaAppointmentNo,
|
||||
String fbaAppointmentStatus, String vehicleAppointmentStatus) {
|
||||
this(planPalletIds, fbaAppointmentNo, fbaAppointmentStatus, vehicleAppointmentStatus, null, null);
|
||||
@@ -35,8 +44,15 @@ public final class DispatchRequests {
|
||||
}
|
||||
}
|
||||
|
||||
public record DraftPallet(@NotNull Long planPalletId, @NotBlank @Size(max = 64) String draftToken, Long draftContainerId) {
|
||||
public record DraftPallet(Long planPalletId, @NotBlank @Size(max = 64) String draftToken, Long draftContainerId,
|
||||
Long returnReceiptId, String sourceType) {
|
||||
public DraftPallet(Long planPalletId, String draftToken, Long draftContainerId) { this(planPalletId, draftToken, draftContainerId, null, "INBOUND_PLAN"); }
|
||||
public DraftPallet(Long planPalletId, String draftToken) { this(planPalletId, draftToken, null); }
|
||||
@jakarta.validation.constraints.AssertTrue(message = "请选择唯一且匹配类型的实物来源")
|
||||
public boolean isValidSource() {
|
||||
return returnReceiptId != null ? planPalletId == null && "RETURN_RECEIPT".equals(sourceType)
|
||||
: planPalletId != null && (sourceType == null || "INBOUND_PLAN".equals(sourceType));
|
||||
}
|
||||
}
|
||||
|
||||
public record DraftContainer(@NotBlank @Size(max = 64) String draftToken, @NotNull Integer capacity) {}
|
||||
|
||||
@@ -18,6 +18,7 @@ public class DispatchItem {
|
||||
private String customerId;
|
||||
private Long planId;
|
||||
private Long planPalletId;
|
||||
private Long returnReceiptId;
|
||||
private Long warehousePalletId;
|
||||
private Long orderId;
|
||||
private String overseasWarehouseId;
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ public class DispatchPalletLock {
|
||||
private String activeKey;
|
||||
private Long planId;
|
||||
private Long planPalletId;
|
||||
private Long returnReceiptId;
|
||||
private Long warehousePalletId;
|
||||
private Long dispatchOrderId;
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.lastleg.dispatch.mapper;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReturnDispatchCandidateMapper {
|
||||
String SELECT = "SELECT r.id AS returnReceiptId,r.receipt_no AS returnReceiptNo,r.order_id AS orderId," +
|
||||
"r.customer_id AS customerId,o.customer_name AS customerName,o.order_type AS orderType,o.order_no AS orderNo," +
|
||||
"o.order_no AS orderRefs,b.inbound_code AS inboundCode,p.pallet_id AS warehousePalletId,p.pallet_no AS palletNo," +
|
||||
"p.overseas_warehouse_id AS overseasWarehouseId,b.overseas_warehouse_name AS overseasWarehouseName," +
|
||||
"b.overseas_warehouse_code AS overseasWarehouseCode,d.destination_type AS destinationType," +
|
||||
"d.destination_warehouse_code AS destinationWarehouseCode,d.destination_warehouse_code AS fbaWarehouseCode," +
|
||||
"d.destination_warehouse_name AS destinationWarehouseName,d.delivery_address AS deliveryAddress,d.postal_code AS postalCode," +
|
||||
"d.contact_name AS contactName,d.contact_phone AS contactPhone,'RECEIVED' AS sourceStage," +
|
||||
"LEAST(p.available_packages,r.received_packages-(SELECT COALESCE(SUM(a.packages),0) FROM ll_dispatch_pda_pick_order a " +
|
||||
"WHERE a.customer_id=r.customer_id AND a.return_receipt_id=r.id)) AS packageCount " +
|
||||
"FROM ll_return_stock_receipt r JOIN ll_order o ON o.customer_id=r.customer_id AND o.id=r.order_id " +
|
||||
"JOIN ll_inventory_position p ON p.customer_id=r.customer_id AND p.id=r.inventory_position_id " +
|
||||
"JOIN ll_inventory_balance b ON b.customer_id=r.customer_id AND b.id=r.inventory_balance_id " +
|
||||
"JOIN ll_exception_order e ON e.customer_id=r.customer_id AND e.id=r.exception_order_id " +
|
||||
"JOIN ll_delivery_order d ON d.customer_id=r.customer_id AND d.id=e.delivery_order_id ";
|
||||
String ELIGIBLE = " AND o.detained=0 AND p.available_packages>0 AND r.received_packages>(SELECT COALESCE(SUM(a.packages),0) " +
|
||||
"FROM ll_dispatch_pda_pick_order a WHERE a.customer_id=r.customer_id AND a.return_receipt_id=r.id) ";
|
||||
@Select("<script>" + SELECT + "WHERE 1=1 " + ELIGIBLE +
|
||||
"<if test='customerId != null'>AND r.customer_id=#{customerId} </if>" +
|
||||
"<if test='orderNo != null'>AND (o.order_no LIKE CONCAT('%',#{orderNo},'%') OR b.inbound_code LIKE CONCAT('%',#{orderNo},'%') OR r.receipt_no LIKE CONCAT('%',#{orderNo},'%')) </if>" +
|
||||
"<if test='fba != null'>AND d.destination_warehouse_code=#{fba} </if>" +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock k WHERE k.customer_id=r.customer_id " +
|
||||
"AND k.source_pallet_key=CONCAT('RECEIVED:',p.pallet_id) AND k.status='ACTIVE' " +
|
||||
"AND (k.expires_at IS NULL OR k.expires_at>CURRENT_TIMESTAMP) AND (#{draftToken} IS NULL OR k.draft_token IS NULL OR k.draft_token != #{draftToken})) " +
|
||||
"ORDER BY r.received_at,r.id</script>")
|
||||
List<DispatchPalletCandidate> available(@Param("customerId") String customerId, @Param("orderNo") String orderNo,
|
||||
@Param("fba") String fba, @Param("draftToken") String draftToken);
|
||||
|
||||
@Select("<script>" + SELECT + "WHERE r.customer_id=#{customerId} AND r.id IN " +
|
||||
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>" + ELIGIBLE + " ORDER BY r.id FOR UPDATE</script>")
|
||||
List<DispatchPalletCandidate> lockSelected(@Param("customerId") String customerId, @Param("ids") List<Long> ids);
|
||||
|
||||
@Select("<script>SELECT DISTINCT customer_id FROM ll_return_stock_receipt WHERE id IN " +
|
||||
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")
|
||||
List<String> customers(@Param("ids") List<Long> ids);
|
||||
}
|
||||
+81
-36
@@ -3,42 +3,87 @@ package com.baiyue.lastleg.dispatch.model;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record DispatchPalletCandidate(
|
||||
Long planId,
|
||||
Long planPalletId,
|
||||
Long warehousePalletId,
|
||||
Long orderId,
|
||||
String customerId,
|
||||
String customerName,
|
||||
String planNo,
|
||||
String orderType,
|
||||
BigDecimal volumeWeightKg,
|
||||
BigDecimal grossWeightKg,
|
||||
String containerNo,
|
||||
LocalDateTime appointmentAt,
|
||||
LocalDateTime expectedLoadingAt,
|
||||
String overseasWarehouseId,
|
||||
String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
String orderNo,
|
||||
String orderRefs,
|
||||
String inboundCode,
|
||||
String palletNo,
|
||||
String destinationWarehouseId,
|
||||
String destinationWarehouseCode,
|
||||
String destinationWarehouseName,
|
||||
String fbaWarehouseCode,
|
||||
Integer packageCount,
|
||||
BigDecimal volumeCbm,
|
||||
String sourceStage,
|
||||
String cartonMarks,
|
||||
String relabelCartonMarks,
|
||||
String destinationType,
|
||||
String deliveryAddress,
|
||||
String postalCode,
|
||||
String contactName,
|
||||
String contactPhone
|
||||
) {
|
||||
@lombok.Data
|
||||
@lombok.NoArgsConstructor
|
||||
@lombok.experimental.Accessors(fluent = true)
|
||||
@com.fasterxml.jackson.annotation.JsonAutoDetect(fieldVisibility = com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY)
|
||||
public class DispatchPalletCandidate {
|
||||
private Long planId;
|
||||
private Long planPalletId;
|
||||
private Long warehousePalletId;
|
||||
private Long orderId;
|
||||
private String customerId;
|
||||
private String customerName;
|
||||
private String planNo;
|
||||
private String orderType;
|
||||
private BigDecimal volumeWeightKg;
|
||||
private BigDecimal grossWeightKg;
|
||||
private String containerNo;
|
||||
private LocalDateTime appointmentAt;
|
||||
private LocalDateTime expectedLoadingAt;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseCode;
|
||||
private String overseasWarehouseName;
|
||||
private String orderNo;
|
||||
private String orderRefs;
|
||||
private String inboundCode;
|
||||
private String palletNo;
|
||||
private String destinationWarehouseId;
|
||||
private String destinationWarehouseCode;
|
||||
private String destinationWarehouseName;
|
||||
private String fbaWarehouseCode;
|
||||
private Integer packageCount;
|
||||
private BigDecimal volumeCbm;
|
||||
private String sourceStage;
|
||||
private String cartonMarks;
|
||||
private String relabelCartonMarks;
|
||||
private String destinationType;
|
||||
private String deliveryAddress;
|
||||
private String postalCode;
|
||||
private String contactName;
|
||||
private String contactPhone;
|
||||
private Long returnReceiptId;
|
||||
private String returnReceiptNo;
|
||||
@com.fasterxml.jackson.annotation.JsonProperty("sourceType")
|
||||
public String sourceType() { return returnReceiptId == null ? "INBOUND_PLAN" : "RETURN_RECEIPT"; }
|
||||
|
||||
public DispatchPalletCandidate(Long planId, Long planPalletId, Long warehousePalletId, Long orderId, String customerId, String customerName, String planNo, String orderType, BigDecimal volumeWeightKg, BigDecimal grossWeightKg, String containerNo, LocalDateTime appointmentAt, LocalDateTime expectedLoadingAt, String overseasWarehouseId, String overseasWarehouseCode, String overseasWarehouseName, String orderNo, String orderRefs, String inboundCode, String palletNo, String destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, String fbaWarehouseCode, Integer packageCount, BigDecimal volumeCbm, String sourceStage, String cartonMarks, String relabelCartonMarks, String destinationType, String deliveryAddress, String postalCode, String contactName, String contactPhone) {
|
||||
this.planId = planId;
|
||||
this.planPalletId = planPalletId;
|
||||
this.warehousePalletId = warehousePalletId;
|
||||
this.orderId = orderId;
|
||||
this.customerId = customerId;
|
||||
this.customerName = customerName;
|
||||
this.planNo = planNo;
|
||||
this.orderType = orderType;
|
||||
this.volumeWeightKg = volumeWeightKg;
|
||||
this.grossWeightKg = grossWeightKg;
|
||||
this.containerNo = containerNo;
|
||||
this.appointmentAt = appointmentAt;
|
||||
this.expectedLoadingAt = expectedLoadingAt;
|
||||
this.overseasWarehouseId = overseasWarehouseId;
|
||||
this.overseasWarehouseCode = overseasWarehouseCode;
|
||||
this.overseasWarehouseName = overseasWarehouseName;
|
||||
this.orderNo = orderNo;
|
||||
this.orderRefs = orderRefs;
|
||||
this.inboundCode = inboundCode;
|
||||
this.palletNo = palletNo;
|
||||
this.destinationWarehouseId = destinationWarehouseId;
|
||||
this.destinationWarehouseCode = destinationWarehouseCode;
|
||||
this.destinationWarehouseName = destinationWarehouseName;
|
||||
this.fbaWarehouseCode = fbaWarehouseCode;
|
||||
this.packageCount = packageCount;
|
||||
this.volumeCbm = volumeCbm;
|
||||
this.sourceStage = sourceStage;
|
||||
this.cartonMarks = cartonMarks;
|
||||
this.relabelCartonMarks = relabelCartonMarks;
|
||||
this.destinationType = destinationType;
|
||||
this.deliveryAddress = deliveryAddress;
|
||||
this.postalCode = postalCode;
|
||||
this.contactName = contactName;
|
||||
this.contactPhone = contactPhone;
|
||||
}
|
||||
|
||||
public DispatchPalletCandidate(Long planId, Long planPalletId, Long warehousePalletId, Long orderId,
|
||||
String overseasWarehouseId, String overseasWarehouseName, String orderNo,
|
||||
String orderRefs, String inboundCode, String palletNo, String destinationWarehouseId,
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ public class DispatchPdaPickOrder {
|
||||
private String customerId;
|
||||
private Long pickId;
|
||||
private Long receiptId;
|
||||
private Long returnReceiptId;
|
||||
private Long orderId;
|
||||
private Long packages;
|
||||
private Long productQuantity;
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@ public class DispatchPdaWorkflowService {
|
||||
return task(customerId, order, task, items.findByDispatchOrder(customerId, order.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public DispatchPdaResponses.Picked pick(String warehouseId, String dispatchNo, long itemId,
|
||||
String operatorId, String operatorName,
|
||||
DispatchPdaRequests.Pick request) {
|
||||
@@ -147,7 +147,7 @@ public class DispatchPdaWorkflowService {
|
||||
pick.setProductQuantity(request.productQuantity()); pick.setIdempotencyKey(request.idempotencyKey());
|
||||
pick.setPickedAt(LocalDateTime.now()); pick.setCreatedAt(pick.getPickedAt()); picks.insert(pick);
|
||||
pickAttribution.attribute(customerId, pick,
|
||||
"RELABEL_ORDER".equals(item.getSourceBusinessType()) ? item.getOrderId() : null);
|
||||
"RELABEL_ORDER".equals(item.getSourceBusinessType()) ? item.getOrderId() : null, item.getReturnReceiptId());
|
||||
long total = picked + request.packages();
|
||||
order.setPickedPackageCount((int) (n(order.getPickedPackageCount()) + request.packages()));
|
||||
task.setStatus("PICKING"); task.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
+37
-8
@@ -15,7 +15,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -24,12 +24,14 @@ public class DispatchPickOrderAttributionService {
|
||||
@Resource private DispatchPdaPickOrderMapper attributions;
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private LastlegOrderStatusHistoryMapper history;
|
||||
@Resource private com.baiyue.lastleg.inventory.mapper.ReturnStockReceiptMapper returnReceipts;
|
||||
|
||||
/** Acquire source order locks before any inventory lock, matching order hold/release. */
|
||||
@org.springframework.transaction.annotation.Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||
public void preparePicking(String customerId, long positionId, Long fallbackOrderId) {
|
||||
var sourceIds = new java.util.TreeSet<Long>();
|
||||
attributions.availableReceipts(customerId, positionId).forEach(receipt -> sourceIds.add(receipt.getOrderId()));
|
||||
returnReceipts.available(customerId, positionId).forEach(receipt -> sourceIds.add(receipt.getOrderId()));
|
||||
if (fallbackOrderId != null && fallbackOrderId > 0) sourceIds.add(fallbackOrderId);
|
||||
if (sourceIds.isEmpty()) throw conflict("PDA pick has no attributable source order");
|
||||
for (Long orderId : sourceIds) {
|
||||
@@ -42,6 +44,10 @@ public class DispatchPickOrderAttributionService {
|
||||
.anyMatch(receipt -> !sourceIds.contains(receipt.getOrderId()))) {
|
||||
throw conflict("PDA pick source orders changed; reload the task");
|
||||
}
|
||||
for (var receipt : returnReceipts.available(customerId, positionId)) {
|
||||
if (!sourceIds.contains(receipt.getOrderId())) throw conflict("Returned source orders changed; reload the task");
|
||||
if (returnReceipts.lock(customerId, receipt.getId()) == null) throw conflict("Returned stock receipt disappeared");
|
||||
}
|
||||
}
|
||||
|
||||
/** Call after the inventory position has been locked/moved and the PDA pick row has been inserted. */
|
||||
@@ -55,14 +61,18 @@ public class DispatchPickOrderAttributionService {
|
||||
* authoritative tail-leg order, so use that order only for relabel picks.
|
||||
*/
|
||||
public List<OrderPackages> attribute(String customerId, DispatchPdaPick pick, Long fallbackOrderId) {
|
||||
return attribute(customerId, pick, fallbackOrderId, null);
|
||||
}
|
||||
public List<OrderPackages> attribute(String customerId, DispatchPdaPick pick, Long fallbackOrderId, Long selectedReturnReceiptId) {
|
||||
long remainingPackages = value(pick.getPackages());
|
||||
long totalPackages = remainingPackages;
|
||||
long totalQuantity = value(pick.getProductQuantity());
|
||||
long allocatedPackages = 0;
|
||||
long allocatedQuantity = 0;
|
||||
Map<Long, Long> pickedByOrder = new LinkedHashMap<>();
|
||||
for (InboundStockReceipt receipt : attributions.availableReceipts(customerId,
|
||||
pick.getInventoryPositionId())) {
|
||||
Map<Long, Long> pickedByOrder = new TreeMap<>();
|
||||
List<DispatchPdaPickOrder> pendingAttributions = new java.util.ArrayList<>();
|
||||
for (InboundStockReceipt receipt : selectedReturnReceiptId == null ? attributions.availableReceipts(customerId,
|
||||
pick.getInventoryPositionId()) : List.<InboundStockReceipt>of()) {
|
||||
if (remainingPackages == 0) break;
|
||||
long receiptPackages = value(receipt.getReceivedPackages())
|
||||
- attributions.attributedPackages(customerId, receipt.getId());
|
||||
@@ -80,17 +90,36 @@ public class DispatchPickOrderAttributionService {
|
||||
row.setPackages(packages);
|
||||
row.setProductQuantity(quantity);
|
||||
row.setCreatedAt(pick.getPickedAt() == null ? LocalDateTime.now() : pick.getPickedAt());
|
||||
attributions.insert(row);
|
||||
pendingAttributions.add(row);
|
||||
pickedByOrder.merge(receipt.getOrderId(), packages, Long::sum);
|
||||
remainingPackages -= packages;
|
||||
allocatedPackages = nextPackages;
|
||||
allocatedQuantity = nextQuantity;
|
||||
}
|
||||
if (remainingPackages != 0 && fallbackOrderId != null && fallbackOrderId > 0) {
|
||||
for (var receipt : returnReceipts.available(customerId, pick.getInventoryPositionId())) {
|
||||
if (remainingPackages == 0) break;
|
||||
if (selectedReturnReceiptId != null && !selectedReturnReceiptId.equals(receipt.getId())) continue;
|
||||
var locked = returnReceipts.lock(customerId, receipt.getId());
|
||||
if (locked == null) throw conflict("Returned stock receipt disappeared");
|
||||
long available = locked.getReceivedPackages() - returnReceipts.picked(customerId, locked.getId());
|
||||
if (available <= 0) continue;
|
||||
long packages = Math.min(remainingPackages, available);
|
||||
long nextPackages = allocatedPackages + packages;
|
||||
long nextQuantity = totalPackages == 0 ? 0 : Math.floorDiv(totalQuantity * nextPackages, totalPackages);
|
||||
DispatchPdaPickOrder row = new DispatchPdaPickOrder();
|
||||
row.setId(IdWorker.getId()); row.setCustomerId(customerId); row.setPickId(pick.getId());
|
||||
row.setReturnReceiptId(locked.getId()); row.setOrderId(locked.getOrderId()); row.setPackages(packages);
|
||||
row.setProductQuantity(nextQuantity - allocatedQuantity);
|
||||
row.setCreatedAt(pick.getPickedAt() == null ? LocalDateTime.now() : pick.getPickedAt());
|
||||
pendingAttributions.add(row); pickedByOrder.merge(locked.getOrderId(), packages, Long::sum);
|
||||
remainingPackages -= packages; allocatedPackages = nextPackages; allocatedQuantity = nextQuantity;
|
||||
}
|
||||
if (remainingPackages != 0 && selectedReturnReceiptId == null && fallbackOrderId != null && fallbackOrderId > 0) {
|
||||
pickedByOrder.merge(fallbackOrderId, remainingPackages, Long::sum);
|
||||
remainingPackages = 0;
|
||||
}
|
||||
if (remainingPackages != 0) throw conflict("PDA pick cannot be attributed to inbound receipt orders");
|
||||
pendingAttributions.forEach(attributions::insert);
|
||||
for (Map.Entry<Long, Long> entry : pickedByOrder.entrySet()) {
|
||||
if (orders.increasePicked(customerId, entry.getKey(), entry.getValue()) != 1) {
|
||||
throw conflict("Picked packages exceed tail-leg order remaining packages");
|
||||
@@ -102,7 +131,7 @@ public class DispatchPickOrderAttributionService {
|
||||
/** Call once for each departing PDA pick; repeated dispatch departure is protected by its outer idempotency key. */
|
||||
public List<OrderPackages> depart(String customerId, String outboundNo, String dispatchNo, List<DispatchPdaPick> picks,
|
||||
LocalDateTime departedAt) {
|
||||
Map<Long, Long> shippedByOrder = new LinkedHashMap<>();
|
||||
Map<Long, Long> shippedByOrder = new TreeMap<>();
|
||||
for (DispatchPdaPick pick : picks) {
|
||||
for (DispatchPdaPickOrder row : attributions.listPick(customerId, pick.getId())) {
|
||||
shippedByOrder.merge(row.getOrderId(), value(row.getPackages()), Long::sum);
|
||||
@@ -117,7 +146,7 @@ public class DispatchPickOrderAttributionService {
|
||||
throw conflict("Tail-leg outbound sub-order status could not be synchronized");
|
||||
}
|
||||
long shipped = value(before.getShippedPackageCount()) + entry.getValue();
|
||||
String to = shipped == value(before.getPackageCount()) ? "COMPLETED" : "PARTIALLY_SHIPPED";
|
||||
String to = shipped == value(before.getPackageCount()) + orders.returnedPackages(customerId, entry.getKey()) ? "COMPLETED" : "PARTIALLY_SHIPPED";
|
||||
LastlegOrderStatusHistory row = new LastlegOrderStatusHistory();
|
||||
row.setCustomerId(customerId);
|
||||
row.setOrderId(entry.getKey());
|
||||
|
||||
+47
-16
@@ -62,6 +62,14 @@ public class DispatchService {
|
||||
@Resource private DispatchDraftMapper drafts;
|
||||
@Resource private DispatchDraftContainerMapper draftContainers;
|
||||
@Resource private DispatchCandidateMapper candidates;
|
||||
@Resource private com.baiyue.lastleg.dispatch.mapper.ReturnDispatchCandidateMapper returnCandidates;
|
||||
|
||||
public String resolveCustomerScopeForSources(List<Long> planPalletIds, List<Long> returnReceiptIds) {
|
||||
var scopes = new java.util.ArrayList<String>();
|
||||
if (planPalletIds != null && !planPalletIds.isEmpty()) scopes.addAll(candidates.findCustomerScopesByPlanPalletIds(planPalletIds));
|
||||
if (returnReceiptIds != null && !returnReceiptIds.isEmpty()) scopes.addAll(returnCandidates.customers(returnReceiptIds));
|
||||
return requireSingleCustomerScope(scopes.stream().distinct().toList(), "Selected dispatch sources");
|
||||
}
|
||||
@Resource private DispatchPdaTaskMapper pdaTasks;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private LastlegOrderMapper tailOrders;
|
||||
@@ -120,8 +128,11 @@ public class DispatchService {
|
||||
.toUpperCase(java.util.Locale.ROOT).contains(fbaWarehouseCode.trim().toUpperCase(java.util.Locale.ROOT)))).toList();
|
||||
}
|
||||
}
|
||||
if (planIdsForReturns(sourceStage)) result = java.util.stream.Stream.concat(result.stream(),
|
||||
returnCandidates.available(null, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(draftToken)).stream()).toList();
|
||||
return result;
|
||||
}
|
||||
private boolean planIdsForReturns(String sourceStage) { return sourceStage == null || sourceStage.isBlank() || "RECEIVED".equals(sourceStage); }
|
||||
public List<DispatchPalletCandidate> platformCandidates(String orderNo, String fbaWarehouseCode, String sourceStage) {
|
||||
return platformCandidates(orderNo, fbaWarehouseCode, sourceStage, null);
|
||||
}
|
||||
@@ -134,7 +145,7 @@ public class DispatchService {
|
||||
private List<DispatchContainerResponse> containers(String token) {
|
||||
return draftContainers.findByToken(token).stream().map(c -> new DispatchContainerResponse(String.valueOf(c.getId()), c.getCapacity(),
|
||||
locks.findActiveByDraftContainer(token, c.getId()).stream()
|
||||
.map(x -> String.valueOf(x.getPlanPalletId())).toList())).toList();
|
||||
.map(x -> x.getReturnReceiptId() == null ? String.valueOf(x.getPlanPalletId()) : "RETURN:" + x.getReturnReceiptId()).toList())).toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -177,7 +188,7 @@ public class DispatchService {
|
||||
boundContainer = new DispatchDraftContainer(); boundContainer.setId(IdWorker.getId()); boundContainer.setDraftId(draft.getId()); boundContainer.setDraftToken(request.draftToken()); boundContainer.setSequenceNo(1); boundContainer.setCapacity(500); boundContainer.setCreatedAt(LocalDateTime.now()); boundContainer.setUpdatedAt(LocalDateTime.now()); draftContainers.insert(boundContainer);
|
||||
}
|
||||
if (boundContainer == null || !request.draftToken().equals(boundContainer.getDraftToken()) || !Objects.equals(boundContainer.getDraftId(), draft.getId())) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "装车托盘不存在");
|
||||
DispatchPalletCandidate candidate = requireDraftCandidate(customerId, request.planPalletId());
|
||||
DispatchPalletCandidate candidate = requireDraftSource(customerId, request);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
DispatchPalletLock active = activeLock(customerId, candidate);
|
||||
if (active != null && active.getExpiresAt() != null && !active.getExpiresAt().isAfter(now)) {
|
||||
@@ -199,6 +210,7 @@ public class DispatchService {
|
||||
lock.setActiveKey(candidate.sourcePalletKey());
|
||||
lock.setPlanId(candidate.planId());
|
||||
lock.setPlanPalletId(candidate.planPalletId());
|
||||
lock.setReturnReceiptId(candidate.returnReceiptId());
|
||||
lock.setWarehousePalletId(candidate.warehousePalletId());
|
||||
lock.setDraftToken(request.draftToken());
|
||||
lock.setDraftContainerId(request.draftContainerId());
|
||||
@@ -218,7 +230,7 @@ public class DispatchService {
|
||||
|
||||
@Transactional
|
||||
public void unlockDraftPallet(String customerId, String operatorId, DispatchRequests.DraftPallet request) {
|
||||
DispatchPalletCandidate candidate = requireDraftCandidate(customerId, request.planPalletId());
|
||||
DispatchPalletCandidate candidate = requireDraftSource(customerId, request);
|
||||
DispatchPalletLock active = activeLock(customerId, candidate);
|
||||
if (active == null) return;
|
||||
requireDraftOwner(active, operatorId, request.draftToken());
|
||||
@@ -226,7 +238,9 @@ public class DispatchService {
|
||||
DispatchOrder dispatch = orders.lockById(customerId, active.getDispatchOrderId());
|
||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
requirePalletEditingAllowed(dispatch);
|
||||
DispatchItem item = items.lockByPlanPallet(customerId, dispatch.getId(), request.planPalletId());
|
||||
DispatchItem item = request.returnReceiptId() == null ? items.lockByPlanPallet(customerId, dispatch.getId(), request.planPalletId())
|
||||
: items.selectOne(new LambdaQueryWrapper<DispatchItem>().eq(DispatchItem::getCustomerId, customerId)
|
||||
.eq(DispatchItem::getDispatchOrderId, dispatch.getId()).eq(DispatchItem::getReturnReceiptId, request.returnReceiptId()).last("FOR UPDATE"));
|
||||
if (item != null) removeLockedItem(dispatch, item, List.of(active));
|
||||
else releaseLock(active, LocalDateTime.now());
|
||||
return;
|
||||
@@ -261,6 +275,7 @@ public class DispatchService {
|
||||
@Transactional
|
||||
public DispatchResponse preArrange(String customerId, String operatorId, DispatchRequests.PreArrange request) {
|
||||
if (StringUtils.hasText(request.relabelNo())) {
|
||||
if (!request.returnReceiptIds().isEmpty()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "换标与退回来源不能混用");
|
||||
return preArrangeRelabel(customerId, operatorId, request);
|
||||
}
|
||||
if (!StringUtils.hasText(customerId)) {
|
||||
@@ -268,13 +283,20 @@ public class DispatchService {
|
||||
"No customer business data is available for dispatch");
|
||||
}
|
||||
List<Long> palletIds = request.planPalletIds().stream().filter(Objects::nonNull).distinct().sorted().toList();
|
||||
if (palletIds.isEmpty() || palletIds.size() != request.planPalletIds().size()) {
|
||||
List<Long> receiptIds = request.returnReceiptIds().stream().distinct().sorted().toList();
|
||||
if (palletIds.size() + receiptIds.size() > 500) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "每次最多选择500项来源");
|
||||
if ((palletIds.isEmpty() && receiptIds.isEmpty()) || palletIds.size() != request.planPalletIds().size()
|
||||
|| receiptIds.size() != request.returnReceiptIds().size()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Plan pallet IDs must be unique and non-empty");
|
||||
}
|
||||
List<DispatchPalletCandidate> selected = candidates.lockSelected(customerId, palletIds);
|
||||
if (selected.size() != palletIds.size()) {
|
||||
List<DispatchPalletCandidate> selected = new java.util.ArrayList<>();
|
||||
if (!palletIds.isEmpty()) selected.addAll(candidates.lockSelected(customerId, palletIds));
|
||||
if (!receiptIds.isEmpty()) selected.addAll(returnCandidates.lockSelected(customerId, receiptIds));
|
||||
if (selected.size() != palletIds.size() + receiptIds.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more selected pallets are not in this tenant");
|
||||
}
|
||||
if (selected.stream().map(DispatchPalletCandidate::sourcePalletKey).distinct().count() != selected.size())
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "同一实物托盘不能重复选择,请先拆托核对来源");
|
||||
if (selected.stream().anyMatch(candidate -> !hasValidDestination(candidate))) {
|
||||
boolean legacyMissingFba = selected.stream().allMatch(candidate -> !"PRIVATE".equals(candidate.destinationType()));
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, legacyMissingFba
|
||||
@@ -338,24 +360,22 @@ public class DispatchService {
|
||||
if (refreshing) orders.updateById(dispatch); else orders.insert(dispatch);
|
||||
|
||||
List<DispatchItem> previousItems = items.findByDispatchOrder(customerId, dispatch.getId());
|
||||
Set<Long> selectedPalletIds = selected.stream().map(DispatchPalletCandidate::planPalletId)
|
||||
Set<String> selectedPalletIds = selected.stream().map(DispatchService::selectionKey)
|
||||
.collect(Collectors.toSet());
|
||||
if (refreshing) {
|
||||
List<DispatchPalletLock> previousLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
|
||||
.eq(DispatchPalletLock::getCustomerId, customerId)
|
||||
.eq(DispatchPalletLock::getDispatchOrderId, dispatch.getId())
|
||||
.eq(DispatchPalletLock::getStatus, "ACTIVE"));
|
||||
previousItems.stream().filter(item -> item.getPlanPalletId() != null
|
||||
&& !selectedPalletIds.contains(item.getPlanPalletId()))
|
||||
previousItems.stream().filter(item -> !selectedPalletIds.contains(selectionKey(item)))
|
||||
.forEach(item -> {
|
||||
previousLocks.stream().filter(lock -> Objects.equals(lock.getPlanPalletId(), item.getPlanPalletId()))
|
||||
previousLocks.stream().filter(lock -> Objects.equals(lock.getPlanPalletId(), item.getPlanPalletId()) && Objects.equals(lock.getReturnReceiptId(), item.getReturnReceiptId()))
|
||||
.forEach(lock -> releaseLock(lock, now));
|
||||
items.deleteById(item.getId());
|
||||
});
|
||||
}
|
||||
Set<Long> existingPalletIds = previousItems.stream()
|
||||
.filter(item -> item.getPlanPalletId() == null || selectedPalletIds.contains(item.getPlanPalletId()))
|
||||
.map(DispatchItem::getPlanPalletId).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
Set<String> existingPalletIds = previousItems.stream()
|
||||
.map(DispatchService::selectionKey).filter(selectedPalletIds::contains).collect(Collectors.toSet());
|
||||
|
||||
for (DispatchPalletCandidate candidate : selected) {
|
||||
DispatchPalletLock lock = activeLock(customerId, candidate);
|
||||
@@ -373,6 +393,7 @@ public class DispatchService {
|
||||
lock.setActiveKey(candidate.sourcePalletKey());
|
||||
lock.setPlanId(candidate.planId());
|
||||
lock.setPlanPalletId(candidate.planPalletId());
|
||||
lock.setReturnReceiptId(candidate.returnReceiptId());
|
||||
lock.setWarehousePalletId(candidate.warehousePalletId());
|
||||
lock.setDispatchOrderId(dispatch.getId());
|
||||
lock.setOperatorId(trimToNull(operatorId));
|
||||
@@ -387,13 +408,14 @@ public class DispatchService {
|
||||
"Pallet is already locked by another active dispatch: " + candidate.palletNo(), exception);
|
||||
}
|
||||
}
|
||||
if (existingPalletIds.contains(candidate.planPalletId())) continue;
|
||||
if (existingPalletIds.contains(selectionKey(candidate))) continue;
|
||||
DispatchItem item = new DispatchItem();
|
||||
item.setId(IdWorker.getId());
|
||||
item.setDispatchOrderId(dispatch.getId());
|
||||
item.setCustomerId(customerId);
|
||||
item.setPlanId(candidate.planId());
|
||||
item.setPlanPalletId(candidate.planPalletId());
|
||||
item.setReturnReceiptId(candidate.returnReceiptId());
|
||||
item.setWarehousePalletId(candidate.warehousePalletId());
|
||||
item.setOrderId(candidate.orderId());
|
||||
item.setOverseasWarehouseId(candidate.overseasWarehouseId());
|
||||
@@ -564,7 +586,7 @@ public class DispatchService {
|
||||
.eq(DispatchPalletLock::getCustomerId, dispatch.getCustomerId())
|
||||
.eq(DispatchPalletLock::getDispatchOrderId, dispatch.getId())
|
||||
.eq(DispatchPalletLock::getStatus, "ACTIVE"));
|
||||
List<DispatchPalletLock> matchingLocks = activeLocks.stream().filter(lock -> Objects.equals(lock.getPlanPalletId(), item.getPlanPalletId())
|
||||
List<DispatchPalletLock> matchingLocks = activeLocks.stream().filter(lock -> Objects.equals(lock.getPlanPalletId(), item.getPlanPalletId()) && Objects.equals(lock.getReturnReceiptId(), item.getReturnReceiptId())
|
||||
|| (item.getWarehousePalletId() != null && Objects.equals(lock.getWarehousePalletId(), item.getWarehousePalletId())))
|
||||
.toList();
|
||||
return removeLockedItem(dispatch, item, matchingLocks);
|
||||
@@ -705,6 +727,15 @@ public class DispatchService {
|
||||
}
|
||||
return found.get(0);
|
||||
}
|
||||
private static String selectionKey(DispatchPalletCandidate value) { return value.returnReceiptId() == null ? "PLAN:" + value.planPalletId() : "RETURN:" + value.returnReceiptId(); }
|
||||
private static String selectionKey(DispatchItem value) { return value.getReturnReceiptId() == null ? "PLAN:" + value.getPlanPalletId() : "RETURN:" + value.getReturnReceiptId(); }
|
||||
private DispatchPalletCandidate requireDraftSource(String customerId, DispatchRequests.DraftPallet request) {
|
||||
if (!request.isValidSource()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "请选择唯一实际来源");
|
||||
if (request.returnReceiptId() == null) return requireDraftCandidate(customerId, request.planPalletId());
|
||||
var found = returnCandidates.lockSelected(customerId, List.of(request.returnReceiptId()));
|
||||
if (found.size() != 1) throw new ResponseStatusException(HttpStatus.CONFLICT, "退回实收来源当前不可预排");
|
||||
return found.get(0);
|
||||
}
|
||||
|
||||
private DispatchPalletLock activeLock(String customerId, DispatchPalletCandidate candidate) {
|
||||
DispatchPalletLock active = locks.lockActive(customerId, candidate.sourcePalletKey());
|
||||
|
||||
Reference in New Issue
Block a user