reactor:【IoT 物联网】清理 components 和 protocol,基本已经融合到 gateway

This commit is contained in:
YunaiV
2025-06-09 23:28:24 +08:00
parent 479e0356ad
commit 800a85f7bc
63 changed files with 1 additions and 5759 deletions

View File

@@ -9,8 +9,6 @@
<modules>
<module>yudao-module-iot-api</module>
<module>yudao-module-iot-biz</module>
<module>yudao-module-iot-net-components</module>
<module>yudao-module-iot-protocol</module>
<module>yudao-module-iot-core</module>
<module>yudao-module-iot-gateway</module>
</modules>

View File

@@ -1,22 +0,0 @@
package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.Map;
/**
* IoT 设备【配置】设置 Request DTO
*
* @author 芋道源码
*/
@Data
public class IotDeviceConfigSetReqDTO extends IotDeviceDownstreamAbstractReqDTO {
/**
* 配置
*/
@NotNull(message = "配置不能为空")
private Map<String, Object> config;
}

View File

@@ -1,30 +0,0 @@
package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
/**
* IoT 设备下行的抽象 Request DTO
*
* @author 芋道源码
*/
@Data
public abstract class IotDeviceDownstreamAbstractReqDTO {
/**
* 请求编号
*/
private String requestId;
/**
* 产品标识
*/
@NotEmpty(message = "产品标识不能为空")
private String productKey;
/**
* 设备名称
*/
@NotEmpty(message = "设备名称不能为空")
private String deviceName;
}

View File

@@ -1,66 +0,0 @@
package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream;
import cn.hutool.core.map.MapUtil;
import lombok.Data;
import java.util.Map;
/**
* IoT 设备【OTA】升级下发 Request DTO更新固件消息
*
* @author 芋道源码
*/
@Data
public class IotDeviceOtaUpgradeReqDTO extends IotDeviceDownstreamAbstractReqDTO {
/**
* 固件编号
*/
private Long firmwareId;
/**
* 固件版本
*/
private String version;
/**
* 签名方式
*
* 例如说MD5、SHA256
*/
private String signMethod;
/**
* 固件文件签名
*/
private String fileSign;
/**
* 固件文件大小
*/
private Long fileSize;
/**
* 固件文件 URL
*/
private String fileUrl;
/**
* 自定义信息,建议使用 JSON 格式
*/
private String information;
public static IotDeviceOtaUpgradeReqDTO build(Map<?, ?> map) {
return new IotDeviceOtaUpgradeReqDTO()
.setFirmwareId(MapUtil.getLong(map, "firmwareId")).setVersion((String) map.get("version"))
.setSignMethod((String) map.get("signMethod")).setFileSign((String) map.get("fileSign"))
.setFileSize(MapUtil.getLong(map, "fileSize")).setFileUrl((String) map.get("fileUrl"))
.setInformation((String) map.get("information"));
}
public static Map<?, ?> build(IotDeviceOtaUpgradeReqDTO dto) {
return MapUtil.builder()
.put("firmwareId", dto.getFirmwareId()).put("version", dto.getVersion())
.put("signMethod", dto.getSignMethod()).put("fileSign", dto.getFileSign())
.put("fileSize", dto.getFileSize()).put("fileUrl", dto.getFileUrl())
.put("information", dto.getInformation())
.build();
}
}

View File

@@ -1,24 +0,0 @@
package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
// TODO @芋艿:从 server => plugin => device 是否有必要?从阿里云 iot 来看,没有这个功能?!
// TODO @芋艿:是不是改成 read 更好?在看看阿里云的 topic 设计
/**
* IoT 设备【属性】获取 Request DTO
*
* @author 芋道源码
*/
@Data
public class IotDevicePropertyGetReqDTO extends IotDeviceDownstreamAbstractReqDTO {
/**
* 属性标识数组
*/
@NotEmpty(message = "属性标识数组不能为空")
private List<String> identifiers;
}

View File

@@ -1,22 +0,0 @@
package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.Map;
/**
* IoT 设备【属性】设置 Request DTO
*
* @author 芋道源码
*/
@Data
public class IotDevicePropertySetReqDTO extends IotDeviceDownstreamAbstractReqDTO {
/**
* 属性参数
*/
@NotEmpty(message = "属性参数不能为空")
private Map<String, Object> properties;
}

View File

@@ -1,26 +0,0 @@
package cn.iocoder.yudao.module.iot.api.device.dto.control.downstream;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.Map;
/**
* IoT 设备【服务】调用 Request DTO
*
* @author 芋道源码
*/
@Data
public class IotDeviceServiceInvokeReqDTO extends IotDeviceDownstreamAbstractReqDTO {
/**
* 服务标识
*/
@NotEmpty(message = "服务标识不能为空")
private String identifier;
/**
* 调用参数
*/
private Map<String, Object> params;
}

View File

@@ -1,35 +0,0 @@
package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream;
import lombok.Data;
// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/progress
/**
* IoT 设备【OTA】升级进度 Request DTO上报更新固件进度
*
* @author 芋道源码
*/
@Data
public class IotDeviceOtaProgressReqDTO extends IotDeviceUpstreamAbstractReqDTO {
/**
* 固件编号
*/
private Long firmwareId;
/**
* 升级状态
*
* 枚举 {@link cn.iocoder.yudao.module.iot.enums.ota.IotOtaUpgradeRecordStatusEnum}
*/
private Integer status;
/**
* 升级进度,百分比
*/
private Integer progress;
/**
* 升级进度描述
*/
private String description;
}

View File

@@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream;
// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/pull
/**
* IoT 设备【OTA】升级下拉 Request DTO拉取固件更新
*
* @author 芋道源码
*/
public class IotDeviceOtaPullReqDTO {
/**
* 固件编号
*/
private Long firmwareId;
/**
* 固件版本
*/
private String version;
}

View File

@@ -1,21 +0,0 @@
package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream;
// TODO @芋艿:待实现:/ota/${productKey}/${deviceName}/report
/**
* IoT 设备【OTA】上报 Request DTO上报固件版本
*
* @author 芋道源码
*/
public class IotDeviceOtaReportReqDTO {
/**
* 固件编号
*/
private Long firmwareId;
/**
* 固件版本
*/
private String version;
}

View File

@@ -1,16 +0,0 @@
package cn.iocoder.yudao.module.iot.enums;
import cn.iocoder.yudao.framework.common.enums.RpcConstants;
/**
* API 相关的枚举
*
* @author 芋道源码
*/
public class ApiConstants {
public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/iot";
public static final String VERSION = "1.0.0";
}

View File

@@ -15,8 +15,5 @@ public class DictTypeConstants {
public static final String VALIDATE_TYPE = "iot_validate_type";
public static final String DEVICE_STATE = "iot_device_state";
public static final String IOT_DATA_BRIDGE_DIRECTION_ENUM = "iot_data_bridge_direction_enum";
public static final String IOT_DATA_BRIDGE_TYPE_ENUM = "iot_data_bridge_type_enum";
}

View File

@@ -1,37 +0,0 @@
package cn.iocoder.yudao.module.iot.enums.plugin;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT 部署方式枚举
*
* @author haohao
*/
@RequiredArgsConstructor
@Getter
public enum IotPluginDeployTypeEnum implements ArrayValuable<Integer> {
JAR(0, "JAR 部署"),
STANDALONE(1, "独立部署");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginDeployTypeEnum::getDeployType).toArray(Integer[]::new);
/**
* 部署方式
*/
private final Integer deployType;
/**
* 部署方式名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@@ -1,37 +0,0 @@
package cn.iocoder.yudao.module.iot.enums.plugin;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* IoT 插件状态枚举
*
* @author haohao
*/
@RequiredArgsConstructor
@Getter
public enum IotPluginStatusEnum implements ArrayValuable<Integer> {
STOPPED(0, "停止"),
RUNNING(1, "运行");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginStatusEnum::getStatus).toArray(Integer[]::new);
/**
* 状态
*/
private final Integer status;
/**
* 状态名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@@ -1,37 +0,0 @@
package cn.iocoder.yudao.module.iot.enums.plugin;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IoT 插件类型枚举
*
* @author haohao
*/
@AllArgsConstructor
@Getter
public enum IotPluginTypeEnum implements ArrayValuable<Integer> {
NORMAL(0, "普通插件"),
DEVICE(1, "设备插件");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotPluginTypeEnum::getType).toArray(Integer[]::new);
/**
* 类型
*/
private final Integer type;
/**
* 类型名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@@ -1,47 +0,0 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IoT 产品脚本语言枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum IotProductScriptLanguageEnum implements ArrayValuable<String> {
JAVASCRIPT("javascript", "JavaScript"),
JAVA("java", "Java"),
PYTHON("python", "Python"),
;
public static final String[] ARRAYS = Arrays.stream(values()).map(IotProductScriptLanguageEnum::getCode)
.toArray(String[]::new);
/**
* 编码
*/
private final String code;
/**
* 名称
*/
private final String name;
@Override
public String[] array() {
return ARRAYS;
}
public static IotProductScriptLanguageEnum getByCode(String code) {
return Arrays.stream(values())
.filter(type -> type.getCode().equals(code))
.findFirst()
.orElse(null);
}
}

View File

@@ -1,54 +0,0 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
// TODO @haohao要不复用 commonstatus
/**
* IoT 产品脚本状态枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum IotProductScriptStatusEnum implements ArrayValuable<Integer> {
ENABLE(0, "启用"),
DISABLE(1, "禁用"),
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotProductScriptStatusEnum::getStatus)
.toArray(Integer[]::new);
/**
* 状态值
*/
private final Integer status;
/**
* 状态名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
public static IotProductScriptStatusEnum getByStatus(Integer status) {
return Arrays.stream(values())
.filter(type -> type.getStatus().equals(status))
.findFirst()
.orElse(null);
}
public static boolean isEnable(Integer status) {
return ENABLE.getStatus().equals(status);
}
public static boolean isDisable(Integer status) {
return DISABLE.getStatus().equals(status);
}
}

View File

@@ -1,50 +0,0 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IoT 产品脚本类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum IotProductScriptTypeEnum implements ArrayValuable<Integer> {
PROPERTY_PARSER(1, "property_parser", "属性解析"),
EVENT_PARSER(2, "event_parser", "事件解析"),
COMMAND_ENCODER(3, "command_encoder", "命令编码"),
;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotProductScriptTypeEnum::getCode)
.toArray(Integer[]::new);
/**
* 编码
*/
private final Integer code;
/**
* 类型
*/
private final String type;
/**
* 名称
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
public static IotProductScriptTypeEnum getByCode(Integer code) {
return Arrays.stream(values())
.filter(type -> type.getCode().equals(code))
.findFirst()
.orElse(null);
}
}

View File

@@ -55,7 +55,7 @@ public class IotDeviceDownstreamServiceImpl implements IotDeviceDownstreamServic
if (StrUtil.isEmpty(serverId)) {
throw exception(DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL);
}
deviceMessageProducer.sendGatewayDeviceMessage(serverId, message);
deviceMessageProducer.sendDeviceMessageToGateway(serverId, message);
// 3.2 发送给服务器(用于设备日志等的记录)
deviceMessageProducer.sendDeviceMessage(message);

View File

@@ -1 +0,0 @@
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-module-iot</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-iot-net-components</artifactId>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>
物联网网络组件模块,提供与物联网设备通讯、管理的网络组件实现
</description>
<modules>
<module>yudao-module-iot-net-component-core</module>
<module>yudao-module-iot-net-component-emqx</module>
<module>yudao-module-iot-net-component-server</module>
</modules>
</project>

View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-module-iot-net-components</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-iot-net-component-core</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
物联网网络组件核心模块
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- IoT 协议模块 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-protocol</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<optional>true</optional>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -1,153 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.core.message;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONObject;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
/**
* IoT MQTT 消息模型
* <p>
* 基于 MQTT 协议规范实现的标准消息格式,兼容 Alink 协议
*
* @author haohao
*/
@Data
@Builder
public class IotMqttMessage {
/**
* 消息 ID
*/
private String id;
/**
* 协议版本
*/
@Builder.Default
private String version = "1.0";
/**
* 消息方法
*/
private String method;
/**
* 消息参数
*/
private Map<String, Object> params;
/**
* 转换为 JSONObject
*
* @return JSONObject 对象
*/
public JSONObject toJsonObject() {
JSONObject json = new JSONObject();
json.set("id", id);
json.set("version", version);
json.set("method", method);
json.set("params", params != null ? params : new JSONObject());
return json;
}
/**
* 转换为 JSON 字符串
*
* @return JSON 字符串
*/
public String toJsonString() {
return toJsonObject().toString();
}
/**
* 创建设备服务调用消息
*
* @param requestId 请求 ID为空时自动生成
* @param serviceIdentifier 服务标识符
* @param params 服务参数
* @return MQTT 消息对象
*/
public static IotMqttMessage createServiceInvokeMessage(String requestId, String serviceIdentifier,
Map<String, Object> params) {
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service." + serviceIdentifier)
.params(params)
.build();
}
/**
* 创建设备属性设置消息
*
* @param requestId 请求 ID为空时自动生成
* @param properties 设备属性
* @return MQTT 消息对象
*/
public static IotMqttMessage createPropertySetMessage(String requestId, Map<String, Object> properties) {
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service.property.set")
.params(properties)
.build();
}
/**
* 创建设备属性获取消息
*
* @param requestId 请求 ID为空时自动生成
* @param identifiers 要获取的属性标识符列表
* @return MQTT 消息对象
*/
public static IotMqttMessage createPropertyGetMessage(String requestId, String[] identifiers) {
JSONObject params = new JSONObject();
params.set("identifiers", identifiers);
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service.property.get")
.params(params)
.build();
}
/**
* 创建设备配置设置消息
*
* @param requestId 请求 ID为空时自动生成
* @param configs 设备配置
* @return MQTT 消息对象
*/
public static IotMqttMessage createConfigSetMessage(String requestId, Map<String, Object> configs) {
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service.config.set")
.params(configs)
.build();
}
/**
* 创建设备 OTA 升级消息
*
* @param requestId 请求 ID为空时自动生成
* @param otaInfo OTA 升级信息
* @return MQTT 消息对象
*/
public static IotMqttMessage createOtaUpgradeMessage(String requestId, Map<String, Object> otaInfo) {
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service.ota.upgrade")
.params(otaInfo)
.build();
}
/**
* 生成请求 ID
*
* @return 请求 ID
*/
public static String generateRequestId() {
return IdUtil.fastSimpleUUID();
}
}

View File

@@ -1,93 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.core.pojo;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
/**
* IoT 标准协议响应实体类
* <p>
* 用于统一 MQTT 和 HTTP 的响应格式
*
* @author haohao
*/
@Data
public class IotStandardResponse {
/**
* 消息 ID
*/
private String id;
/**
* 状态码
*/
private Integer code;
/**
* 响应数据
*/
private Object data;
/**
* 响应消息
*/
private String message;
/**
* 方法名
*/
private String method;
/**
* 协议版本
*/
private String version;
/**
* 创建成功响应
*
* @param id 消息 ID
* @param method 方法名
* @return 成功响应
*/
public static IotStandardResponse success(String id, String method) {
return success(id, method, null);
}
/**
* 创建成功响应
*
* @param id 消息 ID
* @param method 方法名
* @param data 响应数据
* @return 成功响应
*/
public static IotStandardResponse success(String id, String method, Object data) {
return new IotStandardResponse()
.setId(id)
.setCode(200)
.setData(data)
.setMessage("success")
.setMethod(method)
.setVersion("1.0");
}
/**
* 创建错误响应
*
* @param id 消息 ID
* @param method 方法名
* @param code 错误码
* @param message 错误消息
* @return 错误响应
*/
public static IotStandardResponse error(String id, String method, Integer code, String message) {
return new IotStandardResponse()
.setId(id)
.setCode(code)
.setData(null)
.setMessage(StrUtil.blankToDefault(message, "error"))
.setMethod(method)
.setVersion("1.0");
}
}

View File

@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-module-iot-net-components</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-iot-net-component-emqx</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
物联网网络组件 EMQX 模块
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-net-component-core</artifactId>
<version>${revision}</version>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mqtt</artifactId>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -1,83 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.emqx.config;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
import cn.iocoder.yudao.module.iot.net.component.emqx.upstream.IotDeviceUpstreamServer;
import io.vertx.core.Vertx;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.MqttClientOptions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.event.EventListener;
/**
* IoT 网络组件 EMQX 的自动配置类
*
* @author haohao
*/
@AutoConfiguration
@EnableConfigurationProperties(IotNetComponentEmqxProperties.class)
@ConditionalOnProperty(prefix = "yudao.iot.component.emqx", name = "enabled", havingValue = "true")
@ComponentScan(basePackages = {
"cn.iocoder.yudao.module.iot.net.component.emqx" // 只扫描 EMQX 组件包
}) // TODO @haohao自动配置后不需要这个哈。
@Slf4j
public class IotNetComponentEmqxAutoConfiguration {
/**
* 初始化 EMQX 组件
*
* @param event 应用启动事件
*/
@EventListener(ApplicationStartedEvent.class)
public void initialize(ApplicationStartedEvent event) {
log.info("[IotNetComponentEmqxAutoConfiguration][开始初始化]");
// 从应用上下文中获取需要的 Bean
// TODO @芋艿:看看要不要监听下
log.info("[initialize][IoT EMQX 组件初始化完成]");
}
/**
* 创建 Vert.x 实例
*/
@Bean(name = "emqxVertx")
public Vertx vertx() {
return Vertx.vertx();
}
/**
* 创建 MQTT 客户端
*/
@Bean
public MqttClient mqttClient(@Qualifier("emqxVertx") Vertx vertx, IotNetComponentEmqxProperties emqxProperties) {
MqttClientOptions options = new MqttClientOptions()
.setClientId("yudao-iot-downstream-" + IdUtil.fastSimpleUUID())
.setUsername(emqxProperties.getMqttUsername())
.setPassword(emqxProperties.getMqttPassword());
// 设置 SSL 选项
options.setSsl(ObjUtil.defaultIfNull(emqxProperties.getMqttSsl(), false));
return MqttClient.create(vertx, options);
}
/**
* 创建设备上行服务器
*/
@Bean(name = "emqxDeviceUpstreamServer", initMethod = "start", destroyMethod = "stop")
public IotDeviceUpstreamServer deviceUpstreamServer(
IotDeviceUpstreamApi deviceUpstreamApi,
IotNetComponentEmqxProperties emqxProperties,
@Qualifier("emqxVertx") Vertx vertx,
MqttClient mqttClient) {
return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi, vertx, mqttClient);
}
}

View File

