From fbb664026d12065ffa31456daab056bcafcb942d Mon Sep 17 00:00:00 2001
From: haohao <1036606149@qq.com>
Date: Fri, 23 May 2025 23:23:49 +0800
Subject: [PATCH] =?UTF-8?q?feat:=E3=80=90IOT=E3=80=91=E6=96=B0=E5=A2=9E=20?=
=?UTF-8?q?HTTP=20=E5=8D=8F=E8=AE=AE=E6=94=AF=E6=8C=81=E5=8F=8A=E7=9B=B8?=
=?UTF-8?q?=E5=85=B3=E8=A7=A3=E6=9E=90=E5=99=A8=EF=BC=8C=E5=AE=8C=E5=96=84?=
=?UTF-8?q?=E5=8D=8F=E8=AE=AE=E8=BD=AC=E6=8D=A2=E5=99=A8=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../yudao-module-iot-protocol/pom.xml | 12 +
.../config/IotProtocolAutoConfiguration.java | 52 ++-
.../protocol/constants/IotHttpConstants.java | 166 +++++++++
.../protocol/constants/IotLogConstants.java | 91 +++++
.../protocol/constants/IotTopicConstants.java | 87 ++++-
.../convert/IotProtocolConverter.java | 48 +++
.../impl/DefaultIotProtocolConverter.java | 131 +++++++
.../enums/IotMessageDirectionEnum.java | 49 +++
.../protocol/enums/IotMessageTypeEnum.java | 140 +++++++
.../protocol/enums/IotProtocolTypeEnum.java | 79 ++++
.../iot/protocol/message/IotAlinkMessage.java | 2 +-
.../message/impl/IotAlinkMessageParser.java | 2 +-
.../message/impl/IotHttpMessageParser.java | 348 ++++++++++++++++++
.../iot/protocol/util/IotHttpTopicUtils.java | 279 ++++++++++++++
.../iot/protocol/util/IotTopicParser.java | 237 ++++++++++++
.../iot/protocol/util/IotTopicUtils.java | 14 +-
.../IotProtocolAutoConfigurationTest.java | 71 ++++
.../example/AliyunHttpProtocolExample.java | 166 +++++++++
.../impl/IotHttpMessageParserTest.java | 259 +++++++++++++
.../protocol/util/IotHttpTopicUtilsTest.java | 186 ++++++++++
.../iot/protocol/util/IotTopicUtilsTest.java | 81 ++++
21 files changed, 2489 insertions(+), 11 deletions(-)
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotHttpConstants.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotLogConstants.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/IotProtocolConverter.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/impl/DefaultIotProtocolConverter.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/enums/IotMessageDirectionEnum.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/enums/IotMessageTypeEnum.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/enums/IotProtocolTypeEnum.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParser.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotHttpTopicUtils.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicParser.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/config/IotProtocolAutoConfigurationTest.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/example/AliyunHttpProtocolExample.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParserTest.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/util/IotHttpTopicUtilsTest.java
create mode 100644 yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicUtilsTest.java
diff --git a/yudao-module-iot/yudao-module-iot-protocol/pom.xml b/yudao-module-iot/yudao-module-iot-protocol/pom.xml
index 16c84608c6..3a5a9e1158 100644
--- a/yudao-module-iot/yudao-module-iot-protocol/pom.xml
+++ b/yudao-module-iot/yudao-module-iot-protocol/pom.xml
@@ -53,6 +53,18 @@
+ * 如果用户没有自定义协议转换器,则使用默认实现 + * 默认会注册 Alink 和 HTTP 协议解析器 + * + * @param iotAlinkMessageParser Alink 协议解析器 + * @param iotHttpMessageParser HTTP 协议解析器 + * @return 默认协议转换器 + */ + @Bean + @ConditionalOnMissingBean + public IotProtocolConverter iotProtocolConverter(IotMessageParser iotAlinkMessageParser, + IotMessageParser iotHttpMessageParser) { + DefaultIotProtocolConverter converter = new DefaultIotProtocolConverter(); + + // 注册 HTTP 协议解析器 + converter.registerParser(IotProtocolTypeEnum.HTTP.getCode(), iotHttpMessageParser); + + // 注意:Alink 协议解析器已经在 DefaultIotProtocolConverter 构造函数中注册 + // 如果需要使用自定义的 Alink 解析器实例,可以重新注册 + // converter.registerParser(IotProtocolTypeEnum.ALINK.getCode(), + // iotAlinkMessageParser); + + return converter; + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotHttpConstants.java b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotHttpConstants.java new file mode 100644 index 0000000000..aeb4b3240f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotHttpConstants.java @@ -0,0 +1,166 @@ +package cn.iocoder.yudao.module.iot.protocol.constants; + +/** + * IoT HTTP 协议常量类 + *
+ * 用于统一管理 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"; + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotLogConstants.java b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotLogConstants.java new file mode 100644 index 0000000000..05b7179870 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotLogConstants.java @@ -0,0 +1,91 @@ +package cn.iocoder.yudao.module.iot.protocol.constants; + +/** + * IoT 协议日志消息常量类 + *
+ * 用于统一管理协议模块中的日志消息常量 + * + * @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={}"; + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotTopicConstants.java b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotTopicConstants.java index fd0ebb0656..59453518cd 100644 --- a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/constants/IotTopicConstants.java +++ b/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 { */ public static final String REPLY_SUFFIX = "_reply"; -} \ No newline at end of file + /** + * 方法名前缀常量 + */ + 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"; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/IotProtocolConverter.java b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/IotProtocolConverter.java new file mode 100644 index 0000000000..f659edb7b4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/IotProtocolConverter.java @@ -0,0 +1,48 @@ +package cn.iocoder.yudao.module.iot.protocol.convert; + +import cn.iocoder.yudao.module.iot.protocol.message.IotAlinkMessage; +import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse; + +/** + * IoT 协议转换器接口 + *
+ * 用于在不同协议之间进行转换 + * + * @author haohao + */ +public interface IotProtocolConverter { + + /** + * 将字节数组转换为标准消息 + * + * @param topic 主题 + * @param payload 消息负载 + * @param protocol 协议类型 + * @return 标准消息对象,转换失败返回 null + */ + IotAlinkMessage 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(); +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/impl/DefaultIotProtocolConverter.java b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/impl/DefaultIotProtocolConverter.java new file mode 100644 index 0000000000..e5d4703ff2 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/convert/impl/DefaultIotProtocolConverter.java @@ -0,0 +1,131 @@ +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.IotAlinkMessage; +import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser; +import cn.iocoder.yudao.module.iot.protocol.message.IotStandardResponse; +import cn.iocoder.yudao.module.iot.protocol.message.impl.IotAlinkMessageParser; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * 默认 IoT 协议转换器实现 + *
+ * 支持多种协议的转换,可以通过注册不同的消息解析器来扩展支持的协议
+ *
+ * @author haohao
+ */
+@Slf4j
+public class DefaultIotProtocolConverter implements IotProtocolConverter {
+
+ /**
+ * 消息解析器映射
+ * Key: 协议类型,Value: 消息解析器
+ */
+ private final Map
* 基于阿里云 Alink 协议规范实现的标准消息格式
- * @see 阿里云物联网 —— Alink 协议
*
* @author haohao
+ * @see 阿里云物联网 —— Alink 协议
*/
@Data
@Builder
diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotAlinkMessageParser.java b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotAlinkMessageParser.java
index 1fdb3e4222..745c653120 100644
--- a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotAlinkMessageParser.java
+++ b/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 {
JSONObject json = JSONUtil.parseObj(message);
String id = json.getStr("id");
String method = json.getStr("method");
-
+
if (StrUtil.isBlank(method)) {
// 尝试从 topic 中解析方法
method = IotTopicUtils.parseMethodFromTopic(topic);
diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParser.java b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParser.java
new file mode 100644
index 0000000000..10b4c49d7c
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParser.java
@@ -0,0 +1,348 @@
+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.IotAlinkMessage;
+import cn.iocoder.yudao.module.iot.protocol.message.IotMessageParser;
+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 协议消息解析器实现
+ *
+ * 参考阿里云IoT平台HTTPS协议标准,支持设备认证和数据上报两种消息类型:
+ *
+ * 1. 设备认证消息格式:
+ *
+ *
+ * 2. 数据上报消息格式:
+ *
+ *
+ * 参考阿里云IoT平台HTTPS协议标准,支持以下路径格式:
+ * 1. 设备认证:/auth
+ * 2. 数据上报:/topic/${actualTopic}
+ *
+ * 其中 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;
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicParser.java b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicParser.java
new file mode 100644
index 0000000000..05873d2bdb
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicParser.java
@@ -0,0 +1,237 @@
+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 主题解析器
+ *
+ * 用于解析各种格式的 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);
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicUtils.java b/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicUtils.java
index 6520ce375d..6bd447e5a9 100644
--- a/yudao-module-iot/yudao-module-iot-protocol/src/main/java/cn/iocoder/yudao/module/iot/protocol/util/IotTopicUtils.java
+++ b/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 {
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];
}
@@ -146,12 +146,12 @@ public class IotTopicUtils {
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];
}
@@ -166,19 +166,19 @@ public class IotTopicUtils {
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;
}
}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/config/IotProtocolAutoConfigurationTest.java b/yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/config/IotProtocolAutoConfigurationTest.java
new file mode 100644
index 0000000000..b27cc5f0db
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/config/IotProtocolAutoConfigurationTest.java
@@ -0,0 +1,71 @@
+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.IotAlinkMessageParser;
+import cn.iocoder.yudao.module.iot.protocol.message.impl.IotHttpMessageParser;
+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 testIotAlinkMessageParser() {
+ // 测试 Alink 协议解析器 Bean 创建
+ IotMessageParser parser = configuration.iotAlinkMessageParser();
+
+ assertNotNull(parser);
+ assertInstanceOf(IotAlinkMessageParser.class, parser);
+ }
+
+ @Test
+ void testIotHttpMessageParser() {
+ // 测试 HTTP 协议解析器 Bean 创建
+ IotMessageParser parser = configuration.iotHttpMessageParser();
+
+ assertNotNull(parser);
+ assertInstanceOf(IotHttpMessageParser.class, parser);
+ }
+
+ @Test
+ void testIotProtocolConverter() {
+ // 创建解析器实例
+ IotMessageParser alinkParser = configuration.iotAlinkMessageParser();
+ IotMessageParser httpParser = configuration.iotHttpMessageParser();
+
+ // 测试协议转换器 Bean 创建
+ IotProtocolConverter converter = configuration.iotProtocolConverter(alinkParser, httpParser);
+
+ assertNotNull(converter);
+
+ // 验证支持的协议
+ assertTrue(converter.supportsProtocol(IotProtocolTypeEnum.ALINK.getCode()));
+ assertTrue(converter.supportsProtocol(IotProtocolTypeEnum.HTTP.getCode()));
+
+ // 验证支持的协议数量
+ String[] supportedProtocols = converter.getSupportedProtocols();
+ assertEquals(2, supportedProtocols.length);
+ }
+
+ @Test
+ void testBeanNameConstants() {
+ // 测试 Bean 名称常量定义
+ assertEquals("iotAlinkMessageParser", IotProtocolAutoConfiguration.IOT_ALINK_MESSAGE_PARSER_BEAN_NAME);
+ assertEquals("iotHttpMessageParser", IotProtocolAutoConfiguration.IOT_HTTP_MESSAGE_PARSER_BEAN_NAME);
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/example/AliyunHttpProtocolExample.java b/yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/example/AliyunHttpProtocolExample.java
new file mode 100644
index 0000000000..a1c1dae562
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/example/AliyunHttpProtocolExample.java
@@ -0,0 +1,166 @@
+package cn.iocoder.yudao.module.iot.protocol.example;
+
+import cn.hutool.json.JSONObject;
+import cn.iocoder.yudao.module.iot.protocol.util.IotHttpTopicUtils;
+
+/**
+ * 阿里云IoT平台HTTPS协议示例
+ *
+ * 参考阿里云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"));
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParserTest.java b/yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParserTest.java
new file mode 100644
index 0000000000..9241d9b20d
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-protocol/src/test/java/cn/iocoder/yudao/module/iot/protocol/message/impl/IotHttpMessageParserTest.java
@@ -0,0 +1,259 @@
+package cn.iocoder.yudao.module.iot.protocol.message.impl;
+
+import cn.hutool.json.JSONObject;
+import cn.iocoder.yudao.module.iot.protocol.message.IotAlinkMessage;
+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} 单元测试
+ *
+ * 测试阿里云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);
+
+ // 解析消息
+ IotAlinkMessage result = parser.parse(topic, payload);
+
+ // 验证结果
+ assertNotNull(result);
+ assertNotNull(result.getId());
+ assertEquals("device.auth", result.getMethod());
+ assertEquals("default", result.getVersion());
+ assertNotNull(result.getParams());
+
+ Map
+ * POST /auth HTTP/1.1
+ * Content-Type: application/json
+ * {
+ * "productKey": "a1AbC***",
+ * "deviceName": "device01",
+ * "clientId": "device01_001",
+ * "timestamp": "1501668289957",
+ * "sign": "xxxxx",
+ * "signmethod": "hmacsha1",
+ * "version": "default"
+ * }
+ *
+ *
+ * POST /topic/${topic} HTTP/1.1
+ * password: ${token}
+ * Content-Type: application/octet-stream
+ * ${payload}
+ *
+ *
+ * @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 IotAlinkMessage 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 IotAlinkMessage 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