Commit b5ce269fef66f761d1fe0adac4f53436724a4bae

Authored by 安浩浩
1 parent 516e3a23

【功能新增】IoT: 重构物联网模块,新增组件支持,更新依赖项,优化代码结构。具体更改包括:将插件模块替换为组件模块,添加 HTTP 和 EMQX 组件,更新相…

…关依赖,优化心跳和下行处理逻辑,增加组件配置属性,完善文档说明。
Showing 36 changed files with 2700 additions and 96 deletions
yudao-module-iot/pom.xml
... ... @@ -10,7 +10,8 @@
10 10 <modules>
11 11 <module>yudao-module-iot-api</module>
12 12 <module>yudao-module-iot-biz</module>
13   - <module>yudao-module-iot-plugins</module>
  13 + <module>yudao-module-iot-components</module>
  14 + <!-- <module>yudao-module-iot-plugins</module>-->
14 15 </modules>
15 16 <modelVersion>4.0.0</modelVersion>
16 17  
... ...
yudao-module-iot/yudao-module-iot-biz/pom.xml
... ... @@ -24,6 +24,16 @@
24 24 <artifactId>yudao-module-iot-api</artifactId>
25 25 <version>${revision}</version>
26 26 </dependency>
  27 + <dependency>
  28 + <groupId>cn.iocoder.boot</groupId>
  29 + <artifactId>yudao-module-iot-component-http</artifactId>
  30 + <version>${revision}</version>
  31 + </dependency>
  32 + <dependency>
  33 + <groupId>cn.iocoder.boot</groupId>
  34 + <artifactId>yudao-module-iot-component-emqx</artifactId>
  35 + <version>${revision}</version>
  36 + </dependency>
27 37  
28 38 <dependency>
29 39 <groupId>cn.iocoder.boot</groupId>
... ... @@ -70,11 +80,11 @@
70 80 </dependency>
71 81  
72 82 <!-- 脚本插件相关 -->
73   - <dependency>
74   - <groupId>cn.iocoder.boot</groupId>
75   - <artifactId>yudao-module-iot-plugin-script</artifactId>
76   - <version>${revision}</version>
77   - </dependency>
  83 + <!-- <dependency>-->
  84 + <!-- <groupId>cn.iocoder.boot</groupId>-->
  85 + <!-- <artifactId>yudao-module-iot-plugin-script</artifactId>-->
  86 + <!-- <version>${revision}</version>-->
  87 + <!-- </dependency>-->
78 88  
79 89 <!-- 消息队列相关 -->
80 90 <dependency>
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java
... ... @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*;
5 5 import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService;
6 6 import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
7 7 import jakarta.annotation.Resource;
  8 +import org.springframework.context.annotation.Primary;
8 9 import org.springframework.validation.annotation.Validated;
9 10 import org.springframework.web.bind.annotation.RestController;
10 11  
... ... @@ -15,6 +16,7 @@ import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
15 16 */
16 17 @RestController
17 18 @Validated
  19 +@Primary
