fix(ops): implement missing updateBadgeArea method

This commit is contained in:
lzh
2026-01-19 14:41:00 +08:00
parent 4f2036d145
commit a71a29f548

View File

@@ -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;
/**
* 工牌设备状态服务实现
* <p>
* 基于 Redis Hash 存储设备状态Set 维护区域设备索引
*
* @author lzh
*/
@Slf4j
@Service
public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, InitializingBean {
@Resource
private RedisTemplate<String, Object> 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<String, Object> 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<Long> 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<Object, Object> 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<BadgeDeviceStatusDTO> batchGetBadgeStatus(List<Long> deviceIds) {
if (deviceIds == null || deviceIds.isEmpty()) {
return Collections.emptyList();
}
return deviceIds.stream()
.map(this::getBadgeStatus)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@Override
public List<BadgeDeviceStatusDTO> listBadgesByArea(Long areaId) {
if (areaId == null) {
return Collections.emptyList();
}
try {
String areaKey = AREA_BADGES_KEY_PREFIX + areaId;
Set<Object> 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<BadgeDeviceStatusDTO> listAvailableBadges(Long areaId) {
if (areaId == null) {
return Collections.emptyList();
}
try {
String areaKey = AREA_BADGES_KEY_PREFIX + areaId;
Set<Object> 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<BadgeDeviceStatusDTO> listActiveBadges() {
try {
Set<String> 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<Object, Object> 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<String, Object> 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<BadgeDeviceStatusDTO> activeBadges = listActiveBadges();
long thresholdMillis = System.currentTimeMillis() - (HEARTBEAT_TIMEOUT_MINUTES * 60L * 1000L);
List<Long> 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<BadgeDeviceStatusDTO> listBadgesWithCurrentOrder() {
try {
Set<String> 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<String, Object> 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<String> keys = redisTemplate.keys(BADGE_STATUS_KEY_PREFIX + "*");
if (keys == null || keys.isEmpty()) {
return;
}
int count = 0;
for (String key : keys) {
Map<Object, Object> 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<Object, Object> 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;
}
}
}