fix(ops): 修复工牌关机重启后工单状态不一致漏洞
问题场景: 1. 工牌有执行中工单(ARRIVED)后关机 2. 工牌重启,Redis状态丢失/过期,设备变为IDLE 3. 系统推送新工单 4. 信标检测仍在用旧工单配置,导致状态混乱 修复方案: 1. 派发新工单前检查并清理/取消旧工单残留 2. 设备离线时自动取消未完成的工单 3. 信标检测器增加工单切换检测,清理旧检测状态 涉及文件: - BadgeDeviceStatusEventListener: 增加旧工单清理和离线事件监听 - BadgeDeviceStatusServiceImpl: 设备离线时发布事件 - BeaconDetectionRuleProcessor: 工单切换检测 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Long, Long> lastDetectedOrderCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 处理蓝牙属性上报
|
||||
* <p>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理设备所有区域的检测状态
|
||||
* <p>
|
||||
* 用于工单切换场景,清理本地缓存。
|
||||
* Redis 数据(arrivedTime、signalLoss、rssiWindow)由以下路径清理:
|
||||
* <ul>
|
||||
* <li>工单完成时:SignalLossRuleProcessor.cleanupRedisData()</li>
|
||||
* <li>自然过期:Redis TTL 自动清理</li>
|
||||
* <li>新数据覆盖:每次检测都会更新滑动窗口</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
*/
|
||||
private void cleanupAllDetectionState(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
return;
|
||||
}
|
||||
// 清理本地缓存
|
||||
lastDetectedOrderCache.remove(deviceId);
|
||||
log.info("[BeaconDetection] 已清理设备工单切换检测状态: deviceId={}", deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 监听工单状态变更事件,同步更新设备工单关联
|
||||
* <p>
|
||||
@@ -135,7 +141,7 @@ public class BadgeDeviceStatusEventListener {
|
||||
* </ul>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Object, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工牌设备离线事件
|
||||
* <p>
|
||||
* 当设备离线且有正在执行的工单时发布,由监听器处理工单自动取消
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user