refactor(ops): 简化AreaDevice与BadgeDevice服务实现

- AreaDeviceService: 移���设备索引缓存逻辑(由IoT模块管理)
- AreaDeviceServiceImpl: 简化实现,直接查询数据库
- BadgeDeviceStatusService: 更新接口方法签名
- BadgeDeviceStatusServiceImpl: 修复语法错误,简化实现
- BadgeDeviceStatusEventListener: 适配事件处理逻辑
- CleanOrderEventListener: 更新事件处理

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-02-01 00:57:38 +08:00
parent 3839da2966
commit 5d8c4045d4
6 changed files with 327 additions and 491 deletions

View File

@@ -8,9 +8,6 @@ import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
@@ -25,6 +22,7 @@ import org.springframework.stereotype.Component;
* - 使用 @EventListener 监听事件,在事务内同步执行
* - 确保 IoT 模块能实时查询到正确的设备工单信息
* - 只处理保洁类型的工单
* - 不记录业务日志,由 CleanOrderEventListener 统一处理
* <p>
* 状态处理:
* <table>
@@ -41,12 +39,12 @@ import org.springframework.stereotype.Component;
* <tr>
* <td>CONFIRMED</td>
* <td>保持BUSY</td>
* <td>设置</td>
* <td>更新状态</td>
* </tr>
* <tr>
* <td>ARRIVED</td>
* <td>保持BUSY</td>
* <td>设置完整信息</td>
* <td>更新状态+信标</td>
* </tr>
* <tr>
* <td>PAUSED</td>
@@ -80,9 +78,6 @@ public class BadgeDeviceStatusEventListener {
@Resource
private OrderQueueService orderQueueService;
@Resource
private EventLogRecorder eventLogRecorder;
/**
* 监听工单状态变更事件,同步更新设备工单关联
* <p>
@@ -117,7 +112,7 @@ public class BadgeDeviceStatusEventListener {
}
// 根据状态更新设备工单关联
handleOrderStatusTransition(deviceId, orderId, newStatus, event);
handleOrderStatusTransition(deviceId, orderId, newStatus, event, order);
} catch (Exception e) {
log.error("[BadgeDeviceStatusEventListener] 处理工单状态变更事件失败: orderId={}", event.getOrderId(), e);
@@ -126,177 +121,154 @@ public class BadgeDeviceStatusEventListener {
/**
* 根据工单状态更新设备工牌关联
* <p>
* 设计原则:每个状态只更新变化的部分,避免重复设置相同数据
* <p>
* 状态处理逻辑:
* <ul>
* <li>DISPATCHED: 首次设置工单关联orderId + areaId</li>
* <li>CONFIRMED: 只更新状态orderId/areaId 已在 DISPATCHED 设置</li>
* <li>ARRIVED: 更新状态 + 信标MAC</li>
* <li>PAUSED: 只更新设备状态,工单关联保持</li>
* <li>COMPLETED: 清除工单关联,根据等待任务决定设备状态</li>
* <li>CANCELLED: 清除工单关联(如果是当前工单),根据等待任务决定设备状态</li>
* </ul>
*/
private void handleOrderStatusTransition(Long deviceId, Long orderId, WorkOrderStatusEnum newStatus,
OrderStateChangedEvent event) {
var waitingTasks = orderQueueService.getWaitingTasksByUserIdFromDb(deviceId);
OrderStateChangedEvent event, OpsOrderDO order) {
switch (newStatus) {
case DISPATCHED:
// 工单已推送到工牌,设置工单关联和区域信息
// currentAreaId 应该设置为工单所属区域,而非设备物理位置
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY, null, "新工单已推送");
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order != null && order.getAreaId() != null) {
badgeDeviceStatusService.setCurrentOrderInfo(
deviceId, orderId,
event.getNewStatus().getStatus(),
order.getAreaId(),
null
);
log.info("[BadgeDeviceStatusEventListener] 工单已推送,设备状态转为 BUSY: deviceId={}, orderId={}, areaId={}",
deviceId, orderId, order.getAreaId());
} else {
badgeDeviceStatusService.setCurrentOrder(deviceId, orderId);
log.info("[BadgeDeviceStatusEventListener] 工单已推送,设备状态转为 BUSY: deviceId={}, orderId={}", deviceId,
orderId);
}
handleDispatched(deviceId, orderId, order);
break;
case CONFIRMED:
// 设备按键确认,更新工单关联和区域信息
// currentAreaId 应该设置为工单所属区域,而非设备物理位置
OpsOrderDO order = opsOrderMapper.selectById(orderId);
if (order != null && order.getAreaId() != null) {
badgeDeviceStatusService.setCurrentOrderInfo(
deviceId, orderId,
event.getNewStatus().getStatus(),
order.getAreaId(),
null
);
log.debug("[BadgeDeviceStatusEventListener] 工单已确认,更新区域信息: deviceId={}, orderId={}, areaId={}",
deviceId, orderId, order.getAreaId());
} else {
badgeDeviceStatusService.setCurrentOrder(deviceId, orderId);
log.debug("[BadgeDeviceStatusEventListener] 工单已确认: deviceId={}, orderId={}", deviceId, orderId);
}
handleConfirmed(deviceId, orderId);
break;
case ARRIVED:
// 设备已到岗设置完整的工单信息areaId, beaconMac
updateDeviceOrderInfo(deviceId, orderId, event);
recordOrderArrivedLog(orderId, deviceId, event);
log.info("[BadgeDeviceStatusEventListener] 工单已到岗,更新设备工单信息: deviceId={}, orderId={}", deviceId, orderId);
handleArrived(deviceId, orderId, event);
break;
case PAUSED:
// 检查是否是 P0 打断场景
Long urgentOrderId = event.getPayloadLong("urgentOrderId");
if (urgentOrderId != null) {
// P0 打断场景:不修改设备状态,保持当前状态
// 紧接着会有 P0 工单的 DISPATCHED 事件,将当前工单更新为 P0 工单
log.info(
"[BadgeDeviceStatusEventListener] P0打断场景工单已暂停等待P0工单派发: pausedOrderId={}, urgentOrderId={}, deviceId={}",
orderId, urgentOrderId, deviceId);
} else {
// 普通暂停场景:设备状态转为 PAUSED
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.PAUSED, null, "任务暂停");
log.info("[BadgeDeviceStatusEventListener] 任务暂停,设备状态转为 PAUSED: deviceId={}", deviceId);
}
handlePaused(deviceId, orderId, event);
break;
case COMPLETED:
// 任务完成,清除工单关联
badgeDeviceStatusService.clearCurrentOrder(deviceId);
// 检查是否有等待任务,决定设备状态
if (waitingTasks != null && !waitingTasks.isEmpty()) {
// 有等待任务,设备状态保持 BUSY由后续 DISPATCHED 事件更新)
log.info("[BadgeDeviceStatusEventListener] 任务完成,有{}个等待任务,设备保持 BUSY: deviceId={}",
waitingTasks.size(), deviceId);
} else {
// 无等待任务,设备状态转为 IDLE
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.IDLE, null, "任务完成,无新任务");
log.info("[BadgeDeviceStatusEventListener] 任务完成,无等待任务,设备转为 IDLE: deviceId={}", deviceId);
}
handleCompleted(deviceId);
break;
case CANCELLED:
// 检查被取消的工单是否是设备当前正在执行的工单
BadgeDeviceStatusDTO deviceStatus = badgeDeviceStatusService.getBadgeStatus(deviceId);
Long currentOrderId = deviceStatus != null ? deviceStatus.getCurrentOpsOrderId() : null;
if (orderId.equals(currentOrderId)) {
// 取消的是当前正在执行的工单,清除工单关联
badgeDeviceStatusService.clearCurrentOrder(deviceId);
// 检查是否有等待任务,决定设备状态
if (waitingTasks != null && !waitingTasks.isEmpty()) {
// 有等待任务,设备状态保持 BUSY由后续 DISPATCHED 事件更新)
log.info("[BadgeDeviceStatusEventListener] 当前工单已取消,有{}个等待任务,设备保持 BUSY: deviceId={}",
waitingTasks.size(), deviceId);
} else {
// 无等待任务,设备状态转为 IDLE
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.IDLE, null, "工单已取消");
log.info("[BadgeDeviceStatusEventListener] 当前工单已取消,无等待任务,设备转为 IDLE: deviceId={}", deviceId);
}
} else {
// 取消的不是当前工单(可能是队列中的等待任务),不需要修改设备状态
log.debug(
"[BadgeDeviceStatusEventListener] 取消的工单非当前执行工单,跳过设备状态更新: cancelledOrderId={}, currentOrderId={}, deviceId={}",
orderId, currentOrderId, deviceId);
}
handleCancelled(deviceId, orderId);
break;
default:
// 其他状态不处理
break;
}
}
/**
* 更新设备工单信息ARRIVED 状态专用
* <p>
* 设置完整的工单信息工单ID、工单状态、区域ID、信标MAC
* 处理工单推送状态(首次设置工单关联
*/
private void updateDeviceOrderInfo(Long deviceId, Long orderId, OrderStateChangedEvent event) {
try {
// 从 payload 中提取信息
Long areaId = event.getPayloadLong("areaId");
private void handleDispatched(Long deviceId, Long orderId, OpsOrderDO order) {
// 设备状态转为 BUSY
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.BUSY, null, "新工单已推送");
String beaconMac = null;
Object beaconMacObj = event.getPayload().get("beaconMac");
if (beaconMacObj != null) {
beaconMac = String.valueOf(beaconMacObj);
}
// 使用 BadgeDeviceStatusService 设置完整工单信息
// 首次设置工单关联orderId + status + areaId
if (order != null && order.getAreaId() != null) {
badgeDeviceStatusService.setCurrentOrderInfo(
deviceId,
orderId,
event.getNewStatus().getStatus(),
areaId,
beaconMac);
log.debug("[BadgeDeviceStatusEventListener] 设备工单信息已更新: deviceId={}, orderId={}, areaId={}, beaconMac={}",
deviceId, orderId, areaId, beaconMac);
} catch (Exception e) {
log.error("[BadgeDeviceStatusEventListener] 更新设备工单信息失败: deviceId={}, orderId={}", deviceId, orderId, e);
deviceId, orderId,
WorkOrderStatusEnum.DISPATCHED.getStatus(),
order.getAreaId(),
null
);
log.info("[BadgeDeviceStatusEventListener] 工单已推送: deviceId={}, orderId={}, areaId={}",
deviceId, orderId, order.getAreaId());
} else {
badgeDeviceStatusService.setCurrentOrder(deviceId, orderId);
log.info("[BadgeDeviceStatusEventListener] 工单已推送(无区域信息): deviceId={}, orderId={}", deviceId, orderId);
}
}
/**
* 记录工单到岗业务日志
* 处理工单确认状态(只更新状态,不重复设置 orderId/areaId
*/
private void recordOrderArrivedLog(Long orderId, Long deviceId, OrderStateChangedEvent event) {
try {
Long areaId = event.getPayloadLong("areaId");
String deviceKey = (String) event.getPayload().get("deviceKey");
private void handleConfirmed(Long deviceId, Long orderId) {
// 只更新工单状态orderId/areaId 已在 DISPATCHED 设置
badgeDeviceStatusService.updateOrderStatus(deviceId, WorkOrderStatusEnum.CONFIRMED.getStatus());
log.debug("[BadgeDeviceStatusEventListener] 工单已确认: deviceId={}, orderId={}", deviceId, orderId);
}
eventLogRecorder.record(EventLogRecord.builder()
.module("clean")
.domain(EventDomain.BEACON)
.eventType("ORDER_ARRIVED")
.message(String.format("蓝牙信标自动到岗确认 [设备:%s, 区域:%d]", deviceKey, areaId))
.targetId(orderId)
.targetType("order")
.deviceId(deviceId)
.personId(deviceId)
.build());
/**
* 处理工单到岗状态(更新状态 + 信标MAC
*/
private void handleArrived(Long deviceId, Long orderId, OrderStateChangedEvent event) {
// 从 payload 中提取信标信息
String beaconMac = (String) event.getPayload().get("beaconMac");
} catch (Exception e) {
log.warn("[BadgeDeviceStatusEventListener] 记录到岗业务日志失败: orderId={}", orderId, e);
// 更新状态和信标MACorderId/areaId 已设置)
badgeDeviceStatusService.updateOrderStatusAndBeacon(deviceId, orderId,
WorkOrderStatusEnum.ARRIVED.getStatus(), beaconMac);
log.debug("[BadgeDeviceStatusEventListener] 工单已到岗,更新信标: deviceId={}, orderId={}, beaconMac={}",
deviceId, orderId, beaconMac);
}
/**
* 处理工单暂停状态
*/
private void handlePaused(Long deviceId, Long orderId, OrderStateChangedEvent event) {
Long urgentOrderId = event.getPayloadLong("urgentOrderId");
if (urgentOrderId != null) {
// P0 打断场景:不修改设备状态,等待 P0 工单的 DISPATCHED 事件
log.info("[BadgeDeviceStatusEventListener] P0打断场景等待P0工单派发: pausedOrderId={}, urgentOrderId={}",
orderId, urgentOrderId);
} else {
// 普通暂停场景:设备状态转为 PAUSED
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.PAUSED, null, "任务暂停");
log.info("[BadgeDeviceStatusEventListener] 任务暂停: deviceId={}", deviceId);
}
}
/**
* 处理工单完成状态(清除工单关联)
*/
private void handleCompleted(Long deviceId) {
badgeDeviceStatusService.clearCurrentOrder(deviceId);
// 检查是否有等待任务,决定设备状态
var waitingTasks = orderQueueService.getWaitingTasksByUserIdFromDb(deviceId);
if (waitingTasks != null && !waitingTasks.isEmpty()) {
log.info("[BadgeDeviceStatusEventListener] 任务完成,有{}个等待任务,设备保持 BUSY: deviceId={}",
waitingTasks.size(), deviceId);
} else {
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.IDLE, null, "任务完成,无新任务");
log.info("[BadgeDeviceStatusEventListener] 任务完成,无等待任务,设备转为 IDLE: deviceId={}", deviceId);
}
}
/**
* 处理工单取消状态
*/
private void handleCancelled(Long deviceId, Long orderId) {
// 检查被取消的工单是否是设备当前正在执行的工单
BadgeDeviceStatusDTO deviceStatus = badgeDeviceStatusService.getBadgeStatus(deviceId);
Long currentOrderId = deviceStatus != null ? deviceStatus.getCurrentOpsOrderId() : null;
if (orderId.equals(currentOrderId)) {
badgeDeviceStatusService.clearCurrentOrder(deviceId);
// 检查是否有等待任务,决定设备状态
var waitingTasks = orderQueueService.getWaitingTasksByUserIdFromDb(deviceId);
if (waitingTasks != null && !waitingTasks.isEmpty()) {
log.info("[BadgeDeviceStatusEventListener] 当前工单已取消,有{}个等待任务,设备保持 BUSY: deviceId={}",
waitingTasks.size(), deviceId);
} else {
badgeDeviceStatusService.updateBadgeStatus(deviceId, BadgeDeviceStatusEnum.IDLE, null, "工单已取消");
log.info("[BadgeDeviceStatusEventListener] 当前工单已取消,无等待任务,设备转为 IDLE: deviceId={}", deviceId);
}
} else {
log.debug("[BadgeDeviceStatusEventListener] 取消的工单非当前执行工单,跳过: cancelledOrderId={}, currentOrderId={}",
orderId, currentOrderId);
}
}
}

