From bbf8f2f7923f6afd6b06eaa8594b7d6f8b07fa97 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Wed, 21 Oct 2026 21:49:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=BE=E7=A8=8B=E8=AE=A2=E5=8D=95=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=9F=A5=E8=AF=A2=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/OrderPreDepartureClient.java | 24 ++++++++ .../lastleg/service/LastlegOrderService.java | 60 +++++++++++++++++++ .../InternalPreDepartureOrderController.java | 56 +++++++++++++++++ .../order/dto/PreDepartureOrderDtos.java | 17 ++++++ .../service/UnifiedOrderQueryService.java | 8 +-- 5 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/client/OrderPreDepartureClient.java create mode 100644 order/src/main/java/com/baiyue/order/controller/InternalPreDepartureOrderController.java create mode 100644 order/src/main/java/com/baiyue/order/dto/PreDepartureOrderDtos.java diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/client/OrderPreDepartureClient.java b/lastleg-service/src/main/java/com/baiyue/lastleg/client/OrderPreDepartureClient.java new file mode 100644 index 000000000..dfa4320ac --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/client/OrderPreDepartureClient.java @@ -0,0 +1,24 @@ +package com.baiyue.lastleg.client; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.PageResult; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@FeignClient(name="order-service", contextId="lastlegOrderPreDepartureClient", path="/internal/order/v1/predeparture-orders") +public interface OrderPreDepartureClient { + @GetMapping + ApiResponse> page(@RequestHeader("X-Internal-Service-Token") String token, + @RequestParam(required=false) Long customerId, @RequestParam(required=false) String orderNo, + @RequestParam(required=false) String inboundCode, @RequestParam int page, @RequestParam int size); + record Row(Long orderId, Long customerId, String orderNo, String inboundCode, String mainStatus, + String customerName, String lxCode, String receivingWarehouseName, String destinationWarehouseName, + String destinationCode, Integer packageCount, BigDecimal volumeCbm, BigDecimal grossWeightKg, + BigDecimal netWeightKg, LocalDateTime expectedArrivalAt, String containerNo, String cabinetNo, + LocalDateTime estimatedSailingAt, LocalDateTime actualSailingAt, LocalDateTime createdAt, LocalDateTime updatedAt) {} +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java index 6e556f464..7b9bdd6b0 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java @@ -19,6 +19,7 @@ import com.baiyue.lastleg.entity.LastlegOrderOperationLog; import com.baiyue.lastleg.entity.LastlegOrderManualNode; import com.baiyue.lastleg.entity.ReceivingWarehouseOperation; import com.baiyue.lastleg.entity.ContainerArrivalTimeOperation; +import com.baiyue.lastleg.client.OrderPreDepartureClient; import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway; import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; @@ -42,6 +43,7 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.server.ResponseStatusException; import java.nio.charset.StandardCharsets; @@ -87,12 +89,23 @@ public class LastlegOrderService { @Resource private ContainerShippingGateMapper containerShippingGates; @Resource private ReturnOrderReferenceResolver returnOrderReferences; @Resource private com.baiyue.lastleg.transfer.service.TransferQuantityService transferQuantities; + @Resource private OrderPreDepartureClient orderPreDepartureClient; + @Value("${baiyue.security.internal-service-token:}") private String internalServiceToken; public PageResult page(String customerId, LastlegOrderRequests.Query query) { return page(customerId, query, null); } public PageResult page(String customerId, LastlegOrderRequests.Query query, String relabelNo) { + if (query.sailingStage() == LastlegOrderRequests.SailingStage.BEFORE_DEPARTURE) { + Long customer = parseLong(customerId); + var response = orderPreDepartureClient.page(internalServiceToken, customer, query.orderNo(), query.inboundCode(), + query.page().intValue(), query.size().intValue()); + if (response == null || response.data() == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "订单服务暂时不可用"); + List rows = response.data().records().stream().map(this::preDepartureSummary).toList(); + return new PageResult<>(rows, response.data().page(), response.data().size(), response.data().total(), response.data().pages()); + } + if (query.sailingStage() == null && relabelNo == null) return aggregateAllPage(customerId, query); Page page = orderMapper.selectPage( new Page<>(query.page(), query.size()), filter(customerId, query, relabelNo).orderByDesc(LastlegOrder::getCreatedAt).orderByDesc(LastlegOrder::getId)); Map> mergeRelations = palletMergeRelations(page.getRecords()); @@ -112,6 +125,53 @@ public class LastlegOrderService { page.getCurrent(), page.getSize(), page.getTotal(), page.getPages()); } + /** 全部页签合并订单服务的未开船头程数据与本服务已有尾程单,尾程记录优先覆盖同一来源订单。 */ + private PageResult aggregateAllPage(String customerId, LastlegOrderRequests.Query query) { + var source = orderPreDepartureClient.page(internalServiceToken, parseLong(customerId), query.orderNo(), query.inboundCode(), 1, 2000); + if (source == null || source.data() == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "订单服务暂时不可用"); + List existing = orderMapper.selectList(filter(customerId, query, null) + .orderByDesc(LastlegOrder::getCreatedAt).orderByDesc(LastlegOrder::getId)); + Map existingNodes = containerNodes(existing); + Map existingWeights = orderWeights(existing); + Map merged = new LinkedHashMap<>(); + source.data().records().forEach(row -> { + var summary = preDepartureSummary(row); + merged.put(orderKey(summary.customerId(), summary.sourceOrderNo()), summary); + }); + for (LastlegOrder order : existing) { + String key = orderKey(order.getCustomerId(), StringUtils.hasText(order.getSourceOrderNo()) ? order.getSourceOrderNo() : order.getOrderNo()); + merged.put(key, summary(order, List.of(), StringUtils.hasText(order.getSourceContainerBookingId()) + ? existingNodes.get(order.getSourceContainerBookingId()) : null, "UNSCHEDULED", + existingWeights.get(order.getId()), List.of(), null, null)); + } + List rows = merged.values().stream().sorted(java.util.Comparator.comparing(LastlegOrderSummary::createdAt, + java.util.Comparator.nullsLast(java.util.Comparator.reverseOrder()))).toList(); + int from = Math.min((query.page().intValue() - 1) * query.size().intValue(), rows.size()); + int to = Math.min(from + query.size().intValue(), rows.size()); + long total = rows.size(); + return new PageResult<>(rows.subList(from, to), query.page(), query.size(), total, + total == 0 ? 0 : (total + query.size() - 1) / query.size()); + } + + private static String orderKey(String customerId, String orderNo) { + return String.valueOf(customerId) + ":" + String.valueOf(orderNo); + } + + /** 将订单服务权威头程摘要投影为只读尾程列表行;尚未生成尾程单的字段保持为空。 */ + private LastlegOrderSummary preDepartureSummary(OrderPreDepartureClient.Row row) { + LastlegOrder order = new LastlegOrder(); + // 头程聚合行尚未生成尾程单,尾程订单号必须保持为空;来源头程订单号单独保留。 + order.setId(row.orderId()); order.setOrderNo(null); order.setOrderType("FIRST_LEG"); + order.setInboundCode(row.inboundCode()); order.setLxCode(row.lxCode()); order.setSourceOrderNo(row.orderNo()); + order.setCustomerId(row.customerId() == null ? null : String.valueOf(row.customerId())); order.setCustomerName(row.customerName()); + order.setSourceWarehouseName(row.receivingWarehouseName()); order.setOverseasWarehouseName(row.receivingWarehouseName()); + order.setFbaWarehouseCode(row.destinationCode()); + order.setContainerNo(row.containerNo()); order.setPackageCount(row.packageCount()); order.setVolumeCbm(row.volumeCbm()); + order.setEstimatedArrivalAt(row.expectedArrivalAt()); order.setCurrentStatus(row.mainStatus()); + order.setCreatedAt(row.createdAt()); order.setUpdatedAt(row.updatedAt()); order.setVersion(0); + return summary(order, List.of(), null, "UNSCHEDULED", null, List.of(), null, null); + } + @Transactional(readOnly = true) public PageResult containerProgressPage( LastlegOrderRequests.ContainerProgressQuery query) { diff --git a/order/src/main/java/com/baiyue/order/controller/InternalPreDepartureOrderController.java b/order/src/main/java/com/baiyue/order/controller/InternalPreDepartureOrderController.java new file mode 100644 index 000000000..6507b1cf8 --- /dev/null +++ b/order/src/main/java/com/baiyue/order/controller/InternalPreDepartureOrderController.java @@ -0,0 +1,56 @@ +package com.baiyue.order.controller; + +import com.baiyue.common.core.api.ApiResponse; +import com.baiyue.common.core.api.PageResult; +import com.baiyue.order.dto.PreDepartureOrderDtos; +import com.baiyue.order.dto.UnifiedOrderQuery; +import com.baiyue.order.dto.UnifiedOrderResponse; +import com.baiyue.order.service.UnifiedOrderQueryService; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +/** Service-only aggregate query. It intentionally has no platform-user permission. */ +@RestController +@RequestMapping("/internal/order/v1/predeparture-orders") +public class InternalPreDepartureOrderController { + @Resource private UnifiedOrderQueryService service; + @Value("${baiyue.security.internal-service-token}") private String expectedToken; + + @GetMapping + public ApiResponse> page( + @RequestHeader(value="X-Internal-Service-Token", required=false) String token, + @RequestParam(required=false) Long customerId, @RequestParam(required=false) String orderNo, + @RequestParam(required=false) String inboundCode, @RequestParam(defaultValue="1") int page, + @RequestParam(defaultValue="200") int size) { + if (expectedToken == null || expectedToken.isBlank() || token == null || !java.security.MessageDigest.isEqual(expectedToken.getBytes(StandardCharsets.UTF_8), token.getBytes(StandardCharsets.UTF_8))) throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.FORBIDDEN); + if (page < 1 || size < 1 || size > 2000) throw new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST); + Map all = new LinkedHashMap<>(); + // 由统一订单服务一次返回全部履约状态,再按实际开船事实筛选。 + for (long sourcePage = 1; ; sourcePage++) { + UnifiedOrderQuery q = new UnifiedOrderQuery(null, orderNo, "PRE_DEPARTURE", customerId, null, null, + inboundCode, null, null, null, null, null, null, null, null, null, null, null, + null, null, sourcePage, 200, null); + var result = service.platformPage(customerId, q); + result.records().forEach(row -> { if (row.transport() == null || row.transport().actualSailingAt() == null) all.putIfAbsent(row.orderId(), row); }); + if (sourcePage >= result.pages()) break; + } + List rows = all.values().stream().sorted(Comparator.comparing(UnifiedOrderResponse.Row::createdAt, Comparator.nullsLast(Comparator.reverseOrder()))) + .map(this::row).toList(); + int from = Math.min((page - 1) * size, rows.size()); + int to = Math.min(from + size, rows.size()); + long total = rows.size(), pages = total == 0 ? 0 : (total + size - 1) / size; + return ApiResponse.success(new PageResult<>(rows.subList(from, to), page, size, total, pages)); + } + private PreDepartureOrderDtos.Row row(UnifiedOrderResponse.Row r) { + var t = r.transport(); + return new PreDepartureOrderDtos.Row(r.orderId(), r.customerId(), r.orderNo(), r.inboundCode(), r.mainStatus(), + r.customerName(), r.lxCode(), r.receivingWarehouseName(), r.destinationWarehouseName(), r.destinationCode(), + r.totalPackageCount(), r.actualVolumeCbm() == null ? r.expectedVolumeCbm() : r.actualVolumeCbm(), + r.actualGrossWeightKg(), r.totalNetWeightKg(), r.expectedArrivalAt(), t == null ? null : t.containerNo(), + t == null ? null : t.cabinetNo(), t == null ? null : t.estimatedSailingAt(), t == null ? null : t.actualSailingAt(), r.createdAt(), r.updatedAt()); + } +} diff --git a/order/src/main/java/com/baiyue/order/dto/PreDepartureOrderDtos.java b/order/src/main/java/com/baiyue/order/dto/PreDepartureOrderDtos.java new file mode 100644 index 000000000..789f50ac0 --- /dev/null +++ b/order/src/main/java/com/baiyue/order/dto/PreDepartureOrderDtos.java @@ -0,0 +1,17 @@ +package com.baiyue.order.dto; + +import java.time.LocalDateTime; + +/** Internal read model used by lastleg to show orders before an actual sailing fact exists. */ +public final class PreDepartureOrderDtos { + private PreDepartureOrderDtos() {} + public record Query(Long customerId, String orderNo, String inboundCode, int page, int size) {} + public record Row(Long orderId, Long customerId, String orderNo, String inboundCode, + String mainStatus, String customerName, String lxCode, + String receivingWarehouseName, String destinationWarehouseName, + String destinationCode, Integer packageCount, java.math.BigDecimal volumeCbm, + java.math.BigDecimal grossWeightKg, java.math.BigDecimal netWeightKg, + LocalDateTime expectedArrivalAt, String containerNo, String cabinetNo, + LocalDateTime estimatedSailingAt, LocalDateTime actualSailingAt, + LocalDateTime createdAt, LocalDateTime updatedAt) {} +} diff --git a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java index f3e317a93..94efaa141 100644 --- a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java +++ b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java @@ -148,7 +148,7 @@ public class UnifiedOrderQueryService { /** 状态投影和当前页摘要共用原筛选,候选订单用于服务内批量收敛范围。 */ private FirstlegUnifiedOrderClient.Query statusRequest(Long tenantId, UnifiedOrderQuery query, List candidates) { - String status = Set.of("PENDING_REVIEW", "COMPLETED").contains( + String status = Set.of("PENDING_REVIEW", "COMPLETED", "PRE_DEPARTURE").contains( query.mainStatus() == null ? "" : query.mainStatus()) ? null : query.mainStatus(); return new FirstlegUnifiedOrderClient.Query(tenantId, query.keyword(), query.orderNo(), status, query.customerId(), query.receivingWarehouseId(), query.destinationWarehouseId(), query.inboundCode(), @@ -261,7 +261,7 @@ public class UnifiedOrderQueryService { private PageResult pageFromFirstleg(Long tenantId, UnifiedOrderQuery query, boolean includeCoLoadGroups) { List matching = matchingStatusRows(tenantId, query).stream() - .filter(row -> !StringUtils.hasText(query.mainStatus()) || query.mainStatus().equals(row.mainStatus())) + .filter(row -> !StringUtils.hasText(query.mainStatus()) || "PRE_DEPARTURE".equals(query.mainStatus()) || query.mainStatus().equals(row.mainStatus())) .collect(Collectors.collectingAndThen(Collectors.toMap(FirstlegUnifiedOrderClient.StatusRow::orderId, Function.identity(), (left, right) -> left, java.util.LinkedHashMap::new), values -> List.copyOf(values.values()))); @@ -682,7 +682,7 @@ public class UnifiedOrderQueryService { if (query.taxMode() != null && !Set.of("TAX_REFUND", "BUY_DECLARATION").contains(query.taxMode())) { throw new BizException(ResultCode.BAD_REQUEST, "taxMode仅支持TAX_REFUND或BUY_DECLARATION"); } - if (StringUtils.hasText(query.mainStatus()) + if (StringUtils.hasText(query.mainStatus()) && !"PRE_DEPARTURE".equals(query.mainStatus()) && !MAIN_STATUSES.contains(query.mainStatus()) && !WORKFLOW_STATUSES.contains(query.mainStatus()) ) { @@ -708,7 +708,7 @@ public class UnifiedOrderQueryService { } private boolean requiresFirstlegQuery(UnifiedOrderQuery query) { - return (StringUtils.hasText(query.mainStatus()) + return ("PRE_DEPARTURE".equals(query.mainStatus()) || StringUtils.hasText(query.mainStatus()) && !Set.of("PENDING_REVIEW", "COMPLETED").contains(query.mainStatus())) || query.expectedArrivalDate() != null || StringUtils.hasText(query.feeReviewStatus()) || StringUtils.hasText(query.containerNo())