reactor:【IoT 物联网】合并 messagebus 和 common 包,统一到 core 包

This commit is contained in:
YunaiV
2025-05-29 07:33:34 +08:00
parent 6cf2eb07d7
commit 385cea8d90
21 changed files with 136 additions and 157 deletions

View File

@@ -0,0 +1,5 @@
package cn.iocoder.yudao.module.iot.core.biz;
// TODO @芋艿:待实现
public interface IotDeviceCommonApi {
}

View File

@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.iot.core.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
// TODO @芋艿:需要添加对应的 DTO以及上下行的链路网关、网关服务、设备等
/**
* IoT 设备消息标识符枚举
*/
@Getter
@RequiredArgsConstructor
public enum IotDeviceMessageIdentifierEnum {
PROPERTY_GET("get"), // 下行 TODO 芋艿【讨论】貌似这个“上行”更合理device 主动拉取配置。和 IotDevicePropertyGetReqDTO 一样的配置
PROPERTY_SET("set"), // 下行
PROPERTY_REPORT("report"), // 上行
STATE_ONLINE("online"), // 上行
STATE_OFFLINE("offline"), // 上行
CONFIG_GET("get"), // 上行 TODO 芋艿:【讨论】暂时没有上行的场景
CONFIG_SET("set"), // 下行
SERVICE_INVOKE("${identifier}"), // 下行
SERVICE_REPLY_SUFFIX("_reply"), // 芋艿TODO 芋艿:【讨论】上行 or 下行
OTA_UPGRADE("upgrade"), // 下行
OTA_PULL("pull"), // 上行
OTA_PROGRESS("progress"), // 上行
OTA_REPORT("report"), // 上行
REGISTER_REGISTER("register"), // 上行
REGISTER_REGISTER_SUB("register_sub"), // 上行
REGISTER_UNREGISTER_SUB("unregister_sub"), // 下行
TOPOLOGY_ADD("topology_add"), // 下行;
;
/**
* 标志符
*/
private final String identifier;
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.iot.core.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT 设备消息类型枚举
*/
@Getter
@RequiredArgsConstructor
public enum IotDeviceMessageTypeEnum implements ArrayValuable<String> {
STATE("state"), // 设备状态
PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置
OTA("ota"), // 设备 OTA可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级
REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册
TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new);
/**
* 属性
*/
private final String type;
@Override
public String[] array() {
return ARRAYS;
}
}

View File