18 20 public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
19 21  
20 22 @Resource
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductScriptServiceImpl.java
... ... @@ -9,18 +9,13 @@ import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProduct
9 9 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
10 10 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO;
11 11 import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductScriptMapper;
12   -import cn.iocoder.yudao.module.iot.plugin.script.context.PluginScriptContext;
13   -import cn.iocoder.yudao.module.iot.plugin.script.service.ScriptService;
14 12 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
15 13 import jakarta.annotation.Resource;
16 14 import lombok.extern.slf4j.Slf4j;
17 15 import org.springframework.stereotype.Service;
18 16 import org.springframework.validation.annotation.Validated;
19 17  
20   -import java.time.LocalDateTime;
21   -import java.util.HashMap;
22 18 import java.util.List;
23   -import java.util.Map;
24 19  
25 20 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
26 21 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS;
... ... @@ -43,8 +38,8 @@ public class IotProductScriptServiceImpl implements IotProductScriptService {
43 38 @Resource
44 39 private IotProductService productService;
45 40  
46   - @Resource
47   - private ScriptService scriptService;
  41 +// @Resource
  42 +// private ScriptService scriptService;
48 43  
49 44 @Override
50 45 public Long createProductScript(IotProductScriptSaveReqVO createReqVO) {
... ... @@ -121,89 +116,90 @@ public class IotProductScriptServiceImpl implements IotProductScriptService {
121 116  
122 117 @Override
123 118 public IotProductScriptTestRespVO testProductScript(IotProductScriptTestReqVO testReqVO) {
124   - long startTime = System.currentTimeMillis();
125   -
126   - try {
127   - // 验证产品是否存在
128   - validateProductExists(testReqVO.getProductId());
129   -
130   - // 根据ID获取已保存的脚本(如果有)
131   - IotProductScriptDO existingScript = null;
132   - if (testReqVO.getId() != null) {
133   - existingScript = getProductScript(testReqVO.getId());
134   - }
135   -
136   - // 创建测试上下文
137   - PluginScriptContext context = new PluginScriptContext();
138   - IotProductDO product = productService.getProduct(testReqVO.getProductId());
139   -
140   - // 设置设备上下文(使用产品信息,没有具体设备)
141   - context.withDeviceContext(product.getProductKey(), null);
142   -
143   - // 设置输入参数
144   - Map<String, Object> params = new HashMap<>();
145   - params.put("input", testReqVO.getTestInput());
146   - params.put("productKey", product.getProductKey());
147   - params.put("scriptType", testReqVO.getScriptType());
148   -
149   - // 根据脚本类型设置特定参数
150   - switch (testReqVO.getScriptType()) {
151   - case 1: // PROPERTY_PARSER
152   - params.put("method", "property");
153   - break;
154   - case 2: // EVENT_PARSER
155   - params.put("method", "event");
156   - params.put("identifier", "default");
157   - break;
158   - case 3: // COMMAND_ENCODER
159   - params.put("method", "command");
160   - break;
161   - default:
162   - // 默认不添加额外参数
163   - }
164   -
165   - // 添加所有参数到上下文
166   - for (Map.Entry<String, Object> entry : params.entrySet()) {
167   - context.setParameter(entry.getKey(), entry.getValue());
168   - }
169   -
170   - // 执行脚本
171   - Object result = scriptService.executeScript(
172   - testReqVO.getScriptLanguage(),
173   - testReqVO.getScriptContent(),
174   - context);
175   -
176   - // 更新测试结果(如果是已保存的脚本)
177   - if (existingScript != null) {
178   - IotProductScriptDO updateObj = new IotProductScriptDO();
179   - updateObj.setId(existingScript.getId());
180   - updateObj.setLastTestTime(LocalDateTime.now());
181   - updateObj.setLastTestResult(1); // 1表示成功
182   - productScriptMapper.updateById(updateObj);
183   - }
184   -
185   - long executionTime = System.currentTimeMillis() - startTime;
186   - return IotProductScriptTestRespVO.success(result, executionTime);
187   -
188   - } catch (Exception e) {
189   - log.error("[testProductScript][测试脚本异常]", e);
190   -
191   - // 如果是已保存的脚本,更新测试失败状态
192   - if (testReqVO.getId() != null) {
193   - try {
194   - IotProductScriptDO updateObj = new IotProductScriptDO();
195   - updateObj.setId(testReqVO.getId());
196   - updateObj.setLastTestTime(LocalDateTime.now());
197   - updateObj.setLastTestResult(0); // 0表示失败
198   - productScriptMapper.updateById(updateObj);
199   - } catch (Exception ex) {
200   - log.error("[testProductScript][更新脚本测试结果异常]", ex);
201   - }
202   - }
203   -
204   - long executionTime = System.currentTimeMillis() - startTime;
205   - return IotProductScriptTestRespVO.error(e.getMessage(), executionTime);
206   - }
  119 +// long startTime = System.currentTimeMillis();
  120 +//
  121 +// try {
  122 +// // 验证产品是否存在
  123 +// validateProductExists(testReqVO.getProductId());
  124 +//
  125 +// // 根据ID获取已保存的脚本(如果有)
  126 +// IotProductScriptDO existingScript = null;
  127 +// if (testReqVO.getId() != null) {
  128 +// existingScript = getProductScript(testReqVO.getId());
  129 +// }
  130 +//
  131 +// // 创建测试上下文
  132 +// PluginScriptContext context = new PluginScriptContext();
  133 +// IotProductDO product = productService.getProduct(testReqVO.getProductId());
  134 +//
  135 +// // 设置设备上下文(使用产品信息,没有具体设备)
  136 +// context.withDeviceContext(product.getProductKey(), null);
  137 +//
  138 +// // 设置输入参数
  139 +// Map<String, Object> params = new HashMap<>();
  140 +// params.put("input", testReqVO.getTestInput());
  141 +// params.put("productKey", product.getProductKey());
  142 +// params.put("scriptType", testReqVO.getScriptType());
  143 +//
  144 +// // 根据脚本类型设置特定参数
  145 +// switch (testReqVO.getScriptType()) {
  146 +// case 1: // PROPERTY_PARSER
  147 +// params.put("method", "property");
  148 +// break;
  149 +// case 2: // EVENT_PARSER
  150 +// params.put("method", "event");
  151 +// params.put("identifier", "default");
  152 +// break;
  153 +// case 3: // COMMAND_ENCODER
  154 +// params.put("method", "command");
  155 +// break;
  156 +// default:
  157 +// // 默认不添加额外参数
  158 +// }
  159 +//
  160 +// // 添加所有参数到上下文
  161 +// for (Map.Entry<String, Object> entry : params.entrySet()) {
  162 +// context.setParameter(entry.getKey(), entry.getValue());
  163 +// }
  164 +//
  165 +// // 执行脚本
  166 +// Object result = scriptService.executeScript(
  167 +// testReqVO.getScriptLanguage(),
  168 +// testReqVO.getScriptContent(),
  169 +// context);
  170 +//
  171 +// // 更新测试结果(如果是已保存的脚本)
  172 +// if (existingScript != null) {
  173 +// IotProductScriptDO updateObj = new IotProductScriptDO();
  174 +// updateObj.setId(existingScript.getId());
  175 +// updateObj.setLastTestTime(LocalDateTime.now());
  176 +// updateObj.setLastTestResult(1); // 1表示成功
  177 +// productScriptMapper.updateById(updateObj);
  178 +// }
  179 +//
  180 +// long executionTime = System.currentTimeMillis() - startTime;
  181 +// return IotProductScriptTestRespVO.success(result, executionTime);
  182 +//
  183 +// } catch (Exception e) {
  184 +// log.error("[testProductScript][测试脚本异常]", e);
  185 +//
  186 +// // 如果是已保存的脚本,更新测试失败状态
  187 +// if (testReqVO.getId() != null) {
  188 +// try {
  189 +// IotProductScriptDO updateObj = new IotProductScriptDO();
  190 +// updateObj.setId(testReqVO.getId());
  191 +// updateObj.setLastTestTime(LocalDateTime.now());
  192 +// updateObj.setLastTestResult(0); // 0表示失败
  193 +// productScriptMapper.updateById(updateObj);
  194 +// } catch (Exception ex) {
  195 +// log.error("[testProductScript][更新脚本测试结果异常]", ex);
  196 +// }
  197 +// }
  198 +//
  199 +// long executionTime = System.currentTimeMillis() - startTime;
  200 +// return IotProductScriptTestRespVO.error(e.getMessage(), executionTime);
  201 +// }
  202 + return null;
207 203 }
208 204  
209 205 @Override
... ...
yudao-module-iot/yudao-module-iot-components/README.md 0 → 100644
  1 +# IOT 组件使用说明
  2 +
  3 +## 组件介绍
  4 +
  5 +该模块包含多个 IoT 设备连接组件,提供不同的通信协议支持:
  6 +
  7 +- `yudao-module-iot-component-core`: 核心接口和通用类
  8 +- `yudao-module-iot-component-http`: 基于 HTTP 协议的设备通信组件
  9 +- `yudao-module-iot-component-emqx`: 基于 MQTT/EMQX 的设备通信组件
  10 +
  11 +## 组件架构
  12 +
  13 +### 架构设计
  14 +
  15 +各组件采用统一的架构设计和命名规范:
  16 +
  17 +- 配置类: `IotComponentXxxAutoConfiguration` - 提供Bean定义和组件初始化逻辑
  18 +- 属性类: `IotComponentXxxProperties` - 定义组件的配置属性
  19 +- 下行接口: `*DownstreamHandler` - 处理从平台到设备的下行通信
  20 +- 上行接口: `*UpstreamServer` - 处理从设备到平台的上行通信
  21 +
  22 +### Bean 命名规范
  23 +
  24 +为避免 Bean 冲突,各个组件中的 Bean 已添加特定前缀:
  25 +
  26 +- HTTP 组件: `httpDeviceUpstreamServer`, `httpDeviceDownstreamHandler`
  27 +- EMQX 组件: `emqxDeviceUpstreamServer`, `emqxDeviceDownstreamHandler`
  28 +
  29 +### 组件启用规则
  30 +
  31 +现在系统支持同时使用多个组件,但有以下规则:
  32 +
  33 +1. 当`yudao.iot.component.emqx.enabled=true`时,核心模块将优先使用EMQX组件
  34 +2. 如果同时启用了多个组件,需要在业务代码中使用`@Qualifier`指定要使用的具体实现
  35 +
  36 +> **重要提示:**
  37 +> 1. 组件库内部的默认配置文件**不会**被自动加载。必须将上述配置添加到主应用的配置文件中。
  38 +> 2. 所有配置项现在都已增加空值处理,配置缺失时将使用合理的默认值
  39 +> 3. `mqtt-host` 是唯一必须配置的参数,其他参数均有默认值
  40 +> 4. `mqtt-ssl` 和 `auth-port` 缺失时的默认值分别为 `false` 和 `8080`
  41 +> 5. `mqtt-topics` 缺失时将使用默认主题 `/device/#`
  42 +
  43 +### 如何引用特定的 Bean
  44 +
  45 +在其他组件中引用这些 Bean 时,需要使用 `@Qualifier` 注解指定 Bean 名称:
  46 +
  47 +```java
  48 +import org.springframework.beans.factory.annotation.Autowired;
  49 +import org.springframework.beans.factory.annotation.Qualifier;
  50 +import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
  51 +
  52 +@Service
  53 +public class YourServiceClass {
  54 +
  55 + // 注入 HTTP 组件的下行处理器
  56 + @Autowired
  57 + @Qualifier("httpDeviceDownstreamHandler")
  58 + private IotDeviceDownstreamHandler httpDeviceDownstreamHandler;
  59 +
  60 + // 注入 EMQX 组件的下行处理器
  61 + @Autowired
  62 + @Qualifier("emqxDeviceDownstreamHandler")
  63 + private IotDeviceDownstreamHandler emqxDeviceDownstreamHandler;
  64 +
  65 + // 使用示例
  66 + public void example() {
  67 + // 使用 HTTP 组件
  68 + httpDeviceDownstreamHandler.invokeDeviceService(...);
  69 +
  70 + // 使用 EMQX 组件
  71 + emqxDeviceDownstreamHandler.invokeDeviceService(...);
  72 + }
  73 +}
  74 +```
  75 +
  76 +### 组件选择指南
  77 +
  78 +- **HTTP 组件**:适用于简单场景,设备通过 HTTP 接口与平台通信
  79 +- **EMQX 组件**:适用于实时性要求高的场景,基于 MQTT 协议,支持发布/订阅模式
  80 +
  81 +## 常见问题
  82 +
  83 +### 1. 配置未加载问题
  84 +
  85 +如果遇到以下日志:
  86 +
  87 +```
  88 +MQTT配置: host=null, port=null, username=null, ssl=null
  89 +[connectMqtt][MQTT Host为null,无法连接]
  90 +```
  91 +
  92 +这表明配置没有被正确加载。请确保:
  93 +
  94 +1. 在主应用的配置文件中(如 `application.yml` 或 `application-dev.yml`)添加了必要的 EMQX 配置
  95 +2. 配置前缀正确:`yudao.iot.component.emqx`
  96 +3. 配置了必要的 `mqtt-host` 属性
  97 +
  98 +### 2. mqttSsl 空指针异常
  99 +
  100 +如果遇到以下错误:
  101 +
  102 +```
  103 +Cannot invoke "java.lang.Boolean.booleanValue()" because the return value of "cn.iocoder.yudao.module.iot.component.emqx.config.IotEmqxComponentProperties.getMqttSsl()" is null
  104 +```
  105 +
  106 +此问题已通过代码修复,现在会自动使用默认值 `false`。同样适用于其他配置项的空值问题。
  107 +
  108 +### 3. authPort 空指针异常
  109 +
  110 +如果遇到以下错误:
  111 +
  112 +```
  113 +Cannot invoke "java.lang.Integer.intValue()" because the return value of "cn.iocoder.yudao.module.iot.component.emqx.config.IotEmqxComponentProperties.getAuthPort()" is null
  114 +```
  115 +
  116 +此问题已通过代码修复,现在会自动使用默认值 `8080`。
  117 +
  118 +### 4. Bean注入问题
  119 +
  120 +如果遇到以下错误:
  121 +
  122 +```
  123 +Parameter 1 of method deviceDownstreamServer in IotPluginCommonAutoConfiguration required a single bean, but 2 were found
  124 +```
  125 +
  126 +此问题已通过修改核心配置类来解决。现在系统会根据组件的启用状态自动选择合适的实现:
  127 +
  128 +1. 优先使用EMQX组件(当`yudao.iot.component.emqx.enabled=true`时)
  129 +2. 如果EMQX未启用,则使用HTTP组件(当`yudao.iot.component.http.enabled=true`时)
  130 +
  131 +如果需要同时使用两个组件,业务代码中必须使用`@Qualifier`明确指定要使用的Bean。
  132 +
  133 +### 5. 使用默认配置
  134 +
  135 +组件现已加入完善的默认配置和空值处理机制,使配置更加灵活。但需要注意的是,这些默认配置值必须通过在主应用配置文件中设置相应的属性才能生效。
... ...
yudao-module-iot/yudao-module-iot-components/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <parent>
  6 + <artifactId>yudao-module-iot</artifactId>
  7 + <groupId>cn.iocoder.boot</groupId>
  8 + <version>${revision}</version>
  9 + </parent>
  10 + <modelVersion>4.0.0</modelVersion>
  11 +
  12 + <artifactId>yudao-module-iot-components</artifactId>
  13 + <packaging>pom</packaging>
  14 +
  15 + <name>${project.artifactId}</name>
  16 + <description>
  17 + 物联网组件模块,提供与物联网设备通讯、管理的组件实现
  18 + </description>
  19 +
  20 + <modules>
  21 + <module>yudao-module-iot-component-core</module>
  22 + <module>yudao-module-iot-component-http</module>
  23 + <module>yudao-module-iot-component-emqx</module>
  24 + </modules>
  25 +
  26 +</project>
0 27 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <parent>
  6 + <artifactId>yudao-module-iot-components</artifactId>
  7 + <groupId>cn.iocoder.boot</groupId>
  8 + <version>${revision}</version>
  9 + </parent>
  10 + <modelVersion>4.0.0</modelVersion>
  11 +
  12 + <artifactId>yudao-module-iot-component-core</artifactId>
  13 + <packaging>jar</packaging>
  14 +
  15 + <name>${project.artifactId}</name>
  16 + <description>
  17 + 物联网组件核心模块
  18 + </description>
  19 +
  20 + <dependencies>
  21 + <dependency>
  22 + <groupId>cn.iocoder.boot</groupId>
  23 + <artifactId>yudao-module-iot-api</artifactId>
  24 + <version>${revision}</version>
  25 + </dependency>
  26 +
  27 + <dependency>
  28 + <groupId>org.springframework.boot</groupId>
  29 + <artifactId>spring-boot-starter</artifactId>
  30 + </dependency>
  31 +
  32 + <!-- Web 相关 -->
  33 + <dependency>
  34 + <groupId>org.springframework</groupId>
  35 + <artifactId>spring-web</artifactId>
  36 + </dependency>
  37 +
  38 + <!-- 工具类相关 -->
  39 + <dependency>
  40 + <groupId>io.vertx</groupId>
  41 + <artifactId>vertx-web</artifactId>
  42 + <optional>true</optional>
  43 + </dependency>
  44 +
  45 + <!-- 参数校验 -->
  46 + <dependency>
  47 + <groupId>org.springframework.boot</groupId>
  48 + <artifactId>spring-boot-starter-validation</artifactId>
  49 + <optional>true</optional>
  50 + </dependency>
  51 + </dependencies>
  52 +</project>
0 53 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/java/cn/iocoder/yudao/module/iot/component/core/config/IotComponentCommonAutoConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.core.config;
  2 +
  3 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  4 +import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
  5 +import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamServer;
  6 +import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentInstanceHeartbeatJob;
  7 +import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
  8 +import cn.iocoder.yudao.module.iot.component.core.upstream.IotDeviceUpstreamClient;
  9 +import org.springframework.beans.factory.annotation.Qualifier;
  10 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  11 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  12 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  13 +import org.springframework.context.annotation.Bean;
  14 +import org.springframework.scheduling.annotation.EnableScheduling;
  15 +
  16 +/**
  17 + * IoT 组件的通用自动配置类
  18 + *
  19 + * @author haohao
  20 + */
  21 +@AutoConfiguration
  22 +@EnableConfigurationProperties(IotComponentCommonProperties.class)
  23 +@EnableScheduling // 开启定时任务,因为 IotComponentInstanceHeartbeatJob 是一个定时任务
  24 +public class IotComponentCommonAutoConfiguration {
  25 +
  26 + /**
  27 + * 创建EMQX设备下行服务器
  28 + * 当yudao.iot.component.emqx.enabled=true时,使用emqxDeviceDownstreamHandler
  29 + */
  30 + @Bean
  31 + @ConditionalOnProperty(prefix = "yudao.iot.component.emqx", name = "enabled", havingValue = "true")
  32 + public IotDeviceDownstreamServer emqxDeviceDownstreamServer(IotComponentCommonProperties properties,
  33 + @Qualifier("emqxDeviceDownstreamHandler") IotDeviceDownstreamHandler deviceDownstreamHandler) {
  34 + return new IotDeviceDownstreamServer(properties, deviceDownstreamHandler);
  35 + }
  36 +
  37 + @Bean(initMethod = "init", destroyMethod = "stop")
  38 + public IotComponentInstanceHeartbeatJob pluginInstanceHeartbeatJob(IotDeviceUpstreamApi deviceUpstreamApi,
  39 + IotDeviceDownstreamServer deviceDownstreamServer,
  40 + IotComponentCommonProperties commonProperties,
  41 + IotComponentRegistry componentRegistry) {
  42 + return new IotComponentInstanceHeartbeatJob(deviceUpstreamApi, deviceDownstreamServer, commonProperties,
  43 + componentRegistry);
  44 + }
  45 +
  46 + @Bean
  47 + public IotDeviceUpstreamClient deviceUpstreamClient() {
  48 + return new IotDeviceUpstreamClient();
  49 + }
  50 +}
0 51 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/java/cn/iocoder/yudao/module/iot/component/core/config/IotComponentCommonProperties.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.core.config;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +import org.springframework.validation.annotation.Validated;
  6 +
  7 +/**
  8 + * IoT 组件通用配置属性
  9 + *
  10 + * @author haohao
  11 + */
  12 +@ConfigurationProperties(prefix = "yudao.iot.component.core")
  13 +@Validated
  14 +@Data
  15 +public class IotComponentCommonProperties {
  16 +
  17 + /**
  18 + * 组件的唯一标识
  19 + * <p>
  20 + * 注意:该值将在运行时由各组件设置,不再从配置读取
  21 + */
  22 + private String pluginKey;
  23 +
  24 +}
0 25 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/java/cn/iocoder/yudao/module/iot/component/core/downstream/IotDeviceDownstreamHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.core.downstream;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
  5 +
  6 +/**
  7 + * IoT 设备下行处理器
  8 + * <p>
  9 + * 目的:每个 plugin 需要实现,用于处理 server 下行的指令(请求),从而实现从 server => plugin => device 的下行流程
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +public interface IotDeviceDownstreamHandler {
  14 +
  15 + /**
  16 + * 调用设备服务
  17 + *
  18 + * @param invokeReqDTO 调用设备服务的请求
  19 + * @return 是否成功
  20 + */
  21 + CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO);
  22 +
  23 + /**
  24 + * 获取设备属性
  25 + *
  26 + * @param getReqDTO 获取设备属性的请求
  27 + * @return 是否成功
  28 + */
  29 + CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO);
  30 +
  31 + /**
  32 + * 设置设备属性
  33 + *
  34 + * @param setReqDTO 设置设备属性的请求
  35 + * @return 是否成功
  36 + */
  37 + CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO);
  38 +
  39 + /**
  40 + * 设置设备配置
  41 + *
  42 + * @param setReqDTO 设置设备配置的请求
  43 + * @return 是否成功
  44 + */
  45 + CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO);
  46 +
  47 + /**
  48 + * 升级设备 OTA
  49 + *
  50 + * @param upgradeReqDTO 升级设备 OTA 的请求
  51 + * @return 是否成功
  52 + */
  53 + CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO);
  54 +
  55 +}
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/java/cn/iocoder/yudao/module/iot/component/core/downstream/IotDeviceDownstreamServer.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.core.downstream;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
  5 +import cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonProperties;
  6 +import lombok.RequiredArgsConstructor;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * IoT 设备下行服务,直接转发给 device 设备
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +@RequiredArgsConstructor
  16 +public class IotDeviceDownstreamServer {
  17 +
  18 + private final IotComponentCommonProperties properties;
  19 + private final IotDeviceDownstreamHandler deviceDownstreamHandler;
  20 +
  21 + /**
  22 + * 调用设备服务
  23 + *
  24 + * @param invokeReqDTO 调用设备服务的请求
  25 + * @return 是否成功
  26 + */
  27 + public CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO) {
  28 + return deviceDownstreamHandler.invokeDeviceService(invokeReqDTO);
  29 + }
  30 +
  31 + /**
  32 + * 获取设备属性
  33 + *
  34 + * @param getReqDTO 获取设备属性的请求
  35 + * @return 是否成功
  36 + */
  37 + public CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) {
  38 + return deviceDownstreamHandler.getDeviceProperty(getReqDTO);
  39 + }
  40 +
  41 + /**
  42 + * 设置设备属性
  43 + *
  44 + * @param setReqDTO 设置设备属性的请求
  45 + * @return 是否成功
  46 + */
  47 + public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO) {
  48 + return deviceDownstreamHandler.setDeviceProperty(setReqDTO);
  49 + }
  50 +
  51 + /**
  52 + * 设置设备配置
  53 + *
  54 + * @param setReqDTO 设置设备配置的请求
  55 + * @return 是否成功
  56 + */
  57 + public CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) {
  58 + return deviceDownstreamHandler.setDeviceConfig(setReqDTO);
  59 + }
  60 +
  61 + /**
  62 + * 升级设备 OTA
  63 + *
  64 + * @param upgradeReqDTO 升级设备 OTA 的请求
  65 + * @return 是否成功
  66 + */
  67 + public CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) {
  68 + return deviceDownstreamHandler.upgradeDeviceOta(upgradeReqDTO);
  69 + }
  70 +
  71 + /**
  72 + * 获得内部组件标识
  73 + *
  74 + * @return 组件标识
  75 + */
  76 + public String getComponentId() {
  77 + return properties.getPluginKey();
  78 + }
  79 +
  80 +}
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/java/cn/iocoder/yudao/module/iot/component/core/heartbeat/IotComponentInstanceHeartbeatJob.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.core.heartbeat;
  2 +
  3 +import cn.hutool.system.SystemUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  6 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
  7 +import cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonProperties;
  8 +import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamServer;
  9 +import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry.IotComponentInfo;
  10 +import lombok.RequiredArgsConstructor;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +import org.springframework.scheduling.annotation.Scheduled;
  13 +
  14 +import java.lang.management.ManagementFactory;
  15 +import java.util.concurrent.TimeUnit;
  16 +
  17 +/**
  18 + * IoT 组件实例心跳定时任务
  19 + * <p>
  20 + * 将组件的状态定时上报给 server 服务器
  21 + * <p>
  22 + * 用于定时发送心跳给服务端
  23 + */
  24 +@RequiredArgsConstructor
  25 +@Slf4j
  26 +public class IotComponentInstanceHeartbeatJob {
  27 +
  28 + /**
  29 + * 内嵌模式的端口值(固定为0)
  30 + */
  31 + private static final Integer EMBEDDED_PORT = 0;
  32 +
  33 + private final IotDeviceUpstreamApi deviceUpstreamApi;
  34 + private final IotDeviceDownstreamServer deviceDownstreamServer;
  35 + private final IotComponentCommonProperties commonProperties;
  36 + private final IotComponentRegistry componentRegistry;
  37 +
  38 + /**
  39 + * 初始化方法由Spring调用
  40 + * 注册当前组件并发送上线心跳
  41 + */
  42 + public void init() {
  43 + // 将当前组件注册到注册表
  44 + String processId = getProcessId();
  45 + String hostIp = SystemUtil.getHostInfo().getAddress();
  46 +
  47 + // 注册当前组件
  48 + componentRegistry.registerComponent(
  49 + commonProperties.getPluginKey(),
  50 + hostIp,
  51 + EMBEDDED_PORT,
  52 + processId);
  53 +
  54 + // 发送所有组件的上线心跳
  55 + for (IotComponentInfo component : componentRegistry.getAllComponents()) {
  56 + try {
  57 + CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(
  58 + buildPluginInstanceHeartbeatReqDTO(component, true));
  59 + log.info("[init][组件({})上线结果:{})]", component.getPluginKey(), result);
  60 + } catch (Exception e) {
  61 + log.error("[init][组件({})上线发送异常]", component.getPluginKey(), e);
  62 + }
  63 + }
  64 + }
  65 +
  66 + /**
  67 + * 停止方法由Spring调用
  68 + * 发送下线心跳并注销组件
  69 + */
  70 + public void stop() {
  71 + // 发送所有组件的下线心跳
  72 + for (IotComponentInfo component : componentRegistry.getAllComponents()) {
  73 + try {
  74 + CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(
  75 + buildPluginInstanceHeartbeatReqDTO(component, false));
  76 + log.info("[stop][组件({})下线结果:{})]", component.getPluginKey(), result);
  77 + } catch (Exception e) {
  78 + log.error("[stop][组件({})下线发送异常]", component.getPluginKey(), e);
  79 + }
  80 + }
  81 +
  82 + // 注销当前组件
  83 + componentRegistry.unregisterComponent(commonProperties.getPluginKey());
  84 + }
  85 +
  86 + /**
  87 + * 定时发送心跳
  88 + */
  89 + @Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES) // 1 分钟执行一次
  90 + public void execute() {
  91 + // 发送所有组件的心跳
  92 + for (IotComponentInfo component : componentRegistry.getAllComponents()) {
  93 + try {
  94 + CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(
  95 + buildPluginInstanceHeartbeatReqDTO(component, true));
  96 + log.info("[execute][组件({})心跳结果:{})]", component.getPluginKey(), result);
  97 + } catch (Exception e) {
  98 + log.error("[execute][组件({})心跳发送异常]", component.getPluginKey(), e);
  99 + }
  100 + }
  101 + }
  102 +
  103 + /**
  104 + * 构建心跳DTO
  105 + *
  106 + * @param component 组件信息
  107 + * @param online 是否在线
  108 + * @return 心跳DTO
  109 + */
  110 + private IotPluginInstanceHeartbeatReqDTO buildPluginInstanceHeartbeatReqDTO(IotComponentInfo component,
  111 + Boolean online) {
  112 + return new IotPluginInstanceHeartbeatReqDTO()
  113 + .setPluginKey(component.getPluginKey())
  114 + .setProcessId(component.getProcessId())
  115 + .setHostIp(component.getHostIp())
  116 + .setDownstreamPort(component.getDownstreamPort())
  117 + .setOnline(online);
  118 + }
  119 +
  120 + /**
  121 + * 获取当前进程ID
  122 + *
  123 + * @return 进程ID
  124 + */
  125 + private String getProcessId() {
  126 + // 获取进程的 name
  127 + String name = ManagementFactory.getRuntimeMXBean().getName();
  128 + // 分割名称,格式为 pid@hostname
  129 + return name.split("@")[0];
  130 + }
  131 +}
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/java/cn/iocoder/yudao/module/iot/component/core/heartbeat/IotComponentRegistry.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.core.heartbeat;
  2 +
  3 +import lombok.Data;
  4 +import lombok.ToString;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.springframework.stereotype.Component;
  7 +
  8 +import java.util.Collection;
  9 +import java.util.Map;
  10 +import java.util.concurrent.ConcurrentHashMap;
  11 +
  12 +/**
  13 + * IoT 组件注册表
  14 + * <p>
  15 + * 用于管理多个组件的注册信息,解决多组件心跳问题
  16 + */
  17 +@Component
  18 +@Slf4j
  19 +public class IotComponentRegistry {
  20 +
  21 + /**
  22 + * 组件信息
  23 + */
  24 + @Data
  25 + @ToString
  26 + public static class IotComponentInfo {
  27 + /**
  28 + * 组件Key
  29 + */
  30 + private final String pluginKey;
  31 + /**
  32 + * 主机IP
  33 + */
  34 + private final String hostIp;
  35 + /**
  36 + * 下游端口
  37 + */
  38 + private final Integer downstreamPort;
  39 + /**
  40 + * 进程ID
  41 + */
  42 + private final String processId;
  43 + }
  44 +
  45 + /**
  46 + * 组件映射表,key为组件Key
  47 + */
  48 + private final Map<String, IotComponentInfo> components = new ConcurrentHashMap<>();
  49 +
  50 + /**
  51 + * 注册组件
  52 + *
  53 + * @param pluginKey 组件Key
  54 + * @param hostIp 主机IP
  55 + * @param downstreamPort 下游端口
  56 + * @param processId 进程ID
  57 + */
  58 + public void registerComponent(String pluginKey, String hostIp, Integer downstreamPort, String processId) {
  59 + log.info("[registerComponent][注册组件, pluginKey={}, hostIp={}, downstreamPort={}, processId={}]",
  60 + pluginKey, hostIp, downstreamPort, processId);
  61 + components.put(pluginKey, new IotComponentInfo(pluginKey, hostIp, downstreamPort, processId));
  62 + }
  63 +
  64 + /**
  65 + * 注销组件
  66 + *
  67 + * @param pluginKey 组件Key
  68 + */
  69 + public void unregisterComponent(String pluginKey) {
  70 + log.info("[unregisterComponent][注销组件, pluginKey={}]", pluginKey);
  71 + components.remove(pluginKey);
  72 + }
  73 +
  74 + /**
  75 + * 获取所有组件
  76 + *
  77 + * @return 所有组件集合
  78 + */
  79 + public Collection<IotComponentInfo> getAllComponents() {
  80 + return components.values();
  81 + }
  82 +
  83 + /**
  84 + * 获取指定组件
  85 + *
  86 + * @param pluginKey 组件Key
  87 + * @return 组件信息
  88 + */
  89 + public IotComponentInfo getComponent(String pluginKey) {
  90 + return components.get(pluginKey);
  91 + }
  92 +}