@@ -1,82 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.emqx.config;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* IoT EMQX 网络组件配置属性
*
* @author haohao
*/
@ConfigurationProperties(prefix = "yudao.iot.component.emqx")
@Data
@Validated
public class IotNetComponentEmqxProperties {
/**
* 是否启用 EMQX 组件
*/
private Boolean enabled;
/**
* MQTT 服务主机
*/
@NotBlank(message = "MQTT 服务器主机不能为空")
private String mqttHost;
/**
* MQTT 服务端口
*/
@NotNull(message = "MQTT 服务器端口不能为空")
private Integer mqttPort;
/**
* MQTT 服务用户名
*/
@NotBlank(message = "MQTT 服务器用户名不能为空")
private String mqttUsername;
/**
* MQTT 服务密码
*/
@NotBlank(message = "MQTT 服务器密码不能为空")
private String mqttPassword;
/**
* 是否启用 SSL
*/
@NotNull(message = "MQTT SSL 配置不能为空")
private Boolean mqttSsl;
/**
* 订阅的主题列表
*/
@NotEmpty(message = "MQTT 订阅主题不能为空")
private String[] mqttTopics;
/**
* 认证端口
*/
@NotNull(message = "认证端口不能为空")
private Integer authPort;
// TODO @haohao可以使用 Duration 类型,可读性更好
/**
* 重连延迟时间(毫秒)
* <p>
* 默认值5000 毫秒
*/
private Integer reconnectDelayMs = 5000;
/**
* 连接超时时间(毫秒)
* <p>
* 默认值10000 毫秒
*/
private Integer connectionTimeoutMs = 10000;
}

View File

@@ -1,121 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.emqx.downstream;
// TODO @芋艿:后续再支持下;@haohao改成消费者
///**
// * EMQX 网络组件的 {@link IotDeviceDownstreamHandler} 实现类
// *
// * @author 芋道源码
// */
//@Slf4j
//public class IotDeviceDownstreamHandlerImpl {
//
// /**
// * MQTT 客户端
// */
// private final MqttClient mqttClient;
//
// /**
// * 构造函数
// *
// * @param mqttClient MQTT 客户端
// */
// public IotDeviceDownstreamHandlerImpl(MqttClient mqttClient) {
// this.mqttClient = mqttClient;
// }
//
// @Override
// public CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO reqDTO) {
// log.info("[invokeService][开始调用设备服务][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
//
// // 验证参数
// if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null || reqDTO.getIdentifier() == null) {
// log.error("[invokeService][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
// return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
// }
//
// try {
// // 构建请求主题
// String topic = IotDeviceTopicEnum.buildServiceTopic(reqDTO.getProductKey(), reqDTO.getDeviceName(),
// reqDTO.getIdentifier());
//
// // 构建请求消息
// String requestId = StrUtil.isNotEmpty(reqDTO.getRequestId()) ? reqDTO.getRequestId()
// : IotNetComponentCommonUtils.generateRequestId();
// IotMqttMessage message = IotMqttMessage.createServiceInvokeMessage(
// requestId, reqDTO.getIdentifier(), reqDTO.getParams());
//
// // 发送消息
// publishMessage(topic, message.toJsonObject());
//
// log.info("[invokeService][调用设备服务成功][requestId: {}][topic: {}]", requestId, topic);
// return CommonResult.success(true);
// } catch (Exception e) {
// log.error("[invokeService][调用设备服务异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e);
// return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
// }
// }
//
// @Override
// public CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) {
// // 暂未实现,返回成功
// return CommonResult.success(true);
// }
//
// @Override
// public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO reqDTO) {
// log.info("[setProperty][开始设置设备属性][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
//
// // 验证参数
// if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null) {
// log.error("[setProperty][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
// return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
// }
//
// try {
// // 构建请求主题
// String topic = IotDeviceTopicEnum.buildPropertySetTopic(reqDTO.getProductKey(), reqDTO.getDeviceName());
//
// // 构建请求消息
// String requestId = StrUtil.isNotEmpty(reqDTO.getRequestId()) ? reqDTO.getRequestId()
// : IotNetComponentCommonUtils.generateRequestId();
// IotMqttMessage message = IotMqttMessage.createPropertySetMessage(requestId, reqDTO.getProperties());
//
// // 发送消息
// publishMessage(topic, message.toJsonObject());
//
// log.info("[setProperty][设置设备属性成功][requestId: {}][topic: {}]", requestId, topic);
// return CommonResult.success(true);
// } catch (Exception e) {
// log.error("[setProperty][设置设备属性异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e);
// return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
// }
// }
//
// @Override
// public CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) {
// // 暂未实现,返回成功
// return CommonResult.success(true);
// }
//
// @Override
// public CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) {
// // 暂未实现,返回成功
// return CommonResult.success(true);
// }
//
// /**
// * 发布 MQTT 消息
// *
// * @param topic 主题
// * @param payload 消息内容
// */
// private void publishMessage(String topic, JSONObject payload) {
// mqttClient.publish(
// topic,
// Buffer.buffer(payload.toString()),
// MqttQoS.AT_LEAST_ONCE,
// false,
// false);
// log.info("[publishMessage][发送消息成功][topic: {}][payload: {}]", topic, payload);
// }
//}

View File

@@ -1,261 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.emqx.upstream;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
import cn.iocoder.yudao.module.iot.net.component.emqx.config.IotNetComponentEmqxProperties;
import cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router.IotDeviceAuthVertxHandler;
import cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router.IotDeviceMqttMessageHandler;
import cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router.IotDeviceWebhookVertxHandler;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServer;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.mqtt.MqttClient;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* IoT 设备上行服务端,接收来自 device 设备的请求,转发给 server 服务器
* <p>
* 协议HTTP、MQTT
*
* @author haohao
*/
@Slf4j
public class IotDeviceUpstreamServer {
/**
* 默认 QoS 级别
*/
private static final MqttQoS DEFAULT_QOS = MqttQoS.AT_LEAST_ONCE;
private final Vertx vertx;
private final HttpServer server;
private final MqttClient client;
private final IotNetComponentEmqxProperties emqxProperties;
private final IotDeviceMqttMessageHandler mqttMessageHandler;
/**
* 服务运行状态标志
*/
private volatile boolean isRunning = false;
public IotDeviceUpstreamServer(IotNetComponentEmqxProperties emqxProperties,
IotDeviceUpstreamApi deviceUpstreamApi,
Vertx vertx,
MqttClient client) {
this.vertx = vertx;
this.emqxProperties = emqxProperties;
this.client = client;
// 创建 Router 实例
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create()); // 处理 Body
router.post(IotDeviceAuthVertxHandler.PATH)
// MQTT 认证不必须通过 HTTP 进行,但 HTTP 认证是 EMQX 等 MQTT 服务器支持的一种灵活的认证方式
.handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi));
// 添加 Webhook 处理器,用于处理设备连接和断开连接事件
router.post(IotDeviceWebhookVertxHandler.PATH)
.handler(new IotDeviceWebhookVertxHandler(deviceUpstreamApi));
// 创建 HttpServer 实例
this.server = vertx.createHttpServer().requestHandler(router);
this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client);
}
/**
* 启动 HTTP 服务器、MQTT 客户端
*/
public void start() {
if (isRunning) {
log.warn("[start][服务已经在运行中,请勿重复启动]");
return;
}
log.info("[start][开始启动服务]");
// 检查 authPort 是否为 null
// TODO @haohaoauthPort 里面搞默认值?包括下面,这个类不搞任何默认值,都交给 emqxProperties
Integer authPort = emqxProperties.getAuthPort();
if (authPort == null) {
log.warn("[start][authPort 为 null使用默认端口 8080]");
authPort = 8080; // 默认端口
}
// 获取连接超时时间
int connectionTimeoutMs = emqxProperties.getConnectionTimeoutMs() != null
? emqxProperties.getConnectionTimeoutMs()
: 10000;
// 1. 启动 HTTP 服务器
final Integer finalAuthPort = authPort; // 为 lambda 表达式创建 final 变量
CompletableFuture<Void> httpFuture = server.listen(finalAuthPort)
.toCompletionStage()
.toCompletableFuture()
.thenAccept(v -> log.info("[start][HTTP 服务器启动完成,端口: {}]", server.actualPort()));
// 2. 连接 MQTT Broker
CompletableFuture<Void> mqttFuture = connectMqtt()
.toCompletionStage()
.toCompletableFuture()
.thenAccept(v -> {
// 2.1 添加 MQTT 断开重连监听器
client.closeHandler(closeEvent -> {
log.warn("[closeHandler][MQTT 连接已断开,准备重连]");
reconnectWithDelay();
});
// 2.2 设置 MQTT 消息处理器
setupMessageHandler();
});
// 3. 等待所有服务启动完成
CompletableFuture.allOf(httpFuture, mqttFuture)
.orTimeout(connectionTimeoutMs, TimeUnit.MILLISECONDS)
.whenComplete((result, error) -> {
if (error != null) {
log.error("[start][服务启动失败]", error);
} else {
isRunning = true;
log.info("[start][所有服务启动完成]");
}
});
}
/**
* 设置 MQTT 消息处理器
*/
private void setupMessageHandler() {
client.publishHandler(mqttMessageHandler::handle);
log.debug("[setupMessageHandler][MQTT 消息处理器设置完成]");
}
/**
* 重连 MQTT 客户端
*/
private void reconnectWithDelay() {
if (!isRunning) {
log.info("[reconnectWithDelay][服务已停止,不再尝试重连]");
return;
}
// 获取重连延迟时间
int reconnectDelayMs = emqxProperties.getReconnectDelayMs() != null
? emqxProperties.getReconnectDelayMs()
: 5000;
vertx.setTimer(reconnectDelayMs, id -> {
log.info("[reconnectWithDelay][开始重新连接 MQTT]");
connectMqtt();
});
}
/**
* 连接 MQTT Broker 并订阅主题
*
* @return 连接结果的 Future
*/
private Future<Void> connectMqtt() {
// 检查必要的 MQTT 配置
String host = emqxProperties.getMqttHost();
Integer port = emqxProperties.getMqttPort();
if (StrUtil.isBlank(host)) {
String msg = "[connectMqtt][MQTT Host 为空,无法连接]";
log.error(msg);
return Future.failedFuture(new IllegalStateException(msg));
}
if (port == null) {
log.warn("[connectMqtt][MQTT Port 为 null使用默认端口 1883]");
port = 1883; // 默认 MQTT 端口
}
final Integer finalPort = port;
return client.connect(finalPort, host)
.compose(connAck -> {
log.info("[connectMqtt][MQTT 客户端连接成功]");
return subscribeToTopics();
})
.recover(error -> {
log.error("[connectMqtt][连接 MQTT Broker 失败:]", error);
reconnectWithDelay();
return Future.failedFuture(error);
});
}
/**
* 订阅设备上行消息主题
*
* @return 订阅结果的 Future
*/
private Future<Void> subscribeToTopics() {
String[] topics = emqxProperties.getMqttTopics();
if (ArrayUtil.isEmpty(topics)) {
log.warn("[subscribeToTopics][未配置 MQTT 主题或为 null使用默认主题]");
topics = new String[]{"/device/#"}; // 默认订阅所有设备上下行主题
}
// 使用协调器追踪多个 Future 的完成状态
Future<Void> result = Future.succeededFuture();
for (String topic : topics) {
if (StrUtil.isBlank(topic)) {
log.warn("[subscribeToTopics][跳过空主题]");
continue;
}
result = result.compose(v -> client.subscribe(topic, DEFAULT_QOS.value())
.<Void>map(ack -> {
log.info("[subscribeToTopics][订阅主题成功: {}]", topic);
return null;
})
.recover(err -> {
log.error("[subscribeToTopics][订阅主题失败: {}]", topic, err);
return Future.failedFuture(err);
}));
}
return result;
}
/**
* 停止服务
*/
public void stop() {
if (!isRunning) {
log.warn("[stop][服务已经停止,无需再次停止]");
return;
}
log.info("[stop][开始停止服务]");
// 1. 取消 MQTT 主题订阅
if (client.isConnected()) {
for (String topic : emqxProperties.getMqttTopics()) {
try {
client.unsubscribe(topic);
} catch (Exception e) {
log.warn("[stop][取消订阅主题异常: {}]", topic, e);
}
}
}
// 2. 关闭 MQTT 客户端
try {
if (client.isConnected()) {
client.disconnect();
}
} catch (Exception e) {
log.warn("[stop][关闭 MQTT 客户端异常]", e);
}
// 3. 关闭 HTTP 服务器
try {
server.close();
} catch (Exception e) {
log.warn("[stop][关闭 HTTP 服务器异常]", e);
}
// 4. 更新状态
isRunning = false;
log.info("[stop][服务已停止]");
}
}

View File

@@ -1,64 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceEmqxAuthReqDTO;
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
/**
* IoT EMQX 连接认证的 Vert.x Handler
* <p>
* 参考:<a href="https://docs.emqx.com/zh/emqx/latest/access-control/authn/http.html">EMQX HTTP</a>
* <p>
* 注意:该处理器需要返回特定格式:{"result": "allow"} 或 {"result": "deny"}
* 以符合 EMQX 认证插件的要求,因此不使用 IotStandardResponse 实体类
*
* @author haohao
*/
@RequiredArgsConstructor
@Slf4j
public class IotDeviceAuthVertxHandler implements Handler<RoutingContext> {
public static final String PATH = "/mqtt/auth";
private final IotDeviceUpstreamApi deviceUpstreamApi;
@Override
public void handle(RoutingContext routingContext) {
try {
// 构建认证请求 DTO
JsonObject json = routingContext.body().asJsonObject();
String clientId = json.getString("clientid");
String username = json.getString("username");
String password = json.getString("password");
IotDeviceEmqxAuthReqDTO authReqDTO = new IotDeviceEmqxAuthReqDTO()
.setClientId(clientId)
.setUsername(username)
.setPassword(password);
// 调用认证 API
CommonResult<Boolean> authResult = deviceUpstreamApi.authenticateEmqxConnection(authReqDTO);
if (authResult.getCode() != 0 || !authResult.getData()) {
// 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny"));
return;
}
// 响应结果
// 注意:这里必须返回 {"result": "allow"} 格式,以符合 EMQX 认证插件的要求
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "allow"));
} catch (Exception e) {
log.error("[handle][EMQX 认证异常]", e);
// 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny"));
}
}
}

View File

