From a71a29f5484bc6c84f397c7648cbf9104471ca70 Mon Sep 17 00:00:00 2001 From: lzh Date: Mon, 19 Jan 2026 14:41:00 +0800 Subject: [PATCH] fix(ops): implement missing updateBadgeArea method --- .../badge/BadgeDeviceStatusServiceImpl.java | 581 ++++++++++++++++++ 1 file changed, 581 insertions(+) create mode 100644 viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/badge/BadgeDeviceStatusServiceImpl.java diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/badge/BadgeDeviceStatusServiceImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/badge/BadgeDeviceStatusServiceImpl.java new file mode 100644 index 0000000..d65ac77 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/core/badge/BadgeDeviceStatusServiceImpl.java @@ -0,0 +1,581 @@ +package com.viewsh.module.ops.core.badge; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO; +import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum; +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; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 工牌设备状态服务实现 + *

+ * 基于 Redis Hash 存储设备状态,Set 维护区域设备索引 + * + * @author lzh + */ +@Slf4j +@Service +public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, InitializingBean { + + @Resource + private RedisTemplate redisTemplate; + + @Resource + private ObjectMapper objectMapper; + + /** + * Redis Key 前缀 + */ + private static final String BADGE_STATUS_KEY_PREFIX = "ops:badge:status:"; + private static final String AREA_BADGES_KEY_PREFIX = "ops:area:badges:"; + + /** + * 心跳超时时间(分钟) + */ + private static final int HEARTBEAT_TIMEOUT_MINUTES = 30; + + /** + * 状态过期时间(小时) + */ + private static final int STATUS_EXPIRE_HOURS = 24; + + @Override + public void afterPropertiesSet() { + // 启动时初始化区域设备索引 + initAreaDeviceIndex(); + } + + // ==================== 状态管理 ==================== + + @Override + public void updateBadgeStatus(Long deviceId, BadgeDeviceStatusEnum status, Long operatorId, String reason) { + if (deviceId == null || status == null) { + return; + } + + try { + String key = BADGE_STATUS_KEY_PREFIX + deviceId; + + // 获取当前状态 + BadgeDeviceStatusDTO currentStatus = getBadgeStatus(deviceId); + + // 验证状态转换 + if (currentStatus != null && currentStatus.getStatus() != null) { + if (!currentStatus.getStatus().canTransitionTo(status)) { + log.warn("非法的状态转换: deviceId={}, from={}, to={}, reason={}", + deviceId, currentStatus.getStatus(), status, reason); + return; + } + } + + // 更新状态 + Map statusMap = new HashMap<>(); + statusMap.put("deviceId", deviceId); + statusMap.put("status", status.getCode()); + statusMap.put("statusChangeTime", LocalDateTime.now().toString()); + + if (currentStatus != null) { + statusMap.put("deviceCode", currentStatus.getDeviceCode()); + statusMap.put("batteryLevel", currentStatus.getBatteryLevel()); + statusMap.put("currentAreaId", currentStatus.getCurrentAreaId()); + statusMap.put("currentAreaName", currentStatus.getCurrentAreaName()); + statusMap.put("currentOpsOrderId", currentStatus.getCurrentOpsOrderId()); + statusMap.put("lastHeartbeatTime", currentStatus.getLastHeartbeatTime()); + } + + redisTemplate.opsForHash().putAll(key, statusMap); + redisTemplate.expire(key, STATUS_EXPIRE_HOURS, TimeUnit.HOURS); + + log.info("更新工牌设备状态: deviceId={}, status={}, operatorId={}, reason={}", + deviceId, status, operatorId, reason); + + } catch (Exception e) { + log.error("更新工牌设备状态失败: deviceId={}, status={}", deviceId, status, e); + } + } + + @Override + public void batchUpdateBadgeStatus(List deviceIds, BadgeDeviceStatusEnum status, Long operatorId, String reason) { + if (deviceIds == null || deviceIds.isEmpty() || status == null) { + return; + } + + for (Long deviceId : deviceIds) { + updateBadgeStatus(deviceId, status, operatorId, reason); + } + + log.info("批量更新工牌设备状态: count={}, status={}, operatorId={}", deviceIds.size(), status, operatorId); + } + + // ==================== 状态查询 ==================== + + @Override + public BadgeDeviceStatusDTO getBadgeStatus(Long deviceId) { + if (deviceId == null) { + return null; + } + + try { + String key = BADGE_STATUS_KEY_PREFIX + deviceId; + Map map = redisTemplate.opsForHash().entries(key); + + if (map.isEmpty()) { + return null; + } + + return mapToDto(map); + + } catch (Exception e) { + log.error("获取工牌设备状态失败: deviceId={}", deviceId, e); + return null; + } + } + + @Override + public List batchGetBadgeStatus(List deviceIds) { + if (deviceIds == null || deviceIds.isEmpty()) { + return Collections.emptyList(); + } + + return deviceIds.stream() + .map(this::getBadgeStatus) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + @Override + public List listBadgesByArea(Long areaId) { + if (areaId == null) { + return Collections.emptyList(); + } + + try { + String areaKey = AREA_BADGES_KEY_PREFIX + areaId; + Set deviceIds = redisTemplate.opsForSet().members(areaKey); + + if (deviceIds == null || deviceIds.isEmpty()) { + return Collections.emptyList(); + } + + return deviceIds.stream() + .map(id -> Long.parseLong(id.toString())) + .map(this::getBadgeStatus) + .filter(Objects::nonNull) + .filter(dto -> dto.getStatus() != null && dto.getStatus().isActive()) + .sorted(Comparator.comparing(BadgeDeviceStatusDTO::getStatusChangeTime)) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("查询区域工牌设备失败: areaId={}", areaId, e); + return Collections.emptyList(); + } + } + + @Override + public List listAvailableBadges(Long areaId) { + if (areaId == null) { + return Collections.emptyList(); + } + + try { + String areaKey = AREA_BADGES_KEY_PREFIX + areaId; + Set deviceIds = redisTemplate.opsForSet().members(areaKey); + + if (deviceIds == null || deviceIds.isEmpty()) { + return Collections.emptyList(); + } + + return deviceIds.stream() + .map(id -> Long.parseLong(id.toString())) + .map(this::getBadgeStatus) + .filter(Objects::nonNull) + .filter(dto -> dto.getStatus() == BadgeDeviceStatusEnum.IDLE) + .sorted(Comparator.comparing(BadgeDeviceStatusDTO::getLastHeartbeatTime).reversed()) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("查询可接单工牌设备失败: areaId={}", areaId, e); + return Collections.emptyList(); + } + } + + @Override + public List listActiveBadges() { + try { + Set keys = redisTemplate.keys(BADGE_STATUS_KEY_PREFIX + "*"); + + if (keys == null || keys.isEmpty()) { + return Collections.emptyList(); + } + + return keys.stream() + .map(key -> { + String deviceIdStr = key.substring(BADGE_STATUS_KEY_PREFIX.length()); + return getBadgeStatus(Long.parseLong(deviceIdStr)); + }) + .filter(Objects::nonNull) + .filter(dto -> dto.getStatus() != null && dto.getStatus().isActive()) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("查询活跃工牌设备失败", e); + return Collections.emptyList(); + } + } + + // ==================== 心跳处理 ==================== + + @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 + BadgeDeviceStatusEnum newStatus = currentStatus; + if (currentStatus == null || currentStatus == BadgeDeviceStatusEnum.OFFLINE) { + newStatus = BadgeDeviceStatusEnum.IDLE; + } + + // 更新状态 + 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")); + + 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); + } + } + + // ==================== 在线状态检查 ==================== + + @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) { + return; + } + + try { + String key = BADGE_STATUS_KEY_PREFIX + deviceId; + redisTemplate.opsForHash().put(key, "currentOpsOrderId", orderId); + log.debug("设置工牌设备当前工单: deviceId={}, orderId={}", deviceId, orderId); + } catch (Exception e) { + log.error("设置工牌设备当前工单失败: deviceId={}, orderId={}", deviceId, orderId, e); + } + } + + @Override + public void clearCurrentOrder(Long deviceId) { + if (deviceId == null) { + return; + } + + try { + String key = BADGE_STATUS_KEY_PREFIX + deviceId; + redisTemplate.opsForHash().delete(key, "currentOpsOrderId"); + log.debug("清除工牌设备当前工单: deviceId={}", deviceId); + } catch (Exception e) { + log.error("清除工牌设备当前工单失败: deviceId={}", deviceId, e); + } + } + + @Override + public List listBadgesWithCurrentOrder() { + try { + Set keys = redisTemplate.keys(BADGE_STATUS_KEY_PREFIX + "*"); + + if (keys == null || keys.isEmpty()) { + return Collections.emptyList(); + } + + return keys.stream() + .map(key -> { + String deviceIdStr = key.substring(BADGE_STATUS_KEY_PREFIX.length()); + return getBadgeStatus(Long.parseLong(deviceIdStr)); + }) + .filter(Objects::nonNull) + .filter(BadgeDeviceStatusDTO::hasCurrentOrder) + .collect(Collectors.toList()); + + } catch (Exception e) { + log.error("查询有工单的工牌设备失败", e); + return Collections.emptyList(); + } + } + + @Override + public void updateBadgeArea(Long deviceId, Long areaId, String areaName) { + if (deviceId == null) { + return; + } + + try { + String key = BADGE_STATUS_KEY_PREFIX + deviceId; + + Map statusMap = new HashMap<>(); + if (areaId != null) { + statusMap.put("currentAreaId", areaId); + } + if (areaName != null) { + statusMap.put("currentAreaName", areaName); + } + + if (!statusMap.isEmpty()) { + redisTemplate.opsForHash().putAll(key, statusMap); + } + + if (areaId != null) { + addToAreaIndex(deviceId, areaId); + } + + log.debug("更新工牌设备区域: deviceId={}, areaId={}, areaName={}", deviceId, areaId, areaName); + } catch (Exception e) { + log.error("更新工牌设备区域失败: deviceId={}", deviceId, e); + } + } + + // ==================== 区域管理 ==================== + + @Override + public void initAreaDeviceIndex() { + log.info("开始初始化区域设备索引..."); + // TODO: 从数据库加载区域设备关系并建立索引 + // 这里需要查询 ops_area_device_relation 表,relation_type = 'BADGE' + // 由于该表在 IoT 模块,暂时留空,后续可以通过 Feign 调用或创建本地 Mapper + log.info("区域设备索引初始化完成"); + } + + @Override + public void refreshAreaDeviceIndex() { + log.info("开始刷新区域设备索引..."); + // TODO: 重新从数据库加载 + initAreaDeviceIndex(); + } + + @Override + public void addToAreaIndex(Long deviceId, Long areaId) { + if (deviceId == null || areaId == null) { + return; + } + + try { + String areaKey = AREA_BADGES_KEY_PREFIX + areaId; + redisTemplate.opsForSet().add(areaKey, deviceId.toString()); + log.debug("添加设备到区域索引: deviceId={}, areaId={}", deviceId, areaId); + } catch (Exception e) { + log.error("添加设备到区域索引失败: deviceId={}, areaId={}", deviceId, areaId, e); + } + } + + @Override + public void removeFromAreaIndex(Long deviceId, Long areaId) { + if (deviceId == null || areaId == null) { + return; + } + + try { + String areaKey = AREA_BADGES_KEY_PREFIX + areaId; + redisTemplate.opsForSet().remove(areaKey, deviceId.toString()); + log.debug("从区域索引移除设备: deviceId={}, areaId={}", deviceId, areaId); + } catch (Exception e) { + log.error("从区域索引移除设备失败: deviceId={}, areaId={}", deviceId, areaId, e); + } + } + + // ==================== 设备管理 ==================== + + @Override + public void deleteBadgeStatus(Long deviceId) { + if (deviceId == null) { + return; + } + + try { + String key = BADGE_STATUS_KEY_PREFIX + deviceId; + redisTemplate.delete(key); + log.info("删除工牌设备状态: deviceId={}", deviceId); + } catch (Exception e) { + log.error("删除工牌设备状态失败: deviceId={}", deviceId, e); + } + } + + @Override + public void clearOfflineBadges() { + try { + Set keys = redisTemplate.keys(BADGE_STATUS_KEY_PREFIX + "*"); + + if (keys == null || keys.isEmpty()) { + return; + } + + int count = 0; + for (String key : keys) { + Map map = redisTemplate.opsForHash().entries(key); + String statusStr = (String) map.get("status"); + if (BadgeDeviceStatusEnum.OFFLINE.getCode().equals(statusStr)) { + redisTemplate.delete(key); + count++; + } + } + + log.info("清理离线设备状态: count={}", count); + + } catch (Exception e) { + log.error("清理离线设备状态失败", e); + } + } + + // ========== 私有方法 ========== + + /** + * 将 Map 转换为 DTO + */ + private BadgeDeviceStatusDTO mapToDto(Map map) { + try { + BadgeDeviceStatusDTO dto = new BadgeDeviceStatusDTO(); + + dto.setDeviceId(getLong(map.get("deviceId"))); + dto.setDeviceCode((String) map.get("deviceCode")); + + String statusStr = (String) map.get("status"); + dto.setStatus(BadgeDeviceStatusEnum.fromCode(statusStr)); + + dto.setBatteryLevel(getInteger(map.get("batteryLevel"))); + dto.setCurrentAreaId(getLong(map.get("currentAreaId"))); + dto.setCurrentAreaName((String) map.get("currentAreaName")); + dto.setCurrentOpsOrderId(getLong(map.get("currentOpsOrderId"))); + dto.setLastHeartbeatTime(getLong(map.get("lastHeartbeatTime"))); + + // 解析时间 + String timeStr = (String) map.get("statusChangeTime"); + if (timeStr != null) { + dto.setStatusChangeTime(LocalDateTime.parse(timeStr)); + } + + return dto; + } catch (Exception e) { + log.error("Map 转 DTO 失败", e); + return null; + } + } + + private Long getLong(Object value) { + if (value == null) { + return null; + } + if (value instanceof Long) { + return (Long) value; + } + try { + return Long.parseLong(value.toString()); + } catch (Exception e) { + return null; + } + } + + private Integer getInteger(Object value) { + if (value == null) { + return null; + } + if (value instanceof Integer) { + return (Integer) value; + } + try { + return Integer.parseInt(value.toString()); + } catch (Exception e) { + return null; + } + } +}