From 44c7d7b9f591105c13f072a2bf309b82c6c622c4 Mon Sep 17 00:00:00 2001 From: lzh Date: Thu, 29 Jan 2026 13:04:19 +0800 Subject: [PATCH] refactor(ops): simplify badge status management by removing redundant heartbeat logic --- .../badge/BadgeDeviceStatusService.java | 390 ++++++++---------- .../badge/BadgeDeviceStatusServiceImpl.java | 116 +----- .../dispatch/BadgeDeviceDispatchTest.java | 24 +- 3 files changed, 185 insertions(+), 345 deletions(-) diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusService.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusService.java index ad2c298b..b3b3896d 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusService.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/badge/BadgeDeviceStatusService.java @@ -24,250 +24,202 @@ import java.util.List; */ public interface BadgeDeviceStatusService { - // ==================== 状态管理 ==================== + // ==================== 状态管理 ==================== - /** - * 更新工牌设备状态 - *

- * 状态转换会记录状态变更时间和操作原因 - * - * @param deviceId 设备ID - * @param status 目标状态 - * @param operatorId 操作人ID(可为null,表示系统操作) - * @param reason 状态变更原因 - */ - void updateBadgeStatus(Long deviceId, BadgeDeviceStatusEnum status, Long operatorId, String reason); + /** + * 更新工牌设备状态 + *

+ * 状态转换会记录状态变更时间和操作原因 + * + * @param deviceId 设备ID + * @param status 目标状态 + * @param operatorId 操作人ID(可为null,表示系统操作) + * @param reason 状态变更原因 + */ + void updateBadgeStatus(Long deviceId, BadgeDeviceStatusEnum status, Long operatorId, String reason); - /** - * 批量更新工牌设备状态 - * - * @param deviceIds 设备ID列表 - * @param status 目标状态 - * @param operatorId 操作人ID - * @param reason 状态变更原因 - */ - void batchUpdateBadgeStatus(List deviceIds, BadgeDeviceStatusEnum status, Long operatorId, String reason); + /** + * 批量更新工牌设备状态 + * + * @param deviceIds 设备ID列表 + * @param status 目标状态 + * @param operatorId 操作人ID + * @param reason 状态变更原因 + */ + void batchUpdateBadgeStatus(List deviceIds, BadgeDeviceStatusEnum status, Long operatorId, String reason); - // ==================== 状态查询 ==================== + // ==================== 状态查询 ==================== - /** - * 获取工牌设备状态 - * - * @param deviceId 设备ID - * @return 设备状态DTO,不存在返回null - */ - BadgeDeviceStatusDTO getBadgeStatus(Long deviceId); + /** + * 获取工牌设备状态 + * + * @param deviceId 设备ID + * @return 设备状态DTO,不存在返回null + */ + BadgeDeviceStatusDTO getBadgeStatus(Long deviceId); - /** - * 批量获取工牌设备状态 - * - * @param deviceIds 设备ID列表 - * @return 设备状态DTO列表 - */ - List batchGetBadgeStatus(List deviceIds); + /** + * 批量获取工牌设备状态 + * + * @param deviceIds 设备ID列表 + * @return 设备状态DTO列表 + */ + List batchGetBadgeStatus(List deviceIds); - /** - * 获取指定区域的工牌设备列表 - *

- * 只返回非 OFFLINE 状态的设备 - * - * @param areaId 区域ID - * @return 设备状态DTO列表 - */ - List listBadgesByArea(Long areaId); + /** + * 获取指定区域的工牌设备列表 + *

+ * 只返回非 OFFLINE 状态的设备 + * + * @param areaId 区域ID + * @return 设备状态DTO列表 + */ + List listBadgesByArea(Long areaId); - /** - * 获取可接单的工牌设备(IDLE 状态) - * - * @param areaId 区域ID - * @return 可接单设备列表 - */ - List listAvailableBadges(Long areaId); + /** + * 获取可接单的工牌设备(IDLE 状态) + * + * @param areaId 区域ID + * @return 可接单设备列表 + */ + List listAvailableBadges(Long areaId); - /** - * 获取所有活跃的工牌设备(非OFFLINE状态) - * - * @return 活跃设备列表 - */ - List listActiveBadges(); + /** + * 获取所有活跃的工牌设备(非OFFLINE状态) + * + * @return 活跃设备列表 + */ + List listActiveBadges(); - // ==================== 心跳处理 ==================== + // ==================== 状态更新 (IoT 事件驱动) ==================== - /** - * 处理工牌设备心跳 - *

- * 更新最后心跳时间和电量: - *

- * - * @param deviceId 设备ID - * @param deviceCode 设备编码 - * @param batteryLevel 电量(0-100) - */ - void handleHeartbeat(Long deviceId, String deviceCode, Integer batteryLevel); + /** + * 更新工牌设备在线状态 + *

+ * 用于处理 IoT 设备状态变更事件和定时对账: + *

+ * + * @param deviceId 设备ID + * @param deviceCode 设备编码 + * @param areaId 区域ID(可为null) + * @param status 目标状态(IDLE 或 OFFLINE) + * @param reason 状态变更原因 + */ + void updateBadgeOnlineStatus(Long deviceId, String deviceCode, Long areaId, + BadgeDeviceStatusEnum status, String reason); - /** - * 处理工牌设备心跳(带区域信息) - * - * @param deviceId 设备ID - * @param deviceCode 设备编码 - * @param batteryLevel 电量(0-100) - * @param areaId 当前所在区域ID - * @param areaName 当前所在区域名称 - */ - void handleHeartbeatWithArea(Long deviceId, String deviceCode, Integer batteryLevel, - Long areaId, String areaName); + // ==================== 在线状态检查 ==================== - /** - * 更新工牌设备在线状态 - *

- * 用于处理 IoT 设备状态变更事件和定时对账: - *

- *

- * 与 handleHeartbeat 区别: - * - handleHeartbeat:处理位置上报心跳,包含电量等详细信息 - * - updateBadgeOnlineStatus:仅处理在线/离线状态变更 - * - * @param deviceId 设备ID - * @param deviceCode 设备编码 - * @param areaId 区域ID(可为null) - * @param status 目标状态(IDLE 或 OFFLINE) - * @param reason 状态变更原因 - */ - void updateBadgeOnlineStatus(Long deviceId, String deviceCode, Long areaId, - BadgeDeviceStatusEnum status, String reason); + /** + * 检查工牌设备是否在线(非OFFLINE状态) + * + * @param deviceId 设备ID + * @return 是否在线 + */ + boolean isBadgeOnline(Long deviceId); - // ==================== 在线状态检查 ==================== + // ==================== 工单关联 ==================== - /** - * 检查工牌设备是否在线(非OFFLINE状态) - * - * @param deviceId 设备ID - * @return 是否在线 - */ - boolean isBadgeOnline(Long deviceId); + /** + * 设置当前工单 + * + * @param deviceId 设备ID + * @param orderId 工单ID + */ + void setCurrentOrder(Long deviceId, Long orderId); - /** - * 检查工牌设备心跳是否超时 - * - * @param deviceId 设备ID - * @param thresholdMinutes 超时阈值(分钟) - * @return 是否超时 - */ - boolean isHeartbeatTimeout(Long deviceId, int thresholdMinutes); + /** + * 设置当前工单详细信息(原子操作,并发安全) + *

+ * 一次性设置工单ID、工单状态、区域ID、信标MAC + * 使用 Redis Hash 的 hset() 操作,保证原子性 + * + * @param deviceId 设备ID + * @param orderId 工单ID + * @param orderStatus 工单状态(DISPATCHED/ARRIVED/PAUSED) + * @param areaId 区域ID + * @param beaconMac 信标MAC地址(可为null) + */ + void setCurrentOrderInfo(Long deviceId, Long orderId, String orderStatus, Long areaId, String beaconMac); - /** - * 检查心跳超时并将超时设备设为OFFLINE - *

- * 定时任务调用,默认超时时间为30分钟 - */ - void checkAndMarkOfflineDevices(); + /** + * 更新工单状态 + * + * @param deviceId 设备ID + * @param orderStatus 工单状态 + */ + void updateOrderStatus(Long deviceId, String orderStatus); - // ==================== 工单关联 ==================== + /** + * 清除当前工单(包括工单ID、工单状态、信标MAC) + * + * @param deviceId 设备ID + */ + void clearCurrentOrder(Long deviceId); - /** - * 设置当前工单 - * - * @param deviceId 设备ID - * @param orderId 工单ID - */ - void setCurrentOrder(Long deviceId, Long orderId); + /** + * 获取当前有工单的设备列表 + * + * @return 有工单的设备列表 + */ + List listBadgesWithCurrentOrder(); - /** - * 设置当前工单详细信息(原子操作,并发安全) - *

- * 一次性设置工单ID、工单状态、区域ID、信标MAC - * 使用 Redis Hash 的 hset() 操作,保证原子性 - * - * @param deviceId 设备ID - * @param orderId 工单ID - * @param orderStatus 工单状态(DISPATCHED/ARRIVED/PAUSED) - * @param areaId 区域ID - * @param beaconMac 信标MAC地址(可为null) - */ - void setCurrentOrderInfo(Long deviceId, Long orderId, String orderStatus, Long areaId, String beaconMac); + // ==================== 区域管理 ==================== - /** - * 更新工单状态 - * - * @param deviceId 设备ID - * @param orderStatus 工单状态 - */ - void updateOrderStatus(Long deviceId, String orderStatus); + /** + * 更新工牌设备所在区域 + * + * @param deviceId 设备ID + * @param areaId 区域ID + * @param areaName 区域名称 + */ + void updateBadgeArea(Long deviceId, Long areaId, String areaName); - /** - * 清除当前工单(包括工单ID、工单状态、信标MAC) - * - * @param deviceId 设备ID - */ - void clearCurrentOrder(Long deviceId); + /** + * 初始化区域设备索引 + *

+ * 从 ops_area_device_relation 表加载 BADGE 类型的设备, + * 建立区域到设备的索引关系 + */ + void initAreaDeviceIndex(); - /** - * 获取当前有工单的设备列表 - * - * @return 有工单的设备列表 - */ - List listBadgesWithCurrentOrder(); + /** + * 刷新区域设备索引 + *

+ * 重新从数据库加载区域设备关系 + */ + void refreshAreaDeviceIndex(); - // ==================== 区域管理 ==================== + /** + * 将设备添加到区域索引 + * + * @param deviceId 设备ID + * @param areaId 区域ID + */ + void addToAreaIndex(Long deviceId, Long areaId); - /** - * 更新工牌设备所在区域 - * - * @param deviceId 设备ID - * @param areaId 区域ID - * @param areaName 区域名称 - */ - void updateBadgeArea(Long deviceId, Long areaId, String areaName); + /** + * 从区域索引移除设备 + * + * @param deviceId 设备ID + * @param areaId 区域ID + */ + void removeFromAreaIndex(Long deviceId, Long areaId); - /** - * 初始化区域设备索引 - *

- * 从 ops_area_device_relation 表加载 BADGE 类型的设备, - * 建立区域到设备的索引关系 - */ - void initAreaDeviceIndex(); + // ==================== 设备管理 ==================== - /** - * 刷新区域设备索引 - *

- * 重新从数据库加载区域设备关系 - */ - void refreshAreaDeviceIndex(); + /** + * 删除工牌设备状态 + * + * @param deviceId 设备ID + */ + void deleteBadgeStatus(Long deviceId); - /** - * 将设备添加到区域索引 - * - * @param deviceId 设备ID - * @param areaId 区域ID - */ - void addToAreaIndex(Long deviceId, Long areaId); - - /** - * 从区域索引移除设备 - * - * @param deviceId 设备ID - * @param areaId 区域ID - */ - void removeFromAreaIndex(Long deviceId, Long areaId); - - // ==================== 设备管理 ==================== - - /** - * 删除工牌设备状态 - * - * @param deviceId 设备ID - */ - void deleteBadgeStatus(Long deviceId); - - /** - * 清理所有离线设备的状态 - */ - void clearOfflineBadges(); + /** + * 清理所有离线设备的状态 + */ + void clearOfflineBadges(); } 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 92717d0c..fe433d7d 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 @@ -7,7 +7,7 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.scheduling.annotation.Scheduled; + import org.springframework.stereotype.Service; import java.time.LocalDateTime; @@ -245,80 +245,7 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I } } - // ==================== 心跳处理 ==================== - - @Override - public void handleHeartbeat(Long deviceId, String deviceCode, Integer batteryLevel) { - handleHeartbeatWithArea(deviceId, deviceCode, batteryLevel, null, null); - } - - @Override - public void handleHeartbeatWithArea(Long deviceId, String deviceCode, Integer batteryLevel, - Long areaId, String areaName) { - if (deviceId == null) { - return; - } - - try { - String key = BADGE_STATUS_KEY_PREFIX + deviceId; - Long now = System.currentTimeMillis(); - - // 获取当前状态 - Map currentMap = redisTemplate.opsForHash().entries(key); - BadgeDeviceStatusEnum currentStatus = null; - if (!currentMap.isEmpty()) { - String statusStr = (String) currentMap.get("status"); - currentStatus = BadgeDeviceStatusEnum.fromCode(statusStr); - } - - // 如果之前是 OFFLINE,转为 IDLE - // 如果当前状态为空,初始化为 IDLE - BadgeDeviceStatusEnum newStatus = currentStatus; - if (currentStatus == null) { - newStatus = BadgeDeviceStatusEnum.IDLE; - } else if (currentStatus == BadgeDeviceStatusEnum.OFFLINE) { - // 如果已标记为离线,心跳不再自动复活设备 - // 必须依赖 IoT 上线事件或定时对账 Job 来恢复在线状态 - // 这样做是为了防止因离线事件和心跳包乱序(幽灵心跳)导致的“假在线”问题 - log.debug("设备处于离线状态但收到心跳,保持离线状态: deviceId={}", deviceId); - newStatus = BadgeDeviceStatusEnum.OFFLINE; - } - - // 更新状态 - Map statusMap = new HashMap<>(); - statusMap.put("deviceId", deviceId); - statusMap.put("deviceCode", deviceCode != null ? deviceCode : currentMap.get("deviceCode")); - statusMap.put("status", newStatus.getCode()); - statusMap.put("batteryLevel", batteryLevel != null ? batteryLevel : currentMap.get("batteryLevel")); - statusMap.put("lastHeartbeatTime", now); - statusMap.put("statusChangeTime", LocalDateTime.now().toString()); - - // 更新区域信息 - if (areaId != null) { - statusMap.put("currentAreaId", areaId); - statusMap.put("currentAreaName", areaName); - // 更新区域索引 - addToAreaIndex(deviceId, areaId); - } else { - statusMap.put("currentAreaId", currentMap.getOrDefault("currentAreaId", null)); - statusMap.put("currentAreaName", currentMap.getOrDefault("currentAreaName", null)); - } - - // 保持当前工单相关字段 - statusMap.put("currentOpsOrderId", currentMap.get("currentOpsOrderId")); - statusMap.put("currentOrderStatus", currentMap.get("currentOrderStatus")); - statusMap.put("beaconMac", currentMap.get("beaconMac")); - - redisTemplate.opsForHash().putAll(key, statusMap); - redisTemplate.expire(key, STATUS_EXPIRE_HOURS, TimeUnit.HOURS); - - log.debug("处理工牌设备心跳: deviceId={}, deviceCode={}, batteryLevel={}, status={}", - deviceId, deviceCode, batteryLevel, newStatus); - - } catch (Exception e) { - log.error("处理工牌设备心跳失败: deviceId={}", deviceId, e); - } - } + // ==================== 状态更新 (IoT 事件驱动) ==================== @Override public void updateBadgeOnlineStatus(Long deviceId, String deviceCode, Long areaId, @@ -342,7 +269,7 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I statusMap.put("statusChangeTime", LocalDateTime.now().toString()); statusMap.put("statusChangeReason", reason); - // 如果是上线(非 OFFLINE 状态),更新心跳时间 + // 如果是上线(非 OFFLINE 状态),更新心跳时间(此处作为活跃时间使用) if (status.isActive()) { statusMap.put("lastHeartbeatTime", now); } @@ -389,49 +316,12 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I } } - // ==================== 在线状态检查 ==================== - @Override public boolean isBadgeOnline(Long deviceId) { BadgeDeviceStatusDTO status = getBadgeStatus(deviceId); return status != null && status.isOnline(); } - @Override - public boolean isHeartbeatTimeout(Long deviceId, int thresholdMinutes) { - BadgeDeviceStatusDTO status = getBadgeStatus(deviceId); - return status == null || status.isHeartbeatTimeout(thresholdMinutes); - } - - @Override - @Scheduled(cron = "0 */5 * * * ?") - public void checkAndMarkOfflineDevices() { - try { - List activeBadges = listActiveBadges(); - - long thresholdMillis = System.currentTimeMillis() - (HEARTBEAT_TIMEOUT_MINUTES * 60L * 1000L); - - List offlineDeviceIds = new ArrayList<>(); - for (BadgeDeviceStatusDTO device : activeBadges) { - if (device.getLastHeartbeatTime() == null || - device.getLastHeartbeatTime() < thresholdMillis) { - updateBadgeStatus(device.getDeviceId(), BadgeDeviceStatusEnum.OFFLINE, null, "心跳超时"); - offlineDeviceIds.add(device.getDeviceId()); - } - } - - if (!offlineDeviceIds.isEmpty()) { - log.info("标记心跳超时设备为离线: count={}, deviceIds={}", - offlineDeviceIds.size(), offlineDeviceIds); - } - - } catch (Exception e) { - log.error("检查心跳超时失败", e); - } - } - - // ==================== 工单关联 ==================== - @Override public void setCurrentOrder(Long deviceId, Long orderId) { if (deviceId == null || orderId == null) { diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceDispatchTest.java b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceDispatchTest.java index 916f2535..07db3ecd 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceDispatchTest.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/test/java/com/viewsh/module/ops/environment/service/dispatch/BadgeDeviceDispatchTest.java @@ -32,7 +32,7 @@ import static org.mockito.Mockito.*; */ @Slf4j @SpringJUnitConfig(classes = BadgeDispatchTestConfig.class) -@TestExecutionListeners({DependencyInjectionTestExecutionListener.class}) +@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class }) public class BadgeDeviceDispatchTest { @Resource @@ -114,32 +114,30 @@ public class BadgeDeviceDispatchTest { log.info("测试:心跳调用"); log.info("========================================"); - // 模拟心跳调用 - badgeDeviceStatusService.handleHeartbeatWithArea( + // 模拟状态更新调用 + badgeDeviceStatusService.updateBadgeOnlineStatus( TEST_DEVICE_ID, TEST_DEVICE_CODE, - 75, TEST_AREA_ID, - "A座2楼男卫" - ); + BadgeDeviceStatusEnum.IDLE, + "测试心跳"); // 验证调用 - verify(badgeDeviceStatusService).handleHeartbeatWithArea( + verify(badgeDeviceStatusService).updateBadgeOnlineStatus( eq(TEST_DEVICE_ID), eq(TEST_DEVICE_CODE), - eq(75), eq(TEST_AREA_ID), - eq("A座2楼男卫") - ); - + eq(BadgeDeviceStatusEnum.IDLE), + eq("测试心跳")); + // Mock getBadgeStatus return BadgeDeviceStatusDTO mockStatus = new BadgeDeviceStatusDTO(); mockStatus.setStatus(BadgeDeviceStatusEnum.IDLE); mockStatus.setBatteryLevel(75); mockStatus.setCurrentAreaName("A座2楼男卫"); - + when(badgeDeviceStatusService.getBadgeStatus(TEST_DEVICE_ID)).thenReturn(mockStatus); - + BadgeDeviceStatusDTO status = badgeDeviceStatusService.getBadgeStatus(TEST_DEVICE_ID); assertNotNull(status); assertEquals(BadgeDeviceStatusEnum.IDLE, status.getStatus());