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:
@@ -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