diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventListener.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventListener.java
index a9cc44c5..cd0b7857 100644
--- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventListener.java
+++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/integration/listener/BadgeDeviceStatusEventListener.java
@@ -15,9 +15,6 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
-import org.springframework.transaction.PlatformTransactionManager;
-import org.springframework.transaction.TransactionDefinition;
-import org.springframework.transaction.support.TransactionTemplate;
/**
* 工牌设备状态事件监听器
@@ -87,9 +84,6 @@ public class BadgeDeviceStatusEventListener {
@Resource
private OrderLifecycleManager orderLifecycleManager;
- @Resource
- private PlatformTransactionManager transactionManager;
-
/**
* 监听工单状态变更事件,同步更新设备工单关联
*
@@ -180,40 +174,27 @@ public class BadgeDeviceStatusEventListener {
/**
* 处理工单推送状态(首次设置工单关联)
+ *
+ * 若 Redis 里检测到旧 orderId(正常业务不应出现),仅打 ERROR 告警并清理 Redis 关联。
+ * 此前版本会在此处"自动取消旧工单",但那是对"数据已错乱"场景的暴力兜底:
+ *
+ * - 取消使用 REQUIRES_NEW 独立事务且吞异常,失败时新单照常落地,旧单残留,形成越清越多
+ * - 真正的防线应在 DispatchEngine.autoDispatchNext 入口做设备空闲校验
+ *
+ * 现改为被动告警,暴露问题等待定位,避免误杀保洁员正在执行的任务。
*/
private void handleDispatched(Long deviceId, Long orderId, OpsOrderDO order) {
- // 检查并清理旧工单(防止工单切换时状态残留)
BadgeDeviceStatusDTO deviceStatus = badgeDeviceStatusService.getBadgeStatus(deviceId);
if (deviceStatus != null && deviceStatus.getCurrentOpsOrderId() != null) {
Long oldOrderId = deviceStatus.getCurrentOpsOrderId();
if (!oldOrderId.equals(orderId)) {
- log.warn("[BadgeDeviceStatusEventListener] 派发新工单时检测到旧工单残留: " +
- "deviceId={}, oldOrderId={}, newOrderId={}", deviceId, oldOrderId, orderId);
-
- // 检查旧工单是否仍在进行中,如果是则先取消
OpsOrderDO oldOrder = opsOrderMapper.selectById(oldOrderId);
- if (oldOrder != null) {
- WorkOrderStatusEnum oldStatus = WorkOrderStatusEnum.fromStatus(oldOrder.getStatus());
- if (oldStatus == WorkOrderStatusEnum.DISPATCHED
- || oldStatus == WorkOrderStatusEnum.CONFIRMED
- || oldStatus == WorkOrderStatusEnum.ARRIVED) {
- // 旧工单仍在进行,先取消
- // 使用 REQUIRES_NEW 独立事务,避免内层异常标记外层事务 rollback-only
- log.warn("[BadgeDeviceStatusEventListener] 取消残留的旧工单: oldOrderId={}", oldOrderId);
- try {
- TransactionTemplate txTemplate = new TransactionTemplate(transactionManager);
- txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
- txTemplate.executeWithoutResult(status -> {
- orderLifecycleManager.cancelOrder(oldOrderId, deviceId,
- OperatorTypeEnum.SYSTEM, "新工单派发,自动取消旧工单");
- });
- } catch (Exception e) {
- log.error("[BadgeDeviceStatusEventListener] 取消旧工单失败: oldOrderId={}", oldOrderId, e);
- }
- }
- }
+ String oldStatus = oldOrder != null ? oldOrder.getStatus() : "NOT_FOUND";
+ log.error("[BadgeDeviceStatusEventListener] 派发新工单时检测到旧工单残留(数据可能已错乱,需人工核查): " +
+ "deviceId={}, oldOrderId={}, oldStatus={}, newOrderId={}",
+ deviceId, oldOrderId, oldStatus, orderId);
- // 确保设备状态清理(无论旧工单是否取消成功)
+ // 清理 Redis 中对旧工单的关联(纯 Redis 操作,不触达状态机)
badgeDeviceStatusService.clearCurrentOrder(deviceId);
}
}
diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngineImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngineImpl.java
index 48ab3741..1cf884d8 100644
--- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngineImpl.java
+++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/dispatch/DispatchEngineImpl.java
@@ -178,6 +178,22 @@ public class DispatchEngineImpl implements DispatchEngine {
public DispatchResult autoDispatchNext(Long completedOrderId, Long assigneeId) {
log.info("任务完成后自动派发下一单: completedOrderId={}, assigneeId={}", completedOrderId, assigneeId);
+ if (assigneeId == null) {
+ log.warn("autoDispatchNext 缺少执行人,跳过派发: completedOrderId={}", completedOrderId);
+ return DispatchResult.success("缺少执行人,跳过派发", null);
+ }
+
+ // 空闲校验:若执行人仍挂着其他活跃工单(DISPATCHED/CONFIRMED/ARRIVED/PAUSED),
+ // 说明设备尚未真正空闲,不应再派发新任务——否则会触发"同一设备并行多单"的状态错乱,
+ // 典型场景是管理员手动取消一个僵尸 DISPATCHED 单时,handleCancelled 会调到这里。
+ List activeOrders = orderMapper.selectActiveByAssignee(assigneeId, completedOrderId);
+ if (!activeOrders.isEmpty()) {
+ OpsOrderDO head = activeOrders.get(0);
+ log.info("执行人仍有活跃工单,跳过自动派发: assigneeId={}, completedOrderId={}, activeCount={}, sampleOrderId={}, sampleStatus={}",
+ assigneeId, completedOrderId, activeOrders.size(), head.getId(), head.getStatus());
+ return DispatchResult.success("执行人非空闲,跳过派发", assigneeId);
+ }
+
Long fallbackAreaId = null;
OpsOrderDO completedOrder = orderMapper.selectById(completedOrderId);
if (completedOrder != null) {
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 f1454b99..6df51062 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
@@ -92,6 +92,28 @@ public interface OpsOrderMapper extends BaseMapperX {
.last("LIMIT 1"));
}
+ /**
+ * 查询执行人名下尚未结束的工单(DISPATCHED/CONFIRMED/ARRIVED/PAUSED)
+ *
+ * 用于 autoDispatchNext 等调度入口的空闲校验:若该执行人仍挂着活跃工单,
+ * 则不应再派发新任务,避免"越清越多"的级联派发。
+ *
+ * @param assigneeId 执行人ID(工牌设备ID)
+ * @param excludeOrderId 需要排除的工单ID(通常是刚完成/取消触发本次调度的工单),可传 null
+ * @return 活跃工单列表,按创建时间升序
+ */
+ default List selectActiveByAssignee(Long assigneeId, Long excludeOrderId) {
+ return selectList(new LambdaQueryWrapperX()
+ .eq(OpsOrderDO::getAssigneeId, assigneeId)
+ .in(OpsOrderDO::getStatus,
+ WorkOrderStatusEnum.DISPATCHED.getStatus(),
+ WorkOrderStatusEnum.CONFIRMED.getStatus(),
+ WorkOrderStatusEnum.ARRIVED.getStatus(),
+ WorkOrderStatusEnum.PAUSED.getStatus())
+ .ne(excludeOrderId != null, OpsOrderDO::getId, excludeOrderId)
+ .orderByAsc(OpsOrderDO::getCreateTime));
+ }
+
// ==================== 统计聚合查询 ====================
/**