fix(ops): 修复队列脏数据导致工单永远排队无法派发

问题:autoCompleteOrder 直接调用 orderStateMachine.forceTransition,
绕过责任链导致队列记录残留 WAITING,autoDispatchNext 反复命中脏数据
失败,人员状态永远 BUSY,新工单全部 ENQUEUE_ONLY 形成死循环。

修复:
1. SecurityOrderServiceImpl 所有状态操作统一走 OrderLifecycleManager,
   移除对 OrderStateMachine 的直接依赖
2. autoDispatchNext 增加循环遍历 + 工单状态校验,跳过并清理非 QUEUED
   的脏队列记录,增加 maxSkip=50 防护上限
3. forceComplete 返回值校验,失败时抛异常而非静默继续

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-26 15:30:02 +08:00
parent 8406a80655
commit bd70f3bc8a
3 changed files with 129 additions and 104 deletions

View File

@@ -14,9 +14,6 @@ import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.OperatorTypeEnum;
import com.viewsh.module.ops.enums.OrderQueueStatusEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.infrastructure.log.annotation.BusinessLog;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -76,17 +73,6 @@ public class DispatchEngineImpl implements DispatchEngine {
// ==================== 核心调度方法 ====================
@Override
@BusinessLog(
type = LogType.ORDER_DISPATCHED,
scope = LogScope.ORDER,
description = "工单自动派发",
includeParams = true,
includeResult = true,
result = "#result.success",
params = {"#context.orderId", "#context.businessType", "#context.priority"},
targetId = "#context.orderId",
targetType = "order"
)
@Transactional(rollbackFor = Exception.class)
public DispatchResult dispatch(OrderDispatchContext context) {
log.info("开始调度工单: orderId={}, businessType={}, areaId={}, priority={}",
@@ -202,33 +188,60 @@ public class DispatchEngineImpl implements DispatchEngine {
if (waitingTasks.isEmpty()) {
log.info("无等待任务,执行人变为空闲: assigneeId={}", assigneeId);
// 发布事件,由业务层更新执行人状态
return DispatchResult.success("无等待工单,执行人保持空闲", assigneeId);
}
// 动态总分重排后,派发得分最低的等待工单
OrderQueueDTO nextTask = waitingTasks.get(0);
log.info("派发下一单: queueId={}, orderId={}, score={}, floorDiff={}, waitMinutes={}",
nextTask.getId(), nextTask.getOpsOrderId(), nextTask.getQueueScore(),
nextTask.getFloorDiff(), nextTask.getWaitMinutes());
// 遍历等待任务列表,跳过不可派发的脏数据
int maxSkip = 50;
int skipped = 0;
for (OrderQueueDTO nextTask : waitingTasks) {
// 防御性校验:检查工单实际状态是否仍为 QUEUED
OpsOrderDO order = orderMapper.selectById(nextTask.getOpsOrderId());
if (order == null || !WorkOrderStatusEnum.QUEUED.getStatus().equals(order.getStatus())) {
// 工单已不在 QUEUED 状态(可能被 forceTransition 直接完成),清理脏队列记录
log.warn("跳过不可派发的队列记录: queueId={}, orderId={}, orderStatus={}",
nextTask.getId(), nextTask.getOpsOrderId(),
order != null ? order.getStatus() : "NOT_FOUND");
try {
orderQueueService.updateStatus(nextTask.getId(), OrderQueueStatusEnum.REMOVED);
} catch (Exception e) {
log.error("清理脏队列记录失败: queueId={}", nextTask.getId(), e);
}
skipped++;
if (skipped >= maxSkip) {
log.error("跳过脏数据超过上限,终止: assigneeId={}, skipped={}", assigneeId, skipped);
break;
}
continue;
}
OrderTransitionRequest request = OrderTransitionRequest.builder()
.orderId(nextTask.getOpsOrderId())
.targetStatus(WorkOrderStatusEnum.DISPATCHED)
.queueId(nextTask.getId())
.assigneeId(assigneeId)
.operatorType(OperatorTypeEnum.SYSTEM)
.operatorId(assigneeId)
.reason("等待队列动态重排后自动派发")
.build();
log.info("派发下一单: queueId={}, orderId={}, score={}, floorDiff={}, waitMinutes={}",
nextTask.getId(), nextTask.getOpsOrderId(), nextTask.getQueueScore(),
nextTask.getFloorDiff(), nextTask.getWaitMinutes());
OrderTransitionResult result = orderLifecycleManager.transition(request);
OrderTransitionRequest request = OrderTransitionRequest.builder()
.orderId(nextTask.getOpsOrderId())
.targetStatus(WorkOrderStatusEnum.DISPATCHED)
.queueId(nextTask.getId())
.assigneeId(assigneeId)
.operatorType(OperatorTypeEnum.SYSTEM)
.operatorId(assigneeId)
.reason("等待队列动态重排后自动派发")
.build();
if (result.isSuccess()) {
return DispatchResult.success("已按队列总分派发下一单", assigneeId);
} else {
return DispatchResult.fail("按队列总分派发下一单失败: " + result.getMessage());
OrderTransitionResult result = orderLifecycleManager.transition(request);
if (result.isSuccess()) {
return DispatchResult.success("按队列总分派发下一单", assigneeId);
} else {
log.warn("派发下一单失败,尝试下一条: orderId={}, error={}",
nextTask.getOpsOrderId(), result.getMessage());
}
}
// 所有等待任务都不可派发
log.info("所有等待任务均不可派发,执行人变为空闲: assigneeId={}", assigneeId);
return DispatchResult.success("无可派发工单,执行人保持空闲", assigneeId);
}
// ==================== 策略注册 ====================

View File

@@ -22,7 +22,9 @@ import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO;
import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper;
import com.viewsh.module.ops.service.fsm.OrderStateMachine;
import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager;
import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest;
import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionResult;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -62,7 +64,7 @@ public class SecurityOrderServiceImpl implements SecurityOrderService {
private AreaPathBuilder areaPathBuilder;
@Resource
private OrderStateMachine orderStateMachine;
private OrderLifecycleManager orderLifecycleManager;
@Resource
private EventLogRecorder eventLogRecorder;
@@ -151,9 +153,15 @@ public class SecurityOrderServiceImpl implements SecurityOrderService {
// 如果 userId 为 nullopen-api 调用),取已分配人员
Long effectiveUserId = resolveOperatorId(orderId, userId);
// 状态转换DISPATCHED → CONFIRMED扩展表时间 + 业务日志由 EventListener 统一记录
orderStateMachine.transition(order, WorkOrderStatusEnum.CONFIRMED,
OperatorTypeEnum.SECURITY_GUARD, effectiveUserId, "安保人员确认接单");
// 状态转换DISPATCHED → CONFIRMED走完整责任链,确保队列同步 + 事件发布
OrderTransitionRequest request = OrderTransitionRequest.builder()
.orderId(orderId)
.targetStatus(WorkOrderStatusEnum.CONFIRMED)
.operatorType(OperatorTypeEnum.SECURITY_GUARD)
.operatorId(effectiveUserId)
.reason("安保人员确认接单")
.build();
orderLifecycleManager.transition(request);
log.info("安保工单确认: orderId={}, userId={}", orderId, effectiveUserId);
}
@@ -172,15 +180,19 @@ public class SecurityOrderServiceImpl implements SecurityOrderService {
log.info("安保工单已处于终态,跳过自动完单: orderId={}, status={}", orderId, currentStatus);
return;
}
// 2. PENDING未派单直接取消PENDING → CANCELLED 规则已支持
// 2. PENDING未派单直接取消PENDING → CANCELLED 是合法转换
if (currentStatus == WorkOrderStatusEnum.PENDING) {
orderStateMachine.transition(order, WorkOrderStatusEnum.CANCELLED,
OperatorTypeEnum.SYSTEM, null, effectiveRemark + "(工单未派单)");
orderLifecycleManager.cancelOrder(orderId, null, OperatorTypeEnum.SYSTEM,
effectiveRemark + "(工单未派单)");
}
// 3. 其他非终态DISPATCHED / CONFIRMED / ARRIVED / PAUSED→ 强制跳转 COMPLETED
// 3. 其他非终态(QUEUED / DISPATCHED / CONFIRMED / ARRIVED / PAUSED
// 通过 forceComplete 走完整责任链(队列清理 + 事件发布),避免脏数据残留
else {
orderStateMachine.forceTransition(order, WorkOrderStatusEnum.COMPLETED,
OperatorTypeEnum.SYSTEM, null, effectiveRemark);
OrderTransitionResult result = orderLifecycleManager.forceComplete(
orderId, null, OperatorTypeEnum.SYSTEM, effectiveRemark);
if (!result.isSuccess()) {
throw new IllegalStateException("强制完成工单失败: " + result.getMessage());
}
}
log.info("安保工单自动结单: orderId={}, 原始状态={}", orderId, currentStatus);
@@ -211,9 +223,9 @@ public class SecurityOrderServiceImpl implements SecurityOrderService {
// 如果 operatorId 为 nullopen-api 调用),取已分配人员
Long effectiveOperatorId = resolveOperatorId(req.getOrderId(), req.getOperatorId());
// 状态转换 → COMPLETED扩展表 completedTime + 业务日志由 EventListener 统一记录,主表 endTime 由状态机统一设置
orderStateMachine.transition(order, WorkOrderStatusEnum.COMPLETED,
OperatorTypeEnum.SECURITY_GUARD, effectiveOperatorId, "安保人员提交处理结果");
// 状态转换 → COMPLETED走完整责任链,确保队列同步 + 事件发布
orderLifecycleManager.completeOrder(req.getOrderId(), effectiveOperatorId,
OperatorTypeEnum.SECURITY_GUARD, "安保人员提交处理结果");
// 更新扩展表:结果 + 图片
OpsOrderSecurityExtDO extUpdate = new OpsOrderSecurityExtDO();

View File

@@ -13,7 +13,9 @@ import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator;
import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator;
import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO;
import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper;
import com.viewsh.module.ops.service.fsm.OrderStateMachine;
import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager;
import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionRequest;
import com.viewsh.module.ops.core.lifecycle.model.OrderTransitionResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -52,7 +54,7 @@ public class SecurityOrderServiceTest {
@Mock
private OrderEventPublisher orderEventPublisher;
@Mock
private OrderStateMachine orderStateMachine;
private OrderLifecycleManager orderLifecycleManager;
// 模拟数据库
private Map<Long, OpsOrderDO> orderDB;
@@ -199,14 +201,14 @@ public class SecurityOrderServiceTest {
// 执行
securityOrderService.confirmOrder(TEST_ORDER_ID, userId);
// 验证状态机调用
verify(orderStateMachine).transition(
eq(order),
eq(WorkOrderStatusEnum.CONFIRMED),
eq(OperatorTypeEnum.SECURITY_GUARD),
eq(userId),
eq("安保人员确认接单")
);
// 验证走 orderLifecycleManager.transition()
ArgumentCaptor<OrderTransitionRequest> reqCaptor = ArgumentCaptor.forClass(OrderTransitionRequest.class);
verify(orderLifecycleManager).transition(reqCaptor.capture());
OrderTransitionRequest capturedReq = reqCaptor.getValue();
assertEquals(TEST_ORDER_ID, capturedReq.getOrderId());
assertEquals(WorkOrderStatusEnum.CONFIRMED, capturedReq.getTargetStatus());
assertEquals(OperatorTypeEnum.SECURITY_GUARD, capturedReq.getOperatorType());
assertEquals(userId, capturedReq.getOperatorId());
// 验证不再直接写扩展表时间(由 EventListener 统一处理)
verify(securityExtMapper, never()).insertOrUpdateSelective(any());
@@ -235,9 +237,8 @@ public class SecurityOrderServiceTest {
() -> securityOrderService.confirmOrder(TEST_ORDER_ID, 2001L));
assertTrue(exception.getMessage().contains("工单类型不匹配"));
// 验证状态机未被调用
verify(orderStateMachine, never()).transition(
any(), any(), any(), anyLong(), anyString());
// 验证 lifecycleManager 未被调用
verify(orderLifecycleManager, never()).transition(any(OrderTransitionRequest.class));
}
// ==================== 自动完单测试 ====================
@@ -247,21 +248,21 @@ public class SecurityOrderServiceTest {
// 准备
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED);
orderDB.put(TEST_ORDER_ID, order);
when(orderLifecycleManager.forceComplete(anyLong(), any(), any(), anyString()))
.thenReturn(OrderTransitionResult.builder().success(true).orderId(TEST_ORDER_ID).build());
// 执行
securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除");
// 验证状态机调用ARRIVED 非终态,走 forceTransition
verify(orderStateMachine).forceTransition(
eq(order),
eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SYSTEM),
// 验证走 forceComplete完整责任链
verify(orderLifecycleManager).forceComplete(
eq(TEST_ORDER_ID),
isNull(),
eq(OperatorTypeEnum.SYSTEM),
eq("告警自动解除")
);
// 验证不直接写扩展表
verify(securityExtMapper, never()).insertOrUpdateSelective(any());
// 验证不直接调用 orderStateMachine已完全移除依赖
}
@Test
@@ -269,14 +270,16 @@ public class SecurityOrderServiceTest {
// 准备
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED);
orderDB.put(TEST_ORDER_ID, order);
when(orderLifecycleManager.forceComplete(anyLong(), any(), any(), anyString()))
.thenReturn(OrderTransitionResult.builder().success(true).orderId(TEST_ORDER_ID).build());
// 执行remark 为空
securityOrderService.autoCompleteOrder(TEST_ORDER_ID, null);
// 验证使用默认备注
verify(orderStateMachine).forceTransition(
any(), eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SYSTEM), isNull(),
verify(orderLifecycleManager).forceComplete(
eq(TEST_ORDER_ID), isNull(),
eq(OperatorTypeEnum.SYSTEM),
eq("系统自动完单")
);
}
@@ -286,14 +289,16 @@ public class SecurityOrderServiceTest {
// 准备
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED);
orderDB.put(TEST_ORDER_ID, order);
when(orderLifecycleManager.forceComplete(anyLong(), any(), any(), anyString()))
.thenReturn(OrderTransitionResult.builder().success(true).orderId(TEST_ORDER_ID).build());
// 执行remark 为空白字符串
securityOrderService.autoCompleteOrder(TEST_ORDER_ID, " ");
// 验证使用默认备注
verify(orderStateMachine).forceTransition(
any(), eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SYSTEM), isNull(),
verify(orderLifecycleManager).forceComplete(
eq(TEST_ORDER_ID), isNull(),
eq(OperatorTypeEnum.SYSTEM),
eq("系统自动完单")
);
}
@@ -313,16 +318,15 @@ public class SecurityOrderServiceTest {
// 执行
securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除");
// 验证:走正常 transition → CANCELLED
verify(orderStateMachine).transition(
eq(order),
eq(WorkOrderStatusEnum.CANCELLED),
eq(OperatorTypeEnum.SYSTEM),
// 验证:走 cancelOrder完整责任链
verify(orderLifecycleManager).cancelOrder(
eq(TEST_ORDER_ID),
isNull(),
eq(OperatorTypeEnum.SYSTEM),
eq("告警自动解除(工单未派单)")
);
// 不应调用 forceTransition
verify(orderStateMachine, never()).forceTransition(any(), any(), any(), any(), any());
// 不应调用 forceComplete
verify(orderLifecycleManager, never()).forceComplete(anyLong(), any(), any(), anyString());
}
@Test
@@ -330,16 +334,17 @@ public class SecurityOrderServiceTest {
// 准备DISPATCHED 状态工单
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.DISPATCHED);
orderDB.put(TEST_ORDER_ID, order);
when(orderLifecycleManager.forceComplete(anyLong(), any(), any(), anyString()))
.thenReturn(OrderTransitionResult.builder().success(true).orderId(TEST_ORDER_ID).build());
// 执行
securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除");
// 验证:走 forceTransition → COMPLETED
verify(orderStateMachine).forceTransition(
eq(order),
eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SYSTEM),
// 验证:走 forceComplete → COMPLETED(通过完整责任链)
verify(orderLifecycleManager).forceComplete(
eq(TEST_ORDER_ID),
isNull(),
eq(OperatorTypeEnum.SYSTEM),
eq("告警自动解除")
);
}
@@ -353,9 +358,9 @@ public class SecurityOrderServiceTest {
// 执行:不应抛异常
securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除");
// 验证:状态机未被调用
verify(orderStateMachine, never()).transition(any(), any(), any(), any(), any());
verify(orderStateMachine, never()).forceTransition(any(), any(), any(), any(), any());
// 验证:生命周期管理器未被调用
verify(orderLifecycleManager, never()).forceComplete(anyLong(), any(), any(), anyString());
verify(orderLifecycleManager, never()).cancelOrder(anyLong(), any(), any(), anyString());
}
@Test
@@ -363,16 +368,17 @@ public class SecurityOrderServiceTest {
// 准备DISPATCHED 状态工单
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.DISPATCHED);
orderDB.put(TEST_ORDER_ID, order);
when(orderLifecycleManager.forceComplete(anyLong(), any(), any(), anyString()))
.thenReturn(OrderTransitionResult.builder().success(true).orderId(TEST_ORDER_ID).build());
// 执行
securityOrderService.falseAlarmOrder(TEST_ORDER_ID);
// 验证forceTransition 被调用(通过 autoCompleteOrder 委托)
verify(orderStateMachine).forceTransition(
eq(order),
eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SYSTEM),
// 验证forceComplete 被调用(通过 autoCompleteOrder 委托)
verify(orderLifecycleManager).forceComplete(
eq(TEST_ORDER_ID),
isNull(),
eq(OperatorTypeEnum.SYSTEM),
eq("误报")
);
@@ -403,12 +409,11 @@ public class SecurityOrderServiceTest {
// 执行
securityOrderService.manualCompleteOrder(req);
// 验证状态机调用
verify(orderStateMachine).transition(
eq(order),
eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SECURITY_GUARD),
// 验证走 orderLifecycleManager.completeOrder()
verify(orderLifecycleManager).completeOrder(
eq(TEST_ORDER_ID),
eq(2001L),
eq(OperatorTypeEnum.SECURITY_GUARD),
eq("安保人员提交处理结果")
);
@@ -421,11 +426,6 @@ public class SecurityOrderServiceTest {
assertNotNull(extUpdate.getResultImgUrls());
assertTrue(extUpdate.getResultImgUrls().contains("result1.jpg"));
assertNull(extUpdate.getCompletedTime()); // 时间由 EventListener 写入
// 验证主表 endTime 更新
ArgumentCaptor<OpsOrderDO> orderUpdateCaptor = ArgumentCaptor.forClass(OpsOrderDO.class);
verify(opsOrderMapper).updateById(orderUpdateCaptor.capture());
assertNotNull(orderUpdateCaptor.getValue().getEndTime());
}
@Test