0 93 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/java/cn/iocoder/yudao/module/iot/component/core/pojo/IotStandardResponse.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.core.pojo;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT 标准协议响应实体类
  7 + * <p>
  8 + * 用于统一 MQTT 和 HTTP 的响应格式
  9 + *
  10 + * @author haohao
  11 + */
  12 +@Data
  13 +public class IotStandardResponse {
  14 +
  15 + /**
  16 + * 消息ID
  17 + */
  18 + private String id;
  19 +
  20 + /**
  21 + * 状态码
  22 + */
  23 + private Integer code;
  24 +
  25 + /**
  26 + * 响应数据
  27 + */
  28 + private Object data;
  29 +
  30 + /**
  31 + * 响应消息
  32 + */
  33 + private String message;
  34 +
  35 + /**
  36 + * 方法名
  37 + */
  38 + private String method;
  39 +
  40 + /**
  41 + * 协议版本
  42 + */
  43 + private String version;
  44 +
  45 + /**
  46 + * 创建成功响应
  47 + *
  48 + * @param id 消息ID
  49 + * @param method 方法名
  50 + * @return 成功响应
  51 + */
  52 + public static IotStandardResponse success(String id, String method) {
  53 + return success(id, method, null);
  54 + }
  55 +
  56 + /**
  57 + * 创建成功响应
  58 + *
  59 + * @param id 消息ID
  60 + * @param method 方法名
  61 + * @param data 响应数据
  62 + * @return 成功响应
  63 + */
  64 + public static IotStandardResponse success(String id, String method, Object data) {
  65 + return new IotStandardResponse()
  66 + .setId(id)
  67 + .setCode(200)
  68 + .setData(data)
  69 + .setMessage("success")
  70 + .setMethod(method)
  71 + .setVersion("1.0");
  72 + }
  73 +
  74 + /**
  75 + * 创建错误响应
  76 + *
  77 + * @param id 消息ID
  78 + * @param method 方法名
  79 + * @param code 错误码
  80 + * @param message 错误消息
  81 + * @return 错误响应
  82 + */
  83 + public static IotStandardResponse error(String id, String method, Integer code, String message) {
  84 + return new IotStandardResponse()
  85 + .setId(id)
  86 + .setCode(code)
  87 + .setData(null)
  88 + .setMessage(message)
  89 + .setMethod(method)
  90 + .setVersion("1.0");
  91 + }
  92 +
  93 +}
0 94 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/java/cn/iocoder/yudao/module/iot/component/core/upstream/IotDeviceUpstreamClient.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.core.upstream;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  5 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.*;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +
  8 +import javax.annotation.Resource;
  9 +
  10 +/**
  11 + * 设备数据 Upstream 上行客户端
  12 + * <p>
  13 + * 直接调用 IotDeviceUpstreamApi 接口
  14 + *
  15 + * @author haohao
  16 + */
  17 +@Slf4j
  18 +public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi {
  19 +
  20 + @Resource
  21 + private IotDeviceUpstreamApi deviceUpstreamApi;
  22 +
  23 + @Override
  24 + public CommonResult<Boolean> updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) {
  25 + return deviceUpstreamApi.updateDeviceState(updateReqDTO);
  26 + }
  27 +
  28 + @Override
  29 + public CommonResult<Boolean> reportDeviceEvent(IotDeviceEventReportReqDTO reportReqDTO) {
  30 + return deviceUpstreamApi.reportDeviceEvent(reportReqDTO);
  31 + }
  32 +
  33 + @Override
  34 + public CommonResult<Boolean> registerDevice(IotDeviceRegisterReqDTO registerReqDTO) {
  35 + return deviceUpstreamApi.registerDevice(registerReqDTO);
  36 + }
  37 +
  38 + @Override
  39 + public CommonResult<Boolean> registerSubDevice(IotDeviceRegisterSubReqDTO registerReqDTO) {
  40 + return deviceUpstreamApi.registerSubDevice(registerReqDTO);
  41 + }
  42 +
  43 + @Override
  44 + public CommonResult<Boolean> addDeviceTopology(IotDeviceTopologyAddReqDTO addReqDTO) {
  45 + return deviceUpstreamApi.addDeviceTopology(addReqDTO);
  46 + }
  47 +
  48 + @Override
  49 + public CommonResult<Boolean> authenticateEmqxConnection(IotDeviceEmqxAuthReqDTO authReqDTO) {
  50 + return deviceUpstreamApi.authenticateEmqxConnection(authReqDTO);
  51 + }
  52 +
  53 + @Override
  54 + public CommonResult<Boolean> reportDeviceProperty(IotDevicePropertyReportReqDTO reportReqDTO) {
  55 + return deviceUpstreamApi.reportDeviceProperty(reportReqDTO);
  56 + }
  57 +
  58 + @Override
  59 + public CommonResult<Boolean> heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) {
  60 + return deviceUpstreamApi.heartbeatPluginInstance(heartbeatReqDTO);
  61 + }
  62 +}
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/java/cn/iocoder/yudao/module/iot/component/core/util/IotPluginCommonUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.core.util;
  2 +
  3 +import cn.hutool.core.util.IdUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.hutool.system.SystemUtil;
  6 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  7 +import cn.iocoder.yudao.module.iot.component.core.pojo.IotStandardResponse;
  8 +import io.vertx.core.http.HttpHeaders;
  9 +import io.vertx.ext.web.RoutingContext;
  10 +import org.springframework.http.MediaType;
  11 +
  12 +/**
  13 + * IoT 插件的通用工具类
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public class IotPluginCommonUtils {
  18 +
  19 + /**
  20 + * 流程实例的进程编号
  21 + */
  22 + private static String processId;
  23 +
  24 + public static String getProcessId() {
  25 + if (StrUtil.isEmpty(processId)) {
  26 + initProcessId();
  27 + }
  28 + return processId;
  29 + }
  30 +
  31 + private synchronized static void initProcessId() {
  32 + processId = String.format("%s@%d@%s", // IP@PID@${uuid}
  33 + SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID(), IdUtil.fastSimpleUUID());
  34 + }
  35 +
  36 + /**
  37 + * 将对象转换为JSON字符串后写入HTTP响应
  38 + *
  39 + * @param routingContext 路由上下文
  40 + * @param data 数据对象
  41 + */
  42 + @SuppressWarnings("deprecation")
  43 + public static void writeJsonResponse(RoutingContext routingContext, Object data) {
  44 + routingContext.response()
  45 + .setStatusCode(200)
  46 + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
  47 + .end(JsonUtils.toJsonString(data));
  48 + }
  49 +
  50 + /**
  51 + * 生成标准JSON格式的响应并写入HTTP响应(基于IotStandardResponse)
  52 + * <p>
  53 + * 推荐使用此方法,统一MQTT和HTTP的响应格式。使用方式:
  54 + *
  55 + * <pre>
  56 + * // 成功响应
  57 + * IotStandardResponse response = IotStandardResponse.success(requestId, method, data);
  58 + * IotPluginCommonUtils.writeJsonResponse(routingContext, response);
  59 + *
  60 + * // 错误响应
  61 + * IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method, code, message);
  62 + * IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse);
  63 + * </pre>
  64 + *
  65 + * @param routingContext 路由上下文
  66 + * @param response IotStandardResponse响应对象
  67 + */
  68 + @SuppressWarnings("deprecation")
  69 + public static void writeJsonResponse(RoutingContext routingContext, IotStandardResponse response) {
  70 + routingContext.response()
  71 + .setStatusCode(200)
  72 + .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE)
  73 + .end(JsonUtils.toJsonString(response));
  74 + }
  75 +
  76 +}
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/resources/META-INF/spring.factories 0 → 100644
  1 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2 + cn.iocoder.yudao.module.iot.component.core.config.IotPluginCommonAutoConfiguration
0 3 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-core/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 0 → 100644
  1 +cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonAutoConfiguration
