fix(ops): 恢复集成消费项目上下文

This commit is contained in:
lzh
2026-04-29 22:21:02 +08:00
parent 026a126824
commit f080cff7b7
17 changed files with 842 additions and 666 deletions

View File

@@ -46,6 +46,9 @@ public class BadgeDeviceStatusEventHandler implements RocketMQListener<String> {
@Resource
private AreaDeviceService areaDeviceService;
@Resource
private IntegrationProjectContextExecutor projectContextExecutor;
@Override
public void onMessage(String message) {
try {
@@ -66,12 +69,16 @@ public class BadgeDeviceStatusEventHandler implements RocketMQListener<String> {
}
private void handleDeviceStatusChange(IotDeviceStatusChangedEventDTO event) {
if (!isBadgeDevice(event.getDeviceId())) {
log.debug("[BadgeDeviceStatusEventHandler] 非工牌设备,忽略状态同步: deviceId={}", event.getDeviceId());
OpsAreaDeviceRelationDO relation = getBadgeRelation(event.getDeviceId());
if (relation == null) {
log.debug("[BadgeDeviceStatusEventHandler] ignore non-badge device status event: deviceId={}", event.getDeviceId());
return;
}
projectContextExecutor.execute(event.getProjectId(), null, relation.getAreaId(), event.getDeviceId(), event.getEventId(),
() -> updateBadgeDeviceStatus(event, relation.getAreaId()));
}
Long areaId = getAreaIdByDeviceId(event.getDeviceId());
private void updateBadgeDeviceStatus(IotDeviceStatusChangedEventDTO event, Long areaId) {
if (event.isOnline()) {
badgeDeviceStatusService.updateBadgeOnlineStatus(
event.getDeviceId(), event.getDeviceName(), event.getNickname(), areaId,
@@ -84,39 +91,22 @@ public class BadgeDeviceStatusEventHandler implements RocketMQListener<String> {
BadgeDeviceStatusEnum.OFFLINE, "设备离线");
return;
}
log.debug("[BadgeDeviceStatusEventHandler] 忽略未处理的状态变更: deviceId={}, newStatus={}",
log.debug("[BadgeDeviceStatusEventHandler] ignore device status event: deviceId={}, newStatus={}",
event.getDeviceId(), event.getNewStatus());
}
private boolean isBadgeDevice(Long deviceId) {
if (deviceId == null) {
return false;
}
try {
OpsAreaDeviceRelationDO relation = areaDeviceService.getByDeviceId(deviceId);
return relation != null && "BADGE".equals(relation.getRelationType());
} catch (Exception e) {
log.warn("[BadgeDeviceStatusEventHandler] 查询设备绑定关系失败: deviceId={}", deviceId, e);
return false;
}
}
private Long getAreaIdByDeviceId(Long deviceId) {
private OpsAreaDeviceRelationDO getBadgeRelation(Long deviceId) {
if (deviceId == null) {
return null;
}
try {
OpsAreaDeviceRelationDO relation = areaDeviceService.getByDeviceId(deviceId);
if (relation != null && "BADGE".equals(relation.getRelationType())) {
return relation.getAreaId();
}
return null;
return relation != null && "BADGE".equals(relation.getRelationType()) ? relation : null;
} catch (Exception e) {
log.warn("[BadgeDeviceStatusEventHandler] 查询设备所属区域失败: deviceId={}", deviceId, e);
log.warn("[BadgeDeviceStatusEventHandler] query badge relation failed: deviceId={}", deviceId, e);
return null;
}
}
private void executeInTenantContext(Long tenantId, Runnable runnable) {
Long currentTenantId = TenantContextHolder.getTenantId();
if (tenantId == null || Objects.equals(currentTenantId, tenantId)) {

View File

@@ -50,6 +50,9 @@ public class CleanOrderArriveEventHandler implements RocketMQListener<String> {
@Resource
private OrderLifecycleManager orderLifecycleManager;
@Resource
private IntegrationProjectContextExecutor projectContextExecutor;
@Override
public void onMessage(String message) {
try {
@@ -61,7 +64,9 @@ public class CleanOrderArriveEventHandler implements RocketMQListener<String> {
log.debug("[CleanOrderArriveEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId());
return;
}
executeInTenantContext(event.getTenantId(), () -> handleOrderArrive(event));
executeInTenantContext(event.getTenantId(), () -> projectContextExecutor.execute(
event.getProjectId(), event.getOrderId(), event.getAreaId(), event.getDeviceId(),
event.getEventId(), () -> handleOrderArrive(event)));
} catch (Exception e) {
log.error("[CleanOrderArriveEventHandler] 消息处理失败: message={}", message, e);
throw new RuntimeException("保洁工单到岗事件处理失败", e);

View File

@@ -1,265 +1,270 @@
package com.viewsh.module.ops.environment.integration.consumer;
package com.viewsh.module.ops.environment.integration.consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
import com.viewsh.framework.tenant.core.util.TenantUtils;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.environment.constants.CleanNotificationConstants;
import com.viewsh.module.ops.environment.integration.dto.CleanOrderAuditEventDTO;
import com.viewsh.module.ops.environment.service.voice.TtsQueueMessage;
import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogModule;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.environment.constants.CleanNotificationConstants;
import com.viewsh.module.ops.environment.integration.dto.CleanOrderAuditEventDTO;
import com.viewsh.module.ops.environment.service.voice.TtsQueueMessage;
import com.viewsh.module.ops.environment.service.voice.VoiceBroadcastService;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogModule;
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord;
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
import com.viewsh.module.ops.infrastructure.redis.OpsRedisKeyBuilder;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 保洁工单审计事件消费者
* <p>
* 订阅 IoT 模块发布的保洁工单审计事件
* 用于记录非状态变更的业务审计日志(如警告发送、抑制操作等)
* <p>
* RocketMQ 配置:
* - Topic: ops-order-audit
* - ConsumerGroup: ops-clean-order-audit-group
*
* @author AI
*/
@Slf4j
@Component
@RocketMQMessageListener(
topic = "ops-order-audit",
consumerGroup = "ops-clean-order-audit-group",
consumeMode = ConsumeMode.CONCURRENTLY,
selectorExpression = "*",
accessKey = "${rocketmq.consumer.access-key:}",
secretKey = "${rocketmq.consumer.secret-key:}"
)
public class CleanOrderAuditEventHandler implements RocketMQListener<String> {
/**
* 幂等性控制 Key 模式
*/
private static final String DEDUP_KEY_PATTERN = "ops:clean:dedup:audit:%s";
/**
* 幂等性控制 TTL
*/
private static final int DEDUP_TTL_SECONDS = 300;
private static final String TRIGGER_SOURCE_QUERY = "IOT_BUTTON_QUERY";
@Resource
private ObjectMapper objectMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private EventLogRecorder eventLogRecorder;
@Resource
private VoiceBroadcastService voiceBroadcastService;
@Resource
private OpsOrderMapper opsOrderMapper;
@Override
public void onMessage(String message) {
try {
// 1. JSON 反序列化
CleanOrderAuditEventDTO event = objectMapper.readValue(message, CleanOrderAuditEventDTO.class);
// 2. 幂等性检查
/**
* 保洁工单审计事件消费者
* <p>
* 订阅 IoT 模块发布的保洁工单审计事件
* 用于记录非状态变更的业务审计日志(如警告发送、抑制操作等)
* <p>
* RocketMQ 配置:
* - Topic: ops-order-audit
* - ConsumerGroup: ops-clean-order-audit-group
*
* @author AI
*/
@Slf4j
@Component
@RocketMQMessageListener(
topic = "ops-order-audit",
consumerGroup = "ops-clean-order-audit-group",
consumeMode = ConsumeMode.CONCURRENTLY,
selectorExpression = "*",
accessKey = "${rocketmq.consumer.access-key:}",
secretKey = "${rocketmq.consumer.secret-key:}"
)
public class CleanOrderAuditEventHandler implements RocketMQListener<String> {
/**
* 幂等性控制 Key 模式
*/
private static final String DEDUP_KEY_PATTERN = "ops:clean:dedup:audit:%s";
/**
* 幂等性控制 TTL
*/
private static final int DEDUP_TTL_SECONDS = 300;
private static final String TRIGGER_SOURCE_QUERY = "IOT_BUTTON_QUERY";
@Resource
private ObjectMapper objectMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private EventLogRecorder eventLogRecorder;
@Resource
private VoiceBroadcastService voiceBroadcastService;
@Resource
private OpsOrderMapper opsOrderMapper;
@Resource
private IntegrationProjectContextExecutor projectContextExecutor;
@Override
public void onMessage(String message) {
try {
// 1. JSON 反序列化
CleanOrderAuditEventDTO event = objectMapper.readValue(message, CleanOrderAuditEventDTO.class);
// 2. 幂等性检查
String dedupKey = OpsRedisKeyBuilder.eventDedup(event.getTenantId(), "audit", event.getEventId());
Boolean firstTime = stringRedisTemplate.opsForValue()
.setIfAbsent(dedupKey, "1", DEDUP_TTL_SECONDS, TimeUnit.SECONDS);
if (!firstTime) {
log.debug("[CleanOrderAuditEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId());
return;
}
// 3. 业务处理
executeInTenantContext(event.getTenantId(), () -> handleAuditEvent(event));
} catch (Exception e) {
log.error("[CleanOrderAuditEventHandler] 消息处理失败: message={}", message, e);
// 审计日志失败不抛出异常,避免影响主流程
}
}
/**
* 处理审计事件
*/
private void handleAuditEvent(CleanOrderAuditEventDTO event) {
log.debug("[CleanOrderAuditEventHandler] 收到审计事件: eventId={}, auditType={}, message={}",
event.getEventId(), event.getAuditType(), event.getMessage());
if (TRIGGER_SOURCE_QUERY.equals(event.getTriggerSource())) {
handleQueryEvent(event);
return;
}
// 1. 跳过与状态变更日志重复的审计事件(到岗确认/自动完成请求已由 CleanOrderEventListener 记录)
String auditType = event.getAuditType();
if (LogType.BEACON_ARRIVE_CONFIRMED.getCode().equals(auditType)
|| LogType.BEACON_COMPLETE_REQUESTED.getCode().equals(auditType)) {
log.debug("[CleanOrderAuditEventHandler] 跳过重复审计事件: eventId={}, auditType={}",
event.getEventId(), auditType);
return;
}
// 2. 确定日志级别和域
EventDomain domain = determineDomain(auditType);
EventLevel level = determineLevel(auditType);
LogType logType = auditType != null ? LogType.getByCode(auditType) : null;
String eventType = logType != null ? logType.getCode() : (auditType != null ? auditType : "AUDIT");
// 3. 记录审计日志
eventLogRecorder.record(
EventLogRecord.builder()
.module(LogModule.CLEAN)
.domain(domain)
.eventType(eventType)
.message(event.getMessage())
.targetId(event.getOrderId())
.targetType(event.getOrderId() != null ? "order" : null)
.deviceId(event.getDeviceId())
.level(level)
.build()
);
log.debug("[CleanOrderAuditEventHandler] 审计日志已记录: eventId={}, auditType={}",
event.getEventId(), auditType);
// 2. 如果是 TTS 请求,调用 IoT 模块下发语音
if (LogType.TTS_REQUEST.getCode().equals(auditType) && event.getDeviceId() != null) {
handleTtsRequest(event);
}
}
/**
* 处理 TTS 请求
* <p>
* 调用 IoT 模块的设备控制接口,下发语音播报到工牌设备
*
* @param event 审计事件
*/
private void handleTtsRequest(CleanOrderAuditEventDTO event) {
// 1. 从审计数据中提取 TTS 文本
String ttsText = null;
if (event.getData() != null && event.getData().containsKey("tts")) {
ttsText = (String) event.getData().get("tts");
}
if (ttsText == null || ttsText.isEmpty()) {
log.warn("[CleanOrderAuditEventHandler] TTS 文本为空,跳过下发: eventId={}", event.getEventId());
return;
}
sendTts(event.getDeviceId(), ttsText, event.getOrderId());
}
/**
* 处理查询事件 (IOT_BUTTON_QUERY)
* <p>
* 查询事件仅播报当前工单地点信息,不涉及循环播报停止和状态流转。
* 循环播报的停止由确认路径CleanOrderEventListener.handleConfirmed负责。
*/
private void handleQueryEvent(CleanOrderAuditEventDTO event) {
log.info("[CleanOrderAuditEventHandler] Handling query event: eventId={}", event.getEventId());
Long deviceId = event.getDeviceId();
if (deviceId == null) {
log.warn("[CleanOrderAuditEventHandler] Query event missing deviceId: eventId={}", event.getEventId());
return;
}
// 1. 获取当前正在处理的工单DISPATCHED, CONFIRMED, ARRIVED 状态)
String currentAreaName = null;
OpsOrderDO currentOrder = opsOrderMapper.selectOne(new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getAssigneeDeviceId, deviceId)
.in(OpsOrderDO::getStatus, Arrays.asList(
WorkOrderStatusEnum.DISPATCHED.getStatus(),
WorkOrderStatusEnum.CONFIRMED.getStatus(),
WorkOrderStatusEnum.ARRIVED.getStatus()))
.orderByAsc(OpsOrderDO::getId)
.last("LIMIT 1"));
if (currentOrder != null && currentOrder.getLocation() != null) {
currentAreaName = currentOrder.getLocation();
}
// 2. 查询待办工单数量QUEUED 状态,不含当前处理中工单)
Long pendingCount = opsOrderMapper.selectCount(new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getAssigneeDeviceId, deviceId)
.eq(OpsOrderDO::getStatus, WorkOrderStatusEnum.QUEUED.getStatus()));
// 3. 构建 TTS 文本(使用统一模板构建器)
String ttsText = CleanNotificationConstants.VoiceBuilder.buildQuery(currentAreaName, pendingCount.intValue());
// 4. 直接下发 TTS按键响应需立即播报不走队列
Long orderId = currentOrder != null ? currentOrder.getId() : null;
sendTts(deviceId, ttsText, orderId);
}
/**
* 下发 TTS 语音播报(直接发送,不走队列,按键响应需立即播报)
*/
private void sendTts(Long deviceId, String text, Long orderId) {
try {
voiceBroadcastService.broadcastDirect(deviceId, text,
TtsQueueMessage.TTS_FLAG_URGENT, orderId);
log.info("[CleanOrderAuditEventHandler] TTS 直接下发成功: deviceId={}, text={}", deviceId, text);
} catch (Exception e) {
log.error("[CleanOrderAuditEventHandler] TTS 直接下发异常: deviceId={}", deviceId, e);
}
}
/**
* 确定事件域
*/
private EventDomain determineDomain(String auditType) {
if (auditType == null) {
return EventDomain.SYSTEM;
}
if (auditType.startsWith("BEACON_") || auditType.contains("BEACON")) {
return EventDomain.BEACON;
} else if (LogType.TTS_REQUEST.getCode().equals(auditType)) {
return EventDomain.DEVICE;
} else {
return EventDomain.AUDIT;
}
}
/**
* 确定日志级别
*/
private EventLevel determineLevel(String auditType) {
if (auditType != null && (auditType.endsWith("_WARNING") ||
auditType.endsWith("_SUPPRESSED") ||
auditType.endsWith("_REJECTED"))) {
return EventLevel.WARN;
}
return EventLevel.INFO;
Boolean firstTime = stringRedisTemplate.opsForValue()
.setIfAbsent(dedupKey, "1", DEDUP_TTL_SECONDS, TimeUnit.SECONDS);
if (!firstTime) {
log.debug("[CleanOrderAuditEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId());
return;
}
// 3. 业务处理
executeInTenantContext(event.getTenantId(), () -> projectContextExecutor.execute(
event.getProjectId(), event.getOrderId(), event.getAreaId(), event.getDeviceId(),
event.getEventId(), () -> handleAuditEvent(event)));
} catch (Exception e) {
log.error("[CleanOrderAuditEventHandler] 消息处理失败: message={}", message, e);
// 审计日志失败不抛出异常,避免影响主流程
}
}
/**
* 处理审计事件
*/
private void handleAuditEvent(CleanOrderAuditEventDTO event) {
log.debug("[CleanOrderAuditEventHandler] 收到审计事件: eventId={}, auditType={}, message={}",
event.getEventId(), event.getAuditType(), event.getMessage());
if (TRIGGER_SOURCE_QUERY.equals(event.getTriggerSource())) {
handleQueryEvent(event);
return;
}
// 1. 跳过与状态变更日志重复的审计事件(到岗确认/自动完成请求已由 CleanOrderEventListener 记录)
String auditType = event.getAuditType();
if (LogType.BEACON_ARRIVE_CONFIRMED.getCode().equals(auditType)
|| LogType.BEACON_COMPLETE_REQUESTED.getCode().equals(auditType)) {
log.debug("[CleanOrderAuditEventHandler] 跳过重复审计事件: eventId={}, auditType={}",
event.getEventId(), auditType);
return;
}
// 2. 确定日志级别和域
EventDomain domain = determineDomain(auditType);
EventLevel level = determineLevel(auditType);
LogType logType = auditType != null ? LogType.getByCode(auditType) : null;
String eventType = logType != null ? logType.getCode() : (auditType != null ? auditType : "AUDIT");
// 3. 记录审计日志
eventLogRecorder.record(
EventLogRecord.builder()
.module(LogModule.CLEAN)
.domain(domain)
.eventType(eventType)
.message(event.getMessage())
.targetId(event.getOrderId())
.targetType(event.getOrderId() != null ? "order" : null)
.deviceId(event.getDeviceId())
.level(level)
.build()
);
log.debug("[CleanOrderAuditEventHandler] 审计日志已记录: eventId={}, auditType={}",
event.getEventId(), auditType);
// 2. 如果是 TTS 请求,调用 IoT 模块下发语音
if (LogType.TTS_REQUEST.getCode().equals(auditType) && event.getDeviceId() != null) {
handleTtsRequest(event);
}
}
/**
* 处理 TTS 请求
* <p>
* 调用 IoT 模块的设备控制接口,下发语音播报到工牌设备
*
* @param event 审计事件
*/
private void handleTtsRequest(CleanOrderAuditEventDTO event) {
// 1. 从审计数据中提取 TTS 文本
String ttsText = null;
if (event.getData() != null && event.getData().containsKey("tts")) {
ttsText = (String) event.getData().get("tts");
}
if (ttsText == null || ttsText.isEmpty()) {
log.warn("[CleanOrderAuditEventHandler] TTS 文本为空,跳过下发: eventId={}", event.getEventId());
return;
}
sendTts(event.getDeviceId(), ttsText, event.getOrderId());
}
/**
* 处理查询事件 (IOT_BUTTON_QUERY)
* <p>
* 查询事件仅播报当前工单地点信息,不涉及循环播报停止和状态流转。
* 循环播报的停止由确认路径CleanOrderEventListener.handleConfirmed负责。
*/
private void handleQueryEvent(CleanOrderAuditEventDTO event) {
log.info("[CleanOrderAuditEventHandler] Handling query event: eventId={}", event.getEventId());
Long deviceId = event.getDeviceId();
if (deviceId == null) {
log.warn("[CleanOrderAuditEventHandler] Query event missing deviceId: eventId={}", event.getEventId());
return;
}
// 1. 获取当前正在处理的工单DISPATCHED, CONFIRMED, ARRIVED 状态)
String currentAreaName = null;
OpsOrderDO currentOrder = opsOrderMapper.selectOne(new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getAssigneeDeviceId, deviceId)
.in(OpsOrderDO::getStatus, Arrays.asList(
WorkOrderStatusEnum.DISPATCHED.getStatus(),
WorkOrderStatusEnum.CONFIRMED.getStatus(),
WorkOrderStatusEnum.ARRIVED.getStatus()))
.orderByAsc(OpsOrderDO::getId)
.last("LIMIT 1"));
if (currentOrder != null && currentOrder.getLocation() != null) {
currentAreaName = currentOrder.getLocation();
}
// 2. 查询待办工单数量QUEUED 状态,不含当前处理中工单)
Long pendingCount = opsOrderMapper.selectCount(new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getAssigneeDeviceId, deviceId)
.eq(OpsOrderDO::getStatus, WorkOrderStatusEnum.QUEUED.getStatus()));
// 3. 构建 TTS 文本(使用统一模板构建器)
String ttsText = CleanNotificationConstants.VoiceBuilder.buildQuery(currentAreaName, pendingCount.intValue());
// 4. 直接下发 TTS按键响应需立即播报不走队列
Long orderId = currentOrder != null ? currentOrder.getId() : null;
sendTts(deviceId, ttsText, orderId);
}
/**
* 下发 TTS 语音播报(直接发送,不走队列,按键响应需立即播报)
*/
private void sendTts(Long deviceId, String text, Long orderId) {
try {
voiceBroadcastService.broadcastDirect(deviceId, text,
TtsQueueMessage.TTS_FLAG_URGENT, orderId);
log.info("[CleanOrderAuditEventHandler] TTS 直接下发成功: deviceId={}, text={}", deviceId, text);
} catch (Exception e) {
log.error("[CleanOrderAuditEventHandler] TTS 直接下发异常: deviceId={}", deviceId, e);
}
}
/**
* 确定事件域
*/
private EventDomain determineDomain(String auditType) {
if (auditType == null) {
return EventDomain.SYSTEM;
}
if (auditType.startsWith("BEACON_") || auditType.contains("BEACON")) {
return EventDomain.BEACON;
} else if (LogType.TTS_REQUEST.getCode().equals(auditType)) {
return EventDomain.DEVICE;
} else {
return EventDomain.AUDIT;
}
}
/**
* 确定日志级别
*/
private EventLevel determineLevel(String auditType) {
if (auditType != null && (auditType.endsWith("_WARNING") ||
auditType.endsWith("_SUPPRESSED") ||
auditType.endsWith("_REJECTED"))) {
return EventLevel.WARN;
}
return EventLevel.INFO;
}
private void executeInTenantContext(Long tenantId, Runnable runnable) {

View File

@@ -50,6 +50,9 @@ public class CleanOrderCompleteEventHandler implements RocketMQListener<String>
@Resource
private OrderLifecycleManager orderLifecycleManager;
@Resource
private IntegrationProjectContextExecutor projectContextExecutor;
@Override
public void onMessage(String message) {
try {
@@ -61,7 +64,9 @@ public class CleanOrderCompleteEventHandler implements RocketMQListener<String>
log.debug("[CleanOrderCompleteEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId());
return;
}
executeInTenantContext(event.getTenantId(), () -> handleOrderComplete(event));
executeInTenantContext(event.getTenantId(), () -> projectContextExecutor.execute(
event.getProjectId(), event.getOrderId(), event.getAreaId(), event.getDeviceId(),
event.getEventId(), () -> handleOrderComplete(event)));
} catch (Exception e) {
log.error("[CleanOrderCompleteEventHandler] 消息处理失败: message={}", message, e);
throw new RuntimeException("保洁工单完单事件处理失败", e);

View File

@@ -47,6 +47,9 @@ public class CleanOrderConfirmEventHandler implements RocketMQListener<String> {
@Resource
private OrderLifecycleManager orderLifecycleManager;
@Resource
private IntegrationProjectContextExecutor projectContextExecutor;
@Override
public void onMessage(String message) {
try {
@@ -58,7 +61,9 @@ public class CleanOrderConfirmEventHandler implements RocketMQListener<String> {
log.debug("[CleanOrderConfirmEventHandler] 重复消息,跳过处理: eventId={}", event.getEventId());
return;
}
executeInTenantContext(event.getTenantId(), () -> handleConfirmEvent(event));
executeInTenantContext(event.getTenantId(), () -> projectContextExecutor.execute(
event.getProjectId(), event.getOrderId(), event.getAreaId(), event.getDeviceId(),
event.getEventId(), () -> handleConfirmEvent(event)));
} catch (Exception e) {
log.error("[CleanOrderConfirmEventHandler] 消息处理失败: message={}", message, e);
throw new RuntimeException("保洁工单确认事件处理失败", e);

View File

@@ -1,11 +1,15 @@
package com.viewsh.module.ops.environment.integration.consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.viewsh.framework.tenant.core.context.ProjectContextHolder;
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
import com.viewsh.framework.tenant.core.util.ProjectUtils;
import com.viewsh.framework.tenant.core.util.TenantUtils;
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
import com.viewsh.module.iot.api.device.dto.ResetTrafficCounterReqDTO;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.PriorityEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
@@ -103,6 +107,9 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
@Resource
private OpsOrderMapper opsOrderMapper;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Override
public void onMessage(String message) {
try {
@@ -120,7 +127,8 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
}
// 3. 业务处理
executeInTenantContext(event.getTenantId(), () -> handleOrderCreate(event));
executeInTenantContext(event.getTenantId(), () ->
executeInProjectContext(event, () -> handleOrderCreate(event)));
} catch (Exception e) {
log.error("[CleanOrderCreateEventHandler] 消息处理失败: message={}", message, e);
@@ -538,6 +546,41 @@ public class CleanOrderCreateEventHandler implements RocketMQListener<String> {
return null;
}
private void executeInProjectContext(CleanOrderCreateEventDTO event, Runnable runnable) {
Long projectId = resolveProjectId(event);
Long currentProjectId = ProjectContextHolder.getProjectId();
if (projectId == null) {
throw new IllegalStateException("Clean order create event missing projectId, eventId="
+ event.getEventId() + ", areaId=" + event.getAreaId());
}
if (Objects.equals(currentProjectId, projectId)) {
runnable.run();
return;
}
ProjectUtils.execute(projectId, runnable);
}
private Long resolveProjectId(CleanOrderCreateEventDTO event) {
if (event.getProjectId() != null) {
return event.getProjectId();
}
Long currentProjectId = ProjectContextHolder.getProjectId();
if (currentProjectId != null) {
return currentProjectId;
}
Long areaId = event.getAreaId();
if (areaId == null) {
return null;
}
OpsBusAreaDO area = ProjectUtils.executeIgnore(() -> opsBusAreaMapper.selectById(areaId));
if (area == null) {
log.warn("[CleanOrderCreateEventHandler] area not found when resolving projectId: eventId={}, areaId={}",
event.getEventId(), areaId);
return null;
}
return area.getProjectId();
}
private void executeInTenantContext(Long tenantId, Runnable runnable) {
Long currentTenantId = TenantContextHolder.getTenantId();
if (tenantId == null || Objects.equals(currentTenantId, tenantId)) {

View File

@@ -0,0 +1,91 @@
package com.viewsh.module.ops.environment.integration.consumer;
import com.viewsh.framework.tenant.core.context.ProjectContextHolder;
import com.viewsh.framework.tenant.core.util.ProjectUtils;
import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO;
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.area.OpsAreaDeviceRelationMapper;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Objects;
@Slf4j
@Component
public class IntegrationProjectContextExecutor {
@Resource
private OpsOrderMapper opsOrderMapper;
@Resource
private OpsBusAreaMapper opsBusAreaMapper;
@Resource
private OpsAreaDeviceRelationMapper opsAreaDeviceRelationMapper;
public void execute(Long projectId, Long orderId, Long areaId, Long deviceId, String eventId, Runnable runnable) {
Long resolvedProjectId = resolveProjectId(projectId, orderId, areaId, deviceId, eventId);
Long currentProjectId = ProjectContextHolder.getProjectId();
if (resolvedProjectId == null) {
throw new IllegalStateException("integration event missing projectId, eventId=" + eventId
+ ", orderId=" + orderId + ", areaId=" + areaId + ", deviceId=" + deviceId);
}
if (Objects.equals(currentProjectId, resolvedProjectId)) {
runnable.run();
return;
}
ProjectUtils.execute(resolvedProjectId, runnable);
}
private Long resolveProjectId(Long projectId, Long orderId, Long areaId, Long deviceId, String eventId) {
if (projectId != null) {
return projectId;
}
Long currentProjectId = ProjectContextHolder.getProjectId();
if (currentProjectId != null) {
return currentProjectId;
}
Long projectIdByOrder = resolveProjectIdByOrder(orderId);
if (projectIdByOrder != null) {
return projectIdByOrder;
}
Long projectIdByArea = resolveProjectIdByArea(areaId);
if (projectIdByArea != null) {
return projectIdByArea;
}
Long projectIdByDevice = resolveProjectIdByDevice(deviceId);
if (projectIdByDevice == null) {
log.warn("[IntegrationProjectContextExecutor] projectId not resolved: eventId={}, orderId={}, areaId={}, deviceId={}",
eventId, orderId, areaId, deviceId);
}
return projectIdByDevice;
}
private Long resolveProjectIdByOrder(Long orderId) {
if (orderId == null) {
return null;
}
OpsOrderDO order = ProjectUtils.executeIgnore(() -> opsOrderMapper.selectById(orderId));
return order != null ? order.getProjectId() : null;
}
private Long resolveProjectIdByArea(Long areaId) {
if (areaId == null) {
return null;
}
OpsBusAreaDO area = ProjectUtils.executeIgnore(() -> opsBusAreaMapper.selectById(areaId));
return area != null ? area.getProjectId() : null;
}
private Long resolveProjectIdByDevice(Long deviceId) {
if (deviceId == null) {
return null;
}
OpsAreaDeviceRelationDO relation = ProjectUtils.executeIgnore(() -> opsAreaDeviceRelationMapper.selectByDeviceId(deviceId));
return relation != null ? relation.getProjectId() : null;
}
}

View File

@@ -42,6 +42,9 @@ public class TrajectoryEnterEventHandler implements RocketMQListener<String> {
@Resource
private DeviceTrajectoryService trajectoryService;
@Resource
private IntegrationProjectContextExecutor projectContextExecutor;
@Override
public void onMessage(String message) {
try {
@@ -57,17 +60,8 @@ public class TrajectoryEnterEventHandler implements RocketMQListener<String> {
event.getEventId(), event.getDeviceId(), event.getAreaId());
// 解析事件时间
LocalDateTime enterTime = parseEventTime(event.getEventTime());
// 创建轨迹记录
trajectoryService.recordEnter(
event.getDeviceId(),
event.getDeviceName(),
event.getNickname(),
event.getAreaId(),
event.getBeaconMac(),
event.getEnterRssi(),
enterTime);
projectContextExecutor.execute(event.getProjectId(), null, event.getAreaId(), event.getDeviceId(),
event.getEventId(), () -> handleTrajectoryEnter(event));
} catch (Exception e) {
log.error("[TrajectoryEnterHandler] 消息处理失败message={}", message, e);
@@ -75,6 +69,18 @@ public class TrajectoryEnterEventHandler implements RocketMQListener<String> {
}
}
private void handleTrajectoryEnter(TrajectoryEnterEventDTO event) {
LocalDateTime enterTime = parseEventTime(event.getEventTime());
trajectoryService.recordEnter(
event.getDeviceId(),
event.getDeviceName(),
event.getNickname(),
event.getAreaId(),
event.getBeaconMac(),
event.getEnterRssi(),
enterTime);
}
private LocalDateTime parseEventTime(String eventTime) {
if (eventTime == null || eventTime.isEmpty()) {
return LocalDateTime.now();

View File

@@ -42,6 +42,9 @@ public class TrajectoryLeaveEventHandler implements RocketMQListener<String> {
@Resource
private DeviceTrajectoryService trajectoryService;
@Resource
private IntegrationProjectContextExecutor projectContextExecutor;
@Override
public void onMessage(String message) {
try {
@@ -57,15 +60,8 @@ public class TrajectoryLeaveEventHandler implements RocketMQListener<String> {
event.getEventId(), event.getDeviceId(), event.getAreaId(), event.getLeaveReason());
// 解析事件时间
LocalDateTime leaveTime = parseEventTime(event.getEventTime());
// 更新轨迹记录
trajectoryService.recordLeave(
event.getDeviceId(),
event.getAreaId(),
event.getLeaveReason(),
event.getEnterTimestamp(),
leaveTime);
projectContextExecutor.execute(event.getProjectId(), null, event.getAreaId(), event.getDeviceId(),
event.getEventId(), () -> handleTrajectoryLeave(event));
} catch (Exception e) {
log.error("[TrajectoryLeaveHandler] 消息处理失败message={}", message, e);
@@ -73,6 +69,16 @@ public class TrajectoryLeaveEventHandler implements RocketMQListener<String> {
}
}
private void handleTrajectoryLeave(TrajectoryLeaveEventDTO event) {
LocalDateTime leaveTime = parseEventTime(event.getEventTime());
trajectoryService.recordLeave(
event.getDeviceId(),
event.getAreaId(),
event.getLeaveReason(),
event.getEnterTimestamp(),
leaveTime);
}
private LocalDateTime parseEventTime(String eventTime) {
if (eventTime == null || eventTime.isEmpty()) {
return LocalDateTime.now();

View File

@@ -51,6 +51,9 @@ public class BaseDeviceEventDTO {
@JsonProperty("tenantId")
private Long tenantId;
@JsonProperty("projectId")
private Long projectId;
/**
* 事件时间
*/

View File

@@ -1,66 +1,68 @@
package com.viewsh.module.ops.environment.integration.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 保洁工单到岗事件 DTO
* <p>
* 由 IoT 模块发布Ops 模块消费
*
* @author AI
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CleanOrderArriveEventDTO {
/**
* 事件IDUUID用于幂等性控制
*/
private String eventId;
/**
* 工单类型CLEAN=保洁)
*/
private String orderType;
/**
* 工单ID
*/
private Long orderId;
/**
* 设备ID保洁员工牌设备ID
*/
private Long deviceId;
/**
* 设备Key
*/
private String deviceKey;
/**
* 区域ID
*/
package com.viewsh.module.ops.environment.integration.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 保洁工单到岗事件 DTO
* <p>
* 由 IoT 模块发布Ops 模块消费
*
* @author AI
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CleanOrderArriveEventDTO {
/**
* 事件IDUUID用于幂等性控制
*/
private String eventId;
/**
* 工单类型CLEAN=保洁)
*/
private String orderType;
/**
* 工单ID
*/
private Long orderId;
/**
* 设备ID保洁员工牌设备ID
*/
private Long deviceId;
/**
* 设备Key
*/
private String deviceKey;
/**
* 区域ID
*/
private Long areaId;
private Long tenantId;
/**
* 触发来源IOT_BEACON=蓝牙信标检测)
*/
private String triggerSource;
/**
* 触发数据JSON 格式的附加信息)
* <p>
* 示例:{"beaconMac":"F0:C8:60:1D:10:BB","rssi":-65,"windowSnapshot":[-70,-68,-65,-64,-66]}
*/
private Map<String, Object> triggerData;
}
private Long projectId;
/**
* 触发来源IOT_BEACON=蓝牙信标检测)
*/
private String triggerSource;
/**
* 触发数据JSON 格式的附加信息)
* <p>
* 示例:{"beaconMac":"F0:C8:60:1D:10:BB","rssi":-65,"windowSnapshot":[-70,-68,-65,-64,-66]}
*/
private Map<String, Object> triggerData;
}

View File

@@ -1,37 +1,37 @@
package com.viewsh.module.ops.environment.integration.dto;
package com.viewsh.module.ops.environment.integration.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 保洁工单审计事件 DTO
* <p>
* 由 IoT 模块发布Ops 模块消费
* 用于记录非状态变更的业务审计日志(如警告发送、抑制操作等)
*
* @author AI
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CleanOrderAuditEventDTO {
/**
* 事件IDUUID用于幂等性控制
*/
private String eventId;
/**
* 工单ID可选部分审计事件可能没有工单ID
*/
private Long orderId;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 保洁工单审计事件 DTO
* <p>
* 由 IoT 模块发布Ops 模块消费
* 用于记录非状态变更的业务审计日志(如警告发送、抑制操作等)
*
* @author AI
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CleanOrderAuditEventDTO {
/**
* 事件IDUUID用于幂等性控制
*/
private String eventId;
/**
* 工单ID可选部分审计事件可能没有工单ID
*/
private Long orderId;
/**
* 触发来源 (如 IOT_BUTTON_QUERY)
*/
@@ -40,40 +40,42 @@ public class CleanOrderAuditEventDTO {
/**
* 审计类型
* <p>
* - BEACON_ARRIVE_CONFIRMED: 蓝牙信标到岗确认
* - BEACON_LEAVE_WARNING_SENT: 离开区域警告已发送
* - COMPLETE_SUPPRESSED_INVALID: 作业时长不足,抑制自动完成
* - BEACON_COMPLETE_REQUESTED: 信号丢失超时自动完成请求
* - TTS_REQUEST: TTS 语音播报请求
* - ARRIVE_REJECTED: 到岗请求被拒绝(状态不符)
*/
private String auditType;
/**
* 设备ID
*/
private Long deviceId;
/**
* 设备Key
*/
private String deviceKey;
/**
* 区域ID
*/
* <p>
* - BEACON_ARRIVE_CONFIRMED: 蓝牙信标到岗确认
* - BEACON_LEAVE_WARNING_SENT: 离开区域警告已发送
* - COMPLETE_SUPPRESSED_INVALID: 作业时长不足,抑制自动完成
* - BEACON_COMPLETE_REQUESTED: 信号丢失超时自动完成请求
* - TTS_REQUEST: TTS 语音播报请求
* - ARRIVE_REJECTED: 到岗请求被拒绝(状态不符)
*/
private String auditType;
/**
* 设备ID
*/
private Long deviceId;
/**
* 设备Key
*/
private String deviceKey;
/**
* 区域ID
*/
private Long areaId;
private Long tenantId;
/**
* 消息内容
*/
private String message;
/**
* 审计数据JSON 格式的附加信息)
*/
private Map<String, Object> data;
}
private Long projectId;
/**
* 消息内容
*/
private String message;
/**
* 审计数据JSON 格式的附加信息)
*/
private Map<String, Object> data;
}

View File

@@ -1,66 +1,68 @@
package com.viewsh.module.ops.environment.integration.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 保洁工单完成事件 DTO
* <p>
* 由 IoT 模块发布Ops 模块消费
*
* @author AI
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CleanOrderCompleteEventDTO {
/**
* 事件IDUUID用于幂等性控制
*/
private String eventId;
/**
* 工单类型CLEAN=保洁)
*/
private String orderType;
/**
* 工单ID
*/
private Long orderId;
/**
* 设备ID保洁员工牌设备ID
*/
private Long deviceId;
/**
* 设备Key
*/
private String deviceKey;
/**
* 区域ID
*/
package com.viewsh.module.ops.environment.integration.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 保洁工单完成事件 DTO
* <p>
* 由 IoT 模块发布Ops 模块消费
*
* @author AI
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CleanOrderCompleteEventDTO {
/**
* 事件IDUUID用于幂等性控制
*/
private String eventId;
/**
* 工单类型CLEAN=保洁)
*/
private String orderType;
/**
* 工单ID
*/
private Long orderId;
/**
* 设备ID保洁员工牌设备ID
*/
private Long deviceId;
/**
* 设备Key
*/
private String deviceKey;
/**
* 区域ID
*/
private Long areaId;
private Long tenantId;
/**
* 触发来源IOT_SIGNAL_LOSS=信号丢失超时)
*/
private String triggerSource;
/**
* 触发数据JSON 格式的附加信息)
* <p>
* 示例:{"durationMs":1800000,"lastLossTime":1704067200000,"completionReason":"SIGNAL_LOSS_TIMEOUT"}
*/
private Map<String, Object> triggerData;
}
private Long projectId;
/**
* 触发来源IOT_SIGNAL_LOSS=信号丢失超时)
*/
private String triggerSource;
/**
* 触发数据JSON 格式的附加信息)
* <p>
* 示例:{"durationMs":1800000,"lastLossTime":1704067200000,"completionReason":"SIGNAL_LOSS_TIMEOUT"}
*/
private Map<String, Object> triggerData;
}

View File

@@ -1,67 +1,72 @@
package com.viewsh.module.ops.environment.integration.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 保洁工单创建事件 DTO
* <p>
* 由 IoT 模块发布Ops 模块消费
*
* @author AI
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CleanOrderCreateEventDTO {
/**
* 事件IDUUID用于幂等性控制
*/
private String eventId;
/**
* 工单类型CLEAN=保洁)
*/
private String orderType;
/**
* 区域ID
*/
private Long areaId;
/**
* 触发来源IOT_TRAFFIC=客流阈值/IOT_BEACON=蓝牙信标/IOT_SIGNAL_LOSS=信号丢失超时)
*/
private String triggerSource;
/**
* 触发设备ID
*/
private Long triggerDeviceId;
/**
* 触发设备Key
*/
private String triggerDeviceKey;
/**
* 优先级0=P0紧急 1=P1重要 2=P2普通
*/
package com.viewsh.module.ops.environment.integration.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
/**
* 保洁工单创建事件 DTO
* <p>
* 由 IoT 模块发布Ops 模块消费
*
* @author AI
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CleanOrderCreateEventDTO {
/**
* 事件IDUUID用于幂等性控制
*/
private String eventId;
/**
* 工单类型CLEAN=保洁)
*/
private String orderType;
/**
* 区域ID
*/
private Long areaId;
/**
* 触发来源IOT_TRAFFIC=客流阈值/IOT_BEACON=蓝牙信标/IOT_SIGNAL_LOSS=信号丢失超时)
*/
private String triggerSource;
/**
* 触发设备ID
*/
private Long triggerDeviceId;
/**
* 触发设备Key
*/
private String triggerDeviceKey;
/**
* 优先级0=P0紧急 1=P1重要 2=P2普通
*/
private Integer priority;
private Long tenantId;
/**
* 触发数据JSON 格式的附加信息)
* <p>
* 客流阈值触发示例:{"actualCount":150,"baseValue":1000,"threshold":100,"exceededCount":50}
* 信标检测触发示例:{"rssi":-65,"beaconMac":"F0:C8:60:1D:10:BB"}
*/
private Map<String, Object> triggerData;
}
/**
* Project id.
*/
private Long projectId;
/**
* 触发数据JSON 格式的附加信息)
* <p>
* 客流阈值触发示例:{"actualCount":150,"baseValue":1000,"threshold":100,"exceededCount":50}
* 信标检测触发示例:{"rssi":-65,"beaconMac":"F0:C8:60:1D:10:BB"}
*/
private Map<String, Object> triggerData;
}

View File

@@ -1,109 +1,111 @@
package com.viewsh.module.ops.environment.integration.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* IoT 设备状态变更事件 DTO
* <p>
* 用于接收 IoT 模块发布的设备状态变更事件
* 解耦 Ops 模块与 iot-core 的依赖
*
* @author AI
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class IotDeviceStatusChangedEventDTO {
/**
* 事件ID唯一标识用于幂等性处理
*/
private String eventId;
/**
* 设备ID
*/
private Long deviceId;
/**
* 设备名称deviceName
*/
private String deviceName;
/**
* 设备昵称nickname用户可读的显示名称
*/
private String nickname;
/**
* 产品ID
*/
private Long productId;
/**
* 产品标识符productKey
*/
private String productKey;
/**
* 租户ID
*/
private Long tenantId;
/**
* 事件时间
*/
private LocalDateTime eventTime;
/**
* 旧状态0=INACTIVE, 1=ONLINE, 2=OFFLINE
*/
private Integer oldStatus;
/**
* 新状态0=INACTIVE, 1=ONLINE, 2=OFFLINE
*/
private Integer newStatus;
/**
* 状态变更原因
*/
private String reason;
// ==================== 状态常量 ====================
/**
* 设备状态:在线
*/
public static final Integer STATUS_ONLINE = 1;
/**
* 设备状态:离线
*/
public static final Integer STATUS_OFFLINE = 2;
/**
* 设备状态:未激活
*/
public static final Integer STATUS_INACTIVE = 0;
/**
* 判断是否上线
*/
public boolean isOnline() {
return STATUS_ONLINE.equals(newStatus);
}
/**
* 判断是否离线
*/
public boolean isOffline() {
return STATUS_OFFLINE.equals(newStatus);
}
}
package com.viewsh.module.ops.environment.integration.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* IoT 设备状态变更事件 DTO
* <p>
* 用于接收 IoT 模块发布的设备状态变更事件
* 解耦 Ops 模块与 iot-core 的依赖
*
* @author AI
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class IotDeviceStatusChangedEventDTO {
/**
* 事件ID唯一标识用于幂等性处理
*/
private String eventId;
/**
* 设备ID
*/
private Long deviceId;
/**
* 设备名称deviceName
*/
private String deviceName;
/**
* 设备昵称nickname用户可读的显示名称
*/
private String nickname;
/**
* 产品ID
*/
private Long productId;
/**
* 产品标识符productKey
*/
private String productKey;
/**
* 租户ID
*/
private Long tenantId;
private Long projectId;
/**
* 事件时间
*/
private LocalDateTime eventTime;
/**
* 旧状态0=INACTIVE, 1=ONLINE, 2=OFFLINE
*/
private Integer oldStatus;
/**
* 新状态0=INACTIVE, 1=ONLINE, 2=OFFLINE
*/
private Integer newStatus;
/**
* 状态变更原因
*/
private String reason;
// ==================== 状态常量 ====================
/**
* 设备状态:在线
*/
public static final Integer STATUS_ONLINE = 1;
/**
* 设备状态:离线
*/
public static final Integer STATUS_OFFLINE = 2;
/**
* 设备状态:未激活
*/
public static final Integer STATUS_INACTIVE = 0;
/**
* 判断是否上线
*/
public boolean isOnline() {
return STATUS_ONLINE.equals(newStatus);
}
/**
* 判断是否离线
*/
public boolean isOffline() {
return STATUS_OFFLINE.equals(newStatus);
}
}

View File

@@ -62,4 +62,6 @@ public class TrajectoryEnterEventDTO {
* 租户ID
*/
private Long tenantId;
private Long projectId;
}

View File

@@ -69,4 +69,6 @@ public class TrajectoryLeaveEventDTO {
* 租户ID
*/
private Long tenantId;
private Long projectId;
}