diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/workorder/OpsOrderMapper.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/workorder/OpsOrderMapper.java index e19a7262..997f4fac 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/workorder/OpsOrderMapper.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/workorder/OpsOrderMapper.java @@ -142,6 +142,30 @@ public interface OpsOrderMapper extends BaseMapperX { .last("FOR UPDATE")); } + /** + * 查询执行人最近一条已完成工单的区域(用于楼层基准兜底) + *

+ * 用途:{@code OrderQueueServiceEnhanced.resolveBaselineAreaId} 的二级兜底。 + * 当执行人当前没有 PROCESSING 工单时(短暂空闲),用最近完成的那一单的 + * 区域作为"物理位置推断",保证楼层差评分在空闲期仍然生效。 + *

+ * 时间窗:通过 {@code since} 过滤,超过窗口仍空闲则认为轨迹失效, + * 返回 null 让调用方降级到更外层的兜底(fallbackAreaId 或无楼层模式)。 + * + * @param assigneeId 执行人ID + * @param since 只考虑 updateTime 晚于此时间的工单(如 now - 24h) + * @return 最近一条 COMPLETED 工单的 areaId;无匹配返回 null + */ + default Long selectLatestCompletedAreaIdByAssignee(Long assigneeId, LocalDateTime since) { + OpsOrderDO order = selectOne(new LambdaQueryWrapperX() + .eq(OpsOrderDO::getAssigneeId, assigneeId) + .eq(OpsOrderDO::getStatus, WorkOrderStatusEnum.COMPLETED.getStatus()) + .ge(since != null, OpsOrderDO::getUpdateTime, since) + .orderByDesc(OpsOrderDO::getUpdateTime) + .last("LIMIT 1")); + return order != null ? order.getAreaId() : null; + } + // ==================== 统计聚合查询 ==================== /** diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/queue/OrderQueueServiceEnhanced.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/queue/OrderQueueServiceEnhanced.java index ef723a69..2401de09 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/queue/OrderQueueServiceEnhanced.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/queue/OrderQueueServiceEnhanced.java @@ -115,6 +115,9 @@ public class OrderQueueServiceEnhanced implements OrderQueueService { // TODO: 触发紧急派单流程(在派单引擎中实现) } + // 5. 事务提交后按全局楼层重排一次:新入队工单立即按楼层差参与排序,不等下一次 rebuild + triggerQueueRebuildAfterCommit(userId, null); + return queueDO.getId(); } @@ -512,10 +515,31 @@ public class OrderQueueServiceEnhanced implements OrderQueueService { Integer baseFloorNo = resolveFloorNo(baselineAreaId); LocalDateTime now = LocalDateTime.now(); + // 批量装载 orders + areas,消除 N+1:100 条 WAITING 从 200 次 SELECT 降为 2 次。 + List orderIds = waitingQueues.stream() + .map(OpsOrderQueueDO::getOpsOrderId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + Map orderIdToAreaId = orderIds.isEmpty() + ? Collections.emptyMap() + : orderMapper.selectBatchIds(orderIds).stream() + .filter(o -> o.getAreaId() != null) + .collect(Collectors.toMap(OpsOrderDO::getId, OpsOrderDO::getAreaId, + (a, b) -> a)); + List areaIds = orderIdToAreaId.values().stream().distinct().collect(Collectors.toList()); + Map areaIdToFloorNo = areaIds.isEmpty() + ? Collections.emptyMap() + : areaMapper.selectBatchIds(areaIds).stream() + .filter(a -> a.getFloorNo() != null) + .collect(Collectors.toMap(OpsBusAreaDO::getId, OpsBusAreaDO::getFloorNo, + (a, b) -> a)); + List rebuiltTasks = new ArrayList<>(waitingQueues.size()); for (OpsOrderQueueDO queueDO : waitingQueues) { OrderQueueDTO dto = convertToDTO(queueDO); - Integer targetFloorNo = resolveFloorNo(resolveOrderAreaId(queueDO.getOpsOrderId())); + Long targetAreaId = orderIdToAreaId.get(queueDO.getOpsOrderId()); + Integer targetFloorNo = targetAreaId != null ? areaIdToFloorNo.get(targetAreaId) : null; QueueScoreResult result = queueScoreCalculator.calculate(QueueScoreContext.builder() .priority(queueDO.getPriority()) .baseFloorNo(baseFloorNo) @@ -740,7 +764,17 @@ public class OrderQueueServiceEnhanced implements OrderQueueService { ); } + /** + * 解析楼层基准区域(三级兜底) + *

    + *
  1. 当前 PROCESSING 工单的区域——表示“正在做的楼层”
  2. + *
  3. 最近 24 小时内已完成工单的区域——投射保洁员最近的物理位置
  4. + *
  5. 调用方显式传入的 {@code fallbackAreaId}(如 autoDispatchNext 传的 completedOrder.areaId)
  6. + *