@@ -1,287 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
import cn.iocoder.yudao.module.iot.net.component.core.constants.IotDeviceTopicEnum;
import cn.iocoder.yudao.module.iot.net.component.core.pojo.IotStandardResponse;
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.buffer.Buffer;
import io.vertx.mqtt.MqttClient;
import io.vertx.mqtt.messages.MqttPublishMessage;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* IoT 设备 MQTT 消息处理器
* <p>
* 参考:<a href=
* "https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services">设备属性、事件、服务</a>
*/
@Slf4j
public class IotDeviceMqttMessageHandler {
// TODO @haohao下面的有办法也抽到 IotDeviceTopicEnum 么?想的是,尽量把这些 method、topic、url 统一化;
private static final String PROPERTY_METHOD = "thing.event.property.post";
private static final String EVENT_METHOD_PREFIX = "thing.event.";
private static final String EVENT_METHOD_SUFFIX = ".post";
private final IotDeviceUpstreamApi deviceUpstreamApi;
private final MqttClient mqttClient;
public IotDeviceMqttMessageHandler(IotDeviceUpstreamApi deviceUpstreamApi, MqttClient mqttClient) {
this.deviceUpstreamApi = deviceUpstreamApi;
this.mqttClient = mqttClient;
}
/**
* 处理MQTT消息
*
* @param message MQTT发布消息
*/
public void handle(MqttPublishMessage message) {
String topic = message.topicName();
String payload = message.payload().toString();
log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload);
try {
if (StrUtil.isEmpty(payload)) {
log.warn("[messageHandler][消息内容为空][topic: {}]", topic);
return;
}
handleMessage(topic, payload);
} catch (Exception e) {
log.error("[messageHandler][处理消息失败][topic: {}][payload: {}]", topic, payload, e);
}
}
/**
* 根据主题类型处理消息
*
* @param topic 主题
* @param payload 消息内容
*/
private void handleMessage(String topic, String payload) {
// 校验前缀
if (!topic.startsWith(IotDeviceTopicEnum.SYS_TOPIC_PREFIX.getTopic())) {
log.warn("[handleMessage][未知的消息类型][topic: {}]", topic);
return;
}
// 处理设备属性上报消息
if (topic.endsWith(IotDeviceTopicEnum.PROPERTY_POST_TOPIC.getTopic())) {
log.info("[handleMessage][接收到设备属性上报][topic: {}]", topic);
handlePropertyPost(topic, payload);
return;
}
// 处理设备事件上报消息
if (topic.contains(IotDeviceTopicEnum.EVENT_POST_TOPIC_PREFIX.getTopic()) &&
topic.endsWith(IotDeviceTopicEnum.EVENT_POST_TOPIC_SUFFIX.getTopic())) {
log.info("[handleMessage][接收到设备事件上报][topic: {}]", topic);
handleEventPost(topic, payload);
return;
}
// 未知消息类型
log.warn("[handleMessage][未知的消息类型][topic: {}]", topic);
}
/**
* 处理设备属性上报消息
*
* @param topic 主题
* @param payload 消息内容
*/
private void handlePropertyPost(String topic, String payload) {
try {
// 解析消息内容
JSONObject jsonObject = JSONUtil.parseObj(payload);
String[] topicParts = parseTopic(topic);
if (topicParts == null) {
return;
}
// 构建设备属性上报请求对象
IotDevicePropertyReportReqDTO reportReqDTO = buildPropertyReportDTO(jsonObject, topicParts);
// 调用上游 API 处理设备上报数据
deviceUpstreamApi.reportDeviceProperty(reportReqDTO);
log.info("[handlePropertyPost][处理设备属性上报成功][topic: {}]", topic);
// 发送响应消息
sendResponse(topic, jsonObject, PROPERTY_METHOD, null);
} catch (Exception e) {
log.error("[handlePropertyPost][处理设备属性上报失败][topic: {}][payload: {}]", topic, payload, e);
}
}
/**
* 处理设备事件上报消息
*
* @param topic 主题
* @param payload 消息内容
*/
private void handleEventPost(String topic, String payload) {
try {
// 解析消息内容
JSONObject jsonObject = JSONUtil.parseObj(payload);
String[] topicParts = parseTopic(topic);
if (topicParts == null) {
return;
}
// 构建设备事件上报请求对象
IotDeviceEventReportReqDTO reportReqDTO = buildEventReportDTO(jsonObject, topicParts);
// 调用上游 API 处理设备上报数据
deviceUpstreamApi.reportDeviceEvent(reportReqDTO);
log.info("[handleEventPost][处理设备事件上报成功][topic: {}]", topic);
// 从 topic 中获取事件标识符
String eventIdentifier = getEventIdentifier(topicParts, topic);
if (eventIdentifier == null) {
return;
}
// 发送响应消息
String method = EVENT_METHOD_PREFIX + eventIdentifier + EVENT_METHOD_SUFFIX;
sendResponse(topic, jsonObject, method, null);
} catch (Exception e) {
log.error("[handleEventPost][处理设备事件上报失败][topic: {}][payload: {}]", topic, payload, e);
}
}
/**
* 解析主题,获取主题各部分
*
* @param topic 主题
* @return 主题各部分数组如果解析失败返回null
*/
private String[] parseTopic(String topic) {
String[] topicParts = topic.split("/");
if (topicParts.length < 7) {
log.warn("[parseTopic][主题格式不正确][topic: {}]", topic);
return null;
}
return topicParts;
}
/**
* 从主题部分中获取事件标识符
*
* @param topicParts 主题各部分
* @param topic 原始主题,用于日志
* @return 事件标识符如果获取失败返回null
*/
private String getEventIdentifier(String[] topicParts, String topic) {
try {
return topicParts[6];
} catch (ArrayIndexOutOfBoundsException e) {
log.warn("[getEventIdentifier][无法从主题中获取事件标识符][topic: {}][topicParts: {}]",
topic, Arrays.toString(topicParts));
return null;
}
}
/**
* 发送响应消息
*
* @param topic 原始主题
* @param jsonObject 原始消息JSON对象
* @param method 响应方法
* @param customData 自定义数据,可为 null
*/
private void sendResponse(String topic, JSONObject jsonObject, String method, Object customData) {
String replyTopic = IotDeviceTopicEnum.getReplyTopic(topic);
// 响应结果
IotStandardResponse response = IotStandardResponse.success(
jsonObject.getStr("id"), method, customData);
try {
mqttClient.publish(replyTopic, Buffer.buffer(JsonUtils.toJsonString(response)),
MqttQoS.AT_LEAST_ONCE, false, false);
log.info("[sendResponse][发送响应消息成功][topic: {}]", replyTopic);
} catch (Exception e) {
log.error("[sendResponse][发送响应消息失败][topic: {}][response: {}]", replyTopic, response, e);
}
}
/**
* 构建设备属性上报请求对象
*
* @param jsonObject 消息内容
* @param topicParts 主题部分
* @return 设备属性上报请求对象
*/
private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject, String[] topicParts) {
// TODO @haohaoIotDevicePropertyReportReqDTO 可以考虑链式哈。其它也是,尽量让同类参数在一行;这样,阅读起来更聚焦;
IotDevicePropertyReportReqDTO reportReqDTO = new IotDevicePropertyReportReqDTO();
reportReqDTO.setRequestId(jsonObject.getStr("id"));
reportReqDTO.setProcessId(IotNetComponentCommonUtils.getProcessId());
reportReqDTO.setReportTime(LocalDateTime.now());
reportReqDTO.setProductKey(topicParts[2]);
reportReqDTO.setDeviceName(topicParts[3]);
// 只使用标准 JSON格式处理属性数据
JSONObject params = jsonObject.getJSONObject("params");
if (params == null) {
log.warn("[buildPropertyReportDTO][消息格式不正确缺少params字段][jsonObject: {}]", jsonObject);
params = new JSONObject();
}
// 将标准格式的params转换为平台需要的properties格式
Map<String, Object> properties = new HashMap<>();
for (Map.Entry<String, Object> entry : params.entrySet()) {
String key = entry.getKey();
Object valueObj = entry.getValue();
// 如果是复杂结构包含value和time
if (valueObj instanceof JSONObject valueJson) {
properties.put(key, valueJson.getOrDefault("value", valueObj));
} else {
properties.put(key, valueObj);
}
}
reportReqDTO.setProperties(properties);
return reportReqDTO;
}
/**
* 构建设备事件上报请求对象
*
* @param jsonObject 消息内容
* @param topicParts 主题部分
* @return 设备事件上报请求对象
*/
private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) {
IotDeviceEventReportReqDTO reportReqDTO = new IotDeviceEventReportReqDTO();
reportReqDTO.setRequestId(jsonObject.getStr("id"));
reportReqDTO.setProcessId(IotNetComponentCommonUtils.getProcessId());
reportReqDTO.setReportTime(LocalDateTime.now());
reportReqDTO.setProductKey(topicParts[2]);
reportReqDTO.setDeviceName(topicParts[3]);
reportReqDTO.setIdentifier(topicParts[6]);
// 只使用标准JSON格式处理事件参数
JSONObject params = jsonObject.getJSONObject("params");
if (params == null) {
log.warn("[buildEventReportDTO][消息格式不正确缺少params字段][jsonObject: {}]", jsonObject);
params = new JSONObject();
}
reportReqDTO.setParams(params);
return reportReqDTO;
}
}

View File

@@ -1,152 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.emqx.upstream.router;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.net.component.core.util.IotNetComponentCommonUtils;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.Collections;
/**
* IoT EMQX Webhook 事件处理的 Vert.x Handler
* <p>
* 参考:<a href="https://docs.emqx.com/zh/emqx/latest/data-integration/webhook.html">EMQX Webhook</a>
* <p>
* 注意:该处理器需要返回特定格式:{"result": "success"} 或 {"result": "error"}
* 以符合 EMQX Webhook 插件的要求,因此不使用 IotStandardResponse 实体类。
*
* @author haohao
*/
@RequiredArgsConstructor
@Slf4j
public class IotDeviceWebhookVertxHandler implements Handler<RoutingContext> {
public static final String PATH = "/mqtt/webhook";
private final IotDeviceUpstreamApi deviceUpstreamApi;
@Override
public void handle(RoutingContext routingContext) {
try {
// 解析请求体
JsonObject json = routingContext.body().asJsonObject();
String event = json.getString("event");
String clientId = json.getString("clientid");
String username = json.getString("username");
// 处理不同的事件类型
switch (event) {
case "client.connected":
handleClientConnected(clientId, username);
break;
case "client.disconnected":
handleClientDisconnected(clientId, username);
break;
default:
log.info("[handle][未处理的 Webhook 事件] event={}, clientId={}, username={}", event, clientId, username);
break;
}
// 返回成功响应
// 注意:这里必须返回 {"result": "success"} 格式,以符合 EMQX Webhook 插件的要求
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "success"));
} catch (Exception e) {
log.error("[handle][处理 Webhook 事件异常]", e);
// 注意:这里必须返回 {"result": "error"} 格式,以符合 EMQX Webhook 插件的要求
IotNetComponentCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "error"));
}
}
/**
* 处理客户端连接事件
*
* @param clientId 客户端ID
* @param username 用户名
*/
private void handleClientConnected(String clientId, String username) {
// 解析产品标识和设备名称
if (StrUtil.isEmpty(username) || "undefined".equals(username)) {
log.warn("[handleClientConnected][客户端连接事件,但用户名为空] clientId={}", clientId);
return;
}
String[] parts = parseUsername(username);
if (parts == null) {
return;
}
// 更新设备状态为在线
IotDeviceStateUpdateReqDTO updateReqDTO = new IotDeviceStateUpdateReqDTO();
updateReqDTO.setProductKey(parts[1]);
updateReqDTO.setDeviceName(parts[0]);
updateReqDTO.setState(IotDeviceStateEnum.ONLINE.getState());
updateReqDTO.setProcessId(IotNetComponentCommonUtils.getProcessId());
updateReqDTO.setReportTime(LocalDateTime.now());
CommonResult<Boolean> result = deviceUpstreamApi.updateDeviceState(updateReqDTO);
if (result.getCode() != 0 || !result.getData()) {
log.error("[handleClientConnected][更新设备状态为在线失败] clientId={}, username={}, code={}, msg={}",
clientId, username, result.getCode(), result.getMsg());
} else {
log.info("[handleClientConnected][更新设备状态为在线成功] clientId={}, username={}", clientId, username);
}
}
/**
* 处理客户端断开连接事件
*
* @param clientId 客户端ID
* @param username 用户名
*/
private void handleClientDisconnected(String clientId, String username) {
// 解析产品标识和设备名称
if (StrUtil.isEmpty(username) || "undefined".equals(username)) {
log.warn("[handleClientDisconnected][客户端断开连接事件,但用户名为空] clientId={}", clientId);
return;
}
String[] parts = parseUsername(username);
if (parts == null) {
return;
}
// 更新设备状态为离线
IotDeviceStateUpdateReqDTO offlineReqDTO = new IotDeviceStateUpdateReqDTO();
offlineReqDTO.setProductKey(parts[1]);
offlineReqDTO.setDeviceName(parts[0]);
offlineReqDTO.setState(IotDeviceStateEnum.OFFLINE.getState());
offlineReqDTO.setProcessId(IotNetComponentCommonUtils.getProcessId());
offlineReqDTO.setReportTime(LocalDateTime.now());
CommonResult<Boolean> offlineResult = deviceUpstreamApi.updateDeviceState(offlineReqDTO);
if (offlineResult.getCode() != 0 || !offlineResult.getData()) {
log.error("[handleClientDisconnected][更新设备状态为离线失败] clientId={}, username={}, code={}, msg={}",
clientId, username, offlineResult.getCode(), offlineResult.getMsg());
} else {
log.info("[handleClientDisconnected][更新设备状态为离线成功] clientId={}, username={}", clientId, username);
}
}
/**
* 解析用户名,格式为 deviceName&productKey
*
* @param username 用户名
* @return 解析结果,[0] 为 deviceName[1] 为 productKey解析失败返回 null
*/
private String[] parseUsername(String username) {
if (StrUtil.isEmpty(username)) {
return null;
}
String[] parts = username.split("&");
if (parts.length != 2) {
log.warn("[parseUsername][用户名格式({})不正确,无法解析产品标识和设备名称]", username);
return null;
}
return parts;
}
}

View File

@@ -1,18 +0,0 @@
# EMQX组件默认配置
yudao:
iot:
component:
# 核心组件配置
core:
plugin-key: emqx # 插件的唯一标识
# EMQX组件配置
# emqx:
# enabled: true # 启用EMQX组件
# mqtt-host: 127.0.0.1 # MQTT服务器主机地址
# mqtt-port: 1883 # MQTT服务器端口
# mqtt-username: yudao # MQTT服务器用户名
# mqtt-password: 123456 # MQTT服务器密码
# mqtt-ssl: false # 是否启用SSL
# mqtt-topics: # 订阅的主题列表
# - "/sys/#"
# auth-port: 8101 # 认证端口

View File

@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-net-components</artifactId>
<version>${revision}</version>
</parent>
<artifactId>yudao-module-iot-net-component-server</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
IoT 网络组件的独立运行服务,用于聚合和启动多个网络组件实例。
</description>
<dependencies>
<!-- Web 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Net Component EMQX -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-net-component-emqx</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</project>

View File

@@ -1,52 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.server.config;
import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
import cn.iocoder.yudao.module.iot.net.component.server.upstream.IotComponentUpstreamClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.client.RestTemplate;
/**
* IoT 网络组件服务器配置类
*
* @author haohao
*/
@Configuration
@EnableConfigurationProperties(IotNetComponentServerProperties.class)
@EnableScheduling
public class IotNetComponentServerConfiguration {
/**
* 配置 RestTemplate
*
* @param properties 配置
* @return RestTemplate
*/
@Bean
// TODO @haohao貌似要独立一个 restTemplate 的名字?不然容易冲突;
public RestTemplate restTemplate(IotNetComponentServerProperties properties) {
return new RestTemplateBuilder()
.connectTimeout(properties.getUpstreamConnectTimeout())
.readTimeout(properties.getUpstreamReadTimeout())
.build();
}
/**
* 配置设备上行客户端
*
* @param properties 配置
* @param restTemplate RestTemplate
* @return 上行客户端
*/
@Bean
@Primary
public IotDeviceUpstreamApi deviceUpstreamApi(IotNetComponentServerProperties properties,
RestTemplate restTemplate) {
return new IotComponentUpstreamClient(properties, restTemplate);
}
}

View File

@@ -1,34 +0,0 @@
package cn.iocoder.yudao.module.iot.net.component.server.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import java.time.Duration;
/**
* IoT 网络组件服务配置属性
*
* @author haohao
*/
@ConfigurationProperties(prefix = "yudao.iot.component.server")
@Validated
@Data
public class IotNetComponentServerProperties {
/**
* 上行 URL用于向主应用程序上报数据
*/
private String upstreamUrl = "http://127.0.0.1:48080";
/**
* 上行连接超时时间
*/
private Duration upstreamConnectTimeout = Duration.ofSeconds(30);
/**
* 上行读取超时时间
*/
private Duration upstreamReadTimeout = Duration.ofSeconds(30);
}

View File

@@ -1,254 +0,0 @@
# IoT 协议模块 (yudao-module-iot-protocol)
## 概述
本模块是物联网协议处理的核心组件,提供统一的协议解析、转换和消息处理功能。作为 `yudao-module-iot-biz`
`yudao-module-iot-gateway-server` 等模块的共享包,实现了协议层面的抽象和统一。
## 主要功能
### 1. 协议消息模型
- **IotMqttMessage**: 基于 MQTT 协议规范的标准消息模型(默认实现)
- **IotStandardResponse**: 统一的响应格式,支持 MQTT 和 HTTP 协议
### 2. 主题管理
- **IotTopicConstants**: 主题常量定义
- **IotTopicUtils**: MQTT 主题构建和解析工具
- **IotHttpTopicUtils**: HTTP 主题构建和解析工具
- **IotTopicParser**: 高级主题解析器,支持提取设备信息、消息类型等
### 3. 协议转换
- **IotMessageParser**: 消息解析器接口
- **IotMqttMessageParser**: MQTT 协议解析器实现(默认)
- **IotHttpMessageParser**: HTTP 协议解析器实现
- **IotProtocolConverter**: 协议转换器接口
- **DefaultIotProtocolConverter**: 默认协议转换器实现
### 4. 枚举定义
- **IotProtocolTypeEnum**: 协议类型枚举
- **IotMessageTypeEnum**: 消息类型枚举
- **IotMessageDirectionEnum**: 消息方向枚举
## 使用示例
### 1. 构建主题
#### MQTT 主题
```java
// 构建设备属性设置主题
String topic = IotTopicUtils.buildPropertySetTopic("productKey", "deviceName");
// 结果: /sys/productKey/deviceName/thing/service/property/set
// 构建事件上报主题
String eventTopic = IotTopicUtils.buildEventPostTopic("productKey", "deviceName", "temperature");
// 结果: /sys/productKey/deviceName/thing/event/temperature/post
// 获取响应主题
String replyTopic = IotTopicUtils.getReplyTopic(topic);
// 结果: /sys/productKey/deviceName/thing/service/property/set_reply
```
#### HTTP 主题
```java
// 构建属性设置路径
String propSetPath = IotHttpTopicUtils.buildPropertySetPath("productKey", "deviceName");
// 结果: /topic/sys/productKey/deviceName/thing/service/property/set
// 构建属性获取路径
String propGetPath = IotHttpTopicUtils.buildPropertyGetPath("productKey", "deviceName");
// 结果: /topic/sys/productKey/deviceName/thing/service/property/get
// 构建事件上报路径
String eventPath = IotHttpTopicUtils.buildEventPostPath("productKey", "deviceName", "alarm");
// 结果: /topic/sys/productKey/deviceName/thing/event/alarm/post
// 构建自定义主题路径
String customPath = IotHttpTopicUtils.buildCustomTopicPath("productKey", "deviceName", "user/get");
// 结果: /topic/productKey/deviceName/user/get
```
### 2. 解析主题
```java
// 解析 MQTT 主题信息
IotTopicParser.TopicInfo info = IotTopicParser.parse("/sys/pk/dn/thing/service/property/set");
System.out.
println("产品Key: "+info.getProductKey()); // pk
System.out.
println("设备名称: "+info.getDeviceName()); // dn
System.out.
println("消息类型: "+info.getMessageType()); // PROPERTY_SET
System.out.
println("消息方向: "+info.getDirection()); // DOWNSTREAM
// 解析 HTTP 主题信息
String httpPath = "/topic/sys/pk/dn/thing/service/property/set";
String actualTopic = IotHttpTopicUtils.extractActualTopic(httpPath); // /sys/pk/dn/thing/service/property/set
String productKey = IotHttpTopicUtils.parseProductKeyFromTopic(actualTopic); // pk
String deviceName = IotHttpTopicUtils.parseDeviceNameFromTopic(actualTopic); // dn
```
### 3. 创建 MQTT 消息
```java
// 创建属性设置消息
Map<String, Object> properties = new HashMap<>();
properties.
put("temperature",25.5);
IotMqttMessage message = IotMqttMessage.createPropertySetMessage("123456", properties);
// 转换为 JSON 字符串
String json = message.toJsonString();
```
### 4. HTTP 协议消息处理
#### HTTP 消息格式
```json
{
"deviceKey": "productKey/deviceName",
"messageId": "123456",
"action": "property.set",
"version": "1.0",
"data": {
"temperature": 25.5,
"humidity": 60.0
}
}
```
#### 使用 HTTP 协议解析器
```java
// 创建 HTTP 协议解析器
IotHttpMessageParser httpParser = new IotHttpMessageParser();
// 解析 HTTP 消息
String topic = "/topic/sys/productKey/deviceName/thing/service/property/set";
byte[] payload = httpMessage.getBytes(StandardCharsets.UTF_8);
IotMqttMessage message = httpParser.parse(topic, payload);
// 格式化 HTTP 响应
IotStandardResponse response = IotStandardResponse.success("123456", "property.set", data);
byte[] responseBytes = httpParser.formatResponse(response);
```
### 5. 使用协议转换器
```java
@Autowired
private IotProtocolConverter protocolConverter;
// 转换 MQTT 消息(推荐使用)
IotMqttMessage mqttMessage = protocolConverter.convertToStandardMessage(mqttTopic, mqttPayload, "mqtt");
// 转换 HTTP 消息
IotMqttMessage httpMessage = protocolConverter.convertToStandardMessage(httpTopic, httpPayload, "http");
// 创建响应
IotStandardResponse response = IotStandardResponse.success("123456", "property.set", data);
byte[] responseBytes = protocolConverter.convertFromStandardResponse(response, "mqtt");
```
### 6. 自定义协议解析器
```java
@Component
public class CustomMessageParser implements IotMessageParser {
@Override
public IotMqttMessage parse(String topic, byte[] payload) {
// 实现自定义协议解析逻辑
return null;
}
@Override
public byte[] formatResponse(IotStandardResponse response) {
// 实现自定义响应格式化逻辑
return new byte[0];
}
@Override
public boolean canHandle(String topic) {
// 判断是否能处理该主题
return topic.startsWith("/custom/");
}
}
// 注册到协议转换器
@Autowired
private DefaultIotProtocolConverter converter;
@PostConstruct
public void init() {
converter.registerParser("custom", new CustomMessageParser());
}
```
## 支持的协议类型
- **MQTT**: 标准 MQTT 协议,支持设备属性、事件、服务调用(默认协议)
- **HTTP**: HTTP 协议,支持设备通过 HTTP API 进行通信
- **MQTT_RAW**: MQTT 原始协议
- **TCP**: TCP 协议
- **UDP**: UDP 协议
- **CUSTOM**: 自定义协议
## 协议对比
| 协议类型 | 传输方式 | 消息格式 | 主题格式 | 适用场景 |
|----------|------|------|----------------------------------------------------------------------------------------------------------------------------|---------------|
| MQTT | MQTT | JSON | `/sys/{productKey}/{deviceName}/...`<br/>`/mqtt/{productKey}/{deviceName}/...`<br/>`/device/{productKey}/{deviceName}/...` | 实时性要求高的设备(推荐) |
| HTTP | HTTP | JSON | `/topic/sys/{productKey}/{deviceName}/...`<br/>`/topic/{productKey}/{deviceName}/...` | 间歇性通信的设备 |
| MQTT_RAW | MQTT | 原始 | 自定义格式 | 特殊协议设备 |
## 模块依赖
本模块是一个基础模块,依赖项最小化:
- `yudao-common`: 基础工具类
- `hutool-all`: 工具库
- `lombok`: 简化代码
- `spring-boot-starter`: Spring Boot 基础支持
## 扩展点
### 1. 自定义消息解析器
实现 `IotMessageParser` 接口,支持新的协议格式。
### 2. 自定义协议转换器
实现 `IotProtocolConverter` 接口,提供更复杂的转换逻辑。
### 3. 自定义主题格式
扩展 `IotTopicParser``parseCustomTopic` 方法,支持自定义主题格式。
## 注意事项
1. 本模块设计为无状态的工具模块,避免引入有状态的组件
2. 所有的工具类都采用静态方法,便于直接调用
3. 异常处理采用返回 null 的方式,调用方需要做好空值检查
4. 日志级别建议设置为 INFO 或 WARN避免过多的 DEBUG 日志
5. HTTP 协议解析器使用设备标识 `deviceKey`(格式:`productKey/deviceName`)来标识设备
## 版本更新
- v1.0.0: 基础功能实现,支持 MQTT 协议和 HTTP 协议支持
- 后续版本将支持更多协议类型和高级功能