0 2 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <parent>
  6 + <artifactId>yudao-module-iot-components</artifactId>
  7 + <groupId>cn.iocoder.boot</groupId>
  8 + <version>${revision}</version>
  9 + </parent>
  10 + <modelVersion>4.0.0</modelVersion>
  11 + <artifactId>yudao-module-iot-component-emqx</artifactId>
  12 + <packaging>jar</packaging>
  13 +
  14 + <name>${project.artifactId}</name>
  15 + <description>
  16 + 物联网组件 EMQX 模块
  17 + </description>
  18 +
  19 + <dependencies>
  20 + <dependency>
  21 + <groupId>cn.iocoder.boot</groupId>
  22 + <artifactId>yudao-module-iot-component-core</artifactId>
  23 + <version>${revision}</version>
  24 + </dependency>
  25 +
  26 + <!-- 工具类相关 -->
  27 + <dependency>
  28 + <groupId>io.vertx</groupId>
  29 + <artifactId>vertx-web</artifactId>
  30 + </dependency>
  31 + <dependency>
  32 + <groupId>io.vertx</groupId>
  33 + <artifactId>vertx-mqtt</artifactId>
  34 + </dependency>
  35 +
  36 + <!-- 参数校验 -->
  37 + <dependency>
  38 + <groupId>org.springframework.boot</groupId>
  39 + <artifactId>spring-boot-starter-validation</artifactId>
  40 + <optional>true</optional>
  41 + </dependency>
  42 + </dependencies>
  43 +
  44 +</project>
0 45 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/component/emqx/config/IotComponentEmqxAutoConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.emqx.config;
  2 +
  3 +import cn.hutool.core.util.IdUtil;
  4 +import cn.hutool.system.SystemUtil;
  5 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  6 +import cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonProperties;
  7 +import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
  8 +import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
  9 +import cn.iocoder.yudao.module.iot.component.emqx.downstream.IotDeviceDownstreamHandlerImpl;
  10 +import cn.iocoder.yudao.module.iot.component.emqx.upstream.IotDeviceUpstreamServer;
  11 +import io.vertx.core.Vertx;
  12 +import io.vertx.mqtt.MqttClient;
  13 +import io.vertx.mqtt.MqttClientOptions;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  16 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  17 +import org.springframework.boot.context.event.ApplicationStartedEvent;
  18 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  19 +import org.springframework.context.annotation.Bean;
  20 +import org.springframework.context.annotation.ComponentScan;
  21 +import org.springframework.context.event.EventListener;
  22 +
  23 +import java.lang.management.ManagementFactory;
  24 +
  25 +/**
  26 + * IoT 组件 EMQX 的自动配置类
  27 + *
  28 + * @author haohao
  29 + */
  30 +@Slf4j
  31 +@AutoConfiguration
  32 +@EnableConfigurationProperties(IotComponentEmqxProperties.class)
  33 +@ConditionalOnProperty(prefix = "yudao.iot.component.emqx", name = "enabled", havingValue = "true", matchIfMissing = false)
  34 +@ComponentScan(basePackages = {
  35 + "cn.iocoder.yudao.module.iot.component.core", // 核心包
  36 + "cn.iocoder.yudao.module.iot.component.emqx" // EMQX组件包
  37 +})
  38 +public class IotComponentEmqxAutoConfiguration {
  39 +
  40 + /**
  41 + * 组件key
  42 + */
  43 + private static final String PLUGIN_KEY = "emqx";
  44 +
  45 + public IotComponentEmqxAutoConfiguration() {
  46 + log.info("[IotComponentEmqxAutoConfiguration][已启动]");
  47 + }
  48 +
  49 + @EventListener(ApplicationStartedEvent.class)
  50 + public void initialize(ApplicationStartedEvent event) {
  51 + // 从应用上下文中获取需要的Bean
  52 + IotComponentRegistry componentRegistry = event.getApplicationContext().getBean(IotComponentRegistry.class);
  53 + IotComponentCommonProperties commonProperties = event.getApplicationContext().getBean(IotComponentCommonProperties.class);
  54 +
  55 + // 设置当前组件的核心标识
  56 + commonProperties.setPluginKey(PLUGIN_KEY);
  57 +
  58 + // 将EMQX组件注册到组件注册表
  59 + componentRegistry.registerComponent(
  60 + PLUGIN_KEY,
  61 + SystemUtil.getHostInfo().getAddress(),
  62 + 0, // 内嵌模式固定为0
  63 + getProcessId()
  64 + );
  65 +
  66 + log.info("[initialize][IoT EMQX 组件初始化完成]");
  67 + }
  68 +
  69 + @Bean
  70 + public Vertx vertx() {
  71 + return Vertx.vertx();
  72 + }
  73 +
  74 + @Bean
  75 + public MqttClient mqttClient(Vertx vertx, IotComponentEmqxProperties emqxProperties) {
  76 + log.info("MQTT配置: host={}, port={}, username={}, ssl={}",
  77 + emqxProperties.getMqttHost(), emqxProperties.getMqttPort(),
  78 + emqxProperties.getMqttUsername(), emqxProperties.getMqttSsl());
  79 +
  80 + MqttClientOptions options = new MqttClientOptions()
  81 + .setClientId("yudao-iot-downstream-" + IdUtil.fastSimpleUUID())
  82 + .setUsername(emqxProperties.getMqttUsername())
  83 + .setPassword(emqxProperties.getMqttPassword());
  84 +
  85 + if (emqxProperties.getMqttSsl() != null) {
  86 + options.setSsl(emqxProperties.getMqttSsl());
  87 + } else {
  88 + options.setSsl(false);
  89 + log.warn("MQTT SSL配置为null,默认设置为false");
  90 + }
  91 +
  92 + return MqttClient.create(vertx, options);
  93 + }
  94 +
  95 + @Bean(name = "emqxDeviceUpstreamServer", initMethod = "start", destroyMethod = "stop")
  96 + public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi,
  97 + IotComponentEmqxProperties emqxProperties,
  98 + Vertx vertx,
  99 + MqttClient mqttClient,
  100 + IotComponentRegistry componentRegistry) {
  101 + return new IotDeviceUpstreamServer(emqxProperties, deviceUpstreamApi, vertx, mqttClient, componentRegistry);
  102 + }
  103 +
  104 + @Bean(name = "emqxDeviceDownstreamHandler")
  105 + public IotDeviceDownstreamHandler deviceDownstreamHandler(MqttClient mqttClient) {
  106 + return new IotDeviceDownstreamHandlerImpl(mqttClient);
  107 + }
  108 +
  109 + /**
  110 + * 获取当前进程ID
  111 + *
  112 + * @return 进程ID
  113 + */
  114 + private String getProcessId() {
  115 + // 获取进程的 name
  116 + String name = ManagementFactory.getRuntimeMXBean().getName();
  117 + // 分割名称,格式为 pid@hostname
  118 + String pid = name.split("@")[0];
  119 + return pid;
  120 + }
  121 +}
0 122 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/component/emqx/config/IotComponentEmqxProperties.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.emqx.config;
  2 +
  3 +import jakarta.validation.constraints.NotBlank;
  4 +import jakarta.validation.constraints.NotEmpty;
  5 +import jakarta.validation.constraints.NotNull;
  6 +import lombok.Data;
  7 +import org.springframework.boot.context.properties.ConfigurationProperties;
  8 +
  9 +/**
  10 + * IoT EMQX组件配置属性
  11 + */
  12 +@ConfigurationProperties(prefix = "yudao.iot.component.emqx")
  13 +@Data
  14 +public class IotComponentEmqxProperties {
  15 +
  16 + /**
  17 + * 是否启用EMQX组件
  18 + */
  19 + private Boolean enabled;
  20 +
  21 + /**
  22 + * 服务主机
  23 + */
  24 + @NotBlank(message = "MQTT服务器主机不能为空")
  25 + private String mqttHost;
  26 + /**
  27 + * 服务端口
  28 + */
  29 + @NotNull(message = "MQTT服务器端口不能为空")
  30 + private Integer mqttPort;
  31 + /**
  32 + * 服务用户名
  33 + */
  34 + @NotBlank(message = "MQTT服务器用户名不能为空")
  35 + private String mqttUsername;
  36 + /**
  37 + * 服务密码
  38 + */
  39 + @NotBlank(message = "MQTT服务器密码不能为空")
  40 + private String mqttPassword;
  41 + /**
  42 + * 是否启用 SSL
  43 + */
  44 + @NotNull(message = "MQTT SSL配置不能为空")
  45 + private Boolean mqttSsl;
  46 +
  47 + /**
  48 + * 订阅的主题列表
  49 + */
  50 + @NotEmpty(message = "MQTT订阅主题不能为空")
  51 + private String[] mqttTopics;
  52 +
  53 + /**
  54 + * 认证端口
  55 + */
  56 + @NotNull(message = "认证端口不能为空")
  57 + private Integer authPort;
  58 +
  59 +}
0 60 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/component/emqx/downstream/IotDeviceDownstreamHandlerImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.emqx.downstream;
  2 +
  3 +import cn.hutool.core.util.IdUtil;
  4 +import cn.hutool.json.JSONObject;
  5 +import cn.hutool.json.JSONUtil;
  6 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  7 +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
  8 +import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
  9 +import io.netty.handler.codec.mqtt.MqttQoS;
  10 +import io.vertx.core.buffer.Buffer;
  11 +import io.vertx.mqtt.MqttClient;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +
  14 +import java.util.Map;
  15 +
  16 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.MQTT_TOPIC_ILLEGAL;
  17 +
  18 +/**
  19 + * EMQX 插件的 {@link IotDeviceDownstreamHandler} 实现类
  20 + *
  21 + * @author 芋道源码
  22 + */
  23 +@Slf4j
  24 +public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler {
  25 +
  26 + private static final String SYS_TOPIC_PREFIX = "/sys/";
  27 +
  28 + // TODO @haohao:是不是可以类似 IotDeviceConfigSetVertxHandler 的建议,抽到统一的枚举类
  29 + // TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈。;回复 都使用 Alink 格式,方便后续扩展。
  30 + // 设备服务调用 标准 JSON
  31 + // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}
  32 + // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/${tsl.service.identifier}_reply
  33 + private static final String SERVICE_TOPIC_PREFIX = "/thing/service/";
  34 +
  35 + // 设置设备属性 标准 JSON
  36 + // 请求Topic:/sys/${productKey}/${deviceName}/thing/service/property/set
  37 + // 响应Topic:/sys/${productKey}/${deviceName}/thing/service/property/set_reply
  38 + private static final String PROPERTY_SET_TOPIC = "/thing/service/property/set";
  39 +
  40 + private final MqttClient mqttClient;
  41 +
  42 + /**
  43 + * 构造函数
  44 + *
  45 + * @param mqttClient MQTT客户端
  46 + */
  47 + public IotDeviceDownstreamHandlerImpl(MqttClient mqttClient) {
  48 + this.mqttClient = mqttClient;
  49 + }
  50 +
  51 + @Override
  52 + public CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO reqDTO) {
  53 + log.info("[invokeService][开始调用设备服务][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
  54 +
  55 + // 验证参数
  56 + if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null || reqDTO.getIdentifier() == null) {
  57 + log.error("[invokeService][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
  58 + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
  59 + }
  60 +
  61 + try {
  62 + // 构建请求主题
  63 + String topic = buildServiceTopic(reqDTO.getProductKey(), reqDTO.getDeviceName(), reqDTO.getIdentifier());
  64 + // 构建请求消息
  65 + String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId();
  66 + JSONObject request = buildServiceRequest(requestId, reqDTO.getIdentifier(), reqDTO.getParams());
  67 + // 发送消息
  68 + publishMessage(topic, request);
  69 +
  70 + log.info("[invokeService][调用设备服务成功][requestId: {}][topic: {}]", requestId, topic);
  71 + return CommonResult.success(true);
  72 + } catch (Exception e) {
  73 + log.error("[invokeService][调用设备服务异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e);
  74 + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
  75 + }
  76 + }
  77 +
  78 + @Override
  79 + public CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) {
  80 + return CommonResult.success(true);
  81 + }
  82 +
  83 + @Override
  84 + public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO reqDTO) {
  85 + // 验证参数
  86 + log.info("[setProperty][开始设置设备属性][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
  87 + if (reqDTO.getProductKey() == null || reqDTO.getDeviceName() == null) {
  88 + log.error("[setProperty][参数不完整][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO));
  89 + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
  90 + }
  91 +
  92 + try {
  93 + // 构建请求主题
  94 + String topic = buildPropertySetTopic(reqDTO.getProductKey(), reqDTO.getDeviceName());
  95 + // 构建请求消息
  96 + String requestId = reqDTO.getRequestId() != null ? reqDTO.getRequestId() : generateRequestId();
  97 + JSONObject request = buildPropertySetRequest(requestId, reqDTO.getProperties());
  98 + // 发送消息
  99 + publishMessage(topic, request);
  100 +
  101 + log.info("[setProperty][设置设备属性成功][requestId: {}][topic: {}]", requestId, topic);
  102 + return CommonResult.success(true);
  103 + } catch (Exception e) {
  104 + log.error("[setProperty][设置设备属性异常][reqDTO: {}]", JSONUtil.toJsonStr(reqDTO), e);
  105 + return CommonResult.error(MQTT_TOPIC_ILLEGAL.getCode(), MQTT_TOPIC_ILLEGAL.getMsg());
  106 + }
  107 + }
  108 +
  109 + @Override
  110 + public CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) {
  111 + return CommonResult.success(true);
  112 + }
  113 +
  114 + @Override
  115 + public CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) {
  116 + return CommonResult.success(true);
  117 + }
  118 +
  119 + /**
  120 + * 构建服务调用主题
  121 + */
  122 + private String buildServiceTopic(String productKey, String deviceName, String serviceIdentifier) {
  123 + return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + SERVICE_TOPIC_PREFIX + serviceIdentifier;
  124 + }
  125 +
  126 + /**
  127 + * 构建属性设置主题
  128 + */
  129 + private String buildPropertySetTopic(String productKey, String deviceName) {
  130 + return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + PROPERTY_SET_TOPIC;
  131 + }
  132 +
  133 + // TODO @haohao:这个,后面搞个对象,会不会好点哈?
  134 +
  135 + /**
  136 + * 构建服务调用请求
  137 + */
  138 + private JSONObject buildServiceRequest(String requestId, String serviceIdentifier, Map<String, Object> params) {
  139 + return new JSONObject()
  140 + .set("id", requestId)
  141 + .set("version", "1.0")
  142 + .set("method", "thing.service." + serviceIdentifier)
  143 + .set("params", params != null ? params : new JSONObject());
  144 + }
  145 +
  146 + /**
  147 + * 构建属性设置请求
  148 + */
  149 + private JSONObject buildPropertySetRequest(String requestId, Map<String, Object> properties) {
  150 + return new JSONObject()
  151 + .set("id", requestId)
  152 + .set("version", "1.0")
  153 + .set("method", "thing.service.property.set")
  154 + .set("params", properties);
  155 + }
  156 +
  157 + /**
  158 + * 发布 MQTT 消息
  159 + */
  160 + private void publishMessage(String topic, JSONObject payload) {
  161 + mqttClient.publish(
  162 + topic,
  163 + Buffer.buffer(payload.toString()),
  164 + MqttQoS.AT_LEAST_ONCE,
  165 + false,
  166 + false);
  167 + log.info("[publishMessage][发送消息成功][topic: {}][payload: {}]", topic, payload);
  168 + }
  169 +
  170 + /**
  171 + * 生成请求 ID
  172 + */
  173 + private String generateRequestId() {
  174 + return IdUtil.fastSimpleUUID();
  175 + }
  176 +
  177 +}
