fix(ops): 恢复集成消费项目上下文
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -51,6 +51,9 @@ public class BaseDeviceEventDTO {
|
||||
@JsonProperty("tenantId")
|
||||
private Long tenantId;
|
||||
|
||||
@JsonProperty("projectId")
|
||||
private Long projectId;
|
||||
|
||||
/**
|
||||
* 事件时间
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* 事件ID(UUID,用于幂等性控制)
|
||||
*/
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 事件ID(UUID,用于幂等性控制)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* 事件ID(UUID,用于幂等性控制)
|
||||
*/
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 事件ID(UUID,用于幂等性控制)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* 事件ID(UUID,用于幂等性控制)
|
||||
*/
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 事件ID(UUID,用于幂等性控制)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* 事件ID(UUID,用于幂等性控制)
|
||||
*/
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 事件ID(UUID,用于幂等性控制)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,4 +62,6 @@ public class TrajectoryEnterEventDTO {
|
||||
* 租户ID
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
private Long projectId;
|
||||
}
|
||||
|
||||
@@ -69,4 +69,6 @@ public class TrajectoryLeaveEventDTO {
|
||||
* 租户ID
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
private Long projectId;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user