View File

@@ -1,71 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-module-iot</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-iot-protocol</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
物联网协议模块,提供 topic 解析、协议转换等功能
作为 iot-biz 和 iot-gateway 的共享包
</description>
<dependencies>
<!-- 项目内部依赖 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId>
<scope>provided</scope> <!-- 设为 provided只有 biz 需要使用 -->
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- IoT 相关依赖 -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<scope>provided</scope>
</dependency>
<!-- 测试相关 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,74 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.config;
import cn.iocoder.yudao.module.iot.protocol.convert.IotProtocolConverter;
import cn.iocoder.yudao.module.iot.protocol.convert.impl.DefaultIotProtocolConverter;
import cn.iocoder.yudao.module.iot.protocol.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
import cn.iocoder.yudao.module.iot.protocol.message.impl.IotHttpMessageParser;
import cn.iocoder.yudao.module.iot.protocol.message.impl.IotMqttMessageParser;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* IoT 协议模块自动配置类
*
* @author haohao
*/
@Configuration(proxyBeanMethods = false)
public class IotProtocolAutoConfiguration {
/**
* Bean 名称常量
*/
public static final String IOT_MQTT_MESSAGE_PARSER_BEAN_NAME = "iotMqttMessageParser";
public static final String IOT_HTTP_MESSAGE_PARSER_BEAN_NAME = "iotHttpMessageParser";
/**
* 注册 MQTT 协议消息解析器
*
* @return MQTT 协议消息解析器
*/
@Bean
@ConditionalOnMissingBean(name = IOT_MQTT_MESSAGE_PARSER_BEAN_NAME)
public IotMessageParser iotMqttMessageParser() {
return new IotMqttMessageParser();
}
/**
* 注册 HTTP 协议消息解析器
*
* @return HTTP 协议消息解析器
*/
@Bean
@ConditionalOnMissingBean(name = IOT_HTTP_MESSAGE_PARSER_BEAN_NAME)
public IotMessageParser iotHttpMessageParser() {
return new IotHttpMessageParser();
}
/**
* 注册默认协议转换器
* <p>
* 如果用户没有自定义协议转换器,则使用默认实现
* 默认会注册 MQTT 和 HTTP 协议解析器
*
* @param iotMqttMessageParser MQTT 协议解析器
* @param iotHttpMessageParser HTTP 协议解析器
* @return 默认协议转换器
*/
@Bean
@ConditionalOnMissingBean
public IotProtocolConverter iotProtocolConverter(IotMessageParser iotMqttMessageParser,
IotMessageParser iotHttpMessageParser) {
DefaultIotProtocolConverter converter = new DefaultIotProtocolConverter();
// 注册 MQTT 协议解析器(默认实现)
converter.registerParser(IotProtocolTypeEnum.MQTT.getCode(), iotMqttMessageParser);
// 注册 HTTP 协议解析器
converter.registerParser(IotProtocolTypeEnum.HTTP.getCode(), iotHttpMessageParser);
return converter;
}
}

View File

@@ -1,166 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.constants;
/**
* IoT HTTP 协议常量类
* <p>
* 用于统一管理 HTTP 协议中的常量,包括路径、字段名、默认值等
*
* @author haohao
*/
public class IotHttpConstants {
/**
* 路径常量
*/
public static class Path {
/**
* 认证路径
*/
public static final String AUTH = "/auth";
/**
* 主题路径前缀
*/
public static final String TOPIC_PREFIX = "/topic";
}
/**
* 认证字段常量
*/
public static class AuthField {
/**
* 产品Key
*/
public static final String PRODUCT_KEY = "productKey";
/**
* 设备名称
*/
public static final String DEVICE_NAME = "deviceName";
/**
* 客户端ID
*/
public static final String CLIENT_ID = "clientId";
/**
* 时间戳
*/
public static final String TIMESTAMP = "timestamp";
/**
* 签名
*/
public static final String SIGN = "sign";
/**
* 签名方法
*/
public static final String SIGN_METHOD = "signmethod";
/**
* 版本
*/
public static final String VERSION = "version";
}
/**
* 消息字段常量
*/
public static class MessageField {
/**
* 消息ID
*/
public static final String ID = "id";
/**
* 方法名
*/
public static final String METHOD = "method";
/**
* 版本
*/
public static final String VERSION = "version";
/**
* 参数
*/
public static final String PARAMS = "params";
/**
* 数据
*/
public static final String DATA = "data";
}
/**
* 响应字段常量
*/
public static class ResponseField {
/**
* 状态码
*/
public static final String CODE = "code";
/**
* 消息
*/
public static final String MESSAGE = "message";
/**
* 信息
*/
public static final String INFO = "info";
/**
* 令牌
*/
public static final String TOKEN = "token";
/**
* 消息ID
*/
public static final String MESSAGE_ID = "messageId";
}
/**
* 默认值常量
*/
public static class DefaultValue {
/**
* 默认签名方法
*/
public static final String SIGN_METHOD = "hmacmd5";
/**
* 默认版本
*/
public static final String VERSION = "default";
/**
* 默认消息版本
*/
public static final String MESSAGE_VERSION = "1.0";
/**
* 未知方法名
*/
public static final String UNKNOWN_METHOD = "unknown";
}
/**
* 方法名常量
*/
public static class Method {
/**
* 设备认证
*/
public static final String DEVICE_AUTH = "device.auth";
/**
* 自定义消息
*/
public static final String CUSTOM_MESSAGE = "custom.message";
}
}

View File

@@ -1,91 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.constants;
/**
* IoT 协议日志消息常量类
* <p>
* 用于统一管理协议模块中的日志消息常量
*
* @author haohao
*/
public class IotLogConstants {
/**
* HTTP 协议日志消息
*/
public static class Http {
/**
* 收到空消息内容
*/
public static final String RECEIVED_EMPTY_MESSAGE = "[HTTP] 收到空消息内容, topic={}";
/**
* 不支持的路径格式
*/
public static final String UNSUPPORTED_PATH_FORMAT = "[HTTP] 不支持的路径格式, topic={}";
/**
* 解析消息失败
*/
public static final String PARSE_MESSAGE_FAILED = "[HTTP] 解析消息失败, topic={}";
/**
* 认证消息非JSON格式
*/
public static final String AUTH_MESSAGE_NOT_JSON = "[HTTP] 认证消息非JSON格式, message={}";
/**
* 认证消息缺少必需字段
*/
public static final String AUTH_MESSAGE_MISSING_REQUIRED_FIELDS = "[HTTP] 认证消息缺少必需字段, message={}";
/**
* 格式化响应失败
*/
public static final String FORMAT_RESPONSE_FAILED = "[HTTP] 格式化响应失败";
}
/**
* 协议转换器日志消息
*/
public static class Converter {
/**
* 注册协议解析器
*/
public static final String REGISTER_PARSER = "[协议转换器] 注册协议解析器: protocol={}, parser={}";
/**
* 移除协议解析器
*/
public static final String REMOVE_PARSER = "[协议转换器] 移除协议解析器: protocol={}";
/**
* 不支持的协议类型
*/
public static final String UNSUPPORTED_PROTOCOL = "[协议转换器] 不支持的协议类型: protocol={}";
/**
* 转换消息失败
*/
public static final String CONVERT_MESSAGE_FAILED = "[协议转换器] 转换消息失败: protocol={}, topic={}";
/**
* 格式化响应失败
*/
public static final String FORMAT_RESPONSE_FAILED = "[协议转换器] 格式化响应失败: protocol={}";
/**
* 自动选择协议
*/
public static final String AUTO_SELECT_PROTOCOL = "[协议转换器] 自动选择协议: protocol={}, topic={}";
/**
* 协议解析失败,尝试下一个
*/
public static final String PROTOCOL_PARSE_FAILED_TRY_NEXT = "[协议转换器] 协议解析失败,尝试下一个: protocol={}, topic={}";
/**
* 无法自动识别协议
*/
public static final String CANNOT_AUTO_RECOGNIZE_PROTOCOL = "[协议转换器] 无法自动识别协议: topic={}";
}
}

View File

@@ -1,157 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.constants;
/**
* IoT 设备主题常量类
* <p>
* 用于统一管理 MQTT 协议中的主题常量,基于 Alink 协议规范
*
* @author haohao
*/
public class IotTopicConstants {
/**
* 系统主题前缀
*/
public static final String SYS_TOPIC_PREFIX = "/sys/";
/**
* 服务调用主题前缀
*/
public static final String SERVICE_TOPIC_PREFIX = "/thing/service/";
/**
* 设备属性设置主题
* 请求Topic/sys/${productKey}/${deviceName}/thing/service/property/set
* 响应Topic/sys/${productKey}/${deviceName}/thing/service/property/set_reply
*/
public static final String PROPERTY_SET_TOPIC = "/thing/service/property/set";
/**
* 设备属性获取主题
* 请求Topic/sys/${productKey}/${deviceName}/thing/service/property/get
* 响应Topic/sys/${productKey}/${deviceName}/thing/service/property/get_reply
*/
public static final String PROPERTY_GET_TOPIC = "/thing/service/property/get";
/**
* 设备配置设置主题
* 请求Topic/sys/${productKey}/${deviceName}/thing/service/config/set
* 响应Topic/sys/${productKey}/${deviceName}/thing/service/config/set_reply
*/
public static final String CONFIG_SET_TOPIC = "/thing/service/config/set";
/**
* 设备OTA升级主题
* 请求Topic/sys/${productKey}/${deviceName}/thing/service/ota/upgrade
* 响应Topic/sys/${productKey}/${deviceName}/thing/service/ota/upgrade_reply
*/
public static final String OTA_UPGRADE_TOPIC = "/thing/service/ota/upgrade";
/**
* 设备属性上报主题
* 请求Topic/sys/${productKey}/${deviceName}/thing/event/property/post
* 响应Topic/sys/${productKey}/${deviceName}/thing/event/property/post_reply
*/
public static final String PROPERTY_POST_TOPIC = "/thing/event/property/post";
/**
* 设备事件上报主题前缀
*/
public static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/";
/**
* 设备事件上报主题后缀
*/
public static final String EVENT_POST_TOPIC_SUFFIX = "/post";
/**
* 响应主题后缀
*/
public static final String REPLY_SUFFIX = "_reply";
/**
* 方法名前缀常量
*/
public static class MethodPrefix {
/**
* 物模型服务前缀
*/
public static final String THING_SERVICE = "thing.service.";
/**
* 物模型事件前缀
*/
public static final String THING_EVENT = "thing.event.";
}
/**
* 完整方法名常量
*/
public static class Method {
/**
* 属性设置方法
*/
public static final String PROPERTY_SET = "thing.service.property.set";
/**
* 属性获取方法
*/
public static final String PROPERTY_GET = "thing.service.property.get";
/**
* 属性上报方法
*/
public static final String PROPERTY_POST = "thing.event.property.post";
/**
* 配置设置方法
*/
public static final String CONFIG_SET = "thing.service.config.set";
/**
* OTA升级方法
*/
public static final String OTA_UPGRADE = "thing.service.ota.upgrade";
/**
* 设备上线方法
*/
public static final String DEVICE_ONLINE = "device.online";
/**
* 设备下线方法
*/
public static final String DEVICE_OFFLINE = "device.offline";
/**
* 心跳方法
*/
public static final String HEARTBEAT = "heartbeat";
}
/**
* 主题关键字常量
*/
public static class Keyword {
/**
* 事件关键字
*/
public static final String EVENT = "event";
/**
* 服务关键字
*/
public static final String SERVICE = "service";
/**
* 属性关键字
*/
public static final String PROPERTY = "property";
/**
* 上报关键字
*/
public static final String POST = "post";
}
}

View File

@@ -1,48 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.convert;
import cn.iocoder.yudao.module.iot.protocol.message.IotMqttMessage;
import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
/**
* IoT 协议转换器接口
* <p>
* 用于在不同协议之间进行转换
*
* @author haohao
*/
public interface IotProtocolConverter {
/**
* 将字节数组转换为标准消息
*
* @param topic 主题
* @param payload 消息负载
* @param protocol 协议类型
* @return 标准消息对象,转换失败返回 null
*/
IotMqttMessage convertToStandardMessage(String topic, byte[] payload, String protocol);
/**
* 将标准响应转换为字节数组
*
* @param response 标准响应
* @param protocol 协议类型
* @return 字节数组,转换失败返回空数组
*/
byte[] convertFromStandardResponse(IotStandardResponse response, String protocol);
/**
* 检查是否支持指定协议
*
* @param protocol 协议类型
* @return 如果支持返回 true否则返回 false
*/
boolean supportsProtocol(String protocol);
/**
* 获取支持的协议类型列表
*
* @return 协议类型数组
*/
String[] getSupportedProtocols();
}

View File

@@ -1,132 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.convert.impl;
import cn.iocoder.yudao.module.iot.protocol.constants.IotLogConstants;
import cn.iocoder.yudao.module.iot.protocol.convert.IotProtocolConverter;
import cn.iocoder.yudao.module.iot.protocol.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
import cn.iocoder.yudao.module.iot.protocol.message.IotMqttMessage;
import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
import cn.iocoder.yudao.module.iot.protocol.message.impl.IotMqttMessageParser;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 默认 IoT 协议转换器实现
* <p>
* 支持多种协议的转换,可以通过注册不同的消息解析器来扩展支持的协议
*
* @author haohao
*/
@Slf4j
public class DefaultIotProtocolConverter implements IotProtocolConverter {
/**
* 消息解析器映射
* Key: 协议类型Value: 消息解析器
*/
private final Map<String, IotMessageParser> parsers = new HashMap<>();
/**
* 构造函数,初始化默认支持的协议
*/
public DefaultIotProtocolConverter() {
// 注册 MQTT 协议解析器作为默认实现
IotMqttMessageParser mqttParser = new IotMqttMessageParser();
registerParser(IotProtocolTypeEnum.MQTT.getCode(), mqttParser);
}
/**
* 注册消息解析器
*
* @param protocol 协议类型
* @param parser 消息解析器
*/
public void registerParser(String protocol, IotMessageParser parser) {
parsers.put(protocol, parser);
log.info(IotLogConstants.Converter.REGISTER_PARSER, protocol, parser.getClass().getSimpleName());
}
/**
* 移除消息解析器
*
* @param protocol 协议类型
*/
public void removeParser(String protocol) {
parsers.remove(protocol);
log.info(IotLogConstants.Converter.REMOVE_PARSER, protocol);
}
@Override
public IotMqttMessage convertToStandardMessage(String topic, byte[] payload, String protocol) {
IotMessageParser parser = parsers.get(protocol);
if (parser == null) {
log.warn(IotLogConstants.Converter.UNSUPPORTED_PROTOCOL, protocol);
return null;
}
try {
return parser.parse(topic, payload);
} catch (Exception e) {
log.error(IotLogConstants.Converter.CONVERT_MESSAGE_FAILED, protocol, topic, e);
return null;
}
}
@Override
public byte[] convertFromStandardResponse(IotStandardResponse response, String protocol) {
IotMessageParser parser = parsers.get(protocol);
if (parser == null) {
log.warn(IotLogConstants.Converter.UNSUPPORTED_PROTOCOL, protocol);
return new byte[0];
}
try {
return parser.formatResponse(response);
} catch (Exception e) {
log.error(IotLogConstants.Converter.FORMAT_RESPONSE_FAILED, protocol, e);
return new byte[0];
}
}
@Override
public boolean supportsProtocol(String protocol) {
return parsers.containsKey(protocol);
}
@Override
public String[] getSupportedProtocols() {
Set<String> protocols = parsers.keySet();
return protocols.toArray(new String[0]);
}
/**
* 根据主题自动选择合适的协议解析器
*
* @param topic 主题
* @param payload 消息负载
* @return 解析后的标准消息,如果无法解析返回 null
*/
public IotMqttMessage autoConvert(String topic, byte[] payload) {
// 遍历所有解析器,找到能处理该主题的解析器
for (Map.Entry<String, IotMessageParser> entry : parsers.entrySet()) {
IotMessageParser parser = entry.getValue();
if (parser.canHandle(topic)) {
try {
IotMqttMessage message = parser.parse(topic, payload);
if (message != null) {
log.debug(IotLogConstants.Converter.AUTO_SELECT_PROTOCOL, entry.getKey(), topic);
return message;
}
} catch (Exception e) {
log.debug(IotLogConstants.Converter.PROTOCOL_PARSE_FAILED_TRY_NEXT, entry.getKey(), topic);
}
}
}
log.warn(IotLogConstants.Converter.CANNOT_AUTO_RECOGNIZE_PROTOCOL, topic);
return null;
}
}

