优化
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.lastleg.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 国内 Warehouse Service 平台公共仓库只读目录。 */
|
||||
@FeignClient(name = "warehouse-service", contextId = "domesticWarehouseDirectoryClient",
|
||||
url = "${baiyue.clients.domestic-warehouse.url:}")
|
||||
public interface DomesticWarehouseDirectoryClient {
|
||||
@GetMapping("/internal/warehouses/0/{warehouseId}/active")
|
||||
ApiResponse<Detail> activeWarehouse(
|
||||
@PathVariable("warehouseId") long warehouseId,
|
||||
@RequestHeader("X-Internal-Service-Token") String internalToken);
|
||||
|
||||
record Detail(Warehouse warehouse, List<Address> addresses) { }
|
||||
|
||||
record Warehouse(Long id, Long tenantId, String warehouseCode, String warehouseName,
|
||||
String warehouseType, String countryCode, String status) { }
|
||||
|
||||
record Address(String stateProvince) { }
|
||||
}
|
||||
+23
-6
@@ -7,7 +7,8 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderDetailResponse;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderSummary;
|
||||
import com.baiyue.lastleg.dto.PendingContainerSummary;
|
||||
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail;
|
||||
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import com.baiyue.lastleg.service.LastlegOrderService;
|
||||
@@ -77,11 +78,23 @@ public class LastlegOrderController {
|
||||
@Operation(summary = "分页查询待分配柜级摘要",
|
||||
description = "按稳定 sourceContainerBookingId 聚合待分配尾程订单;平台跨客户查询。Required authority: "
|
||||
+ OverseasPermissions.LAST_MILE_ORDER_LIST)
|
||||
public ApiResponse<PageResult<PendingContainerSummary>> containerSummaries(
|
||||
public ApiResponse<PageResult<ContainerPlanProgressSummary>> containerSummaries(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid LastlegOrderRequests.PendingContainerQuery query) {
|
||||
@Valid LastlegOrderRequests.ContainerProgressQuery query) {
|
||||
platformScope(user, null);
|
||||
return ApiResponse.success(orderService.pendingContainerPage(query));
|
||||
return ApiResponse.success(orderService.containerProgressPage(query));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/last-leg/orders/container-summaries/{sourceContainerBookingId}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
|
||||
@Operation(summary = "查询柜级入库计划进度详情",
|
||||
description = "按稳定 sourceContainerBookingId 返回柜内全部尾程订单、入仓码及各自计划状态,不受订单分页影响。Required authority: "
|
||||
+ OverseasPermissions.LAST_MILE_ORDER_LIST)
|
||||
public ApiResponse<ContainerPlanProgressDetail> containerSummaryDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String sourceContainerBookingId) {
|
||||
platformScope(user, null);
|
||||
return ApiResponse.success(orderService.containerProgressDetail(sourceContainerBookingId));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/last-leg/orders/{id}")
|
||||
@@ -110,7 +123,9 @@ public class LastlegOrderController {
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/{id}/destination")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
|
||||
@Operation(summary = "Transfer one order to a destination warehouse", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT)
|
||||
@Operation(summary = "将单票尾程订单转入 FBA 目的仓",
|
||||
description = "目标仓必须来自国内 Warehouse Service 的平台公共 ACTIVE FBA 主档,ID/编码/名称必须一致。Required authority: "
|
||||
+ OverseasPermissions.LAST_MILE_ORDER_EDIT)
|
||||
public ApiResponse<Integer> transferOne(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@@ -121,7 +136,9 @@ public class LastlegOrderController {
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/destination")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
|
||||
@Operation(summary = "Transfer multiple orders to a destination warehouse", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_EDIT)
|
||||
@Operation(summary = "批量将尾程订单转入 FBA 目的仓",
|
||||
description = "目标仓必须来自国内 Warehouse Service 的平台公共 ACTIVE FBA 主档,ID/编码/名称必须一致。Required authority: "
|
||||
+ OverseasPermissions.LAST_MILE_ORDER_EDIT)
|
||||
public ApiResponse<Integer> transferBatch(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 柜级摘要及柜内全部尾程订单、入仓码和计划状态。 */
|
||||
public record ContainerPlanProgressDetail(
|
||||
ContainerPlanProgressSummary summary,
|
||||
List<Item> orders
|
||||
) {
|
||||
public ContainerPlanProgressDetail {
|
||||
orders = orders == null ? List.of() : List.copyOf(orders);
|
||||
}
|
||||
|
||||
public record Item(
|
||||
Long orderId,
|
||||
String customerId,
|
||||
String orderNo,
|
||||
String inboundCode,
|
||||
String orderStatus,
|
||||
Long planId,
|
||||
String planNo,
|
||||
String planStatus,
|
||||
boolean planValid,
|
||||
LocalDateTime plannedArrivalAt
|
||||
) {
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 平台按稳定来源柜查询的入库计划完整度事实。 */
|
||||
public record ContainerPlanProgressSummary(
|
||||
String sourceContainerBookingId,
|
||||
String containerNo,
|
||||
long totalOrderCount,
|
||||
long inboundCodeCount,
|
||||
long missingInboundCodeCount,
|
||||
long planCount,
|
||||
long plannedInboundCodeCount,
|
||||
long pendingPlanCount,
|
||||
LocalDateTime plannedArrivalAt,
|
||||
boolean allPlansReady,
|
||||
String blockingReason
|
||||
) {
|
||||
}
|
||||
@@ -36,14 +36,14 @@ public final class LastlegOrderRequests {
|
||||
}
|
||||
}
|
||||
|
||||
public record PendingContainerQuery(
|
||||
public record ContainerProgressQuery(
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size,
|
||||
String containerNo,
|
||||
LocalDateTime createdFrom,
|
||||
LocalDateTime createdTo
|
||||
) {
|
||||
public PendingContainerQuery {
|
||||
public ContainerProgressQuery {
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.baiyue.lastleg.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 平台待分配尾程订单按来源货柜聚合的只读摘要。 */
|
||||
public record PendingContainerSummary(
|
||||
String sourceContainerBookingId,
|
||||
String containerNo,
|
||||
long orderCount,
|
||||
long inboundCodeCount,
|
||||
long customerCount,
|
||||
long totalPackageCount,
|
||||
long totalPalletCount,
|
||||
BigDecimal totalVolumeCbm,
|
||||
boolean eligible,
|
||||
String ineligibleReason
|
||||
) {
|
||||
}
|
||||
+6
-2
@@ -6,9 +6,13 @@ import java.util.List;
|
||||
public final class ReceivingContainerResponses {
|
||||
private ReceivingContainerResponses() { }
|
||||
|
||||
public record Candidate(String containerNo, String status, int planCount, int inboundCodeCount,
|
||||
public record Candidate(String sourceContainerBookingId, String containerNo, String status,
|
||||
int totalOrderCount, int inboundCodeCount, int missingInboundCodeCount,
|
||||
int planCount, int plannedInboundCodeCount, int pendingPlanCount,
|
||||
List<String> destinationCodes, int plannedPalletCount,
|
||||
int totalPackages, int receivedPackages, boolean currentAccount) { }
|
||||
int totalPackages, int receivedPackages,
|
||||
boolean operable, boolean allPlansReady, String blockReason,
|
||||
LocalDateTime plannedArrivalAt, boolean currentAccount) { }
|
||||
|
||||
public record WarehouseProfile(String warehouseId, String warehouseCode, String warehouseName) { }
|
||||
|
||||
|
||||
+2
@@ -16,6 +16,8 @@ public class ReceivingContainerSession {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private String customerId;
|
||||
private String sourceContainerBookingId;
|
||||
private String containerFactHash;
|
||||
private String containerNo;
|
||||
private Long overseasWarehouseId;
|
||||
private String overseasWarehouseName;
|
||||
|
||||
+129
-17
@@ -96,28 +96,24 @@ public class ReceivingContainerService {
|
||||
public List<ReceivingContainerResponses.Candidate> candidates(String operatorKey, long overseasWarehouseId) {
|
||||
ReceivingContainerSession current = sessions.findActiveByOperator(activeOperatorKey(operatorKey));
|
||||
requireSessionWarehouse(current, overseasWarehouseId);
|
||||
Map<String, List<InboundPlan>> grouped = new LinkedHashMap<>();
|
||||
plans.selectList(new LambdaQueryWrapper<InboundPlan>()
|
||||
.ne(InboundPlan::getStatus, "COMPLETED").orderByAsc(InboundPlan::getCreatedAt)).forEach(plan -> {
|
||||
if (plan.getContainerNo() != null && !plan.getContainerNo().isBlank()) {
|
||||
grouped.computeIfAbsent(plan.getContainerNo(), ignored -> new ArrayList<>()).add(plan);
|
||||
}
|
||||
});
|
||||
List<LastlegOrder> orderFacts = orders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.isNotNull(LastlegOrder::getSourceContainerBookingId)
|
||||
.isNotNull(LastlegOrder::getContainerNo)
|
||||
.orderByAsc(LastlegOrder::getSourceContainerBookingId, LastlegOrder::getId));
|
||||
List<InboundPlan> planFacts = plans.selectList(new LambdaQueryWrapper<InboundPlan>().orderByAsc(InboundPlan::getId));
|
||||
List<InboundPlanLine> lineFacts = lines.selectList(new LambdaQueryWrapper<InboundPlanLine>().orderByAsc(InboundPlanLine::getId));
|
||||
Map<String, List<LastlegOrder>> grouped = orderFacts.stream()
|
||||
.filter(order -> trim(order.getSourceContainerBookingId()) != null && trim(order.getContainerNo()) != null)
|
||||
.collect(Collectors.groupingBy(LastlegOrder::getSourceContainerBookingId, LinkedHashMap::new, Collectors.toList()));
|
||||
String operatorLockKey = activeOperatorKey(operatorKey);
|
||||
Set<String> reservedByOthers = sessions.findActiveContainers().stream()
|
||||
.filter(session -> !operatorLockKey.equals(session.getActiveOperatorKey()))
|
||||
.map(ReceivingContainerSession::getActiveContainerKey).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
return grouped.entrySet().stream()
|
||||
.filter(entry -> eligible(entry.getValue(), overseasWarehouseId))
|
||||
.filter(entry -> !reservedByOthers.contains(activeContainerKey(entry.getKey())))
|
||||
.map(entry -> {
|
||||
requireSingleCustomer(entry.getValue(), entry.getKey());
|
||||
Totals totals = totals(entry.getValue());
|
||||
boolean active = current != null && entry.getKey().equals(current.getContainerNo());
|
||||
return new ReceivingContainerResponses.Candidate(entry.getKey(), active ? current.getStatus() : "WAIT_SIGN",
|
||||
entry.getValue().size(), totals.inboundCount(), totals.destinationCodes(), totals.plannedPalletCount(),
|
||||
totals.total(), totals.received(), active);
|
||||
}).toList();
|
||||
.map(entry -> readiness(entry.getKey(), entry.getValue(), planFacts, lineFacts, overseasWarehouseId))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(value -> !reservedByOthers.contains(activeContainerKey(value.containerNo())))
|
||||
.map(value -> candidate(value, current)).toList();
|
||||
}
|
||||
|
||||
public ReceivingContainerResponses.Session detail(String operatorKey, long overseasWarehouseId, String containerNo) {
|
||||
@@ -144,6 +140,7 @@ public class ReceivingContainerService {
|
||||
public ReceivingContainerResponses.Session prepare(String operatorKey, String operatorName, long overseasWarehouseId, String containerNo,
|
||||
ReceivingContainerRequests.Action request) {
|
||||
PrepareCommand command = new PrepareCommand(containerNo, request.idempotencyKey());
|
||||
ContainerReadiness readiness = requireReadyContainer(containerNo, overseasWarehouseId, true);
|
||||
ReceivingContainerOperation replay = replay(operatorKey, "PREPARE", request.idempotencyKey(), command);
|
||||
if (replay != null) {
|
||||
ReceivingContainerResponses.Session result = read(replay, ReceivingContainerResponses.Session.class);
|
||||
@@ -176,6 +173,8 @@ public class ReceivingContainerService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ReceivingContainerSession session = new ReceivingContainerSession();
|
||||
session.setId(IdWorker.getId()); session.setCustomerId(customerId); session.setContainerNo(containerNo);
|
||||
session.setSourceContainerBookingId(readiness.sourceContainerBookingId());
|
||||
session.setContainerFactHash(readiness.factHash());
|
||||
session.setOverseasWarehouseId(overseasWarehouseId);
|
||||
session.setOverseasWarehouseName(receivingWarehouse.warehouseName());
|
||||
session.setOperatorKey(operatorKey); session.setOperatorName(operatorName);
|
||||
@@ -381,9 +380,117 @@ public class ReceivingContainerService {
|
||||
ReceivingContainerSession session = lock ? sessions.lockActiveByOperator(key)
|
||||
: sessions.findActiveByOperator(key);
|
||||
if (session == null) throw notFound("Current receiving container session was not found");
|
||||
requireStableSessionFacts(session, lock);
|
||||
return session;
|
||||
}
|
||||
|
||||
private ContainerReadiness requireReadyContainer(String containerNo, long overseasWarehouseId, boolean lock) {
|
||||
LambdaQueryWrapper<LastlegOrder> query = new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getContainerNo, containerNo).orderByAsc(LastlegOrder::getId);
|
||||
if (lock) query.last("FOR UPDATE");
|
||||
List<LastlegOrder> orderFacts = orders.selectList(query);
|
||||
Set<String> bookingIds = orderFacts.stream().map(LastlegOrder::getSourceContainerBookingId)
|
||||
.map(ReceivingContainerService::trim).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
if (orderFacts.isEmpty() || bookingIds.size() != 1) {
|
||||
throw conflict("Container must resolve exactly one sourceContainerBookingId");
|
||||
}
|
||||
List<InboundPlan> planFacts = plans.selectList(new LambdaQueryWrapper<InboundPlan>()
|
||||
.eq(InboundPlan::getContainerNo, containerNo).orderByAsc(InboundPlan::getId));
|
||||
List<InboundPlanLine> lineFacts = lines.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.in(InboundPlanLine::getOrderId, orderFacts.stream().map(LastlegOrder::getId).toList())
|
||||
.orderByAsc(InboundPlanLine::getId));
|
||||
ContainerReadiness value = readiness(bookingIds.iterator().next(), orderFacts, planFacts, lineFacts,
|
||||
overseasWarehouseId);
|
||||
if (value == null || !value.operable()) {
|
||||
throw conflict(value == null ? "Container is assigned to another overseas warehouse" : value.blockReason());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void requireStableSessionFacts(ReceivingContainerSession session, boolean lock) {
|
||||
if (trim(session.getContainerFactHash()) == null) return;
|
||||
ContainerReadiness current = requireReadyContainer(session.getContainerNo(), session.getOverseasWarehouseId(), lock);
|
||||
if (!Objects.equals(session.getSourceContainerBookingId(), current.sourceContainerBookingId())
|
||||
|| !Objects.equals(session.getContainerFactHash(), current.factHash())) {
|
||||
throw conflict("Container orders or inbound plans changed after prepare; restart the receiving session");
|
||||
}
|
||||
}
|
||||
|
||||
private ContainerReadiness readiness(String sourceContainerBookingId, List<LastlegOrder> orderFacts,
|
||||
List<InboundPlan> planFacts, List<InboundPlanLine> lineFacts,
|
||||
long overseasWarehouseId) {
|
||||
Set<Long> orderIds = orderFacts.stream().map(LastlegOrder::getId).collect(Collectors.toSet());
|
||||
Map<Long, List<InboundPlanLine>> linesByOrder = lineFacts.stream()
|
||||
.filter(line -> orderIds.contains(line.getOrderId()))
|
||||
.collect(Collectors.groupingBy(InboundPlanLine::getOrderId));
|
||||
Map<Long, InboundPlan> plansById = planFacts.stream().collect(Collectors.toMap(InboundPlan::getId,
|
||||
value -> value, (left, right) -> left));
|
||||
List<InboundPlan> matchedPlans = linesByOrder.values().stream().flatMap(List::stream)
|
||||
.map(line -> plansById.get(line.getPlanId())).filter(Objects::nonNull).distinct().toList();
|
||||
Set<Long> assignedWarehouses = matchedPlans.stream().map(InboundPlan::getOverseasWarehouseId)
|
||||
.filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
orderFacts.stream().map(LastlegOrder::getOverseasWarehouseId).map(ReceivingContainerService::trim)
|
||||
.filter(Objects::nonNull).forEach(value -> {
|
||||
try { assignedWarehouses.add(Long.valueOf(value)); }
|
||||
catch (NumberFormatException ignored) { assignedWarehouses.add(Long.MIN_VALUE); }
|
||||
});
|
||||
if (assignedWarehouses.size() > 1) throw conflict("Container plans have inconsistent receiving warehouse assignments");
|
||||
if (!assignedWarehouses.isEmpty() && !assignedWarehouses.contains(overseasWarehouseId)) return null;
|
||||
Set<String> customers = orderFacts.stream().map(LastlegOrder::getCustomerId).collect(Collectors.toSet());
|
||||
String containerNo = orderFacts.stream().map(LastlegOrder::getContainerNo).filter(Objects::nonNull).findFirst().orElse(null);
|
||||
int totalOrders = orderFacts.size();
|
||||
int inboundCodes = (int) orderFacts.stream().map(LastlegOrder::getInboundCode).map(ReceivingContainerService::trim)
|
||||
.filter(Objects::nonNull).distinct().count();
|
||||
int plannedOrders = (int) orderFacts.stream().filter(order -> {
|
||||
List<InboundPlanLine> values = linesByOrder.getOrDefault(order.getId(), List.of());
|
||||
return values.size() == 1 && plansById.containsKey(values.get(0).getPlanId());
|
||||
}).count();
|
||||
int planCount = matchedPlans.size();
|
||||
boolean allPlansReady = totalOrders > 0 && customers.size() == 1 && inboundCodes == totalOrders
|
||||
&& plannedOrders == totalOrders && planCount == totalOrders;
|
||||
String customerId = customers.size() == 1 ? customers.iterator().next() : null;
|
||||
boolean shippingReady = customerId != null && shippingGates.receivingAvailable(customerId, containerNo) == 1;
|
||||
boolean operable = allPlansReady && shippingReady;
|
||||
int pending = Math.max(0, totalOrders - plannedOrders);
|
||||
String blockReason = operable ? null : (!allPlansReady
|
||||
? plannedOrders + "个入仓码已生成计划," + pending + "个订单未操作/缺入仓码"
|
||||
: "柜号尚未达到海外收货可操作节点");
|
||||
LocalDateTime arrival = orderFacts.stream().map(LastlegOrder::getEstimatedWarehouseAt)
|
||||
.filter(Objects::nonNull).min(LocalDateTime::compareTo).orElse(null);
|
||||
String factHash = hash(orderFacts.stream().sorted(Comparator.comparing(LastlegOrder::getId)).map(order -> {
|
||||
List<InboundPlanLine> values = linesByOrder.getOrDefault(order.getId(), List.of());
|
||||
return order.getId() + ":" + trim(order.getInboundCode()) + ":" + values.stream()
|
||||
.map(InboundPlanLine::getPlanId).sorted().toList();
|
||||
}).toList());
|
||||
Map<Long, LastlegOrder> ordersById = orderFacts.stream().collect(Collectors.toMap(LastlegOrder::getId,
|
||||
value -> value, (left, right) -> left));
|
||||
List<InboundRef> matchedRefs = lineFacts.stream().filter(line -> orderIds.contains(line.getOrderId()))
|
||||
.map(line -> new InboundRef(plansById.get(line.getPlanId()), line, ordersById.get(line.getOrderId())))
|
||||
.filter(ref -> ref.plan() != null && ref.order() != null).toList();
|
||||
List<ReceivingContainerResponses.Inbound> inboundValues = matchedRefs.stream().map(this::inbound).toList();
|
||||
List<String> destinationCodes = inboundValues.stream().map(this::destinationCode)
|
||||
.filter(Objects::nonNull).distinct().sorted().toList();
|
||||
Totals totals = new Totals(inboundValues.size(), destinationCodes,
|
||||
inboundValues.stream().mapToInt(ReceivingContainerResponses.Inbound::plannedPalletCount).sum(),
|
||||
inboundValues.stream().mapToInt(ReceivingContainerResponses.Inbound::totalPackages).sum(),
|
||||
inboundValues.stream().mapToInt(ReceivingContainerResponses.Inbound::receivedPackages).sum());
|
||||
return new ContainerReadiness(sourceContainerBookingId, containerNo, totalOrders, inboundCodes,
|
||||
totalOrders - inboundCodes, planCount, plannedOrders, pending, allPlansReady, operable,
|
||||
blockReason, arrival, factHash, totals);
|
||||
}
|
||||
|
||||
private ReceivingContainerResponses.Candidate candidate(ContainerReadiness value,
|
||||
ReceivingContainerSession current) {
|
||||
boolean active = current != null && value.containerNo().equals(current.getContainerNo());
|
||||
Totals totals = value.totals();
|
||||
return new ReceivingContainerResponses.Candidate(value.sourceContainerBookingId(), value.containerNo(),
|
||||
active ? current.getStatus() : "WAIT_SIGN", value.totalOrderCount(), value.inboundCodeCount(),
|
||||
value.missingInboundCodeCount(), value.planCount(), value.plannedInboundCodeCount(),
|
||||
value.pendingPlanCount(), totals.destinationCodes(), totals.plannedPalletCount(), totals.total(),
|
||||
totals.received(), value.operable(), value.allPlansReady(), value.blockReason(),
|
||||
value.plannedArrivalAt(), active);
|
||||
}
|
||||
|
||||
private ReceivingContainerSession requireSigned(String operatorKey) {
|
||||
ReceivingContainerSession session = requireCurrent(operatorKey, true);
|
||||
if (!List.of("SIGNED", "RECEIVING").contains(session.getStatus())) {
|
||||
@@ -858,5 +965,10 @@ public class ReceivingContainerService {
|
||||
private record FbaSkuAllocation(InboundRef ref, String skuCode, String productName, int packages) { }
|
||||
private record Totals(int inboundCount, List<String> destinationCodes, int plannedPalletCount,
|
||||
int total, int received) { }
|
||||
private record ContainerReadiness(String sourceContainerBookingId, String containerNo,
|
||||
int totalOrderCount, int inboundCodeCount, int missingInboundCodeCount,
|
||||
int planCount, int plannedInboundCodeCount, int pendingPlanCount,
|
||||
boolean allPlansReady, boolean operable, String blockReason,
|
||||
LocalDateTime plannedArrivalAt, String factHash, Totals totals) { }
|
||||
private record PrepareCommand(String containerNo, String idempotencyKey) { }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package com.baiyue.lastleg.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.dto.PendingContainerSummary;
|
||||
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail;
|
||||
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
@@ -13,35 +14,69 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
String PENDING_CONTAINER_FILTER = " FROM ll_order o " +
|
||||
String CONTAINER_PROGRESS_FILTER = " FROM ll_order o LEFT JOIN ll_inbound_plan p " +
|
||||
"ON p.customer_id=o.customer_id AND p.source_order_id=o.id " +
|
||||
"WHERE o.source_container_booking_id IS NOT NULL AND o.source_container_booking_id<>'' " +
|
||||
"<if test='q.containerNo != null and q.containerNo != \"\"'>AND o.container_no LIKE CONCAT('%',#{q.containerNo},'%') </if>" +
|
||||
"<if test='q.createdFrom != null'>AND o.created_at >=#{q.createdFrom} </if>" +
|
||||
"<if test='q.createdTo != null'>AND o.created_at <=#{q.createdTo} </if>" +
|
||||
"GROUP BY o.source_container_booking_id " +
|
||||
"HAVING SUM(CASE WHEN o.current_status IN ('PENDING_ASSIGNMENT','DESTINATION_ASSIGNED') THEN 1 ELSE 0 END)>0 ";
|
||||
"GROUP BY o.source_container_booking_id ";
|
||||
|
||||
@Select("<script>SELECT o.source_container_booking_id AS sourceContainerBookingId," +
|
||||
"CASE WHEN COUNT(DISTINCT NULLIF(o.container_no,''))=1 THEN MAX(NULLIF(o.container_no,'')) ELSE NULL END AS containerNo," +
|
||||
"COUNT(*) AS orderCount,COUNT(DISTINCT o.inbound_code) AS inboundCodeCount," +
|
||||
"COUNT(DISTINCT o.customer_id) AS customerCount,COALESCE(SUM(o.package_count),0) AS totalPackageCount," +
|
||||
"COALESCE(SUM(o.pallet_count),0) AS totalPalletCount,COALESCE(SUM(o.volume_cbm),0) AS totalVolumeCbm," +
|
||||
"CASE WHEN SUM(CASE WHEN o.current_status IN ('PENDING_ASSIGNMENT','DESTINATION_ASSIGNED') THEN 0 ELSE 1 END)=0 " +
|
||||
String CONTAINER_PROGRESS_COLUMNS = "SELECT o.source_container_booking_id AS sourceContainerBookingId," +
|
||||
"CASE WHEN COUNT(NULLIF(o.container_no,''))=COUNT(*) AND COUNT(DISTINCT NULLIF(o.container_no,''))=1 " +
|
||||
"THEN MAX(NULLIF(o.container_no,'')) ELSE NULL END AS containerNo," +
|
||||
"COUNT(*) AS totalOrderCount,COUNT(DISTINCT NULLIF(TRIM(o.inbound_code),'')) AS inboundCodeCount," +
|
||||
"SUM(CASE WHEN NULLIF(TRIM(o.inbound_code),'') IS NULL THEN 1 ELSE 0 END) AS missingInboundCodeCount," +
|
||||
"COUNT(DISTINCT p.id) AS planCount," +
|
||||
"COUNT(DISTINCT CASE WHEN p.id IS NOT NULL AND NULLIF(TRIM(p.plan_no),'') IS NOT NULL " +
|
||||
"AND NULLIF(TRIM(p.status),'') IS NOT NULL THEN NULLIF(TRIM(o.inbound_code),'') END) AS plannedInboundCodeCount," +
|
||||
"SUM(CASE WHEN p.id IS NULL OR NULLIF(TRIM(p.plan_no),'') IS NULL OR NULLIF(TRIM(p.status),'') IS NULL " +
|
||||
"THEN 1 ELSE 0 END) AS pendingPlanCount," +
|
||||
"CASE WHEN COUNT(o.estimated_warehouse_at)=COUNT(*) " +
|
||||
"AND COUNT(DISTINCT o.estimated_warehouse_at)=1 THEN MAX(o.estimated_warehouse_at) ELSE NULL END AS plannedArrivalAt," +
|
||||
"CASE WHEN SUM(CASE WHEN NULLIF(TRIM(o.inbound_code),'') IS NULL THEN 1 ELSE 0 END)=0 " +
|
||||
"AND SUM(CASE WHEN p.id IS NULL OR NULLIF(TRIM(p.plan_no),'') IS NULL OR NULLIF(TRIM(p.status),'') IS NULL " +
|
||||
"THEN 1 ELSE 0 END)=0 AND COUNT(DISTINCT p.id)=COUNT(*) " +
|
||||
"AND COUNT(NULLIF(o.container_no,''))=COUNT(*) AND COUNT(DISTINCT NULLIF(o.container_no,''))=1 " +
|
||||
"THEN TRUE ELSE FALSE END AS eligible," +
|
||||
"THEN TRUE ELSE FALSE END AS allPlansReady," +
|
||||
"CASE WHEN COUNT(NULLIF(o.container_no,''))<>COUNT(*) " +
|
||||
"OR COUNT(DISTINCT NULLIF(o.container_no,''))<>1 THEN '柜号缺失或不一致' " +
|
||||
"WHEN SUM(CASE WHEN o.current_status IN ('PENDING_ASSIGNMENT','DESTINATION_ASSIGNED') THEN 0 ELSE 1 END)>0 " +
|
||||
"THEN '柜内存在已生成计划或非待分配订单' ELSE NULL END AS ineligibleReason" +
|
||||
PENDING_CONTAINER_FILTER +
|
||||
"ORDER BY MAX(o.created_at) DESC,o.source_container_booking_id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<PendingContainerSummary> pagePendingContainers(@Param("q") LastlegOrderRequests.PendingContainerQuery query,
|
||||
@Param("offset") long offset,
|
||||
@Param("limit") long limit);
|
||||
"WHEN SUM(CASE WHEN NULLIF(TRIM(o.inbound_code),'') IS NULL THEN 1 ELSE 0 END)>0 " +
|
||||
"THEN CONCAT('存在',SUM(CASE WHEN NULLIF(TRIM(o.inbound_code),'') IS NULL THEN 1 ELSE 0 END),'票缺少入仓码') " +
|
||||
"WHEN SUM(CASE WHEN p.id IS NULL OR NULLIF(TRIM(p.plan_no),'') IS NULL OR NULLIF(TRIM(p.status),'') IS NULL " +
|
||||
"THEN 1 ELSE 0 END)>0 THEN CONCAT('存在',SUM(CASE WHEN p.id IS NULL OR NULLIF(TRIM(p.plan_no),'') IS NULL " +
|
||||
"OR NULLIF(TRIM(p.status),'') IS NULL THEN 1 ELSE 0 END),'票尚未生成有效入库计划') " +
|
||||
"WHEN COUNT(DISTINCT p.id)<>COUNT(*) THEN '入库计划与尾程订单关联不一致' ELSE NULL END AS blockingReason ";
|
||||
|
||||
@Select("<script>SELECT progress.* FROM (" + CONTAINER_PROGRESS_COLUMNS + CONTAINER_PROGRESS_FILTER +
|
||||
") progress ORDER BY CASE WHEN progress.plannedArrivalAt IS NULL THEN 1 ELSE 0 END," +
|
||||
"progress.plannedArrivalAt ASC,progress.sourceContainerBookingId ASC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<ContainerPlanProgressSummary> pageContainerProgress(
|
||||
@Param("q") LastlegOrderRequests.ContainerProgressQuery query,
|
||||
@Param("offset") long offset,
|
||||
@Param("limit") long limit);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM (SELECT o.source_container_booking_id" +
|
||||
PENDING_CONTAINER_FILTER + ") pending_containers</script>")
|
||||
long countPendingContainers(@Param("q") LastlegOrderRequests.PendingContainerQuery query);
|
||||
CONTAINER_PROGRESS_FILTER + ") container_progress</script>")
|
||||
long countContainerProgress(@Param("q") LastlegOrderRequests.ContainerProgressQuery query);
|
||||
|
||||
@Select("<script>" + CONTAINER_PROGRESS_COLUMNS + " FROM ll_order o LEFT JOIN ll_inbound_plan p " +
|
||||
"ON p.customer_id=o.customer_id AND p.source_order_id=o.id " +
|
||||
"WHERE o.source_container_booking_id=#{sourceContainerBookingId} GROUP BY o.source_container_booking_id</script>")
|
||||
ContainerPlanProgressSummary findContainerProgress(
|
||||
@Param("sourceContainerBookingId") String sourceContainerBookingId);
|
||||
|
||||
@Select("SELECT o.id AS orderId,o.customer_id AS customerId,o.order_no AS orderNo,o.inbound_code AS inboundCode," +
|
||||
"o.current_status AS orderStatus,p.id AS planId,p.plan_no AS planNo,p.status AS planStatus," +
|
||||
"CASE WHEN p.id IS NOT NULL AND NULLIF(TRIM(p.plan_no),'') IS NOT NULL " +
|
||||
"AND NULLIF(TRIM(p.status),'') IS NOT NULL THEN TRUE ELSE FALSE END AS planValid," +
|
||||
"o.estimated_warehouse_at AS plannedArrivalAt FROM ll_order o LEFT JOIN ll_inbound_plan p " +
|
||||
"ON p.customer_id=o.customer_id AND p.source_order_id=o.id " +
|
||||
"WHERE o.source_container_booking_id=#{sourceContainerBookingId} " +
|
||||
"ORDER BY CASE WHEN o.estimated_warehouse_at IS NULL THEN 1 ELSE 0 END," +
|
||||
"o.estimated_warehouse_at ASC,o.customer_id ASC,o.id ASC")
|
||||
List<ContainerPlanProgressDetail.Item> findContainerProgressItems(
|
||||
@Param("sourceContainerBookingId") String sourceContainerBookingId);
|
||||
|
||||
@Select("SELECT * FROM ll_order WHERE customer_id=#{customerId} AND source_region=#{sourceRegion} " +
|
||||
"AND source_inbound_id=#{sourceInboundId} LIMIT 1")
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient;
|
||||
import feign.FeignException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/** 通过国内 Warehouse Service 权威主档校验尾程转仓 FBA 目的仓。 */
|
||||
@Component
|
||||
public class FbaWarehouseValidator {
|
||||
private static final String INVALID_MESSAGE = "所选FBA仓不存在或已停用";
|
||||
|
||||
@Resource private DomesticWarehouseDirectoryClient client;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
|
||||
public VerifiedFbaWarehouse required(String requestedId, String requestedCode, String requestedName) {
|
||||
long warehouseId = parseId(requestedId);
|
||||
var response = lookup(warehouseId);
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| response.data().warehouse() == null) {
|
||||
throw invalid();
|
||||
}
|
||||
DomesticWarehouseDirectoryClient.Warehouse warehouse = response.data().warehouse();
|
||||
if (!Objects.equals(warehouse.id(), warehouseId)
|
||||
|| !Objects.equals(warehouse.tenantId(), 0L)
|
||||
|| !"FBA".equals(warehouse.warehouseType())
|
||||
|| !"ACTIVE".equals(warehouse.status())
|
||||
|| !same(requestedCode, warehouse.warehouseCode())
|
||||
|| !same(requestedName, warehouse.warehouseName())) {
|
||||
throw invalid();
|
||||
}
|
||||
String region = firstRegion(response.data().addresses());
|
||||
return new VerifiedFbaWarehouse(warehouse.id(), warehouse.warehouseCode().trim(),
|
||||
warehouse.warehouseName().trim(), trimToNull(warehouse.countryCode()), region);
|
||||
}
|
||||
|
||||
private com.baiyue.common.core.api.ApiResponse<DomesticWarehouseDirectoryClient.Detail> lookup(long warehouseId) {
|
||||
try {
|
||||
return client.activeWarehouse(warehouseId, internalToken);
|
||||
} catch (FeignException exception) {
|
||||
if (exception.status() >= 400 && exception.status() < 500) throw invalid();
|
||||
throw unavailable(exception);
|
||||
} catch (ResponseStatusException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
throw unavailable(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private long parseId(String value) {
|
||||
try {
|
||||
long id = Long.parseLong(value == null ? "" : value.trim());
|
||||
if (id <= 0) throw invalid();
|
||||
return id;
|
||||
} catch (NumberFormatException exception) {
|
||||
throw invalid();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean same(String requested, String actual) {
|
||||
return StringUtils.hasText(requested) && StringUtils.hasText(actual)
|
||||
&& requested.trim().equals(actual.trim());
|
||||
}
|
||||
|
||||
private String firstRegion(List<DomesticWarehouseDirectoryClient.Address> addresses) {
|
||||
if (addresses == null) return null;
|
||||
return addresses.stream().filter(Objects::nonNull)
|
||||
.map(DomesticWarehouseDirectoryClient.Address::stateProvince)
|
||||
.map(FbaWarehouseValidator::trimToNull).filter(Objects::nonNull).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private ResponseStatusException invalid() {
|
||||
return new ResponseStatusException(HttpStatus.BAD_REQUEST, INVALID_MESSAGE);
|
||||
}
|
||||
|
||||
private ResponseStatusException unavailable(RuntimeException cause) {
|
||||
return new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"国内FBA仓主数据暂不可用", cause);
|
||||
}
|
||||
|
||||
private static String trimToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
||||
public record VerifiedFbaWarehouse(long id, String code, String name, String countryCode, String region) { }
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderDetailResponse;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderSummary;
|
||||
import com.baiyue.lastleg.dto.PendingContainerSummary;
|
||||
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail;
|
||||
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
@@ -39,6 +40,7 @@ public class LastlegOrderService {
|
||||
@Resource private LastlegOrderDetailMapper detailMapper;
|
||||
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
|
||||
@Resource private InboundPlanCartonMapper cartonMapper;
|
||||
@Resource private FbaWarehouseValidator fbaWarehouseValidator;
|
||||
|
||||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
||||
Page<LastlegOrder> page = orderMapper.selectPage(
|
||||
@@ -48,20 +50,33 @@ public class LastlegOrderService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<PendingContainerSummary> pendingContainerPage(
|
||||
LastlegOrderRequests.PendingContainerQuery query) {
|
||||
public PageResult<ContainerPlanProgressSummary> containerProgressPage(
|
||||
LastlegOrderRequests.ContainerProgressQuery query) {
|
||||
if (query.createdFrom() != null && query.createdTo() != null
|
||||
&& query.createdFrom().isAfter(query.createdTo())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"createdFrom must not be after createdTo");
|
||||
}
|
||||
long total = orderMapper.countPendingContainers(query);
|
||||
List<PendingContainerSummary> records = total == 0 ? List.of()
|
||||
: orderMapper.pagePendingContainers(query, (query.page() - 1) * query.size(), query.size());
|
||||
long total = orderMapper.countContainerProgress(query);
|
||||
List<ContainerPlanProgressSummary> records = total == 0 ? List.of()
|
||||
: orderMapper.pageContainerProgress(query, (query.page() - 1) * query.size(), query.size());
|
||||
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
|
||||
return new PageResult<>(records, query.page(), query.size(), total, pages);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ContainerPlanProgressDetail containerProgressDetail(String sourceContainerBookingId) {
|
||||
String bookingId = sourceContainerBookingId == null ? null : sourceContainerBookingId.trim();
|
||||
if (!StringUtils.hasText(bookingId)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订ID不能为空");
|
||||
}
|
||||
ContainerPlanProgressSummary summary = orderMapper.findContainerProgress(bookingId);
|
||||
if (summary == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
|
||||
}
|
||||
return new ContainerPlanProgressDetail(summary, orderMapper.findContainerProgressItems(bookingId));
|
||||
}
|
||||
|
||||
public LastlegOrderDetailResponse detail(String customerId, long orderId) {
|
||||
LastlegOrder order = findOwned(customerId, orderId);
|
||||
List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
@@ -83,6 +98,9 @@ public class LastlegOrderService {
|
||||
if (ids.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required");
|
||||
}
|
||||
FbaWarehouseValidator.VerifiedFbaWarehouse target = fbaWarehouseValidator.required(
|
||||
request.warehouseId(), request.warehouseCode(), request.warehouseName());
|
||||
String warehouseId = Long.toString(target.id());
|
||||
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
|
||||
.in(LastlegOrder::getId, ids).last("FOR UPDATE"));
|
||||
@@ -95,25 +113,25 @@ public class LastlegOrderService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (LastlegOrder order : orders) {
|
||||
String fromStatus = order.getCurrentStatus();
|
||||
order.setTransferWarehouseId(request.warehouseId());
|
||||
order.setTransferWarehouseCode(request.warehouseCode());
|
||||
order.setTransferWarehouseName(request.warehouseName());
|
||||
order.setTransferWarehouseId(warehouseId);
|
||||
order.setTransferWarehouseCode(target.code());
|
||||
order.setTransferWarehouseName(target.name());
|
||||
order.setCurrentStatus(DESTINATION_ASSIGNED);
|
||||
order.setUpdatedAt(now);
|
||||
orderMapper.updateById(order);
|
||||
|
||||
LastlegOrderDetail detailUpdate = new LastlegOrderDetail();
|
||||
detailUpdate.setDestinationWarehouseId(request.warehouseId());
|
||||
detailUpdate.setDestinationWarehouseCode(request.warehouseCode());
|
||||
detailUpdate.setDestinationWarehouseName(request.warehouseName());
|
||||
detailUpdate.setDestinationWarehouseId(warehouseId);
|
||||
detailUpdate.setDestinationWarehouseCode(target.code());
|
||||
detailUpdate.setDestinationWarehouseName(target.name());
|
||||
detailMapper.update(detailUpdate, new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())
|
||||
.eq(LastlegOrderDetail::getOrderId, order.getId()));
|
||||
|
||||
InboundPlanCarton cartonUpdate = new InboundPlanCarton();
|
||||
cartonUpdate.setDestinationWarehouseId(request.warehouseId());
|
||||
cartonUpdate.setDestinationWarehouseCode(request.warehouseCode());
|
||||
cartonUpdate.setDestinationWarehouseName(request.warehouseName());
|
||||
cartonUpdate.setDestinationWarehouseId(warehouseId);
|
||||
cartonUpdate.setDestinationWarehouseCode(target.code());
|
||||
cartonUpdate.setDestinationWarehouseName(target.name());
|
||||
cartonMapper.update(cartonUpdate, new LambdaQueryWrapper<InboundPlanCarton>()
|
||||
.eq(InboundPlanCarton::getCustomerId, order.getCustomerId())
|
||||
.eq(InboundPlanCarton::getOrderId, order.getId()));
|
||||
@@ -124,7 +142,7 @@ public class LastlegOrderService {
|
||||
history.setFromStatus(fromStatus);
|
||||
history.setToStatus(DESTINATION_ASSIGNED);
|
||||
history.setRemark(StringUtils.hasText(request.remark())
|
||||
? request.remark() : "Destination warehouse assigned: " + request.warehouseCode());
|
||||
? request.remark() : "Destination warehouse assigned: " + target.code());
|
||||
history.setCreatedAt(now);
|
||||
historyMapper.insert(history);
|
||||
}
|
||||
|
||||
+10
@@ -190,6 +190,7 @@ public class LoadingCompletedConsumer {
|
||||
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
|
||||
order.setPackageCount(source.packageCount()); order.setPalletCount(dropshipPallets.size());
|
||||
order.setProductQuantity(source.productQuantity()); order.setVolumeCbm(source.volumeCbm());
|
||||
order.setEstimatedWarehouseAt(parsePlannedArrival(payload.estimatedWarehouseArrivalAt()));
|
||||
order.setVolumeWeightKg(source.items().stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO,BigDecimal::add));
|
||||
order.setCurrentStatus("PENDING_ASSIGNMENT"); order.setSourceEventId(event.eventId());
|
||||
@@ -237,6 +238,15 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDateTime parsePlannedArrival(String value) {
|
||||
if (value == null || value.isBlank()) return null;
|
||||
try {
|
||||
return LocalDateTime.parse(value.trim());
|
||||
} catch (java.time.format.DateTimeParseException exception) {
|
||||
throw new IllegalArgumentException("事件计划到仓时间 estimatedWarehouseArrivalAt 无效", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void createInboundOrderSnapshot(EventEnvelope event, FirstLegLoadingPlanCompletedV1 payload,
|
||||
FirstLegLoadingPlanCompletedV1.Order source, LastlegOrder order,
|
||||
String orderHash) {
|
||||
|
||||
Reference in New Issue
Block a user