View File

@@ -359,6 +359,9 @@ public class CleanOrderEventListener {
playVoice(deviceId, arrivedMessage);
}
// 4. 记录到岗业务日志
recordOrderArrivedLog(orderId, deviceId, event);
log.info("[CleanOrderEventListener] 到岗时间已记录: orderId={}, deviceId={}", orderId, deviceId);
}
@@ -787,6 +790,32 @@ public class CleanOrderEventListener {
}
}
/**
* 记录工单到岗业务日志
*/
private void recordOrderArrivedLog(Long orderId, Long deviceId, OrderStateChangedEvent event) {
try {
Long areaId = event.getPayloadLong("areaId");
String deviceKey = (String) event.getPayload().get("deviceKey");
String beaconMac = (String) event.getPayload().get("beaconMac");
eventLogRecorder.record(EventLogRecord.builder()
.module("clean")
.domain(EventDomain.BEACON)
.eventType("ORDER_ARRIVED")
.message(String.format("蓝牙信标自动到岗确认 [设备:%s, 区域:%d, 信标:%s]",
deviceKey, areaId, beaconMac))
.targetId(orderId)
.targetType("order")
.deviceId(deviceId)
.personId(deviceId)
.build());
} catch (Exception e) {
log.warn("[CleanOrderEventListener] 记录到岗业务日志失败: orderId={}", orderId, e);
}
}
/**
* 记录工单完成业务日志
*/

