fix(dropship): serialize outbound creation and pick exact carton subsets
This commit is contained in:
@@ -25,6 +25,7 @@ public class OutboundOrder {
|
||||
private Integer packageCount;
|
||||
private Integer palletCount;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String createdBy;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
+2
@@ -15,6 +15,8 @@ public interface OutboundOrderMapper extends BaseMapper<OutboundOrder> {
|
||||
OutboundOrder findGlobalByNo(@Param("outboundNo") String outboundNo);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
OutboundOrder findByIdempotency(@Param("customerId") String customerId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1 FOR UPDATE")
|
||||
OutboundOrder lockByIdempotency(@Param("customerId") String customerId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1")
|
||||
OutboundOrder findByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
|
||||
|
||||
+60
-6
@@ -73,6 +73,11 @@ import java.time.format.DateTimeFormatter;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
@Service
|
||||
public class DeliveryTransferService {
|
||||
@@ -103,6 +108,7 @@ public class DeliveryTransferService {
|
||||
@Resource private RelabelItemMapper relabelItems;
|
||||
@Resource private RelabelApplicationService relabelService;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.TransferResult platformTransfer(String dispatchNo, String operatorId,
|
||||
@@ -254,17 +260,33 @@ public class DeliveryTransferService {
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.PdaTask createOneClick(String customerId, String operatorId, DeliveryRequests.OneClickOutbound request) {
|
||||
String requestHash = oneClickRequestHash(request);
|
||||
OutboundOrder existing = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (existing != null) return taskResult(existing, pdaTasks.lockByOutbound(customerId, existing.getId()));
|
||||
if (existing != null) return replayOneClick(existing, requestHash);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String selectedWarehouseId = validateOneClickItems(customerId, request.items());
|
||||
InventoryPosition firstPosition = inventoryPositions.selectById(request.items().get(0).inventoryPositionId());
|
||||
if (firstPosition == null || !customerId.equals(firstPosition.getCustomerId())) {
|
||||
throw conflict("所选库存不是可用的一件代发库存,或已被其他作业占用");
|
||||
}
|
||||
String selectedWarehouseId = required(firstPosition.getOverseasWarehouseId(), "库存缺少所属海外仓");
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo(orderNoGenerator.next("ONE_CLICK_DROPSHIP", selectedWarehouseId, outbound.getId()));
|
||||
outbound.setOutboundType("ONE_CLICK_DROPSHIP"); outbound.setStatus("WAIT_PICK"); outbound.setRecipientInfo(request.recipient().trim());
|
||||
outbound.setPackageCount(request.items().stream().mapToInt(i -> i.packages().intValue()).sum()); outbound.setPalletCount(request.items().size());
|
||||
outbound.setIdempotencyKey(request.idempotencyKey().trim()); outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now);
|
||||
outboundOrders.insert(outbound);
|
||||
outbound.setRequestHash(requestHash);
|
||||
// Claim the unique command key before taking inventory locks. A racing insert
|
||||
// waits for the first transaction; use a current read after it commits, not
|
||||
// the REPEATABLE READ snapshot established by the initial lookup.
|
||||
try {
|
||||
outboundOrders.insert(outbound);
|
||||
} catch (DuplicateKeyException duplicate) {
|
||||
OutboundOrder concurrent = outboundOrders.lockByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (concurrent == null) throw duplicate;
|
||||
return replayOneClick(concurrent, requestHash);
|
||||
}
|
||||
selectedWarehouseId = validateOneClickItems(customerId, request.items());
|
||||
Set<Long> physicalPalletIds = new HashSet<>();
|
||||
for (DeliveryRequests.OneClickItem input : request.items()) {
|
||||
InventoryPosition position = inventoryPositions.selectById(input.inventoryPositionId());
|
||||
@@ -286,6 +308,23 @@ public class DeliveryTransferService {
|
||||
return taskResult(outbound, task);
|
||||
}
|
||||
|
||||
private DeliveryResponses.PdaTask replayOneClick(OutboundOrder existing, String requestHash) {
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(existing.getOutboundType())
|
||||
|| !requestHash.equals(existing.getRequestHash())) {
|
||||
throw conflict("相同幂等键不能用于不同的一件代发出库请求");
|
||||
}
|
||||
return taskResult(existing, pdaTasks.lockByOutbound(existing.getCustomerId(), existing.getId()));
|
||||
}
|
||||
|
||||
String oneClickRequestHash(DeliveryRequests.OneClickOutbound request) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(objectMapper.writeValueAsString(request).getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (JsonProcessingException | NoSuchAlgorithmException error) {
|
||||
throw new IllegalStateException("无法生成一件代发出库请求摘要", error);
|
||||
}
|
||||
}
|
||||
|
||||
public List<DeliveryResponses.OneClickCandidate> oneClickCandidates(String customerId, String warehouseId) {
|
||||
return dropshipProfiles.listOutboundCandidates(customerId,
|
||||
StringUtils.hasText(warehouseId) ? warehouseId.trim() : null).stream()
|
||||
@@ -491,8 +530,19 @@ public class DeliveryTransferService {
|
||||
}
|
||||
DropshipOrderProfile profile = dropshipProfiles.findPosition(customerId, request.inventoryPositionId());
|
||||
if (profile == null) throw conflict("Scanned pallet is not one-click dropship inventory");
|
||||
List<DropshipCarton> cartons = StringUtils.hasText(item.getCartonMarks())
|
||||
? Arrays.stream(item.getCartonMarks().split(",")).map(String::trim).filter(StringUtils::hasText)
|
||||
List<String> submittedMarks = request.cartonMarks() == null ? List.of() : request.cartonMarks();
|
||||
List<String> selectedMarks = submittedMarks.stream().map(String::trim).filter(StringUtils::hasText).distinct().sorted().toList();
|
||||
Set<String> frozenMarks = new HashSet<>(Arrays.asList(Optional.ofNullable(item.getCartonMarks()).orElse("").split(",")));
|
||||
if (!submittedMarks.isEmpty() && (selectedMarks.size() != submittedMarks.size()
|
||||
|| selectedMarks.size() != request.packages()
|
||||
|| StringUtils.hasText(item.getCartonMarks()) && !frozenMarks.containsAll(selectedMarks))) {
|
||||
throw conflict("拣货箱唛必须唯一、数量一致且属于当前出库明细");
|
||||
}
|
||||
if (StringUtils.hasText(item.getCartonMarks()) && selectedMarks.isEmpty()) {
|
||||
throw conflict("请提交本次实际拣货的指定箱唛");
|
||||
}
|
||||
List<DropshipCarton> cartons = !selectedMarks.isEmpty()
|
||||
? selectedMarks.stream()
|
||||
.map(mark -> dropshipCartons.findByProfileAndMark(customerId, profile.getId(), mark)).filter(Objects::nonNull)
|
||||
.filter(carton -> "AVAILABLE".equals(carton.getStatus()) && item.getSkuCode().equals(carton.getSkuCode())).toList()
|
||||
: dropshipCartons.lockAvailable(customerId, profile.getId(), item.getSkuCode(), request.packages());
|
||||
@@ -644,6 +694,8 @@ public class DeliveryTransferService {
|
||||
OutboundPalletItem item) {
|
||||
if (item.getWarehousePalletId() == null) return List.of();
|
||||
List<DeliveryResponses.PdaPickPosition> result = new ArrayList<>();
|
||||
Set<String> frozenMarks = new HashSet<>(Arrays.asList(
|
||||
Optional.ofNullable(item.getCartonMarks()).orElse("").split(",")));
|
||||
for (InventoryPosition position : inventoryPositions.listPallet(customerId, item.getWarehousePalletId())) {
|
||||
long available = value(position.getAvailablePackages());
|
||||
if (available <= 0 || !warehouseId.equals(position.getOverseasWarehouseId())) continue;
|
||||
@@ -654,7 +706,9 @@ public class DeliveryTransferService {
|
||||
List<String> marks = profile == null ? List.of() : dropshipCartons.listProfile(customerId, profile.getId())
|
||||
.stream().filter(carton -> "AVAILABLE".equals(carton.getStatus()))
|
||||
.filter(carton -> Objects.equals(item.getSkuCode(), carton.getSkuCode()))
|
||||
.filter(carton -> !StringUtils.hasText(item.getCartonMarks()) || frozenMarks.contains(carton.getCartonMark()))
|
||||
.map(DropshipCarton::getCartonMark).filter(StringUtils::hasText).distinct().limit(available).toList();
|
||||
if (profile != null && marks.isEmpty()) continue;
|
||||
if (!matchesOutboundItem(customerId, warehouseId, item, balance) && marks.isEmpty()) continue;
|
||||
if (marks.isEmpty()) {
|
||||
result.add(pdaPosition(position, item, balance, null, available, value(position.getPickedPackages())));
|
||||
@@ -669,7 +723,7 @@ public class DeliveryTransferService {
|
||||
InventoryBalance balance, String cartonMark,
|
||||
long available, long picked) {
|
||||
return new DeliveryResponses.PdaPickPosition(position.getId(), position.getPalletNo(), cartonMark,
|
||||
balance.getSkuCode(), balance.getInboundCode(), available, picked);
|
||||
item.getSkuCode(), balance.getInboundCode(), available, picked);
|
||||
}
|
||||
|
||||
private boolean matchesOutboundItem(String customerId, String warehouseId, OutboundPalletItem item,
|
||||
|
||||
Reference in New Issue
Block a user