This commit is contained in:
2026-09-08 18:16:54 +08:00
parent 1a1a19dea7
commit fcb4c055e2
32 changed files with 1239 additions and 46 deletions
@@ -45,6 +45,38 @@ public class ExceptionOrderController {
return ApiResponse.success(service.create(scope(user), operatorId(user), operatorName(user), request));
}
@GetMapping("/exception-orders/{exceptionNo}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_LIST + "')")
public ApiResponse<ExceptionOrderResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@PathVariable String exceptionNo) {
return ApiResponse.success(service.detail(requiredScope(user, customerId), exceptionNo));
}
@PostMapping("/exception-orders/{exceptionNo}/start-processing")
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_PROCESS + "')")
public ApiResponse<ExceptionOrderResponses.CommandResult> startProcessing(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@PathVariable String exceptionNo, @Valid @RequestBody ExceptionOrderRequests.StartProcessing request) {
return ApiResponse.success(service.startProcessing(requiredScope(user, customerId), operatorId(user), exceptionNo, request));
}
@PostMapping("/exception-orders/{exceptionNo}/complete")
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_PROCESS + "')")
public ApiResponse<ExceptionOrderResponses.CommandResult> complete(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@PathVariable String exceptionNo, @Valid @RequestBody ExceptionOrderRequests.Complete request) {
return ApiResponse.success(service.complete(requiredScope(user, customerId), operatorId(user), exceptionNo, request));
}
@PostMapping("/exception-orders/{exceptionNo}/cancel")
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_PROCESS + "')")
public ApiResponse<ExceptionOrderResponses.CommandResult> cancel(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
@PathVariable String exceptionNo, @Valid @RequestBody ExceptionOrderRequests.Cancel request) {
return ApiResponse.success(service.cancel(requiredScope(user, customerId), operatorId(user), exceptionNo, request));
}
@GetMapping("/delivery-orders/{deliveryNo}/return-inbound-options")
@PreAuthorize("hasAuthority('" + OverseasPermissions.EXCEPTION_ORDER_LIST + "')")
public ApiResponse<List<ExceptionOrderResponses.Relation>> returnOptions(@AuthenticationPrincipal AuthenticatedUser user,
@@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
public final class ExceptionOrderRequests {
private ExceptionOrderRequests() {}
@@ -29,6 +30,32 @@ public final class ExceptionOrderRequests {
@NotBlank @Size(max = 1000) String reason,
@NotBlank @Size(max = 32) String returnDirection,
@Size(max = 128) String returnLogisticsNo,
@Min(1) Integer packageCount,
@Size(max = 128) String skuCode) {}
@NotNull @Size(min = 1, max = 200) List<ReturnItem> items,
@NotBlank @Size(max = 255) String idempotencyKey) {}
public record ReturnItem(@NotNull Long outboundPalletItemId,
@NotNull Long inventoryPositionId,
@Min(1) long packageCount) {}
public record StartProcessing(@Min(0) long expectedVersion,
@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String handlingAction) {}
public record Complete(@Min(0) long expectedVersion,
@NotBlank @Size(max = 255) String idempotencyKey,
@NotBlank @Size(max = 2000) String handlingResult,
@NotNull @Size(min = 1, max = 20) List<String> evidenceFileIds,
@Size(max = 200) List<ReturnReceipt> receipts) {}
public record ReturnReceipt(@NotNull Long outboundPalletItemId,
@NotNull Long inventoryPositionId,
@Min(1) long receivedPackages,
@NotNull Long targetPalletId,
@NotBlank @Size(max = 128) String targetPalletNo,
@NotNull Long targetLocationId,
@NotBlank @Size(max = 128) String targetLocationCode) {}
public record Cancel(@Min(0) long expectedVersion,
@NotBlank @Size(max = 255) String idempotencyKey,
@NotBlank @Size(max = 1000) String reason) {}
}
@@ -1,6 +1,7 @@
package com.baiyue.lastleg.exceptionorder.dto;
import java.time.LocalDateTime;
import java.util.List;
public final class ExceptionOrderResponses {
private ExceptionOrderResponses() {}
@@ -13,13 +14,22 @@ public final class ExceptionOrderResponses {
Long deliveryOrderId, String deliveryNo, String outboundNo,
String skuCode, Integer packageCount, String returnDirection,
String returnLogisticsNo, String handlingAction,
String handlerName, LocalDateTime exceptionAt, LocalDateTime updatedAt) {}
String handlingResult, String handlerName, LocalDateTime exceptionAt,
LocalDateTime completedAt, LocalDateTime updatedAt, long version,
List<String> evidenceFileIds, List<String> allowedActions) {}
public record Relation(Long lastMileOrderId, String lastMileOrderNo,
Long inboundOrderId, String inboundCode,
String customerId, String customerName,
String overseasWarehouseId, String overseasWarehouseName,
String deliveryNo, String outboundNo, Integer availablePackages) {}
String deliveryNo, String outboundNo, Integer availablePackages,
List<ReturnSource> returnSources) {}
public record Created(Long id, String exceptionNo, String status) {}
public record ReturnSource(Long outboundPalletItemId, Long inventoryPositionId,
String palletNo, String skuCode, long maxReturnPackages) {}
public record Detail(Row order, List<ReturnSource> returnSources) {}
public record CommandResult(Long id, String exceptionNo, String status, long version,
Long inventoryAdjustmentId, long restoredPackages,
LocalDateTime completedAt, boolean replayed) {}
public record Created(Long id, String exceptionNo, String status, long version) {}
}
@@ -32,11 +32,16 @@ public class ExceptionOrder {
private String returnDirection;
private String returnLogisticsNo;
private String handlingAction;
private String handlingResult;
private String evidenceFileIdsJson;
private Long inventoryAdjustmentId;
private String handlerId;
private String handlerName;
private String sourceType;
private String createdBy;
private LocalDateTime exceptionAt;
private LocalDateTime completedAt;
private Long version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,21 @@
package com.baiyue.lastleg.exceptionorder.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_exception_order_command")
public class ExceptionOrderCommand {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String customerId;
private Long exceptionOrderId;
private String action;
private String idempotencyKey;
private String requestHash;
private String resultJson;
private LocalDateTime createdAt;
}
@@ -0,0 +1,23 @@
package com.baiyue.lastleg.exceptionorder.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_exception_return_item")
public class ExceptionReturnItem {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String customerId;
private Long exceptionOrderId;
private Long outboundPalletItemId;
private Long inventoryPositionId;
private String palletNo;
private String skuCode;
private Long maxReturnPackages;
private Long approvedReturnPackages;
private LocalDateTime createdAt;
}
@@ -0,0 +1,15 @@
package com.baiyue.lastleg.exceptionorder.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrderCommand;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ExceptionOrderCommandMapper extends BaseMapper<ExceptionOrderCommand> {
@Select("SELECT * FROM ll_exception_order_command WHERE customer_id=#{customerId} AND action=#{action} AND idempotency_key=#{key} LIMIT 1")
ExceptionOrderCommand findByKey(@Param("customerId") String customerId,
@Param("action") String action,
@Param("key") String key);
}
@@ -5,6 +5,7 @@ import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrder;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface ExceptionOrderMapper extends BaseMapper<ExceptionOrder> {
@@ -12,4 +13,10 @@ public interface ExceptionOrderMapper extends BaseMapper<ExceptionOrder> {
"AND exception_type='RETURN' LIMIT 1")
ExceptionOrder findDeliveryReturn(@Param("customerId") String customerId,
@Param("deliveryOrderId") Long deliveryOrderId);
@Select("SELECT * FROM ll_exception_order WHERE customer_id=#{customerId} AND exception_no=#{exceptionNo} LIMIT 1 FOR UPDATE")
ExceptionOrder lockByNo(@Param("customerId") String customerId, @Param("exceptionNo") String exceptionNo);
@Select("SELECT * FROM ll_exception_order WHERE customer_id=#{customerId} AND exception_no=#{exceptionNo} LIMIT 1")
ExceptionOrder findByNo(@Param("customerId") String customerId, @Param("exceptionNo") String exceptionNo);
}
@@ -0,0 +1,16 @@
package com.baiyue.lastleg.exceptionorder.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.exceptionorder.entity.ExceptionReturnItem;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface ExceptionReturnItemMapper extends BaseMapper<ExceptionReturnItem> {
@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);
}
@@ -8,15 +8,31 @@ import com.baiyue.lastleg.delivery.entity.DeliveryOperation;
import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
import com.baiyue.lastleg.delivery.mapper.DeliveryOperationMapper;
import com.baiyue.lastleg.delivery.mapper.DeliveryOrderMapper;
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
import com.baiyue.lastleg.delivery.entity.OutboundPdaPick;
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPdaPickMapper;
import com.baiyue.lastleg.entity.LastlegInboundOrder;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderRequests;
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderResponses;
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrder;
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrderCommand;
import com.baiyue.lastleg.exceptionorder.entity.ExceptionReturnItem;
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderCommandMapper;
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderMapper;
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionReturnItemMapper;
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreRequest;
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreResponse;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
import com.baiyue.lastleg.mapper.LastlegInboundOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import jakarta.annotation.Resource;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@@ -26,8 +42,16 @@ import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@Service
public class ExceptionOrderService {
@@ -37,6 +61,13 @@ public class ExceptionOrderService {
@Resource private LastlegInboundOrderMapper inboundOrders;
@Resource private DeliveryOrderMapper deliveryOrders;
@Resource private DeliveryOperationMapper deliveryOperations;
@Resource private OutboundOrderMapper outboundOrders;
@Resource private OutboundPalletItemMapper outboundPalletItems;
@Resource private OutboundPdaPickMapper outboundPdaPicks;
@Resource private ExceptionReturnItemMapper returnItems;
@Resource private ExceptionOrderCommandMapper commands;
@Resource private InventoryCommandService inventoryCommands;
@Resource private ObjectMapper objectMapper;
public PageResult<ExceptionOrderResponses.Row> page(String scopedCustomerId, ExceptionOrderRequests.Query query) {
String customerId = customerScope(scopedCustomerId, query.customerId());
@@ -65,6 +96,12 @@ public class ExceptionOrderService {
return relation(scopedCustomerId, requestedCustomerId, inboundCode, lastMileOrderNo);
}
public ExceptionOrderResponses.Detail detail(String customerId, String exceptionNo) {
ExceptionOrder order = exceptionOrders.findByNo(customerId, exceptionNo);
if (order == null) throw notFound("异常订单不存在");
return new ExceptionOrderResponses.Detail(row(order), storedReturnSources(order));
}
public List<ExceptionOrderResponses.Relation> returnCandidates(String customerId, String deliveryNo) {
DeliveryOrder delivery = deliveryOrders.findByNo(customerId, deliveryNo);
if (delivery == null) throw notFound("派送单不存在");
@@ -72,7 +109,8 @@ public class ExceptionOrderService {
.eq(LastlegOrder::getCustomerId, customerId)
.eq(LastlegOrder::getDeliveryOrderNo, deliveryNo)
.orderByAsc(LastlegOrder::getOrderNo)).stream()
.map(order -> relation(customerId, customerId, order.getInboundCode(), order.getOrderNo()))
.map(order -> withReturnSources(
relation(customerId, customerId, order.getInboundCode(), order.getOrderNo()), delivery))
.distinct().toList();
}
@@ -88,19 +126,40 @@ public class ExceptionOrderService {
@Transactional
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.findByNo(customerId, deliveryNo);
if (delivery == null) throw notFound("派送单不存在");
ExceptionOrder existing = exceptionOrders.findDeliveryReturn(customerId, delivery.getId());
if (existing != null) return new ExceptionOrderResponses.Created(existing.getId(), existing.getExceptionNo(), existing.getStatus());
if (existing != null) throw conflict("该派送单已存在退回异常单");
List<ExceptionOrderResponses.Relation> candidates = returnCandidates(customerId, deliveryNo);
ExceptionOrderResponses.Relation selected = candidates.stream()
.filter(candidate -> Objects.equals(candidate.inboundOrderId(), request.inboundOrderId()))
.findFirst().orElseThrow(() -> conflict("所选入仓单不属于该派送单"));
List<ExceptionOrderResponses.ReturnSource> available = selected.returnSources();
List<ExceptionOrderRequests.ReturnItem> requested = request.items();
validateReturnItems(available, requested);
int packages = Math.toIntExact(requested.stream().mapToLong(ExceptionOrderRequests.ReturnItem::packageCount).sum());
Set<String> skus = available.stream().filter(source -> requested.stream().anyMatch(item -> sameSource(source, item)))
.map(ExceptionOrderResponses.ReturnSource::skuCode).filter(StringUtils::hasText).collect(java.util.stream.Collectors.toSet());
String sku = skus.size() == 1 ? skus.iterator().next() : "MIXED";
ExceptionOrderResponses.Created created = insert(selected, delivery, "RETURN", request.reason(),
request.packageCount(), request.skuCode(), request.returnDirection(), request.returnLogisticsNo(),
packages, sku, request.returnDirection(), request.returnLogisticsNo(),
"DELIVERY_RETURN", operatorId, operatorName);
for (ExceptionOrderRequests.ReturnItem requestedItem : requested) {
ExceptionOrderResponses.ReturnSource source = available.stream()
.filter(candidate -> sameSource(candidate, requestedItem)).findFirst().orElseThrow();
ExceptionReturnItem item = new ExceptionReturnItem();
item.setId(IdWorker.getId()); item.setCustomerId(customerId); item.setExceptionOrderId(created.id());
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.setCreatedAt(LocalDateTime.now()); returnItems.insert(item);
}
LocalDateTime now = LocalDateTime.now();
delivery.setStatus("RETURN_PENDING");
delivery.setUpdatedAt(now);
@@ -117,9 +176,84 @@ public class ExceptionOrderService {
operation.setOperationNode("MARK_RETURN"); operation.setOperatorId(operatorId); operation.setOperatorName(operatorName);
operation.setResult("已生成异常退货单 " + created.exceptionNo()); operation.setCreatedAt(now);
deliveryOperations.insert(operation);
recordCommand(customerId, created.id(), "MARK_RETURN", request.idempotencyKey(), requestHash, created);
return created;
}
@Transactional
public ExceptionOrderResponses.CommandResult startProcessing(String customerId, String operatorId,
String exceptionNo,
ExceptionOrderRequests.StartProcessing request) {
String requestHash = hash(request);
ExceptionOrderCommand replay = commands.findByKey(customerId, "START_PROCESSING", request.idempotencyKey().trim());
if (replay != null) return replayResult(replay, requestHash);
ExceptionOrder order = lock(customerId, exceptionNo, request.expectedVersion(), "PENDING");
LocalDateTime now = LocalDateTime.now();
order.setStatus("PROCESSING"); order.setHandlingAction(trim(request.handlingAction()));
order.setHandlerId(operatorId); order.setUpdatedAt(now); order.setVersion(version(order) + 1);
exceptionOrders.updateById(order);
ExceptionOrderResponses.CommandResult result = commandResult(order, 0, false);
recordCommand(customerId, order.getId(), "START_PROCESSING", request.idempotencyKey(), requestHash, result);
return result;
}
@Transactional
public ExceptionOrderResponses.CommandResult cancel(String customerId, String operatorId,
String exceptionNo,
ExceptionOrderRequests.Cancel request) {
String requestHash = hash(request);
ExceptionOrderCommand replay = commands.findByKey(customerId, "CANCEL", request.idempotencyKey().trim());
if (replay != null) return replayResult(replay, requestHash);
ExceptionOrder order = lock(customerId, exceptionNo, request.expectedVersion(), "PENDING");
LocalDateTime now = LocalDateTime.now();
order.setStatus("CANCELLED"); order.setHandlingResult(trim(request.reason()));
order.setHandlerId(operatorId); order.setUpdatedAt(now); order.setVersion(version(order) + 1);
exceptionOrders.updateById(order);
ExceptionOrderResponses.CommandResult result = commandResult(order, 0, false);
recordCommand(customerId, order.getId(), "CANCEL", request.idempotencyKey(), requestHash, result);
return result;
}
@Transactional
public ExceptionOrderResponses.CommandResult complete(String customerId, String operatorId,
String exceptionNo,
ExceptionOrderRequests.Complete request) {
String requestHash = hash(request);
ExceptionOrderCommand replay = commands.findByKey(customerId, "COMPLETE", request.idempotencyKey().trim());
if (replay != null) return replayResult(replay, requestHash);
ExceptionOrder order = lock(customerId, exceptionNo, request.expectedVersion(), "PROCESSING");
ReturnedInventoryRestoreResponse restored = null;
if ("RETURN".equals(order.getExceptionType()) && "OVERSEAS_WAREHOUSE".equals(order.getReturnDirection())) {
List<ExceptionReturnItem> stored = returnItems.findByException(customerId, order.getId());
if (stored.isEmpty()) throw conflict("退回异常单缺少已冻结的原出库库存明细");
if (request.receipts() == null || request.receipts().isEmpty()) throw badRequest("请填写退回实收明细");
restored = inventoryCommands.restoreReturnedInventory(customerId, operatorId,
new ReturnedInventoryRestoreRequest(order.getExceptionNo(), order.getOutboundNo(),
order.getOverseasWarehouseId(), order.getInboundOrderId(),
stored.stream().map(item -> new ReturnedInventoryRestoreRequest.SourceItem(
item.getOutboundPalletItemId(), item.getInventoryPositionId(), item.getSkuCode(),
Objects.requireNonNullElse(item.getApprovedReturnPackages(), 0L))).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()));
} 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()));
order.setInventoryAdjustmentId(restored == null ? null : restored.adjustmentId());
order.setHandlerId(operatorId); order.setCompletedAt(now); order.setUpdatedAt(now);
order.setVersion(version(order) + 1); exceptionOrders.updateById(order);
closeReturnedDelivery(order, operatorId, now);
long restoredPackages = restored == null ? 0 : restored.restoredPackages();
ExceptionOrderResponses.CommandResult result = commandResult(order, restoredPackages, false);
recordCommand(customerId, order.getId(), "COMPLETE", request.idempotencyKey(), requestHash, result);
return result;
}
private ExceptionOrderResponses.Relation relation(String scopedCustomerId, String requestedCustomerId,
String inboundCode, String lastMileOrderNo) {
String customerId = customerScope(scopedCustomerId, requestedCustomerId);
@@ -150,7 +284,7 @@ public class ExceptionOrderService {
resolvedCustomer, tail == null ? null : tail.getCustomerName(),
tail == null ? null : tail.getOverseasWarehouseId(), tail == null ? null : tail.getOverseasWarehouseName(),
tail == null ? null : tail.getDeliveryOrderNo(), tail == null ? null : tail.getOutboundOrderNo(),
tail == null ? inboundOrder.getPackageCount() : tail.getPackageCount());
tail == null ? inboundOrder.getPackageCount() : tail.getPackageCount(), List.of());
}
private ExceptionOrderResponses.Created insert(ExceptionOrderResponses.Relation relation, DeliveryOrder delivery,
@@ -180,10 +314,11 @@ public class ExceptionOrderService {
value.setHandlerId(operatorId); value.setHandlerName(operatorName);
}
value.setSourceType(sourceType); value.setCreatedBy(operatorId);
value.setVersion(0L);
value.setExceptionAt(now); value.setCreatedAt(now); value.setUpdatedAt(now);
try { exceptionOrders.insert(value); }
catch (DuplicateKeyException exception) { throw conflict("该关联单据已存在异常退货单"); }
return new ExceptionOrderResponses.Created(value.getId(), value.getExceptionNo(), value.getStatus());
return new ExceptionOrderResponses.Created(value.getId(), value.getExceptionNo(), value.getStatus(), version(value));
}
private LastlegOrder uniqueTail(String customerId, String orderNo, String inboundCode) {
@@ -227,8 +362,159 @@ public class ExceptionOrderService {
value.getOverseasWarehouseName(), value.getLastMileOrderId(), value.getLastMileOrderNo(),
value.getInboundOrderId(), value.getInboundCode(), value.getDeliveryOrderId(), value.getDeliveryNo(),
value.getOutboundNo(), value.getSkuCode(), value.getPackageCount(), value.getReturnDirection(),
value.getReturnLogisticsNo(), value.getHandlingAction(), value.getHandlerName(),
value.getExceptionAt(), value.getUpdatedAt());
value.getReturnLogisticsNo(), value.getHandlingAction(), value.getHandlingResult(), value.getHandlerName(),
value.getExceptionAt(), value.getCompletedAt(), value.getUpdatedAt(), version(value),
readStringList(value.getEvidenceFileIdsJson()), allowedActions(value));
}
private ExceptionOrderResponses.Relation withReturnSources(ExceptionOrderResponses.Relation relation,
DeliveryOrder delivery) {
List<ExceptionOrderResponses.ReturnSource> sources = returnSources(delivery).stream()
.filter(source -> relation.inboundCode() == null || Objects.equals(source.inboundCode(), relation.inboundCode()))
.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(),
relation.outboundNo(), relation.availablePackages(), sources);
}
private List<Source> returnSources(DeliveryOrder delivery) {
OutboundOrder outbound = outboundOrders.findByNo(delivery.getCustomerId(), delivery.getOutboundNo());
if (outbound == null) throw conflict("派送单缺少原出库单据");
Map<Long, OutboundPalletItem> itemById = new LinkedHashMap<>();
outboundPalletItems.findByOutbound(delivery.getCustomerId(), outbound.getId())
.forEach(item -> itemById.put(item.getId(), item));
Map<String, Source> aggregated = new LinkedHashMap<>();
for (OutboundPdaPick pick : outboundPdaPicks.findByOutbound(delivery.getCustomerId(), outbound.getId())) {
OutboundPalletItem item = itemById.get(pick.getOutboundPalletItemId());
if (item == null || pick.getInventoryPositionId() == null) continue;
String key = item.getId() + ":" + pick.getInventoryPositionId();
aggregated.merge(key, new Source(item.getId(), pick.getInventoryPositionId(), item.getPalletNo(),
item.getSkuCode(), item.getInboundCode(), Objects.requireNonNullElse(pick.getPackages(), 0L)),
(left, right) -> left.withPackages(left.maxReturnPackages() + right.maxReturnPackages()));
}
return new ArrayList<>(aggregated.values());
}
private List<ExceptionOrderResponses.ReturnSource> storedReturnSources(ExceptionOrder order) {
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();
}
private void validateReturnItems(List<ExceptionOrderResponses.ReturnSource> available,
List<ExceptionOrderRequests.ReturnItem> requested) {
Set<String> unique = new java.util.HashSet<>();
for (ExceptionOrderRequests.ReturnItem item : requested) {
String key = item.outboundPalletItemId() + ":" + item.inventoryPositionId();
if (!unique.add(key)) throw badRequest("退回库存明细不能重复");
ExceptionOrderResponses.ReturnSource source = available.stream().filter(value -> sameSource(value, item))
.findFirst().orElseThrow(() -> conflict("所选退回库存不属于该派送单和入仓单"));
if (item.packageCount() > source.maxReturnPackages()) throw conflict("退回箱数超过原出库箱数");
}
}
private boolean sameSource(ExceptionOrderResponses.ReturnSource source, ExceptionOrderRequests.ReturnItem item) {
return Objects.equals(source.outboundPalletItemId(), item.outboundPalletItemId())
&& Objects.equals(source.inventoryPositionId(), item.inventoryPositionId());
}
private ExceptionOrder lock(String customerId, String exceptionNo, long expectedVersion, String expectedStatus) {
ExceptionOrder order = exceptionOrders.lockByNo(customerId, exceptionNo);
if (order == null) throw notFound("异常订单不存在");
if (version(order) != expectedVersion) throw conflict("异常订单版本已变化,请刷新后重试");
if (!expectedStatus.equals(order.getStatus())) throw conflict("当前异常状态不允许执行该操作");
return order;
}
private void closeReturnedDelivery(ExceptionOrder order, String operatorId, LocalDateTime now) {
if (!"RETURN".equals(order.getExceptionType()) || !StringUtils.hasText(order.getDeliveryNo())) return;
DeliveryOrder delivery = deliveryOrders.findByNo(order.getCustomerId(), order.getDeliveryNo());
if (delivery == null || !"RETURN_PENDING".equals(delivery.getStatus())) {
throw conflict("关联派送单不是退货处理中状态");
}
delivery.setStatus("RETURNED"); delivery.setUpdatedAt(now); deliveryOrders.updateById(delivery);
for (LastlegOrder tail : lastMileOrders.selectList(new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getCustomerId, order.getCustomerId())
.eq(LastlegOrder::getDeliveryOrderNo, order.getDeliveryNo()))) {
tail.setAbnormal(false); tail.setSubOrderStatus("DELIVERY:RETURNED"); tail.setUpdatedAt(now);
lastMileOrders.updateById(tail);
}
DeliveryOperation operation = new DeliveryOperation();
operation.setCustomerId(order.getCustomerId()); operation.setDeliveryOrderId(delivery.getId());
operation.setOperationNode("RETURN_COMPLETED"); operation.setOperatorId(operatorId);
operation.setOperatorName(operatorId); operation.setResult("异常退回处理完成:" + order.getExceptionNo());
operation.setCreatedAt(now); deliveryOperations.insert(operation);
}
private List<String> allowedActions(ExceptionOrder order) {
return switch (Objects.requireNonNullElse(order.getStatus(), "")) {
case "PENDING" -> List.of("START_PROCESSING", "CANCEL_EXCEPTION");
case "PROCESSING" -> List.of("COMPLETE_EXCEPTION");
default -> List.of();
};
}
private ExceptionOrderResponses.CommandResult commandResult(ExceptionOrder order, long restoredPackages,
boolean replayed) {
return new ExceptionOrderResponses.CommandResult(order.getId(), order.getExceptionNo(), order.getStatus(),
version(order), order.getInventoryAdjustmentId(), restoredPackages, order.getCompletedAt(), replayed);
}
private void recordCommand(String customerId, Long orderId, String action, String idempotencyKey,
String requestHash, Object result) {
ExceptionOrderCommand command = new ExceptionOrderCommand();
command.setId(IdWorker.getId()); command.setCustomerId(customerId); command.setExceptionOrderId(orderId);
command.setAction(action); command.setIdempotencyKey(idempotencyKey.trim());
command.setRequestHash(requestHash); command.setResultJson(write(result)); command.setCreatedAt(LocalDateTime.now());
commands.insert(command);
}
private ExceptionOrderResponses.CommandResult replayResult(ExceptionOrderCommand command, String requestHash) {
requireReplayHash(command, requestHash);
try {
ExceptionOrderResponses.CommandResult result = objectMapper.readValue(command.getResultJson(), ExceptionOrderResponses.CommandResult.class);
return new ExceptionOrderResponses.CommandResult(result.id(), result.exceptionNo(), result.status(), result.version(),
result.inventoryAdjustmentId(), result.restoredPackages(), result.completedAt(), true);
} catch (JsonProcessingException exception) { throw new IllegalStateException("异常命令结果损坏", exception); }
}
private ExceptionOrderResponses.Created replayCreated(ExceptionOrderCommand command, String requestHash) {
requireReplayHash(command, requestHash);
try { return objectMapper.readValue(command.getResultJson(), ExceptionOrderResponses.Created.class); }
catch (JsonProcessingException exception) { throw new IllegalStateException("异常命令结果损坏", exception); }
}
private void requireReplayHash(ExceptionOrderCommand command, String requestHash) {
if (!Objects.equals(command.getRequestHash(), requestHash)) throw conflict("幂等键已用于不同请求");
}
private String hash(Object value) {
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(write(value).getBytes(StandardCharsets.UTF_8))); }
catch (NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
}
private String write(Object value) {
try { return objectMapper.writeValueAsString(value); }
catch (JsonProcessingException exception) { throw new IllegalStateException("无法保存异常处理结果", exception); }
}
private List<String> readStringList(String value) {
if (!StringUtils.hasText(value)) return List.of();
try { return objectMapper.readValue(value, new TypeReference<>() { }); }
catch (JsonProcessingException exception) { throw new IllegalStateException("异常处理证据数据损坏", exception); }
}
private static long version(ExceptionOrder order) { return Objects.requireNonNullElse(order.getVersion(), 0L); }
private record Source(Long outboundPalletItemId, Long inventoryPositionId, String palletNo,
String skuCode, String inboundCode, long maxReturnPackages) {
Source withPackages(long packages) { return new Source(outboundPalletItemId, inventoryPositionId, palletNo, skuCode, inboundCode, packages); }
ExceptionOrderResponses.ReturnSource response() {
return new ExceptionOrderResponses.ReturnSource(outboundPalletItemId, inventoryPositionId,
palletNo, skuCode, maxReturnPackages);
}
}
private String customerScope(String scopedCustomerId, String requestedCustomerId) {