@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.iot.core.messagebus.config;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.local.LocalIotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq.RocketMQIotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* IoT 消息总线自动配置
*
* @author 芋道源码
*/
@AutoConfiguration
@EnableConfigurationProperties(IotMessageBusProperties.class)
@Slf4j
public class IotMessageBusAutoConfiguration {
@Bean
public IotDeviceMessageProducer deviceMessageProducer(IotMessageBus messageBus) {
return new IotDeviceMessageProducer(messageBus);
}
// ==================== Local 实现 ====================
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "local", matchIfMissing = true)
public static class LocalIotMessageBusConfiguration {
@Bean
public IotMessageBus localIotMessageBus(ApplicationContext applicationContext) {
log.info("[localIotMessageBus][创建 Local IoT 消息总线]");
return new LocalIotMessageBus(applicationContext);
}
}
// ==================== RocketMQ 实现 ====================
@Configuration
@ConditionalOnProperty(prefix = "yudao.iot.message-bus", name = "type", havingValue = "rocketmq")
@ConditionalOnClass(RocketMQTemplate.class)
public static class RocketMQIotMessageBusConfiguration {
@Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public IotMessageBus rocketMQIotMessageBus(RocketMQProperties rocketMQProperties, RocketMQTemplate rocketMQTemplate) {
log.info("[rocketMQIotMessageBus][创建 RocketMQ IoT 消息总线]");
return new RocketMQIotMessageBus(rocketMQProperties, rocketMQTemplate);
}
}
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.iot.core.messagebus.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
/**
* IoT 消息总线配置属性
*
* @author 芋道源码
*/
@ConfigurationProperties("yudao.iot.message-bus")
@Data
@Validated
public class IotMessageBusProperties {
/**
* 消息总线类型
*
* 可选值local、redis、rocketmq、rabbitmq
*/
@NotNull(message = "IoT 消息总线类型不能为空")
private String type = "local";
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.iot.core.messagebus.core;
/**
* IoT 消息总线接口
*
* 用于在 IoT 系统中发布和订阅消息,支持多种消息中间件实现
*
* @author 芋道源码
*/
public interface IotMessageBus {
/**
* 发布消息到消息总线
*
* @param topic 主题
* @param message 消息内容
*/
void post(String topic, Object message);
/**
* 注册消息订阅者
*
* @param subscriber 订阅者
*/
void register(IotMessageBusSubscriber<?> subscriber);
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.iot.core.messagebus.core;
/**
* IoT 消息总线订阅者接口
*
* 用于处理从消息总线接收到的消息
*
* @author 芋道源码
*/
public interface IotMessageBusSubscriber<T> {
/**
* @return 主题
*/
String getTopic();
/**
* @return 分组
*/
String getGroup();
/**
* 处理接收到的消息
*
* @param message 消息内容
*/
void onMessage(T message);
}

View File

@@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.iot.core.messagebus.core.local;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class LocalIotMessage {
private String topic;
private Object message;
}

View File

@@ -0,0 +1,67 @@
package cn.iocoder.yudao.module.iot.core.messagebus.core.local;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBusSubscriber;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 本地的 {@link IotMessageBus} 实现类
*
* 注意:仅适用于单机场景!!!
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class LocalIotMessageBus implements IotMessageBus {
private final ApplicationContext applicationContext;
/**
* 订阅者映射表
* Key: topic
*/
private final Map<String, List<IotMessageBusSubscriber<?>>> subscribers = new HashMap<>();
@Override
public void post(String topic, Object message) {
applicationContext.publishEvent(new LocalIotMessage(topic, message));
}
@Override
public void register(IotMessageBusSubscriber<?> subscriber) {
String topic = subscriber.getTopic();
List<IotMessageBusSubscriber<?>> topicSubscribers = subscribers.computeIfAbsent(topic, k -> new ArrayList<>());
topicSubscribers.add(subscriber);
log.info("[register][topic({}/{}) 注册消费者({})成功]",
topic, subscriber.getGroup(), subscriber.getClass().getName());
}
@EventListener
@SuppressWarnings({"unchecked", "rawtypes"})
public void onMessage(LocalIotMessage message) {
String topic = message.getTopic();
List<IotMessageBusSubscriber<?>> topicSubscribers = subscribers.get(topic);
if (CollUtil.isEmpty(topicSubscribers)) {
return;
}
for (IotMessageBusSubscriber subscriber : topicSubscribers) {
try {
subscriber.onMessage(message.getMessage());
} catch (Exception ex) {
log.error("[onMessage][topic({}/{}) message({}) 消费者({}) 处理异常]",
subscriber.getTopic(), subscriber.getGroup(), message.getMessage(), subscriber.getClass().getName(), ex);
}
}
}
}

View File

@@ -0,0 +1,98 @@
package cn.iocoder.yudao.module.iot.core.messagebus.core.rocketmq;
import cn.hutool.core.util.TypeUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBusSubscriber;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import jakarta.annotation.PreDestroy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
/**
* 基于 RocketMQ 的 {@link IotMessageBus} 实现类
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class RocketMQIotMessageBus implements IotMessageBus {
private final RocketMQProperties rocketMQProperties;
private final RocketMQTemplate rocketMQTemplate;
/**
* 主题对应的消费者映射
*/
private final List<DefaultMQPushConsumer> topicConsumers = new ArrayList<>();
@Override
public void post(String topic, Object message) {
SendResult result = rocketMQTemplate.syncSend(topic, JsonUtils.toJsonString(message));
log.info("[post][topic({}) 发送消息({}) result({})]", topic, message, result);
}
@Override
@SneakyThrows
public void register(IotMessageBusSubscriber<?> subscriber) {
Type type = TypeUtil.getTypeArgument(subscriber.getClass(), 0);
if (type == null) {
throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
}
// 1.1 创建 DefaultMQPushConsumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
consumer.setNamesrvAddr(rocketMQProperties.getNameServer());
consumer.setConsumerGroup(subscriber.getGroup());
// 1.2 订阅主题
consumer.subscribe(subscriber.getTopic(), "*");
// 1.3 设置消息监听器
consumer.setMessageListener((MessageListenerConcurrently) (messages, context) -> {
for (MessageExt messageExt : messages) {
try {
byte[] body = messageExt.getBody();
subscriber.onMessage(JsonUtils.parseObject(body, type));
} catch (Exception ex) {
log.error("[onMessage][topic({}/{}) message({}) 消费者({}) 处理异常]",
subscriber.getTopic(), subscriber.getGroup(), messageExt, subscriber.getClass().getName(), ex);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
// 1.4 启动消费者
consumer.start();
// 2. 保存消费者引用
topicConsumers.add(consumer);
}
/**
* 销毁时关闭所有消费者
*/
@PreDestroy
public void destroy() {
for (DefaultMQPushConsumer consumer : topicConsumers) {
try {
consumer.shutdown();
log.info("[destroy][关闭 group({}) 的消费者成功]", consumer.getConsumerGroup());
} catch (Exception e) {
log.error("[destroy]关闭 group({}) 的消费者异常]", consumer.getConsumerGroup(), e);
}
}
}
}

View File

@@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.iot.core.mq.message;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageIdentifierEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
// TODO @芋艿:参考阿里云的物模型,优化 IoT 上下行消息的设计,尽量保持一致(渐进式,不要一口气)!
/**
* IoT 设备消息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class IotDeviceMessage {
/**
* 请求编号
*/
private String requestId;
/**
* 设备信息
*/
private String productKey;
/**
* 设备名称
*/
private String deviceName;
/**
* 设备标识
*/
private String deviceKey;
/**
* 消息类型
*
* 枚举 {@link IotDeviceMessageTypeEnum}
*/
private String type;
/**
* 标识符
*
* 枚举 {@link IotDeviceMessageIdentifierEnum}
*/
private String identifier;
/**
* 请求参数
*
* 例如说:属性上报的 properties、事件上报的 params
*/
private Object data;
/**
* 响应码
*
* 目前只有 server 下行消息给 device 设备时,才会有响应码
*/
private Integer code;
/**
* 上报时间
*/
private LocalDateTime reportTime;
/**
* 租户编号
*/
private Long tenantId;
}

View File

@@ -0,0 +1,48 @@
package cn.iocoder.yudao.module.iot.core.mq.producer;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import lombok.RequiredArgsConstructor;
/**
* IoT 设备消息生产者
*
* @author 芋道源码
*/
@RequiredArgsConstructor
public class IotDeviceMessageProducer {
/**
* 【消息总线】应用的设备消息 Topic由 iot-gateway 发给 iot-biz 进行消费
*/
private static final String MESSAGE_BUS_DEVICE_MESSAGE_TOPIC = "iot_device_message";
/**
* 【消息总线】设备消息 Topic由 iot-biz 发送给 iot-gateway 的某个 “server”(protocol) 进行消费
*
* 其中,%s 就是该“server”(protocol) 的标识
*/
private static final String MESSAGE_BUS_GATEWAY_DEVICE_MESSAGE_TOPIC = MESSAGE_BUS_DEVICE_MESSAGE_TOPIC + "/%s";
private final IotMessageBus messageBus;
/**
* 发送设备消息
*
* @param message 设备消息
*/
public void sendDeviceMessage(IotDeviceMessage message) {
messageBus.post(MESSAGE_BUS_DEVICE_MESSAGE_TOPIC, message);
}
/**
* 发送网关设备消息
*
* @param server 网关的 server 标识
* @param message 设备消息
*/
public void sendGatewayDeviceMessage(String server, Object message) {
messageBus.post(String.format(MESSAGE_BUS_GATEWAY_DEVICE_MESSAGE_TOPIC, server), message);
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.module.iot.core.messagebus.config.IotMessageBusAutoConfiguration