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:
lzh
2026-02-03 22:59:06 +08:00
parent 3443d4dcd4
commit 5edbc9f287
3 changed files with 204 additions and 10 deletions

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}