|
@@ -0,0 +1,443 @@
|
|
|
|
|
+package com.sckw.transport.service.dashboard;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
|
|
+import com.sckw.core.model.constant.Global;
|
|
|
|
|
+import com.sckw.core.model.constant.NumberConstant;
|
|
|
|
|
+import com.sckw.core.model.enums.CarWaybillV1Enum;
|
|
|
|
|
+import com.sckw.core.model.enums.GatekeeperStatusEnum;
|
|
|
|
|
+import com.sckw.core.utils.CollectionUtils;
|
|
|
|
|
+import com.sckw.transport.api.model.vo.CurrentCapacityAnalysisVo;
|
|
|
|
|
+import com.sckw.transport.model.KwtGatekeeperWaybillOrder;
|
|
|
|
|
+import com.sckw.transport.model.KwtWaybillOrder;
|
|
|
|
|
+import com.sckw.transport.model.KwtWaybillOrderAddress;
|
|
|
|
|
+import com.sckw.transport.model.KwtWaybillOrderSubtask;
|
|
|
|
|
+import com.sckw.transport.repository.KwtGatekeeperWaybillOrderRepository;
|
|
|
|
|
+import com.sckw.transport.repository.KwtWaybillOrderAddressRepository;
|
|
|
|
|
+import com.sckw.transport.repository.KwtWaybillOrderRepository;
|
|
|
|
|
+import com.sckw.transport.repository.KwtWaybillOrderSubtaskRepository;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.time.Instant;
|
|
|
|
|
+import java.time.temporal.ChronoUnit;
|
|
|
|
|
+import java.util.AbstractMap;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Comparator;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+import java.util.Objects;
|
|
|
|
|
+import java.util.Optional;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
+import java.util.stream.Collectors;
|
|
|
|
|
+import java.util.stream.Stream;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 当前运力分析服务:以 {@code kwt_waybill_order_subtask} 为主表口径统计各运单车辆状态
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 主要统计三类状态:
|
|
|
|
|
+ * 1. 待入场 (Pending Entry)
|
|
|
|
|
+ * 2. 装卸作业中 (Loading Operation)
|
|
|
|
|
+ * 3. 配送中/运输中 (Delivering)
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class SubtaskCapacityAnalysisService {
|
|
|
|
|
+
|
|
|
|
|
+ private final KwtWaybillOrderSubtaskRepository waybillOrderSubtaskRepository;
|
|
|
|
|
+ private final KwtWaybillOrderAddressRepository waybillOrderAddressRepository;
|
|
|
|
|
+ private final KwtGatekeeperWaybillOrderRepository gatekeeperWaybillOrderRepository;
|
|
|
|
|
+ private final KwtWaybillOrderRepository waybillOrderRepository;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 执行当前运力分析
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 核心流程:
|
|
|
|
|
+ * 1. 查询有效子任务并聚合出每个运单的最大状态及关键时间点。
|
|
|
|
|
+ * 2. 加载辅助数据(卸货地址、门岗离场时间、主表更新时间)。
|
|
|
|
|
+ * 3. 根据业务规则将运单分类为:待入场、装卸中、配送中。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return 当前运力分析结果 VO
|
|
|
|
|
+ */
|
|
|
|
|
+ public CurrentCapacityAnalysisVo analyze() {
|
|
|
|
|
+ log.info("=== 开始执行当前运力分析 ===");
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 查询所有有效的子任务(排除已删除和已取消的)
|
|
|
|
|
+ // 仅查询必要字段以减少内存占用:运单ID、状态、更新时间
|
|
|
|
|
+ log.debug("步骤1: 查询有效子任务...");
|
|
|
|
|
+ List<KwtWaybillOrderSubtask> subtasks = waybillOrderSubtaskRepository.list(
|
|
|
|
|
+ Wrappers.<KwtWaybillOrderSubtask>lambdaQuery()
|
|
|
|
|
+ .select(KwtWaybillOrderSubtask::getWOrderId, KwtWaybillOrderSubtask::getStatus, KwtWaybillOrderSubtask::getUpdateTime)
|
|
|
|
|
+ .eq(KwtWaybillOrderSubtask::getDelFlag, Global.NO)
|
|
|
|
|
+ .ne(KwtWaybillOrderSubtask::getStatus, CarWaybillV1Enum.CANCELLED.getCode()));
|
|
|
|
|
+
|
|
|
|
|
+ CurrentCapacityAnalysisVo vo = new CurrentCapacityAnalysisVo();
|
|
|
|
|
+ if (CollectionUtils.isEmpty(subtasks)) {
|
|
|
|
|
+ log.warn("未查询到有效子任务,返回空结果");
|
|
|
|
|
+ vo.setPendingEntryCount(0);
|
|
|
|
|
+ vo.setLoadingOperationCount(0);
|
|
|
|
|
+ vo.setDeliveringCount(0);
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("步骤1完成: 查询到 {} 条有效子任务", subtasks.size());
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 按运单ID聚合子任务信息
|
|
|
|
|
+ // 对于每个运单,提取最大状态值以及“出场后、卸货前”窗口期的最后更新时间
|
|
|
|
|
+ log.debug("步骤2: 按运单ID聚合子任务信息...");
|
|
|
|
|
+ Map<Long, WaybillSubtaskAgg> aggByWaybill = subtasks.stream()
|
|
|
|
|
+ .filter(st -> st.getWOrderId() != null && st.getStatus() != null)
|
|
|
|
|
+ .collect(Collectors.groupingBy(
|
|
|
|
|
+ KwtWaybillOrderSubtask::getWOrderId,
|
|
|
|
|
+ Collectors.reducing(new WaybillSubtaskAgg(), SubtaskCapacityAnalysisService::partialAgg, SubtaskCapacityAnalysisService::mergeAgg)));
|
|
|
|
|
+
|
|
|
|
|
+ Set<Long> wOrderIds = aggByWaybill.keySet();
|
|
|
|
|
+ log.info("步骤2完成: 聚合得到 {} 个唯一运单ID", wOrderIds.size());
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 加载辅助数据
|
|
|
|
|
+ log.debug("步骤3: 加载辅助数据...");
|
|
|
|
|
+
|
|
|
|
|
+ // 3.1 加载包含卸货地址的运单ID集合
|
|
|
|
|
+ Set<Long> hasUnloadWaybillIds = loadUnloadWaybillIds(wOrderIds);
|
|
|
|
|
+ log.debug("步骤3.1: 加载到 {} 个包含卸货地址的运单", hasUnloadWaybillIds.size());
|
|
|
|
|
+
|
|
|
|
|
+ // 3.2 加载门岗离场时间
|
|
|
|
|
+ Map<Long, Date> leaveTimeByWaybill = loadGatekeeperLeaveTime(wOrderIds);
|
|
|
|
|
+ log.debug("步骤3.2: 加载到 {} 个运单的门岗离场时间", leaveTimeByWaybill.size());
|
|
|
|
|
+
|
|
|
|
|
+ // 3.3 加载运单主表的更新时间
|
|
|
|
|
+ Map<Long, Date> waybillUpdate = loadWaybillUpdateTime(wOrderIds);
|
|
|
|
|
+ log.debug("步骤3.3: 加载到 {} 个运单的主表更新时间", waybillUpdate.size());
|
|
|
|
|
+
|
|
|
|
|
+ // 计算两小时前的时间点,用于判断无卸货地址运单是否处于活跃运输状态
|
|
|
|
|
+ Date twoHoursAgo = Date.from(Instant.now().minus(2, ChronoUnit.HOURS));
|
|
|
|
|
+ log.debug("步骤3.4: 计算活跃运输判定时间点: {}", twoHoursAgo);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 分类统计各运单状态
|
|
|
|
|
+ log.debug("步骤4: 开始分类统计各运单状态...");
|
|
|
|
|
+ CapacityCounts counts = aggByWaybill.entrySet().stream()
|
|
|
|
|
+ .map(e -> classify(e.getKey(), e.getValue(), hasUnloadWaybillIds, leaveTimeByWaybill, waybillUpdate, twoHoursAgo))
|
|
|
|
|
+ .reduce(CapacityCounts.ZERO, CapacityCounts::add);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("=== 运力分析完成 === - 待入场: {}, 装卸作业中: {}, 配送中: {}",
|
|
|
|
|
+ counts.pending(), counts.loading(), counts.delivering());
|
|
|
|
|
+
|
|
|
|
|
+ vo.setPendingEntryCount(counts.pending());
|
|
|
|
|
+ vo.setLoadingOperationCount(counts.loading());
|
|
|
|
|
+ vo.setDeliveringCount(counts.delivering());
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 部分聚合:将单个子任务转换为聚合对象
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 逻辑:
|
|
|
|
|
+ * 1. 记录当前子任务的状态。
|
|
|
|
|
+ * 2. 如果状态处于“出场后、卸货前”窗口期,记录其更新时间,用于后续判断活跃性。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param st 子任务对象
|
|
|
|
|
+ * @return 聚合对象
|
|
|
|
|
+ */
|
|
|
|
|
+ private static WaybillSubtaskAgg partialAgg(KwtWaybillOrderSubtask st) {
|
|
|
|
|
+ WaybillSubtaskAgg a = new WaybillSubtaskAgg();
|
|
|
|
|
+ a.maxStatus = st.getStatus();
|
|
|
|
|
+ // 如果状态处于“出场后、卸货前”窗口期,记录其更新时间
|
|
|
|
|
+ if (inPostExitPreUnloadWindow(st.getStatus()) && st.getUpdateTime() != null) {
|
|
|
|
|
+ a.lastUpdateWhenPostExit = st.getUpdateTime();
|
|
|
|
|
+ }
|
|
|
|
|
+ return a;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 合并聚合:合并两个聚合对象
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 逻辑:
|
|
|
|
|
+ * 1. 取两个对象中较大的状态值作为最终状态。
|
|
|
|
|
+ * 2. 取“出场后、卸货前”窗口期的最晚更新时间。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param a 聚合对象A
|
|
|
|
|
+ * @param b 聚合对象B
|
|
|
|
|
+ * @return 合并后的聚合对象
|
|
|
|
|
+ */
|
|
|
|
|
+ private static WaybillSubtaskAgg mergeAgg(WaybillSubtaskAgg a, WaybillSubtaskAgg b) {
|
|
|
|
|
+ WaybillSubtaskAgg r = new WaybillSubtaskAgg();
|
|
|
|
|
+ // 取最大状态值,代表该运单达到的最高进度
|
|
|
|
|
+ r.maxStatus = Math.max(a.maxStatus, b.maxStatus);
|
|
|
|
|
+ // 取“出场后、卸货前”窗口期的最晚更新时间
|
|
|
|
|
+ r.lastUpdateWhenPostExit = Stream.of(a.lastUpdateWhenPostExit, b.lastUpdateWhenPostExit)
|
|
|
|
|
+ .filter(Objects::nonNull)
|
|
|
|
|
+ .max(Date::compareTo)
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+ return r;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 判断状态是否处于“出场后、卸货前”窗口期
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 定义范围:WAIT_LOADING <= status < COMPLETION_LOADING
|
|
|
|
|
+ * 此区间通常对应车辆已出场但在途或等待卸货的状态。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param status 状态码
|
|
|
|
|
+ * @return 是否在窗口期内
|
|
|
|
|
+ */
|
|
|
|
|
+ private static boolean inPostExitPreUnloadWindow(int status) {
|
|
|
|
|
+ return status >= CarWaybillV1Enum.WAIT_LOADING.getCode()
|
|
|
|
|
+ && status < CarWaybillV1Enum.COMPLETION_LOADING.getCode();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 分类单个运单的状态
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 优先级:
|
|
|
|
|
+ * 1. 无效/驳回状态 -> 忽略
|
|
|
|
|
+ * 2. 配送中 (Delivering)
|
|
|
|
|
+ * 3. 待入场 (Pending Entry)
|
|
|
|
|
+ * 4. 装卸作业中 (Loading Operation)
|
|
|
|
|
+ * 5. 其他 -> 忽略
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param wid 运单ID
|
|
|
|
|
+ * @param agg 运单子任务聚合信息
|
|
|
|
|
+ * @param hasUnloadWaybillIds 包含卸货地址的运单ID集合
|
|
|
|
|
+ * @param leaveTimeByWaybill 门岗离场时间映射
|
|
|
|
|
+ * @param waybillUpdate 运单主表更新时间映射
|
|
|
|
|
+ * @param twoHoursAgo 两小时前的时间点
|
|
|
|
|
+ * @return 分类计数结果
|
|
|
|
|
+ */
|
|
|
|
|
+ private static CapacityCounts classify(
|
|
|
|
|
+ Long wid,
|
|
|
|
|
+ WaybillSubtaskAgg agg,
|
|
|
|
|
+ Set<Long> hasUnloadWaybillIds,
|
|
|
|
|
+ Map<Long, Date> leaveTimeByWaybill,
|
|
|
|
|
+ Map<Long, Date> waybillUpdate,
|
|
|
|
|
+ Date twoHoursAgo) {
|
|
|
|
|
+
|
|
|
|
|
+ int ms = agg.maxStatus;
|
|
|
|
|
+
|
|
|
|
|
+ // 无效状态或审核驳回,不计入任何类别
|
|
|
|
|
+ if (ms <= 0 || Objects.equals(ms, CarWaybillV1Enum.REVIEW_REJECTION.getCode())) {
|
|
|
|
|
+ log.trace("运单ID: {} 状态无效或已驳回 (status={}), 忽略", wid, ms);
|
|
|
|
|
+ return CapacityCounts.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ boolean hasUnload = hasUnloadWaybillIds.contains(wid);
|
|
|
|
|
+
|
|
|
|
|
+ // 优先判断是否为“配送中”
|
|
|
|
|
+ if (isDelivering(ms, hasUnload, leaveTimeByWaybill.get(wid), agg.lastUpdateWhenPostExit, waybillUpdate.get(wid), twoHoursAgo)) {
|
|
|
|
|
+ log.trace("运单ID: {} 分类为: 配送中", wid);
|
|
|
|
|
+ return CapacityCounts.deliveringOne();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 判断是否为“待入场”
|
|
|
|
|
+ if (ms == CarWaybillV1Enum.PENDING_VEHICLE.getCode()) {
|
|
|
|
|
+ log.trace("运单ID: {} 分类为: 待入场", wid);
|
|
|
|
|
+ return CapacityCounts.pendingOne();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 判断是否为“装卸作业中”
|
|
|
|
|
+ // 包括:拒绝通行、出场完成、称重通行
|
|
|
|
|
+ if (ms == CarWaybillV1Enum.REFUSE_TRAFFIC.getCode()
|
|
|
|
|
+ || ms == CarWaybillV1Enum.EXIT_COMPLETED.getCode()
|
|
|
|
|
+ || ms == CarWaybillV1Enum.WEIGHT_TRAFFIC.getCode()) {
|
|
|
|
|
+ log.trace("运单ID: {} 分类为: 装卸作业中 (status={})", wid, ms);
|
|
|
|
|
+ return CapacityCounts.loadingOne();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 其他状态不计入
|
|
|
|
|
+ log.trace("运单ID: {} 状态未匹配任何分类 (status={}), 忽略", wid, ms);
|
|
|
|
|
+ return CapacityCounts.ZERO;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 判断运单是否处于“配送中”状态
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 逻辑:
|
|
|
|
|
+ * 1. 排除审核驳回和已取消状态
|
|
|
|
|
+ * 2. 如果有卸货地址:状态在 [WAIT_LOADING, COMPLETION_LOADING) 之间视为配送中
|
|
|
|
|
+ * 3. 如果没有卸货地址:
|
|
|
|
|
+ * - 状态必须在 [WAIT_LOADING, COMPLETION_LOADING) 之间
|
|
|
|
|
+ * - 且参考时间(门岗离场时间 > 子任务更新时间 > 运单更新时间)必须在两小时内
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param ms 最大状态码
|
|
|
|
|
+ * @param hasUnload 是否有卸货地址
|
|
|
|
|
+ * @param gateLeave 门岗离场时间
|
|
|
|
|
+ * @param lastPostExitSubtaskUpd 子任务在出场后卸货前的最后更新时间
|
|
|
|
|
+ * @param waybillUpdate 运单主表更新时间
|
|
|
|
|
+ * @param twoHoursAgo 两小时前的时间点
|
|
|
|
|
+ * @return 是否配送中
|
|
|
|
|
+ */
|
|
|
|
|
+ private static boolean isDelivering(int ms, boolean hasUnload, Date gateLeave, Date lastPostExitSubtaskUpd,
|
|
|
|
|
+ Date waybillUpdate, Date twoHoursAgo) {
|
|
|
|
|
+ // 排除终止状态
|
|
|
|
|
+ if (Objects.equals(ms, CarWaybillV1Enum.REVIEW_REJECTION.getCode())
|
|
|
|
|
+ || Objects.equals(ms, CarWaybillV1Enum.CANCELLED.getCode())) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (hasUnload) {
|
|
|
|
|
+ // 有卸货地址:只要状态在等待装货到完成装货之间,即视为在途/配送
|
|
|
|
|
+ // 因为存在明确的卸货点,状态在此区间即代表正在执行运输任务
|
|
|
|
|
+ boolean isDelivering = ms >= CarWaybillV1Enum.WAIT_LOADING.getCode()
|
|
|
|
|
+ && ms < CarWaybillV1Enum.COMPLETION_LOADING.getCode();
|
|
|
|
|
+ if (isDelivering) {
|
|
|
|
|
+ log.trace("运单有卸货地址且状态在配送区间, 判定为配送中");
|
|
|
|
|
+ }
|
|
|
|
|
+ return isDelivering;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 无卸货地址:必须满足状态区间且时间在两小时内
|
|
|
|
|
+ // 防止历史数据或长时间停滞的数据被误判为配送中
|
|
|
|
|
+ if (ms < CarWaybillV1Enum.WAIT_LOADING.getCode()
|
|
|
|
|
+ || ms >= CarWaybillV1Enum.COMPLETION_LOADING.getCode()) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确定参考时间:优先使用门岗离场时间,其次子任务更新时间,最后运单更新时间
|
|
|
|
|
+ Date ref = Optional.ofNullable(gateLeave)
|
|
|
|
|
+ .or(() -> Optional.ofNullable(lastPostExitSubtaskUpd))
|
|
|
|
|
+ .or(() -> Optional.ofNullable(waybillUpdate))
|
|
|
|
|
+ .orElse(null);
|
|
|
|
|
+
|
|
|
|
|
+ // 参考时间存在且未超过两小时
|
|
|
|
|
+ boolean isActive = ref != null && !ref.before(twoHoursAgo);
|
|
|
|
|
+ if (isActive) {
|
|
|
|
|
+ log.trace("运单无卸货地址,但状态在配送区间且最近活跃 (ref={}), 判定为配送中", ref);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.trace("运单无卸货地址,虽状态在配送区间但不活跃 (ref={}), 不判定为配送中", ref);
|
|
|
|
|
+ }
|
|
|
|
|
+ return isActive;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载包含卸货地址的运单ID集合
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param wOrderIds 运单ID集合
|
|
|
|
|
+ * @return 包含卸货地址的运单ID集合
|
|
|
|
|
+ */
|
|
|
|
|
+ private Set<Long> loadUnloadWaybillIds(Set<Long> wOrderIds) {
|
|
|
|
|
+ if (CollectionUtils.isEmpty(wOrderIds)) {
|
|
|
|
|
+ return Set.of();
|
|
|
|
|
+ }
|
|
|
|
|
+ log.debug("正在查询 {} 个运单的地址信息以筛选卸货地址...", wOrderIds.size());
|
|
|
|
|
+ // addressType = 2 表示卸货地址
|
|
|
|
|
+ Set<Long> result = waybillOrderAddressRepository.queryBywOrderIds(new ArrayList<>(wOrderIds)).stream()
|
|
|
|
|
+ .filter(a -> Objects.equals(NumberConstant.TWO, a.getAddressType()))
|
|
|
|
|
+ .map(KwtWaybillOrderAddress::getWOrderId)
|
|
|
|
|
+ .filter(Objects::nonNull)
|
|
|
|
|
+ .collect(Collectors.toSet());
|
|
|
|
|
+ log.debug("筛选出 {} 个包含卸货地址的运单", result.size());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载门岗离场时间
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param wOrderIds 运单ID集合
|
|
|
|
|
+ * @return 运单ID -> 离场时间的映射
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<Long, Date> loadGatekeeperLeaveTime(Set<Long> wOrderIds) {
|
|
|
|
|
+ if (CollectionUtils.isEmpty(wOrderIds)) {
|
|
|
|
|
+ return Map.of();
|
|
|
|
|
+ }
|
|
|
|
|
+ log.debug("正在查询 {} 个运单的门岗记录...", wOrderIds.size());
|
|
|
|
|
+ List<KwtGatekeeperWaybillOrder> rows = gatekeeperWaybillOrderRepository.queryGatekeeperWaybillOrderByWOrderIds(new ArrayList<>(wOrderIds));
|
|
|
|
|
+ if (CollectionUtils.isEmpty(rows)) {
|
|
|
|
|
+ log.debug("未查询到任何门岗记录");
|
|
|
|
|
+ return Map.of();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 按运单ID分组,并提取每个运单最新的已离场记录的时间
|
|
|
|
|
+ Map<Long, Date> result = rows.stream()
|
|
|
|
|
+ .collect(Collectors.groupingBy(KwtGatekeeperWaybillOrder::getWOrderId))
|
|
|
|
|
+ .entrySet().stream()
|
|
|
|
|
+ .map(SubtaskCapacityAnalysisService::latestExitedLeaveEntry)
|
|
|
|
|
+ .flatMap(Optional::stream)
|
|
|
|
|
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
|
|
|
|
+
|
|
|
|
|
+ log.debug("解析出 {} 个运单的有效离场时间", result.size());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取单个运单最新的已离场记录的离场时间
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param e 运单ID及其门岗记录列表
|
|
|
|
|
+ * @return 运单ID -> 离场时间的可选映射
|
|
|
|
|
+ */
|
|
|
|
|
+ private static Optional<Map.Entry<Long, Date>> latestExitedLeaveEntry(Map.Entry<Long, List<KwtGatekeeperWaybillOrder>> e) {
|
|
|
|
|
+ Long wid = e.getKey();
|
|
|
|
|
+ return e.getValue().stream()
|
|
|
|
|
+ // 取ID最大的记录(通常代表最新)
|
|
|
|
|
+ .max(Comparator.comparing(KwtGatekeeperWaybillOrder::getId, Comparator.nullsLast(Long::compareTo)))
|
|
|
|
|
+ // 过滤出状态为“已出场”或“空车出场”的记录
|
|
|
|
|
+ .filter(gk -> GatekeeperStatusEnum.EXITED.getCode().equals(gk.getStatus())
|
|
|
|
|
+ || GatekeeperStatusEnum.EMPTY_EXITED.getCode().equals(gk.getStatus()))
|
|
|
|
|
+ // 取离场时间,若为空则取更新时间
|
|
|
|
|
+ .map(gk -> Optional.ofNullable(gk.getLeaveTime()).orElse(gk.getUpdateTime()))
|
|
|
|
|
+ .filter(Objects::nonNull)
|
|
|
|
|
+ .map(leave -> new AbstractMap.SimpleEntry<>(wid, leave));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载运单主表的更新时间
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param wOrderIds 运单ID集合
|
|
|
|
|
+ * @return 运单ID -> 更新时间的映射
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<Long, Date> loadWaybillUpdateTime(Set<Long> wOrderIds) {
|
|
|
|
|
+ if (CollectionUtils.isEmpty(wOrderIds)) {
|
|
|
|
|
+ return Map.of();
|
|
|
|
|
+ }
|
|
|
|
|
+ log.debug("正在查询 {} 个运单的主表更新时间...", wOrderIds.size());
|
|
|
|
|
+ Map<Long, Date> result = waybillOrderRepository.listByIds(wOrderIds).stream()
|
|
|
|
|
+ .filter(wo -> wo.getId() != null && wo.getUpdateTime() != null)
|
|
|
|
|
+ .collect(Collectors.toMap(KwtWaybillOrder::getId, KwtWaybillOrder::getUpdateTime));
|
|
|
|
|
+ log.debug("获取到 {} 个运单的主表更新时间", result.size());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 容量计数记录
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 用于累加统计三种状态的运单数量
|
|
|
|
|
+ */
|
|
|
|
|
+ private record CapacityCounts(int pending, int loading, int delivering) {
|
|
|
|
|
+ static final CapacityCounts ZERO = new CapacityCounts(0, 0, 0);
|
|
|
|
|
+
|
|
|
|
|
+ static CapacityCounts pendingOne() {
|
|
|
|
|
+ return new CapacityCounts(1, 0, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static CapacityCounts loadingOne() {
|
|
|
|
|
+ return new CapacityCounts(0, 1, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ static CapacityCounts deliveringOne() {
|
|
|
|
|
+ return new CapacityCounts(0, 0, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CapacityCounts add(CapacityCounts o) {
|
|
|
|
|
+ return new CapacityCounts(pending + o.pending, loading + o.loading, delivering + o.delivering);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 运单子任务聚合信息
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 用于在 Stream 归约过程中暂存每个运单的关键状态信息
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final class WaybillSubtaskAgg {
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 最大状态码
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 代表该运单下所有子任务中达到的最高业务状态
|
|
|
|
|
+ */
|
|
|
|
|
+ private int maxStatus;
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 在“出场后、卸货前”窗口期的最后更新时间
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 用于辅助判断无卸货地址运单的活跃性
|
|
|
|
|
+ */
|
|
|
|
|
+ private Date lastUpdateWhenPostExit;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|