map, String key, BigDecimal defaultValue) {
+ if (map == null) {
+ return defaultValue;
+ }
+ Object value = map.get(key);
+ if (value == null) {
+ return defaultValue;
+ }
+ if (value instanceof BigDecimal) {
+ return (BigDecimal) value;
+ }
+ if (value instanceof Number) {
+ return BigDecimal.valueOf(((Number) value).doubleValue());
+ }
+ if (value instanceof String) {
+ try {
+ return new BigDecimal((String) value);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+ return defaultValue;
+ }
+
}
diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
index e35cd9b43..7711ae0d8 100644
--- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
+++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
@@ -229,4 +229,53 @@ public class JsonUtils {
return JSONUtil.isTypeJSONObject(str);
}
+ /**
+ * 将 Object 转换为目标类型
+ *
+ * 避免先转 jsonString 再 parseObject 的性能损耗
+ *
+ * @param obj 源对象(可以是 Map、POJO 等)
+ * @param clazz 目标类型
+ * @return 转换后的对象
+ */
+ public static T convertObject(Object obj, Class clazz) {
+ if (obj == null) {
+ return null;
+ }
+ if (clazz.isInstance(obj)) {
+ return clazz.cast(obj);
+ }
+ return objectMapper.convertValue(obj, clazz);
+ }
+
+ /**
+ * 将 Object 转换为目标类型(支持泛型)
+ *
+ * @param obj 源对象
+ * @param typeReference 目标类型引用
+ * @return 转换后的对象
+ */
+ public static T convertObject(Object obj, TypeReference typeReference) {
+ if (obj == null) {
+ return null;
+ }
+ return objectMapper.convertValue(obj, typeReference);
+ }
+
+ /**
+ * 将 Object 转换为 List 类型
+ *
+ * 避免先转 jsonString 再 parseArray 的性能损耗
+ *
+ * @param obj 源对象(可以是 List、数组等)
+ * @param clazz 目标元素类型
+ * @return 转换后的 List
+ */
+ public static List convertList(Object obj, Class clazz) {
+ if (obj == null) {
+ return new ArrayList<>();
+ }
+ return objectMapper.convertValue(obj, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+ }
+
}
diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java
index 8b5a0fcfc..aed2f02df 100644
--- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java
+++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java
@@ -15,6 +15,7 @@ import java.util.function.Consumer;
*
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
* 2. SFunction column + 泛型:支持任意类字段(主表、子表、三表),推荐写法, 让编译器自动推断 S 类型
+ *
* @param 数据类型
*/
public class MPJLambdaWrapperX extends MPJLambdaWrapper {
@@ -122,6 +123,12 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper {
return this;
}
+ @Override
+ public MPJLambdaWrapperX orderByAsc(SFunction column) {
+ super.orderByAsc(true, column);
+ return this;
+ }
+
@Override
public MPJLambdaWrapperX last(String lastSql) {
super.last(lastSql);
diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
index e79437b43..8297dbce1 100644
--- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
+++ b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java
@@ -15,7 +15,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.core.task.AsyncListenableTaskExecutor;
+import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.List;
@@ -30,12 +30,12 @@ public class BpmFlowableConfiguration {
/**
* 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean
- *
+ *
* 如果不创建,会导致项目启动时,Flowable 报错的问题
*/
@Bean(name = "applicationTaskExecutor")
@ConditionalOnMissingBean(name = "applicationTaskExecutor")
- public AsyncListenableTaskExecutor taskExecutor() {
+ public AsyncTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(8);
diff --git a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
index 75582a054..8848f8183 100644
--- a/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
+++ b/yudao-module-bpm/yudao-module-bpm-server/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java
@@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import lombok.Setter;
import org.flowable.bpmn.model.*;
+import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
@@ -90,4 +91,21 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
}
+ // ========== 屏蔽解析器覆写 ==========
+
+ @Override
+ public void setCollectionExpression(Expression collectionExpression) {
+ // 保持自定义变量名,忽略解析器写入的 collection 表达式
+ }
+
+ @Override
+ public void setCollectionVariable(String collectionVariable) {
+ // 保持自定义变量名,忽略解析器写入的 collection 变量名
+ }
+
+ @Override
+ public void setCollectionElementVariable(String collectionElementVariable) {
+ // 保持自定义变量名,忽略解析器写入的单元素变量名
+ }
+
}
diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java
index 45fc4df13..1b3dc0f96 100644
--- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java
+++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java
@@ -70,7 +70,7 @@ public class ApiAccessLogRespVO {
@Schema(description = "操作分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "操作分类", converter = DictConvert.class)
- @DictFormat(cn.iocoder.yudao.module.infra.enums.DictTypeConstants.OPERATE_TYPE)
+ @DictFormat(DictTypeConstants.OPERATE_TYPE)
private Integer operateType;
@Schema(description = "开始请求时间", requiredMode = Schema.RequiredMode.REQUIRED)
diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
index 025d61390..3679dbf1c 100644
--- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
+++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
@@ -26,13 +26,26 @@ public interface ErrorCodeConstants {
// ========== 设备 1-050-003-000 ============
ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在");
ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一");
- ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "有子设备,不允许删除");
+ ErrorCode DEVICE_GATEWAY_HAS_SUB = new ErrorCode(1_050_003_002, "网关设备存在已绑定的子设备,不允许删除");
ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在");
ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在");
ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备");
ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!");
ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关");
ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
+ ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关");
+ ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑");
+ // 拓扑管理相关错误码 1-050-003-100
+ ErrorCode DEVICE_TOPO_PARAMS_INVALID = new ErrorCode(1_050_003_100, "拓扑管理参数无效");
+ ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效");
+ ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败");
+ ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关");
+ // 设备注册相关错误码 1-050-003-200
+ ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
+ ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
+ ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
+ ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败");
+ ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册");
// ========== 产品分类 1-050-004-000 ==========
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java
deleted file mode 100644
index e9dbe2f65..000000000
--- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageIdentifierEnum.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package cn.iocoder.yudao.module.iot.enums.device;
-
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-
-// TODO @芋艿:需要添加对应的 DTO,以及上下行的链路,网关、网关服务、设备等
-/**
- * IoT 设备消息标识符枚举
- */
-@Deprecated
-@Getter
-@RequiredArgsConstructor
-public enum IotDeviceMessageIdentifierEnum {
-
- PROPERTY_GET("get"), // 下行
- PROPERTY_SET("set"), // 下行
- PROPERTY_REPORT("report"), // 上行
-
- STATE_ONLINE("online"), // 上行
- STATE_OFFLINE("offline"), // 上行
-
- CONFIG_GET("get"), // 上行 TODO 芋艿:【讨论】暂时没有上行的场景
- CONFIG_SET("set"), // 下行
-
- SERVICE_INVOKE("${identifier}"), // 下行
- SERVICE_REPLY_SUFFIX("_reply"), // 芋艿:TODO 芋艿:【讨论】上行 or 下行
-
- OTA_UPGRADE("upgrade"), // 下行
- OTA_PULL("pull"), // 上行
- OTA_PROGRESS("progress"), // 上行
- OTA_REPORT("report"), // 上行
-
- REGISTER_REGISTER("register"), // 上行
- REGISTER_REGISTER_SUB("register_sub"), // 上行
- REGISTER_UNREGISTER_SUB("unregister_sub"), // 下行
-
- TOPOLOGY_ADD("topology_add"), // 下行;
- ;
-
- /**
- * 标志符
- */
- private final String identifier;
-
-}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java
deleted file mode 100644
index 9131210ab..000000000
--- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/device/IotDeviceMessageTypeEnum.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package cn.iocoder.yudao.module.iot.enums.device;
-
-import cn.iocoder.yudao.framework.common.core.ArrayValuable;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-
-import java.util.Arrays;
-
-/**
- * IoT 设备消息类型枚举
- */
-@Deprecated
-@Getter
-@RequiredArgsConstructor
-public enum IotDeviceMessageTypeEnum implements ArrayValuable {
-
- STATE("state"), // 设备状态
- PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
- EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
- SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
- CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置
- OTA("ota"), // 设备 OTA:可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级
- REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册
- TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑
-
- public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new);
-
- /**
- * 属性
- */
- private final String type;
-
- @Override
- public String[] array() {
- return ARRAYS;
- }
-
-}
diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotLocationTypeEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotLocationTypeEnum.java
deleted file mode 100644
index 11989ec71..000000000
--- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/product/IotLocationTypeEnum.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package cn.iocoder.yudao.module.iot.enums.product;
-
-import cn.iocoder.yudao.framework.common.core.ArrayValuable;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-import java.util.Arrays;
-
-/**
- * IoT 定位方式枚举类
- *
- * @author alwayssuper
- */
-@AllArgsConstructor
-@Getter
-public enum IotLocationTypeEnum implements ArrayValuable {
-
- IP(1, "IP 定位"),
- DEVICE(2, "设备上报"),
- MANUAL(3, "手动定位");
-
- public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotLocationTypeEnum::getType).toArray(Integer[]::new);
-
- /**
- * 类型
- */
- private final Integer type;
- /**
- * 描述
- */
- private final String description;
-
- @Override
- public Integer[] array() {
- return ARRAYS;
- }
-
-}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
index 29d540e73..cc0cb071a 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/IotDeviceCommonApi.java
@@ -4,6 +4,12 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceGetReqDTO;
import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotSubDeviceRegisterFullReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO;
+
+import java.util.List;
/**
* IoT 设备通用 API
@@ -28,4 +34,20 @@ public interface IotDeviceCommonApi {
*/
CommonResult getDevice(IotDeviceGetReqDTO infoReqDTO);
+ /**
+ * 直连/网关设备动态注册(一型一密)
+ *
+ * @param reqDTO 动态注册请求
+ * @return 注册结果(包含 DeviceSecret)
+ */
+ CommonResult registerDevice(IotDeviceRegisterReqDTO reqDTO);
+
+ /**
+ * 网关子设备动态注册(网关代理转发)
+ *
+ * @param reqDTO 子设备注册请求(包含网关标识和子设备列表)
+ * @return 注册结果列表
+ */
+ CommonResult> registerSubDevices(IotSubDeviceRegisterFullReqDTO reqDTO);
+
}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
index 9e62a2fc0..2f25fb496 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotDeviceAuthReqDTO.java
@@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.iot.core.biz.dto;
import jakarta.validation.constraints.NotEmpty;
+import lombok.AllArgsConstructor;
import lombok.Data;
+import lombok.NoArgsConstructor;
/**
* IoT 设备认证 Request DTO
@@ -9,6 +11,8 @@ import lombok.Data;
* @author 芋道源码
*/
@Data
+@NoArgsConstructor
+@AllArgsConstructor
public class IotDeviceAuthReqDTO {
/**
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java
new file mode 100644
index 000000000..76bf5ffb3
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/biz/dto/IotSubDeviceRegisterFullReqDTO.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.iot.core.biz.dto;
+
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * IoT 子设备动态注册 Request DTO
+ *
+ * 额外包含了网关设备的标识信息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class IotSubDeviceRegisterFullReqDTO {
+
+ /**
+ * 网关设备 ProductKey
+ */
+ @NotEmpty(message = "网关产品标识不能为空")
+ private String gatewayProductKey;
+
+ /**
+ * 网关设备 DeviceName
+ */
+ @NotEmpty(message = "网关设备名称不能为空")
+ private String gatewayDeviceName;
+
+ /**
+ * 子设备注册列表
+ */
+ @NotNull(message = "子设备注册列表不能为空")
+ private List subDevices;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
index e62b78e24..d98003284 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
@@ -24,12 +24,28 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable {
// TODO 芋艿:要不要加个 ping 消息;
+ // ========== 拓扑管理 ==========
+ // 可参考:https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships
+
+ TOPO_ADD("thing.topo.add", "添加拓扑关系", true),
+ TOPO_DELETE("thing.topo.delete", "删除拓扑关系", true),
+ TOPO_GET("thing.topo.get", "获取拓扑关系", true),
+ TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false),
+
+ // ========== 设备注册 ==========
+ // 可参考:https://help.aliyun.com/zh/iot/user-guide/unique-certificate-per-product-verification
+
+ DEVICE_REGISTER("thing.auth.register", "设备动态注册", true),
+ SUB_DEVICE_REGISTER("thing.auth.register.sub", "子设备动态注册", true),
+
// ========== 设备属性 ==========
// 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
PROPERTY_POST("thing.property.post", "属性上报", true),
PROPERTY_SET("thing.property.set", "属性设置", false),
+ PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有
+
// ========== 设备事件 ==========
// 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services
@@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable {
OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false),
OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true),
+
;
public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod)
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageTypeEnum.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageTypeEnum.java
deleted file mode 100644
index e2fe8be20..000000000
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageTypeEnum.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package cn.iocoder.yudao.module.iot.core.enums;
-
-import cn.iocoder.yudao.framework.common.core.ArrayValuable;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-
-import java.util.Arrays;
-
-/**
- * IoT 设备消息类型枚举
- */
-@Getter
-@RequiredArgsConstructor
-public enum IotDeviceMessageTypeEnum implements ArrayValuable {
-
- STATE("state"), // 设备状态
-// PROPERTY("property"), // 设备属性:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
- EVENT("event"), // 设备事件:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
- SERVICE("service"), // 设备服务:可参考 https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 设备属性、事件、服务
- CONFIG("config"), // 设备配置:可参考 https://help.aliyun.com/zh/iot/user-guide/remote-configuration-1 远程配置
- OTA("ota"), // 设备 OTA:可参考 https://help.aliyun.com/zh/iot/user-guide/ota-update OTA 升级
- REGISTER("register"), // 设备注册:可参考 https://help.aliyun.com/zh/iot/user-guide/register-devices 设备身份注册
- TOPOLOGY("topology"),; // 设备拓扑:可参考 https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships 设备拓扑
-
- public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageTypeEnum::getType).toArray(String[]::new);
-
- /**
- * 属性
- */
- private final String type;
-
- @Override
- public String[] array() {
- return ARRAYS;
- }
-
-}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java
new file mode 100644
index 000000000..198702671
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/IotDeviceIdentity.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.iot.core.topic;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * IoT 设备标识
+ *
+ * 用于标识一个设备的基本信息(productKey + deviceName)
+ *
+ * @author 芋道源码
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class IotDeviceIdentity {
+
+ /**
+ * 产品标识
+ */
+ @NotEmpty(message = "产品标识不能为空")
+ private String productKey;
+
+ /**
+ * 设备名称
+ */
+ @NotEmpty(message = "设备名称不能为空")
+ private String deviceName;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
new file mode 100644
index 000000000..b8db15f18
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterReqDTO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.core.topic.auth;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+/**
+ * IoT 设备动态注册 Request DTO
+ *
+ * 用于直连设备/网关的一型一密动态注册:使用 productSecret 验证,返回 deviceSecret
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 一型一密
+ */
+@Data
+public class IotDeviceRegisterReqDTO {
+
+ /**
+ * 产品标识
+ */
+ @NotEmpty(message = "产品标识不能为空")
+ private String productKey;
+
+ /**
+ * 设备名称
+ */
+ @NotEmpty(message = "设备名称不能为空")
+ private String deviceName;
+
+ /**
+ * 产品密钥
+ */
+ @NotEmpty(message = "产品密钥不能为空")
+ private String productSecret;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java
new file mode 100644
index 000000000..707f79890
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotDeviceRegisterRespDTO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.core.topic.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * IoT 设备动态注册 Response DTO
+ *
+ * 用于直连设备/网关的一型一密动态注册响应
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 一型一密
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class IotDeviceRegisterRespDTO {
+
+ /**
+ * 产品标识
+ */
+ private String productKey;
+
+ /**
+ * 设备名称
+ */
+ private String deviceName;
+
+ /**
+ * 设备密钥
+ */
+ private String deviceSecret;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
new file mode 100644
index 000000000..cf34a1db2
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.iot.core.topic.auth;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+/**
+ * IoT 子设备动态注册 Request DTO
+ *
+ * 用于 thing.auth.register.sub 消息的 params 数组元素
+ *
+ * 特殊:网关子设备的动态注册,必须已经创建好该网关子设备(不然哪来的 {@link #deviceName} 字段)。更多的好处,是设备不用提前烧录 deviceSecret 密钥。
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 动态注册子设备
+ */
+@Data
+public class IotSubDeviceRegisterReqDTO {
+
+ /**
+ * 子设备 ProductKey
+ */
+ @NotEmpty(message = "产品标识不能为空")
+ private String productKey;
+
+ /**
+ * 子设备 DeviceName
+ */
+ @NotEmpty(message = "设备名称不能为空")
+ private String deviceName;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java
new file mode 100644
index 000000000..a45f14def
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java
@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.iot.core.topic.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * IoT 子设备动态注册 Response DTO
+ *
+ * 用于 thing.auth.register.sub 响应的设备信息
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 动态注册子设备
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class IotSubDeviceRegisterRespDTO {
+
+ /**
+ * 子设备 ProductKey
+ */
+ private String productKey;
+
+ /**
+ * 子设备 DeviceName
+ */
+ private String deviceName;
+
+ /**
+ * 分配的 DeviceSecret
+ */
+ private String deviceSecret;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java
new file mode 100644
index 000000000..3b6a7a7d4
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/event/IotDeviceEventPostReqDTO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.iot.core.topic.event;
+
+import lombok.Data;
+
+/**
+ * IoT 设备事件上报 Request DTO
+ *
+ * 用于 thing.event.post 消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 设备上报事件
+ */
+@Data
+public class IotDeviceEventPostReqDTO {
+
+ /**
+ * 事件标识符
+ */
+ private String identifier;
+
+ /**
+ * 事件输出参数
+ */
+ private Object value;
+
+ /**
+ * 上报时间(毫秒时间戳,可选)
+ */
+ private Long time;
+
+ /**
+ * 创建事件上报 DTO
+ *
+ * @param identifier 事件标识符
+ * @param value 事件值
+ * @return DTO 对象
+ */
+ public static IotDeviceEventPostReqDTO of(String identifier, Object value) {
+ return of(identifier, value, null);
+ }
+
+ /**
+ * 创建事件上报 DTO(带时间)
+ *
+ * @param identifier 事件标识符
+ * @param value 事件值
+ * @param time 上报时间
+ * @return DTO 对象
+ */
+ public static IotDeviceEventPostReqDTO of(String identifier, Object value, Long time) {
+ return new IotDeviceEventPostReqDTO().setIdentifier(identifier).setValue(value).setTime(time);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java
new file mode 100644
index 000000000..bc97dd944
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * IoT Topic 消息体 DTO 定义
+ *
+ * 定义设备与平台通信的消息体结构,遵循(参考)阿里云 Alink 协议规范
+ *
+ * @see 阿里云 Alink 协议
+ */
+package cn.iocoder.yudao.module.iot.core.topic;
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java
new file mode 100644
index 000000000..24494984e
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java
@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.module.iot.core.topic.property;
+
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * IoT 设备属性批量上报 Request DTO
+ *
+ * 用于 thing.event.property.pack.post 消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 网关批量上报数据
+ */
+@Data
+public class IotDevicePropertyPackPostReqDTO {
+
+ /**
+ * 网关自身属性
+ *
+ * key: 属性标识符
+ * value: 属性值
+ */
+ private Map properties;
+
+ /**
+ * 网关自身事件
+ *
+ * key: 事件标识符
+ * value: 事件值对象(包含 value 和 time)
+ */
+ private Map events;
+
+ /**
+ * 子设备数据列表
+ */
+ private List subDevices;
+
+ /**
+ * 事件值对象
+ */
+ @Data
+ public static class EventValue {
+
+ /**
+ * 事件参数
+ */
+ private Object value;
+
+ /**
+ * 上报时间(毫秒时间戳)
+ */
+ private Long time;
+
+ }
+
+ /**
+ * 子设备数据
+ */
+ @Data
+ public static class SubDeviceData {
+
+ /**
+ * 子设备标识
+ */
+ private IotDeviceIdentity identity;
+
+ /**
+ * 子设备属性
+ *
+ * key: 属性标识符
+ * value: 属性值
+ */
+ private Map properties;
+
+ /**
+ * 子设备事件
+ *
+ * key: 事件标识符
+ * value: 事件值对象(包含 value 和 time)
+ */
+ private Map events;
+
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java
new file mode 100644
index 000000000..2e537442d
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPostReqDTO.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.iot.core.topic.property;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * IoT 设备属性上报 Request DTO
+ *
+ * 用于 thing.property.post 消息的 params 参数
+ *
+ * 本质是一个 Map,key 为属性标识符,value 为属性值
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 设备上报属性
+ */
+public class IotDevicePropertyPostReqDTO extends HashMap {
+
+ public IotDevicePropertyPostReqDTO() {
+ super();
+ }
+
+ public IotDevicePropertyPostReqDTO(Map properties) {
+ super(properties);
+ }
+
+ /**
+ * 创建属性上报 DTO
+ *
+ * @param properties 属性数据
+ * @return DTO 对象
+ */
+ public static IotDevicePropertyPostReqDTO of(Map properties) {
+ return new IotDevicePropertyPostReqDTO(properties);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
new file mode 100644
index 000000000..97ec33200
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * IoT 设备拓扑添加 Request DTO
+ *
+ * 用于 thing.topo.add 消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 添加拓扑关系
+ */
+@Data
+public class IotDeviceTopoAddReqDTO {
+
+ /**
+ * 子设备认证信息列表
+ *
+ * 复用 {@link IotDeviceAuthReqDTO},包含 clientId、username、password
+ */
+ @NotEmpty(message = "子设备认证信息列表不能为空")
+ private List subDevices;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java
new file mode 100644
index 000000000..0198206fe
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoChangeReqDTO.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * IoT 设备拓扑关系变更通知 Request DTO
+ *
+ * 用于 thing.topo.change 下行消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 通知网关拓扑关系变化
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class IotDeviceTopoChangeReqDTO {
+
+ public static final Integer STATUS_CREATE = 0;
+ public static final Integer STATUS_DELETE = 1;
+
+ /**
+ * 拓扑关系状态
+ */
+ private Integer status;
+
+ /**
+ * 子设备列表
+ */
+ private List subList;
+
+ public static IotDeviceTopoChangeReqDTO ofCreate(List subList) {
+ return new IotDeviceTopoChangeReqDTO(STATUS_CREATE, subList);
+ }
+
+ public static IotDeviceTopoChangeReqDTO ofDelete(List subList) {
+ return new IotDeviceTopoChangeReqDTO(STATUS_DELETE, subList);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java
new file mode 100644
index 000000000..71ee2bb8b
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * IoT 设备拓扑删除 Request DTO
+ *
+ * 用于 thing.topo.delete 消息的 params 参数
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 删除拓扑关系
+ */
+@Data
+public class IotDeviceTopoDeleteReqDTO {
+
+ /**
+ * 子设备标识列表
+ */
+ @Valid
+ @NotEmpty(message = "子设备标识列表不能为空")
+ private List subDevices;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java
new file mode 100644
index 000000000..7a61af0a5
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetReqDTO.java
@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import lombok.Data;
+
+/**
+ * IoT 设备拓扑关系获取 Request DTO
+ *
+ * 用于 thing.topo.get 请求的 params 参数(目前为空,预留扩展)
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 获取拓扑关系
+ */
+@Data
+public class IotDeviceTopoGetReqDTO {
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java
new file mode 100644
index 000000000..69c9b1555
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoGetRespDTO.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.iot.core.topic.topo;
+
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * IoT 设备拓扑关系获取 Response DTO
+ *
+ * 用于 thing.topo.get 响应
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 获取拓扑关系
+ */
+@Data
+public class IotDeviceTopoGetRespDTO {
+
+ /**
+ * 子设备列表
+ */
+ private List subDevices;
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
index 2bc488007..609d0a60a 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceAuthUtils.java
@@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.iot.core.util;
+import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
/**
* IoT 设备【认证】的工具类,参考阿里云
@@ -13,73 +13,40 @@ import lombok.NoArgsConstructor;
*/
public class IotDeviceAuthUtils {
- /**
- * 认证信息
- */
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public static class AuthInfo {
-
- /**
- * 客户端 ID
- */
- private String clientId;
-
- /**
- * 用户名
- */
- private String username;
-
- /**
- * 密码
- */
- private String password;
-
- }
-
- /**
- * 设备信息
- */
- @Data
- public static class DeviceInfo {
-
- private String productKey;
-
- private String deviceName;
-
- }
-
- public static AuthInfo getAuthInfo(String productKey, String deviceName, String deviceSecret) {
+ public static IotDeviceAuthReqDTO getAuthInfo(String productKey, String deviceName, String deviceSecret) {
String clientId = buildClientId(productKey, deviceName);
String username = buildUsername(productKey, deviceName);
- String content = "clientId" + clientId +
- "deviceName" + deviceName +
- "deviceSecret" + deviceSecret +
- "productKey" + productKey;
- String password = buildPassword(deviceSecret, content);
- return new AuthInfo(clientId, username, password);
+ String password = buildPassword(deviceSecret,
+ buildContent(clientId, productKey, deviceName, deviceSecret));
+ return new IotDeviceAuthReqDTO(clientId, username, password);
}
- private static String buildClientId(String productKey, String deviceName) {
+ public static String buildClientId(String productKey, String deviceName) {
return String.format("%s.%s", productKey, deviceName);
}
- private static String buildUsername(String productKey, String deviceName) {
+ public static String buildUsername(String productKey, String deviceName) {
return String.format("%s&%s", deviceName, productKey);
}
- private static String buildPassword(String deviceSecret, String content) {
- return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, deviceSecret.getBytes())
+ public static String buildPassword(String deviceSecret, String content) {
+ return DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.utf8Bytes(deviceSecret))
.digestHex(content);
}
- public static DeviceInfo parseUsername(String username) {
+ private static String buildContent(String clientId, String productKey, String deviceName, String deviceSecret) {
+ return "clientId" + clientId +
+ "deviceName" + deviceName +
+ "deviceSecret" + deviceSecret +
+ "productKey" + productKey;
+ }
+
+ public static IotDeviceIdentity parseUsername(String username) {
String[] usernameParts = username.split("&");
if (usernameParts.length != 2) {
return null;
}
- return new DeviceInfo().setProductKey(usernameParts[1]).setDeviceName(usernameParts[0]);
+ return new IotDeviceIdentity(usernameParts[1], usernameParts[0]);
}
}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
index 5c1ac2600..b7d9894f0 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
@@ -72,7 +72,7 @@ public class IotDeviceMessageUtils {
/**
* 判断消息中是否包含指定的标识符
- *
+ *
* 对于不同消息类型的处理:
* - EVENT_POST/SERVICE_INVOKE:检查 params.identifier 是否匹配
* - STATE_UPDATE:检查 params.state 是否匹配
@@ -99,6 +99,17 @@ public class IotDeviceMessageUtils {
return false;
}
+ /**
+ * 判断消息中是否不包含指定的标识符
+ *
+ * @param message 消息
+ * @param identifier 要检查的标识符
+ * @return 是否不包含
+ */
+ public static boolean notContainsIdentifier(IotDeviceMessage message, String identifier) {
+ return !containsIdentifier(message, identifier);
+ }
+
/**
* 将 params 解析为 Map
*
@@ -144,20 +155,19 @@ public class IotDeviceMessageUtils {
return null;
}
- // 策略1:如果 params 不是 Map,直接返回该值(适用于简单的单属性消息)
+ // 策略 1:如果 params 不是 Map,直接返回该值(适用于简单的单属性消息)
if (!(params instanceof Map)) {
return params;
}
+ // 策略 2:直接通过标识符获取属性值
Map paramsMap = (Map) params;
-
- // 策略2:直接通过标识符获取属性值
Object directValue = paramsMap.get(identifier);
if (directValue != null) {
return directValue;
}
- // 策略3:从 properties 字段中获取(适用于标准属性上报消息)
+ // 策略 3:从 properties 字段中获取(适用于标准属性上报消息)
Object properties = paramsMap.get("properties");
if (properties instanceof Map) {
Map propertiesMap = (Map) properties;
@@ -167,7 +177,7 @@ public class IotDeviceMessageUtils {
}
}
- // 策略4:从 data 字段中获取(适用于某些消息格式)
+ // 策略 4:从 data 字段中获取(适用于某些消息格式)
Object data = paramsMap.get("data");
if (data instanceof Map) {
Map dataMap = (Map) data;
@@ -177,13 +187,13 @@ public class IotDeviceMessageUtils {
}
}
- // 策略5:从 value 字段中获取(适用于单值消息)
+ // 策略 5:从 value 字段中获取(适用于单值消息)
Object value = paramsMap.get("value");
if (value != null) {
return value;
}
- // 策略6:如果 Map 只有两个字段且包含 identifier,返回另一个字段的值
+ // 策略 6:如果 Map 只有两个字段且包含 identifier,返回另一个字段的值
if (paramsMap.size() == 2 && paramsMap.containsKey("identifier")) {
for (Map.Entry entry : paramsMap.entrySet()) {
if (!"identifier".equals(entry.getKey())) {
@@ -196,6 +206,43 @@ public class IotDeviceMessageUtils {
return null;
}
+ /**
+ * 从服务调用消息中提取输入参数
+ *
+ * 服务调用消息的 params 结构通常为:
+ * {
+ * "identifier": "serviceIdentifier",
+ * "inputData": { ... } 或 "inputParams": { ... }
+ * }
+ *
+ * @param message 设备消息
+ * @return 输入参数 Map,如果未找到则返回 null
+ */
+ @SuppressWarnings("unchecked")
+ public static Map extractServiceInputParams(IotDeviceMessage message) {
+ // 1. 参数校验
+ Object params = message.getParams();
+ if (params == null) {
+ return null;
+ }
+ if (!(params instanceof Map)) {
+ return null;
+ }
+ Map paramsMap = (Map) params;
+
+ // 尝试从 inputData 字段获取
+ Object inputData = paramsMap.get("inputData");
+ if (inputData instanceof Map) {
+ return (Map) inputData;
+ }
+ // 尝试从 inputParams 字段获取
+ Object inputParams = paramsMap.get("inputParams");
+ if (inputParams instanceof Map) {
+ return (Map) inputParams;
+ }
+ return null;
+ }
+
// ========== Topic 相关 ==========
public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {
diff --git a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java
index a6d669d17..b0d39be51 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java
+++ b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java
@@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.iot.core.util;
+import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.*;
/**
* {@link IotDeviceMessageUtils} 的单元测试
@@ -138,4 +138,72 @@ public class IotDeviceMessageUtilsTest {
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result); // 应该返回直接标识符的值
}
+
+ // ========== notContainsIdentifier 测试 ==========
+
+ /**
+ * 测试 notContainsIdentifier 与 containsIdentifier 的互补性
+ * **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
+ * **Validates: Requirements 4.1**
+ */
+ @Test
+ public void testNotContainsIdentifier_complementary_whenContains() {
+ // 准备参数:消息包含指定标识符
+ IotDeviceMessage message = new IotDeviceMessage();
+ message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
+ Map params = new HashMap<>();
+ params.put("temperature", 25);
+ message.setParams(params);
+ String identifier = "temperature";
+
+ // 调用 & 断言:验证互补性
+ boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
+ boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
+ assertTrue(containsResult);
+ assertFalse(notContainsResult);
+ assertEquals(!containsResult, notContainsResult);
+ }
+
+ /**
+ * 测试 notContainsIdentifier 与 containsIdentifier 的互补性
+ * **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
+ * **Validates: Requirements 4.1**
+ */
+ @Test
+ public void testNotContainsIdentifier_complementary_whenNotContains() {
+ // 准备参数:消息不包含指定标识符
+ IotDeviceMessage message = new IotDeviceMessage();
+ message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
+ Map params = new HashMap<>();
+ params.put("temperature", 25);
+ message.setParams(params);
+ String identifier = "humidity";
+
+ // 调用 & 断言:验证互补性
+ boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
+ boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
+ assertFalse(containsResult);
+ assertTrue(notContainsResult);
+ assertEquals(!containsResult, notContainsResult);
+ }
+
+ /**
+ * 测试 notContainsIdentifier 与 containsIdentifier 的互补性 - 空参数场景
+ * **Property 2: notContainsIdentifier 与 containsIdentifier 互补性**
+ * **Validates: Requirements 4.1**
+ */
+ @Test
+ public void testNotContainsIdentifier_complementary_nullParams() {
+ // 准备参数:params 为 null
+ IotDeviceMessage message = new IotDeviceMessage();
+ message.setParams(null);
+ String identifier = "temperature";
+
+ // 调用 & 断言:验证互补性
+ boolean containsResult = IotDeviceMessageUtils.containsIdentifier(message, identifier);
+ boolean notContainsResult = IotDeviceMessageUtils.notContainsIdentifier(message, identifier);
+ assertFalse(containsResult);
+ assertTrue(notContainsResult);
+ assertEquals(!containsResult, notContainsResult);
+ }
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml
index 7136d3eb3..9bf984ef4 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/pom.xml
+++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml
@@ -48,6 +48,12 @@
vertx-mqtt
+
+
+ org.eclipse.californium
+ californium-core
+
+
cn.iocoder.cloud
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java
index 94dd309dd..2fcea2e46 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/IotDeviceMessageCodec.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.codec;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
/**
- * {@link cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage} 的编解码器
+ * {@link IotDeviceMessage} 的编解码器
*
* @author 芋道源码
*/
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java
index 9086480d3..5a4e47fe1 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java
@@ -18,7 +18,7 @@ import org.springframework.stereotype.Component;
@Component
public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec {
- private static final String TYPE = "Alink";
+ public static final String TYPE = "Alink";
@Data
@NoArgsConstructor
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
index 4f42a8c2f..05098cccb 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
@@ -13,7 +13,7 @@ import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
- * TCP 二进制格式 {@link IotDeviceMessage} 编解码器
+ * TCP/UDP 二进制格式 {@link IotDeviceMessage} 编解码器
*
* 二进制协议格式(所有数值使用大端序):
*
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
index 10ffbdf5c..7d62ce2e0 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
@@ -11,7 +11,7 @@ import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
- * TCP JSON 格式 {@link IotDeviceMessage} 编解码器
+ * TCP/UDP JSON 格式 {@link IotDeviceMessage} 编解码器
*
* 采用纯 JSON 格式传输,格式如下:
* {
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
index 3e573efdd..a4e93a84f 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.iot.gateway.config;
import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapDownstreamSubscriber;
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol;
@@ -10,13 +12,15 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttDownstreamSubscr
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttDownstreamHandler;
-import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsDownstreamSubscriber;
-import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.IotMqttWsUpstreamProtocol;
-import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.manager.IotMqttWsConnectionManager;
-import cn.iocoder.yudao.module.iot.gateway.protocol.mqttws.router.IotMqttWsDownstreamHandler;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol;
import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager;
+import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpDownstreamSubscriber;
+import cn.iocoder.yudao.module.iot.gateway.protocol.udp.IotUdpUpstreamProtocol;
+import cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager.IotUdpSessionManager;
+import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketDownstreamSubscriber;
+import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.IotWebSocketUpstreamProtocol;
+import cn.iocoder.yudao.module.iot.gateway.protocol.websocket.manager.IotWebSocketConnectionManager;
import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
import io.vertx.core.Vertx;
@@ -40,9 +44,15 @@ public class IotGatewayConfiguration {
@Slf4j
public static class HttpProtocolConfiguration {
+ @Bean(name = "httpVertx", destroyMethod = "close")
+ public Vertx httpVertx() {
+ return Vertx.vertx();
+ }
+
@Bean
- public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties) {
- return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp());
+ public IotHttpUpstreamProtocol iotHttpUpstreamProtocol(IotGatewayProperties gatewayProperties,
+ @Qualifier("httpVertx") Vertx httpVertx) {
+ return new IotHttpUpstreamProtocol(gatewayProperties.getProtocol().getHttp(), httpVertx);
}
@Bean
@@ -110,11 +120,9 @@ public class IotGatewayConfiguration {
@Bean
public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler,
IotDeviceMessageService messageService,
- IotDeviceService deviceService,
IotTcpConnectionManager connectionManager,
IotMessageBus messageBus) {
- return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager,
- messageBus);
+ return new IotTcpDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
}
@@ -157,39 +165,88 @@ public class IotGatewayConfiguration {
}
/**
- * IoT 网关 MQTT WebSocket 协议配置类
+ * IoT 网关 UDP 协议配置类
*/
@Configuration
- @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.mqtt-ws", name = "enabled", havingValue = "true")
+ @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.udp", name = "enabled", havingValue = "true")
@Slf4j
- public static class MqttWsProtocolConfiguration {
+ public static class UdpProtocolConfiguration {
- @Bean(name = "mqttWsVertx", destroyMethod = "close")
- public Vertx mqttWsVertx() {
+ @Bean(name = "udpVertx", destroyMethod = "close")
+ public Vertx udpVertx() {
return Vertx.vertx();
}
@Bean
- public IotMqttWsUpstreamProtocol iotMqttWsUpstreamProtocol(IotGatewayProperties gatewayProperties,
- IotDeviceMessageService messageService,
- IotMqttWsConnectionManager connectionManager,
- @Qualifier("mqttWsVertx") Vertx mqttWsVertx) {
- return new IotMqttWsUpstreamProtocol(gatewayProperties.getProtocol().getMqttWs(),
- messageService, connectionManager, mqttWsVertx);
+ public IotUdpUpstreamProtocol iotUdpUpstreamProtocol(IotGatewayProperties gatewayProperties,
+ IotDeviceService deviceService,
+ IotDeviceMessageService messageService,
+ IotUdpSessionManager sessionManager,
+ @Qualifier("udpVertx") Vertx udpVertx) {
+ return new IotUdpUpstreamProtocol(gatewayProperties.getProtocol().getUdp(),
+ deviceService, messageService, sessionManager, udpVertx);
}
@Bean
- public IotMqttWsDownstreamHandler iotMqttWsDownstreamHandler(IotDeviceMessageService messageService,
- IotDeviceService deviceService,
- IotMqttWsConnectionManager connectionManager) {
- return new IotMqttWsDownstreamHandler(messageService, deviceService, connectionManager);
+ public IotUdpDownstreamSubscriber iotUdpDownstreamSubscriber(IotUdpUpstreamProtocol protocolHandler,
+ IotDeviceMessageService messageService,
+ IotUdpSessionManager sessionManager,
+ IotMessageBus messageBus) {
+ return new IotUdpDownstreamSubscriber(protocolHandler, messageService, sessionManager, messageBus);
+ }
+
+ }
+
+ /**
+ * IoT 网关 CoAP 协议配置类
+ */
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.coap", name = "enabled", havingValue = "true")
+ @Slf4j
+ public static class CoapProtocolConfiguration {
+
+ @Bean
+ public IotCoapUpstreamProtocol iotCoapUpstreamProtocol(IotGatewayProperties gatewayProperties) {
+ return new IotCoapUpstreamProtocol(gatewayProperties.getProtocol().getCoap());
}
@Bean
- public IotMqttWsDownstreamSubscriber iotMqttWsDownstreamSubscriber(IotMqttWsUpstreamProtocol mqttWsUpstreamProtocol,
- IotMqttWsDownstreamHandler downstreamHandler,
- IotMessageBus messageBus) {
- return new IotMqttWsDownstreamSubscriber(mqttWsUpstreamProtocol, downstreamHandler, messageBus);
+ public IotCoapDownstreamSubscriber iotCoapDownstreamSubscriber(IotCoapUpstreamProtocol coapUpstreamProtocol,
+ IotMessageBus messageBus) {
+ return new IotCoapDownstreamSubscriber(coapUpstreamProtocol, messageBus);
+ }
+
+ }
+
+ /**
+ * IoT 网关 WebSocket 协议配置类
+ */
+ @Configuration
+ @ConditionalOnProperty(prefix = "yudao.iot.gateway.protocol.websocket", name = "enabled", havingValue = "true")
+ @Slf4j
+ public static class WebSocketProtocolConfiguration {
+
+ @Bean(name = "websocketVertx", destroyMethod = "close")
+ public Vertx websocketVertx() {
+ return Vertx.vertx();
+ }
+
+ @Bean
+ public IotWebSocketUpstreamProtocol iotWebSocketUpstreamProtocol(IotGatewayProperties gatewayProperties,
+ IotDeviceService deviceService,
+ IotDeviceMessageService messageService,
+ IotWebSocketConnectionManager connectionManager,
+ @Qualifier("websocketVertx") Vertx websocketVertx) {
+ return new IotWebSocketUpstreamProtocol(gatewayProperties.getProtocol().getWebsocket(),
+ deviceService, messageService, connectionManager, websocketVertx);
+ }
+
+ @Bean
+ public IotWebSocketDownstreamSubscriber iotWebSocketDownstreamSubscriber(IotWebSocketUpstreamProtocol protocolHandler,
+ IotDeviceMessageService messageService,
+ IotWebSocketConnectionManager connectionManager,
+ IotMessageBus messageBus) {
+ return new IotWebSocketDownstreamSubscriber(protocolHandler, messageService, connectionManager, messageBus);
}
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
index 7655a3759..9a86ee600 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
@@ -93,6 +93,21 @@ public class IotGatewayProperties {
*/
private MqttWsProperties mqttWs;
+ /**
+ * UDP 组件配置
+ */
+ private UdpProperties udp;
+
+ /**
+ * CoAP 组件配置
+ */
+ private CoapProperties coap;
+
+ /**
+ * WebSocket 组件配置
+ */
+ private WebSocketProperties websocket;
+
}
@Data
@@ -503,4 +518,129 @@ public class IotGatewayProperties {
}
+ @Data
+ public static class UdpProperties {
+
+ /**
+ * 是否开启
+ */
+ @NotNull(message = "是否开启不能为空")
+ private Boolean enabled;
+
+ /**
+ * 服务端口(默认 8093)
+ */
+ private Integer port = 8093;
+
+ /**
+ * 接收缓冲区大小(默认 64KB)
+ */
+ private Integer receiveBufferSize = 65536;
+
+ /**
+ * 发送缓冲区大小(默认 64KB)
+ */
+ private Integer sendBufferSize = 65536;
+
+ /**
+ * 会话超时时间(毫秒,默认 60 秒)
+ *
+ * 用于清理不活跃的设备地址映射
+ */
+ private Long sessionTimeoutMs = 60000L;
+
+ /**
+ * 会话清理间隔(毫秒,默认 30 秒)
+ */
+ private Long sessionCleanIntervalMs = 30000L;
+
+ }
+
+ @Data
+ public static class CoapProperties {
+
+ /**
+ * 是否开启
+ */
+ @NotNull(message = "是否开启不能为空")
+ private Boolean enabled;
+
+ /**
+ * 服务端口(CoAP 默认端口 5683)
+ */
+ @NotNull(message = "服务端口不能为空")
+ private Integer port = 5683;
+
+ /**
+ * 最大消息大小(字节)
+ */
+ @NotNull(message = "最大消息大小不能为空")
+ private Integer maxMessageSize = 1024;
+
+ /**
+ * ACK 超时时间(毫秒)
+ */
+ @NotNull(message = "ACK 超时时间不能为空")
+ private Integer ackTimeout = 2000;
+
+ /**
+ * 最大重传次数
+ */
+ @NotNull(message = "最大重传次数不能为空")
+ private Integer maxRetransmit = 4;
+
+ }
+
+ @Data
+ public static class WebSocketProperties {
+
+ /**
+ * 是否开启
+ */
+ @NotNull(message = "是否开启不能为空")
+ private Boolean enabled;
+
+ /**
+ * 服务器端口(默认:8094)
+ */
+ private Integer port = 8094;
+
+ /**
+ * WebSocket 路径(默认:/ws)
+ */
+ @NotEmpty(message = "WebSocket 路径不能为空")
+ private String path = "/ws";
+
+ /**
+ * 最大消息大小(字节,默认 64KB)
+ */
+ private Integer maxMessageSize = 65536;
+
+ /**
+ * 最大帧大小(字节,默认 64KB)
+ */
+ private Integer maxFrameSize = 65536;
+
+ /**
+ * 空闲超时时间(秒,默认 60)
+ */
+ private Integer idleTimeoutSeconds = 60;
+
+ /**
+ * 是否启用 SSL(wss://)
+ */
+ private Boolean sslEnabled = false;
+
+ /**
+ * SSL 证书路径
+ */
+ private String sslCertPath;
+
+ /**
+ * SSL 私钥路径
+ */
+ private String sslKeyPath;
+
+ }
+
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java
new file mode 100644
index 000000000..d01cdc416
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapDownstreamSubscriber.java
@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
+
+import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus;
+import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * IoT 网关 CoAP 订阅者:接收下行给设备的消息
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class IotCoapDownstreamSubscriber implements IotMessageSubscriber {
+
+ private final IotCoapUpstreamProtocol protocol;
+
+ private final IotMessageBus messageBus;
+
+ @PostConstruct
+ public void init() {
+ messageBus.register(this);
+ }
+
+ @Override
+ public String getTopic() {
+ return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId());
+ }
+
+ @Override
+ public String getGroup() {
+ // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group
+ return getTopic();
+ }
+
+ @Override
+ public void onMessage(IotDeviceMessage message) {
+ // 如需支持,可通过 CoAP Observe 模式实现(设备订阅资源,服务器推送变更)
+ log.warn("[onMessage][IoT 网关 CoAP 协议暂不支持下行消息,忽略消息:{}]", message);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java
new file mode 100644
index 000000000..e10bd9889
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/IotCoapUpstreamProtocol.java
@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
+
+import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
+import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.router.*;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.CoapServer;
+import org.eclipse.californium.core.config.CoapConfig;
+import org.eclipse.californium.elements.config.Configuration;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * IoT 网关 CoAP 协议:接收设备上行消息
+ *
+ * 基于 Eclipse Californium 实现,支持:
+ * 1. 认证:POST /auth
+ * 2. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
+ * 3. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotCoapUpstreamProtocol {
+
+ private final IotGatewayProperties.CoapProperties coapProperties;
+
+ private CoapServer coapServer;
+
+ @Getter
+ private final String serverId;
+
+ public IotCoapUpstreamProtocol(IotGatewayProperties.CoapProperties coapProperties) {
+ this.coapProperties = coapProperties;
+ this.serverId = IotDeviceMessageUtils.generateServerId(coapProperties.getPort());
+ }
+
+ @PostConstruct
+ public void start() {
+ try {
+ // 1.1 创建网络配置(Californium 3.x API)
+ Configuration config = Configuration.createStandardWithoutFile();
+ config.set(CoapConfig.COAP_PORT, coapProperties.getPort());
+ config.set(CoapConfig.MAX_MESSAGE_SIZE, coapProperties.getMaxMessageSize());
+ config.set(CoapConfig.ACK_TIMEOUT, coapProperties.getAckTimeout(), TimeUnit.MILLISECONDS);
+ config.set(CoapConfig.MAX_RETRANSMIT, coapProperties.getMaxRetransmit());
+ // 1.2 创建 CoAP 服务器
+ coapServer = new CoapServer(config);
+
+ // 2.1 添加 /auth 认证资源
+ IotCoapAuthHandler authHandler = new IotCoapAuthHandler();
+ IotCoapAuthResource authResource = new IotCoapAuthResource(this, authHandler);
+ coapServer.add(authResource);
+ // 2.2 添加 /auth/register/device 设备动态注册资源(一型一密)
+ IotCoapRegisterHandler registerHandler = new IotCoapRegisterHandler();
+ IotCoapRegisterResource registerResource = new IotCoapRegisterResource(registerHandler);
+ authResource.add(new CoapResource("register") {{
+ add(registerResource);
+ }});
+ // 2.3 添加 /topic 根资源(用于上行消息)
+ IotCoapUpstreamHandler upstreamHandler = new IotCoapUpstreamHandler();
+ IotCoapUpstreamTopicResource topicResource = new IotCoapUpstreamTopicResource(this, upstreamHandler);
+ coapServer.add(topicResource);
+
+ // 3. 启动服务器
+ coapServer.start();
+ log.info("[start][IoT 网关 CoAP 协议启动成功,端口:{},资源:/auth, /auth/register/device, /topic]", coapProperties.getPort());
+ } catch (Exception e) {
+ log.error("[start][IoT 网关 CoAP 协议启动失败]", e);
+ throw e;
+ }
+ }
+
+ @PreDestroy
+ public void stop() {
+ if (coapServer != null) {
+ try {
+ coapServer.stop();
+ log.info("[stop][IoT 网关 CoAP 协议已停止]");
+ } catch (Exception e) {
+ log.error("[stop][IoT 网关 CoAP 协议停止失败]", e);
+ }
+ }
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java
new file mode 100644
index 000000000..94536a643
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * CoAP 协议实现包
+ *
+ * 提供基于 Eclipse Californium 的 IoT 设备连接和消息处理功能
+ *
+ * URI 路径:
+ * - 认证:POST /auth
+ * - 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
+ * - 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
+ *
+ * Token 通过 CoAP Option 2088 携带
+ */
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap;
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java
new file mode 100644
index 000000000..43fb77608
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthHandler.java
@@ -0,0 +1,117 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
+import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
+import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
+import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.coap.CoAP;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+
+import java.util.Map;
+
+/**
+ * IoT 网关 CoAP 协议的【认证】处理器
+ *
+ * 参考 {@link cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpAuthHandler}
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotCoapAuthHandler {
+
+ private final IotDeviceTokenService deviceTokenService;
+ private final IotDeviceCommonApi deviceApi;
+ private final IotDeviceMessageService deviceMessageService;
+
+ public IotCoapAuthHandler() {
+ this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
+ this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
+ this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
+ }
+
+ /**
+ * 处理认证请求
+ *
+ * @param exchange CoAP 交换对象
+ * @param protocol 协议对象
+ */
+ @SuppressWarnings("unchecked")
+ public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
+ try {
+ // 1.1 解析请求体
+ byte[] payload = exchange.getRequestPayload();
+ if (payload == null || payload.length == 0) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
+ return;
+ }
+ Map body;
+ try {
+ body = JsonUtils.parseObject(new String(payload), Map.class);
+ } catch (Exception e) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
+ return;
+ }
+ // 1.2 解析参数
+ String clientId = MapUtil.getStr(body, "clientId");
+ if (StrUtil.isEmpty(clientId)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "clientId 不能为空");
+ return;
+ }
+ String username = MapUtil.getStr(body, "username");
+ if (StrUtil.isEmpty(username)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "username 不能为空");
+ return;
+ }
+ String password = MapUtil.getStr(body, "password");
+ if (StrUtil.isEmpty(password)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "password 不能为空");
+ return;
+ }
+
+ // 2.1 执行认证
+ CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO()
+ .setClientId(clientId).setUsername(username).setPassword(password));
+ if (result.isError()) {
+ log.warn("[handle][认证失败,clientId: {}, 错误: {}]", clientId, result.getMsg());
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败:" + result.getMsg());
+ return;
+ }
+ if (!BooleanUtil.isTrue(result.getData())) {
+ log.warn("[handle][认证失败,clientId: {}]", clientId);
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "认证失败");
+ return;
+ }
+ // 2.2 生成 Token
+ IotDeviceIdentity deviceInfo = deviceTokenService.parseUsername(username);
+ Assert.notNull(deviceInfo, "设备信息不能为空");
+ String token = deviceTokenService.createToken(deviceInfo.getProductKey(), deviceInfo.getDeviceName());
+ Assert.notBlank(token, "生成 token 不能为空");
+
+ // 3. 执行上线
+ IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline();
+ deviceMessageService.sendDeviceMessage(message,
+ deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
+
+ // 4. 返回成功响应
+ log.info("[handle][认证成功,productKey: {}, deviceName: {}]",
+ deviceInfo.getProductKey(), deviceInfo.getDeviceName());
+ IotCoapUtils.respondSuccess(exchange, MapUtil.of("token", token));
+ } catch (Exception e) {
+ log.error("[handle][认证处理异常]", e);
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
+ }
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java
new file mode 100644
index 000000000..9d0d90cb3
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapAuthResource.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
+
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+
+/**
+ * IoT 网关 CoAP 协议的认证资源(/auth)
+ *
+ * 设备通过此资源进行认证,获取 Token
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotCoapAuthResource extends CoapResource {
+
+ public static final String PATH = "auth";
+
+ private final IotCoapUpstreamProtocol protocol;
+ private final IotCoapAuthHandler authHandler;
+
+ public IotCoapAuthResource(IotCoapUpstreamProtocol protocol,
+ IotCoapAuthHandler authHandler) {
+ super(PATH);
+ this.protocol = protocol;
+ this.authHandler = authHandler;
+ log.info("[IotCoapAuthResource][创建 CoAP 认证资源: /{}]", PATH);
+ }
+
+ @Override
+ public void handlePOST(CoapExchange exchange) {
+ log.debug("[handlePOST][收到 /auth POST 请求]");
+ authHandler.handle(exchange, protocol);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java
new file mode 100644
index 000000000..8ffbe4f67
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterHandler.java
@@ -0,0 +1,98 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterReqDTO;
+import cn.iocoder.yudao.module.iot.core.topic.auth.IotDeviceRegisterRespDTO;
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.coap.CoAP;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+
+import java.util.Map;
+
+/**
+ * IoT 网关 CoAP 协议的【设备动态注册】处理器
+ *
+ * 用于直连设备/网关的一型一密动态注册,不需要认证
+ *
+ * @author 芋道源码
+ * @see 阿里云 - 一型一密
+ * @see cn.iocoder.yudao.module.iot.gateway.protocol.http.router.IotHttpRegisterHandler
+ */
+@Slf4j
+public class IotCoapRegisterHandler {
+
+ private final IotDeviceCommonApi deviceApi;
+
+ public IotCoapRegisterHandler() {
+ this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
+ }
+
+ /**
+ * 处理设备动态注册请求
+ *
+ * @param exchange CoAP 交换对象
+ */
+ @SuppressWarnings("unchecked")
+ public void handle(CoapExchange exchange) {
+ try {
+ // 1.1 解析请求体
+ byte[] payload = exchange.getRequestPayload();
+ if (payload == null || payload.length == 0) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
+ return;
+ }
+ Map body;
+ try {
+ body = JsonUtils.parseObject(new String(payload), Map.class);
+ } catch (Exception e) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体 JSON 格式错误");
+ return;
+ }
+
+ // 1.2 解析参数
+ String productKey = MapUtil.getStr(body, "productKey");
+ if (StrUtil.isEmpty(productKey)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
+ return;
+ }
+ String deviceName = MapUtil.getStr(body, "deviceName");
+ if (StrUtil.isEmpty(deviceName)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
+ return;
+ }
+ String productSecret = MapUtil.getStr(body, "productSecret");
+ if (StrUtil.isEmpty(productSecret)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productSecret 不能为空");
+ return;
+ }
+
+ // 2. 调用动态注册
+ IotDeviceRegisterReqDTO reqDTO = new IotDeviceRegisterReqDTO()
+ .setProductKey(productKey)
+ .setDeviceName(deviceName)
+ .setProductSecret(productSecret);
+ CommonResult result = deviceApi.registerDevice(reqDTO);
+ if (result.isError()) {
+ log.warn("[handle][设备动态注册失败,productKey: {}, deviceName: {}, 错误: {}]",
+ productKey, deviceName, result.getMsg());
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST,
+ "设备动态注册失败:" + result.getMsg());
+ return;
+ }
+
+ // 3. 返回成功响应
+ log.info("[handle][设备动态注册成功,productKey: {}, deviceName: {}]", productKey, deviceName);
+ IotCoapUtils.respondSuccess(exchange, result.getData());
+ } catch (Exception e) {
+ log.error("[handle][设备动态注册处理异常]", e);
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
+ }
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java
new file mode 100644
index 000000000..05fd1ec89
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapRegisterResource.java
@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+
+/**
+ * IoT 网关 CoAP 协议的设备动态注册资源(/auth/register/device)
+ *
+ * 用于直连设备/网关的一型一密动态注册,不需要认证
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotCoapRegisterResource extends CoapResource {
+
+ public static final String PATH = "device";
+
+ private final IotCoapRegisterHandler registerHandler;
+
+ public IotCoapRegisterResource(IotCoapRegisterHandler registerHandler) {
+ super(PATH);
+ this.registerHandler = registerHandler;
+ log.info("[IotCoapRegisterResource][创建 CoAP 设备动态注册资源: /auth/register/{}]", PATH);
+ }
+
+ @Override
+ public void handlePOST(CoapExchange exchange) {
+ log.debug("[handlePOST][收到设备动态注册请求]");
+ registerHandler.handle(exchange);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java
new file mode 100644
index 000000000..d33eb464b
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamHandler.java
@@ -0,0 +1,110 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.text.StrPool;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.core.topic.IotDeviceIdentity;
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.util.IotCoapUtils;
+import cn.iocoder.yudao.module.iot.gateway.service.auth.IotDeviceTokenService;
+import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.coap.CoAP;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+
+import java.util.List;
+
+/**
+ * IoT 网关 CoAP 协议的【上行】处理器
+ *
+ * 处理设备通过 CoAP 协议发送的上行消息,包括:
+ * 1. 属性上报:POST /topic/sys/{productKey}/{deviceName}/thing/property/post
+ * 2. 事件上报:POST /topic/sys/{productKey}/{deviceName}/thing/event/post
+ *
+ * Token 通过自定义 CoAP Option 2088 携带
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotCoapUpstreamHandler {
+
+ private final IotDeviceTokenService deviceTokenService;
+ private final IotDeviceMessageService deviceMessageService;
+
+ public IotCoapUpstreamHandler() {
+ this.deviceTokenService = SpringUtil.getBean(IotDeviceTokenService.class);
+ this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
+ }
+
+ /**
+ * 处理 CoAP 请求
+ *
+ * @param exchange CoAP 交换对象
+ * @param protocol 协议对象
+ */
+ public void handle(CoapExchange exchange, IotCoapUpstreamProtocol protocol) {
+ try {
+ // 1. 解析通用参数
+ List uriPath = exchange.getRequestOptions().getUriPath();
+ String productKey = CollUtil.get(uriPath, 2);
+ String deviceName = CollUtil.get(uriPath, 3);
+ byte[] payload = exchange.getRequestPayload();
+ if (StrUtil.isEmpty(productKey)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "productKey 不能为空");
+ return;
+ }
+ if (StrUtil.isEmpty(deviceName)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "deviceName 不能为空");
+ return;
+ }
+ if (ArrayUtil.isEmpty(payload)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "请求体不能为空");
+ return;
+ }
+
+ // 2. 认证:从自定义 Option 获取 token
+ String token = IotCoapUtils.getTokenFromOption(exchange, IotCoapUtils.OPTION_TOKEN);
+ if (StrUtil.isEmpty(token)) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 不能为空");
+ return;
+ }
+ // 验证 token
+ IotDeviceIdentity deviceInfo = deviceTokenService.verifyToken(token);
+ if (deviceInfo == null) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.UNAUTHORIZED, "token 无效或已过期");
+ return;
+ }
+ // 验证设备信息匹配
+ if (ObjUtil.notEqual(productKey, deviceInfo.getProductKey())
+ || ObjUtil.notEqual(deviceName, deviceInfo.getDeviceName())) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.FORBIDDEN, "设备信息与 token 不匹配");
+ return;
+ }
+
+ // 2.1 解析 method:deviceName 后面的路径,用 . 拼接
+ // 路径格式:[topic, sys, productKey, deviceName, thing, property, post]
+ String method = String.join(StrPool.DOT, uriPath.subList(4, uriPath.size()));
+
+ // 2.2 解码消息
+ IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
+ if (ObjUtil.notEqual(method, message.getMethod())) {
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.BAD_REQUEST, "method 不匹配");
+ return;
+ }
+ // 2.3 发送消息到消息总线
+ deviceMessageService.sendDeviceMessage(message, productKey, deviceName, protocol.getServerId());
+
+ // 3. 返回成功响应
+ IotCoapUtils.respondSuccess(exchange, MapUtil.of("messageId", message.getId()));
+ } catch (Exception e) {
+ log.error("[handle][CoAP 请求处理异常]", e);
+ IotCoapUtils.respondError(exchange, CoAP.ResponseCode.INTERNAL_SERVER_ERROR, "服务器内部错误");
+ }
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java
new file mode 100644
index 000000000..1c694483f
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/router/IotCoapUpstreamTopicResource.java
@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap.router;
+
+import cn.iocoder.yudao.module.iot.gateway.protocol.coap.IotCoapUpstreamProtocol;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+import org.eclipse.californium.core.server.resources.Resource;
+
+/**
+ * IoT 网关 CoAP 协议的【上行】Topic 资源
+ *
+ * 支持任意深度的路径匹配:
+ * - /topic/sys/{productKey}/{deviceName}/thing/property/post
+ * - /topic/sys/{productKey}/{deviceName}/thing/event/{eventId}/post
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class IotCoapUpstreamTopicResource extends CoapResource {
+
+ public static final String PATH = "topic";
+
+ private final IotCoapUpstreamProtocol protocol;
+ private final IotCoapUpstreamHandler upstreamHandler;
+
+ /**
+ * 创建根资源(/topic)
+ */
+ public IotCoapUpstreamTopicResource(IotCoapUpstreamProtocol protocol,
+ IotCoapUpstreamHandler upstreamHandler) {
+ this(PATH, protocol, upstreamHandler);
+ log.info("[IotCoapUpstreamTopicResource][创建 CoAP 上行 Topic 资源: /{}]", PATH);
+ }
+
+ /**
+ * 创建子资源(动态路径)
+ */
+ private IotCoapUpstreamTopicResource(String name,
+ IotCoapUpstreamProtocol protocol,
+ IotCoapUpstreamHandler upstreamHandler) {
+ super(name);
+ this.protocol = protocol;
+ this.upstreamHandler = upstreamHandler;
+ }
+
+ @Override
+ public Resource getChild(String name) {
+ // 递归创建动态子资源,支持任意深度路径
+ return new IotCoapUpstreamTopicResource(name, protocol, upstreamHandler);
+ }
+
+ @Override
+ public void handleGET(CoapExchange exchange) {
+ upstreamHandler.handle(exchange, protocol);
+ }
+
+ @Override
+ public void handlePOST(CoapExchange exchange) {
+ upstreamHandler.handle(exchange, protocol);
+ }
+
+ @Override
+ public void handlePUT(CoapExchange exchange) {
+ upstreamHandler.handle(exchange, protocol);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java
new file mode 100644
index 000000000..9d5cdf3ff
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/coap/util/IotCoapUtils.java
@@ -0,0 +1,84 @@
+package cn.iocoder.yudao.module.iot.gateway.protocol.coap.util;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import org.eclipse.californium.core.coap.CoAP;
+import org.eclipse.californium.core.coap.MediaTypeRegistry;
+import org.eclipse.californium.core.coap.Option;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+
+import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*;
+
+/**
+ * IoT CoAP 协议工具类
+ *
+ * @author 芋道源码
+ */
+public class IotCoapUtils {
+
+ /**
+ * 自定义 CoAP Option 编号,用于携带 Token
+ *
+ * CoAP Option 范围 2048-65535 属于实验/自定义范围
+ */
+ public static final int OPTION_TOKEN = 2088;
+
+ /**
+ * 返回成功响应
+ *
+ * @param exchange CoAP 交换对象
+ * @param data 响应数据
+ */
+ public static void respondSuccess(CoapExchange exchange, Object data) {
+ CommonResult