Commit fbb664026d12065ffa31456daab056bcafcb942d

Authored by haohao
1 parent ab4b148d

feat:【IOT】新增 HTTP 协议支持及相关解析器,完善协议转换器功能

Showing 21 changed files with 2489 additions and 11 deletions
yudao-module-iot/yudao-module-iot-protocol/pom.xml
... ... @@ -53,6 +53,18 @@
53 53 <artifactId>vertx-web</artifactId>
54 54 <scope>provided</scope>
55 55 </dependency>
  56 +
  57 + <!-- 测试相关 -->
  58 + <dependency>
  59 + <groupId>org.junit.jupiter</groupId>
  60 + <artifactId>junit-jupiter</artifactId>
  61 + <scope>test</scope>
  62 + </dependency>
  63 + <dependency>
  64 + <groupId>org.springframework.boot</groupId>
  65 + <artifactId>spring-boot-starter-test</artifactId>
  66 + <scope>test</scope>
  67 + </dependency>
56 68 </dependencies>
57 69  
58 70 </project>
59 71 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/config/IotProtocolAutoConfiguration.java
1 1 package cn.iocoder.yudao.module.iot.protocol.config;
2 2  
  3 +import cn.iocoder.yudao.module.iot.protocol.convert.IotProtocolConverter;
  4 +import cn.iocoder.yudao.module.iot.protocol.convert.impl.DefaultIotProtocolConverter;
  5 +import cn.iocoder.yudao.module.iot.protocol.enums.IotProtocolTypeEnum;
3 6 import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
4 7 import cn.iocoder.yudao.module.iot.protocol.message.impl.IotAlinkMessageParser;
  8 +import cn.iocoder.yudao.module.iot.protocol.message.impl.IotHttpMessageParser;
  9 +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
5 10 import org.springframework.context.annotation.Bean;
6 11 import org.springframework.context.annotation.Configuration;
7 12  
... ... @@ -14,12 +19,57 @@ import org.springframework.context.annotation.Configuration;
14 19 public class IotProtocolAutoConfiguration {
15 20  
16 21 /**
  22 + * Bean 名称常量
  23 + */
  24 + public static final String IOT_ALINK_MESSAGE_PARSER_BEAN_NAME = "iotAlinkMessageParser";
  25 + public static final String IOT_HTTP_MESSAGE_PARSER_BEAN_NAME = "iotHttpMessageParser";
  26 +
  27 + /**
17 28 * 注册 Alink 协议消息解析器
18 29 *
19 30 * @return Alink 协议消息解析器
20 31 */
21 32 @Bean
  33 + @ConditionalOnMissingBean(name = IOT_ALINK_MESSAGE_PARSER_BEAN_NAME)
22 34 public IotMessageParser iotAlinkMessageParser() {
23 35 return new IotAlinkMessageParser();
24 36 }
25   -}
26 37 \ No newline at end of file
  38 +
  39 + /**
  40 + * 注册 HTTP 协议消息解析器
  41 + *
  42 + * @return HTTP 协议消息解析器
  43 + */
  44 + @Bean
  45 + @ConditionalOnMissingBean(name = IOT_HTTP_MESSAGE_PARSER_BEAN_NAME)
  46 + public IotMessageParser iotHttpMessageParser() {
  47 + return new IotHttpMessageParser();
  48 + }
  49 +
  50 + /**
  51 + * 注册默认协议转换器
  52 + * <p>
  53 + * 如果用户没有自定义协议转换器,则使用默认实现
  54 + * 默认会注册 Alink 和 HTTP 协议解析器
  55 + *
  56 + * @param iotAlinkMessageParser Alink 协议解析器
  57 + * @param iotHttpMessageParser HTTP 协议解析器
  58 + * @return 默认协议转换器
  59 + */
  60 + @Bean
  61 + @ConditionalOnMissingBean
  62 + public IotProtocolConverter iotProtocolConverter(IotMessageParser iotAlinkMessageParser,
  63 + IotMessageParser iotHttpMessageParser) {
  64 + DefaultIotProtocolConverter converter = new DefaultIotProtocolConverter();
  65 +
  66 + // 注册 HTTP 协议解析器
  67 + converter.registerParser(IotProtocolTypeEnum.HTTP.getCode(), iotHttpMessageParser);
  68 +
  69 + // 注意:Alink 协议解析器已经在 DefaultIotProtocolConverter 构造函数中注册
  70 + // 如果需要使用自定义的 Alink 解析器实例,可以重新注册
  71 + // converter.registerParser(IotProtocolTypeEnum.ALINK.getCode(),
  72 + // iotAlinkMessageParser);
  73 +
  74 + return converter;
  75 + }
  76 +}
27 77 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotHttpConstants.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.constants;
  2 +
  3 +/**
  4 + * IoT HTTP 协议常量类
  5 + * <p>
  6 + * 用于统一管理 HTTP 协议中的常量,包括路径、字段名、默认值等
  7 + *
  8 + * @author haohao
  9 + */
  10 +public class IotHttpConstants {
  11 +
  12 + /**
  13 + * 路径常量
  14 + */
  15 + public static class Path {
  16 + /**
  17 + * 认证路径
  18 + */
  19 + public static final String AUTH = "/auth";
  20 +
  21 + /**
  22 + * 主题路径前缀
  23 + */
  24 + public static final String TOPIC_PREFIX = "/topic";
  25 + }
  26 +
  27 + /**
  28 + * 认证字段常量
  29 + */
  30 + public static class AuthField {
  31 + /**
  32 + * 产品Key
  33 + */
  34 + public static final String PRODUCT_KEY = "productKey";
  35 +
  36 + /**
  37 + * 设备名称
  38 + */
  39 + public static final String DEVICE_NAME = "deviceName";
  40 +
  41 + /**
  42 + * 客户端ID
  43 + */
  44 + public static final String CLIENT_ID = "clientId";
  45 +
  46 + /**
  47 + * 时间戳
  48 + */
  49 + public static final String TIMESTAMP = "timestamp";
  50 +
  51 + /**
  52 + * 签名
  53 + */
  54 + public static final String SIGN = "sign";
  55 +
  56 + /**
  57 + * 签名方法
  58 + */
  59 + public static final String SIGN_METHOD = "signmethod";
  60 +
  61 + /**
  62 + * 版本
  63 + */
  64 + public static final String VERSION = "version";
  65 + }
  66 +
  67 + /**
  68 + * 消息字段常量
  69 + */
  70 + public static class MessageField {
  71 + /**
  72 + * 消息ID
  73 + */
  74 + public static final String ID = "id";
  75 +
  76 + /**
  77 + * 方法名
  78 + */
  79 + public static final String METHOD = "method";
  80 +
  81 + /**
  82 + * 版本
  83 + */
  84 + public static final String VERSION = "version";
  85 +
  86 + /**
  87 + * 参数
  88 + */
  89 + public static final String PARAMS = "params";
  90 +
  91 + /**
  92 + * 数据
  93 + */
  94 + public static final String DATA = "data";
  95 + }
  96 +
  97 + /**
  98 + * 响应字段常量
  99 + */
  100 + public static class ResponseField {
  101 + /**
  102 + * 状态码
  103 + */
  104 + public static final String CODE = "code";
  105 +
  106 + /**
  107 + * 消息
  108 + */
  109 + public static final String MESSAGE = "message";
  110 +
  111 + /**
  112 + * 信息
  113 + */
  114 + public static final String INFO = "info";
  115 +
  116 + /**
  117 + * 令牌
  118 + */
  119 + public static final String TOKEN = "token";
  120 +
  121 + /**
  122 + * 消息ID
  123 + */
  124 + public static final String MESSAGE_ID = "messageId";
  125 + }
  126 +
  127 + /**
  128 + * 默认值常量
  129 + */
  130 + public static class DefaultValue {
  131 + /**
  132 + * 默认签名方法
  133 + */
  134 + public static final String SIGN_METHOD = "hmacmd5";
  135 +
  136 + /**
  137 + * 默认版本
  138 + */
  139 + public static final String VERSION = "default";
  140 +
  141 + /**
  142 + * 默认消息版本
  143 + */
  144 + public static final String MESSAGE_VERSION = "1.0";
  145 +
  146 + /**
  147 + * 未知方法名
  148 + */
  149 + public static final String UNKNOWN_METHOD = "unknown";
  150 + }
  151 +
  152 + /**
  153 + * 方法名常量
  154 + */
  155 + public static class Method {
  156 + /**
  157 + * 设备认证
  158 + */
  159 + public static final String DEVICE_AUTH = "device.auth";
  160 +
  161 + /**
  162 + * 自定义消息
  163 + */
  164 + public static final String CUSTOM_MESSAGE = "custom.message";
  165 + }
  166 +}
0 167 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotLogConstants.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.constants;
  2 +
  3 +/**
  4 + * IoT 协议日志消息常量类
  5 + * <p>
  6 + * 用于统一管理协议模块中的日志消息常量
  7 + *
  8 + * @author haohao
  9 + */
  10 +public class IotLogConstants {
  11 +
  12 + /**
  13 + * HTTP 协议日志消息
  14 + */
  15 + public static class Http {
  16 + /**
  17 + * 收到空消息内容
  18 + */
  19 + public static final String RECEIVED_EMPTY_MESSAGE = "[HTTP] 收到空消息内容, topic={}";
  20 +
  21 + /**
  22 + * 不支持的路径格式
  23 + */
  24 + public static final String UNSUPPORTED_PATH_FORMAT = "[HTTP] 不支持的路径格式, topic={}";
  25 +
  26 + /**
  27 + * 解析消息失败
  28 + */
  29 + public static final String PARSE_MESSAGE_FAILED = "[HTTP] 解析消息失败, topic={}";
  30 +
  31 + /**
  32 + * 认证消息非JSON格式
  33 + */
  34 + public static final String AUTH_MESSAGE_NOT_JSON = "[HTTP] 认证消息非JSON格式, message={}";
  35 +
  36 + /**
  37 + * 认证消息缺少必需字段
  38 + */
  39 + public static final String AUTH_MESSAGE_MISSING_REQUIRED_FIELDS = "[HTTP] 认证消息缺少必需字段, message={}";
  40 +
  41 + /**
  42 + * 格式化响应失败
  43 + */
  44 + public static final String FORMAT_RESPONSE_FAILED = "[HTTP] 格式化响应失败";
  45 + }
  46 +
  47 + /**
  48 + * 协议转换器日志消息
  49 + */
  50 + public static class Converter {
  51 + /**
  52 + * 注册协议解析器
  53 + */
  54 + public static final String REGISTER_PARSER = "[协议转换器] 注册协议解析器: protocol={}, parser={}";
  55 +
  56 + /**
  57 + * 移除协议解析器
  58 + */
  59 + public static final String REMOVE_PARSER = "[协议转换器] 移除协议解析器: protocol={}";
  60 +
  61 + /**
  62 + * 不支持的协议类型
  63 + */
  64 + public static final String UNSUPPORTED_PROTOCOL = "[协议转换器] 不支持的协议类型: protocol={}";
  65 +
  66 + /**
  67 + * 转换消息失败
  68 + */
  69 + public static final String CONVERT_MESSAGE_FAILED = "[协议转换器] 转换消息失败: protocol={}, topic={}";
  70 +
  71 + /**
  72 + * 格式化响应失败
  73 + */
  74 + public static final String FORMAT_RESPONSE_FAILED = "[协议转换器] 格式化响应失败: protocol={}";
  75 +
  76 + /**
  77 + * 自动选择协议
  78 + */
  79 + public static final String AUTO_SELECT_PROTOCOL = "[协议转换器] 自动选择协议: protocol={}, topic={}";
  80 +
  81 + /**
  82 + * 协议解析失败,尝试下一个
  83 + */
  84 + public static final String PROTOCOL_PARSE_FAILED_TRY_NEXT = "[协议转换器] 协议解析失败,尝试下一个: protocol={}, topic={}";
  85 +
  86 + /**
  87 + * 无法自动识别协议
  88 + */
  89 + public static final String CANNOT_AUTO_RECOGNIZE_PROTOCOL = "[协议转换器] 无法自动识别协议: topic={}";
  90 + }
  91 +}