0 178 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/component/emqx/upstream/IotDeviceUpstreamServer.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.emqx.upstream;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  5 +import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
  6 +import cn.iocoder.yudao.module.iot.component.emqx.config.IotComponentEmqxProperties;
  7 +import cn.iocoder.yudao.module.iot.component.emqx.upstream.router.IotDeviceAuthVertxHandler;
  8 +import cn.iocoder.yudao.module.iot.component.emqx.upstream.router.IotDeviceMqttMessageHandler;
  9 +import cn.iocoder.yudao.module.iot.component.emqx.upstream.router.IotDeviceWebhookVertxHandler;
  10 +import io.netty.handler.codec.mqtt.MqttQoS;
  11 +import io.vertx.core.Future;
  12 +import io.vertx.core.Vertx;
  13 +import io.vertx.core.http.HttpServer;
  14 +import io.vertx.ext.web.Router;
  15 +import io.vertx.ext.web.handler.BodyHandler;
  16 +import io.vertx.mqtt.MqttClient;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +
  19 +import java.util.concurrent.CompletableFuture;
  20 +import java.util.concurrent.TimeUnit;
  21 +
  22 +/**
  23 + * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器
  24 + * <p>
  25 + * 协议:HTTP、MQTT
  26 + *
  27 + * @author haohao
  28 + */
  29 +@Slf4j
  30 +public class IotDeviceUpstreamServer {
  31 +
  32 + /**
  33 + * 重连延迟时间(毫秒)
  34 + */
  35 + private static final int RECONNECT_DELAY_MS = 5000;
  36 + /**
  37 + * 连接超时时间(毫秒)
  38 + */
  39 + private static final int CONNECTION_TIMEOUT_MS = 10000;
  40 + /**
  41 + * 默认 QoS 级别
  42 + */
  43 + private static final MqttQoS DEFAULT_QOS = MqttQoS.AT_LEAST_ONCE;
  44 +
  45 + private final Vertx vertx;
  46 + private final HttpServer server;
  47 + private final MqttClient client;
  48 + private final IotComponentEmqxProperties emqxProperties;
  49 + private final IotDeviceMqttMessageHandler mqttMessageHandler;
  50 + private final IotComponentRegistry componentRegistry;
  51 +
  52 + /**
  53 + * 服务运行状态标志
  54 + */
  55 + private volatile boolean isRunning = false;
  56 +
  57 + public IotDeviceUpstreamServer(IotComponentEmqxProperties emqxProperties,
  58 + IotDeviceUpstreamApi deviceUpstreamApi,
  59 + Vertx vertx,
  60 + MqttClient client,
  61 + IotComponentRegistry componentRegistry) {
  62 + this.vertx = vertx;
  63 + this.emqxProperties = emqxProperties;
  64 + this.client = client;
  65 + this.componentRegistry = componentRegistry;
  66 +
  67 + // 创建 Router 实例
  68 + Router router = Router.router(vertx);
  69 + router.route().handler(BodyHandler.create()); // 处理 Body
  70 + router.post(IotDeviceAuthVertxHandler.PATH)
  71 + // TODO @haohao:疑问,mqtt 的认证,需要通过 http 呀?
  72 + // 回复:MQTT 认证不必须通过 HTTP 进行,但 HTTP 认证是 EMQX 等 MQTT 服务器支持的一种灵活的认证方式
  73 + .handler(new IotDeviceAuthVertxHandler(deviceUpstreamApi));
  74 + // 添加 Webhook 处理器,用于处理设备连接和断开连接事件
  75 + router.post(IotDeviceWebhookVertxHandler.PATH)
  76 + .handler(new IotDeviceWebhookVertxHandler(deviceUpstreamApi));
  77 + // 创建 HttpServer 实例
  78 + this.server = vertx.createHttpServer().requestHandler(router);
  79 + this.mqttMessageHandler = new IotDeviceMqttMessageHandler(deviceUpstreamApi, client);
  80 + }
  81 +
  82 + /**
  83 + * 启动 HTTP 服务器、MQTT 客户端
  84 + */
  85 + public void start() {
  86 + if (isRunning) {
  87 + log.warn("[start][服务已经在运行中,请勿重复启动]");
  88 + return;
  89 + }
  90 + log.info("[start][开始启动服务]");
  91 +
  92 + // 检查authPort是否为null
  93 + Integer authPort = emqxProperties.getAuthPort();
  94 + if (authPort == null) {
  95 + log.warn("[start][authPort为null,使用默认端口8080]");
  96 + authPort = 8080; // 默认端口
  97 + }
  98 +
  99 + // 1. 启动 HTTP 服务器
  100 + final Integer finalAuthPort = authPort; // 为lambda表达式创建final变量
  101 + CompletableFuture<Void> httpFuture = server.listen(finalAuthPort)
  102 + .toCompletionStage()
  103 + .toCompletableFuture()
  104 + .thenAccept(v -> log.info("[start][HTTP服务器启动完成,端口: {}]", server.actualPort()));
  105 +
  106 + // 2. 连接 MQTT Broker
  107 + CompletableFuture<Void> mqttFuture = connectMqtt()
  108 + .toCompletionStage()
  109 + .toCompletableFuture()
  110 + .thenAccept(v -> {
  111 + // 2.1 添加 MQTT 断开重连监听器
  112 + client.closeHandler(closeEvent -> {
  113 + log.warn("[closeHandler][MQTT连接已断开,准备重连]");
  114 + reconnectWithDelay();
  115 + });
  116 + // 2. 设置 MQTT 消息处理器
  117 + setupMessageHandler();
  118 + });
  119 +
  120 + // 3. 等待所有服务启动完成
  121 + CompletableFuture.allOf(httpFuture, mqttFuture)
  122 + .orTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
  123 + .whenComplete((result, error) -> {
  124 + if (error != null) {
  125 + log.error("[start][服务启动失败]", error);
  126 + } else {
  127 + isRunning = true;
  128 + log.info("[start][所有服务启动完成]");
  129 + }
  130 + });
  131 + }
  132 +
  133 + /**
  134 + * 设置 MQTT 消息处理器
  135 + */
  136 + private void setupMessageHandler() {
  137 + client.publishHandler(mqttMessageHandler::handle);
  138 + log.debug("[setupMessageHandler][MQTT消息处理器设置完成]");
  139 + }
  140 +
  141 + /**
  142 + * 重连 MQTT 客户端
  143 + */
  144 + private void reconnectWithDelay() {
  145 + if (!isRunning) {
  146 + log.info("[reconnectWithDelay][服务已停止,不再尝试重连]");
  147 + return;
  148 + }
  149 +
  150 + vertx.setTimer(RECONNECT_DELAY_MS, id -> {
  151 + log.info("[reconnectWithDelay][开始重新连接 MQTT]");
  152 + connectMqtt();
  153 + });
  154 + }
  155 +
  156 + /**
  157 + * 连接 MQTT Broker 并订阅主题
  158 + *
  159 + * @return 连接结果的Future
  160 + */
  161 + private Future<Void> connectMqtt() {
  162 + // 检查必要的MQTT配置
  163 + String host = emqxProperties.getMqttHost();
  164 + Integer port = emqxProperties.getMqttPort();
  165 +
  166 + if (host == null) {
  167 + String msg = "[connectMqtt][MQTT Host为null,无法连接]";
  168 + log.error(msg);
  169 + return Future.failedFuture(new IllegalStateException(msg));
  170 + }
  171 +
  172 + if (port == null) {
  173 + log.warn("[connectMqtt][MQTT Port为null,使用默认端口1883]");
  174 + port = 1883; // 默认MQTT端口
  175 + }
  176 +
  177 + final Integer finalPort = port; // 为lambda表达式创建final变量
  178 + return client.connect(finalPort, host)
  179 + .compose(connAck -> {
  180 + log.info("[connectMqtt][MQTT客户端连接成功]");
  181 + return subscribeToTopics();
  182 + })
  183 + .recover(error -> {
  184 + log.error("[connectMqtt][连接MQTT Broker失败:]", error);
  185 + reconnectWithDelay();
  186 + return Future.failedFuture(error);
  187 + });
  188 + }
  189 +
  190 + /**
  191 + * 订阅设备上行消息主题
  192 + *
  193 + * @return 订阅结果的 Future
  194 + */
  195 + private Future<Void> subscribeToTopics() {
  196 + String[] topics = emqxProperties.getMqttTopics();
  197 + if (ArrayUtil.isEmpty(topics)) {
  198 + log.warn("[subscribeToTopics][未配置MQTT主题或为null,使用默认主题]");
  199 + // 默认订阅所有设备上下行主题
  200 + topics = new String[]{"/device/#"};
  201 + }
  202 + log.info("[subscribeToTopics][开始订阅设备上行消息主题]");
  203 +
  204 + Future<Void> compositeFuture = Future.succeededFuture();
  205 + for (String topic : topics) {
  206 + if (topic == null) {
  207 + continue; // 跳过null主题
  208 + }
  209 + String trimmedTopic = topic.trim();
  210 + if (trimmedTopic.isEmpty()) {
  211 + continue;
  212 + }
  213 + compositeFuture = compositeFuture.compose(v -> client.subscribe(trimmedTopic, DEFAULT_QOS.value())
  214 + .<Void>map(ack -> {
  215 + log.info("[subscribeToTopics][成功订阅主题: {}]", trimmedTopic);
  216 + return null;
  217 + })
  218 + .recover(error -> {
  219 + log.error("[subscribeToTopics][订阅主题失败: {}]", trimmedTopic, error);
  220 + return Future.<Void>succeededFuture(); // 继续订阅其他主题
  221 + }));
  222 + }
  223 + return compositeFuture;
  224 + }
  225 +
  226 + /**
  227 + * 停止所有服务
  228 + */
  229 + public void stop() {
  230 + if (!isRunning) {
  231 + log.warn("[stop][服务未运行,无需停止]");
  232 + return;
  233 + }
  234 + log.info("[stop][开始关闭服务]");
  235 + isRunning = false;
  236 +
  237 + try {
  238 + CompletableFuture<Void> serverFuture = server != null
  239 + ? server.close().toCompletionStage().toCompletableFuture()
  240 + : CompletableFuture.completedFuture(null);
  241 + CompletableFuture<Void> clientFuture = client != null
  242 + ? client.disconnect().toCompletionStage().toCompletableFuture()
  243 + : CompletableFuture.completedFuture(null);
  244 + CompletableFuture<Void> vertxFuture = vertx != null
  245 + ? vertx.close().toCompletionStage().toCompletableFuture()
  246 + : CompletableFuture.completedFuture(null);
  247 +
  248 + // 等待所有资源关闭
  249 + CompletableFuture.allOf(serverFuture, clientFuture, vertxFuture)
  250 + .orTimeout(CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)
  251 + .whenComplete((result, error) -> {
  252 + if (error != null) {
  253 + log.error("[stop][服务关闭过程中发生异常]", error);
  254 + } else {
  255 + log.info("[stop][所有服务关闭完成]");
  256 + }
  257 + });
  258 + } catch (Exception e) {
  259 + log.error("[stop][关闭服务异常]", e);
  260 + throw new RuntimeException("关闭 IoT 设备上行服务失败", e);
  261 + }
  262 + }
  263 +}
0 264 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/component/emqx/upstream/router/IotDeviceAuthVertxHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.emqx.upstream.router;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  5 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEmqxAuthReqDTO;
  6 +import cn.iocoder.yudao.module.iot.component.core.util.IotPluginCommonUtils;
  7 +import io.vertx.core.Handler;
  8 +import io.vertx.core.json.JsonObject;
  9 +import io.vertx.ext.web.RoutingContext;
  10 +import lombok.RequiredArgsConstructor;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +
  13 +import java.util.Collections;
  14 +
  15 +/**
  16 + * IoT EMQX 连接认证的 Vert.x Handler
  17 + * <p>
  18 + * 参考:<a href="https://docs.emqx.com/zh/emqx/latest/access-control/authn/http.html">EMQX HTTP</a>
  19 + * <p>
  20 + * 注意:该处理器需要返回特定格式:{"result": "allow"} 或 {"result": "deny"},
  21 + * 以符合 EMQX 认证插件的要求,因此不使用 IotStandardResponse 实体类
  22 + *
  23 + * @author haohao
  24 + */
  25 +@RequiredArgsConstructor
  26 +@Slf4j
  27 +public class IotDeviceAuthVertxHandler implements Handler<RoutingContext> {
  28 +
  29 + public static final String PATH = "/mqtt/auth";
  30 +
  31 + private final IotDeviceUpstreamApi deviceUpstreamApi;
  32 +
  33 + @Override
  34 + public void handle(RoutingContext routingContext) {
  35 + try {
  36 + // 构建认证请求 DTO
  37 + JsonObject json = routingContext.body().asJsonObject();
  38 + String clientId = json.getString("clientid");
  39 + String username = json.getString("username");
  40 + String password = json.getString("password");
  41 + IotDeviceEmqxAuthReqDTO authReqDTO = new IotDeviceEmqxAuthReqDTO()
  42 + .setClientId(clientId)
  43 + .setUsername(username)
  44 + .setPassword(password);
  45 +
  46 + // 调用认证 API
  47 + CommonResult<Boolean> authResult = deviceUpstreamApi.authenticateEmqxConnection(authReqDTO);
  48 + if (authResult.getCode() != 0 || !authResult.getData()) {
  49 + // 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求
  50 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny"));
  51 + return;
  52 + }
  53 +
  54 + // 响应结果
  55 + // 注意:这里必须返回 {"result": "allow"} 格式,以符合 EMQX 认证插件的要求
  56 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "allow"));
  57 + } catch (Exception e) {
  58 + log.error("[handle][EMQX 认证异常]", e);
  59 + // 注意:这里必须返回 {"result": "deny"} 格式,以符合 EMQX 认证插件的要求
  60 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "deny"));
  61 + }
  62 + }
  63 +
  64 +}
