feat(ops): 重构派单队列评分逻辑,支持楼层差与等待老化综合排序
- 新增 QueueScoreCalculator/QueueScoreContext/QueueScoreResult,统一按优先级分 + 楼层差分 - 等待老化分计算队列总分,并将 PRIORITY_WEIGHT 调整为 1500 - OrderQueueService 新增 rebuildWaitingTasksByUserId 接口,OrderQueueServiceEnhanced 支持按执行人重算 WAITING 队列、以当前执行工单楼层为基准动态重排,并在事务提交后同步刷新 Redis - RedisOrderQueueServiceImpl 支持持久化 baseFloorNo、targetFloorNo、floorDiff、waitMinutes、scoreUpdateTime 等评分明细,清队列时同时清理关联 Hash,避免脏数据残留 - DispatchEngineImpl、CleanerPriorityScheduleStrategy、BadgeDeviceScheduleStrategy 调整为非抢占式派单:P0 忙碌时仅入队等待,空闲时直接派发,自动派单前按总分重排并派发下一单 - CleanOrderServiceImpl 取消 P0 自动打断链路,升级到 P0 后仅重算等待队列并发送通知;补充 QueueScoreCalculatorTest、OrderQueueServiceEnhancedTest、CleanerPriorityScheduleStrategyTest、CleanOrderEndToEndTest 覆盖新行为
This commit is contained in:
@@ -245,13 +245,13 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
if (queueDTO != null) {
|
||||
orderQueueService.adjustPriority(queueDTO.getId(), PriorityEnum.P0, reason);
|
||||
|
||||
// 5. 使用新的调度引擎处理P0紧急插队
|
||||
DispatchResult result = dispatchEngine.urgentInterrupt(orderId, queueDTO.getUserId());
|
||||
// 5. 重算等待队列,P0 不再打断当前任务
|
||||
orderQueueService.rebuildWaitingTasksByUserId(queueDTO.getUserId(), order.getAreaId());
|
||||
|
||||
// 6. 发送优先级升级通知
|
||||
cleanOrderEventListener.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode(), orderId);
|
||||
|
||||
return result.isSuccess();
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -289,11 +289,11 @@ public class CleanOrderServiceImpl implements CleanOrderService {
|
||||
if (queueDTO != null) {
|
||||
orderQueueService.adjustPriority(queueDTO.getId(), newPriority, reason);
|
||||
|
||||
// 6. 如果升级到 P0,触发紧急打断逻辑
|
||||
// 6. 如果升级到 P0,仅重算等待队列,不再触发打断
|
||||
if (newPriority == PriorityEnum.P0) {
|
||||
DispatchResult result = dispatchEngine.urgentInterrupt(orderId, queueDTO.getUserId());
|
||||
orderQueueService.rebuildWaitingTasksByUserId(queueDTO.getUserId(), order.getAreaId());
|
||||
cleanOrderEventListener.sendPriorityUpgradeNotification(queueDTO.getUserId(), order.getOrderCode(), orderId);
|
||||
log.warn("客流升级到P0,触发紧急打断: orderId={}, success={}", orderId, result.isSuccess());
|
||||
log.warn("客流升级到P0,已重算等待队列: orderId={}", orderId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,13 +23,13 @@ import java.util.List;
|
||||
* <p>
|
||||
* 职责:怎么派单
|
||||
* <p>
|
||||
* 策略规则:
|
||||
* <ul>
|
||||
* <li>空闲无任务 → DIRECT_DISPATCH(直接派单)</li>
|
||||
* <li>空闲有等待 → PUSH_AND_ENQUEUE(推送等待+新任务入队)</li>
|
||||
* <li>忙碌且非P0 → ENQUEUE_ONLY(仅入队)</li>
|
||||
* <li>忙碌且P0 → INTERRUPT_AND_DISPATCH(打断并派单)</li>
|
||||
* </ul>
|
||||
* 策略规则:
|
||||
* <ul>
|
||||
* <li>空闲无任务 → DIRECT_DISPATCH(直接派单)</li>
|
||||
* <li>空闲有等待 → PUSH_AND_ENQUEUE(推送等待+新任务入队)</li>
|
||||
* <li>忙碌且非P0 → ENQUEUE_ONLY(仅入队)</li>
|
||||
* <li>忙碌且P0 → ENQUEUE_ONLY(不抢断,进入等待队列)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@@ -106,12 +106,12 @@ public class BadgeDeviceScheduleStrategy implements ScheduleStrategy {
|
||||
// P0紧急任务,需要打断
|
||||
if (assigneeStatus.getCurrentTaskCount() > 0) {
|
||||
Long currentOrderId = deviceStatus.getCurrentOpsOrderId();
|
||||
log.warn("决策: INTERRUPT_AND_DISPATCH - P0紧急任务打断当前任务: currentOrderId={}", currentOrderId);
|
||||
return DispatchDecision.interruptAndDispatch(currentOrderId);
|
||||
} else {
|
||||
log.info("决策: DIRECT_DISPATCH - P0紧急任务直接派单");
|
||||
return DispatchDecision.directDispatch();
|
||||
}
|
||||
log.info("决策: ENQUEUE_ONLY - P0工单不再打断当前任务,进入等待队列: currentOrderId={}", currentOrderId);
|
||||
return DispatchDecision.enqueueOnly();
|
||||
} else {
|
||||
log.info("决策: DIRECT_DISPATCH - P0工单在空闲状态下直接派发");
|
||||
return DispatchDecision.directDispatch();
|
||||
}
|
||||
} else {
|
||||
// 非紧急任务,设备忙碌,入队等待
|
||||
log.info("决策: ENQUEUE_ONLY - 设备忙碌,任务入队等待");
|
||||
@@ -133,15 +133,14 @@ public class BadgeDeviceScheduleStrategy implements ScheduleStrategy {
|
||||
|
||||
// P0任务可以打断任何任务
|
||||
if (urgentContext.isUrgent()) {
|
||||
log.warn("允许打断: P0紧急任务可以打断当前任务");
|
||||
return InterruptDecision.allowByDefault();
|
||||
}
|
||||
|
||||
// P1/P2任务不能打断
|
||||
log.info("拒绝打断: 非P0任务不能打断当前任务");
|
||||
return InterruptDecision.deny(
|
||||
"紧急任务优先级不足",
|
||||
"建议等待当前任务完成"
|
||||
);
|
||||
log.info("拒绝打断: 当前调度已改为非抢占式队列派发");
|
||||
return InterruptDecision.deny("当前调度不再支持抢断", "工单将按队列总分在下一轮派发");
|
||||
}
|
||||
|
||||
log.info("拒绝打断: 当前调度已改为非抢占式队列派发");
|
||||
return InterruptDecision.deny(
|
||||
"当前调度不再支持抢断",
|
||||
"工单将按队列总分在下一轮派发"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.util.List;
|
||||
* <li>空闲无任务 → DIRECT_DISPATCH(直接派单)</li>
|
||||
* <li>空闲有等待 → PUSH_AND_ENQUEUE(推送等待+新任务入队)</li>
|
||||
* <li>忙碌且非P0 → ENQUEUE_ONLY(仅入队)</li>
|
||||
* <li>忙碌且P0 → INTERRUPT_AND_DISPATCH(打断并派单)</li>
|
||||
* <li>忙碌且P0 → ENQUEUE_ONLY(不抢断,进入等待队列)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author lzh
|
||||
@@ -106,10 +106,10 @@ public class CleanerPriorityScheduleStrategy implements ScheduleStrategy {
|
||||
// P0紧急任务,需要打断
|
||||
if (assigneeStatus.getCurrentTaskCount() > 0) {
|
||||
Long currentOrderId = cleanerStatus.getCurrentOpsOrderId();
|
||||
log.warn("决策: INTERRUPT_AND_DISPATCH - P0紧急任务打断当前任务: currentOrderId={}", currentOrderId);
|
||||
return DispatchDecision.interruptAndDispatch(currentOrderId);
|
||||
log.info("决策: ENQUEUE_ONLY - P0工单不再打断当前任务,进入等待队列: currentOrderId={}", currentOrderId);
|
||||
return DispatchDecision.enqueueOnly();
|
||||
} else {
|
||||
log.info("决策: DIRECT_DISPATCH - P0紧急任务直接派单");
|
||||
log.info("决策: DIRECT_DISPATCH - P0工单在空闲状态下直接派发");
|
||||
return DispatchDecision.directDispatch();
|
||||
}
|
||||
} else {
|
||||
@@ -133,15 +133,14 @@ public class CleanerPriorityScheduleStrategy implements ScheduleStrategy {
|
||||
|
||||
// P0任务可以打断任何任务
|
||||
if (urgentContext.isUrgent()) {
|
||||
log.warn("允许打断: P0紧急任务可以打断当前任务");
|
||||
return InterruptDecision.allowByDefault();
|
||||
log.info("拒绝打断: 当前调度已改为非抢占式队列派发");
|
||||
return InterruptDecision.deny("当前调度不再支持抢断", "工单将按队列总分在下一轮派发");
|
||||
}
|
||||
|
||||
// P1/P2任务不能打断
|
||||
log.info("拒绝打断: 非P0任务不能打断当前任务");
|
||||
log.info("拒绝打断: 当前调度已改为非抢占式队列派发");
|
||||
return InterruptDecision.deny(
|
||||
"紧急任务优先级不足",
|
||||
"建议等待当前任务完成"
|
||||
"当前调度不再支持抢断",
|
||||
"工单将按队列总分在下一轮派发"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@ import com.viewsh.module.ops.core.event.OrderEventPublisher;
|
||||
import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager;
|
||||
import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
|
||||
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
|
||||
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
|
||||
import com.viewsh.module.ops.enums.PriorityEnum;
|
||||
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
|
||||
import com.viewsh.module.ops.environment.dal.dataobject.workorder.OpsOrderCleanExtDO;
|
||||
import com.viewsh.module.ops.environment.dal.redis.TrafficActiveOrderRedisDAO;
|
||||
import com.viewsh.module.ops.environment.dal.mysql.workorder.OpsOrderCleanExtMapper;
|
||||
import com.viewsh.module.ops.environment.integration.consumer.*;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
@@ -73,6 +76,8 @@ public class CleanOrderEndToEndTest {
|
||||
@Mock
|
||||
private OpsOrderMapper opsOrderMapper;
|
||||
@Mock
|
||||
private OpsBusAreaMapper opsBusAreaMapper;
|
||||
@Mock
|
||||
private OpsOrderCleanExtMapper cleanExtMapper;
|
||||
@Mock
|
||||
private OrderIdGenerator orderIdGenerator;
|
||||
@@ -94,6 +99,8 @@ public class CleanOrderEndToEndTest {
|
||||
private ValueOperations<String, String> valueOperations;
|
||||
@Mock
|
||||
private VoiceBroadcastService voiceBroadcastService;
|
||||
@Mock
|
||||
private TrafficActiveOrderRedisDAO trafficActiveOrderRedisDAO;
|
||||
|
||||
@Mock
|
||||
private BadgeDeviceStatusService badgeDeviceStatusService;
|
||||
@@ -149,6 +156,7 @@ public class CleanOrderEndToEndTest {
|
||||
|
||||
// 注入 CleanOrderEventListener
|
||||
injectField(cleanOrderService, "cleanOrderEventListener", cleanOrderEventListener);
|
||||
injectField(cleanOrderService, "opsBusAreaMapper", opsBusAreaMapper);
|
||||
|
||||
// 注入 CleanOrderAuditEventHandler 依赖
|
||||
injectField(auditEventHandler, "eventLogRecorder", eventLogRecorder);
|
||||
@@ -156,10 +164,18 @@ public class CleanOrderEndToEndTest {
|
||||
injectField(auditEventHandler, "opsOrderMapper", opsOrderMapper);
|
||||
injectField(auditEventHandler, "stringRedisTemplate", stringRedisTemplate);
|
||||
injectField(auditEventHandler, "objectMapper", objectMapper);
|
||||
injectField(createEventHandler, "trafficActiveOrderRedisDAO", trafficActiveOrderRedisDAO);
|
||||
|
||||
// Stub IotDeviceControlApi for resetTrafficCounter
|
||||
lenient().when(iotDeviceControlApi.resetTrafficCounter(any()))
|
||||
.thenReturn(CommonResult.success(true));
|
||||
lenient().when(opsBusAreaMapper.selectById(anyLong()))
|
||||
.thenAnswer(i -> OpsBusAreaDO.builder()
|
||||
.id(i.getArgument(0))
|
||||
.areaName("测试区域")
|
||||
.parentPath(null)
|
||||
.floorNo(1)
|
||||
.build());
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@@ -338,7 +354,7 @@ public class CleanOrderEndToEndTest {
|
||||
verify(eventLogRecorder).record(any());
|
||||
|
||||
// 2. TTS sent (orderId can be null for TTS_REQUEST events)
|
||||
verify(voiceBroadcastService).broadcastInOrder(eq(5001L), contains("请回到作业区域"), eq((Long) null));
|
||||
verify(voiceBroadcastService).broadcastDirect(eq(5001L), contains("请回到作业区域"), eq(9), eq((Long) null));
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
@@ -378,9 +394,6 @@ public class CleanOrderEndToEndTest {
|
||||
|
||||
when(opsOrderMapper.selectById(orderId)).thenReturn(order);
|
||||
when(orderQueueService.getByOpsOrderId(orderId)).thenReturn(queueDTO);
|
||||
when(dispatchEngine.urgentInterrupt(eq(orderId), eq(2001L)))
|
||||
.thenReturn(DispatchResult.success("Success", 2001L));
|
||||
|
||||
// Execute
|
||||
boolean result = cleanOrderService.upgradePriorityToP0(orderId, "Manual Upgrade");
|
||||
|
||||
@@ -392,7 +405,7 @@ public class CleanOrderEndToEndTest {
|
||||
assertEquals(PriorityEnum.P0.getPriority(), orderCaptor.getValue().getPriority());
|
||||
|
||||
verify(orderQueueService).adjustPriority(eq(500L), eq(PriorityEnum.P0), anyString());
|
||||
verify(dispatchEngine).urgentInterrupt(orderId, 2001L);
|
||||
verify(orderQueueService).rebuildWaitingTasksByUserId(2001L, order.getAreaId());
|
||||
verify(cleanOrderEventListener).sendPriorityUpgradeNotification(eq(2001L), eq("WO-P2"), eq(orderId));
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class CleanerPriorityScheduleStrategyTest {
|
||||
private com.viewsh.module.ops.core.dispatch.DispatchEngine dispatchEngine;
|
||||
|
||||
@Test
|
||||
void testDecide_P0_Interrupt() {
|
||||
void testDecide_P0_EnqueueOnlyWhenBusy() {
|
||||
// Setup
|
||||
OpsCleanerStatusDO c1 = new OpsCleanerStatusDO();
|
||||
c1.setUserId(1L);
|
||||
@@ -54,8 +54,7 @@ class CleanerPriorityScheduleStrategyTest {
|
||||
DispatchDecision decision = strategy.decide(context);
|
||||
|
||||
// Verify
|
||||
assertEquals(DispatchPath.INTERRUPT_AND_DISPATCH, decision.getPath());
|
||||
assertEquals(500L, decision.getInterruptedOrderId());
|
||||
assertEquals(DispatchPath.ENQUEUE_ONLY, decision.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user