From 5edbc9f2872ef134ffcb02c6a8acd628b60312e9 Mon Sep 17 00:00:00 2001 From: lzh Date: Tue, 3 Feb 2026 22:59:06 +0800 Subject: [PATCH] =?UTF-8?q?fix(ops):=20=E4=BF=AE=E5=A4=8D=E5=B7=A5?= =?UTF-8?q?=E7=89=8C=E5=85=B3=E6=9C=BA=E9=87=8D=E5=90=AF=E5=90=8E=E5=B7=A5?= =?UTF-8?q?=E5=8D=95=E7=8A=B6=E6=80=81=E4=B8=8D=E4=B8=80=E8=87=B4=E6=BC=8F?= =?UTF-8?q?=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题场景: 1. 工牌有执行中工单(ARRIVED)后关机 2. 工牌重启,Redis状态丢失/过期,设备变为IDLE 3. 系统推送新工单 4. 信标检测仍在用旧工单配置,导致状态混乱 修复方案: 1. 派发新工单前检查并清理/取消旧工单残留 2. 设备离线时自动取消未完成的工单 3. 信标检测器增加工单切换检测,清理旧检测状态 涉及文件: - BadgeDeviceStatusEventListener: 增加旧工单清理和离线事件监听 - BadgeDeviceStatusServiceImpl: 设备离线时发布事件 - BeaconDetectionRuleProcessor: 工单切换检测 Co-Authored-By: Claude Opus 4.5 --- .../BeaconDetectionRuleProcessor.java | 46 ++++++++- .../BadgeDeviceStatusEventListener.java | 96 ++++++++++++++++++- .../badge/BadgeDeviceStatusServiceImpl.java | 72 ++++++++++++-- 3 files changed, 204 insertions(+), 10 deletions(-) diff --git a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java index 62a667c..18ebcbf 100644 --- a/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java +++ b/viewsh-module-iot/viewsh-module-iot-server/src/main/java/com/viewsh/module/iot/service/rule/clean/processor/BeaconDetectionRuleProcessor.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; /** * 蓝牙信标检测规则处理器 @@ -53,6 +54,12 @@ public class BeaconDetectionRuleProcessor { @Resource private RocketMQTemplate rocketMQTemplate; + /** + * 上次检测的工单ID缓存(设备ID -> 工单ID) + * 用于检测工单切换,清理旧工单的检测状态 + */ + private final Map lastDetectedOrderCache = new ConcurrentHashMap<>(); + /** * 处理蓝牙属性上报 *

@@ -75,12 +82,27 @@ public class BeaconDetectionRuleProcessor { if (currentOrder == null || currentOrder.getAreaId() == null) { log.debug("[BeaconDetection] 无当前工单,跳过检测:deviceId={}", deviceId); + // 无工单时清理本地缓存 + lastDetectedOrderCache.remove(deviceId); return; } Long areaId = currentOrder.getAreaId(); + Long orderId = currentOrder.getOrderId(); + + // 3. 检测工单切换,清理旧工单的检测状态 + Long lastOrderId = lastDetectedOrderCache.get(deviceId); + if (lastOrderId != null && !lastOrderId.equals(orderId)) { + log.warn("[BeaconDetection] 检测到工单切换,清理旧工单的检测状态: " + + "deviceId={}, oldOrderId={}, newOrderId={}", deviceId, lastOrderId, orderId); + // 清理旧的检测状态(清理当前设备的所有区域检测状态) + cleanupAllDetectionState(deviceId); + } + // 更新缓存 + lastDetectedOrderCache.put(deviceId, orderId); + log.debug("[BeaconDetection] 从工单状态获取区域:deviceId={}, areaId={}, orderId={}", - deviceId, areaId, currentOrder.getOrderId()); + deviceId, areaId, orderId); // 3. 获取该区域的信标配置(从 BEACON 类型的设备获取) CleanOrderIntegrationConfigService.AreaDeviceConfigWrapper beaconConfigWrapper = configService @@ -331,4 +353,26 @@ public class BeaconDetectionRuleProcessor { BadgeDeviceStatusRedisDAO.OrderInfo currentOrder = badgeDeviceStatusRedisDAO.getCurrentOrder(deviceId); return currentOrder != null && !currentOrder.getAreaId().equals(areaId); } + + /** + * 清理设备所有区域的检测状态 + *

+ * 用于工单切换场景,清理本地缓存。 + * Redis 数据(arrivedTime、signalLoss、rssiWindow)由以下路径清理: + *

    + *
  • 工单完成时:SignalLossRuleProcessor.cleanupRedisData()
  • + *
  • 自然过期:Redis TTL 自动清理
  • + *
  • 新数据覆盖:每次检测都会更新滑动窗口
  • + *
+ * + * @param deviceId 设备ID + */ + private void cleanupAllDetectionState(Long deviceId) { + if (deviceId == null) { + return; + } + // 清理本地缓存 + lastDetectedOrderCache.remove(deviceId); + log.info("[BeaconDetection] 已清理设备工单切换检测状态: deviceId={}", deviceId); + } } 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 f8e221b..103ea18 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 @@ -3,11 +3,14 @@ package com.viewsh.module.ops.environment.integration.listener; import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO; import com.viewsh.module.ops.api.queue.OrderQueueService; import com.viewsh.module.ops.core.event.OrderStateChangedEvent; +import com.viewsh.module.ops.core.lifecycle.OrderLifecycleManager; import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum; +import com.viewsh.module.ops.enums.OperatorTypeEnum; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService; +import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusServiceImpl.BadgeDeviceOfflineEvent; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; @@ -78,6 +81,9 @@ public class BadgeDeviceStatusEventListener { @Resource private OrderQueueService orderQueueService; + @Resource + private OrderLifecycleManager orderLifecycleManager; + /** * 监听工单状态变更事件,同步更新设备工单关联 *

@@ -135,7 +141,7 @@ public class BadgeDeviceStatusEventListener { * */ private void handleOrderStatusTransition(Long deviceId, Long orderId, WorkOrderStatusEnum newStatus, - OrderStateChangedEvent event, OpsOrderDO order) { + OrderStateChangedEvent event, OpsOrderDO order) { switch (newStatus) { case DISPATCHED: handleDispatched(deviceId, orderId, order); @@ -170,6 +176,37 @@ public class BadgeDeviceStatusEventListener { * 处理工单推送状态(首次设置工单关联) */ 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) { + // 旧工单仍在进行,先取消 + log.warn("[BadgeDeviceStatusEventListener] 取消残留的旧工单: oldOrderId={}", oldOrderId); + try { + orderLifecycleManager.cancelOrder(oldOrderId, deviceId, + OperatorTypeEnum.SYSTEM, "新工单派发,自动取消旧工单"); + } catch (Exception e) { + log.error("[BadgeDeviceStatusEventListener] 取消旧工单失败: oldOrderId={}", oldOrderId, e); + } + } + } + + // 确保设备状态清理(无论旧工单是否取消成功) + badgeDeviceStatusService.clearCurrentOrder(deviceId); + } + } + // 设备状态转为 BUSY badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY, null, "新工单已推送"); @@ -271,4 +308,61 @@ public class BadgeDeviceStatusEventListener { orderId, currentOrderId); } } + + /** + * 监听设备离线事件,自动取消未完成的工单 + */ + @EventListener + public void onDeviceOffline(BadgeDeviceOfflineEvent event) { + try { + Long orderId = event.getOrderId(); + if (orderId == null) { + return; + } + + log.info("[BadgeDeviceStatusEventListener] 收到设备离线事件: deviceId={}, orderId={}, areaId={}", + event.getDeviceId(), orderId, event.getAreaId()); + + // 查询工单状态 + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.warn("[BadgeDeviceStatusEventListener] 工单不存在,清除设备关联: orderId={}", orderId); + badgeDeviceStatusService.clearCurrentOrder(event.getDeviceId()); + return; + } + + WorkOrderStatusEnum currentStatus = WorkOrderStatusEnum.fromStatus(order.getStatus()); + + // 只有进行中的工单才自动取消 + if (currentStatus == WorkOrderStatusEnum.DISPATCHED + || currentStatus == WorkOrderStatusEnum.CONFIRMED + || currentStatus == WorkOrderStatusEnum.ARRIVED) { + + log.info("[BadgeDeviceStatusEventListener] 设备离线,自动取消工单: orderId={}, currentStatus={}", + orderId, currentStatus); + + // 取消工单(系统操作,operatorId 传 null) + orderLifecycleManager.cancelOrder(orderId, null, + OperatorTypeEnum.SYSTEM, "设备离线自动取消"); + + // 注意:工单取消后,handleCancelled 方法会被调用,会自动清理设备状态 + } else if (currentStatus == WorkOrderStatusEnum.COMPLETED + || currentStatus == WorkOrderStatusEnum.CANCELLED) { + // 终态工单,清理设备状态(如果未清理) + BadgeDeviceStatusDTO deviceStatus = badgeDeviceStatusService.getBadgeStatus(event.getDeviceId()); + if (deviceStatus != null && orderId.equals(deviceStatus.getCurrentOpsOrderId())) { + log.info("[BadgeDeviceStatusEventListener] 工单已终态,清除设备关联: orderId={}, currentStatus={}", + orderId, currentStatus); + badgeDeviceStatusService.clearCurrentOrder(event.getDeviceId()); + } + } else { + log.warn("[BadgeDeviceStatusEventListener] 工单状态异常,清除设备关联: orderId={}, currentStatus={}", + orderId, currentStatus); + badgeDeviceStatusService.clearCurrentOrder(event.getDeviceId()); + } + + } catch (Exception e) { + log.error("[BadgeDeviceStatusEventListener] 处理设备离线事件失败: deviceId={}", event.getDeviceId(), e); + } + } } diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java index 1ca4809..da9b54e 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusServiceImpl.java @@ -6,6 +6,7 @@ import com.viewsh.module.ops.service.area.AreaDeviceService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -41,6 +42,9 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I @Resource private AreaDeviceService areaDeviceService; + @Resource + private ApplicationEventPublisher eventPublisher; + /** * Redis Key 前缀 */ @@ -247,6 +251,19 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I // 获取当前状态 Map currentMap = redisTemplate.opsForHash().entries(key); + // 设备离线时,检查是否有正在执行的工单,发布事件处理 + if (status == BadgeDeviceStatusEnum.OFFLINE && currentMap.containsKey("currentOpsOrderId")) { + Object currentOrderIdObj = currentMap.get("currentOpsOrderId"); + Object currentAreaIdObj = currentMap.get("currentAreaId"); + if (currentOrderIdObj != null) { + log.warn("[updateBadgeOnlineStatus] 设备离线,有进行中工单,发布取消事件: deviceId={}, orderId={}", + deviceId, currentOrderIdObj); + // 发布设备离线事件,由监听器处理工单取消 + eventPublisher.publishEvent(new BadgeDeviceOfflineEvent(deviceId, + getLong(currentOrderIdObj), getLong(currentAreaIdObj), reason)); + } + } + // 核心修复逻辑:如果不为 OFFLINE (即 IDLE/BUSY),且当前有正在进行的工单,则强制保持 BUSY 状态 // 防止设备心跳/上线事件将 BUSY 重置为 IDLE,导致调度状态不一致 if (status != BadgeDeviceStatusEnum.OFFLINE && currentMap.containsKey("currentOpsOrderId")) { @@ -282,19 +299,17 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I } // 更新区域信息 - // 注意:currentAreaId 应该表示当前工单所属区域,而非设备物理位置 - // 只有在没有工单时,才同步 IoT 上报的物理位置 + // 注意:currentAreaId 仅表示当前工单所属区域,不存储设备物理位置 + // 无工单时不填充 currentAreaId,只在派单时由工单区域设置 if (areaId != null) { // 检查是否有正在执行的工单 Object currentOrderId = currentMap.get("currentOpsOrderId"); if (currentOrderId != null) { - // 有工单:保持 currentAreaId 不变(用工单区域),不覆盖 + // 有工单:保持 currentAreaId 不变(用工单区域),不覆盖 IoT 物理位置 log.debug("设备 {} 有执行中工单 {},保持 currentAreaId 不变,忽略 IoT 上报的物理位置 {}", deviceId, currentOrderId, areaId); - } else { - // 无工单:用 IoT 上报的物理位置 - statusMap.put("currentAreaId", areaId); } + // 无工单:不填充 currentAreaId,保持为空 } // 保持当前工单相关字段(如果存在) @@ -417,8 +432,8 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I try { String key = BADGE_STATUS_KEY_PREFIX + deviceId; - // 清除工单相关字段:currentOpsOrderId、currentOrderStatus、beaconMac - redisTemplate.opsForHash().delete(key, "currentOpsOrderId", "currentOrderStatus", "beaconMac"); + // 清除工单相关字段:currentOpsOrderId、currentOrderStatus、currentAreaId、currentAreaName、beaconMac + redisTemplate.opsForHash().delete(key, "currentOpsOrderId", "currentOrderStatus", "currentAreaId", "currentAreaName", "beaconMac"); log.info("清除工牌设备当前工单: deviceId={}", deviceId); } catch (Exception e) { log.error("清除工牌设备当前工单失败: deviceId={}", deviceId, e); @@ -585,4 +600,45 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I return null; } } + + /** + * 工牌设备离线事件 + *

+ * 当设备离线且有正在执行的工单时发布,由监听器处理工单自动取消 + */ + public static class BadgeDeviceOfflineEvent { + private final Long deviceId; + private final Long orderId; + private final Long areaId; + private final String reason; + private final long timestamp; + + public BadgeDeviceOfflineEvent(Long deviceId, Long orderId, Long areaId, String reason) { + this.deviceId = deviceId; + this.orderId = orderId; + this.areaId = areaId; + this.reason = reason; + this.timestamp = System.currentTimeMillis(); + } + + public Long getDeviceId() { + return deviceId; + } + + public Long getOrderId() { + return orderId; + } + + public Long getAreaId() { + return areaId; + } + + public String getReason() { + return reason; + } + + public long getTimestamp() { + return timestamp; + } + } }