refactor(ops): 移除 ops-core 中的 BadgeDeviceStatusService

将 BadgeDeviceStatusService 从 ops-core 模块迁移至 environment 模块:
- 删除 ops-biz/core/badge/BadgeDeviceStatusService.java
- 删除 ops-biz/core/badge/BadgeDeviceStatusServiceImpl.java

该服务属于保洁业务特定逻辑,应放置在 environment-biz 模块中。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-01-20 16:16:06 +08:00
parent a83e827919
commit 6d4c2c60cf
2 changed files with 0 additions and 810 deletions

View File

@@ -1,229 +0,0 @@
package com.viewsh.module.ops.core.badge;
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum;
import java.util.List;
/**
* 工牌设备状态服务接口
* <p>
* 职责:
* 1. 管理工牌设备状态Redis 存储和查询)
* 2. 处理设备心跳更新
* 3. 状态转换IDLE ↔ BUSY ↔ PAUSED ↔ OFFLINE
* 4. 区域设备查询
* 5. 当前任务关联
* <p>
* 设计原则:
* - 状态存储在 Redis 中,不依赖数据库表
* - 与保洁员状态解耦,只关注设备本身
* - 为调度引擎提供设备状态查询能力
*
* @author lzh
*/
public interface BadgeDeviceStatusService {
// ==================== 状态管理 ====================
/**
* 更新工牌设备状态
* <p>
* 状态转换会记录状态变更时间和操作原因
*
* @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<Long> deviceIds, BadgeDeviceStatusEnum status, Long operatorId, String reason);
// ==================== 状态查询 ====================
/**
* 获取工牌设备状态
*
* @param deviceId 设备ID
* @return 设备状态DTO不存在返回null
*/
BadgeDeviceStatusDTO getBadgeStatus(Long deviceId);
/**
* 批量获取工牌设备状态
*
* @param deviceIds 设备ID列表
* @return 设备状态DTO列表
*/
List<BadgeDeviceStatusDTO> batchGetBadgeStatus(List<Long> deviceIds);
/**
* 获取指定区域的工牌设备列表
* <p>
* 只返回非 OFFLINE 状态的设备
*
* @param areaId 区域ID
* @return 设备状态DTO列表
*/
List<BadgeDeviceStatusDTO> listBadgesByArea(Long areaId);
/**
* 获取可接单的工牌设备IDLE 状态)
*
* @param areaId 区域ID
* @return 可接单设备列表
*/
List<BadgeDeviceStatusDTO> listAvailableBadges(Long areaId);
/**
* 获取所有活跃的工牌设备非OFFLINE状态
*
* @return 活跃设备列表
*/
List<BadgeDeviceStatusDTO> listActiveBadges();
// ==================== 心跳处理 ====================
/**
* 处理工牌设备心跳
* <p>
* 更新最后心跳时间和电量:
* <ul>
* <li>如果设备之前为 OFFLINE转为 IDLE</li>
* <li>如果设备已存在,更新心跳时间和电量</li>
* <li>如果设备不存在,创建新记录(状态为 IDLE</li>
* </ul>
*
* @param deviceId 设备ID
* @param deviceCode 设备编码
* @param batteryLevel 电量0-100
*/
void handleHeartbeat(Long deviceId, String deviceCode, Integer batteryLevel);
/**
* 处理工牌设备心跳(带区域信息)
*
* @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);
// ==================== 在线状态检查 ====================
/**
* 检查工牌设备是否在线非OFFLINE状态
*
* @param deviceId 设备ID
* @return 是否在线
*/
boolean isBadgeOnline(Long deviceId);
/**
* 检查工牌设备心跳是否超时
*
* @param deviceId 设备ID
* @param thresholdMinutes 超时阈值(分钟)
* @return 是否超时
*/
boolean isHeartbeatTimeout(Long deviceId, int thresholdMinutes);
/**
* 检查心跳超时并将超时设备设为OFFLINE
* <p>
* 定时任务调用默认超时时间为30分钟
*/
void checkAndMarkOfflineDevices();
// ==================== 工单关联 ====================
/**
* 设置当前工单
*
* @param deviceId 设备ID
* @param orderId 工单ID
*/
void setCurrentOrder(Long deviceId, Long orderId);
/**
* 清除当前工单
*
* @param deviceId 设备ID
*/
void clearCurrentOrder(Long deviceId);
/**
* 获取当前有工单的设备列表
*
* @return 有工单的设备列表
*/
List<BadgeDeviceStatusDTO> listBadgesWithCurrentOrder();
// ==================== 区域管理 ====================
/**
* 更新工牌设备所在区域
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @param areaName 区域名称
*/
void updateBadgeArea(Long deviceId, Long areaId, String areaName);
/**
* 初始化区域设备索引
* <p>
* 从 ops_area_device_relation 表加载 BADGE 类型的设备,
* 建立区域到设备的索引关系
*/
void initAreaDeviceIndex();
/**
* 刷新区域设备索引
* <p>
* 重新从数据库加载区域设备关系
*/
void refreshAreaDeviceIndex();
/**
* 将设备添加到区域索引
*
* @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();
}

View File

@@ -1,581 +0,0 @@
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;
}
}
}