0 65 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/component/emqx/upstream/router/IotDeviceMqttMessageHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.emqx.upstream.router;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.json.JSONObject;
  5 +import cn.hutool.json.JSONUtil;
  6 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  7 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  8 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
  9 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
  10 +import cn.iocoder.yudao.module.iot.component.core.pojo.IotStandardResponse;
  11 +import cn.iocoder.yudao.module.iot.component.core.util.IotPluginCommonUtils;
  12 +import io.netty.handler.codec.mqtt.MqttQoS;
  13 +import io.vertx.core.buffer.Buffer;
  14 +import io.vertx.mqtt.MqttClient;
  15 +import io.vertx.mqtt.messages.MqttPublishMessage;
  16 +import lombok.extern.slf4j.Slf4j;
  17 +
  18 +import java.time.LocalDateTime;
  19 +import java.util.Arrays;
  20 +import java.util.HashMap;
  21 +import java.util.Map;
  22 +
  23 +/**
  24 + * IoT 设备 MQTT 消息处理器
  25 + * <p>
  26 + * 参考:<a href="https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services">设备属性、事件、服务</a>
  27 + */
  28 +@Slf4j
  29 +public class IotDeviceMqttMessageHandler {
  30 +
  31 + // TODO @haohao:讨论,感觉 mqtt 和 http,可以做个相对统一的格式哈;回复 都使用 Alink 格式,方便后续扩展。
  32 + // 设备上报属性 标准 JSON
  33 + // 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post
  34 + // 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/property/post_reply
  35 +
  36 + // 设备上报事件 标准 JSON
  37 + // 请求 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post
  38 + // 响应 Topic:/sys/${productKey}/${deviceName}/thing/event/${tsl.event.identifier}/post_reply
  39 +
  40 + private static final String SYS_TOPIC_PREFIX = "/sys/";
  41 + private static final String PROPERTY_POST_TOPIC = "/thing/event/property/post";
  42 + private static final String EVENT_POST_TOPIC_PREFIX = "/thing/event/";
  43 + private static final String EVENT_POST_TOPIC_SUFFIX = "/post";
  44 + private static final String REPLY_SUFFIX = "_reply";
  45 + private static final String PROPERTY_METHOD = "thing.event.property.post";
  46 + private static final String EVENT_METHOD_PREFIX = "thing.event.";
  47 + private static final String EVENT_METHOD_SUFFIX = ".post";
  48 +
  49 + private final IotDeviceUpstreamApi deviceUpstreamApi;
  50 + private final MqttClient mqttClient;
  51 +
  52 + public IotDeviceMqttMessageHandler(IotDeviceUpstreamApi deviceUpstreamApi, MqttClient mqttClient) {
  53 + this.deviceUpstreamApi = deviceUpstreamApi;
  54 + this.mqttClient = mqttClient;
  55 + }
  56 +
  57 + /**
  58 + * 处理MQTT消息
  59 + *
  60 + * @param message MQTT发布消息
  61 + */
  62 + public void handle(MqttPublishMessage message) {
  63 + String topic = message.topicName();
  64 + String payload = message.payload().toString();
  65 + log.info("[messageHandler][接收到消息][topic: {}][payload: {}]", topic, payload);
  66 +
  67 + try {
  68 + if (StrUtil.isEmpty(payload)) {
  69 + log.warn("[messageHandler][消息内容为空][topic: {}]", topic);
  70 + return;
  71 + }
  72 + handleMessage(topic, payload);
  73 + } catch (Exception e) {
  74 + log.error("[messageHandler][处理消息失败][topic: {}][payload: {}]", topic, payload, e);
  75 + }
  76 + }
  77 +
  78 + /**
  79 + * 根据主题类型处理消息
  80 + *
  81 + * @param topic 主题
  82 + * @param payload 消息内容
  83 + */
  84 + private void handleMessage(String topic, String payload) {
  85 + // 校验前缀
  86 + if (!topic.startsWith(SYS_TOPIC_PREFIX)) {
  87 + log.warn("[handleMessage][未知的消息类型][topic: {}]", topic);
  88 + return;
  89 + }
  90 +
  91 + // 处理设备属性上报消息
  92 + if (topic.endsWith(PROPERTY_POST_TOPIC)) {
  93 + log.info("[handleMessage][接收到设备属性上报][topic: {}]", topic);
  94 + handlePropertyPost(topic, payload);
  95 + return;
  96 + }
  97 +
  98 + // 处理设备事件上报消息
  99 + if (topic.contains(EVENT_POST_TOPIC_PREFIX) && topic.endsWith(EVENT_POST_TOPIC_SUFFIX)) {
  100 + log.info("[handleMessage][接收到设备事件上报][topic: {}]", topic);
  101 + handleEventPost(topic, payload);
  102 + return;
  103 + }
  104 +
  105 + // 未知消息类型
  106 + log.warn("[handleMessage][未知的消息类型][topic: {}]", topic);
  107 + }
  108 +
  109 + /**
  110 + * 处理设备属性上报消息
  111 + *
  112 + * @param topic 主题
  113 + * @param payload 消息内容
  114 + */
  115 + private void handlePropertyPost(String topic, String payload) {
  116 + try {
  117 + // 解析消息内容
  118 + JSONObject jsonObject = JSONUtil.parseObj(payload);
  119 + String[] topicParts = parseTopic(topic);
  120 + if (topicParts == null) {
  121 + return;
  122 + }
  123 +
  124 + // 构建设备属性上报请求对象
  125 + IotDevicePropertyReportReqDTO reportReqDTO = buildPropertyReportDTO(jsonObject, topicParts);
  126 +
  127 + // 调用上游 API 处理设备上报数据
  128 + deviceUpstreamApi.reportDeviceProperty(reportReqDTO);
  129 + log.info("[handlePropertyPost][处理设备属性上报成功][topic: {}]", topic);
  130 +
  131 + // 发送响应消息
  132 + sendResponse(topic, jsonObject, PROPERTY_METHOD, null);
  133 + } catch (Exception e) {
  134 + log.error("[handlePropertyPost][处理设备属性上报失败][topic: {}][payload: {}]", topic, payload, e);
  135 + }
  136 + }
  137 +
  138 + /**
  139 + * 处理设备事件上报消息
  140 + *
  141 + * @param topic 主题
  142 + * @param payload 消息内容
  143 + */
  144 + private void handleEventPost(String topic, String payload) {
  145 + try {
  146 + // 解析消息内容
  147 + JSONObject jsonObject = JSONUtil.parseObj(payload);
  148 + String[] topicParts = parseTopic(topic);
  149 + if (topicParts == null) {
  150 + return;
  151 + }
  152 +
  153 + // 构建设备事件上报请求对象
  154 + IotDeviceEventReportReqDTO reportReqDTO = buildEventReportDTO(jsonObject, topicParts);
  155 +
  156 + // 调用上游 API 处理设备上报数据
  157 + deviceUpstreamApi.reportDeviceEvent(reportReqDTO);
  158 + log.info("[handleEventPost][处理设备事件上报成功][topic: {}]", topic);
  159 +
  160 + // 从 topic 中获取事件标识符
  161 + String eventIdentifier = getEventIdentifier(topicParts, topic);
  162 + if (eventIdentifier == null) {
  163 + return;
  164 + }
  165 +
  166 + // 发送响应消息
  167 + String method = EVENT_METHOD_PREFIX + eventIdentifier + EVENT_METHOD_SUFFIX;
  168 + sendResponse(topic, jsonObject, method, null);
  169 + } catch (Exception e) {
  170 + log.error("[handleEventPost][处理设备事件上报失败][topic: {}][payload: {}]", topic, payload, e);
  171 + }
  172 + }
  173 +
  174 + /**
  175 + * 解析主题,获取主题各部分
  176 + *
  177 + * @param topic 主题
  178 + * @return 主题各部分数组,如果解析失败返回null
  179 + */
  180 + private String[] parseTopic(String topic) {
  181 + String[] topicParts = topic.split("/");
  182 + if (topicParts.length < 7) {
  183 + log.warn("[parseTopic][主题格式不正确][topic: {}]", topic);
  184 + return null;
  185 + }
  186 + return topicParts;
  187 + }
  188 +
  189 + /**
  190 + * 从主题部分中获取事件标识符
  191 + *
  192 + * @param topicParts 主题各部分
  193 + * @param topic 原始主题,用于日志
  194 + * @return 事件标识符,如果获取失败返回null
  195 + */
  196 + private String getEventIdentifier(String[] topicParts, String topic) {
  197 + try {
  198 + return topicParts[6];
  199 + } catch (ArrayIndexOutOfBoundsException e) {
  200 + log.warn("[getEventIdentifier][无法从主题中获取事件标识符][topic: {}][topicParts: {}]",
  201 + topic, Arrays.toString(topicParts));
  202 + return null;
  203 + }
  204 + }
  205 +
  206 + /**
  207 + * 发送响应消息
  208 + *
  209 + * @param topic 原始主题
  210 + * @param jsonObject 原始消息JSON对象
  211 + * @param method 响应方法
  212 + * @param customData 自定义数据,可为 null
  213 + */
  214 + private void sendResponse(String topic, JSONObject jsonObject, String method, Object customData) {
  215 + String replyTopic = topic + REPLY_SUFFIX;
  216 +
  217 + // 响应结果
  218 + IotStandardResponse response = IotStandardResponse.success(
  219 + jsonObject.getStr("id"), method, customData);
  220 + try {
  221 + mqttClient.publish(replyTopic, Buffer.buffer(JsonUtils.toJsonString(response)),
  222 + MqttQoS.AT_LEAST_ONCE, false, false);
  223 + log.info("[sendResponse][发送响应消息成功][topic: {}]", replyTopic);
  224 + } catch (Exception e) {
  225 + log.error("[sendResponse][发送响应消息失败][topic: {}][response: {}]", replyTopic, response, e);
  226 + }
  227 + }
  228 +
  229 + /**
  230 + * 构建设备属性上报请求对象
  231 + *
  232 + * @param jsonObject 消息内容
  233 + * @param topicParts 主题部分
  234 + * @return 设备属性上报请求对象
  235 + */
  236 + private IotDevicePropertyReportReqDTO buildPropertyReportDTO(JSONObject jsonObject, String[] topicParts) {
  237 + IotDevicePropertyReportReqDTO reportReqDTO = new IotDevicePropertyReportReqDTO();
  238 + reportReqDTO.setRequestId(jsonObject.getStr("id"));
  239 + reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
  240 + reportReqDTO.setReportTime(LocalDateTime.now());
  241 + reportReqDTO.setProductKey(topicParts[2]);
  242 + reportReqDTO.setDeviceName(topicParts[3]);
  243 +
  244 + // 只使用标准JSON格式处理属性数据
  245 + JSONObject params = jsonObject.getJSONObject("params");
  246 + if (params == null) {
  247 + log.warn("[buildPropertyReportDTO][消息格式不正确,缺少params字段][jsonObject: {}]", jsonObject);
  248 + params = new JSONObject();
  249 + }
  250 +
  251 + // 将标准格式的params转换为平台需要的properties格式
  252 + Map<String, Object> properties = new HashMap<>();
  253 + for (Map.Entry<String, Object> entry : params.entrySet()) {
  254 + String key = entry.getKey();
  255 + Object valueObj = entry.getValue();
  256 +
  257 + // 如果是复杂结构(包含value和time)
  258 + if (valueObj instanceof JSONObject valueJson) {
  259 + properties.put(key, valueJson.getOrDefault("value", valueObj));
  260 + } else {
  261 + properties.put(key, valueObj);
  262 + }
  263 + }
  264 + reportReqDTO.setProperties(properties);
  265 +
  266 + return reportReqDTO;
  267 + }
  268 +
  269 + /**
  270 + * 构建设备事件上报请求对象
  271 + *
  272 + * @param jsonObject 消息内容
  273 + * @param topicParts 主题部分
  274 + * @return 设备事件上报请求对象
  275 + */
  276 + private IotDeviceEventReportReqDTO buildEventReportDTO(JSONObject jsonObject, String[] topicParts) {
  277 + IotDeviceEventReportReqDTO reportReqDTO = new IotDeviceEventReportReqDTO();
  278 + reportReqDTO.setRequestId(jsonObject.getStr("id"));
  279 + reportReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
  280 + reportReqDTO.setReportTime(LocalDateTime.now());
  281 + reportReqDTO.setProductKey(topicParts[2]);
  282 + reportReqDTO.setDeviceName(topicParts[3]);
  283 + reportReqDTO.setIdentifier(topicParts[6]);
  284 +
  285 + // 只使用标准JSON格式处理事件参数
  286 + JSONObject params = jsonObject.getJSONObject("params");
  287 + if (params == null) {
  288 + log.warn("[buildEventReportDTO][消息格式不正确,缺少params字段][jsonObject: {}]", jsonObject);
  289 + params = new JSONObject();
  290 + }
  291 + reportReqDTO.setParams(params);
  292 +
  293 + return reportReqDTO;
  294 + }
  295 +
  296 +}