View File

@@ -153,6 +153,18 @@ public interface BadgeDeviceStatusService {
*/
void updateOrderStatus(Long deviceId, String orderStatus);
/**
* 更新工单状态和信标MAC
* <p>
* 用于到岗场景同时更新工单状态和信标MAC地址
*
* @param deviceId 设备ID
* @param orderId 工单ID
* @param orderStatus 工单状态
* @param beaconMac 信标MAC地址可为null
*/
void updateOrderStatusAndBeacon(Long deviceId, Long orderId, String orderStatus, String beaconMac);
/**
* 清除当前工单包括工单ID、工单状态、信标MAC
*
@@ -178,37 +190,6 @@ public interface BadgeDeviceStatusService {
*/
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);
// ==================== 设备管理 ====================
/**

View File

@@ -53,8 +53,8 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
@Override
public void afterPropertiesSet() {
// 启动时初始化区域设备索引
initAreaDeviceIndex();
// 启动时初始化区域设备配置缓存
areaDeviceService.initConfigCache();
}
// ==================== 状态管理 ====================
@@ -163,13 +163,14 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
}
try {
Set<Long> deviceIds = areaDeviceService.getDeviceIdsByArea(areaId);
// 直接从DB查询该区域的工牌设备ID列表
List<Long> deviceIds = areaDeviceService.getDeviceIdsByAreaAndType(areaId, "BADGE");
if (deviceIds.isEmpty()) {
return Collections.emptyList();
}
// 使用批量查询,避免 N+1
return batchGetBadgeStatus(new ArrayList<>(deviceIds)).stream()
// 批量查询设备状态,避免 N+1
return batchGetBadgeStatus(deviceIds).stream()
.filter(Objects::nonNull)
.filter(dto -> dto.getStatus() != null && dto.getStatus().isActive())
.sorted(Comparator.comparing(BadgeDeviceStatusDTO::getStatusChangeTime))
@@ -188,14 +189,14 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
}
try {
// 使用读穿透方式获取设备ID列表缓存未命中时会从数据库重建
Set<Long> deviceIds = areaDeviceService.getDeviceIdsByArea(areaId);
// 直接从DB查询该区域的工牌设备ID列表
List<Long> deviceIds = areaDeviceService.getDeviceIdsByAreaAndType(areaId, "BADGE");
if (deviceIds.isEmpty()) {
return Collections.emptyList();
}
// 使用批量查询,避免 N+1
return batchGetBadgeStatus(new ArrayList<>(deviceIds)).stream()
// 批量查询设备状态,避免 N+1
return batchGetBadgeStatus(deviceIds).stream()
.filter(Objects::nonNull)
.filter(dto -> dto.getStatus() == BadgeDeviceStatusEnum.IDLE)
.sorted(Comparator.comparing(BadgeDeviceStatusDTO::getLastHeartbeatTime).reversed())
@@ -387,6 +388,26 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
}
}
@Override
public void updateOrderStatusAndBeacon(Long deviceId, Long orderId, String orderStatus, String beaconMac) {
if (deviceId == null) {
return;
}
try {
String key = BADGE_STATUS_KEY_PREFIX + deviceId;
// 更新工单状态和信标MACorderId 和 areaId 已在 DISPATCHED 时设置,不需要重复)
redisTemplate.opsForHash().put(key, "currentOrderStatus", orderStatus);
if (beaconMac != null) {
redisTemplate.opsForHash().put(key, "beaconMac", beaconMac);
}
log.debug("更新工单状态和信标: deviceId={}, orderId={}, orderStatus={}, beaconMac={}",
deviceId, orderId, orderStatus, beaconMac);
} catch (Exception e) {
log.error("更新工单状态和信标失败: deviceId={}, orderId={}", deviceId, orderId, e);
}
}
@Override
public void clearCurrentOrder(Long deviceId) {
if (deviceId == null) {
@@ -448,44 +469,12 @@ public class BadgeDeviceStatusServiceImpl implements BadgeDeviceStatusService, I
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("[BadgeDeviceStatusService] 开始初始化区域设备索引...");
// 委托给 AreaDeviceService 处理
areaDeviceService.initAreaDeviceIndex();
log.info("[BadgeDeviceStatusService] 区域设备索引初始化完成");
}
@Override
public void refreshAreaDeviceIndex() {
log.info("[BadgeDeviceStatusService] 开始刷新区域设备索引...");
// 委托给 AreaDeviceService 处理
areaDeviceService.refreshAreaDeviceIndex();
log.info("[BadgeDeviceStatusService] 区域设备索引刷新完成");
}
@Override
public void addToAreaIndex(Long deviceId, Long areaId) {
areaDeviceService.addToAreaIndex(deviceId, areaId);
}
@Override
public void removeFromAreaIndex(Long deviceId, Long areaId) {
areaDeviceService.removeFromAreaIndex(deviceId, areaId);
}
// ==================== 设备管理 ====================
@Override

View File

@@ -8,7 +8,6 @@ import java.util.List;
* 区域设备关联服务
* <p>
* 职责:管理运营区域与 IoT 设备的关联关系
* 提供缓存能力,支持快速查询
*
* @author lzh
*/
@@ -24,9 +23,17 @@ public interface AreaDeviceService {
List<OpsAreaDeviceRelationDO> listByAreaIdAndType(Long areaId, String relationType);
/**
* 根据区域ID和关联类型查询单个启用的关联关系
* 根据区域ID查询所有关联关系
*
* @param areaId 区域ID
* @return 关联关系列表
*/
List<OpsAreaDeviceRelationDO> listByAreaId(Long areaId);
/**
* 根据区域ID和关联类型查询单个启用的关联关系带缓存
* <p>
* 缓存,返回第一个启用的配置
* 缓存Key: ops:area:{areaId}:type:{relationType}
*
* @param areaId 区域ID
* @param relationType 关联类型
@@ -34,16 +41,10 @@ public interface AreaDeviceService {
*/
OpsAreaDeviceRelationDO getByAreaIdAndType(Long areaId, String relationType);
/**
* 根据区域ID查询所有启用的关联关系
*
* @param areaId 区域ID
* @return 关联关系列表
*/
List<OpsAreaDeviceRelationDO> listByAreaId(Long areaId);
/**
* 根据设备ID查询关联关系
* <p>
* 注意:如果设备关联多个区域,只返回第一个
*
* @param deviceId 设备ID
* @return 关联关系,不存在返回 null
@@ -51,54 +52,53 @@ public interface AreaDeviceService {
OpsAreaDeviceRelationDO getByDeviceId(Long deviceId);
/**
* 初始化区域设备索引
* 获取设备关联的所有区域ID
* <p>
* 从数据库加载所有区域设备关系,建立 Redis 索引
* 从数据库直接查询(无缓存)
*
* @param deviceId 设备ID
* @return 区域ID集合
*/
void initAreaDeviceIndex();
List<Long> getAreaIdsByDevice(Long deviceId);
/**
* 刷新区域设备索引
* 获取区域关联的所有设备ID
* <p>
* 重新从数据库加载并建立索引
*/
void refreshAreaDeviceIndex();
/**
* 获取区域下的设备ID列表带读穿透缓存
* 从数据库直接查询(无缓存)
*
* @param areaId 区域ID
* @return 设备ID集合
* @return 设备ID列表
*/
java.util.Set<Long> getDeviceIdsByArea(Long areaId);
List<Long> getDeviceIdsByArea(Long areaId);
/**
* 添加设备到区域索引
* 获取区域关联的指定类型设备ID
* <p>
* 从数据库直接查询(无缓存)
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @param areaId 区域ID
* @param relationType 关联类型
* @return 设备ID列表
*/
void addToAreaIndex(Long deviceId, Long areaId);
List<Long> getDeviceIdsByAreaAndType(Long areaId, String relationType);
/**
* 从区域索引移除设备
*
* @param deviceId 设备ID
* @param areaId 区域ID
* 初始化区域设备配置缓存
* <p>
* 预热 ops:area:{areaId}:type:{relationType} 缓存
*/
void removeFromAreaIndex(Long deviceId, Long areaId);
void initConfigCache();
/**
* 清除区域缓存
*
* @param areaId 区域ID
* 刷新区域设备配置缓存
*/
void evictAreaCache(Long areaId);
void refreshConfigCache();
/**
* 清除设备缓存
* 清除区域配置缓存
*
* @param deviceId 设备ID
* @param areaId 区域ID
* @param relationType 关联类型
*/
void evictDeviceCache(Long deviceId);
void evictConfigCache(Long areaId, String relationType);
}