0 92 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotTopicConstants.java
... ... @@ -69,4 +69,89 @@ public class IotTopicConstants {
69 69 */
70 70 public static final String REPLY_SUFFIX = "_reply";
71 71  
72   -}
73 72 \ No newline at end of file
  73 + /**
  74 + * 方法名前缀常量
  75 + */
  76 + public static class MethodPrefix {
  77 + /**
  78 + * 物模型服务前缀
  79 + */
  80 + public static final String THING_SERVICE = "thing.service.";
  81 +
  82 + /**
  83 + * 物模型事件前缀
  84 + */
  85 + public static final String THING_EVENT = "thing.event.";
  86 + }
  87 +
  88 + /**
  89 + * 完整方法名常量
  90 + */
  91 + public static class Method {
  92 + /**
  93 + * 属性设置方法
  94 + */
  95 + public static final String PROPERTY_SET = "thing.service.property.set";
  96 +
  97 + /**
  98 + * 属性获取方法
  99 + */
  100 + public static final String PROPERTY_GET = "thing.service.property.get";
  101 +
  102 + /**
  103 + * 属性上报方法
  104 + */
  105 + public static final String PROPERTY_POST = "thing.event.property.post";
  106 +
  107 + /**
  108 + * 配置设置方法
  109 + */
  110 + public static final String CONFIG_SET = "thing.service.config.set";
  111 +
  112 + /**
  113 + * OTA升级方法
  114 + */
  115 + public static final String OTA_UPGRADE = "thing.service.ota.upgrade";
  116 +
  117 + /**
  118 + * 设备上线方法
  119 + */
  120 + public static final String DEVICE_ONLINE = "device.online";
  121 +
  122 + /**
  123 + * 设备下线方法
  124 + */
  125 + public static final String DEVICE_OFFLINE = "device.offline";
  126 +
  127 + /**
  128 + * 心跳方法
  129 + */
  130 + public static final String HEARTBEAT = "heartbeat";
  131 + }
  132 +
  133 + /**
  134 + * 主题关键字常量
  135 + */
  136 + public static class Keyword {
  137 + /**
  138 + * 事件关键字
  139 + */
  140 + public static final String EVENT = "event";
  141 +
  142 + /**
  143 + * 服务关键字
  144 + */
  145 + public static final String SERVICE = "service";
  146 +
  147 + /**
  148 + * 属性关键字
  149 + */
  150 + public static final String PROPERTY = "property";
  151 +
  152 + /**
  153 + * 上报关键字
  154 + */
  155 + public static final String POST = "post";
  156 + }
  157 +
  158 +}
74 159 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/IotProtocolConverter.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.convert;
  2 +
  3 +import cn.iocoder.yudao.module.iot.protocol.message.IotAlinkMessage;
  4 +import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
  5 +
  6 +/**
  7 + * IoT 协议转换器接口
  8 + * <p>
  9 + * 用于在不同协议之间进行转换
  10 + *
  11 + * @author haohao
  12 + */
  13 +public interface IotProtocolConverter {
  14 +
  15 + /**
  16 + * 将字节数组转换为标准消息
  17 + *
  18 + * @param topic 主题
  19 + * @param payload 消息负载
  20 + * @param protocol 协议类型
  21 + * @return 标准消息对象,转换失败返回 null
  22 + */
  23 + IotAlinkMessage convertToStandardMessage(String topic, byte[] payload, String protocol);
  24 +
  25 + /**
  26 + * 将标准响应转换为字节数组
  27 + *
  28 + * @param response 标准响应
  29 + * @param protocol 协议类型
  30 + * @return 字节数组,转换失败返回空数组
  31 + */
  32 + byte[] convertFromStandardResponse(IotStandardResponse response, String protocol);
  33 +
  34 + /**
  35 + * 检查是否支持指定协议
  36 + *
  37 + * @param protocol 协议类型
  38 + * @return 如果支持返回 true,否则返回 false
  39 + */
  40 + boolean supportsProtocol(String protocol);
  41 +
  42 + /**
  43 + * 获取支持的协议类型列表
  44 + *
  45 + * @return 协议类型数组
  46 + */
  47 + String[] getSupportedProtocols();
  48 +}
0 49 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/impl/DefaultIotProtocolConverter.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.convert.impl;
  2 +
  3 +import cn.iocoder.yudao.module.iot.protocol.constants.IotLogConstants;
  4 +import cn.iocoder.yudao.module.iot.protocol.convert.IotProtocolConverter;
  5 +import cn.iocoder.yudao.module.iot.protocol.enums.IotProtocolTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.protocol.message.IotAlinkMessage;
  7 +import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
  8 +import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
  9 +import cn.iocoder.yudao.module.iot.protocol.message.impl.IotAlinkMessageParser;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +
  12 +import java.util.HashMap;
  13 +import java.util.Map;
  14 +import java.util.Set;
  15 +
  16 +/**
  17 + * 默认 IoT 协议转换器实现
  18 + * <p>
  19 + * 支持多种协议的转换,可以通过注册不同的消息解析器来扩展支持的协议
  20 + *
  21 + * @author haohao
  22 + */
  23 +@Slf4j
  24 +public class DefaultIotProtocolConverter implements IotProtocolConverter {
  25 +
  26 + /**
  27 + * 消息解析器映射
  28 + * Key: 协议类型,Value: 消息解析器
  29 + */
  30 + private final Map<String, IotMessageParser> parsers = new HashMap<>();
  31 +
  32 + /**
  33 + * 构造函数,初始化默认支持的协议
  34 + */
  35 + public DefaultIotProtocolConverter() {
  36 + // 注册 Alink 协议解析器
  37 + registerParser(IotProtocolTypeEnum.ALINK.getCode(), new IotAlinkMessageParser());
  38 + }
  39 +
  40 + /**
  41 + * 注册消息解析器
  42 + *
  43 + * @param protocol 协议类型
  44 + * @param parser 消息解析器
  45 + */
  46 + public void registerParser(String protocol, IotMessageParser parser) {
  47 + parsers.put(protocol, parser);
  48 + log.info(IotLogConstants.Converter.REGISTER_PARSER, protocol, parser.getClass().getSimpleName());
  49 + }
  50 +
  51 + /**
  52 + * 移除消息解析器
  53 + *
  54 + * @param protocol 协议类型
  55 + */
  56 + public void removeParser(String protocol) {
  57 + parsers.remove(protocol);
  58 + log.info(IotLogConstants.Converter.REMOVE_PARSER, protocol);
  59 + }
  60 +
  61 + @Override
  62 + public IotAlinkMessage convertToStandardMessage(String topic, byte[] payload, String protocol) {
  63 + IotMessageParser parser = parsers.get(protocol);
  64 + if (parser == null) {
  65 + log.warn(IotLogConstants.Converter.UNSUPPORTED_PROTOCOL, protocol);
  66 + return null;
  67 + }
  68 +
  69 + try {
  70 + return parser.parse(topic, payload);
  71 + } catch (Exception e) {
  72 + log.error(IotLogConstants.Converter.CONVERT_MESSAGE_FAILED, protocol, topic, e);
  73 + return null;
  74 + }
  75 + }
  76 +
  77 + @Override
  78 + public byte[] convertFromStandardResponse(IotStandardResponse response, String protocol) {
  79 + IotMessageParser parser = parsers.get(protocol);
  80 + if (parser == null) {
  81 + log.warn(IotLogConstants.Converter.UNSUPPORTED_PROTOCOL, protocol);
  82 + return new byte[0];
  83 + }
  84 +
  85 + try {
  86 + return parser.formatResponse(response);
  87 + } catch (Exception e) {
  88 + log.error(IotLogConstants.Converter.FORMAT_RESPONSE_FAILED, protocol, e);
  89 + return new byte[0];
  90 + }
  91 + }
  92 +
  93 + @Override
  94 + public boolean supportsProtocol(String protocol) {
  95 + return parsers.containsKey(protocol);
  96 + }
  97 +
  98 + @Override
  99 + public String[] getSupportedProtocols() {
  100 + Set<String> protocols = parsers.keySet();
  101 + return protocols.toArray(new String[0]);
  102 + }
  103 +
  104 + /**
  105 + * 根据主题自动选择合适的协议解析器
  106 + *
  107 + * @param topic 主题
  108 + * @param payload 消息负载
  109 + * @return 解析后的标准消息,如果无法解析返回 null
  110 + */
  111 + public IotAlinkMessage autoConvert(String topic, byte[] payload) {
  112 + // 遍历所有解析器,找到能处理该主题的解析器
  113 + for (Map.Entry<String, IotMessageParser> entry : parsers.entrySet()) {
  114 + IotMessageParser parser = entry.getValue();
  115 + if (parser.canHandle(topic)) {
  116 + try {
  117 + IotAlinkMessage message = parser.parse(topic, payload);
  118 + if (message != null) {
  119 + log.debug(IotLogConstants.Converter.AUTO_SELECT_PROTOCOL, entry.getKey(), topic);
  120 + return message;
  121 + }
  122 + } catch (Exception e) {
  123 + log.debug(IotLogConstants.Converter.PROTOCOL_PARSE_FAILED_TRY_NEXT, entry.getKey(), topic);
  124 + }
  125 + }
  126 + }
  127 +
  128 + log.warn(IotLogConstants.Converter.CANNOT_AUTO_RECOGNIZE_PROTOCOL, topic);
  129 + return null;
  130 + }
  131 +}
0 132 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/enums/IotMessageDirectionEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.enums;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +
  6 +/**
  7 + * IoT 消息方向枚举
  8 + *
  9 + * @author haohao
  10 + */
  11 +@Getter
  12 +@AllArgsConstructor
  13 +public enum IotMessageDirectionEnum {
  14 +
  15 + /**
  16 + * 上行消息(设备到平台)
  17 + */
  18 + UPSTREAM("upstream", "上行"),
  19 +
  20 + /**
  21 + * 下行消息(平台到设备)
  22 + */
  23 + DOWNSTREAM("downstream", "下行");
  24 +
  25 + /**
  26 + * 方向编码
  27 + */
  28 + private final String code;
  29 +
  30 + /**
  31 + * 方向名称
  32 + */
  33 + private final String name;
  34 +
  35 + /**
  36 + * 根据编码获取消息方向
  37 + *
  38 + * @param code 方向编码
  39 + * @return 消息方向枚举,如果未找到返回 null
  40 + */
  41 + public static IotMessageDirectionEnum getByCode(String code) {
  42 + for (IotMessageDirectionEnum direction : values()) {
  43 + if (direction.getCode().equals(code)) {
  44 + return direction;
  45 + }
  46 + }
  47 + return null;
  48 + }
  49 +}
0 50 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/enums/IotMessageTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.enums;
  2 +
  3 +import cn.iocoder.yudao.module.iot.protocol.constants.IotTopicConstants;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +/**
  8 + * IoT 消息类型枚举
  9 + *
  10 + * @author haohao
  11 + */
  12 +@Getter
  13 +@AllArgsConstructor
  14 +public enum IotMessageTypeEnum {
  15 +
  16 + /**
  17 + * 属性上报
  18 + */
  19 + PROPERTY_POST("property.post", "属性上报"),
  20 +
  21 + /**
  22 + * 属性设置
  23 + */
  24 + PROPERTY_SET("property.set", "属性设置"),
  25 +
  26 + /**
  27 + * 属性获取
  28 + */
  29 + PROPERTY_GET("property.get", "属性获取"),
  30 +
  31 + /**
  32 + * 事件上报
  33 + */
  34 + EVENT_POST("event.post", "事件上报"),
  35 +
  36 + /**
  37 + * 服务调用
  38 + */
  39 + SERVICE_INVOKE("service.invoke", "服务调用"),
  40 +
  41 + /**
  42 + * 配置设置
  43 + */
  44 + CONFIG_SET("config.set", "配置设置"),
  45 +
  46 + /**
  47 + * OTA 升级
  48 + */
  49 + OTA_UPGRADE("ota.upgrade", "OTA升级"),
  50 +
  51 + /**
  52 + * 设备上线
  53 + */
  54 + DEVICE_ONLINE("device.online", "设备上线"),
  55 +
  56 + /**
  57 + * 设备下线
  58 + */
  59 + DEVICE_OFFLINE("device.offline", "设备下线"),
  60 +
  61 + /**
  62 + * 心跳
  63 + */
  64 + HEARTBEAT("heartbeat", "心跳");
  65 +
  66 + /**
  67 + * 消息类型编码
  68 + */
  69 + private final String code;
  70 +
  71 + /**
  72 + * 消息类型名称
  73 + */
  74 + private final String name;
  75 +
  76 + /**
  77 + * 根据编码获取消息类型
  78 + *
  79 + * @param code 消息类型编码
  80 + * @return 消息类型枚举,如果未找到返回 null
  81 + */
  82 + public static IotMessageTypeEnum getByCode(String code) {
  83 + for (IotMessageTypeEnum type : values()) {
  84 + if (type.getCode().equals(code)) {
  85 + return type;
  86 + }
  87 + }
  88 + return null;
  89 + }
  90 +
  91 + /**
  92 + * 根据方法名获取消息类型
  93 + *
  94 + * @param method 方法名
  95 + * @return 消息类型枚举,如果未找到返回 null
  96 + */
  97 + public static IotMessageTypeEnum getByMethod(String method) {
  98 + if (method == null) {
  99 + return null;
  100 + }
  101 +
  102 + // 处理 thing.service.xxx 格式
  103 + if (method.startsWith(IotTopicConstants.MethodPrefix.THING_SERVICE)) {
  104 + String servicePart = method.substring(IotTopicConstants.MethodPrefix.THING_SERVICE.length());
  105 + if ("property.set".equals(servicePart)) {
  106 + return PROPERTY_SET;
  107 + } else if ("property.get".equals(servicePart)) {
  108 + return PROPERTY_GET;
  109 + } else if ("config.set".equals(servicePart)) {
  110 + return CONFIG_SET;
  111 + } else if ("ota.upgrade".equals(servicePart)) {
  112 + return OTA_UPGRADE;
  113 + } else {
  114 + return SERVICE_INVOKE;
  115 + }
  116 + }
  117 +
  118 + // 处理 thing.event.xxx 格式
  119 + if (method.startsWith(IotTopicConstants.MethodPrefix.THING_EVENT)) {
  120 + String eventPart = method.substring(IotTopicConstants.MethodPrefix.THING_EVENT.length());
  121 + if ("property.post".equals(eventPart)) {
  122 + return PROPERTY_POST;
  123 + } else {
  124 + return EVENT_POST;
  125 + }
  126 + }
  127 +
  128 + // 其他类型
  129 + switch (method) {
  130 + case IotTopicConstants.Method.DEVICE_ONLINE:
  131 + return DEVICE_ONLINE;
  132 + case IotTopicConstants.Method.DEVICE_OFFLINE:
  133 + return DEVICE_OFFLINE;
  134 + case IotTopicConstants.Method.HEARTBEAT:
  135 + return HEARTBEAT;
  136 + default:
  137 + return null;
  138 + }
  139 + }
  140 +}
