优化
This commit is contained in:
+32
@@ -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,
|
||||
|
||||
+29
-2
@@ -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) {}
|
||||
}
|
||||
|
||||
+13
-3
@@ -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) {}
|
||||
}
|
||||
|
||||
+5
@@ -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;
|
||||
}
|
||||
|
||||
+21
@@ -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;
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
+15
@@ -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);
|
||||
}
|
||||
+7
@@ -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);
|
||||
}
|
||||
|
||||
+16
@@ -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);
|
||||
}
|
||||
+293
-7
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user