View File

@@ -7,22 +7,20 @@ import com.viewsh.module.ops.dal.mysql.area.OpsAreaDeviceRelationMapper;
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.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 区域设备关联服务实现
* <p>
* 职责
* 1. 管理运营区域与 IoT 设备的关联关系
* 2. 维护 Redis 缓存索引
* 3. 提供快速查询能力
* 缓存设计
* - 唯一缓存Key: ops:area:{areaId}:type:{relationType}
* - 用途: 存储区域+类型的设备配置1对1绑定设备
* - N对N设备如工牌: 先查DB获取关联区域再用此Key获取配置
*
* @author lzh
*/
@@ -33,30 +31,15 @@ public class AreaDeviceServiceImpl implements AreaDeviceService, InitializingBea
@Resource
private OpsAreaDeviceRelationMapper relationMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* Redis Key 前缀
*/
private static final String AREA_BADGES_KEY_PREFIX = "ops:area:badges:";
/**
* 设备配置缓存 Key 前缀JSON 格式IoT 可读)
* 缓存Key前缀: ops:area:{areaId}:type:{relationType}
* <p>
* 格式: ops:area:device:{deviceId}
* 唯一的区域设备配置缓存,适用于所有设备类型
*/
private static final String DEVICE_CACHE_KEY_PREFIX = "ops:area:device:";
/**
* 区域+类型配置缓存 Key 前缀JSON 格式IoT 可读)
* <p>
* 格式: ops:area:{areaId}:type:{relationType}
*/
private static final String AREA_TYPE_CACHE_KEY_PREFIX = "ops:area:%s:type:%s";
private static final String CONFIG_CACHE_KEY_PREFIX = "ops:area:%s:type:%s";
/**
* 空值缓存标记
@@ -64,26 +47,22 @@ public class AreaDeviceServiceImpl implements AreaDeviceService, InitializingBea
private static final String NULL_CACHE = "NULL";
/**
* 缓存 TTL24 小时)
* 区域设备关系相对静态,可以设置较长过期时间
* 缓存TTL24小时
*/
private static final int CACHE_TTL_HOURS = 24;
@Override
public void afterPropertiesSet() {
// 启动时初始化区域设备索引
initAreaDeviceIndex();
initConfigCache();
}
@Override
public List<OpsAreaDeviceRelationDO> listByAreaIdAndType(Long areaId, String relationType) {
log.debug("[AreaDevice] 查询区域设备关联areaId={}, relationType={}", areaId, relationType);
return relationMapper.selectListByAreaIdAndRelationType(areaId, relationType);
}
@Override
public List<OpsAreaDeviceRelationDO> listByAreaId(Long areaId) {
log.debug("[AreaDevice] 查询区域所有设备areaId={}", areaId);
return relationMapper.selectListByAreaId(areaId);
}
@@ -93,33 +72,31 @@ public class AreaDeviceServiceImpl implements AreaDeviceService, InitializingBea
return null;
}
// 先从缓存读取(缓存存储的是 AreaDeviceDTO JSON
String cacheKey = String.format(AREA_TYPE_CACHE_KEY_PREFIX, areaId, relationType);
String cacheKey = String.format(CONFIG_CACHE_KEY_PREFIX, areaId, relationType);
// 1. 先读缓存
try {
String cached = stringRedisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if (NULL_CACHE.equals(cached)) {
return null;
}
log.debug("[AreaDevice] 命中区域类型配置缓存areaId={}, type={}", areaId, relationType);
log.debug("[AreaDevice] 命中配置缓存areaId={}, type={}", areaId, relationType);
AreaDeviceDTO dto = JsonUtils.parseObject(cached, AreaDeviceDTO.class);
return toAreaDeviceRelationDO(dto);
}
} catch (Exception e) {
log.warn("[AreaDevice] 读取区域类型配置缓存失败areaId={}, type={}", areaId, relationType, e);
log.warn("[AreaDevice] 读取配置缓存失败areaId={}, type={}", areaId, relationType, e);
}
// 数据库查询
List<OpsAreaDeviceRelationDO> relations = relationMapper.selectListByAreaIdAndRelationType(areaId,
relationType);
// 返回第一个启用的
// 2. 缓存未命中,查数据库
List<OpsAreaDeviceRelationDO> relations = relationMapper.selectListByAreaIdAndRelationType(areaId, relationType);
OpsAreaDeviceRelationDO relation = relations.stream()
.filter(r -> r.getEnabled())
.findFirst()
.orElse(null);
// 写入 JSON 缓存IoT 可读,存储 AreaDeviceDTO
// 3. 写入缓存
try {
if (relation != null) {
AreaDeviceDTO dto = toAreaDeviceDTO(relation);
@@ -129,11 +106,10 @@ public class AreaDeviceServiceImpl implements AreaDeviceService, InitializingBea
CACHE_TTL_HOURS,
TimeUnit.HOURS);
} else {
// 空值缓存,防止穿透
stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE, 1, TimeUnit.MINUTES);
}
} catch (Exception e) {
log.warn("[AreaDevice] 写入区域类型配置缓存失败areaId={}, type={}", areaId, relationType, e);
log.warn("[AreaDevice] 写入配置缓存失败areaId={}, type={}", areaId, relationType, e);
}
return relation;
@@ -144,219 +120,108 @@ public class AreaDeviceServiceImpl implements AreaDeviceService, InitializingBea
if (deviceId == null) {
return null;
}
// 先从缓存读取(缓存存储的是 AreaDeviceDTO JSON
String cacheKey = DEVICE_CACHE_KEY_PREFIX + deviceId;
try {
String cached = stringRedisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if (NULL_CACHE.equals(cached)) {
return null;
}
log.debug("[AreaDevice] 命中设备配置缓存deviceId={}", deviceId);
AreaDeviceDTO dto = JsonUtils.parseObject(cached, AreaDeviceDTO.class);
return toAreaDeviceRelationDO(dto);
}
} catch (Exception e) {
log.warn("[AreaDevice] 读取设备配置缓存失败deviceId={}", deviceId, e);
}
// 从数据库查询
OpsAreaDeviceRelationDO relation = relationMapper.selectByDeviceId(deviceId);
// 写入 JSON 缓存IoT 可读,存储 AreaDeviceDTO
try {
if (relation != null) {
AreaDeviceDTO dto = toAreaDeviceDTO(relation);
stringRedisTemplate.opsForValue().set(
cacheKey,
JsonUtils.toJsonString(dto),
CACHE_TTL_HOURS,
TimeUnit.HOURS);
} else {
// 空值缓存,防止穿透
stringRedisTemplate.opsForValue().set(cacheKey, NULL_CACHE, 1, TimeUnit.MINUTES);
}
} catch (Exception e) {
log.warn("[AreaDevice] 写入设备配置缓存失败deviceId={}", deviceId, e);
}
return relation;
// 直接查数据库,返回第一个关联
return relationMapper.selectByDeviceId(deviceId);
}
@Override
public void initAreaDeviceIndex() {
log.info("[AreaDevice] 开始初始化区域设备索引...");
try {
// 1. 查询所有工牌设备关联,建立区域索引
List<OpsAreaDeviceRelationDO> badgeRelations = relationMapper
.selectListByAreaIdAndRelationType(null, "BADGE");
int badgeCount = 0;
for (OpsAreaDeviceRelationDO relation : badgeRelations) {
if (relation.getAreaId() != null && relation.getDeviceId() != null) {
addToAreaIndex(relation.getDeviceId(), relation.getAreaId());
badgeCount++;
}
}
log.info("[AreaDevice] 工牌设备索引初始化完成:共加载 {} 个工牌设备关联", badgeCount);
// 2. 查询所有信标设备关联,预热区域+类型配置缓存
List<OpsAreaDeviceRelationDO> beaconRelations = relationMapper
.selectListByAreaIdAndRelationType(null, "BEACON");
int beaconCount = 0;
for (OpsAreaDeviceRelationDO relation : beaconRelations) {
if (relation.getAreaId() != null && relation.getEnabled()) {
// 写入区域+类型配置缓存
String cacheKey = String.format(AREA_TYPE_CACHE_KEY_PREFIX, relation.getAreaId(), "BEACON");
try {
AreaDeviceDTO dto = toAreaDeviceDTO(relation);
stringRedisTemplate.opsForValue().set(
cacheKey,
JsonUtils.toJsonString(dto),
CACHE_TTL_HOURS,
TimeUnit.HOURS);
beaconCount++;
} catch (Exception e) {
log.warn("[AreaDevice] 写入信标配置缓存失败areaId={}", relation.getAreaId(), e);
}
}
}
log.info("[AreaDevice] 信标配置缓存初始化完成:共加载 {} 个信标配置", beaconCount);
} catch (Exception e) {
log.error("[AreaDevice] 区域设备索引初始化失败", e);
}
}
@Override
public void refreshAreaDeviceIndex() {
log.info("[AreaDevice] 开始刷新区域设备索引...");
initAreaDeviceIndex();
}
@Override
public Set<Long> getDeviceIdsByArea(Long areaId) {
if (areaId == null) {
return Set.of();
}
String areaKey = AREA_BADGES_KEY_PREFIX + areaId;
try {
Set<Object> members = redisTemplate.opsForSet().members(areaKey);
if (members != null && !members.isEmpty()) {
return members.stream()
.map(m -> Long.parseLong(m.toString()))
.collect(Collectors.toSet());
}
// 缓存未命中,读穿透从数据库重构
log.info("[AreaDevice] 区域索引缓存未命中从数据库重建areaId={}", areaId);
List<OpsAreaDeviceRelationDO> relations = relationMapper.selectListByAreaIdAndRelationType(areaId, "BADGE");
Set<Long> deviceIds = relations.stream()
.filter(OpsAreaDeviceRelationDO::getEnabled)
.map(OpsAreaDeviceRelationDO::getDeviceId)
.collect(Collectors.toSet());
if (!deviceIds.isEmpty()) {
// 写入缓存
String[] idStrings = deviceIds.stream().map(Object::toString).toArray(String[]::new);
redisTemplate.opsForSet().add(areaKey, (Object[]) idStrings);
redisTemplate.expire(areaKey, CACHE_TTL_HOURS, TimeUnit.HOURS);
}
return deviceIds;
} catch (Exception e) {
log.error("[AreaDevice] 获取区域设备索引失败areaId={}", areaId, e);
return Set.of();
}
}
@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());
// 延长过期时间到 24 小时
redisTemplate.expire(areaKey, CACHE_TTL_HOURS, TimeUnit.HOURS);
log.debug("[AreaDevice] 添加设备到区域索引deviceId={}, areaId={}", deviceId, areaId);
} catch (Exception e) {
log.error("[AreaDevice] 添加设备到区域索引失败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("[AreaDevice] 从区域索引移除设备deviceId={}, areaId={}", deviceId, areaId);
} catch (Exception e) {
log.error("[AreaDevice] 从区域索引移除设备失败deviceId={}, areaId={}", deviceId, areaId, e);
}
}
@Override
public void evictAreaCache(Long areaId) {
if (areaId == null) {
return;
}
try {
String areaKey = AREA_BADGES_KEY_PREFIX + areaId;
redisTemplate.delete(areaKey);
log.info("[AreaDevice] 清除区域缓存areaId={}", areaId);
} catch (Exception e) {
log.error("[AreaDevice] 清除区域缓存失败areaId={}", areaId, e);
}
}
@Override
public void evictDeviceCache(Long deviceId) {
public List<Long> getAreaIdsByDevice(Long deviceId) {
if (deviceId == null) {
return;
return List.of();
}
return relationMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getDeviceId, deviceId)
.select(OpsAreaDeviceRelationDO::getAreaId))
.stream()
.map(OpsAreaDeviceRelationDO::getAreaId)
.collect(Collectors.toList());
}
@Override
public List<Long> getDeviceIdsByArea(Long areaId) {
if (areaId == null) {
return List.of();
}
return relationMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getAreaId, areaId)
.select(OpsAreaDeviceRelationDO::getDeviceId))
.stream()
.map(OpsAreaDeviceRelationDO::getDeviceId)
.collect(Collectors.toList());
}
@Override
public List<Long> getDeviceIdsByAreaAndType(Long areaId, String relationType) {
if (areaId == null || relationType == null) {
return List.of();
}
return relationMapper.selectListByAreaIdAndRelationType(areaId, relationType).stream()
.filter(OpsAreaDeviceRelationDO::getEnabled)
.map(OpsAreaDeviceRelationDO::getDeviceId)
.collect(Collectors.toList());
}
@Override
public void initConfigCache() {
log.info("[AreaDevice] 开始初始化区域设备配置缓存...");
try {
String cacheKey = DEVICE_CACHE_KEY_PREFIX + deviceId;
stringRedisTemplate.delete(cacheKey);
log.info("[AreaDevice] 清除设备配置缓存deviceId={}", deviceId);
// 查询所有启用的设备关联
List<OpsAreaDeviceRelationDO> allRelations = relationMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<OpsAreaDeviceRelationDO>()
.eq(OpsAreaDeviceRelationDO::getEnabled, true));
// 按区域+类型分组,预热缓存
int cacheCount = 0;
for (OpsAreaDeviceRelationDO relation : allRelations) {
if (relation.getAreaId() == null || relation.getRelationType() == null) {
continue;
}
String cacheKey = String.format(CONFIG_CACHE_KEY_PREFIX,
relation.getAreaId(), relation.getRelationType());
try {
AreaDeviceDTO dto = toAreaDeviceDTO(relation);
stringRedisTemplate.opsForValue().set(
cacheKey,
JsonUtils.toJsonString(dto),
CACHE_TTL_HOURS,
TimeUnit.HOURS);
cacheCount++;
} catch (Exception e) {
log.warn("[AreaDevice] 写入配置缓存失败areaId={}, type={}",
relation.getAreaId(), relation.getRelationType(), e);
}
}
log.info("[AreaDevice] 配置缓存初始化完成:共加载 {} 个配置", cacheCount);
} catch (Exception e) {
log.error("[AreaDevice] 清除设备配置缓存失败deviceId={}", deviceId, e);
log.error("[AreaDevice] 配置缓存初始化失败", e);
}
}
/**
* 清除区域+类型配置缓存
*
* @param areaId 区域ID
* @param relationType 关联类型
*/
public void evictAreaTypeCache(Long areaId, String relationType) {
@Override
public void refreshConfigCache() {
log.info("[AreaDevice] 开始刷新区域设备配置缓存...");
initConfigCache();
}
@Override
public void evictConfigCache(Long areaId, String relationType) {
if (areaId == null || relationType == null) {
return;
}
try {
String cacheKey = String.format(AREA_TYPE_CACHE_KEY_PREFIX, areaId, relationType);
String cacheKey = String.format(CONFIG_CACHE_KEY_PREFIX, areaId, relationType);
stringRedisTemplate.delete(cacheKey);
log.info("[AreaDevice] 清除区域类型配置缓存areaId={}, type={}", areaId, relationType);
log.info("[AreaDevice] 清除配置缓存areaId={}, type={}", areaId, relationType);
} catch (Exception e) {
log.error("[AreaDevice] 清除区域类型配置缓存失败areaId={}, type={}", areaId, relationType, e);
log.error("[AreaDevice] 清除配置缓存失败areaId={}, type={}", areaId, relationType, e);
}
}