0 141 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/enums/IotProtocolTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.enums;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +
  6 +/**
  7 + * IoT 协议类型枚举
  8 + *
  9 + * @author haohao
  10 + */
  11 +@Getter
  12 +@AllArgsConstructor
  13 +public enum IotProtocolTypeEnum {
  14 +
  15 + /**
  16 + * Alink 协议(阿里云物联网协议)
  17 + */
  18 + ALINK("alink", "Alink 协议"),
  19 +
  20 + /**
  21 + * MQTT 原始协议
  22 + */
  23 + MQTT_RAW("mqtt_raw", "MQTT 原始协议"),
  24 +
  25 + /**
  26 + * HTTP 协议
  27 + */
  28 + HTTP("http", "HTTP 协议"),
  29 +
  30 + /**
  31 + * TCP 协议
  32 + */
  33 + TCP("tcp", "TCP 协议"),
  34 +
  35 + /**
  36 + * UDP 协议
  37 + */
  38 + UDP("udp", "UDP 协议"),
  39 +
  40 + /**
  41 + * 自定义协议
  42 + */
  43 + CUSTOM("custom", "自定义协议");
  44 +
  45 + /**
  46 + * 协议编码
  47 + */
  48 + private final String code;
  49 +
  50 + /**
  51 + * 协议名称
  52 + */
  53 + private final String name;
  54 +
  55 + /**
  56 + * 根据编码获取协议类型
  57 + *
  58 + * @param code 协议编码
  59 + * @return 协议类型枚举,如果未找到返回 null
  60 + */
  61 + public static IotProtocolTypeEnum getByCode(String code) {
  62 + for (IotProtocolTypeEnum type : values()) {
  63 + if (type.getCode().equals(code)) {
  64 + return type;
  65 + }
  66 + }
  67 + return null;
  68 + }
  69 +
  70 + /**
  71 + * 检查是否为有效的协议编码
  72 + *
  73 + * @param code 协议编码
  74 + * @return 如果有效返回 true,否则返回 false
  75 + */
  76 + public static boolean isValidCode(String code) {
  77 + return getByCode(code) != null;
  78 + }
  79 +}
0 80 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/IotAlinkMessage.java
... ... @@ -11,9 +11,9 @@ import java.util.Map;
11 11 * IoT Alink 消息模型
12 12 * <p>
13 13 * 基于阿里云 Alink 协议规范实现的标准消息格式
14   - * @see <a href="https://help.aliyun.com/zh/iot/user-guide/alink-protocol-1">阿里云物联网 —— Alink 协议</a>
15 14 *
16 15 * @author haohao
  16 + * @see <a href="https://help.aliyun.com/zh/iot/user-guide/alink-protocol-1">阿里云物联网 —— Alink 协议</a>
