fix(ops): 队列楼层权重修复——强楼层优先 + 闭环基准兜底 + N+1 优化
问题:楼层差在分数公式中本该主导同优先级排序,但有四个缺陷导致效果不稳:
1. 有 base 无 target 时给 +600 罚分,无 base 时则全免罚——同一工单在
保洁员忙/闲时排序不单调(B)。
2. 基准楼层只在 user 有 PROCESSING 时生效,空闲时完全无楼层信号(A)。
3. enqueue 瞬间 score 不含楼层,要等下一轮 rebuild 才补上(H)。
4. aging 上限 720 > floorDiff 上限 600,等满 4 小时可反超同优先级 10 层差
任务,削弱"强楼层优先"语义(G)。
5. rebuild 内 for 循环对每条 WAITING 单独 selectById(order)+selectById(area),
N+1 问题(F)。
修复:
1. QueueScoreCalculator(B + G)
- FLOOR_WEIGHT 60 → 100:上限 1000 > aging 上限 720,4 小时老化不再反超
同优先级的近楼层任务。
- 删除"有 base 无 target +600"分支:任一侧缺失即 score=0,语义对称。
2. OpsOrderMapper.selectLatestCompletedAreaIdByAssignee(A 二级兜底)
查最近 24h 内已完成工单的 area,用来推断空闲保洁员的物理位置。
超过 24h 视为跨班次、轨迹失效。
3. OrderQueueServiceEnhanced.resolveBaselineAreaId(A 三级兜底)
PROCESSING.area → 最近 24h COMPLETED.area → 调用方传的 fallbackAreaId。
4. OrderQueueServiceEnhanced.enqueue(H)
事务提交后 triggerQueueRebuildAfterCommit(userId, null),新入队工单
立即按楼层差参与排序,不依赖下一次 autoDispatchNext 触发。
5. OrderQueueServiceEnhanced.rebuildWaitingTasksByUserId(F)
批量 selectBatchIds(orders) + selectBatchIds(areas),100 条 WAITING
从 200 次 SELECT 降到 2 次。
权重直观对比(P2=priority×1500=3000):
旧分数 新分数
同层刚入队 3000 3000
差5层刚入队 3000+300=3300 3000+500=3500
差5层等2小时 3000+300-360=2940 3000+500-360=3140
同层等4小时 3000+0-720=2280 3000+0-720=2280
新权重下"差5层等2小时"仍大于"同层刚入队",楼层稳定主导排序;
极端 aging(>4h)仍能让同层任务被近楼层任务压制优先执行。
测试:QueueScoreCalculatorTest(3)、OrderQueueServiceEnhancedTest(1,
已按 selectBatchIds + selectActiveListByUserId 更新 mock)、QueueSyncServiceTest
全绿。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,30 @@ public interface OpsOrderMapper extends BaseMapperX<OpsOrderDO> {
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询执行人最近一条已完成工单的区域(用于楼层基准兜底)
|
||||
* <p>
|
||||
* 用途:{@code OrderQueueServiceEnhanced.resolveBaselineAreaId} 的二级兜底。
|
||||
* 当执行人当前没有 PROCESSING 工单时(短暂空闲),用最近完成的那一单的
|
||||
* 区域作为"物理位置推断",保证楼层差评分在空闲期仍然生效。
|
||||
* <p>
|
||||
* 时间窗:通过 {@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<OpsOrderDO>()
|
||||
.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;
|
||||
}
|
||||
|
||||
// ==================== 统计聚合查询 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<Long> orderIds = waitingQueues.stream()
|
||||
.map(OpsOrderQueueDO::getOpsOrderId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
Map<Long, Long> 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<Long> areaIds = orderIdToAreaId.values().stream().distinct().collect(Collectors.toList());
|
||||
Map<Long, Integer> 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<OrderQueueDTO> 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 {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析楼层基准区域(三级兜底)
|
||||
* <ol>
|
||||
* <li>当前 PROCESSING 工单的区域——表示“正在做的楼层”</li>
|
||||
* <li>最近 24 小时内已完成工单的区域——投射保洁员最近的物理位置</li>
|
||||
* <li>调用方显式传入的 {@code fallbackAreaId}(如 autoDispatchNext 传的 completedOrder.areaId)</li>
|
||||
* </ol>
|
||||
* 都未命中则返回 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<OrderQueueDTO> rebuiltTasks = orderQueueService.rebuildWaitingTasksByUserId(userId, null);
|
||||
|
||||
Reference in New Issue
Block a user