feat(fleet): bind quote versions and reconcile actual return quantities
This commit is contained in:
+1
@@ -31,6 +31,7 @@ public class ExceptionOrder {
|
||||
private Integer packageCount;
|
||||
private String returnDirection;
|
||||
private String returnLogisticsNo;
|
||||
private String returnContextJson;
|
||||
private String handlingAction;
|
||||
private String handlingResult;
|
||||
private String evidenceFileIdsJson;
|
||||
|
||||
+1
@@ -19,5 +19,6 @@ public class ExceptionReturnItem {
|
||||
private String skuCode;
|
||||
private Long maxReturnPackages;
|
||||
private Long approvedReturnPackages;
|
||||
private Long receivedPackages;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import org.apache.ibatis.annotations.Select;
|
||||
@Mapper
|
||||
public interface ExceptionOrderMapper extends BaseMapper<ExceptionOrder> {
|
||||
@Select("SELECT * FROM ll_exception_order WHERE customer_id=#{customerId} AND delivery_order_id=#{deliveryOrderId} " +
|
||||
"AND exception_type='RETURN' LIMIT 1")
|
||||
"AND exception_type='RETURN' AND status IN ('PENDING','PROCESSING') LIMIT 1")
|
||||
ExceptionOrder findDeliveryReturn(@Param("customerId") String customerId,
|
||||
@Param("deliveryOrderId") Long deliveryOrderId);
|
||||
|
||||
|
||||
+12
@@ -13,4 +13,16 @@ public interface ExceptionReturnItemMapper extends BaseMapper<ExceptionReturnIte
|
||||
@Select("SELECT * FROM ll_exception_return_item WHERE customer_id=#{customerId} AND exception_order_id=#{exceptionOrderId} ORDER BY id")
|
||||
List<ExceptionReturnItem> findByException(@Param("customerId") String customerId,
|
||||
@Param("exceptionOrderId") Long exceptionOrderId);
|
||||
|
||||
@Select("SELECT COALESCE(SUM(i.received_packages),0) FROM ll_exception_return_item i " +
|
||||
"JOIN ll_exception_order e ON e.id=i.exception_order_id AND e.customer_id=i.customer_id " +
|
||||
"WHERE e.customer_id=#{customerId} AND e.delivery_order_id=#{deliveryOrderId}")
|
||||
long receivedByDelivery(@Param("customerId") String customerId, @Param("deliveryOrderId") Long deliveryOrderId);
|
||||
|
||||
@Select("SELECT COALESCE(SUM(i.received_packages),0) FROM ll_exception_return_item i " +
|
||||
"JOIN ll_exception_order e ON e.id=i.exception_order_id AND e.customer_id=i.customer_id " +
|
||||
"WHERE e.customer_id=#{customerId} AND e.delivery_order_id=#{deliveryOrderId} " +
|
||||
"AND i.outbound_pallet_item_id=#{itemId} AND i.inventory_position_id=#{positionId}")
|
||||
long receivedBySource(@Param("customerId") String customerId, @Param("deliveryOrderId") Long deliveryOrderId,
|
||||
@Param("itemId") Long itemId, @Param("positionId") Long positionId);
|
||||
}
|
||||
|
||||
+75
-17
@@ -130,7 +130,8 @@ public class ExceptionOrderService {
|
||||
public List<ExceptionOrderResponses.Relation> returnCandidates(String customerId, String deliveryNo) {
|
||||
DeliveryOrder delivery = deliveryOrders.findByNo(customerId, deliveryNo);
|
||||
if (delivery == null) throw notFound("派送单不存在");
|
||||
if (!DeliveryStatusPolicy.allowedActions(delivery.getStatus()).contains("MARK_RETURN")) {
|
||||
if (!DeliveryStatusPolicy.allowedActions(delivery.getStatus()).contains("MARK_RETURN")
|
||||
&& !"RETURN_PENDING".equals(delivery.getStatus())) {
|
||||
throw conflict("当前派送状态不允许标记退回");
|
||||
}
|
||||
return returnCandidates(delivery);
|
||||
@@ -157,19 +158,13 @@ public class ExceptionOrderService {
|
||||
request.skuCode(), null, null, "MANUAL", operatorId, operatorName);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public ExceptionOrderResponses.Created markReturn(String customerId, String operatorId, String operatorName, String deliveryNo,
|
||||
ExceptionOrderRequests.MarkReturn request) {
|
||||
String requestHash = hash(request);
|
||||
ExceptionOrderCommand replay = commands.findByKey(customerId, "MARK_RETURN", request.idempotencyKey().trim());
|
||||
if (replay != null) return replayCreated(replay, requestHash);
|
||||
DeliveryOrder delivery = deliveryOrders.lockByNo(customerId, deliveryNo);
|
||||
if (delivery == null) throw notFound("派送单不存在");
|
||||
if (!DeliveryStatusPolicy.allowedActions(delivery.getStatus()).contains("MARK_RETURN")) {
|
||||
throw conflict("当前派送状态不允许标记退回");
|
||||
}
|
||||
ExceptionOrder existing = exceptionOrders.findDeliveryReturn(customerId, delivery.getId());
|
||||
if (existing != null) throw conflict("该派送单已存在退回异常单");
|
||||
DeliveryOrder delivery = lockForReturn(customerId, deliveryNo);
|
||||
|
||||
List<ExceptionOrderResponses.Relation> candidates = returnCandidates(delivery);
|
||||
ExceptionOrderResponses.Relation selected = candidates.stream()
|
||||
@@ -198,6 +193,7 @@ public class ExceptionOrderService {
|
||||
item.setOutboundPalletItemId(source.outboundPalletItemId()); item.setInventoryPositionId(source.inventoryPositionId());
|
||||
item.setPalletNo(source.palletNo()); item.setSkuCode(source.skuCode());
|
||||
item.setMaxReturnPackages(source.maxReturnPackages()); item.setApprovedReturnPackages(requestedItem.packageCount());
|
||||
item.setReceivedPackages(0L);
|
||||
item.setCreatedAt(LocalDateTime.now()); returnItems.insert(item);
|
||||
}
|
||||
|
||||
@@ -222,6 +218,17 @@ public class ExceptionOrderService {
|
||||
return created;
|
||||
}
|
||||
|
||||
/** Caller keeps the delivery row locked until return creation commits. */
|
||||
DeliveryOrder lockForReturn(String customerId, String deliveryNo) {
|
||||
DeliveryOrder delivery = deliveryOrders.lockByNo(customerId, deliveryNo);
|
||||
if (delivery == null) throw notFound("派送单不存在");
|
||||
if (!DeliveryStatusPolicy.allowedActions(delivery.getStatus()).contains("MARK_RETURN"))
|
||||
throw conflict("当前派送状态不允许标记退回");
|
||||
if (exceptionOrders.findDeliveryReturn(customerId, delivery.getId()) != null)
|
||||
throw conflict("该派送单已存在退回异常单");
|
||||
return delivery;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ExceptionOrderResponses.CommandResult startProcessing(String customerId, String operatorId,
|
||||
String exceptionNo,
|
||||
@@ -247,6 +254,7 @@ public class ExceptionOrderService {
|
||||
ExceptionOrderCommand replay = commands.findByKey(customerId, "CANCEL", request.idempotencyKey().trim());
|
||||
if (replay != null) return replayResult(replay, requestHash);
|
||||
ExceptionOrder order = lock(customerId, exceptionNo, request.expectedVersion(), "PENDING");
|
||||
restoreCancelledReturn(order);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
order.setStatus("CANCELLED"); order.setHandlingResult(trim(request.reason()));
|
||||
order.setHandlerId(operatorId); order.setUpdatedAt(now); order.setVersion(version(order) + 1);
|
||||
@@ -266,29 +274,42 @@ public class ExceptionOrderService {
|
||||
ExceptionOrder order = lock(customerId, exceptionNo, request.expectedVersion(), "PROCESSING");
|
||||
request.evidenceFileIds().stream().distinct().forEach(fileId -> evidenceFiles.validate(customerId, order.getId(), fileId));
|
||||
ReturnedInventoryRestoreResponse restored = null;
|
||||
List<ExceptionReturnItem> stored = List.of();
|
||||
if ("RETURN".equals(order.getExceptionType()) && "OVERSEAS_WAREHOUSE".equals(order.getReturnDirection())) {
|
||||
List<ExceptionReturnItem> stored = returnItems.findByException(customerId, order.getId());
|
||||
var delivery = deliveryOrders.lockByNo(customerId, order.getDeliveryNo());
|
||||
if (delivery == null || !"RETURN_PENDING".equals(delivery.getStatus())) throw conflict("关联派送单不是退回处理中状态");
|
||||
stored = returnItems.findByException(customerId, order.getId());
|
||||
if (stored.isEmpty()) throw conflict("退回异常单缺少已冻结的原出库库存明细");
|
||||
if (request.receipts() == null || request.receipts().isEmpty()) throw badRequest("请填写退回实收明细");
|
||||
Map<Long, Long> received = ReturnReceiptPolicy.quantities(stored, request.receipts());
|
||||
restored = inventoryCommands.restoreReturnedInventory(customerId, operatorId,
|
||||
new ReturnedInventoryRestoreRequest(order.getExceptionNo(), order.getOutboundNo(),
|
||||
order.getOverseasWarehouseId(), order.getInboundOrderId(),
|
||||
stored.stream().map(item -> new ReturnedInventoryRestoreRequest.SourceItem(
|
||||
stored.stream().filter(item -> received.containsKey(item.getId())).map(item -> new ReturnedInventoryRestoreRequest.SourceItem(
|
||||
item.getOutboundPalletItemId(), item.getInventoryPositionId(), item.getSkuCode(),
|
||||
Objects.requireNonNullElse(item.getApprovedReturnPackages(), 0L))).toList(),
|
||||
item.getApprovedReturnPackages() - ReturnReceiptPolicy.received(item))).toList(),
|
||||
request.receipts().stream().map(receipt -> new ReturnedInventoryRestoreRequest.Receipt(
|
||||
receipt.outboundPalletItemId(), receipt.inventoryPositionId(), receipt.receivedPackages(),
|
||||
receipt.targetPalletId(), receipt.targetPalletNo(), receipt.targetLocationId(),
|
||||
receipt.targetLocationCode())).toList(), request.idempotencyKey().trim(),
|
||||
request.evidenceFileIds().stream().map(String::valueOf).toList()));
|
||||
if (restored.restoredPackages() != received.values().stream().mapToLong(Long::longValue).sum())
|
||||
throw conflict("退回库存实际入账数量与本次实收不一致");
|
||||
for (ExceptionReturnItem item : stored) if (received.containsKey(item.getId())) {
|
||||
item.setReceivedPackages(ReturnReceiptPolicy.received(item) + received.get(item.getId()));
|
||||
returnItems.updateById(item);
|
||||
}
|
||||
} else if (request.receipts() != null && !request.receipts().isEmpty()) {
|
||||
throw badRequest("非退回海外仓异常不能提交库存实收明细");
|
||||
}
|
||||
LocalDateTime now = restored == null ? LocalDateTime.now() : restored.completedAt();
|
||||
order.setStatus("COMPLETED"); order.setHandlingResult(trim(request.handlingResult()));
|
||||
order.setEvidenceFileIdsJson(write(request.evidenceFileIds()));
|
||||
boolean completed = stored.isEmpty() || ReturnReceiptPolicy.complete(stored);
|
||||
order.setStatus(completed ? "COMPLETED" : "PROCESSING"); order.setHandlingResult(trim(request.handlingResult()));
|
||||
List<Long> evidence = new ArrayList<>(readEvidenceFileIds(order.getEvidenceFileIdsJson()));
|
||||
evidence.addAll(request.evidenceFileIds());
|
||||
order.setEvidenceFileIdsJson(write(evidence.stream().distinct().toList()));
|
||||
order.setInventoryAdjustmentId(restored == null ? null : restored.adjustmentId());
|
||||
order.setHandlerId(operatorId); order.setCompletedAt(now); order.setUpdatedAt(now);
|
||||
order.setHandlerId(operatorId); order.setCompletedAt(completed ? now : null); order.setUpdatedAt(now);
|
||||
order.setVersion(version(order) + 1); exceptionOrders.updateById(order);
|
||||
closeReturnedDelivery(order, operatorId, now);
|
||||
long restoredPackages = restored == null ? 0 : restored.restoredPackages();
|
||||
@@ -352,6 +373,11 @@ public class ExceptionOrderService {
|
||||
value.setOverseasWarehouseName(delivery == null ? relation.overseasWarehouseName() : delivery.getOverseasWarehouseName());
|
||||
value.setSkuCode(trim(sku)); value.setPackageCount(packages); value.setReturnDirection(trim(direction));
|
||||
value.setReturnLogisticsNo(trim(logisticsNo));
|
||||
if (delivery != null) {
|
||||
LastlegOrder tail = relation.lastMileOrderId() == null ? null : lastMileOrders.selectById(relation.lastMileOrderId());
|
||||
value.setReturnContextJson(write(new ReturnContext(delivery.getStatus(), tail == null ? null : tail.getId(),
|
||||
tail == null ? null : tail.getSubOrderStatus(), tail == null ? null : tail.getAbnormal())));
|
||||
}
|
||||
if (StringUtils.hasText(direction)) {
|
||||
value.setHandlingAction("OVERSEAS_WAREHOUSE".equals(direction) ? "退回海外仓" : "CUSTOMER".equals(direction) ? "退回客户" : "其他退货处理");
|
||||
value.setHandlerId(operatorId); value.setHandlerName(operatorName);
|
||||
@@ -414,7 +440,9 @@ public class ExceptionOrderService {
|
||||
DeliveryOrder delivery) {
|
||||
List<ExceptionOrderResponses.ReturnSource> sources = returnSources(delivery).stream()
|
||||
.filter(source -> relation.inboundCode() == null || Objects.equals(source.inboundCode(), relation.inboundCode()))
|
||||
.map(Source::response).toList();
|
||||
.map(source -> source.withPackages(source.maxReturnPackages() - returnItems.receivedBySource(
|
||||
delivery.getCustomerId(), delivery.getId(), source.outboundPalletItemId(), source.inventoryPositionId())))
|
||||
.filter(source -> source.maxReturnPackages() > 0).map(Source::response).toList();
|
||||
return new ExceptionOrderResponses.Relation(relation.lastMileOrderId(), relation.lastMileOrderNo(),
|
||||
relation.inboundOrderId(), relation.inboundCode(), relation.customerId(), relation.customerName(),
|
||||
relation.overseasWarehouseId(), relation.overseasWarehouseName(), relation.deliveryNo(),
|
||||
@@ -476,7 +504,8 @@ public class ExceptionOrderService {
|
||||
return returnItems.findByException(order.getCustomerId(), order.getId()).stream()
|
||||
.map(item -> new ExceptionOrderResponses.ReturnSource(item.getOutboundPalletItemId(),
|
||||
item.getInventoryPositionId(), item.getPalletNo(), item.getSkuCode(),
|
||||
Objects.requireNonNullElse(item.getApprovedReturnPackages(), 0L))).toList();
|
||||
Objects.requireNonNullElse(item.getApprovedReturnPackages(), 0L) - ReturnReceiptPolicy.received(item)))
|
||||
.filter(item -> item.maxReturnPackages() > 0).toList();
|
||||
}
|
||||
|
||||
private void validateReturnItems(List<ExceptionOrderResponses.ReturnSource> available,
|
||||
@@ -506,10 +535,15 @@ public class ExceptionOrderService {
|
||||
|
||||
private void closeReturnedDelivery(ExceptionOrder order, String operatorId, LocalDateTime now) {
|
||||
if (!"RETURN".equals(order.getExceptionType()) || !StringUtils.hasText(order.getDeliveryNo())) return;
|
||||
// Closing a non-warehouse case does not establish physical cargo handoff or inventory receipt.
|
||||
if (!"OVERSEAS_WAREHOUSE".equals(order.getReturnDirection())) return;
|
||||
DeliveryOrder delivery = deliveryOrders.lockByNo(order.getCustomerId(), order.getDeliveryNo());
|
||||
if (delivery == null || !"RETURN_PENDING".equals(delivery.getStatus())) {
|
||||
throw conflict("关联派送单不是退货处理中状态");
|
||||
}
|
||||
long received = returnItems.receivedByDelivery(order.getCustomerId(), delivery.getId());
|
||||
if (delivery.getPackageCount() == null || received > delivery.getPackageCount()) throw conflict("累计退回量与派送实发数量不一致");
|
||||
if (received < delivery.getPackageCount()) return;
|
||||
delivery.setStatus("RETURNED"); delivery.setUpdatedAt(now);
|
||||
delivery.setVersion(Objects.requireNonNullElse(delivery.getVersion(), 0L) + 1);
|
||||
deliveryOrders.updateById(delivery);
|
||||
@@ -526,6 +560,30 @@ public class ExceptionOrderService {
|
||||
operation.setCreatedAt(now); deliveryOperations.insert(operation);
|
||||
}
|
||||
|
||||
private record ReturnContext(String deliveryStatus, Long tailId, String tailStatus, Boolean abnormal) { }
|
||||
|
||||
private void restoreCancelledReturn(ExceptionOrder order) {
|
||||
if (!"RETURN".equals(order.getExceptionType()) || !StringUtils.hasText(order.getDeliveryNo())) return;
|
||||
ReturnContext previous;
|
||||
try { previous = objectMapper.readValue(order.getReturnContextJson(), ReturnContext.class); }
|
||||
catch (Exception exception) { throw conflict("退回异常缺少原始状态,不能猜测恢复状态"); }
|
||||
if (previous == null || !Set.of("DELIVERING", "DELIVERED", "REJECTED", "RETURN_PENDING").contains(previous.deliveryStatus()))
|
||||
throw conflict("退回异常原始状态无效");
|
||||
DeliveryOrder delivery = deliveryOrders.lockByNo(order.getCustomerId(), order.getDeliveryNo());
|
||||
if (delivery == null || !"RETURN_PENDING".equals(delivery.getStatus())) throw conflict("关联派送状态已变化,不能取消退回");
|
||||
if (returnItems.findByException(order.getCustomerId(), order.getId()).stream().anyMatch(item -> ReturnReceiptPolicy.received(item) > 0))
|
||||
throw conflict("已有实际退回入仓,不能取消退回事实");
|
||||
delivery.setStatus(previous.deliveryStatus()); delivery.setUpdatedAt(LocalDateTime.now());
|
||||
delivery.setVersion(Objects.requireNonNullElse(delivery.getVersion(), 0L) + 1); deliveryOrders.updateById(delivery);
|
||||
if (previous.tailId() != null) {
|
||||
LastlegOrder tail = lastMileOrders.selectById(previous.tailId());
|
||||
if (tail == null || !Objects.equals(tail.getCustomerId(), order.getCustomerId())
|
||||
|| !"DELIVERY:RETURN_PENDING".equals(tail.getSubOrderStatus())) throw conflict("关联尾程状态已变化,不能覆盖");
|
||||
tail.setSubOrderStatus(previous.tailStatus()); tail.setAbnormal(previous.abnormal());
|
||||
tail.setUpdatedAt(LocalDateTime.now()); lastMileOrders.updateById(tail);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> allowedActions(ExceptionOrder order) {
|
||||
return switch (Objects.requireNonNullElse(order.getStatus(), "")) {
|
||||
case "PENDING" -> List.of("START_PROCESSING", "CANCEL_EXCEPTION");
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.lastleg.exceptionorder.service;
|
||||
|
||||
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderRequests.ReturnReceipt;
|
||||
import com.baiyue.lastleg.exceptionorder.entity.ExceptionReturnItem;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
final class ReturnReceiptPolicy {
|
||||
private ReturnReceiptPolicy() { }
|
||||
|
||||
static Map<Long, Long> quantities(List<ExceptionReturnItem> sources, List<ReturnReceipt> receipts) {
|
||||
Map<Long, Long> amounts = new LinkedHashMap<>();
|
||||
if (sources.isEmpty() || receipts == null || receipts.isEmpty()) throw conflict("请填写已冻结来源的退回实收明细");
|
||||
for (ReturnReceipt receipt : receipts) {
|
||||
ExceptionReturnItem source = sources.stream().filter(item -> Objects.equals(item.getOutboundPalletItemId(), receipt.outboundPalletItemId())
|
||||
&& Objects.equals(item.getInventoryPositionId(), receipt.inventoryPositionId())).findFirst()
|
||||
.orElseThrow(() -> conflict("退回实收明细不属于本异常冻结来源"));
|
||||
if (receipt.receivedPackages() <= 0) throw conflict("退回实收箱数必须大于零");
|
||||
amounts.merge(source.getId(), receipt.receivedPackages(), Math::addExact);
|
||||
}
|
||||
for (ExceptionReturnItem source : sources) {
|
||||
long total = Math.addExact(received(source), amounts.getOrDefault(source.getId(), 0L));
|
||||
if (total > source.getApprovedReturnPackages()) throw conflict("累计实收箱数超过批准退回数量");
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
|
||||
static long received(ExceptionReturnItem item) { return Objects.requireNonNullElse(item.getReceivedPackages(), 0L); }
|
||||
static boolean complete(List<ExceptionReturnItem> items) {
|
||||
return !items.isEmpty() && items.stream().allMatch(item -> received(item) == item.getApprovedReturnPackages());
|
||||
}
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
Reference in New Issue
Block a user