17 17 */
18 18 @Data
19 19 @Builder
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotAlinkMessageParser.java
... ... @@ -40,7 +40,7 @@ public class IotAlinkMessageParser implements IotMessageParser {
40 40 JSONObject json = JSONUtil.parseObj(message);
41 41 String id = json.getStr("id");
42 42 String method = json.getStr("method");
43   -
  43 +
44 44 if (StrUtil.isBlank(method)) {
45 45 // 尝试从 topic 中解析方法
46 46 method = IotTopicUtils.parseMethodFromTopic(topic);
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParser.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.message.impl;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.json.JSONObject;
  5 +import cn.hutool.json.JSONUtil;
  6 +import cn.iocoder.yudao.module.iot.protocol.constants.IotHttpConstants;
  7 +import cn.iocoder.yudao.module.iot.protocol.constants.IotLogConstants;
  8 +import cn.iocoder.yudao.module.iot.protocol.constants.IotTopicConstants;
  9 +import cn.iocoder.yudao.module.iot.protocol.message.IotAlinkMessage;
  10 +import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
  11 +import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +
  14 +import java.nio.charset.StandardCharsets;
  15 +import java.util.HashMap;
  16 +import java.util.Map;
  17 +
  18 +/**
  19 + * IoT HTTP 协议消息解析器实现
  20 + * <p>
  21 + * 参考阿里云IoT平台HTTPS协议标准,支持设备认证和数据上报两种消息类型:
  22 + * <p>
  23 + * 1. 设备认证消息格式:
  24 + *
  25 + * <pre>
  26 + * POST /auth HTTP/1.1
  27 + * Content-Type: application/json
  28 + * {
  29 + * "productKey": "a1AbC***",
  30 + * "deviceName": "device01",
  31 + * "clientId": "device01_001",
  32 + * "timestamp": "1501668289957",
  33 + * "sign": "xxxxx",
  34 + * "signmethod": "hmacsha1",
  35 + * "version": "default"
  36 + * }
  37 + * </pre>
  38 + * <p>
  39 + * 2. 数据上报消息格式:
  40 + *
  41 + * <pre>
  42 + * POST /topic/${topic} HTTP/1.1
  43 + * password: ${token}
  44 + * Content-Type: application/octet-stream
  45 + * ${payload}
  46 + * </pre>
  47 + *
  48 + * @author haohao
  49 + */
  50 +@Slf4j
  51 +public class IotHttpMessageParser implements IotMessageParser {
  52 +
  53 + /**
  54 + * 认证路径
  55 + */
  56 + public static final String AUTH_PATH = IotHttpConstants.Path.AUTH;
  57 +
  58 + /**
  59 + * 主题路径前缀
  60 + */
  61 + public static final String TOPIC_PATH_PREFIX = IotHttpConstants.Path.TOPIC_PREFIX;
  62 +
  63 + @Override
  64 + public IotAlinkMessage parse(String topic, byte[] payload) {
  65 + if (payload == null || payload.length == 0) {
  66 + log.warn(IotLogConstants.Http.RECEIVED_EMPTY_MESSAGE, topic);
  67 + return null;
  68 + }
  69 +
  70 + try {
  71 + String message = new String(payload, StandardCharsets.UTF_8);
  72 +
  73 + // 判断是认证请求还是数据上报
  74 + if (AUTH_PATH.equals(topic)) {
  75 + return parseAuthMessage(message);
  76 + } else if (topic.startsWith(TOPIC_PATH_PREFIX)) {
  77 + return parseDataMessage(topic, message);
  78 + } else {
  79 + log.warn(IotLogConstants.Http.UNSUPPORTED_PATH_FORMAT, topic);
  80 + return null;
  81 + }
  82 +
  83 + } catch (Exception e) {
  84 + log.error(IotLogConstants.Http.PARSE_MESSAGE_FAILED, topic, e);
  85 + return null;
  86 + }
  87 + }
  88 +
  89 + /**
  90 + * 解析设备认证消息
  91 + *
  92 + * @param message 认证消息JSON
  93 + * @return 标准消息格式
  94 + */
  95 + private IotAlinkMessage parseAuthMessage(String message) {
  96 + if (!JSONUtil.isTypeJSON(message)) {
  97 + log.warn(IotLogConstants.Http.AUTH_MESSAGE_NOT_JSON, message);
  98 + return null;
  99 + }
  100 +
  101 + JSONObject json = JSONUtil.parseObj(message);
  102 +
  103 + // 验证必需字段
  104 + String productKey = json.getStr(IotHttpConstants.AuthField.PRODUCT_KEY);
  105 + String deviceName = json.getStr(IotHttpConstants.AuthField.DEVICE_NAME);
  106 + String clientId = json.getStr(IotHttpConstants.AuthField.CLIENT_ID);
  107 + String sign = json.getStr(IotHttpConstants.AuthField.SIGN);
  108 +
  109 + if (StrUtil.hasBlank(productKey, deviceName, clientId, sign)) {
  110 + log.warn(IotLogConstants.Http.AUTH_MESSAGE_MISSING_REQUIRED_FIELDS, message);
  111 + return null;
  112 + }
  113 +
  114 + // 构建认证消息
  115 + Map<String, Object> params = new HashMap<>();
  116 + params.put(IotHttpConstants.AuthField.PRODUCT_KEY, productKey);
  117 + params.put(IotHttpConstants.AuthField.DEVICE_NAME, deviceName);
  118 + params.put(IotHttpConstants.AuthField.CLIENT_ID, clientId);
  119 + params.put(IotHttpConstants.AuthField.TIMESTAMP, json.getStr(IotHttpConstants.AuthField.TIMESTAMP));
  120 + params.put(IotHttpConstants.AuthField.SIGN, sign);
  121 + params.put(IotHttpConstants.AuthField.SIGN_METHOD,
  122 + json.getStr(IotHttpConstants.AuthField.SIGN_METHOD, IotHttpConstants.DefaultValue.SIGN_METHOD));
  123 +
  124 + return IotAlinkMessage.builder()
  125 + .id(generateMessageId())
  126 + .method(IotHttpConstants.Method.DEVICE_AUTH)
  127 + .version(json.getStr(IotHttpConstants.AuthField.VERSION, IotHttpConstants.DefaultValue.VERSION))
  128 + .params(params)
  129 + .build();
  130 + }
  131 +
  132 + /**
  133 + * 解析数据上报消息
  134 + *
  135 + * @param topic 主题路径,格式:/topic/${actualTopic}
  136 + * @param message 消息内容
  137 + * @return 标准消息格式
  138 + */
  139 + private IotAlinkMessage parseDataMessage(String topic, String message) {
  140 + // 提取实际的主题,去掉 /topic 前缀
  141 + String actualTopic = topic.substring(TOPIC_PATH_PREFIX.length()); // 直接移除/topic前缀
  142 +
  143 + // 尝试解析为JSON格式
  144 + if (JSONUtil.isTypeJSON(message)) {
  145 + return parseJsonDataMessage(actualTopic, message);
  146 + } else {
  147 + // 原始数据格式
  148 + return parseRawDataMessage(actualTopic, message);
  149 + }
  150 + }
  151 +
  152 + /**
  153 + * 解析JSON格式的数据消息
  154 + *
  155 + * @param topic 实际主题
  156 + * @param message JSON消息
  157 + * @return 标准消息格式
  158 + */
  159 + private IotAlinkMessage parseJsonDataMessage(String topic, String message) {
  160 + JSONObject json = JSONUtil.parseObj(message);
  161 +
  162 + // 生成消息ID
  163 + String messageId = json.getStr(IotHttpConstants.MessageField.ID);
  164 + if (StrUtil.isBlank(messageId)) {
  165 + messageId = generateMessageId();
  166 + }
  167 +
  168 + // 获取方法名
  169 + String method = json.getStr(IotHttpConstants.MessageField.METHOD);
  170 + if (StrUtil.isBlank(method)) {
  171 + // 根据主题推断方法名
  172 + method = inferMethodFromTopic(topic);
  173 + }
  174 +
  175 + // 获取参数
  176 + Object params = json.get(IotHttpConstants.MessageField.PARAMS);
  177 + Map<String, Object> paramsMap = new HashMap<>();
  178 + if (params instanceof Map) {
  179 + paramsMap.putAll((Map<String, Object>) params);
  180 + } else if (params != null) {
  181 + paramsMap.put(IotHttpConstants.MessageField.DATA, params);
  182 + }
  183 +
  184 + return IotAlinkMessage.builder()
  185 + .id(messageId)
  186 + .method(method)
  187 + .version(json.getStr(IotHttpConstants.MessageField.VERSION,
  188 + IotHttpConstants.DefaultValue.MESSAGE_VERSION))
  189 + .params(paramsMap)
  190 + .build();
  191 + }
  192 +
  193 + /**
  194 + * 解析原始数据消息
  195 + *
  196 + * @param topic 实际主题
  197 + * @param message 原始消息
  198 + * @return 标准消息格式
  199 + */
  200 + private IotAlinkMessage parseRawDataMessage(String topic, String message) {
  201 + Map<String, Object> params = new HashMap<>();
  202 + params.put(IotHttpConstants.MessageField.DATA, message);
  203 +
  204 + return IotAlinkMessage.builder()
  205 + .id(generateMessageId())
  206 + .method(inferMethodFromTopic(topic))
  207 + .version(IotHttpConstants.DefaultValue.MESSAGE_VERSION)
  208 + .params(params)
  209 + .build();
  210 + }
  211 +
  212 + /**
  213 + * 根据主题推断方法名
  214 + *
  215 + * @param topic 主题
  216 + * @return 方法名
  217 + */
  218 + private String inferMethodFromTopic(String topic) {
  219 + if (StrUtil.isBlank(topic)) {
  220 + return IotHttpConstants.DefaultValue.UNKNOWN_METHOD;
  221 + }
  222 +
  223 + // 标准系统主题解析
  224 + if (topic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
  225 + if (topic.contains(IotTopicConstants.PROPERTY_SET_TOPIC)) {
  226 + return IotTopicConstants.Method.PROPERTY_SET;
  227 + } else if (topic.contains(IotTopicConstants.PROPERTY_GET_TOPIC)) {
  228 + return IotTopicConstants.Method.PROPERTY_GET;
  229 + } else if (topic.contains(IotTopicConstants.PROPERTY_POST_TOPIC)) {
  230 + return IotTopicConstants.Method.PROPERTY_POST;
  231 + } else if (topic.contains(IotTopicConstants.EVENT_POST_TOPIC_PREFIX)
  232 + && topic.endsWith(IotTopicConstants.EVENT_POST_TOPIC_SUFFIX)) {
  233 + // 自定义事件上报
  234 + String[] parts = topic.split("/");
  235 + // 查找event关键字的位置
  236 + for (int i = 0; i < parts.length; i++) {
  237 + if (IotTopicConstants.Keyword.EVENT.equals(parts[i]) && i + 1 < parts.length) {
  238 + String eventId = parts[i + 1];
  239 + return IotTopicConstants.MethodPrefix.THING_EVENT + eventId + ".post";
  240 + }
  241 + }
  242 + } else if (topic.contains(IotTopicConstants.SERVICE_TOPIC_PREFIX)
  243 + && !topic.contains(IotTopicConstants.Keyword.PROPERTY)) {
  244 + // 自定义服务调用
  245 + String[] parts = topic.split("/");
  246 + // 查找service关键字的位置
  247 + for (int i = 0; i < parts.length; i++) {
  248 + if (IotTopicConstants.Keyword.SERVICE.equals(parts[i]) && i + 1 < parts.length) {
  249 + String serviceId = parts[i + 1];
  250 + return IotTopicConstants.MethodPrefix.THING_SERVICE + serviceId;
  251 + }
  252 + }
  253 + }
  254 + }
  255 +
  256 + // 自定义主题
  257 + return IotHttpConstants.Method.CUSTOM_MESSAGE;
  258 + }
  259 +
  260 + /**
  261 + * 生成消息ID
  262 + *
  263 + * @return 消息ID
  264 + */
  265 + private String generateMessageId() {
  266 + return IotAlinkMessage.generateRequestId();
  267 + }
  268 +
  269 + @Override
  270 + public byte[] formatResponse(IotStandardResponse response) {
  271 + try {
  272 + JSONObject httpResponse = new JSONObject();
  273 +
  274 + // 判断是否为认证响应
  275 + if (IotHttpConstants.Method.DEVICE_AUTH.equals(response.getMethod())) {
  276 + // 认证响应格式
  277 + httpResponse.set(IotHttpConstants.ResponseField.CODE, response.getCode());
  278 + httpResponse.set(IotHttpConstants.ResponseField.MESSAGE, response.getMessage());
  279 +
  280 + if (response.getCode() == 200 && response.getData() != null) {
  281 + JSONObject info = new JSONObject();
  282 + if (response.getData() instanceof Map) {
  283 + Map<String, Object> dataMap = (Map<String, Object>) response.getData();
  284 + info.putAll(dataMap);
  285 + } else {
  286 + info.set(IotHttpConstants.ResponseField.TOKEN, response.getData().toString());
  287 + }
  288 + httpResponse.set(IotHttpConstants.ResponseField.INFO, info);
  289 + }
  290 + } else {
  291 + // 数据上报响应格式
  292 + httpResponse.set(IotHttpConstants.ResponseField.CODE, response.getCode());
  293 + httpResponse.set(IotHttpConstants.ResponseField.MESSAGE, response.getMessage());
  294 +
  295 + if (response.getCode() == 200) {
  296 + JSONObject info = new JSONObject();
  297 + info.set(IotHttpConstants.ResponseField.MESSAGE_ID, response.getId());
  298 + httpResponse.set(IotHttpConstants.ResponseField.INFO, info);
  299 + }
  300 + }
  301 +
  302 + String json = httpResponse.toString();
  303 + return json.getBytes(StandardCharsets.UTF_8);
  304 + } catch (Exception e) {
  305 + log.error(IotLogConstants.Http.FORMAT_RESPONSE_FAILED, e);
  306 + return new byte[0];
  307 + }
  308 + }
  309 +
  310 + @Override
  311 + public boolean canHandle(String topic) {
  312 + // 支持认证路径和主题路径
  313 + return topic != null && (AUTH_PATH.equals(topic) || topic.startsWith(TOPIC_PATH_PREFIX));
  314 + }
  315 +
  316 + /**
  317 + * 从设备标识中解析产品Key和设备名称
  318 + *
  319 + * @param deviceKey 设备标识,格式:productKey/deviceName
  320 + * @return 包含产品Key和设备名称的数组,[0]为产品Key,[1]为设备名称
  321 + */
  322 + public static String[] parseDeviceKey(String deviceKey) {
  323 + if (StrUtil.isBlank(deviceKey)) {
  324 + return null;
  325 + }
  326 +
  327 + String[] parts = deviceKey.split("/");
  328 + if (parts.length != 2) {
  329 + return null;
  330 + }
  331 +
  332 + return new String[]{parts[0], parts[1]};
  333 + }
  334 +
  335 + /**
  336 + * 构建设备标识
  337 + *
  338 + * @param productKey 产品Key
  339 + * @param deviceName 设备名称
  340 + * @return 设备标识,格式:productKey/deviceName
  341 + */
  342 + public static String buildDeviceKey(String productKey, String deviceName) {
  343 + if (StrUtil.isBlank(productKey) || StrUtil.isBlank(deviceName)) {
  344 + return null;
  345 + }
  346 + return productKey + "/" + deviceName;
  347 + }
  348 +}
0 349 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotHttpTopicUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +
  5 +/**
  6 + * IoT HTTP 协议主题工具类
  7 + * <p>
  8 + * 参考阿里云IoT平台HTTPS协议标准,支持以下路径格式:
  9 + * 1. 设备认证:/auth
  10 + * 2. 数据上报:/topic/${actualTopic}
  11 + * <p>
  12 + * 其中 actualTopic 遵循MQTT主题规范,例如:
  13 + * - /sys/{productKey}/{deviceName}/thing/service/property/set
  14 + * - /{productKey}/{deviceName}/user/get
  15 + *
  16 + * @author haohao
  17 + */
  18 +public class IotHttpTopicUtils {
  19 +
  20 + /**
  21 + * 设备认证路径
  22 + */
  23 + public static final String AUTH_PATH = "/auth";
  24 +
  25 + /**
  26 + * 数据上报路径前缀
  27 + */
  28 + public static final String TOPIC_PATH_PREFIX = "/topic";
  29 +
  30 + /**
  31 + * 系统主题前缀
  32 + */
  33 + public static final String SYS_TOPIC_PREFIX = "/sys";
  34 +
  35 + /**
  36 + * 构建设备认证路径
  37 + *
  38 + * @return 认证路径
  39 + */
  40 + public static String buildAuthPath() {
  41 + return AUTH_PATH;
  42 + }
  43 +
  44 + /**
  45 + * 构建数据上报路径
  46 + *
  47 + * @param actualTopic 实际的MQTT主题
  48 + * @return HTTP数据上报路径
  49 + */
  50 + public static String buildTopicPath(String actualTopic) {
  51 + if (StrUtil.isBlank(actualTopic)) {
  52 + return null;
  53 + }
  54 + return TOPIC_PATH_PREFIX + actualTopic;
  55 + }
  56 +
  57 + /**
  58 + * 构建系统属性设置路径
  59 + *
  60 + * @param productKey 产品Key
  61 + * @param deviceName 设备名称
  62 + * @return HTTP路径
  63 + */
  64 + public static String buildPropertySetPath(String productKey, String deviceName) {
  65 + if (StrUtil.hasBlank(productKey, deviceName)) {
  66 + return null;
  67 + }
  68 + String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/service/property/set";
  69 + return buildTopicPath(actualTopic);
  70 + }
  71 +
  72 + /**
  73 + * 构建系统属性获取路径
  74 + *
  75 + * @param productKey 产品Key
  76 + * @param deviceName 设备名称
  77 + * @return HTTP路径
  78 + */
  79 + public static String buildPropertyGetPath(String productKey, String deviceName) {
  80 + if (StrUtil.hasBlank(productKey, deviceName)) {
  81 + return null;
  82 + }
  83 + String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/service/property/get";
  84 + return buildTopicPath(actualTopic);
  85 + }
  86 +
  87 + /**
  88 + * 构建系统属性上报路径
  89 + *
  90 + * @param productKey 产品Key
  91 + * @param deviceName 设备名称
  92 + * @return HTTP路径
  93 + */
  94 + public static String buildPropertyPostPath(String productKey, String deviceName) {
  95 + if (StrUtil.hasBlank(productKey, deviceName)) {
  96 + return null;
  97 + }
  98 + String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/event/property/post";
  99 + return buildTopicPath(actualTopic);
  100 + }
  101 +
  102 + /**
  103 + * 构建系统事件上报路径
  104 + *
  105 + * @param productKey 产品Key
  106 + * @param deviceName 设备名称
  107 + * @param eventIdentifier 事件标识符
  108 + * @return HTTP路径
  109 + */
  110 + public static String buildEventPostPath(String productKey, String deviceName, String eventIdentifier) {
  111 + if (StrUtil.hasBlank(productKey, deviceName, eventIdentifier)) {
  112 + return null;
  113 + }
  114 + String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/event/" + eventIdentifier
  115 + + "/post";
  116 + return buildTopicPath(actualTopic);
  117 + }
  118 +
  119 + /**
  120 + * 构建系统服务调用路径
  121 + *
  122 + * @param productKey 产品Key
  123 + * @param deviceName 设备名称
  124 + * @param serviceIdentifier 服务标识符
  125 + * @return HTTP路径
  126 + */
  127 + public static String buildServiceInvokePath(String productKey, String deviceName, String serviceIdentifier) {
  128 + if (StrUtil.hasBlank(productKey, deviceName, serviceIdentifier)) {
  129 + return null;
  130 + }
  131 + String actualTopic = SYS_TOPIC_PREFIX + "/" + productKey + "/" + deviceName + "/thing/service/"
  132 + + serviceIdentifier;
  133 + return buildTopicPath(actualTopic);
  134 + }
  135 +
  136 + /**
  137 + * 构建自定义主题路径
  138 + *
  139 + * @param productKey 产品Key
  140 + * @param deviceName 设备名称
  141 + * @param customPath 自定义路径
  142 + * @return HTTP路径
  143 + */
  144 + public static String buildCustomTopicPath(String productKey, String deviceName, String customPath) {
  145 + if (StrUtil.hasBlank(productKey, deviceName, customPath)) {
  146 + return null;
  147 + }
  148 + String actualTopic = "/" + productKey + "/" + deviceName + "/" + customPath;
  149 + return buildTopicPath(actualTopic);
  150 + }
  151 +
  152 + /**
  153 + * 从HTTP路径中提取实际主题
  154 + *
  155 + * @param httpPath HTTP路径,格式:/topic/${actualTopic}
  156 + * @return 实际主题,如果解析失败返回null
  157 + */
  158 + public static String extractActualTopic(String httpPath) {
  159 + if (StrUtil.isBlank(httpPath) || !httpPath.startsWith(TOPIC_PATH_PREFIX)) {
  160 + return null;
  161 + }
  162 + return httpPath.substring(TOPIC_PATH_PREFIX.length()); // 直接移除/topic前缀
  163 + }
  164 +
  165 + /**
  166 + * 从主题中解析产品Key
  167 + *
  168 + * @param topic 主题,支持系统主题和自定义主题
  169 + * @return 产品Key,如果无法解析则返回null
  170 + */
  171 + public static String parseProductKeyFromTopic(String topic) {
  172 + if (StrUtil.isBlank(topic)) {
  173 + return null;
  174 + }
  175 +
  176 + String[] parts = topic.split("/");
  177 +
  178 + // 系统主题格式:/sys/{productKey}/{deviceName}/...
  179 + if (parts.length >= 4 && "sys".equals(parts[1])) {
  180 + return parts[2];
  181 + }
  182 +
  183 + // 自定义主题格式:/{productKey}/{deviceName}/...
  184 + // 确保不是不完整的系统主题格式
  185 + if (parts.length >= 3 && StrUtil.isNotBlank(parts[1]) && !"sys".equals(parts[1])) {
  186 + return parts[1];
  187 + }
  188 +
  189 + return null;
  190 + }
  191 +
  192 + /**
  193 + * 从主题中解析设备名称
  194 + *
  195 + * @param topic 主题,支持系统主题和自定义主题
  196 + * @return 设备名称,如果无法解析则返回null
  197 + */
  198 + public static String parseDeviceNameFromTopic(String topic) {
  199 + if (StrUtil.isBlank(topic)) {
  200 + return null;
  201 + }
  202 +
  203 + String[] parts = topic.split("/");
  204 +
  205 + // 系统主题格式:/sys/{productKey}/{deviceName}/...
  206 + if (parts.length >= 4 && "sys".equals(parts[1])) {
  207 + return parts[3];
  208 + }
  209 +
  210 + // 自定义主题格式:/{productKey}/{deviceName}/...
  211 + // 确保不是不完整的系统主题格式
  212 + if (parts.length >= 3 && StrUtil.isNotBlank(parts[2]) && !"sys".equals(parts[1])) {
  213 + return parts[2];
  214 + }
  215 +
  216 + return null;
  217 + }
  218 +
  219 + /**
  220 + * 检查是否为认证路径
  221 + *
  222 + * @param path 路径
  223 + * @return 如果是认证路径返回true,否则返回false
  224 + */
  225 + public static boolean isAuthPath(String path) {
  226 + return AUTH_PATH.equals(path);
  227 + }
  228 +
  229 + /**
  230 + * 检查是否为数据上报路径
  231 + *
  232 + * @param path 路径
  233 + * @return 如果是数据上报路径返回true,否则返回false
  234 + */
  235 + public static boolean isTopicPath(String path) {
  236 + return path != null && path.startsWith(TOPIC_PATH_PREFIX);
  237 + }
  238 +
  239 + /**
  240 + * 检查是否为有效的HTTP路径
  241 + *
  242 + * @param path 路径
  243 + * @return 如果是有效的HTTP路径返回true,否则返回false
  244 + */
  245 + public static boolean isValidHttpPath(String path) {
  246 + return isAuthPath(path) || isTopicPath(path);
  247 + }
  248 +
  249 + /**
  250 + * 检查是否为系统主题
  251 + *
  252 + * @param topic 主题
  253 + * @return 如果是系统主题返回true,否则返回false
  254 + */
  255 + public static boolean isSystemTopic(String topic) {
  256 + return topic != null && topic.startsWith(SYS_TOPIC_PREFIX);
  257 + }
  258 +
  259 + /**
  260 + * 构建响应主题路径
  261 + *
  262 + * @param requestPath 请求路径
  263 + * @return 响应路径,如果无法构建返回null
  264 + */
  265 + public static String buildReplyPath(String requestPath) {
  266 + String actualTopic = extractActualTopic(requestPath);
  267 + if (actualTopic == null) {
  268 + return null;
  269 + }
  270 +
  271 + // 为系统主题添加_reply后缀
  272 + if (isSystemTopic(actualTopic)) {
  273 + String replyTopic = actualTopic + "_reply";
  274 + return buildTopicPath(replyTopic);
  275 + }
  276 +
  277 + return null;
  278 + }
  279 +}
0 280 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicParser.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.module.iot.protocol.constants.IotTopicConstants;
  5 +import cn.iocoder.yudao.module.iot.protocol.enums.IotMessageDirectionEnum;
  6 +import cn.iocoder.yudao.module.iot.protocol.enums.IotMessageTypeEnum;
  7 +import lombok.Data;
  8 +
  9 +/**
  10 + * IoT 主题解析器
  11 + * <p>
  12 + * 用于解析各种格式的 IoT 主题,提取其中的关键信息
  13 + *
  14 + * @author haohao
  15 + */
  16 +public class IotTopicParser {
  17 +
  18 + /**
  19 + * 主题解析结果
  20 + */
  21 + @Data
  22 + public static class TopicInfo {
  23 + /**
  24 + * 产品Key
  25 + */
  26 + private String productKey;
  27 +
  28 + /**
  29 + * 设备名称
  30 + */
  31 + private String deviceName;
  32 +
  33 + /**
  34 + * 消息类型
  35 + */
  36 + private IotMessageTypeEnum messageType;
  37 +
  38 + /**
  39 + * 消息方向
  40 + */
  41 + private IotMessageDirectionEnum direction;
  42 +
  43 + /**
  44 + * 服务标识符(仅服务调用时有效)
  45 + */
  46 + private String serviceIdentifier;
  47 +
  48 + /**
  49 + * 事件标识符(仅事件上报时有效)
  50 + */
  51 + private String eventIdentifier;
  52 +
  53 + /**
  54 + * 是否为响应主题
  55 + */
  56 + private boolean isReply;
  57 +
  58 + /**
  59 + * 原始主题
  60 + */
  61 + private String originalTopic;
  62 + }
  63 +
  64 + /**
  65 + * 解析主题
  66 + *
  67 + * @param topic 主题字符串
  68 + * @return 解析结果,如果解析失败返回 null
  69 + */
  70 + public static TopicInfo parse(String topic) {
  71 + if (StrUtil.isBlank(topic)) {
  72 + return null;
  73 + }
  74 +
  75 + TopicInfo info = new TopicInfo();
  76 + info.setOriginalTopic(topic);
  77 +
  78 + // 检查是否为响应主题
  79 + boolean isReply = topic.endsWith(IotTopicConstants.REPLY_SUFFIX);
  80 + info.setReply(isReply);
  81 +
  82 + // 移除响应后缀,便于后续解析
  83 + String normalizedTopic = isReply ? topic.substring(0, topic.length() - IotTopicConstants.REPLY_SUFFIX.length())
  84 + : topic;
  85 +
  86 + // 解析系统主题
  87 + if (normalizedTopic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
  88 + return parseSystemTopic(info, normalizedTopic);
  89 + }
  90 +
  91 + // 解析自定义主题
  92 + return parseCustomTopic(info, normalizedTopic);
  93 + }
  94 +
  95 + /**
  96 + * 解析系统主题
  97 + * 格式:/sys/{productKey}/{deviceName}/thing/service/{identifier}
  98 + * 或:/sys/{productKey}/{deviceName}/thing/event/{identifier}/post
  99 + */
  100 + private static TopicInfo parseSystemTopic(TopicInfo info, String topic) {
  101 + String[] parts = topic.split("/");
  102 + if (parts.length < 6) {
  103 + return null;
  104 + }
  105 +
  106 + // 解析产品Key和设备名称
  107 + info.setProductKey(parts[2]);
  108 + info.setDeviceName(parts[3]);
  109 +
  110 + // 判断消息方向:包含 /post 通常是上行,其他是下行
  111 + info.setDirection(topic.contains("/post") || topic.contains("/reply") ? IotMessageDirectionEnum.UPSTREAM
  112 + : IotMessageDirectionEnum.DOWNSTREAM);
  113 +
  114 + // 解析具体的消息类型
  115 + if (topic.contains("/thing/service/")) {
  116 + return parseServiceTopic(info, topic, parts);
  117 + } else if (topic.contains("/thing/event/")) {
  118 + return parseEventTopic(info, topic, parts);
  119 + }
  120 +
  121 + return null;
  122 + }
  123 +
  124 + /**
  125 + * 解析服务相关主题
  126 + */
  127 + private static TopicInfo parseServiceTopic(TopicInfo info, String topic, String[] parts) {
  128 + // 查找 service 关键字的位置
  129 + int serviceIndex = -1;
  130 + for (int i = 0; i < parts.length; i++) {
  131 + if ("service".equals(parts[i])) {
  132 + serviceIndex = i;
  133 + break;
  134 + }
  135 + }
  136 +
  137 + if (serviceIndex == -1 || serviceIndex + 1 >= parts.length) {
  138 + return null;
  139 + }
  140 +
  141 + String serviceType = parts[serviceIndex + 1];
  142 +
  143 + // 根据服务类型确定消息类型
  144 + switch (serviceType) {
  145 + case "property":
  146 + if (serviceIndex + 2 < parts.length) {
  147 + String operation = parts[serviceIndex + 2];
  148 + if ("set".equals(operation)) {
  149 + info.setMessageType(IotMessageTypeEnum.PROPERTY_SET);
  150 + } else if ("get".equals(operation)) {
  151 + info.setMessageType(IotMessageTypeEnum.PROPERTY_GET);
  152 + }
  153 + }
  154 + break;
  155 + case "config":
  156 + if (serviceIndex + 2 < parts.length && "set".equals(parts[serviceIndex + 2])) {
  157 + info.setMessageType(IotMessageTypeEnum.CONFIG_SET);
  158 + }
  159 + break;
  160 + case "ota":
  161 + if (serviceIndex + 2 < parts.length && "upgrade".equals(parts[serviceIndex + 2])) {
  162 + info.setMessageType(IotMessageTypeEnum.OTA_UPGRADE);
  163 + }
  164 + break;
  165 + default:
  166 + // 自定义服务
  167 + info.setMessageType(IotMessageTypeEnum.SERVICE_INVOKE);
  168 + info.setServiceIdentifier(serviceType);
  169 + break;
  170 + }
  171 +
  172 + return info;
  173 + }
  174 +
  175 + /**
  176 + * 解析事件相关主题
  177 + */
  178 + private static TopicInfo parseEventTopic(TopicInfo info, String topic, String[] parts) {
  179 + // 查找 event 关键字的位置
  180 + int eventIndex = -1;
  181 + for (int i = 0; i < parts.length; i++) {
  182 + if ("event".equals(parts[i])) {
  183 + eventIndex = i;
  184 + break;
  185 + }
  186 + }
  187 +
  188 + if (eventIndex == -1 || eventIndex + 1 >= parts.length) {
  189 + return null;
  190 + }
  191 +
  192 + String eventType = parts[eventIndex + 1];
  193 +
  194 + if ("property".equals(eventType) && eventIndex + 2 < parts.length && "post".equals(parts[eventIndex + 2])) {
  195 + info.setMessageType(IotMessageTypeEnum.PROPERTY_POST);
  196 + } else {
  197 + // 自定义事件
  198 + info.setMessageType(IotMessageTypeEnum.EVENT_POST);
  199 + info.setEventIdentifier(eventType);
  200 + }
  201 +
  202 + return info;
  203 + }
  204 +
  205 + /**
  206 + * 解析自定义主题
  207 + * 这里可以根据实际需求扩展自定义主题的解析逻辑
  208 + */
  209 + private static TopicInfo parseCustomTopic(TopicInfo info, String topic) {
  210 + // TODO: 根据业务需要实现自定义主题解析逻辑
  211 + return info;
  212 + }
  213 +
  214 + /**
  215 + * 检查主题是否为有效的系统主题
  216 + *
  217 + * @param topic 主题
  218 + * @return 如果是有效的系统主题返回 true,否则返回 false
  219 + */
  220 + public static boolean isValidSystemTopic(String topic) {
  221 + TopicInfo info = parse(topic);
  222 + return info != null &&
  223 + StrUtil.isNotBlank(info.getProductKey()) &&
  224 + StrUtil.isNotBlank(info.getDeviceName()) &&
  225 + info.getMessageType() != null;
  226 + }
  227 +
  228 + /**
  229 + * 检查主题是否为响应主题
  230 + *
  231 + * @param topic 主题
  232 + * @return 如果是响应主题返回 true,否则返回 false
  233 + */
  234 + public static boolean isReplyTopic(String topic) {
  235 + return topic != null && topic.endsWith(IotTopicConstants.REPLY_SUFFIX);
  236 + }
  237 +}
0 238 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicUtils.java
... ... @@ -126,12 +126,12 @@ public class IotTopicUtils {
126 126 if (StrUtil.isBlank(topic) || !topic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
127 127 return null;
128 128 }
129   -
  129 +
130 130 String[] parts = topic.split("/");
131 131 if (parts.length < 4) {
132 132 return null;
133 133 }
134   -
  134 +
135 135 return parts[2];
136 136 }
137 137  
... ... @@ -146,12 +146,12 @@ public class IotTopicUtils {
146 146 if (StrUtil.isBlank(topic) || !topic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
147 147 return null;
148 148 }
149   -
  149 +
150 150 String[] parts = topic.split("/");
151 151 if (parts.length < 4) {
152 152 return null;
153 153 }
154   -
  154 +
155 155 return parts[3];
156 156 }
157 157  
... ... @@ -166,19 +166,19 @@ public class IotTopicUtils {
166 166 if (StrUtil.isBlank(topic) || !topic.startsWith(IotTopicConstants.SYS_TOPIC_PREFIX)) {
167 167 return null;
168 168 }
169   -
  169 +
170 170 // 服务调用主题
171 171 if (topic.contains("/thing/service/")) {
172 172 String servicePart = topic.substring(topic.indexOf("/thing/service/") + "/thing/service/".length());
173 173 return servicePart.replace("/", ".");
174 174 }
175   -
  175 +
176 176 // 事件上报主题
177 177 if (topic.contains("/thing/event/")) {
178 178 String eventPart = topic.substring(topic.indexOf("/thing/event/") + "/thing/event/".length());
179 179 return "event." + eventPart.replace("/", ".");
180 180 }
181   -
  181 +
182 182 return null;
183 183 }
184 184 }
185 185 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/config/IotProtocolAutoConfigurationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.config;
  2 +
  3 +import cn.iocoder.yudao.module.iot.protocol.convert.IotProtocolConverter;
  4 +import cn.iocoder.yudao.module.iot.protocol.enums.IotProtocolTypeEnum;
  5 +import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
  6 +import cn.iocoder.yudao.module.iot.protocol.message.impl.IotAlinkMessageParser;
  7 +import cn.iocoder.yudao.module.iot.protocol.message.impl.IotHttpMessageParser;
  8 +import org.junit.jupiter.api.BeforeEach;
  9 +import org.junit.jupiter.api.Test;
  10 +
  11 +import static org.junit.jupiter.api.Assertions.*;
  12 +
  13 +/**
  14 + * {@link IotProtocolAutoConfiguration} 单元测试
  15 + *
  16 + * @author haohao
  17 + */
  18 +class IotProtocolAutoConfigurationTest {
  19 +
  20 + private IotProtocolAutoConfiguration configuration;
  21 +
  22 + @BeforeEach
  23 + void setUp() {
  24 + configuration = new IotProtocolAutoConfiguration();
  25 + }
  26 +
  27 + @Test
  28 + void testIotAlinkMessageParser() {
  29 + // 测试 Alink 协议解析器 Bean 创建
  30 + IotMessageParser parser = configuration.iotAlinkMessageParser();
  31 +
  32 + assertNotNull(parser);
  33 + assertInstanceOf(IotAlinkMessageParser.class, parser);
  34 + }
  35 +
  36 + @Test
  37 + void testIotHttpMessageParser() {
  38 + // 测试 HTTP 协议解析器 Bean 创建
  39 + IotMessageParser parser = configuration.iotHttpMessageParser();
  40 +
  41 + assertNotNull(parser);
  42 + assertInstanceOf(IotHttpMessageParser.class, parser);
  43 + }
  44 +
  45 + @Test
  46 + void testIotProtocolConverter() {
  47 + // 创建解析器实例
  48 + IotMessageParser alinkParser = configuration.iotAlinkMessageParser();
  49 + IotMessageParser httpParser = configuration.iotHttpMessageParser();
  50 +
  51 + // 测试协议转换器 Bean 创建
  52 + IotProtocolConverter converter = configuration.iotProtocolConverter(alinkParser, httpParser);
  53 +
  54 + assertNotNull(converter);
  55 +
  56 + // 验证支持的协议
  57 + assertTrue(converter.supportsProtocol(IotProtocolTypeEnum.ALINK.getCode()));
  58 + assertTrue(converter.supportsProtocol(IotProtocolTypeEnum.HTTP.getCode()));
  59 +
  60 + // 验证支持的协议数量
  61 + String[] supportedProtocols = converter.getSupportedProtocols();
  62 + assertEquals(2, supportedProtocols.length);
  63 + }
  64 +
  65 + @Test
  66 + void testBeanNameConstants() {
  67 + // 测试 Bean 名称常量定义
  68 + assertEquals("iotAlinkMessageParser", IotProtocolAutoConfiguration.IOT_ALINK_MESSAGE_PARSER_BEAN_NAME);
  69 + assertEquals("iotHttpMessageParser", IotProtocolAutoConfiguration.IOT_HTTP_MESSAGE_PARSER_BEAN_NAME);
  70 + }
  71 +}
0 72 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/example/AliyunHttpProtocolExample.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.example;
  2 +
  3 +import cn.hutool.json.JSONObject;
  4 +import cn.iocoder.yudao.module.iot.protocol.util.IotHttpTopicUtils;
  5 +
  6 +/**
  7 + * 阿里云IoT平台HTTPS协议示例
  8 + * <p>
  9 + * 参考阿里云IoT平台HTTPS连接通信标准,演示设备认证和数据上报的完整流程
  10 + *
  11 + * @author haohao
  12 + */
  13 +public class AliyunHttpProtocolExample {
  14 +
  15 + public static void main(String[] args) {
  16 + System.out.println("=== 阿里云IoT平台HTTPS协议演示 ===\n");
  17 +
  18 + // 演示设备认证
  19 + demonstrateDeviceAuth();
  20 +
  21 + // 演示数据上报
  22 + demonstrateDataUpload();
  23 +
  24 + // 演示路径构建
  25 + demonstratePathBuilding();
  26 + }
  27 +
  28 + /**
  29 + * 演示设备认证流程
  30 + */
  31 + private static void demonstrateDeviceAuth() {
  32 + System.out.println("1. 设备认证流程:");
  33 + System.out.println("认证路径: " + IotHttpTopicUtils.buildAuthPath());
  34 +
  35 + // 构建认证请求消息
  36 + JSONObject authRequest = new JSONObject();
  37 + authRequest.set("productKey", "a1GFjLP****");
  38 + authRequest.set("deviceName", "device123");
  39 + authRequest.set("clientId", "device123_001");
  40 + authRequest.set("timestamp", String.valueOf(System.currentTimeMillis()));
  41 + authRequest.set("sign", "4870141D4067227128CBB4377906C3731CAC221C");
  42 + authRequest.set("signmethod", "hmacsha1");
  43 + authRequest.set("version", "default");
  44 +
  45 + System.out.println("认证请求消息:");
  46 + System.out.println(authRequest.toString());
  47 +
  48 + // 模拟认证响应
  49 + JSONObject authResponse = new JSONObject();
  50 + authResponse.set("code", 0);
  51 + authResponse.set("message", "success");
  52 +
  53 + JSONObject info = new JSONObject();
  54 + info.set("token", "6944e5bfb92e4d4ea3918d1eda39****");
  55 + authResponse.set("info", info);
  56 +
  57 + System.out.println("认证响应:");
  58 + System.out.println(authResponse.toString());
  59 + System.out.println();
  60 + }
  61 +
  62 + /**
  63 + * 演示数据上报流程
  64 + */
  65 + private static void demonstrateDataUpload() {
  66 + System.out.println("2. 数据上报流程:");
  67 +
  68 + String productKey = "a1GFjLP****";
  69 + String deviceName = "device123";
  70 +
  71 + // 属性上报
  72 + String propertyPostPath = IotHttpTopicUtils.buildPropertyPostPath(productKey, deviceName);
  73 + System.out.println("属性上报路径: " + propertyPostPath);
  74 +
  75 + // Alink格式的属性上报消息
  76 + JSONObject propertyMessage = new JSONObject();
  77 + propertyMessage.set("id", "123456");
  78 + propertyMessage.set("version", "1.0");
  79 + propertyMessage.set("method", "thing.event.property.post");
  80 +
  81 + JSONObject propertyParams = new JSONObject();
  82 + JSONObject properties = new JSONObject();
  83 + properties.set("temperature", 25.6);
  84 + properties.set("humidity", 60.3);
  85 + propertyParams.set("properties", properties);
  86 + propertyMessage.set("params", propertyParams);
  87 +
  88 + System.out.println("属性上报消息:");
  89 + System.out.println(propertyMessage.toString());
  90 +
  91 + // 事件上报
  92 + String eventPostPath = IotHttpTopicUtils.buildEventPostPath(productKey, deviceName, "temperatureAlert");
  93 + System.out.println("\n事件上报路径: " + eventPostPath);
  94 +
  95 + JSONObject eventMessage = new JSONObject();
  96 + eventMessage.set("id", "123457");
  97 + eventMessage.set("version", "1.0");
  98 + eventMessage.set("method", "thing.event.temperatureAlert.post");
  99 +
  100 + JSONObject eventParams = new JSONObject();
  101 + eventParams.set("value", new JSONObject().set("alertLevel", "high").set("currentTemp", 45.2));
  102 + eventParams.set("time", System.currentTimeMillis());
  103 + eventMessage.set("params", eventParams);
  104 +
  105 + System.out.println("事件上报消息:");
  106 + System.out.println(eventMessage.toString());
  107 +
  108 + // 模拟数据上报响应
  109 + JSONObject uploadResponse = new JSONObject();
  110 + uploadResponse.set("code", 0);
  111 + uploadResponse.set("message", "success");
  112 +
  113 + JSONObject responseInfo = new JSONObject();
  114 + responseInfo.set("messageId", 892687470447040L);
  115 + uploadResponse.set("info", responseInfo);
  116 +
  117 + System.out.println("\n数据上报响应:");
  118 + System.out.println(uploadResponse.toString());
  119 + System.out.println();
  120 + }
  121 +
  122 + /**
  123 + * 演示路径构建功能
  124 + */
  125 + private static void demonstratePathBuilding() {
  126 + System.out.println("3. 路径构建功能:");
  127 +
  128 + String productKey = "smartProduct";
  129 + String deviceName = "sensor001";
  130 +
  131 + // 系统主题路径
  132 + System.out.println("系统主题路径:");
  133 + System.out.println(" 属性设置: " + IotHttpTopicUtils.buildPropertySetPath(productKey, deviceName));
  134 + System.out.println(" 属性获取: " + IotHttpTopicUtils.buildPropertyGetPath(productKey, deviceName));
  135 + System.out.println(" 属性上报: " + IotHttpTopicUtils.buildPropertyPostPath(productKey, deviceName));
  136 + System.out.println(" 事件上报: " + IotHttpTopicUtils.buildEventPostPath(productKey, deviceName, "alarm"));
  137 + System.out.println(" 服务调用: " + IotHttpTopicUtils.buildServiceInvokePath(productKey, deviceName, "reboot"));
  138 +
  139 + // 自定义主题路径
  140 + System.out.println("\n自定义主题路径:");
  141 + System.out.println(" 用户主题: " + IotHttpTopicUtils.buildCustomTopicPath(productKey, deviceName, "user/get"));
  142 +
  143 + // 响应路径
  144 + String requestPath = IotHttpTopicUtils.buildPropertySetPath(productKey, deviceName);
  145 + String replyPath = IotHttpTopicUtils.buildReplyPath(requestPath);
  146 + System.out.println("\n响应路径:");
  147 + System.out.println(" 请求路径: " + requestPath);
  148 + System.out.println(" 响应路径: " + replyPath);
  149 +
  150 + // 路径解析
  151 + System.out.println("\n路径解析:");
  152 + String testPath = "/topic/sys/smartProduct/sensor001/thing/service/property/set";
  153 + String actualTopic = IotHttpTopicUtils.extractActualTopic(testPath);
  154 + System.out.println(" HTTP路径: " + testPath);
  155 + System.out.println(" 实际主题: " + actualTopic);
  156 + System.out.println(" 产品Key: " + IotHttpTopicUtils.parseProductKeyFromTopic(actualTopic));
  157 + System.out.println(" 设备名称: " + IotHttpTopicUtils.parseDeviceNameFromTopic(actualTopic));
  158 + System.out.println(" 是否为系统主题: " + IotHttpTopicUtils.isSystemTopic(actualTopic));
  159 +
  160 + // 路径类型检查
  161 + System.out.println("\n路径类型检查:");
  162 + System.out.println(" 认证路径检查: " + IotHttpTopicUtils.isAuthPath("/auth"));
  163 + System.out.println(" 数据路径检查: " + IotHttpTopicUtils.isTopicPath("/topic/test"));
  164 + System.out.println(" 有效路径检查: " + IotHttpTopicUtils.isValidHttpPath("/topic/sys/test/device/property"));
  165 + }
  166 +}
0 167 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParserTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.message.impl;
  2 +
  3 +import cn.hutool.json.JSONObject;
  4 +import cn.iocoder.yudao.module.iot.protocol.message.IotAlinkMessage;
  5 +import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse;
  6 +import org.junit.jupiter.api.BeforeEach;
  7 +import org.junit.jupiter.api.Test;
  8 +
  9 +import java.nio.charset.StandardCharsets;
  10 +import java.util.HashMap;
  11 +import java.util.Map;
  12 +
  13 +import static org.junit.jupiter.api.Assertions.*;
  14 +
  15 +/**
  16 + * {@link IotHttpMessageParser} 单元测试
  17 + * <p>
  18 + * 测试阿里云IoT平台HTTPS协议标准的消息解析功能
  19 + *
  20 + * @author haohao
  21 + */
  22 +class IotHttpMessageParserTest {
  23 +
  24 + private IotHttpMessageParser parser;
  25 +
  26 + @BeforeEach
  27 + void setUp() {
  28 + parser = new IotHttpMessageParser();
  29 + }
  30 +
  31 + @Test
  32 + void testCanHandle() {
  33 + // 测试能处理的路径
  34 + assertTrue(parser.canHandle("/auth"));
  35 + assertTrue(parser.canHandle("/topic/sys/test/device1/thing/service/property/set"));
  36 + assertTrue(parser.canHandle("/topic/test/device1/user/get"));
  37 +
  38 + // 测试不能处理的路径
  39 + assertFalse(parser.canHandle("/sys/test/device1/thing/service/property/set"));
  40 + assertFalse(parser.canHandle("/unknown/path"));
  41 + assertFalse(parser.canHandle(null));
  42 + assertFalse(parser.canHandle(""));
  43 + }
  44 +
  45 + @Test
  46 + void testParseAuthMessage() {
  47 + // 构建认证消息
  48 + JSONObject authMessage = new JSONObject();
  49 + authMessage.set("productKey", "a1GFjLP****");
  50 + authMessage.set("deviceName", "device123");
  51 + authMessage.set("clientId", "device123_001");
  52 + authMessage.set("timestamp", "1501668289957");
  53 + authMessage.set("sign", "4870141D4067227128CBB4377906C3731CAC221C");
  54 + authMessage.set("signmethod", "hmacsha1");
  55 + authMessage.set("version", "default");
  56 +
  57 + String topic = "/auth";
  58 + byte[] payload = authMessage.toString().getBytes(StandardCharsets.UTF_8);
  59 +
  60 + // 解析消息
  61 + IotAlinkMessage result = parser.parse(topic, payload);
  62 +
  63 + // 验证结果
  64 + assertNotNull(result);
  65 + assertNotNull(result.getId());
  66 + assertEquals("device.auth", result.getMethod());
  67 + assertEquals("default", result.getVersion());
  68 + assertNotNull(result.getParams());
  69 +
  70 + Map<String, Object> params = result.getParams();
  71 + assertEquals("a1GFjLP****", params.get("productKey"));
  72 + assertEquals("device123", params.get("deviceName"));
  73 + assertEquals("device123_001", params.get("clientId"));
  74 + assertEquals("1501668289957", params.get("timestamp"));
  75 + assertEquals("4870141D4067227128CBB4377906C3731CAC221C", params.get("sign"));
  76 + assertEquals("hmacsha1", params.get("signmethod"));
  77 + }
  78 +
  79 + @Test
  80 + void testParseAuthMessageWithMissingFields() {
  81 + // 构建缺少必需字段的认证消息
  82 + JSONObject authMessage = new JSONObject();
  83 + authMessage.set("productKey", "a1GFjLP****");
  84 + authMessage.set("deviceName", "device123");
  85 + // 缺少 clientId 和 sign
  86 +
  87 + String topic = "/auth";
  88 + byte[] payload = authMessage.toString().getBytes(StandardCharsets.UTF_8);
  89 +
  90 + // 解析消息
  91 + IotAlinkMessage result = parser.parse(topic, payload);
  92 +
  93 + // 验证结果
  94 + assertNull(result);
  95 + }
  96 +
  97 + @Test
  98 + void testParseJsonDataMessage() {
  99 + // 构建JSON格式的数据消息
  100 + JSONObject dataMessage = new JSONObject();
  101 + dataMessage.set("id", "123456");
  102 + dataMessage.set("version", "1.0");
  103 + dataMessage.set("method", "thing.event.property.post");
  104 +
  105 + JSONObject params = new JSONObject();
  106 + JSONObject properties = new JSONObject();
  107 + properties.set("temperature", 25.6);
  108 + properties.set("humidity", 60.3);
  109 + params.set("properties", properties);
  110 + dataMessage.set("params", params);
  111 +
  112 + String topic = "/topic/sys/a1GFjLP****/device123/thing/event/property/post";
  113 + byte[] payload = dataMessage.toString().getBytes(StandardCharsets.UTF_8);
  114 +
  115 + // 解析消息
  116 + IotAlinkMessage result = parser.parse(topic, payload);
  117 +
  118 + // 验证结果
  119 + assertNotNull(result);
  120 + assertEquals("123456", result.getId());
  121 + assertEquals("thing.event.property.post", result.getMethod());
  122 + assertEquals("1.0", result.getVersion());
  123 + assertNotNull(result.getParams());
  124 + assertNotNull(result.getParams().get("properties"));
  125 + }
  126 +
  127 + @Test
  128 + void testParseRawDataMessage() {
  129 + // 原始数据消息
  130 + String rawData = "temperature:25.6,humidity:60.3";
  131 + String topic = "/topic/sys/a1GFjLP****/device123/thing/event/property/post";
  132 + byte[] payload = rawData.getBytes(StandardCharsets.UTF_8);
  133 +
  134 + // 解析消息
  135 + IotAlinkMessage result = parser.parse(topic, payload);
  136 +
  137 + // 验证结果
  138 + assertNotNull(result);
  139 + assertNotNull(result.getId());
  140 + assertEquals("thing.event.property.post", result.getMethod());
  141 + assertEquals("1.0", result.getVersion());
  142 + assertNotNull(result.getParams());
  143 + assertEquals(rawData, result.getParams().get("data"));
  144 + }
  145 +
  146 + @Test
  147 + void testInferMethodFromTopic() {
  148 + // 测试系统主题方法推断
  149 + testInferMethod("/sys/test/device/thing/service/property/set", "thing.service.property.set");
  150 + testInferMethod("/sys/test/device/thing/service/property/get", "thing.service.property.get");
  151 + testInferMethod("/sys/test/device/thing/event/property/post", "thing.event.property.post");
  152 + testInferMethod("/sys/test/device/thing/event/alarm/post", "thing.event.alarm.post");
  153 + testInferMethod("/sys/test/device/thing/service/reboot", "thing.service.reboot");
  154 +
  155 + // 测试自定义主题
  156 + testInferMethod("/test/device/user/get", "custom.message");
  157 + }
  158 +
  159 + private void testInferMethod(String actualTopic, String expectedMethod) {
  160 + String topic = "/topic" + actualTopic;
  161 + String rawData = "test data";
  162 + byte[] payload = rawData.getBytes(StandardCharsets.UTF_8);
  163 +
  164 + IotAlinkMessage result = parser.parse(topic, payload);
  165 + assertNotNull(result);
  166 + assertEquals(expectedMethod, result.getMethod());
  167 + }
  168 +
  169 + @Test
  170 + void testFormatAuthResponse() {
  171 + // 创建认证成功响应
  172 + Map<String, Object> data = new HashMap<>();
  173 + data.put("token", "6944e5bfb92e4d4ea3918d1eda39****");
  174 +
  175 + IotStandardResponse response = IotStandardResponse.success("auth123", "device.auth", data);
  176 +
  177 + // 格式化响应
  178 + byte[] result = parser.formatResponse(response);
  179 +
  180 + // 验证结果
  181 + assertNotNull(result);
  182 + assertTrue(result.length > 0);
  183 +
  184 + String responseStr = new String(result, StandardCharsets.UTF_8);
  185 + JSONObject responseJson = new JSONObject(responseStr);
  186 +
  187 + assertEquals(200, responseJson.getInt("code"));
  188 + assertEquals("success", responseJson.getStr("message"));
  189 + assertNotNull(responseJson.get("info"));
  190 +
  191 + JSONObject info = responseJson.getJSONObject("info");
  192 + assertEquals("6944e5bfb92e4d4ea3918d1eda39****", info.getStr("token"));
  193 + }
  194 +
  195 + @Test
  196 + void testFormatDataResponse() {
  197 + // 创建数据上报响应
  198 + IotStandardResponse response = IotStandardResponse.success("123456", "thing.event.property.post", null);
  199 +
  200 + // 格式化响应
  201 + byte[] result = parser.formatResponse(response);
  202 +
  203 + // 验证结果
  204 + assertNotNull(result);
  205 + assertTrue(result.length > 0);
  206 +
  207 + String responseStr = new String(result, StandardCharsets.UTF_8);
  208 + JSONObject responseJson = new JSONObject(responseStr);
  209 +
  210 + assertEquals(200, responseJson.getInt("code"));
  211 + assertEquals("success", responseJson.getStr("message"));
  212 + assertNotNull(responseJson.get("info"));
  213 +
  214 + JSONObject info = responseJson.getJSONObject("info");
  215 + assertEquals("123456", info.getStr("messageId"));
  216 + }
  217 +
  218 + @Test
  219 + void testParseInvalidMessage() {
  220 + String topic = "/topic/sys/test/device/thing/service/property/set";
  221 +
  222 + // 测试空消息
  223 + assertNull(parser.parse(topic, null));
  224 + assertNull(parser.parse(topic, new byte[0]));
  225 +
  226 + // 测试不支持的路径
  227 + byte[] validPayload = "test data".getBytes(StandardCharsets.UTF_8);
  228 + assertNull(parser.parse("/unknown/path", validPayload));
  229 + }
  230 +
  231 + @Test
  232 + void testParseDeviceKey() {
  233 + // 测试有效的设备标识
  234 + String[] result1 = IotHttpMessageParser.parseDeviceKey("productKey/deviceName");
  235 + assertNotNull(result1);
  236 + assertEquals(2, result1.length);
  237 + assertEquals("productKey", result1[0]);
  238 + assertEquals("deviceName", result1[1]);
  239 +
  240 + // 测试无效的设备标识
  241 + assertNull(IotHttpMessageParser.parseDeviceKey(null));
  242 + assertNull(IotHttpMessageParser.parseDeviceKey(""));
  243 + assertNull(IotHttpMessageParser.parseDeviceKey("invalid"));
  244 + assertNull(IotHttpMessageParser.parseDeviceKey("product/device/extra"));
  245 + }
  246 +
  247 + @Test
  248 + void testBuildDeviceKey() {
  249 + // 测试构建设备标识
  250 + assertEquals("productKey/deviceName",
  251 + IotHttpMessageParser.buildDeviceKey("productKey", "deviceName"));
  252 +
  253 + // 测试无效参数
  254 + assertNull(IotHttpMessageParser.buildDeviceKey(null, "deviceName"));
  255 + assertNull(IotHttpMessageParser.buildDeviceKey("productKey", null));
  256 + assertNull(IotHttpMessageParser.buildDeviceKey("", "deviceName"));
  257 + assertNull(IotHttpMessageParser.buildDeviceKey("productKey", ""));
  258 + }
  259 +}
0 260 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/util/IotHttpTopicUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.util;
  2 +
  3 +import org.junit.jupiter.api.Test;
  4 +
  5 +import static org.junit.jupiter.api.Assertions.*;
  6 +
  7 +/**
  8 + * {@link IotHttpTopicUtils} 单元测试
  9 + *
  10 + * @author haohao
  11 + */
  12 +class IotHttpTopicUtilsTest {
  13 +
  14 + @Test
  15 + void testBuildAuthPath() {
  16 + assertEquals("/auth", IotHttpTopicUtils.buildAuthPath());
  17 + }
  18 +
  19 + @Test
  20 + void testBuildTopicPath() {
  21 + // 测试正常路径
  22 + assertEquals("/topic/sys/test/device/thing/service/property/set",
  23 + IotHttpTopicUtils.buildTopicPath("/sys/test/device/thing/service/property/set"));
  24 +
  25 + // 测试空路径
  26 + assertNull(IotHttpTopicUtils.buildTopicPath(null));
  27 + assertNull(IotHttpTopicUtils.buildTopicPath(""));
  28 + }
  29 +
  30 + @Test
  31 + void testBuildPropertySetPath() {
  32 + String result = IotHttpTopicUtils.buildPropertySetPath("testProduct", "testDevice");
  33 + assertEquals("/topic/sys/testProduct/testDevice/thing/service/property/set", result);
  34 +
  35 + // 测试无效参数
  36 + assertNull(IotHttpTopicUtils.buildPropertySetPath(null, "testDevice"));
  37 + assertNull(IotHttpTopicUtils.buildPropertySetPath("testProduct", null));
  38 + assertNull(IotHttpTopicUtils.buildPropertySetPath("", "testDevice"));
  39 + assertNull(IotHttpTopicUtils.buildPropertySetPath("testProduct", ""));
  40 + }
  41 +
  42 + @Test
  43 + void testBuildPropertyGetPath() {
  44 + String result = IotHttpTopicUtils.buildPropertyGetPath("testProduct", "testDevice");
  45 + assertEquals("/topic/sys/testProduct/testDevice/thing/service/property/get", result);
  46 + }
  47 +
  48 + @Test
  49 + void testBuildPropertyPostPath() {
  50 + String result = IotHttpTopicUtils.buildPropertyPostPath("testProduct", "testDevice");
  51 + assertEquals("/topic/sys/testProduct/testDevice/thing/event/property/post", result);
  52 + }
  53 +
  54 + @Test
  55 + void testBuildEventPostPath() {
  56 + String result = IotHttpTopicUtils.buildEventPostPath("testProduct", "testDevice", "alarm");
  57 + assertEquals("/topic/sys/testProduct/testDevice/thing/event/alarm/post", result);
  58 +
  59 + // 测试无效参数
  60 + assertNull(IotHttpTopicUtils.buildEventPostPath(null, "testDevice", "alarm"));
  61 + assertNull(IotHttpTopicUtils.buildEventPostPath("testProduct", null, "alarm"));
  62 + assertNull(IotHttpTopicUtils.buildEventPostPath("testProduct", "testDevice", null));
  63 + }
  64 +
  65 + @Test
  66 + void testBuildServiceInvokePath() {
  67 + String result = IotHttpTopicUtils.buildServiceInvokePath("testProduct", "testDevice", "reboot");
  68 + assertEquals("/topic/sys/testProduct/testDevice/thing/service/reboot", result);
  69 +
  70 + // 测试无效参数
  71 + assertNull(IotHttpTopicUtils.buildServiceInvokePath(null, "testDevice", "reboot"));
  72 + assertNull(IotHttpTopicUtils.buildServiceInvokePath("testProduct", null, "reboot"));
  73 + assertNull(IotHttpTopicUtils.buildServiceInvokePath("testProduct", "testDevice", null));
  74 + }
  75 +
  76 + @Test
  77 + void testBuildCustomTopicPath() {
  78 + String result = IotHttpTopicUtils.buildCustomTopicPath("testProduct", "testDevice", "user/get");
  79 + assertEquals("/topic/testProduct/testDevice/user/get", result);
  80 +
  81 + // 测试无效参数
  82 + assertNull(IotHttpTopicUtils.buildCustomTopicPath(null, "testDevice", "user/get"));
  83 + assertNull(IotHttpTopicUtils.buildCustomTopicPath("testProduct", null, "user/get"));
  84 + assertNull(IotHttpTopicUtils.buildCustomTopicPath("testProduct", "testDevice", null));
  85 + }
  86 +
  87 + @Test
  88 + void testExtractActualTopic() {
  89 + // 测试正常提取
  90 + String actualTopic = IotHttpTopicUtils
  91 + .extractActualTopic("/topic/sys/testProduct/testDevice/thing/service/property/set");
  92 + assertEquals("/sys/testProduct/testDevice/thing/service/property/set", actualTopic);
  93 +
  94 + // 测试无效路径
  95 + assertNull(IotHttpTopicUtils.extractActualTopic("/auth"));
  96 + assertNull(IotHttpTopicUtils.extractActualTopic("/unknown/path"));
  97 + assertNull(IotHttpTopicUtils.extractActualTopic(null));
  98 + assertNull(IotHttpTopicUtils.extractActualTopic(""));
  99 + }
  100 +
  101 + @Test
  102 + void testParseProductKeyFromTopic() {
  103 + // 测试系统主题
  104 + assertEquals("testProduct",
  105 + IotHttpTopicUtils.parseProductKeyFromTopic("/sys/testProduct/testDevice/thing/service/property/set"));
  106 +
  107 + // 测试自定义主题
  108 + assertEquals("testProduct", IotHttpTopicUtils.parseProductKeyFromTopic("/testProduct/testDevice/user/get"));
  109 +
  110 + // 测试无效主题
  111 + assertNull(IotHttpTopicUtils.parseProductKeyFromTopic("/sys"));
  112 + assertNull(IotHttpTopicUtils.parseProductKeyFromTopic("/single"));
  113 + assertNull(IotHttpTopicUtils.parseProductKeyFromTopic(""));
  114 + assertNull(IotHttpTopicUtils.parseProductKeyFromTopic(null));
  115 + }
  116 +
  117 + @Test
  118 + void testParseDeviceNameFromTopic() {
  119 + // 测试系统主题
  120 + assertEquals("testDevice",
  121 + IotHttpTopicUtils.parseDeviceNameFromTopic("/sys/testProduct/testDevice/thing/service/property/set"));
  122 +
  123 + // 测试自定义主题
  124 + assertEquals("testDevice", IotHttpTopicUtils.parseDeviceNameFromTopic("/testProduct/testDevice/user/get"));
  125 +
  126 + // 测试无效主题
  127 + assertNull(IotHttpTopicUtils.parseDeviceNameFromTopic("/sys/testProduct"));
  128 + assertNull(IotHttpTopicUtils.parseDeviceNameFromTopic("/testProduct"));
  129 + assertNull(IotHttpTopicUtils.parseDeviceNameFromTopic(""));
  130 + assertNull(IotHttpTopicUtils.parseDeviceNameFromTopic(null));
  131 + }
  132 +
  133 + @Test
  134 + void testIsAuthPath() {
  135 + assertTrue(IotHttpTopicUtils.isAuthPath("/auth"));
  136 + assertFalse(IotHttpTopicUtils.isAuthPath("/topic/test"));
  137 + assertFalse(IotHttpTopicUtils.isAuthPath("/unknown"));
  138 + assertFalse(IotHttpTopicUtils.isAuthPath(null));
  139 + assertFalse(IotHttpTopicUtils.isAuthPath(""));
  140 + }
  141 +
  142 + @Test
  143 + void testIsTopicPath() {
  144 + assertTrue(IotHttpTopicUtils.isTopicPath("/topic/sys/test/device/property"));
  145 + assertTrue(IotHttpTopicUtils.isTopicPath("/topic/test"));
  146 + assertFalse(IotHttpTopicUtils.isTopicPath("/auth"));
  147 + assertFalse(IotHttpTopicUtils.isTopicPath("/unknown"));
  148 + assertFalse(IotHttpTopicUtils.isTopicPath(null));
  149 + assertFalse(IotHttpTopicUtils.isTopicPath(""));
  150 + }
  151 +
  152 + @Test
  153 + void testIsValidHttpPath() {
  154 + assertTrue(IotHttpTopicUtils.isValidHttpPath("/auth"));
  155 + assertTrue(IotHttpTopicUtils.isValidHttpPath("/topic/test"));
  156 + assertFalse(IotHttpTopicUtils.isValidHttpPath("/unknown"));
  157 + assertFalse(IotHttpTopicUtils.isValidHttpPath(null));
  158 + assertFalse(IotHttpTopicUtils.isValidHttpPath(""));
  159 + }
  160 +
  161 + @Test
  162 + void testIsSystemTopic() {
  163 + assertTrue(IotHttpTopicUtils.isSystemTopic("/sys/testProduct/testDevice/thing/service/property/set"));
  164 + assertFalse(IotHttpTopicUtils.isSystemTopic("/testProduct/testDevice/user/get"));
  165 + assertFalse(IotHttpTopicUtils.isSystemTopic("/unknown"));
  166 + assertFalse(IotHttpTopicUtils.isSystemTopic(null));
  167 + assertFalse(IotHttpTopicUtils.isSystemTopic(""));
  168 + }
  169 +
  170 + @Test
  171 + void testBuildReplyPath() {
  172 + // 测试系统主题响应路径
  173 + String requestPath = "/topic/sys/testProduct/testDevice/thing/service/property/set";
  174 + String replyPath = IotHttpTopicUtils.buildReplyPath(requestPath);
  175 + assertEquals("/topic/sys/testProduct/testDevice/thing/service/property/set_reply", replyPath);
  176 +
  177 + // 测试非系统主题
  178 + String customPath = "/topic/testProduct/testDevice/user/get";
  179 + assertNull(IotHttpTopicUtils.buildReplyPath(customPath));
  180 +
  181 + // 测试无效路径
  182 + assertNull(IotHttpTopicUtils.buildReplyPath("/auth"));
  183 + assertNull(IotHttpTopicUtils.buildReplyPath("/unknown"));
  184 + assertNull(IotHttpTopicUtils.buildReplyPath(null));
  185 + }
  186 +}
0 187 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.protocol.util;
  2 +
  3 +import org.junit.jupiter.api.Test;
  4 +
  5 +import static org.junit.jupiter.api.Assertions.assertEquals;
  6 +import static org.junit.jupiter.api.Assertions.assertNull;
  7 +
  8 +/**
  9 + * {@link IotTopicUtils} 单元测试
  10 + *
  11 + * @author haohao
  12 + */
  13 +class IotTopicUtilsTest {
  14 +
  15 + @Test
  16 + void testBuildPropertySetTopic() {
  17 + String topic = IotTopicUtils.buildPropertySetTopic("testProduct", "testDevice");
  18 + assertEquals("/sys/testProduct/testDevice/thing/service/property/set", topic);
  19 + }
  20 +
  21 + @Test
  22 + void testBuildPropertyGetTopic() {
  23 + String topic = IotTopicUtils.buildPropertyGetTopic("testProduct", "testDevice");
  24 + assertEquals("/sys/testProduct/testDevice/thing/service/property/get", topic);
  25 + }
  26 +
  27 + @Test
  28 + void testBuildEventPostTopic() {
  29 + String topic = IotTopicUtils.buildEventPostTopic("testProduct", "testDevice", "temperature");
  30 + assertEquals("/sys/testProduct/testDevice/thing/event/temperature/post", topic);
  31 + }
  32 +
  33 + @Test
  34 + void testGetReplyTopic() {
  35 + String requestTopic = "/sys/testProduct/testDevice/thing/service/property/set";
  36 + String replyTopic = IotTopicUtils.getReplyTopic(requestTopic);
  37 + assertEquals("/sys/testProduct/testDevice/thing/service/property/set_reply", replyTopic);
  38 + }
  39 +
  40 + @Test
  41 + void testParseProductKeyFromTopic() {
  42 + String topic = "/sys/testProduct/testDevice/thing/service/property/set";
  43 + String productKey = IotTopicUtils.parseProductKeyFromTopic(topic);
  44 + assertEquals("testProduct", productKey);
  45 + }
  46 +
  47 + @Test
  48 + void testParseDeviceNameFromTopic() {
  49 + String topic = "/sys/testProduct/testDevice/thing/service/property/set";
  50 + String deviceName = IotTopicUtils.parseDeviceNameFromTopic(topic);
  51 + assertEquals("testDevice", deviceName);
  52 + }
  53 +
  54 + @Test
  55 + void testParseMethodFromTopic() {
  56 + // 测试属性设置
  57 + String topic1 = "/sys/testProduct/testDevice/thing/service/property/set";
  58 + String method1 = IotTopicUtils.parseMethodFromTopic(topic1);
  59 + assertEquals("property.set", method1);
  60 +
  61 + // 测试事件上报
  62 + String topic2 = "/sys/testProduct/testDevice/thing/event/temperature/post";
  63 + String method2 = IotTopicUtils.parseMethodFromTopic(topic2);
  64 + assertEquals("event.temperature.post", method2);
  65 +
  66 + // 测试无效主题
  67 + String method3 = IotTopicUtils.parseMethodFromTopic("/invalid/topic");
  68 + assertNull(method3);
  69 + }
  70 +
  71 + @Test
  72 + void testParseInvalidTopic() {
  73 + // 测试空主题
  74 + assertNull(IotTopicUtils.parseProductKeyFromTopic(""));
  75 + assertNull(IotTopicUtils.parseProductKeyFromTopic(null));
  76 +
  77 + // 测试格式错误的主题
  78 + assertNull(IotTopicUtils.parseProductKeyFromTopic("/invalid"));
  79 + assertNull(IotTopicUtils.parseDeviceNameFromTopic("/sys/product"));
  80 + }
  81 +}
0 82 \ No newline at end of file
... ...