0 297 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/src/main/java/cn/iocoder/yudao/module/iot/component/emqx/upstream/router/IotDeviceWebhookVertxHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.emqx.upstream.router;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  6 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
  7 +import cn.iocoder.yudao.module.iot.component.core.util.IotPluginCommonUtils;
  8 +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum;
  9 +import io.vertx.core.Handler;
  10 +import io.vertx.core.json.JsonObject;
  11 +import io.vertx.ext.web.RoutingContext;
  12 +import lombok.RequiredArgsConstructor;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +
  15 +import java.time.LocalDateTime;
  16 +import java.util.Collections;
  17 +
  18 +/**
  19 + * IoT EMQX Webhook 事件处理的 Vert.x Handler
  20 + * <p>
  21 + * 参考:<a href="https://docs.emqx.com/zh/emqx/latest/data-integration/webhook.html">EMQX Webhook</a>
  22 + * <p>
  23 + * 注意:该处理器需要返回特定格式:{"result": "success"} 或 {"result": "error"},
  24 + * 以符合 EMQX Webhook 插件的要求,因此不使用 IotStandardResponse 实体类。
  25 + *
  26 + * @author haohao
  27 + */
  28 +@RequiredArgsConstructor
  29 +@Slf4j
  30 +public class IotDeviceWebhookVertxHandler implements Handler<RoutingContext> {
  31 +
  32 + public static final String PATH = "/mqtt/webhook";
  33 +
  34 + private final IotDeviceUpstreamApi deviceUpstreamApi;
  35 +
  36 + @Override
  37 + public void handle(RoutingContext routingContext) {
  38 + try {
  39 + // 解析请求体
  40 + JsonObject json = routingContext.body().asJsonObject();
  41 + String event = json.getString("event");
  42 + String clientId = json.getString("clientid");
  43 + String username = json.getString("username");
  44 +
  45 + // 处理不同的事件类型
  46 + switch (event) {
  47 + case "client.connected":
  48 + handleClientConnected(clientId, username);
  49 + break;
  50 + case "client.disconnected":
  51 + handleClientDisconnected(clientId, username);
  52 + break;
  53 + default:
  54 + log.info("[handle][未处理的 Webhook 事件] event={}, clientId={}, username={}", event, clientId, username);
  55 + break;
  56 + }
  57 +
  58 + // 返回成功响应
  59 + // 注意:这里必须返回 {"result": "success"} 格式,以符合 EMQX Webhook 插件的要求
  60 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "success"));
  61 + } catch (Exception e) {
  62 + log.error("[handle][处理 Webhook 事件异常]", e);
  63 + // 注意:这里必须返回 {"result": "error"} 格式,以符合 EMQX Webhook 插件的要求
  64 + IotPluginCommonUtils.writeJsonResponse(routingContext, Collections.singletonMap("result", "error"));
  65 + }
  66 + }
  67 +
  68 + /**
  69 + * 处理客户端连接事件
  70 + *
  71 + * @param clientId 客户端ID
  72 + * @param username 用户名
  73 + */
  74 + private void handleClientConnected(String clientId, String username) {
  75 + // 解析产品标识和设备名称
  76 + if (StrUtil.isEmpty(username) || "undefined".equals(username)) {
  77 + log.warn("[handleClientConnected][客户端连接事件,但用户名为空] clientId={}", clientId);
  78 + return;
  79 + }
  80 + String[] parts = parseUsername(username);
  81 + if (parts == null) {
  82 + return;
  83 + }
  84 +
  85 + // 更新设备状态为在线
  86 + IotDeviceStateUpdateReqDTO updateReqDTO = new IotDeviceStateUpdateReqDTO();
  87 + updateReqDTO.setProductKey(parts[1]);
  88 + updateReqDTO.setDeviceName(parts[0]);
  89 + updateReqDTO.setState(IotDeviceStateEnum.ONLINE.getState());
  90 + updateReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
  91 + updateReqDTO.setReportTime(LocalDateTime.now());
  92 + CommonResult<Boolean> result = deviceUpstreamApi.updateDeviceState(updateReqDTO);
  93 + if (result.getCode() != 0 || !result.getData()) {
  94 + log.error("[handleClientConnected][更新设备状态为在线失败] clientId={}, username={}, code={}, msg={}",
  95 + clientId, username, result.getCode(), result.getMsg());
  96 + } else {
  97 + log.info("[handleClientConnected][更新设备状态为在线成功] clientId={}, username={}", clientId, username);
  98 + }
  99 + }
  100 +
  101 + /**
  102 + * 处理客户端断开连接事件
  103 + *
  104 + * @param clientId 客户端ID
  105 + * @param username 用户名
  106 + */
  107 + private void handleClientDisconnected(String clientId, String username) {
  108 + // 解析产品标识和设备名称
  109 + if (StrUtil.isEmpty(username) || "undefined".equals(username)) {
  110 + log.warn("[handleClientDisconnected][客户端断开连接事件,但用户名为空] clientId={}", clientId);
  111 + return;
  112 + }
  113 + String[] parts = parseUsername(username);
  114 + if (parts == null) {
  115 + return;
  116 + }
  117 +
  118 + // 更新设备状态为离线
  119 + IotDeviceStateUpdateReqDTO offlineReqDTO = new IotDeviceStateUpdateReqDTO();
  120 + offlineReqDTO.setProductKey(parts[1]);
  121 + offlineReqDTO.setDeviceName(parts[0]);
  122 + offlineReqDTO.setState(IotDeviceStateEnum.OFFLINE.getState());
  123 + offlineReqDTO.setProcessId(IotPluginCommonUtils.getProcessId());
  124 + offlineReqDTO.setReportTime(LocalDateTime.now());
  125 + CommonResult<Boolean> offlineResult = deviceUpstreamApi.updateDeviceState(offlineReqDTO);
  126 + if (offlineResult.getCode() != 0 || !offlineResult.getData()) {
  127 + log.error("[handleClientDisconnected][更新设备状态为离线失败] clientId={}, username={}, code={}, msg={}",
  128 + clientId, username, offlineResult.getCode(), offlineResult.getMsg());
  129 + } else {
  130 + log.info("[handleClientDisconnected][更新设备状态为离线成功] clientId={}, username={}", clientId, username);
  131 + }
  132 + }
  133 +
  134 + /**
  135 + * 解析用户名,格式为 deviceName&productKey
  136 + *
  137 + * @param username 用户名
  138 + * @return 解析结果,[0] 为 deviceName,[1] 为 productKey,解析失败返回 null
  139 + */
  140 + private String[] parseUsername(String username) {
  141 + if (StrUtil.isEmpty(username)) {
  142 + return null;
  143 + }
  144 + String[] parts = username.split("&");
  145 + if (parts.length != 2) {
  146 + log.warn("[parseUsername][用户名格式({})不正确,无法解析产品标识和设备名称]", username);
  147 + return null;
  148 + }
  149 + return parts;
  150 + }
  151 +
  152 +}
0 153 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 0 → 100644
  1 +cn.iocoder.yudao.module.iot.component.emqx.config.IotComponentEmqxAutoConfiguration
0 2 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-emqx/src/main/resources/application.yml 0 → 100644
  1 +# EMQX组件默认配置
  2 +yudao:
  3 + iot:
  4 + component:
  5 + # 核心组件配置
  6 + core:
  7 + plugin-key: emqx # 插件的唯一标识
  8 + # EMQX组件配置
  9 +# emqx:
  10 +# enabled: true # 启用EMQX组件
  11 +# mqtt-host: 127.0.0.1 # MQTT服务器主机地址
  12 +# mqtt-port: 1883 # MQTT服务器端口
  13 +# mqtt-username: yudao # MQTT服务器用户名
  14 +# mqtt-password: 123456 # MQTT服务器密码
  15 +# mqtt-ssl: false # 是否启用SSL
  16 +# mqtt-topics: # 订阅的主题列表
  17 +# - "/sys/#"
  18 +# auth-port: 8101 # 认证端口
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-http/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5 + <parent>
  6 + <artifactId>yudao-module-iot-components</artifactId>
  7 + <groupId>cn.iocoder.boot</groupId>
  8 + <version>${revision}</version>
  9 + </parent>
  10 + <modelVersion>4.0.0</modelVersion>
  11 + <artifactId>yudao-module-iot-component-http</artifactId>
  12 + <packaging>jar</packaging>
  13 +
  14 + <name>${project.artifactId}</name>
  15 + <description>
  16 + 物联网组件 HTTP 模块
  17 + </description>
  18 +
  19 + <dependencies>
  20 + <dependency>
  21 + <groupId>cn.iocoder.boot</groupId>
  22 + <artifactId>yudao-module-iot-component-core</artifactId>
  23 + <version>${revision}</version>
  24 + </dependency>
  25 +
  26 + <!-- 脚本解析相关 -->
  27 + <!-- <dependency>-->
  28 + <!-- <groupId>cn.iocoder.boot</groupId>-->
  29 + <!-- <artifactId>yudao-module-iot-plugin-script</artifactId>-->
  30 + <!-- <version>${revision}</version>-->
  31 + <!-- </dependency>-->
  32 +
  33 + <!-- 工具类相关 -->
  34 + <dependency>
  35 + <groupId>io.vertx</groupId>
  36 + <artifactId>vertx-web</artifactId>
  37 + </dependency>
  38 +
  39 + <!-- 参数校验 -->
  40 + <dependency>
  41 + <groupId>org.springframework.boot</groupId>
  42 + <artifactId>spring-boot-starter-validation</artifactId>
  43 + <optional>true</optional>
  44 + </dependency>
  45 + </dependencies>
  46 +
  47 +</project>
0 48 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-http/src/main/java/cn/iocoder/yudao/module/iot/component/http/config/IotComponentHttpAutoConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.http.config;
  2 +
  3 +import cn.hutool.system.SystemUtil;
  4 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  5 +import cn.iocoder.yudao.module.iot.component.core.config.IotComponentCommonProperties;
  6 +import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
  7 +import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
  8 +import cn.iocoder.yudao.module.iot.component.http.downstream.IotDeviceDownstreamHandlerImpl;
  9 +import cn.iocoder.yudao.module.iot.component.http.upstream.IotDeviceUpstreamServer;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  12 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  13 +import org.springframework.boot.context.event.ApplicationStartedEvent;
  14 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  15 +import org.springframework.context.ApplicationContext;
  16 +import org.springframework.context.annotation.Bean;
  17 +import org.springframework.context.annotation.ComponentScan;
  18 +import org.springframework.context.event.EventListener;
  19 +
  20 +import java.lang.management.ManagementFactory;
  21 +
  22 +/**
  23 + * IoT 组件 HTTP 的自动配置类
  24 + *
  25 + * @author haohao
  26 + */
  27 +@Slf4j
  28 +@AutoConfiguration
  29 +@EnableConfigurationProperties(IotComponentHttpProperties.class)
  30 +@ConditionalOnProperty(prefix = "yudao.iot.component.http", name = "enabled", havingValue = "true", matchIfMissing = false)
  31 +@ComponentScan(basePackages = {
  32 + "cn.iocoder.yudao.module.iot.component.core", // 核心包
  33 + "cn.iocoder.yudao.module.iot.component.http" // HTTP组件包
  34 +})
  35 +public class IotComponentHttpAutoConfiguration {
  36 +
  37 + /**
  38 + * 组件key
  39 + */
  40 + private static final String PLUGIN_KEY = "http";
  41 +
  42 + public IotComponentHttpAutoConfiguration() {
  43 + log.info("[IotComponentHttpAutoConfiguration][已启动]");
  44 + }
  45 +
  46 + @EventListener(ApplicationStartedEvent.class)
  47 + public void initialize(ApplicationStartedEvent event) {
  48 + // 从应用上下文中获取需要的Bean
  49 + IotComponentRegistry componentRegistry = event.getApplicationContext().getBean(IotComponentRegistry.class);
  50 + IotComponentCommonProperties commonProperties = event.getApplicationContext()
  51 + .getBean(IotComponentCommonProperties.class);
  52 +
  53 + // 设置当前组件的核心标识
  54 + commonProperties.setPluginKey(PLUGIN_KEY);
  55 +
  56 + // 将HTTP组件注册到组件注册表
  57 + componentRegistry.registerComponent(
  58 + PLUGIN_KEY,
  59 + SystemUtil.getHostInfo().getAddress(),
  60 + 0, // 内嵌模式固定为0
  61 + getProcessId());
  62 +
  63 + log.info("[initialize][IoT HTTP 组件初始化完成]");
  64 + }
  65 +
  66 + @Bean(name = "httpDeviceUpstreamServer", initMethod = "start", destroyMethod = "stop")
  67 + public IotDeviceUpstreamServer deviceUpstreamServer(IotDeviceUpstreamApi deviceUpstreamApi,
  68 + IotComponentHttpProperties properties,
  69 + ApplicationContext applicationContext,
  70 + IotComponentRegistry componentRegistry) {
  71 + return new IotDeviceUpstreamServer(properties, deviceUpstreamApi, applicationContext, componentRegistry);
  72 + }
  73 +
  74 + @Bean(name = "httpDeviceDownstreamHandler")
  75 + public IotDeviceDownstreamHandler deviceDownstreamHandler() {
  76 + return new IotDeviceDownstreamHandlerImpl();
  77 + }
  78 +
  79 + /**
  80 + * 获取当前进程ID
  81 + *
  82 + * @return 进程ID
  83 + */
  84 + private String getProcessId() {
  85 + // 获取进程的 name
  86 + String name = ManagementFactory.getRuntimeMXBean().getName();
  87 + // 分割名称,格式为 pid@hostname
  88 + String pid = name.split("@")[0];
  89 + return pid;
  90 + }
  91 +}
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-http/src/main/java/cn/iocoder/yudao/module/iot/component/http/config/IotComponentHttpProperties.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.http.config;
  2 +
  3 +import lombok.Data;
  4 +import org.springframework.boot.context.properties.ConfigurationProperties;
  5 +import org.springframework.validation.annotation.Validated;
  6 +
  7 +/**
  8 + * IoT HTTP组件配置属性
  9 + */
  10 +@ConfigurationProperties(prefix = "yudao.iot.component.http")
  11 +@Validated
  12 +@Data
  13 +public class IotComponentHttpProperties {
  14 +
  15 + /**
  16 + * 是否启用
  17 + */
  18 + private Boolean enabled;
  19 +
  20 + /**
  21 + * HTTP 服务端口
  22 + */
  23 + private Integer serverPort;
  24 +
  25 +}