View File

@@ -1,49 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IoT 消息方向枚举
*
* @author haohao
*/
@Getter
@AllArgsConstructor
public enum IotMessageDirectionEnum {
/**
* 上行消息(设备到平台)
*/
UPSTREAM("upstream", "上行"),
/**
* 下行消息(平台到设备)
*/
DOWNSTREAM("downstream", "下行");
/**
* 方向编码
*/
private final String code;
/**
* 方向名称
*/
private final String name;
/**
* 根据编码获取消息方向
*
* @param code 方向编码
* @return 消息方向枚举,如果未找到返回 null
*/
public static IotMessageDirectionEnum getByCode(String code) {
for (IotMessageDirectionEnum direction : values()) {
if (direction.getCode().equals(code)) {
return direction;
}
}
return null;
}
}

View File

@@ -1,140 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.enums;
import cn.iocoder.yudao.module.iot.protocol.constants.IotTopicConstants;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IoT 消息类型枚举
*
* @author haohao
*/
@Getter
@AllArgsConstructor
public enum IotMessageTypeEnum {
/**
* 属性上报
*/
PROPERTY_POST("property.post", "属性上报"),
/**
* 属性设置
*/
PROPERTY_SET("property.set", "属性设置"),
/**
* 属性获取
*/
PROPERTY_GET("property.get", "属性获取"),
/**
* 事件上报
*/
EVENT_POST("event.post", "事件上报"),
/**
* 服务调用
*/
SERVICE_INVOKE("service.invoke", "服务调用"),
/**
* 配置设置
*/
CONFIG_SET("config.set", "配置设置"),
/**
* OTA 升级
*/
OTA_UPGRADE("ota.upgrade", "OTA升级"),
/**
* 设备上线
*/
DEVICE_ONLINE("device.online", "设备上线"),
/**
* 设备下线
*/
DEVICE_OFFLINE("device.offline", "设备下线"),
/**
* 心跳
*/
HEARTBEAT("heartbeat", "心跳");
/**
* 消息类型编码
*/
private final String code;
/**
* 消息类型名称
*/
private final String name;
/**
* 根据编码获取消息类型
*
* @param code 消息类型编码
* @return 消息类型枚举,如果未找到返回 null
*/
public static IotMessageTypeEnum getByCode(String code) {
for (IotMessageTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
/**
* 根据方法名获取消息类型
*
* @param method 方法名
* @return 消息类型枚举,如果未找到返回 null
*/
public static IotMessageTypeEnum getByMethod(String method) {
if (method == null) {
return null;
}
// 处理 thing.service.xxx 格式
if (method.startsWith(IotTopicConstants.MethodPrefix.THING_SERVICE)) {
String servicePart = method.substring(IotTopicConstants.MethodPrefix.THING_SERVICE.length());
if ("property.set".equals(servicePart)) {
return PROPERTY_SET;
} else if ("property.get".equals(servicePart)) {
return PROPERTY_GET;
} else if ("config.set".equals(servicePart)) {
return CONFIG_SET;
} else if ("ota.upgrade".equals(servicePart)) {
return OTA_UPGRADE;
} else {
return SERVICE_INVOKE;
}
}
// 处理 thing.event.xxx 格式
if (method.startsWith(IotTopicConstants.MethodPrefix.THING_EVENT)) {
String eventPart = method.substring(IotTopicConstants.MethodPrefix.THING_EVENT.length());
if ("property.post".equals(eventPart)) {
return PROPERTY_POST;
} else {
return EVENT_POST;
}
}
// 其他类型
switch (method) {
case IotTopicConstants.Method.DEVICE_ONLINE:
return DEVICE_ONLINE;
case IotTopicConstants.Method.DEVICE_OFFLINE:
return DEVICE_OFFLINE;
case IotTopicConstants.Method.HEARTBEAT:
return HEARTBEAT;
default:
return null;
}
}
}

View File

@@ -1,79 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IoT 协议类型枚举
*
* @author haohao
*/
@Getter
@AllArgsConstructor
public enum IotProtocolTypeEnum {
/**
* MQTT 协议(默认实现)
*/
MQTT("mqtt", "MQTT 协议"),
/**
* MQTT 原始协议
*/
MQTT_RAW("mqtt_raw", "MQTT 原始协议"),
/**
* HTTP 协议
*/
HTTP("http", "HTTP 协议"),
/**
* TCP 协议
*/
TCP("tcp", "TCP 协议"),
/**
* UDP 协议
*/
UDP("udp", "UDP 协议"),
/**
* 自定义协议
*/
CUSTOM("custom", "自定义协议");
/**
* 协议编码
*/
private final String code;
/**
* 协议名称
*/
private final String name;
/**
* 根据编码获取协议类型
*
* @param code 协议编码
* @return 协议类型枚举,如果未找到返回 null
*/
public static IotProtocolTypeEnum getByCode(String code) {
for (IotProtocolTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
/**
* 检查是否为有效的协议编码
*
* @param code 协议编码
* @return 如果有效返回 true否则返回 false
*/
public static boolean isValidCode(String code) {
return getByCode(code) != null;
}
}

View File

@@ -1,36 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.message;
/**
* IoT 消息解析器接口
* <p>
* 用于解析不同协议的消息内容
*
* @author haohao
*/
public interface IotMessageParser {
/**
* 解析消息
*
* @param topic 主题
* @param payload 消息负载
* @return 解析后的标准消息,如果解析失败返回 null
*/
IotMqttMessage parse(String topic, byte[] payload);
/**
* 格式化响应消息
*
* @param response 标准响应
* @return 格式化后的响应字节数组
*/
byte[] formatResponse(IotStandardResponse response);
/**
* 检查是否能够处理指定主题的消息
*
* @param topic 主题
* @return 如果能处理返回 true否则返回 false
*/
boolean canHandle(String topic);
}

View File

@@ -1,154 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.message;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONObject;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
/**
* IoT MQTT 消息模型
* <p>
* 基于 MQTT 协议规范实现的标准消息格式,支持设备属性、事件、服务调用等标准功能
*
* @author haohao
* @see <a href="https://mqtt.org/">MQTT 协议官方规范</a>
*/
@Data
@Builder
public class IotMqttMessage {
/**
* 消息 ID
*/
private String id;
/**
* 协议版本
*/
@Builder.Default
private String version = "1.0";
/**
* 消息方法
*/
private String method;
/**
* 消息参数
*/
private Map<String, Object> params;
/**
* 转换为 JSONObject
*
* @return JSONObject 对象
*/
public JSONObject toJsonObject() {
JSONObject json = new JSONObject();
json.set("id", id);
json.set("version", version);
json.set("method", method);
json.set("params", params != null ? params : new JSONObject());
return json;
}
/**
* 转换为 JSON 字符串
*
* @return JSON 字符串
*/
public String toJsonString() {
return toJsonObject().toString();
}
/**
* 创建设备服务调用消息
*
* @param requestId 请求 ID为空时自动生成
* @param serviceIdentifier 服务标识符
* @param params 服务参数
* @return MQTT 消息对象
*/
public static IotMqttMessage createServiceInvokeMessage(String requestId, String serviceIdentifier,
Map<String, Object> params) {
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service." + serviceIdentifier)
.params(params)
.build();
}
/**
* 创建设备属性设置消息
*
* @param requestId 请求 ID为空时自动生成
* @param properties 设备属性
* @return MQTT 消息对象
*/
public static IotMqttMessage createPropertySetMessage(String requestId, Map<String, Object> properties) {
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service.property.set")
.params(properties)
.build();
}
/**
* 创建设备属性获取消息
*
* @param requestId 请求 ID为空时自动生成
* @param identifiers 要获取的属性标识符列表
* @return MQTT 消息对象
*/
public static IotMqttMessage createPropertyGetMessage(String requestId, String[] identifiers) {
JSONObject params = new JSONObject();
params.set("identifiers", identifiers);
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service.property.get")
.params(params)
.build();
}
/**
* 创建设备配置设置消息
*
* @param requestId 请求 ID为空时自动生成
* @param configs 设备配置
* @return MQTT 消息对象
*/
public static IotMqttMessage createConfigSetMessage(String requestId, Map<String, Object> configs) {
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service.config.set")
.params(configs)
.build();
}
/**
* 创建设备 OTA 升级消息
*
* @param requestId 请求 ID为空时自动生成
* @param otaInfo OTA 升级信息
* @return MQTT 消息对象
*/
public static IotMqttMessage createOtaUpgradeMessage(String requestId, Map<String, Object> otaInfo) {
return IotMqttMessage.builder()
.id(requestId != null ? requestId : generateRequestId())
.method("thing.service.ota.upgrade")
.params(otaInfo)
.build();
}
/**
* 生成请求 ID
*
* @return 请求 ID
*/
public static String generateRequestId() {
return IdUtil.fastSimpleUUID();
}
}

View File

@@ -1,95 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.message;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* IoT 标准协议响应实体类
* <p>
* 用于统一 MQTT 和 HTTP 的响应格式
*
* @author haohao
*/
@Data
@Accessors(chain = true)
public class IotStandardResponse {
/**
* 消息 ID
*/
private String id;
/**
* 状态码
*/
private Integer code;
/**
* 响应数据
*/
private Object data;
/**
* 响应消息
*/
private String message;
/**
* 方法名
*/
private String method;
/**
* 协议版本
*/
private String version;
/**
* 创建成功响应
*
* @param id 消息 ID
* @param method 方法名
* @return 成功响应
*/
public static IotStandardResponse success(String id, String method) {
return success(id, method, null);
}
/**
* 创建成功响应
*
* @param id 消息 ID
* @param method 方法名
* @param data 响应数据
* @return 成功响应
*/
public static IotStandardResponse success(String id, String method, Object data) {
return new IotStandardResponse()
.setId(id)
.setCode(200)
.setData(data)
.setMessage("success")
.setMethod(method)
.setVersion("1.0");
}
/**
* 创建错误响应
*
* @param id 消息 ID
* @param method 方法名
* @param code 错误码
* @param message 错误消息
* @return 错误响应
*/
public static IotStandardResponse error(String id, String method, Integer code, String message) {
return new IotStandardResponse()
.setId(id)
.setCode(code)
.setData(null)
.setMessage(StrUtil.blankToDefault(message, "error"))
.setMethod(method)
.setVersion("1.0");
}
}

View File

@@ -1,348 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.message.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.module.iot.protocol.constants.IotHttpConstants;
import cn.iocoder.yudao.module.iot.protocol.constants.IotLogConstants;
import cn.iocoder.yudao.module.iot.protocol.constants.IotTopicConstants;
import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
import cn.iocoder.yudao.module.iot.protocol.message.IotMqttMessage;
import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* IoT HTTP 协议消息解析器实现
* <p>
* 参考阿里云IoT平台HTTPS协议标准支持设备认证和数据上报两种消息类型
* <p>
* 1. 设备认证消息格式:
*
* <pre>
* POST /auth HTTP/1.1
* Content-Type: application/json
* {
* "productKey": "a1AbC***",
* "deviceName": "device01",
* "clientId": "device01_001",
* "timestamp": "1501668289957",
* "sign": "xxxxx",
* "signmethod": "hmacsha1",
* "version": "default"
* }
* </pre>
* <p>
* 2. 数据上报消息格式:
*
* <pre>
* POST /topic/${topic} HTTP/1.1
* password: ${token}
* Content-Type: application/octet-stream
* ${payload}
* </pre>
*
* @author haohao
*/
@Slf4j
public class IotHttpMessageParser implements IotMessageParser {
/**
* 认证路径
*/
public static final String AUTH_PATH = IotHttpConstants.Path.AUTH;
/**
* 主题路径前缀
*/
public static final String TOPIC_PATH_PREFIX = IotHttpConstants.Path.TOPIC_PREFIX;
@Override
public IotMqttMessage parse(String topic, byte[] payload) {
if (payload == null || payload.length == 0) {
log.warn(IotLogConstants.Http.RECEIVED_EMPTY_MESSAGE, topic);
return null;
}
try {
String message = new String(payload, StandardCharsets.UTF_8);
// 判断是认证请求还是数据上报
if (AUTH_PATH.equals(topic)) {
return parseAuthMessage(message);
} else if (topic.startsWith(TOPIC_PATH_PREFIX)) {
return parseDataMessage(topic, message);
} else {
log.warn(IotLogConstants.Http.UNSUPPORTED_PATH_FORMAT, topic);
return null;
}
} catch (Exception e) {
log.error(IotLogConstants.Http.PARSE_MESSAGE_FAILED, topic, e);
return null;
}
}
/**
* 解析设备认证消息
*
* @param message 认证消息JSON
* @return 标准消息格式
*/
private IotMqttMessage parseAuthMessage(String message) {
if (!JSONUtil.isTypeJSON(message)) {
log.warn(IotLogConstants.Http.AUTH_MESSAGE_NOT_JSON, message);
return null;
}
JSONObject json = JSONUtil.parseObj(message);
// 验证必需字段
String productKey = json.getStr(IotHttpConstants.AuthField.PRODUCT_KEY);
String deviceName = json.getStr(IotHttpConstants.AuthField.DEVICE_NAME);
String clientId = json.getStr(IotHttpConstants.AuthField.CLIENT_ID);
String sign = json.getStr(IotHttpConstants.AuthField.SIGN);
if (StrUtil.hasBlank(productKey, deviceName, clientId, sign)) {
log.warn(IotLogConstants.Http.AUTH_MESSAGE_MISSING_REQUIRED_FIELDS, message);
return null;
}
// 构建认证消息
Map<String, Object> params = new HashMap<>();
params.put(IotHttpConstants.AuthField.PRODUCT_KEY, productKey);
params.put(IotHttpConstants.AuthField.DEVICE_NAME, deviceName);
params.put(IotHttpConstants.AuthField.CLIENT_ID, clientId);
params.put(IotHttpConstants.AuthField.TIMESTAMP, json.getStr(IotHttpConstants.AuthField.TIMESTAMP));
params.put(IotHttpConstants.AuthField.SIGN, sign);
params.put(IotHttpConstants.AuthField.SIGN_METHOD,
json.getStr(IotHttpConstants.AuthField.SIGN_METHOD, IotHttpConstants.DefaultValue.SIGN_METHOD));
return IotMqttMessage.builder()
.id(generateMessageId())
.method(IotHttpConstants.Method.DEVICE_AUTH)
.version(json.getStr(IotHttpConstants.AuthField.VERSION, IotHttpConstants.DefaultValue.VERSION))
.params(params)
.build();
}
/**
* 解析数据上报消息
*
* @param topic 主题路径,格式:/topic/${actualTopic}
* @param message 消息内容
* @return 标准消息格式
*/
private IotMqttMessage parseDataMessage(String topic, String message) {
// 提取实际的主题,去掉 /topic 前缀
String actualTopic = topic.substring(TOPIC_PATH_PREFIX.length()); // 直接移除/topic前缀
// 尝试解析为JSON格式
if (JSONUtil.isTypeJSON(message)) {
return parseJsonDataMessage(actualTopic, message);
} else {
// 原始数据格式
return parseRawDataMessage(actualTopic, message);
}
}
/**
* 解析JSON格式的数据消息
*
* @param topic 实际主题
* @param message JSON消息
* @return 标准消息格式
*/
private IotMqttMessage parseJsonDataMessage(String topic, String message) {
JSONObject json = JSONUtil.parseObj(message);
// 生成消息ID
String messageId = json.getStr(IotHttpConstants.MessageField.ID);
if (StrUtil.isBlank(messageId)) {
messageId = generateMessageId();
}
// 获取方法名
String method = json.getStr(IotHttpConstants.MessageField.METHOD);
if (StrUtil.isBlank(method)) {
// 根据主题推断方法名
method = inferMethodFromTopic(topic);
}
// 获取参数
Object params = json.get(IotHttpConstants.MessageField.PARAMS);
Map<String, Object> paramsMap = new HashMap<>();
if (params instanceof Map) {
paramsMap.putAll((Map<String, Object>) params);
} else if (params != null) {
paramsMap.put(IotHttpConstants.MessageField.DATA, params);
}
return IotMqttMessage.builder()
.id(messageId)
.method(method)
.version(json.getStr(IotHttpConstants.MessageField.VERSION,
IotHttpConstants.DefaultValue.MESSAGE_VERSION))
.params(paramsMap)
.build();
}
/**
* 解析原始数据消息
*
* @param topic 实际主题
* @param message 原始消息
* @return 标准消息格式
*/
private IotMqttMessage parseRawDataMessage(String topic, String message) {
Map<String, Object> params = new HashMap<>();
params.put(IotHttpConstants.MessageField.DATA, message);
return IotMqttMessage.builder()
.id(generateMessageId())
.method(inferMethodFromTopic(topic))
.version(IotHttpConstants.DefaultValue.MESSAGE_VERSION)
.params(params)
.build();
}
/**
* 根据主题推断方法名
*
* @param topic 主题
* @return 方法名
*/
private String inferMethodFromTopic(String topic) {
if (StrUtil.isBlank(topic)) {
return IotHttpConstants.DefaultValue.UNKNOWN_METHOD;
}
// 标准系统主题解析
if (topic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
if (topic.contains(IotTopicConstants.PROPERTY_SET_TOPIC)) {
return IotTopicConstants.Method.PROPERTY_SET;
} else if (topic.contains(IotTopicConstants.PROPERTY_GET_TOPIC)) {
return IotTopicConstants.Method.PROPERTY_GET;
} else if (topic.contains(IotTopicConstants.PROPERTY_POST_TOPIC)) {
return IotTopicConstants.Method.PROPERTY_POST;
} else if (topic.contains(IotTopicConstants.EVENT_POST_TOPIC_PREFIX)
&& topic.endsWith(IotTopicConstants.EVENT_POST_TOPIC_SUFFIX)) {
// 自定义事件上报
String[] parts = topic.split("/");
// 查找event关键字的位置
for (int i = 0; i < parts.length; i++) {
if (IotTopicConstants.Keyword.EVENT.equals(parts[i]) && i + 1 < parts.length) {
String eventId = parts[i + 1];
return IotTopicConstants.MethodPrefix.THING_EVENT + eventId + ".post";
}
}
} else if (topic.contains(IotTopicConstants.SERVICE_TOPIC_PREFIX)
&& !topic.contains(IotTopicConstants.Keyword.PROPERTY)) {
// 自定义服务调用
String[] parts = topic.split("/");
// 查找service关键字的位置
for (int i = 0; i < parts.length; i++) {
if (IotTopicConstants.Keyword.SERVICE.equals(parts[i]) && i + 1 < parts.length) {
String serviceId = parts[i + 1];
return IotTopicConstants.MethodPrefix.THING_SERVICE + serviceId;
}
}
}
}
// 自定义主题
return IotHttpConstants.Method.CUSTOM_MESSAGE;
}
/**
* 生成消息ID
*
* @return 消息ID
*/
private String generateMessageId() {
return IotMqttMessage.generateRequestId();
}
@Override
public byte[] formatResponse(IotStandardResponse response) {
try {
JSONObject httpResponse = new JSONObject();
// 判断是否为认证响应
if (IotHttpConstants.Method.DEVICE_AUTH.equals(response.getMethod())) {
// 认证响应格式
httpResponse.set(IotHttpConstants.ResponseField.CODE, response.getCode());
httpResponse.set(IotHttpConstants.ResponseField.MESSAGE, response.getMessage());
if (response.getCode() == 200 && response.getData() != null) {
JSONObject info = new JSONObject();
if (response.getData() instanceof Map) {
Map<String, Object> dataMap = (Map<String, Object>) response.getData();
info.putAll(dataMap);
} else {
info.set(IotHttpConstants.ResponseField.TOKEN, response.getData().toString());
}
httpResponse.set(IotHttpConstants.ResponseField.INFO, info);
}
} else {
// 数据上报响应格式
httpResponse.set(IotHttpConstants.ResponseField.CODE, response.getCode());
httpResponse.set(IotHttpConstants.ResponseField.MESSAGE, response.getMessage());
if (response.getCode() == 200) {
JSONObject info = new JSONObject();
info.set(IotHttpConstants.ResponseField.MESSAGE_ID, response.getId());
httpResponse.set(IotHttpConstants.ResponseField.INFO, info);
}
}
String json = httpResponse.toString();
return json.getBytes(StandardCharsets.UTF_8);
} catch (Exception e) {
log.error(IotLogConstants.Http.FORMAT_RESPONSE_FAILED, e);
return new byte[0];
}
}
@Override
public boolean canHandle(String topic) {
// 支持认证路径和主题路径
return topic != null && (AUTH_PATH.equals(topic) || topic.startsWith(TOPIC_PATH_PREFIX));
}
/**
* 从设备标识中解析产品Key和设备名称
*
* @param deviceKey 设备标识格式productKey/deviceName
* @return 包含产品Key和设备名称的数组[0]为产品Key[1]为设备名称
*/
public static String[] parseDeviceKey(String deviceKey) {
if (StrUtil.isBlank(deviceKey)) {
return null;
}
String[] parts = deviceKey.split("/");
if (parts.length != 2) {
return null;
}
return new String[]{parts[0], parts[1]};
}
/**
* 构建设备标识
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return 设备标识格式productKey/deviceName
*/
public static String buildDeviceKey(String productKey, String deviceName) {
if (StrUtil.isBlank(productKey) || StrUtil.isBlank(deviceName)) {
return null;
}
return productKey + "/" + deviceName;
}
}

View File

@@ -1,87 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.message.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
import cn.iocoder.yudao.module.iot.protocol.message.IotMqttMessage;
import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
import cn.iocoder.yudao.module.iot.protocol.util.IotTopicUtils;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* IoT MQTT 协议消息解析器实现
* <p>
* 基于 MQTT 协议规范实现的消息解析器,支持设备属性、事件、服务调用等标准功能
*
* @author haohao
*/
@Slf4j
public class IotMqttMessageParser implements IotMessageParser {
@Override
public IotMqttMessage parse(String topic, byte[] payload) {
if (payload == null || payload.length == 0) {
log.warn("[MQTT] 收到空消息内容, topic={}", topic);
return null;
}
try {
String message = new String(payload, StandardCharsets.UTF_8);
if (!JSONUtil.isTypeJSON(message)) {
log.warn("[MQTT] 收到非JSON格式消息, topic={}, message={}", topic, message);
return null;
}
JSONObject json = JSONUtil.parseObj(message);
String id = json.getStr("id");
String method = json.getStr("method");
if (StrUtil.isBlank(method)) {
// 尝试从 topic 中解析方法
method = IotTopicUtils.parseMethodFromTopic(topic);
if (StrUtil.isBlank(method)) {
log.warn("[MQTT] 无法确定消息方法, topic={}, message={}", topic, message);
return null;
}
}
@SuppressWarnings("unchecked")
Map<String, Object> params = (Map<String, Object>) json.getObj("params", Map.class);
return IotMqttMessage.builder()
.id(id)
.method(method)
.version(json.getStr("version", "1.0"))
.params(params)
.build();
} catch (Exception e) {
log.error("[MQTT] 解析消息失败, topic={}", topic, e);
return null;
}
}
@Override
public byte[] formatResponse(IotStandardResponse response) {
try {
String json = JsonUtils.toJsonString(response);
return json.getBytes(StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("[MQTT] 格式化响应失败", e);
return new byte[0];
}
}
@Override
public boolean canHandle(String topic) {
// MQTT 协议支持更多主题格式
return topic != null && (
topic.startsWith("/sys/") || // 兼容现有系统主题
topic.startsWith("/mqtt/") || // 新的通用 MQTT 主题
topic.startsWith("/device/") // 设备主题
);
}
}

View File

@@ -1,279 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.util;
import cn.hutool.core.util.StrUtil;
/**
* IoT HTTP 协议主题工具类
* <p>
* 参考阿里云IoT平台HTTPS协议标准支持以下路径格式
* 1. 设备认证:/auth
* 2. 数据上报:/topic/${actualTopic}
* <p>
* 其中 actualTopic 遵循MQTT主题规范例如
* - /sys/{productKey}/{deviceName}/thing/service/property/set
* - /{productKey}/{deviceName}/user/get
*
* @author haohao
*/
public class IotHttpTopicUtils {
/**
* 设备认证路径
*/
public static final String AUTH_PATH = "/auth";
/**
* 数据上报路径前缀
*/
public static final String TOPIC_PATH_PREFIX = "/topic";
/**
* 系统主题前缀
*/
public static final String SYS_TOPIC_PREFIX = "/sys";
/**
* 构建设备认证路径
*
* @return 认证路径
*/
public static String buildAuthPath() {
return AUTH_PATH;
}
/**
* 构建数据上报路径
*
* @param actualTopic 实际的MQTT主题
* @return HTTP数据上报路径
*/
public static String buildTopicPath(String actualTopic) {
if (StrUtil.isBlank(actualTopic)) {
return null;
}
return TOPIC_PATH_PREFIX + actualTopic;
}
/**
* 构建系统属性设置路径
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return HTTP路径
*/
public static String buildPropertySetPath(String productKey, String deviceName) {
if (StrUtil.hasBlank(productKey, deviceName)) {
return null;
}
String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/service/property/set";
return buildTopicPath(actualTopic);
}
/**
* 构建系统属性获取路径
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return HTTP路径
*/
public static String buildPropertyGetPath(String productKey, String deviceName) {
if (StrUtil.hasBlank(productKey, deviceName)) {
return null;
}
String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/service/property/get";
return buildTopicPath(actualTopic);
}
/**
* 构建系统属性上报路径
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return HTTP路径
*/
public static String buildPropertyPostPath(String productKey, String deviceName) {
if (StrUtil.hasBlank(productKey, deviceName)) {
return null;
}
String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/event/property/post";
return buildTopicPath(actualTopic);
}
/**
* 构建系统事件上报路径
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @param eventIdentifier 事件标识符
* @return HTTP路径
*/
public static String buildEventPostPath(String productKey, String deviceName, String eventIdentifier) {
if (StrUtil.hasBlank(productKey, deviceName, eventIdentifier)) {
return null;
}
String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/event/" + eventIdentifier
+ "/post";
return buildTopicPath(actualTopic);
}
/**
* 构建系统服务调用路径
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @param serviceIdentifier 服务标识符
* @return HTTP路径
*/
public static String buildServiceInvokePath(String productKey, String deviceName, String serviceIdentifier) {
if (StrUtil.hasBlank(productKey, deviceName, serviceIdentifier)) {
return null;
}
String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/service/"
+ serviceIdentifier;
return buildTopicPath(actualTopic);
}
/**
* 构建自定义主题路径
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @param customPath 自定义路径
* @return HTTP路径
*/
public static String buildCustomTopicPath(String productKey, String deviceName, String customPath) {
if (StrUtil.hasBlank(productKey, deviceName, customPath)) {
return null;
}
String actualTopic = "/" + productKey + "/" + deviceName + "/" + customPath;
return buildTopicPath(actualTopic);
}
/**
* 从HTTP路径中提取实际主题
*
* @param httpPath HTTP路径格式/topic/${actualTopic}
* @return 实际主题如果解析失败返回null
*/
public static String extractActualTopic(String httpPath) {
if (StrUtil.isBlank(httpPath) || !httpPath.startsWith(TOPIC_PATH_PREFIX)) {
return null;
}
return httpPath.substring(TOPIC_PATH_PREFIX.length()); // 直接移除/topic前缀
}
/**
* 从主题中解析产品Key
*
* @param topic 主题,支持系统主题和自定义主题
* @return 产品Key如果无法解析则返回null
*/
public static String parseProductKeyFromTopic(String topic) {
if (StrUtil.isBlank(topic)) {
return null;
}
String[] parts = topic.split("/");
// 系统主题格式:/sys/{productKey}/{deviceName}/...
if (parts.length >= 4 && "sys".equals(parts[1])) {
return parts[2];
}
// 自定义主题格式:/{productKey}/{deviceName}/...
// 确保不是不完整的系统主题格式
if (parts.length >= 3 && StrUtil.isNotBlank(parts[1]) && !"sys".equals(parts[1])) {
return parts[1];
}
return null;
}
/**
* 从主题中解析设备名称
*
* @param topic 主题,支持系统主题和自定义主题
* @return 设备名称如果无法解析则返回null
*/
public static String parseDeviceNameFromTopic(String topic) {
if (StrUtil.isBlank(topic)) {
return null;
}
String[] parts = topic.split("/");
// 系统主题格式:/sys/{productKey}/{deviceName}/...
if (parts.length >= 4 && "sys".equals(parts[1])) {
return parts[3];
}
// 自定义主题格式:/{productKey}/{deviceName}/...
// 确保不是不完整的系统主题格式
if (parts.length >= 3 && StrUtil.isNotBlank(parts[2]) && !"sys".equals(parts[1])) {
return parts[2];
}
return null;
}
/**
* 检查是否为认证路径
*
* @param path 路径
* @return 如果是认证路径返回true否则返回false
*/
public static boolean isAuthPath(String path) {
return AUTH_PATH.equals(path);
}
/**
* 检查是否为数据上报路径
*
* @param path 路径
* @return 如果是数据上报路径返回true否则返回false
*/
public static boolean isTopicPath(String path) {
return path != null && path.startsWith(TOPIC_PATH_PREFIX);
}
/**
* 检查是否为有效的HTTP路径
*
* @param path 路径
* @return 如果是有效的HTTP路径返回true否则返回false
*/
public static boolean isValidHttpPath(String path) {
return isAuthPath(path) || isTopicPath(path);
}
/**
* 检查是否为系统主题
*
* @param topic 主题
* @return 如果是系统主题返回true否则返回false
*/
public static boolean isSystemTopic(String topic) {
return topic != null && topic.startsWith(SYS_TOPIC_PREFIX);
}
/**
* 构建响应主题路径
*
* @param requestPath 请求路径
* @return 响应路径如果无法构建返回null
*/
public static String buildReplyPath(String requestPath) {
String actualTopic = extractActualTopic(requestPath);
if (actualTopic == null) {
return null;
}
// 为系统主题添加_reply后缀
if (isSystemTopic(actualTopic)) {
String replyTopic = actualTopic + "_reply";
return buildTopicPath(replyTopic);
}
return null;
}
}

View File

@@ -1,237 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.util;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.protocol.constants.IotTopicConstants;
import cn.iocoder.yudao.module.iot.protocol.enums.IotMessageDirectionEnum;
import cn.iocoder.yudao.module.iot.protocol.enums.IotMessageTypeEnum;
import lombok.Data;
/**
* IoT 主题解析器
* <p>
* 用于解析各种格式的 IoT 主题,提取其中的关键信息
*
* @author haohao
*/
public class IotTopicParser {
/**
* 主题解析结果
*/
@Data
public static class TopicInfo {
/**
* 产品Key
*/
private String productKey;
/**
* 设备名称
*/
private String deviceName;
/**
* 消息类型
*/
private IotMessageTypeEnum messageType;
/**
* 消息方向
*/
private IotMessageDirectionEnum direction;
/**
* 服务标识符(仅服务调用时有效)
*/
private String serviceIdentifier;
/**
* 事件标识符(仅事件上报时有效)
*/
private String eventIdentifier;
/**
* 是否为响应主题
*/
private boolean isReply;
/**
* 原始主题
*/
private String originalTopic;
}
/**
* 解析主题
*
* @param topic 主题字符串
* @return 解析结果,如果解析失败返回 null
*/
public static TopicInfo parse(String topic) {
if (StrUtil.isBlank(topic)) {
return null;
}
TopicInfo info = new TopicInfo();
info.setOriginalTopic(topic);
// 检查是否为响应主题
boolean isReply = topic.endsWith(IotTopicConstants.REPLY_SUFFIX);
info.setReply(isReply);
// 移除响应后缀,便于后续解析
String normalizedTopic = isReply ? topic.substring(0, topic.length() - IotTopicConstants.REPLY_SUFFIX.length())
: topic;
// 解析系统主题
if (normalizedTopic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
return parseSystemTopic(info, normalizedTopic);
}
// 解析自定义主题
return parseCustomTopic(info, normalizedTopic);
}
/**
* 解析系统主题
* 格式:/sys/{productKey}/{deviceName}/thing/service/{identifier}
* 或:/sys/{productKey}/{deviceName}/thing/event/{identifier}/post
*/
private static TopicInfo parseSystemTopic(TopicInfo info, String topic) {
String[] parts = topic.split("/");
if (parts.length < 6) {
return null;
}
// 解析产品Key和设备名称
info.setProductKey(parts[2]);
info.setDeviceName(parts[3]);
// 判断消息方向:包含 /post 通常是上行,其他是下行
info.setDirection(topic.contains("/post") || topic.contains("/reply") ? IotMessageDirectionEnum.UPSTREAM
: IotMessageDirectionEnum.DOWNSTREAM);
// 解析具体的消息类型
if (topic.contains("/thing/service/")) {
return parseServiceTopic(info, topic, parts);
} else if (topic.contains("/thing/event/")) {
return parseEventTopic(info, topic, parts);
}
return null;
}
/**
* 解析服务相关主题
*/
private static TopicInfo parseServiceTopic(TopicInfo info, String topic, String[] parts) {
// 查找 service 关键字的位置
int serviceIndex = -1;
for (int i = 0; i < parts.length; i++) {
if ("service".equals(parts[i])) {
serviceIndex = i;
break;
}
}
if (serviceIndex == -1 || serviceIndex + 1 >= parts.length) {
return null;
}
String serviceType = parts[serviceIndex + 1];
// 根据服务类型确定消息类型
switch (serviceType) {
case "property":
if (serviceIndex + 2 < parts.length) {
String operation = parts[serviceIndex + 2];
if ("set".equals(operation)) {
info.setMessageType(IotMessageTypeEnum.PROPERTY_SET);
} else if ("get".equals(operation)) {
info.setMessageType(IotMessageTypeEnum.PROPERTY_GET);
}
}
break;
case "config":
if (serviceIndex + 2 < parts.length && "set".equals(parts[serviceIndex + 2])) {
info.setMessageType(IotMessageTypeEnum.CONFIG_SET);
}
break;
case "ota":
if (serviceIndex + 2 < parts.length && "upgrade".equals(parts[serviceIndex + 2])) {
info.setMessageType(IotMessageTypeEnum.OTA_UPGRADE);
}
break;
default:
// 自定义服务
info.setMessageType(IotMessageTypeEnum.SERVICE_INVOKE);
info.setServiceIdentifier(serviceType);
break;
}
return info;
}
/**
* 解析事件相关主题
*/
private static TopicInfo parseEventTopic(TopicInfo info, String topic, String[] parts) {
// 查找 event 关键字的位置
int eventIndex = -1;
for (int i = 0; i < parts.length; i++) {
if ("event".equals(parts[i])) {
eventIndex = i;
break;
}
}
if (eventIndex == -1 || eventIndex + 1 >= parts.length) {
return null;
}
String eventType = parts[eventIndex + 1];
if ("property".equals(eventType) && eventIndex + 2 < parts.length && "post".equals(parts[eventIndex + 2])) {
info.setMessageType(IotMessageTypeEnum.PROPERTY_POST);
} else {
// 自定义事件
info.setMessageType(IotMessageTypeEnum.EVENT_POST);
info.setEventIdentifier(eventType);
}
return info;
}
/**
* 解析自定义主题
* 这里可以根据实际需求扩展自定义主题的解析逻辑
*/
private static TopicInfo parseCustomTopic(TopicInfo info, String topic) {
// TODO: 根据业务需要实现自定义主题解析逻辑
return info;
}
/**
* 检查主题是否为有效的系统主题
*
* @param topic 主题
* @return 如果是有效的系统主题返回 true否则返回 false
*/
public static boolean isValidSystemTopic(String topic) {
TopicInfo info = parse(topic);
return info != null &&
StrUtil.isNotBlank(info.getProductKey()) &&
StrUtil.isNotBlank(info.getDeviceName()) &&
info.getMessageType() != null;
}
/**
* 检查主题是否为响应主题
*
* @param topic 主题
* @return 如果是响应主题返回 true否则返回 false
*/
public static boolean isReplyTopic(String topic) {
return topic != null && topic.endsWith(IotTopicConstants.REPLY_SUFFIX);
}
}

View File

@@ -1,184 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.util;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.protocol.constants.IotTopicConstants;
/**
* IoT 主题工具类
* <p>
* 用于构建和解析设备主题
*
* @author haohao
*/
public class IotTopicUtils {
/**
* 构建设备服务调用主题
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @param serviceIdentifier 服务标识符
* @return 完整的主题路径
*/
public static String buildServiceTopic(String productKey, String deviceName, String serviceIdentifier) {
return buildDeviceBaseTopic(productKey, deviceName) +
IotTopicConstants.SERVICE_TOPIC_PREFIX + serviceIdentifier;
}
/**
* 构建设备属性设置主题
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return 完整的主题路径
*/
public static String buildPropertySetTopic(String productKey, String deviceName) {
return buildDeviceBaseTopic(productKey, deviceName) + IotTopicConstants.PROPERTY_SET_TOPIC;
}
/**
* 构建设备属性获取主题
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return 完整的主题路径
*/
public static String buildPropertyGetTopic(String productKey, String deviceName) {
return buildDeviceBaseTopic(productKey, deviceName) + IotTopicConstants.PROPERTY_GET_TOPIC;
}
/**
* 构建设备配置设置主题
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return 完整的主题路径
*/
public static String buildConfigSetTopic(String productKey, String deviceName) {
return buildDeviceBaseTopic(productKey, deviceName) + IotTopicConstants.CONFIG_SET_TOPIC;
}
/**
* 构建设备 OTA 升级主题
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return 完整的主题路径
*/
public static String buildOtaUpgradeTopic(String productKey, String deviceName) {
return buildDeviceBaseTopic(productKey, deviceName) + IotTopicConstants.OTA_UPGRADE_TOPIC;
}
/**
* 构建设备属性上报主题
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return 完整的主题路径
*/
public static String buildPropertyPostTopic(String productKey, String deviceName) {
return buildDeviceBaseTopic(productKey, deviceName) + IotTopicConstants.PROPERTY_POST_TOPIC;
}
/**
* 构建设备事件上报主题
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @param eventIdentifier 事件标识符
* @return 完整的主题路径
*/
public static String buildEventPostTopic(String productKey, String deviceName, String eventIdentifier) {
return buildDeviceBaseTopic(productKey, deviceName) +
IotTopicConstants.EVENT_POST_TOPIC_PREFIX + eventIdentifier + IotTopicConstants.EVENT_POST_TOPIC_SUFFIX;
}
/**
* 获取响应主题
*
* @param requestTopic 请求主题
* @return 响应主题
*/
public static String getReplyTopic(String requestTopic) {
return requestTopic + IotTopicConstants.REPLY_SUFFIX;
}
/**
* 构建设备基础主题
* 格式: /sys/${productKey}/${deviceName}
*
* @param productKey 产品Key
* @param deviceName 设备名称
* @return 设备基础主题
*/
public static String buildDeviceBaseTopic(String productKey, String deviceName) {
return IotTopicConstants.SYS_TOPIC_PREFIX + productKey + "/" + deviceName;
}
/**
* 从主题中解析产品Key
* 格式: /sys/${productKey}/${deviceName}/...
*
* @param topic 主题
* @return 产品Key如果无法解析则返回null
*/
public static String parseProductKeyFromTopic(String topic) {
if (StrUtil.isBlank(topic) || !topic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
return null;
}
String[] parts = topic.split("/");
if (parts.length < 4) {
return null;
}
return parts[2];
}
/**
* 从主题中解析设备名称
* 格式: /sys/${productKey}/${deviceName}/...
*
* @param topic 主题
* @return 设备名称如果无法解析则返回null
*/
public static String parseDeviceNameFromTopic(String topic) {
if (StrUtil.isBlank(topic) || !topic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
return null;
}
String[] parts = topic.split("/");
if (parts.length < 4) {
return null;
}
return parts[3];
}
/**
* 从主题中解析方法名
* 例如:从 /sys/pk/dn/thing/service/property/set 解析出 property.set
*
* @param topic 主题
* @return 方法名如果无法解析则返回null
*/
public static String parseMethodFromTopic(String topic) {
if (StrUtil.isBlank(topic) || !topic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
return null;
}
// 服务调用主题
if (topic.contains("/thing/service/")) {
String servicePart = topic.substring(topic.indexOf("/thing/service/") + "/thing/service/".length());
return servicePart.replace("/", ".");
}
// 事件上报主题
if (topic.contains("/thing/event/")) {
String eventPart = topic.substring(topic.indexOf("/thing/event/") + "/thing/event/".length());
return "event." + eventPart.replace("/", ".");
}
return null;
}
}

View File

@@ -1 +0,0 @@
cn.iocoder.yudao.module.iot.protocol.config.IotProtocolAutoConfiguration

View File

@@ -1,71 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.config;
import cn.iocoder.yudao.module.iot.protocol.convert.IotProtocolConverter;
import cn.iocoder.yudao.module.iot.protocol.enums.IotProtocolTypeEnum;
import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
import cn.iocoder.yudao.module.iot.protocol.message.impl.IotHttpMessageParser;
import cn.iocoder.yudao.module.iot.protocol.message.impl.IotMqttMessageParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link IotProtocolAutoConfiguration} 单元测试
*
* @author haohao
*/
class IotProtocolAutoConfigurationTest {
private IotProtocolAutoConfiguration configuration;
@BeforeEach
void setUp() {
configuration = new IotProtocolAutoConfiguration();
}
@Test
void testIotMqttMessageParser() {
// 测试 MQTT 协议解析器 Bean 创建
IotMessageParser parser = configuration.iotMqttMessageParser();
assertNotNull(parser);
assertInstanceOf(IotMqttMessageParser.class, parser);
}
@Test
void testIotHttpMessageParser() {
// 测试 HTTP 协议解析器 Bean 创建
IotMessageParser parser = configuration.iotHttpMessageParser();
assertNotNull(parser);
assertInstanceOf(IotHttpMessageParser.class, parser);
}
@Test
void testIotProtocolConverter() {
// 创建解析器实例
IotMessageParser mqttParser = configuration.iotMqttMessageParser();
IotMessageParser httpParser = configuration.iotHttpMessageParser();
// 测试协议转换器 Bean 创建
IotProtocolConverter converter = configuration.iotProtocolConverter(mqttParser, httpParser);
assertNotNull(converter);
// 验证支持的协议
assertTrue(converter.supportsProtocol(IotProtocolTypeEnum.MQTT.getCode()));
assertTrue(converter.supportsProtocol(IotProtocolTypeEnum.HTTP.getCode()));
// 验证支持的协议数量
String[] supportedProtocols = converter.getSupportedProtocols();
assertEquals(2, supportedProtocols.length);
}
@Test
void testBeanNameConstants() {
// 测试 Bean 名称常量定义
assertEquals("iotMqttMessageParser", IotProtocolAutoConfiguration.IOT_MQTT_MESSAGE_PARSER_BEAN_NAME);
assertEquals("iotHttpMessageParser", IotProtocolAutoConfiguration.IOT_HTTP_MESSAGE_PARSER_BEAN_NAME);
}
}

View File

@@ -1,166 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.example;
import cn.hutool.json.JSONObject;
import cn.iocoder.yudao.module.iot.protocol.util.IotHttpTopicUtils;
/**
* 阿里云IoT平台HTTPS协议示例
* <p>
* 参考阿里云IoT平台HTTPS连接通信标准演示设备认证和数据上报的完整流程
*
* @author haohao
*/
public class AliyunHttpProtocolExample {
public static void main(String[] args) {
System.out.println("=== 阿里云IoT平台HTTPS协议演示 ===\n");
// 演示设备认证
demonstrateDeviceAuth();
// 演示数据上报
demonstrateDataUpload();
// 演示路径构建
demonstratePathBuilding();
}
/**
* 演示设备认证流程
*/
private static void demonstrateDeviceAuth() {
System.out.println("1. 设备认证流程:");
System.out.println("认证路径: " + IotHttpTopicUtils.buildAuthPath());
// 构建认证请求消息
JSONObject authRequest = new JSONObject();
authRequest.set("productKey", "a1GFjLP****");
authRequest.set("deviceName", "device123");
authRequest.set("clientId", "device123_001");
authRequest.set("timestamp", String.valueOf(System.currentTimeMillis()));
authRequest.set("sign", "4870141D4067227128CBB4377906C3731CAC221C");
authRequest.set("signmethod", "hmacsha1");
authRequest.set("version", "default");
System.out.println("认证请求消息:");
System.out.println(authRequest.toString());
// 模拟认证响应
JSONObject authResponse = new JSONObject();
authResponse.set("code", 0);
authResponse.set("message", "success");
JSONObject info = new JSONObject();
info.set("token", "6944e5bfb92e4d4ea3918d1eda39****");
authResponse.set("info", info);
System.out.println("认证响应:");
System.out.println(authResponse.toString());
System.out.println();
}
/**
* 演示数据上报流程
*/
private static void demonstrateDataUpload() {
System.out.println("2. 数据上报流程:");
String productKey = "a1GFjLP****";
String deviceName = "device123";
// 属性上报
String propertyPostPath = IotHttpTopicUtils.buildPropertyPostPath(productKey, deviceName);
System.out.println("属性上报路径: " + propertyPostPath);
// Alink格式的属性上报消息
JSONObject propertyMessage = new JSONObject();
propertyMessage.set("id", "123456");
propertyMessage.set("version", "1.0");
propertyMessage.set("method", "thing.event.property.post");
JSONObject propertyParams = new JSONObject();
JSONObject properties = new JSONObject();
properties.set("temperature", 25.6);
properties.set("humidity", 60.3);
propertyParams.set("properties", properties);
propertyMessage.set("params", propertyParams);
System.out.println("属性上报消息:");
System.out.println(propertyMessage.toString());
// 事件上报
String eventPostPath = IotHttpTopicUtils.buildEventPostPath(productKey, deviceName, "temperatureAlert");
System.out.println("\n事件上报路径: " + eventPostPath);
JSONObject eventMessage = new JSONObject();
eventMessage.set("id", "123457");
eventMessage.set("version", "1.0");
eventMessage.set("method", "thing.event.temperatureAlert.post");
JSONObject eventParams = new JSONObject();
eventParams.set("value", new JSONObject().set("alertLevel", "high").set("currentTemp", 45.2));
eventParams.set("time", System.currentTimeMillis());
eventMessage.set("params", eventParams);
System.out.println("事件上报消息:");
System.out.println(eventMessage.toString());
// 模拟数据上报响应
JSONObject uploadResponse = new JSONObject();
uploadResponse.set("code", 0);
uploadResponse.set("message", "success");
JSONObject responseInfo = new JSONObject();
responseInfo.set("messageId", 892687470447040L);
uploadResponse.set("info", responseInfo);
System.out.println("\n数据上报响应:");
System.out.println(uploadResponse.toString());
System.out.println();
}
/**
* 演示路径构建功能
*/
private static void demonstratePathBuilding() {
System.out.println("3. 路径构建功能:");
String productKey = "smartProduct";
String deviceName = "sensor001";
// 系统主题路径
System.out.println("系统主题路径:");
System.out.println(" 属性设置: " + IotHttpTopicUtils.buildPropertySetPath(productKey, deviceName));
System.out.println(" 属性获取: " + IotHttpTopicUtils.buildPropertyGetPath(productKey, deviceName));
System.out.println(" 属性上报: " + IotHttpTopicUtils.buildPropertyPostPath(productKey, deviceName));
System.out.println(" 事件上报: " + IotHttpTopicUtils.buildEventPostPath(productKey, deviceName, "alarm"));
System.out.println(" 服务调用: " + IotHttpTopicUtils.buildServiceInvokePath(productKey, deviceName, "reboot"));
// 自定义主题路径
System.out.println("\n自定义主题路径:");
System.out.println(" 用户主题: " + IotHttpTopicUtils.buildCustomTopicPath(productKey, deviceName, "user/get"));
// 响应路径
String requestPath = IotHttpTopicUtils.buildPropertySetPath(productKey, deviceName);
String replyPath = IotHttpTopicUtils.buildReplyPath(requestPath);
System.out.println("\n响应路径:");
System.out.println(" 请求路径: " + requestPath);
System.out.println(" 响应路径: " + replyPath);
// 路径解析
System.out.println("\n路径解析:");
String testPath = "/topic/sys/smartProduct/sensor001/thing/service/property/set";
String actualTopic = IotHttpTopicUtils.extractActualTopic(testPath);
System.out.println(" HTTP路径: " + testPath);
System.out.println(" 实际主题: " + actualTopic);
System.out.println(" 产品Key: " + IotHttpTopicUtils.parseProductKeyFromTopic(actualTopic));
System.out.println(" 设备名称: " + IotHttpTopicUtils.parseDeviceNameFromTopic(actualTopic));
System.out.println(" 是否为系统主题: " + IotHttpTopicUtils.isSystemTopic(actualTopic));
// 路径类型检查
System.out.println("\n路径类型检查:");
System.out.println(" 认证路径检查: " + IotHttpTopicUtils.isAuthPath("/auth"));
System.out.println(" 数据路径检查: " + IotHttpTopicUtils.isTopicPath("/topic/test"));
System.out.println(" 有效路径检查: " + IotHttpTopicUtils.isValidHttpPath("/topic/sys/test/device/property"));
}
}

View File

@@ -1,259 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.message.impl;
import cn.hutool.json.JSONObject;
import cn.iocoder.yudao.module.iot.protocol.message.IotMqttMessage;
import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link IotHttpMessageParser} 单元测试
* <p>
* 测试阿里云IoT平台HTTPS协议标准的消息解析功能
*
* @author haohao
*/
class IotHttpMessageParserTest {
private IotHttpMessageParser parser;
@BeforeEach
void setUp() {
parser = new IotHttpMessageParser();
}
@Test
void testCanHandle() {
// 测试能处理的路径
assertTrue(parser.canHandle("/auth"));
assertTrue(parser.canHandle("/topic/sys/test/device1/thing/service/property/set"));
assertTrue(parser.canHandle("/topic/test/device1/user/get"));
// 测试不能处理的路径
assertFalse(parser.canHandle("/sys/test/device1/thing/service/property/set"));
assertFalse(parser.canHandle("/unknown/path"));
assertFalse(parser.canHandle(null));
assertFalse(parser.canHandle(""));
}
@Test
void testParseAuthMessage() {
// 构建认证消息
JSONObject authMessage = new JSONObject();
authMessage.set("productKey", "a1GFjLP****");
authMessage.set("deviceName", "device123");
authMessage.set("clientId", "device123_001");
authMessage.set("timestamp", "1501668289957");
authMessage.set("sign", "4870141D4067227128CBB4377906C3731CAC221C");
authMessage.set("signmethod", "hmacsha1");
authMessage.set("version", "default");
String topic = "/auth";
byte[] payload = authMessage.toString().getBytes(StandardCharsets.UTF_8);
// 解析消息
IotMqttMessage result = parser.parse(topic, payload);
// 验证结果
assertNotNull(result);
assertNotNull(result.getId());
assertEquals("device.auth", result.getMethod());
assertEquals("default", result.getVersion());
assertNotNull(result.getParams());
Map<String, Object> params = result.getParams();
assertEquals("a1GFjLP****", params.get("productKey"));
assertEquals("device123", params.get("deviceName"));
assertEquals("device123_001", params.get("clientId"));
assertEquals("1501668289957", params.get("timestamp"));
assertEquals("4870141D4067227128CBB4377906C3731CAC221C", params.get("sign"));
assertEquals("hmacsha1", params.get("signmethod"));
}
@Test
void testParseAuthMessageWithMissingFields() {
// 构建缺少必需字段的认证消息
JSONObject authMessage = new JSONObject();
authMessage.set("productKey", "a1GFjLP****");
authMessage.set("deviceName", "device123");
// 缺少 clientId 和 sign
String topic = "/auth";
byte[] payload = authMessage.toString().getBytes(StandardCharsets.UTF_8);
// 解析消息
IotMqttMessage result = parser.parse(topic, payload);
// 验证结果
assertNull(result);
}
@Test
void testParseJsonDataMessage() {
// 构建JSON格式的数据消息
JSONObject dataMessage = new JSONObject();
dataMessage.set("id", "123456");
dataMessage.set("version", "1.0");
dataMessage.set("method", "thing.event.property.post");
JSONObject params = new JSONObject();
JSONObject properties = new JSONObject();
properties.set("temperature", 25.6);
properties.set("humidity", 60.3);
params.set("properties", properties);
dataMessage.set("params", params);
String topic = "/topic/sys/a1GFjLP****/device123/thing/event/property/post";
byte[] payload = dataMessage.toString().getBytes(StandardCharsets.UTF_8);
// 解析消息
IotMqttMessage result = parser.parse(topic, payload);
// 验证结果
assertNotNull(result);
assertEquals("123456", result.getId());
assertEquals("thing.event.property.post", result.getMethod());
assertEquals("1.0", result.getVersion());
assertNotNull(result.getParams());
assertNotNull(result.getParams().get("properties"));
}
@Test
void testParseRawDataMessage() {
// 原始数据消息
String rawData = "temperature:25.6,humidity:60.3";
String topic = "/topic/sys/a1GFjLP****/device123/thing/event/property/post";
byte[] payload = rawData.getBytes(StandardCharsets.UTF_8);
// 解析消息
IotMqttMessage result = parser.parse(topic, payload);
// 验证结果
assertNotNull(result);
assertNotNull(result.getId());
assertEquals("thing.event.property.post", result.getMethod());
assertEquals("1.0", result.getVersion());
assertNotNull(result.getParams());
assertEquals(rawData, result.getParams().get("data"));
}
@Test
void testInferMethodFromTopic() {
// 测试系统主题方法推断
testInferMethod("/sys/test/device/thing/service/property/set", "thing.service.property.set");
testInferMethod("/sys/test/device/thing/service/property/get", "thing.service.property.get");
testInferMethod("/sys/test/device/thing/event/property/post", "thing.event.property.post");
testInferMethod("/sys/test/device/thing/event/alarm/post", "thing.event.alarm.post");
testInferMethod("/sys/test/device/thing/service/reboot", "thing.service.reboot");
// 测试自定义主题
testInferMethod("/test/device/user/get", "custom.message");
}
private void testInferMethod(String actualTopic, String expectedMethod) {
String topic = "/topic" + actualTopic;
String rawData = "test data";
byte[] payload = rawData.getBytes(StandardCharsets.UTF_8);
IotMqttMessage result = parser.parse(topic, payload);
assertNotNull(result);
assertEquals(expectedMethod, result.getMethod());
}
@Test
void testFormatAuthResponse() {
// 创建认证成功响应
Map<String, Object> data = new HashMap<>();
data.put("token", "6944e5bfb92e4d4ea3918d1eda39****");
IotStandardResponse response = IotStandardResponse.success("auth123", "device.auth", data);
// 格式化响应
byte[] result = parser.formatResponse(response);
// 验证结果
assertNotNull(result);
assertTrue(result.length > 0);
String responseStr = new String(result, StandardCharsets.UTF_8);
JSONObject responseJson = new JSONObject(responseStr);
assertEquals(200, responseJson.getInt("code"));
assertEquals("success", responseJson.getStr("message"));
assertNotNull(responseJson.get("info"));
JSONObject info = responseJson.getJSONObject("info");
assertEquals("6944e5bfb92e4d4ea3918d1eda39****", info.getStr("token"));
}
@Test
void testFormatDataResponse() {
// 创建数据上报响应
IotStandardResponse response = IotStandardResponse.success("123456", "thing.event.property.post", null);
// 格式化响应
byte[] result = parser.formatResponse(response);
// 验证结果
assertNotNull(result);
assertTrue(result.length > 0);
String responseStr = new String(result, StandardCharsets.UTF_8);
JSONObject responseJson = new JSONObject(responseStr);
assertEquals(200, responseJson.getInt("code"));
assertEquals("success", responseJson.getStr("message"));
assertNotNull(responseJson.get("info"));
JSONObject info = responseJson.getJSONObject("info");
assertEquals("123456", info.getStr("messageId"));
}
@Test
void testParseInvalidMessage() {
String topic = "/topic/sys/test/device/thing/service/property/set";
// 测试空消息
assertNull(parser.parse(topic, null));
assertNull(parser.parse(topic, new byte[0]));
// 测试不支持的路径
byte[] validPayload = "test data".getBytes(StandardCharsets.UTF_8);
assertNull(parser.parse("/unknown/path", validPayload));
}
@Test
void testParseDeviceKey() {
// 测试有效的设备标识
String[] result1 = IotHttpMessageParser.parseDeviceKey("productKey/deviceName");
assertNotNull(result1);
assertEquals(2, result1.length);
assertEquals("productKey", result1[0]);
assertEquals("deviceName", result1[1]);
// 测试无效的设备标识
assertNull(IotHttpMessageParser.parseDeviceKey(null));
assertNull(IotHttpMessageParser.parseDeviceKey(""));
assertNull(IotHttpMessageParser.parseDeviceKey("invalid"));
assertNull(IotHttpMessageParser.parseDeviceKey("product/device/extra"));
}
@Test
void testBuildDeviceKey() {
// 测试构建设备标识
assertEquals("productKey/deviceName",
IotHttpMessageParser.buildDeviceKey("productKey", "deviceName"));
// 测试无效参数
assertNull(IotHttpMessageParser.buildDeviceKey(null, "deviceName"));
assertNull(IotHttpMessageParser.buildDeviceKey("productKey", null));
assertNull(IotHttpMessageParser.buildDeviceKey("", "deviceName"));
assertNull(IotHttpMessageParser.buildDeviceKey("productKey", ""));
}
}

View File

@@ -1,190 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.message.impl;
import cn.hutool.json.JSONObject;
import cn.iocoder.yudao.module.iot.protocol.message.IotMqttMessage;
import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* IoT MQTT 消息解析器测试类
*
* @author haohao
*/
class IotMqttMessageParserTest {
private IotMqttMessageParser parser;
@BeforeEach
void setUp() {
parser = new IotMqttMessageParser();
}
@Test
void testParseValidJsonMessage() {
// 构建有效的 JSON 消息
JSONObject message = new JSONObject();
message.set("id", "123456");
message.set("version", "1.0");
message.set("method", "thing.service.property.set");
Map<String, Object> params = new HashMap<>();
params.put("temperature", 25.5);
params.put("humidity", 60.0);
message.set("params", params);
String topic = "/sys/productKey/deviceName/thing/service/property/set";
byte[] payload = message.toString().getBytes(StandardCharsets.UTF_8);
// 解析消息
IotMqttMessage result = parser.parse(topic, payload);
// 验证结果
assertNotNull(result);
assertEquals("123456", result.getId());
assertEquals("1.0", result.getVersion());
assertEquals("thing.service.property.set", result.getMethod());
assertNotNull(result.getParams());
assertEquals(25.5, ((Number) result.getParams().get("temperature")).doubleValue());
assertEquals(60.0, ((Number) result.getParams().get("humidity")).doubleValue());
}
@Test
void testParseMessageWithoutMethod() {
// 构建没有 method 字段的消息,应该从 topic 中解析
JSONObject message = new JSONObject();
message.set("id", "789012");
message.set("version", "1.0");
Map<String, Object> params = new HashMap<>();
params.put("voltage", 3.3);
message.set("params", params);
String topic = "/sys/productKey/deviceName/thing/service/property/set";
byte[] payload = message.toString().getBytes(StandardCharsets.UTF_8);
// 解析消息
IotMqttMessage result = parser.parse(topic, payload);
// 验证结果
assertNotNull(result);
assertEquals("789012", result.getId());
assertEquals("1.0", result.getVersion());
assertNotNull(result.getMethod()); // 应该从 topic 中解析出方法
assertNotNull(result.getParams());
assertEquals(3.3, ((Number) result.getParams().get("voltage")).doubleValue());
}
@Test
void testParseInvalidJsonMessage() {
String topic = "/sys/productKey/deviceName/thing/service/property/set";
byte[] payload = "invalid json".getBytes(StandardCharsets.UTF_8);
// 解析消息
IotMqttMessage result = parser.parse(topic, payload);
// 验证结果
assertNull(result);
}
@Test
void testParseEmptyPayload() {
String topic = "/sys/productKey/deviceName/thing/service/property/set";
// 测试 null payload
IotMqttMessage result1 = parser.parse(topic, null);
assertNull(result1);
// 测试空 payload
IotMqttMessage result2 = parser.parse(topic, new byte[0]);
assertNull(result2);
}
@Test
void testFormatResponse() {
// 创建标准响应
IotStandardResponse response = IotStandardResponse.success("123456", "property.set", null);
// 格式化响应
byte[] result = parser.formatResponse(response);
// 验证结果
assertNotNull(result);
assertTrue(result.length > 0);
// 验证 JSON 格式
String jsonString = new String(result, StandardCharsets.UTF_8);
assertTrue(jsonString.contains("123456"));
assertTrue(jsonString.contains("property.set"));
}
@Test
void testCanHandle() {
// 测试支持的主题格式
assertTrue(parser.canHandle("/sys/productKey/deviceName/thing/service/property/set"));
assertTrue(parser.canHandle("/mqtt/productKey/deviceName/property/set"));
assertTrue(parser.canHandle("/device/productKey/deviceName/data"));
// 测试不支持的主题格式
assertFalse(parser.canHandle("/http/device/productKey/deviceName/property/set"));
assertFalse(parser.canHandle("/unknown/topic"));
assertFalse(parser.canHandle(null));
assertFalse(parser.canHandle(""));
}
@Test
void testParseMqttTopicFormat() {
// 测试新的 MQTT 主题格式
JSONObject message = new JSONObject();
message.set("id", "mqtt001");
message.set("version", "1.0");
message.set("method", "device.property.report");
Map<String, Object> params = new HashMap<>();
params.put("signal", 85);
message.set("params", params);
String topic = "/mqtt/productKey/deviceName/property/report";
byte[] payload = message.toString().getBytes(StandardCharsets.UTF_8);
// 解析消息
IotMqttMessage result = parser.parse(topic, payload);
// 验证结果
assertNotNull(result);
assertEquals("mqtt001", result.getId());
assertEquals("device.property.report", result.getMethod());
assertEquals(85, ((Number) result.getParams().get("signal")).intValue());
}
@Test
void testParseDeviceTopicFormat() {
// 测试设备主题格式
JSONObject message = new JSONObject();
message.set("id", "device001");
message.set("version", "1.0");
message.set("method", "sensor.data");
Map<String, Object> params = new HashMap<>();
params.put("timestamp", System.currentTimeMillis());
message.set("params", params);
String topic = "/device/productKey/deviceName/sensor/data";
byte[] payload = message.toString().getBytes(StandardCharsets.UTF_8);
// 解析消息
IotMqttMessage result = parser.parse(topic, payload);
// 验证结果
assertNotNull(result);
assertEquals("device001", result.getId());
assertEquals("sensor.data", result.getMethod());
assertNotNull(result.getParams().get("timestamp"));
}
}

View File

@@ -1,186 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* {@link IotHttpTopicUtils} 单元测试
*
* @author haohao
*/
class IotHttpTopicUtilsTest {
@Test
void testBuildAuthPath() {
assertEquals("/auth", IotHttpTopicUtils.buildAuthPath());
}
@Test
void testBuildTopicPath() {
// 测试正常路径
assertEquals("/topic/sys/test/device/thing/service/property/set",
IotHttpTopicUtils.buildTopicPath("/sys/test/device/thing/service/property/set"));
// 测试空路径
assertNull(IotHttpTopicUtils.buildTopicPath(null));
assertNull(IotHttpTopicUtils.buildTopicPath(""));
}
@Test
void testBuildPropertySetPath() {
String result = IotHttpTopicUtils.buildPropertySetPath("testProduct", "testDevice");
assertEquals("/topic/sys/testProduct/testDevice/thing/service/property/set", result);
// 测试无效参数
assertNull(IotHttpTopicUtils.buildPropertySetPath(null, "testDevice"));
assertNull(IotHttpTopicUtils.buildPropertySetPath("testProduct", null));
assertNull(IotHttpTopicUtils.buildPropertySetPath("", "testDevice"));
assertNull(IotHttpTopicUtils.buildPropertySetPath("testProduct", ""));
}
@Test
void testBuildPropertyGetPath() {
String result = IotHttpTopicUtils.buildPropertyGetPath("testProduct", "testDevice");
assertEquals("/topic/sys/testProduct/testDevice/thing/service/property/get", result);
}
@Test
void testBuildPropertyPostPath() {
String result = IotHttpTopicUtils.buildPropertyPostPath("testProduct", "testDevice");
assertEquals("/topic/sys/testProduct/testDevice/thing/event/property/post", result);
}
@Test
void testBuildEventPostPath() {
String result = IotHttpTopicUtils.buildEventPostPath("testProduct", "testDevice", "alarm");
assertEquals("/topic/sys/testProduct/testDevice/thing/event/alarm/post", result);
// 测试无效参数
assertNull(IotHttpTopicUtils.buildEventPostPath(null, "testDevice", "alarm"));
assertNull(IotHttpTopicUtils.buildEventPostPath("testProduct", null, "alarm"));
assertNull(IotHttpTopicUtils.buildEventPostPath("testProduct", "testDevice", null));
}
@Test
void testBuildServiceInvokePath() {
String result = IotHttpTopicUtils.buildServiceInvokePath("testProduct", "testDevice", "reboot");
assertEquals("/topic/sys/testProduct/testDevice/thing/service/reboot", result);
// 测试无效参数
assertNull(IotHttpTopicUtils.buildServiceInvokePath(null, "testDevice", "reboot"));
assertNull(IotHttpTopicUtils.buildServiceInvokePath("testProduct", null, "reboot"));
assertNull(IotHttpTopicUtils.buildServiceInvokePath("testProduct", "testDevice", null));
}
@Test
void testBuildCustomTopicPath() {
String result = IotHttpTopicUtils.buildCustomTopicPath("testProduct", "testDevice", "user/get");
assertEquals("/topic/testProduct/testDevice/user/get", result);
// 测试无效参数
assertNull(IotHttpTopicUtils.buildCustomTopicPath(null, "testDevice", "user/get"));
assertNull(IotHttpTopicUtils.buildCustomTopicPath("testProduct", null, "user/get"));
assertNull(IotHttpTopicUtils.buildCustomTopicPath("testProduct", "testDevice", null));
}
@Test
void testExtractActualTopic() {
// 测试正常提取
String actualTopic = IotHttpTopicUtils
.extractActualTopic("/topic/sys/testProduct/testDevice/thing/service/property/set");
assertEquals("/sys/testProduct/testDevice/thing/service/property/set", actualTopic);
// 测试无效路径
assertNull(IotHttpTopicUtils.extractActualTopic("/auth"));
assertNull(IotHttpTopicUtils.extractActualTopic("/unknown/path"));
assertNull(IotHttpTopicUtils.extractActualTopic(null));
assertNull(IotHttpTopicUtils.extractActualTopic(""));
}
@Test
void testParseProductKeyFromTopic() {
// 测试系统主题
assertEquals("testProduct",
IotHttpTopicUtils.parseProductKeyFromTopic("/sys/testProduct/testDevice/thing/service/property/set"));
// 测试自定义主题
assertEquals("testProduct", IotHttpTopicUtils.parseProductKeyFromTopic("/testProduct/testDevice/user/get"));
// 测试无效主题
assertNull(IotHttpTopicUtils.parseProductKeyFromTopic("/sys"));
assertNull(IotHttpTopicUtils.parseProductKeyFromTopic("/single"));
assertNull(IotHttpTopicUtils.parseProductKeyFromTopic(""));
assertNull(IotHttpTopicUtils.parseProductKeyFromTopic(null));
}
@Test
void testParseDeviceNameFromTopic() {
// 测试系统主题
assertEquals("testDevice",
IotHttpTopicUtils.parseDeviceNameFromTopic("/sys/testProduct/testDevice/thing/service/property/set"));
// 测试自定义主题
assertEquals("testDevice", IotHttpTopicUtils.parseDeviceNameFromTopic("/testProduct/testDevice/user/get"));
// 测试无效主题
assertNull(IotHttpTopicUtils.parseDeviceNameFromTopic("/sys/testProduct"));
assertNull(IotHttpTopicUtils.parseDeviceNameFromTopic("/testProduct"));
assertNull(IotHttpTopicUtils.parseDeviceNameFromTopic(""));
assertNull(IotHttpTopicUtils.parseDeviceNameFromTopic(null));
}
@Test
void testIsAuthPath() {
assertTrue(IotHttpTopicUtils.isAuthPath("/auth"));
assertFalse(IotHttpTopicUtils.isAuthPath("/topic/test"));
assertFalse(IotHttpTopicUtils.isAuthPath("/unknown"));
assertFalse(IotHttpTopicUtils.isAuthPath(null));
assertFalse(IotHttpTopicUtils.isAuthPath(""));
}
@Test
void testIsTopicPath() {
assertTrue(IotHttpTopicUtils.isTopicPath("/topic/sys/test/device/property"));
assertTrue(IotHttpTopicUtils.isTopicPath("/topic/test"));
assertFalse(IotHttpTopicUtils.isTopicPath("/auth"));
assertFalse(IotHttpTopicUtils.isTopicPath("/unknown"));
assertFalse(IotHttpTopicUtils.isTopicPath(null));
assertFalse(IotHttpTopicUtils.isTopicPath(""));
}
@Test
void testIsValidHttpPath() {
assertTrue(IotHttpTopicUtils.isValidHttpPath("/auth"));
assertTrue(IotHttpTopicUtils.isValidHttpPath("/topic/test"));
assertFalse(IotHttpTopicUtils.isValidHttpPath("/unknown"));
assertFalse(IotHttpTopicUtils.isValidHttpPath(null));
assertFalse(IotHttpTopicUtils.isValidHttpPath(""));
}
@Test
void testIsSystemTopic() {
assertTrue(IotHttpTopicUtils.isSystemTopic("/sys/testProduct/testDevice/thing/service/property/set"));
assertFalse(IotHttpTopicUtils.isSystemTopic("/testProduct/testDevice/user/get"));
assertFalse(IotHttpTopicUtils.isSystemTopic("/unknown"));
assertFalse(IotHttpTopicUtils.isSystemTopic(null));
assertFalse(IotHttpTopicUtils.isSystemTopic(""));
}
@Test
void testBuildReplyPath() {
// 测试系统主题响应路径
String requestPath = "/topic/sys/testProduct/testDevice/thing/service/property/set";
String replyPath = IotHttpTopicUtils.buildReplyPath(requestPath);
assertEquals("/topic/sys/testProduct/testDevice/thing/service/property/set_reply", replyPath);
// 测试非系统主题
String customPath = "/topic/testProduct/testDevice/user/get";
assertNull(IotHttpTopicUtils.buildReplyPath(customPath));
// 测试无效路径
assertNull(IotHttpTopicUtils.buildReplyPath("/auth"));
assertNull(IotHttpTopicUtils.buildReplyPath("/unknown"));
assertNull(IotHttpTopicUtils.buildReplyPath(null));
}
}

View File

@@ -1,81 +0,0 @@
package cn.iocoder.yudao.module.iot.protocol.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* {@link IotTopicUtils} 单元测试
*
* @author haohao
*/
class IotTopicUtilsTest {
@Test
void testBuildPropertySetTopic() {
String topic = IotTopicUtils.buildPropertySetTopic("testProduct", "testDevice");
assertEquals("/sys/testProduct/testDevice/thing/service/property/set", topic);
}
@Test
void testBuildPropertyGetTopic() {
String topic = IotTopicUtils.buildPropertyGetTopic("testProduct", "testDevice");
assertEquals("/sys/testProduct/testDevice/thing/service/property/get", topic);
}
@Test
void testBuildEventPostTopic() {
String topic = IotTopicUtils.buildEventPostTopic("testProduct", "testDevice", "temperature");
assertEquals("/sys/testProduct/testDevice/thing/event/temperature/post", topic);
}
@Test
void testGetReplyTopic() {
String requestTopic = "/sys/testProduct/testDevice/thing/service/property/set";
String replyTopic = IotTopicUtils.getReplyTopic(requestTopic);
assertEquals("/sys/testProduct/testDevice/thing/service/property/set_reply", replyTopic);
}
@Test
void testParseProductKeyFromTopic() {
String topic = "/sys/testProduct/testDevice/thing/service/property/set";
String productKey = IotTopicUtils.parseProductKeyFromTopic(topic);
assertEquals("testProduct", productKey);
}
@Test
void testParseDeviceNameFromTopic() {
String topic = "/sys/testProduct/testDevice/thing/service/property/set";
String deviceName = IotTopicUtils.parseDeviceNameFromTopic(topic);
assertEquals("testDevice", deviceName);
}
@Test
void testParseMethodFromTopic() {
// 测试属性设置
String topic1 = "/sys/testProduct/testDevice/thing/service/property/set";
String method1 = IotTopicUtils.parseMethodFromTopic(topic1);
assertEquals("property.set", method1);
// 测试事件上报
String topic2 = "/sys/testProduct/testDevice/thing/event/temperature/post";
String method2 = IotTopicUtils.parseMethodFromTopic(topic2);
assertEquals("event.temperature.post", method2);
// 测试无效主题
String method3 = IotTopicUtils.parseMethodFromTopic("/invalid/topic");
assertNull(method3);
}
@Test
void testParseInvalidTopic() {
// 测试空主题
assertNull(IotTopicUtils.parseProductKeyFromTopic(""));
assertNull(IotTopicUtils.parseProductKeyFromTopic(null));
// 测试格式错误的主题
assertNull(IotTopicUtils.parseProductKeyFromTopic("/invalid"));
assertNull(IotTopicUtils.parseDeviceNameFromTopic("/sys/product"));
}
}