+ * 都未命中则返回 null,本次排序降级为无楼层模式。 + */ private Long resolveBaselineAreaId(Long userId, Long fallbackAreaId) { + // 一级:当前正在执行的工单 OpsOrderQueueDO processingQueue = orderQueueMapper.selectCurrentExecutingByUserId(userId); if (processingQueue != null) { Long processingAreaId = resolveOrderAreaId(processingQueue.getOpsOrderId()); @@ -748,6 +782,13 @@ public class OrderQueueServiceEnhanced implements OrderQueueService { return processingAreaId; } } + // 二级:最近 24 小时内的已完成工单,推断保洁员当前物理位置 + Long recentAreaId = orderMapper.selectLatestCompletedAreaIdByAssignee( + userId, LocalDateTime.now().minusHours(24)); + if (recentAreaId != null) { + return recentAreaId; + } + // 三级:调用方提示的区域(可为 null) return fallbackAreaId; } diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/queue/QueueScoreCalculator.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/queue/QueueScoreCalculator.java index 5e22b1f3..bd161f51 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/queue/QueueScoreCalculator.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/queue/QueueScoreCalculator.java @@ -9,7 +9,11 @@ import java.time.LocalDateTime; public class QueueScoreCalculator { static final int PRIORITY_WEIGHT = 1500; - static final int FLOOR_WEIGHT = 60; + /** + * 楼层差权重。10 层封顶 × 100 = 1000,大于 aging 上限 720,实现"强楼层优先": + * 等满 4 小时(aging 上限)的任务也不会反超更近楼层的同优先级任务。 + */ + static final int FLOOR_WEIGHT = 100; static final int AGING_WEIGHT = 3; static final int MAX_FLOOR_DIFF = 10; static final int MAX_AGING_MINUTES = 240; @@ -22,11 +26,11 @@ public class QueueScoreCalculator { Integer targetFloorNo = context.getTargetFloorNo(); Integer floorDiff = null; int floorDiffScore = 0; + // 语义对称:只要 baseFloor 或 targetFloor 任一缺失,就视为"信息不足",不参与楼层排序(score=0)。 + // 旧逻辑会在"有 base 无 target"时打 +600 罚分,导致同一工单在保洁员忙碌/空闲时排序不单调。 if (baseFloorNo != null && targetFloorNo != null) { floorDiff = Math.abs(targetFloorNo - baseFloorNo); floorDiffScore = Math.min(floorDiff, MAX_FLOOR_DIFF) * FLOOR_WEIGHT; - } else if (baseFloorNo != null) { - floorDiffScore = MAX_FLOOR_DIFF * FLOOR_WEIGHT; } long waitMinutes = 0; diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/service/queue/OrderQueueServiceEnhancedTest.java b/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/service/queue/OrderQueueServiceEnhancedTest.java index 19f5885a..bdf2a12c 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/service/queue/OrderQueueServiceEnhancedTest.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/test/java/com/viewsh/module/ops/service/queue/OrderQueueServiceEnhancedTest.java @@ -77,13 +77,21 @@ class OrderQueueServiceEnhancedTest { when(orderQueueMapper.selectListByUserIdAndStatus(userId, OrderQueueStatusEnum.WAITING.getStatus())) .thenReturn(List.of(olderFarTask, newerNearTask)); when(orderQueueMapper.selectCurrentExecutingByUserId(userId)).thenReturn(currentTask); - when(orderQueueMapper.selectListByUserId(userId)).thenReturn(List.of(olderFarTask, newerNearTask, currentTask)); + // syncUserQueueToRedis 走 selectActiveListByUserId(Bug#6),只返回活跃态 + when(orderQueueMapper.selectActiveListByUserId(userId)) + .thenReturn(List.of(olderFarTask, newerNearTask, currentTask)); + // resolveOrderAreaId 仍单条 selectById(PROCESSING 工单的 area) when(orderMapper.selectById(900L)).thenReturn(OpsOrderDO.builder().id(900L).areaId(501L).build()); - when(orderMapper.selectById(101L)).thenReturn(OpsOrderDO.builder().id(101L).areaId(503L).build()); - when(orderMapper.selectById(102L)).thenReturn(OpsOrderDO.builder().id(102L).areaId(502L).build()); + // WAITING 工单批量加载 + when(orderMapper.selectBatchIds(org.mockito.ArgumentMatchers.anyCollection())) + .thenReturn(List.of( + OpsOrderDO.builder().id(101L).areaId(503L).build(), + OpsOrderDO.builder().id(102L).areaId(502L).build())); when(areaMapper.selectById(501L)).thenReturn(OpsBusAreaDO.builder().id(501L).floorNo(5).build()); - when(areaMapper.selectById(502L)).thenReturn(OpsBusAreaDO.builder().id(502L).floorNo(6).build()); - when(areaMapper.selectById(503L)).thenReturn(OpsBusAreaDO.builder().id(503L).floorNo(8).build()); + when(areaMapper.selectBatchIds(org.mockito.ArgumentMatchers.anyCollection())) + .thenReturn(List.of( + OpsBusAreaDO.builder().id(503L).floorNo(8).build(), + OpsBusAreaDO.builder().id(502L).floorNo(6).build())); when(orderQueueMapper.updateById(any(OpsOrderQueueDO.class))).thenReturn(1); List rebuiltTasks = orderQueueService.rebuildWaitingTasksByUserId(userId, null);