0 26 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-http/src/main/java/cn/iocoder/yudao/module/iot/component/http/downstream/IotDeviceDownstreamHandlerImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.http.downstream;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.*;
  5 +import cn.iocoder.yudao.module.iot.component.core.downstream.IotDeviceDownstreamHandler;
  6 +
  7 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED;
  8 +
  9 +/**
  10 + * HTTP 插件的 {@link IotDeviceDownstreamHandler} 实现类
  11 + * <p>
  12 + * 但是:由于设备通过 HTTP 短链接接入,导致其实无法下行指导给 device 设备,所以基本都是直接返回失败!!!
  13 + * 类似 MQTT、WebSocket、TCP 插件,是可以实现下行指令的。
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public class IotDeviceDownstreamHandlerImpl implements IotDeviceDownstreamHandler {
  18 +
  19 + @Override
  20 + public CommonResult<Boolean> invokeDeviceService(IotDeviceServiceInvokeReqDTO invokeReqDTO) {
  21 + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持调用设备服务");
  22 + }
  23 +
  24 + @Override
  25 + public CommonResult<Boolean> getDeviceProperty(IotDevicePropertyGetReqDTO getReqDTO) {
  26 + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持获取设备属性");
  27 + }
  28 +
  29 + @Override
  30 + public CommonResult<Boolean> setDeviceProperty(IotDevicePropertySetReqDTO setReqDTO) {
  31 + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性");
  32 + }
  33 +
  34 + @Override
  35 + public CommonResult<Boolean> setDeviceConfig(IotDeviceConfigSetReqDTO setReqDTO) {
  36 + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性");
  37 + }
  38 +
  39 + @Override
  40 + public CommonResult<Boolean> upgradeDeviceOta(IotDeviceOtaUpgradeReqDTO upgradeReqDTO) {
  41 + return CommonResult.error(NOT_IMPLEMENTED.getCode(), "HTTP 不支持设置设备属性");
  42 + }
  43 +
  44 +}
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-http/src/main/java/cn/iocoder/yudao/module/iot/component/http/upstream/IotDeviceUpstreamServer.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.http.upstream;
  2 +
  3 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  4 +import cn.iocoder.yudao.module.iot.component.core.heartbeat.IotComponentRegistry;
  5 +import cn.iocoder.yudao.module.iot.component.http.config.IotComponentHttpProperties;
  6 +import cn.iocoder.yudao.module.iot.component.http.upstream.router.IotDeviceUpstreamVertxHandler;
  7 +import io.vertx.core.Vertx;
  8 +import io.vertx.core.http.HttpServer;
  9 +import io.vertx.ext.web.Router;
  10 +import io.vertx.ext.web.handler.BodyHandler;
  11 +import lombok.extern.slf4j.Slf4j;
  12 +import org.springframework.context.ApplicationContext;
  13 +
  14 +/**
  15 + * IoT 设备下行服务端,接收来自 device 设备的请求,转发给 server 服务器
  16 + * <p>
  17 + * 协议:HTTP
  18 + *
  19 + * @author haohao
  20 + */
  21 +@Slf4j
  22 +public class IotDeviceUpstreamServer {
  23 +
  24 + private final Vertx vertx;
  25 + private final HttpServer server;
  26 + private final IotComponentHttpProperties properties;
  27 + private final IotComponentRegistry componentRegistry;
  28 +
  29 + public IotDeviceUpstreamServer(IotComponentHttpProperties properties,
  30 + IotDeviceUpstreamApi deviceUpstreamApi,
  31 + ApplicationContext applicationContext,
  32 + IotComponentRegistry componentRegistry) {
  33 + this.properties = properties;
  34 + this.componentRegistry = componentRegistry;
  35 +
  36 + // 创建 Vertx 实例
  37 + this.vertx = Vertx.vertx();
  38 + // 创建 Router 实例
  39 + Router router = Router.router(vertx);
  40 + router.route().handler(BodyHandler.create()); // 处理 Body
  41 +
  42 + // 使用统一的 Handler 处理所有上行请求
  43 + IotDeviceUpstreamVertxHandler upstreamHandler = new IotDeviceUpstreamVertxHandler(deviceUpstreamApi,
  44 + applicationContext);
  45 + router.post(IotDeviceUpstreamVertxHandler.PROPERTY_PATH).handler(upstreamHandler);
  46 + router.post(IotDeviceUpstreamVertxHandler.EVENT_PATH).handler(upstreamHandler);
  47 +
  48 + // 创建 HttpServer 实例
  49 + this.server = vertx.createHttpServer().requestHandler(router);
  50 + }
  51 +
  52 + /**
  53 + * 启动 HTTP 服务器
  54 + */
  55 + public void start() {
  56 + log.info("[start][开始启动]");
  57 + server.listen(properties.getServerPort())
  58 + .toCompletionStage()
  59 + .toCompletableFuture()
  60 + .join();
  61 + log.info("[start][启动完成,端口({})]", this.server.actualPort());
  62 + }
  63 +
  64 + /**
  65 + * 停止所有
  66 + */
  67 + public void stop() {
  68 + log.info("[stop][开始关闭]");
  69 + try {
  70 + // 关闭 HTTP 服务器
  71 + if (server != null) {
  72 + server.close()
  73 + .toCompletionStage()
  74 + .toCompletableFuture()
  75 + .join();
  76 + }
  77 +
  78 + // 关闭 Vertx 实例
  79 + if (vertx != null) {
  80 + vertx.close()
  81 + .toCompletionStage()
  82 + .toCompletableFuture()
  83 + .join();
  84 + }
  85 + log.info("[stop][关闭完成]");
  86 + } catch (Exception e) {
  87 + log.error("[stop][关闭异常]", e);
  88 + throw new RuntimeException(e);
  89 + }
  90 + }
  91 +}
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-http/src/main/java/cn/iocoder/yudao/module/iot/component/http/upstream/router/IotDeviceUpstreamVertxHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.component.http.upstream.router;
  2 +
  3 +import cn.hutool.core.util.IdUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  7 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
  8 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
  9 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
  10 +import cn.iocoder.yudao.module.iot.component.core.pojo.IotStandardResponse;
  11 +import cn.iocoder.yudao.module.iot.component.core.util.IotPluginCommonUtils;
  12 +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStateEnum;
  13 +import io.vertx.core.Handler;
  14 +import io.vertx.core.json.JsonObject;
  15 +import io.vertx.ext.web.RoutingContext;
  16 +import lombok.extern.slf4j.Slf4j;
  17 +import org.springframework.context.ApplicationContext;
  18 +
  19 +import java.time.LocalDateTime;
  20 +import java.util.HashMap;
  21 +import java.util.Map;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
  24 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
  25 +
  26 +/**
  27 + * IoT 设备上行统一处理的 Vert.x Handler
  28 + * <p>
  29 + * 统一处理设备属性上报和事件上报的请求
  30 + *
  31 + * @author haohao
  32 + */
  33 +@Slf4j
  34 +public class IotDeviceUpstreamVertxHandler implements Handler<RoutingContext> {
  35 +
  36 + /**
  37 + * 属性上报路径
  38 + */
  39 + public static final String PROPERTY_PATH = "/sys/:productKey/:deviceName/thing/event/property/post";
  40 + /**
  41 + * 事件上报路径
  42 + */
  43 + public static final String EVENT_PATH = "/sys/:productKey/:deviceName/thing/event/:identifier/post";
  44 +
  45 + private static final String PROPERTY_METHOD = "thing.event.property.post";
  46 + private static final String EVENT_METHOD_PREFIX = "thing.event.";
  47 + private static final String EVENT_METHOD_SUFFIX = ".post";
  48 +
  49 + private final IotDeviceUpstreamApi deviceUpstreamApi;
  50 +// private final HttpScriptService scriptService;
  51 +
  52 + public IotDeviceUpstreamVertxHandler(IotDeviceUpstreamApi deviceUpstreamApi,
  53 + ApplicationContext applicationContext) {
  54 + this.deviceUpstreamApi = deviceUpstreamApi;
  55 +// this.scriptService = applicationContext.getBean(HttpScriptService.class);
  56 + }
  57 +
  58 + @Override
  59 + public void handle(RoutingContext routingContext) {
  60 + String path = routingContext.request().path();
  61 + String requestId = IdUtil.fastSimpleUUID();
  62 +
  63 + try {
  64 + // 1. 解析通用参数
  65 + String productKey = routingContext.pathParam("productKey");
  66 + String deviceName = routingContext.pathParam("deviceName");
  67 + JsonObject body = routingContext.body().asJsonObject();
  68 + requestId = ObjUtil.defaultIfBlank(body.getString("id"), requestId);
  69 +
  70 + // 2. 根据路径模式处理不同类型的请求
  71 + CommonResult<Boolean> result;
  72 + String method;
  73 + if (path.matches(".*/thing/event/property/post")) {
  74 + // 处理属性上报
  75 + IotDevicePropertyReportReqDTO reportReqDTO = parsePropertyReportRequest(productKey, deviceName,
  76 + requestId, body);
  77 +
  78 + // 设备上线
  79 + updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName());
  80 +
  81 + // 属性上报
  82 + result = deviceUpstreamApi.reportDeviceProperty(reportReqDTO);
  83 + method = PROPERTY_METHOD;
  84 + } else if (path.matches(".*/thing/event/.+/post")) {
  85 + // 处理事件上报
  86 + String identifier = routingContext.pathParam("identifier");
  87 + IotDeviceEventReportReqDTO reportReqDTO = parseEventReportRequest(productKey, deviceName, identifier,
  88 + requestId, body);
  89 +
  90 + // 设备上线
  91 + updateDeviceState(reportReqDTO.getProductKey(), reportReqDTO.getDeviceName());
  92 +
  93 + // 事件上报
  94 + result = deviceUpstreamApi.reportDeviceEvent(reportReqDTO);
  95 + method = EVENT_METHOD_PREFIX + identifier + EVENT_METHOD_SUFFIX;
  96 + } else {
  97 + // 不支持的请求路径
  98 + IotStandardResponse errorResponse = IotStandardResponse.error(requestId, "unknown",
  99 + BAD_REQUEST.getCode(), "不支持的请求路径");
  100 + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse);
  101 + return;
  102 + }
  103 +
  104 + // 3. 返回标准响应
  105 + IotStandardResponse response;
  106 + if (result.isSuccess()) {
  107 + response = IotStandardResponse.success(requestId, method, result.getData());
  108 + } else {
  109 + response = IotStandardResponse.error(requestId, method, result.getCode(), result.getMsg());
  110 + }
  111 + IotPluginCommonUtils.writeJsonResponse(routingContext, response);
  112 + } catch (Exception e) {
  113 + log.error("[handle][处理上行请求异常] path={}", path, e);
  114 + String method = path.contains("/property/") ? PROPERTY_METHOD
  115 + : EVENT_METHOD_PREFIX + (routingContext.pathParams().containsKey("identifier")
  116 + ? routingContext.pathParam("identifier")
  117 + : "unknown") + EVENT_METHOD_SUFFIX;
  118 + IotStandardResponse errorResponse = IotStandardResponse.error(requestId, method,
  119 + INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
  120 + IotPluginCommonUtils.writeJsonResponse(routingContext, errorResponse);
  121 + }
  122 + }
  123 +
  124 + /**
  125 + * 更新设备状态
  126 + *
  127 + * @param productKey 产品 Key
  128 + * @param deviceName 设备名称
  129 + */
  130 + private void updateDeviceState(String productKey, String deviceName) {
  131 + deviceUpstreamApi.updateDeviceState(((IotDeviceStateUpdateReqDTO) new IotDeviceStateUpdateReqDTO()
  132 + .setRequestId(IdUtil.fastSimpleUUID()).setProcessId(IotPluginCommonUtils.getProcessId())
  133 + .setReportTime(LocalDateTime.now())
  134 + .setProductKey(productKey).setDeviceName(deviceName)).setState(IotDeviceStateEnum.ONLINE.getState()));
  135 + }
  136 +
  137 + /**
  138 + * 解析属性上报请求
  139 + *
  140 + * @param productKey 产品 Key
  141 + * @param deviceName 设备名称
  142 + * @param requestId 请求 ID
  143 + * @param body 请求体
  144 + * @return 属性上报请求 DTO
  145 + */
  146 + private IotDevicePropertyReportReqDTO parsePropertyReportRequest(String productKey, String deviceName,
  147 + String requestId, JsonObject body) {
  148 + // 使用脚本解析数据
  149 +// Map<String, Object> properties = scriptService.parsePropertyData(productKey, deviceName, body);
  150 +
  151 +
  152 + // 如果脚本解析结果为空,使用默认解析逻辑
  153 + // TODO @芋艿:注释说明一下,为什么要这么处理?
  154 +// if (CollUtil.isNotEmpty(properties)) {
  155 + Map<String, Object> properties = new HashMap<>();
  156 + Map<String, Object> params = body.getJsonObject("params") != null ?
  157 + body.getJsonObject("params").getMap() : null;
  158 + if (params != null) {
  159 + // 将标准格式的 params 转换为平台需要的 properties 格式
  160 + for (Map.Entry<String, Object> entry : params.entrySet()) {
  161 + String key = entry.getKey();
  162 + Object valueObj = entry.getValue();
  163 + // 如果是复杂结构(包含 value 和 time)
  164 + if (valueObj instanceof Map) {
  165 + @SuppressWarnings("unchecked")
  166 + Map<String, Object> valueMap = (Map<String, Object>) valueObj;
  167 + properties.put(key, valueMap.getOrDefault("value", valueObj));
  168 + } else {
  169 + properties.put(key, valueObj);
  170 + }
  171 + }
  172 + }
  173 +// }
  174 +
  175 + // 构建属性上报请求 DTO
  176 + return ((IotDevicePropertyReportReqDTO) new IotDevicePropertyReportReqDTO().setRequestId(requestId)
  177 + .setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now())
  178 + .setProductKey(productKey).setDeviceName(deviceName)).setProperties(properties);
  179 + }
  180 +
  181 + /**
  182 + * 解析事件上报请求
  183 + *
  184 + * @param productKey 产品K ey
  185 + * @param deviceName 设备名称
  186 + * @param identifier 事件标识符
  187 + * @param requestId 请求 ID
  188 + * @param body 请求体
  189 + * @return 事件上报请求 DTO
  190 + */
  191 + private IotDeviceEventReportReqDTO parseEventReportRequest(String productKey, String deviceName, String identifier,
  192 + String requestId, JsonObject body) {
  193 + // 使用脚本解析事件数据
  194 +// Map<String, Object> params = scriptService.parseEventData(productKey, deviceName, identifier, body);
  195 + Map<String, Object> params = null;
  196 +
  197 + // 如果脚本解析结果为空,使用默认解析逻辑
  198 +// if (CollUtil.isNotEmpty(params)) {
  199 + if (body.containsKey("params")) {
  200 + params = body.getJsonObject("params").getMap();
  201 + } else {
  202 + // 兼容旧格式
  203 + params = new HashMap<>();
  204 + }
  205 +// }
  206 +
  207 + // 构建事件上报请求 DTO
  208 + return ((IotDeviceEventReportReqDTO) new IotDeviceEventReportReqDTO().setRequestId(requestId)
  209 + .setProcessId(IotPluginCommonUtils.getProcessId()).setReportTime(LocalDateTime.now())
  210 + .setProductKey(productKey).setDeviceName(deviceName)).setIdentifier(identifier).setParams(params);
  211 + }
  212 +}
0 213 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-http/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 0 → 100644
  1 +cn.iocoder.yudao.module.iot.component.http.config.IotComponentHttpAutoConfiguration
0 2 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-components/yudao-module-iot-component-http/src/main/resources/application.yml 0 → 100644
  1 +# HTTP组件默认配置
  2 +yudao:
  3 + iot:
  4 + component:
  5 + core:
  6 + plugin-key: http # 插件的唯一标识
  7 +# http:
  8 +# enabled: true # 是否启用HTTP组件,默认启用
  9 +# server-port: 8092
  10 +
... ...