Commit 0cb148a33dd7db0e2f6ba4a84723aed16ca6984c

Authored by alwayssuper
2 parents 62a868f4 d608c4b9

Merge branch 'feature/iot' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/iot

Showing 28 changed files with 571 additions and 562 deletions
plugins/yudao-module-iot-http-plugin-2.2.0-snapshot.jar
No preview for this file type
yudao-dependencies/pom.xml
... ... @@ -67,7 +67,7 @@
67 67 <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
68 68 <mqtt.version>1.2.5</mqtt.version>
69 69 <pf4j-spring.version>0.9.0</pf4j-spring.version>
70   - <vertx.version>4.4.0</vertx.version>
  70 + <vertx.version>4.5.11</vertx.version>
71 71 <!-- 三方云服务相关 -->
72 72 <okio.version>3.5.0</okio.version>
73 73 <okhttp3.version>4.11.0</okhttp3.version>
... ... @@ -626,6 +626,12 @@
626 626 <artifactId>vertx-web</artifactId>
627 627 <version>${vertx.version}</version>
628 628 </dependency>
  629 + <!-- Vert.x MQTT 模块 -->
  630 + <dependency>
  631 + <groupId>io.vertx</groupId>
  632 + <artifactId>vertx-mqtt</artifactId>
  633 + <version>${vertx.version}</version>
  634 + </dependency>
629 635  
630 636 </dependencies>
631 637 </dependencyManagement>
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/ServiceRegistry.java deleted
1   -package cn.iocoder.yudao.module.iot.api;
2   -
3   -import java.util.HashMap;
4   -import java.util.Map;
5   -
6   -// TODO 芋艿:纠结下
7   -/**
8   - * 服务注册表 - 插架模块使用,无法使用 Spring 注入
9   - */
10   -public class ServiceRegistry {
11   -
12   - private static final Map<Class<?>, Object> services = new HashMap<>();
13   -
14   - /**
15   - * 注册服务
16   - *
17   - * @param serviceClass 服务类
18   - * @param serviceImpl 服务实现
19   - * @param <T> 服务类
20   - */
21   - public static <T> void registerService(Class<T> serviceClass, T serviceImpl) {
22   - services.put(serviceClass, serviceImpl);
23   - }
24   -
25   - /**
26   - * 获得服务
27   - *
28   - * @param serviceClass 服务类
29   - * @param <T> 服务类
30   - * @return 服务实现
31   - */
32   - @SuppressWarnings("unchecked")
33   - public static <T> T getService(Class<T> serviceClass) {
34   - return (T) services.get(serviceClass);
35   - }
36   -
37   -}
38 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/RpcRequest.java deleted
1   -package cn.iocoder.yudao.module.iot.mqttrpc.common;
2   -
3   -import lombok.AllArgsConstructor;
4   -import lombok.Builder;
5   -import lombok.Data;
6   -import lombok.NoArgsConstructor;
7   -
8   -// TODO @芋艿:要不要加个 mqtt 值了的前缀
9   -/**
10   - * MQTT RPC 请求
11   - */
12   -@Data
13   -@Builder
14   -@NoArgsConstructor
15   -@AllArgsConstructor
16   -public class RpcRequest {
17   -
18   - /**
19   - * 方法名
20   - */
21   - private String method;
22   -
23   - /**
24   - * 参数
25   - */
26   - // TODO @haohao:object 对象会不会不好序列化?
27   - private Object[] params;
28   -
29   - /**
30   - * 关联 ID
31   - */
32   - private String correlationId;
33   -
34   - /**
35   - * 回复地址
36   - */
37   - private String replyTo;
38   -
39   -}
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/RpcResponse.java deleted
1   -package cn.iocoder.yudao.module.iot.mqttrpc.common;
2   -
3   -import lombok.AllArgsConstructor;
4   -import lombok.Builder;
5   -import lombok.Data;
6   -import lombok.NoArgsConstructor;
7   -
8   -/**
9   - * MQTT RPC 响应
10   - */
11   -@Data
12   -@Builder
13   -@NoArgsConstructor
14   -@AllArgsConstructor
15   -public class RpcResponse {
16   -
17   - /**
18   - * 关联 ID
19   - */
20   - private String correlationId;
21   -
22   - /**
23   - * 结果
24   - */
25   - // TODO @haohao:object 对象会不会不好反序列化?
26   - private Object result;
27   -
28   - /**
29   - * 错误
30   - */
31   - private String error;
32   -
33   -}
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/SerializationUtils.java deleted
1   -package cn.iocoder.yudao.module.iot.mqttrpc.common;
2   -
3   -import cn.hutool.json.JSONUtil;
4   -
5   -/**
6   - * 序列化工具类
7   - *
8   - */
9   -public class SerializationUtils {
10   -
11   - public static String serialize(Object obj) {
12   - return JSONUtil.toJsonStr(obj);
13   - }
14   -
15   - public static <T> T deserialize(String json, Class<T> clazz) {
16   - return JSONUtil.toBean(json, clazz);
17   - }
18   -
19   -}
20 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/PluginStart.java
1 1 package cn.iocoder.yudao.module.iot.framework.plugin;
2 2  
3   -import java.util.List;
4   -
5   -import javax.annotation.Resource;
6   -
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
  6 +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
  7 +import cn.iocoder.yudao.module.iot.service.plugin.PluginInfoService;
  8 +import lombok.extern.slf4j.Slf4j;
7 9 import org.pf4j.spring.SpringPluginManager;
8 10 import org.springframework.boot.ApplicationArguments;
9 11 import org.springframework.boot.ApplicationRunner;
10 12 import org.springframework.stereotype.Component;
11 13  
12   -import lombok.extern.slf4j.Slf4j;
13   -
14   -import cn.iocoder.yudao.module.iot.service.plugin.PluginInfoService;
15   -import cn.hutool.core.collection.CollUtil;
16   -import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
17   -import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
18   -import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
  14 +import javax.annotation.Resource;
  15 +import java.util.List;
19 16  
  17 +// TODO @芋艿:需要 review 下
20 18 @Component
21 19 @Slf4j
22 20 public class PluginStart implements ApplicationRunner {
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/UnifiedConfiguration.java
1 1 package cn.iocoder.yudao.module.iot.framework.plugin;
2 2  
3   -import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
4   -import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
5 3 import cn.iocoder.yudao.module.iot.framework.plugin.listener.CustomPluginStateListener;
6 4 import lombok.extern.slf4j.Slf4j;
7 5 import org.pf4j.spring.SpringPluginManager;
  6 +import org.springframework.beans.factory.annotation.Value;
8 7 import org.springframework.context.annotation.Bean;
9 8 import org.springframework.context.annotation.Configuration;
10   -import org.springframework.context.annotation.DependsOn;
11 9  
12   -import javax.annotation.Resource;
  10 +import java.nio.file.Paths;
13 11  
  12 +// TODO @芋艿:需要 review 下
14 13 @Slf4j
15 14 @Configuration
16 15 public class UnifiedConfiguration {
17 16  
18   - private static final String SERVICE_REGISTRY_INITIALIZED_MARKER = "serviceRegistryInitializedMarker";
19   -
20   - @Resource
21   - private DeviceDataApi deviceDataApi;
22   -
23   - @Bean(SERVICE_REGISTRY_INITIALIZED_MARKER)
24   - public Object serviceRegistryInitializedMarker() {
25   - ServiceRegistry.registerService(DeviceDataApi.class, deviceDataApi);
26   - log.info("[init][将 DeviceDataApi 实例注册到 ServiceRegistry 中]");
27   - return new Object();
28   - }
  17 + @Value("${pf4j.pluginsDir:pluginsDir}")
  18 + private String pluginsDir;
29 19  
30 20 @Bean
31   - @DependsOn(SERVICE_REGISTRY_INITIALIZED_MARKER)
  21 +// @DependsOn("deviceDataApiImpl")
32 22 public SpringPluginManager pluginManager() {
33 23 log.info("[init][实例化 SpringPluginManager]");
34   - SpringPluginManager springPluginManager = new SpringPluginManager() {
  24 + SpringPluginManager springPluginManager = new SpringPluginManager(Paths.get(pluginsDir)) {
  25 +// SpringPluginManager springPluginManager = new SpringPluginManager() {
  26 +
35 27 @Override
36 28 public void startPlugins() {
37 29 // 禁用插件启动,避免插件启动时,启动所有插件
38 30 log.info("[init][禁用默认启动所有插件]");
39 31 }
  32 +
40 33 };
41 34 springPluginManager.addPluginStateListener(new CustomPluginStateListener());
42 35 return springPluginManager;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/listener/CustomPluginStateListener.java
... ... @@ -5,6 +5,7 @@ import org.pf4j.PluginStateEvent;
5 5 import org.pf4j.PluginStateListener;
6 6 import org.springframework.stereotype.Component;
7 7  
  8 +// TODO @芋艿:需要 review 下
8 9 @Component
9 10 @Slf4j
10 11 public class CustomPluginStateListener implements PluginStateListener {
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/config/MqttConfig.java deleted
1   -package cn.iocoder.yudao.module.iot.mqttrpc.config;
2   -
3   -import lombok.Data;
4   -import org.springframework.boot.context.properties.ConfigurationProperties;
5   -import org.springframework.context.annotation.Configuration;
6   -
7   -@Data
8   -@Configuration
9   -@ConfigurationProperties(prefix = "mqtt")
10   -public class MqttConfig {
11   - /**
12   - * MQTT 代理地址
13   - */
14   - private String broker;
15   -
16   - /**
17   - * MQTT 用户名
18   - */
19   - private String username;
20   -
21   - /**
22   - * MQTT 密码
23   - */
24   - private String password;
25   -
26   - /**
27   - * MQTT 客户端 ID
28   - */
29   - private String clientId;
30   -
31   - /**
32   - * MQTT 请求主题
33   - */
34   - private String requestTopic;
35   -
36   - /**
37   - * MQTT 响应主题前缀
38   - */
39   - private String responseTopicPrefix;
40   -}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/server/RpcServer.java deleted
1   -package cn.iocoder.yudao.module.iot.mqttrpc.server;
2   -
3   -import cn.hutool.core.lang.UUID;
4   -import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcRequest;
5   -import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcResponse;
6   -import cn.iocoder.yudao.module.iot.mqttrpc.common.SerializationUtils;
7   -import cn.iocoder.yudao.module.iot.mqttrpc.config.MqttConfig;
8   -import lombok.extern.slf4j.Slf4j;
9   -import org.eclipse.paho.client.mqttv3.*;
10   -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
11   -import org.springframework.stereotype.Service;
12   -
13   -import javax.annotation.PostConstruct;
14   -import javax.annotation.PreDestroy;
15   -import java.util.HashMap;
16   -import java.util.Map;
17   -
18   -// TODO @芋艿:server 逻辑,再瞅瞅;
19   -// TODO @haohao:如果只写在 iot biz 里,那么后续 server => client 貌似不方便?微信再讨论下~;
20   -@Service
21   -@Slf4j
22   -public class RpcServer {
23   -
24   - private final MqttConfig mqttConfig;
25   - private final MqttClient mqttClient;
26   - private final Map<String, MethodInvoker> methodRegistry = new HashMap<>();
27   -
28   - public RpcServer(MqttConfig mqttConfig) throws MqttException {
29   - this.mqttConfig = mqttConfig;
30   - this.mqttClient = new MqttClient(mqttConfig.getBroker(), "rpc-server-" + UUID.randomUUID(), new MemoryPersistence());
31   - MqttConnectOptions options = new MqttConnectOptions();
32   - options.setAutomaticReconnect(true);
33   - options.setCleanSession(true);
34   - options.setUserName(mqttConfig.getUsername());
35   - options.setPassword(mqttConfig.getPassword().toCharArray());
36   - this.mqttClient.connect(options);
37   - }
38   -
39   - @PostConstruct
40   - public void init() throws MqttException {
41   - mqttClient.subscribe(mqttConfig.getRequestTopic(), this::handleRequest);
42   - log.info("RPC Server subscribed to topic: {}", mqttConfig.getRequestTopic());
43   - }
44   -
45   - private void handleRequest(String topic, MqttMessage message) {
46   - RpcRequest request = SerializationUtils.deserialize(new String(message.getPayload()), RpcRequest.class);
47   - RpcResponse response = new RpcResponse();
48   - response.setCorrelationId(request.getCorrelationId());
49   -
50   - try {
51   - MethodInvoker invoker = methodRegistry.get(request.getMethod());
52   - if (invoker == null) {
53   - throw new NoSuchMethodException("Unknown method: " + request.getMethod());
54   - }
55   - Object result = invoker.invoke(request.getParams());
56   - response.setResult(result);
57   - } catch (Exception e) {
58   - response.setError(e.getMessage());
59   - log.error("Error processing RPC request: {}", e.getMessage(), e);
60   - }
61   -
62   - String replyPayload = SerializationUtils.serialize(response);
63   - MqttMessage replyMessage = new MqttMessage(replyPayload.getBytes());
64   - replyMessage.setQos(1);
65   - try {
66   - mqttClient.publish(request.getReplyTo(), replyMessage);
67   - log.info("Published response to {}", request.getReplyTo());
68   - } catch (MqttException e) {
69   - log.error("Failed to publish response: {}", e.getMessage(), e);
70   - }
71   - }
72   -
73   - /**
74   - * 注册可调用的方法
75   - *
76   - * @param methodName 方法名称
77   - * @param invoker 方法调用器
78   - */
79   - public void registerMethod(String methodName, MethodInvoker invoker) {
80   - methodRegistry.put(methodName, invoker);
81   - log.info("Registered method: {}", methodName);
82   - }
83   -
84   - @PreDestroy
85   - public void cleanup() throws MqttException {
86   - mqttClient.disconnect();
87   - log.info("RPC Server disconnected");
88   - }
89   -
90   - /**
91   - * 方法调用器接口
92   - */
93   - @FunctionalInterface
94   - public interface MethodInvoker {
95   -
96   - Object invoke(Object[] params) throws Exception;
97   -
98   - }
99   -
100   -}
101 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/ExampleService.java deleted
1   -package cn.iocoder.yudao.module.iot.service.plugin;
2   -
3   -import cn.iocoder.yudao.module.iot.mqttrpc.server.RpcServer;
4   -import lombok.RequiredArgsConstructor;
5   -import org.springframework.stereotype.Service;
6   -
7   -import javax.annotation.PostConstruct;
8   -
9   -@Service
10   -@RequiredArgsConstructor
11   -public class ExampleService {
12   -
13   - private final RpcServer rpcServer;
14   -
15   - @PostConstruct
16   - public void registerMethods() {
17   - rpcServer.registerMethod("add", params -> {
18   - if (params.length != 2) {
19   - throw new IllegalArgumentException("add方法需要两个参数");
20   - }
21   - int a = ((Number) params[0]).intValue();
22   - int b = ((Number) params[1]).intValue();
23   - return add(a, b);
24   - });
25   -
26   - rpcServer.registerMethod("concat", params -> {
27   - if (params.length != 2) {
28   - throw new IllegalArgumentException("concat方法需要两个参数");
29   - }
30   - String str1 = params[0].toString();
31   - String str2 = params[1].toString();
32   - return concat(str1, str2);
33   - });
34   - }
35   -
36   - private int add(int a, int b) {
37   - return a + b;
38   - }
39   -
40   - private String concat(String a, String b) {
41   - return a + b;
42   - }
43   -}
44 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/plugin.properties 0 → 100644
  1 +plugin.id=emqx-plugin
  2 +plugin.class=cn.iocoder.yudao.module.iot.plugin.EmqxPlugin
  3 +plugin.version=0.0.1
  4 +plugin.provider=ahh
  5 +plugin.dependencies=
  6 +plugin.description=emqx-plugin-0.0.1
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3 + xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="
  4 + http://maven.apache.org/POM/4.0.0
  5 + http://maven.apache.org/xsd/maven-4.0.0.xsd">
  6 + <parent>
  7 + <artifactId>yudao-module-iot-plugin</artifactId>
  8 + <groupId>cn.iocoder.boot</groupId>
  9 + <version>${revision}</version>
  10 + </parent>
  11 + <modelVersion>4.0.0</modelVersion>
  12 + <packaging>jar</packaging>
  13 +
  14 + <artifactId>yudao-module-iot-emqx-plugin</artifactId>
  15 +
  16 + <name>${project.artifactId}</name>
  17 + <description>
  18 + 物联网 插件模块 - emqx 插件
  19 + </description>
  20 +
  21 + <properties>
  22 + <!-- 插件相关 -->
  23 + <plugin.id>emqx-plugin</plugin.id>
  24 + <plugin.class>cn.iocoder.yudao.module.iot.plugin.EmqxPlugin</plugin.class>
  25 + <plugin.version>0.0.1</plugin.version>
  26 + <plugin.provider>ahh</plugin.provider>
  27 + <plugin.description>emqx-plugin-0.0.1</plugin.description>
  28 + <plugin.dependencies/>
  29 + </properties>
  30 +
  31 + <build>
  32 + <plugins>
  33 + <!-- DOESN'T WORK WITH MAVEN 3 (I defined the plugin metadata in properties section)
  34 + <plugin>
  35 + <groupId>org.codehaus.mojo</groupId>
  36 + <artifactId>properties-maven-plugin</artifactId>
  37 + <version>1.0-alpha-2</version>
  38 + <executions>
  39 + <execution>
  40 + <phase>initialize</phase>
  41 + <goals>
  42 + <goal>read-project-properties</goal>
  43 + </goals>
  44 + <configuration>
  45 + <files>
  46 + <file>plugin.properties</file>
  47 + </files>
  48 + </configuration>
  49 + </execution>
  50 + </executions>
  51 + </plugin>
  52 + -->
  53 +
  54 + <plugin>
  55 + <groupId>org.apache.maven.plugins</groupId>
  56 + <artifactId>maven-antrun-plugin</artifactId>
  57 + <version>1.6</version>
  58 + <executions>
  59 + <execution>
  60 + <id>unzip jar file</id>
  61 + <phase>package</phase>
  62 + <configuration>
  63 + <target>
  64 + <unzip src="target/${project.artifactId}-${project.version}.${project.packaging}"
  65 + dest="target/plugin-classes"/>
  66 + </target>
  67 + </configuration>
  68 + <goals>
  69 + <goal>run</goal>
  70 + </goals>
  71 + </execution>
  72 + </executions>
  73 + </plugin>
  74 +
  75 + <plugin>
  76 + <artifactId>maven-assembly-plugin</artifactId>
  77 + <version>2.3</version>
  78 + <configuration>
  79 + <descriptors>
  80 + <descriptor>
  81 + src/main/assembly/assembly.xml
  82 + </descriptor>
  83 + </descriptors>
  84 + <appendAssemblyId>false</appendAssemblyId>
  85 + </configuration>
  86 + <executions>
  87 + <execution>
  88 + <id>make-assembly</id>
  89 + <phase>package</phase>
  90 + <goals>
  91 + <goal>attached</goal>
  92 + </goals>
  93 + </execution>
  94 + </executions>
  95 + </plugin>
  96 +
  97 + <plugin>
  98 + <groupId>org.apache.maven.plugins</groupId>
  99 + <artifactId>maven-jar-plugin</artifactId>
  100 + <version>2.4</version>
  101 + <configuration>
  102 + <archive>
  103 + <manifestEntries>
  104 + <Plugin-Id>${plugin.id}</Plugin-Id>
  105 + <Plugin-Class>${plugin.class}</Plugin-Class>
  106 + <Plugin-Version>${plugin.version}</Plugin-Version>
  107 + <Plugin-Provider>${plugin.provider}</Plugin-Provider>
  108 + <Plugin-Description>${plugin.description}</Plugin-Description>
  109 + <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
  110 + </manifestEntries>
  111 + </archive>
  112 + </configuration>
  113 + </plugin>
  114 +
  115 + <plugin>
  116 + <artifactId>maven-deploy-plugin</artifactId>
  117 + <configuration>
  118 + <skip>true</skip>
  119 + </configuration>
  120 + </plugin>
  121 + </plugins>
  122 + </build>
  123 +
  124 + <dependencies>
  125 + <!-- 其他依赖项 -->
  126 + <dependency>
  127 + <groupId>org.springframework.boot</groupId>
  128 + <artifactId>spring-boot-starter-web</artifactId>
  129 + </dependency>
  130 + <!-- PF4J Spring 集成 -->
  131 + <dependency>
  132 + <groupId>org.pf4j</groupId>
  133 + <artifactId>pf4j-spring</artifactId>
  134 + <scope>provided</scope>
  135 + </dependency>
  136 + <!-- 项目依赖 -->
  137 + <dependency>
  138 + <groupId>cn.iocoder.boot</groupId>
  139 + <artifactId>yudao-module-iot-api</artifactId>
  140 + <version>${revision}</version>
  141 + </dependency>
  142 + <dependency>
  143 + <groupId>org.projectlombok</groupId>
  144 + <artifactId>lombok</artifactId>
  145 + <version>${lombok.version}</version>
  146 + <scope>provided</scope>
  147 + </dependency>
  148 + <!-- Vert.x 核心依赖 -->
  149 + <dependency>
  150 + <groupId>io.vertx</groupId>
  151 + <artifactId>vertx-core</artifactId>
  152 + </dependency>
  153 + <!-- Vert.x Web 模块 -->
  154 + <dependency>
  155 + <groupId>io.vertx</groupId>
  156 + <artifactId>vertx-web</artifactId>
  157 + </dependency>
  158 + <!-- MQTT -->
  159 + <dependency>
  160 + <groupId>org.eclipse.paho</groupId>
  161 + <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  162 + </dependency>
  163 + </dependencies>
  164 +</project>
0 165 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/src/main/assembly/assembly.xml 0 → 100644
  1 +<assembly>
  2 + <id>plugin</id>
  3 + <formats>
  4 + <format>zip</format>
  5 + </formats>
  6 + <includeBaseDirectory>false</includeBaseDirectory>
  7 + <dependencySets>
  8 + <dependencySet>
  9 + <useProjectArtifact>false</useProjectArtifact>
  10 + <scope>runtime</scope>
  11 + <outputDirectory>lib</outputDirectory>
  12 + <includes>
  13 + <include>*:jar:*</include>
  14 + </includes>
  15 + </dependencySet>
  16 + </dependencySets>
  17 + <!--
  18 + <fileSets>
  19 + <fileSet>
  20 + <directory>target/classes</directory>
  21 + <outputDirectory>classes</outputDirectory>
  22 + </fileSet>
  23 + </fileSets>
  24 + -->
  25 + <fileSets>
  26 + <fileSet>
  27 + <directory>target/plugin-classes</directory>
  28 + <outputDirectory>classes</outputDirectory>
  29 + </fileSet>
  30 + </fileSets>
  31 +</assembly>
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-emqx-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/EmqxPlugin.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.plugin;
  2 +
  3 +import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
  4 +import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +import org.pf4j.Plugin;
  7 +import org.pf4j.PluginWrapper;
  8 +
  9 +import javax.annotation.Resource;
  10 +import java.util.concurrent.ExecutorService;
  11 +import java.util.concurrent.Executors;
  12 +
  13 +@Slf4j
  14 +public class EmqxPlugin extends Plugin {
  15 +
  16 + private ExecutorService executorService;
  17 + @Resource
  18 + private DeviceDataApi deviceDataApi;
  19 +
  20 + public EmqxPlugin(PluginWrapper wrapper) {
  21 + super(wrapper);
  22 + this.executorService = Executors.newSingleThreadExecutor();
  23 + }
  24 +
  25 + @Override
  26 + public void start() {
  27 + log.info("EmqxPlugin.start()");
  28 +
  29 + if (executorService.isShutdown() || executorService.isTerminated()) {
  30 + executorService = Executors.newSingleThreadExecutor();
  31 + }
  32 +
  33 + deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
  34 + if (deviceDataApi == null) {
  35 + log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
  36 + return;
  37 + }
  38 +
  39 + }
  40 +
  41 + @Override
  42 + public void stop() {
  43 + log.info("EmqxPlugin.stop()");
  44 + }
  45 +}
0 46 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/pom.xml
... ... @@ -127,7 +127,7 @@
127 127 <!-- 其他依赖项 -->
128 128 <dependency>
129 129 <groupId>org.springframework.boot</groupId>
130   - <artifactId>spring-boot-starter-web</artifactId>
  130 + <artifactId>spring-boot-starter</artifactId>
131 131 </dependency>
132 132 <!-- PF4J Spring 集成 -->
133 133 <dependency>
... ... @@ -147,20 +147,11 @@
147 147 <version>${lombok.version}</version>
148 148 <scope>provided</scope>
149 149 </dependency>
150   - <!-- Vert.x 核心依赖 -->
151   - <dependency>
152   - <groupId>io.vertx</groupId>
153   - <artifactId>vertx-core</artifactId>
154   - </dependency>
155   - <!-- Vert.x Web 模块 -->
  150 + <!-- Vert.x Web -->
156 151 <dependency>
157 152 <groupId>io.vertx</groupId>
158 153 <artifactId>vertx-web</artifactId>
159   - </dependency>
160   - <!-- MQTT -->
161   - <dependency>
162   - <groupId>org.eclipse.paho</groupId>
163   - <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  154 + <version>4.5.11</version>
164 155 </dependency>
165 156 </dependencies>
166 157 </project>
167 158 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/HttpPluginSpringbootApplication.java
... ... @@ -5,7 +5,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
5 5  
6 6 @SpringBootApplication
7 7 public class HttpPluginSpringbootApplication {
  8 +
8 9 public static void main(String[] args) {
9 10 SpringApplication.run(HttpPluginSpringbootApplication.class, args);
10 11 }
  12 +
11 13 }
12 14 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/config/TestConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.config;
  2 +
  3 +import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
  4 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
  5 +import cn.iocoder.yudao.module.iot.plugin.HttpVertxPlugin;
  6 +import org.pf4j.DefaultPluginManager;
  7 +import org.pf4j.PluginWrapper;
  8 +import org.springframework.context.annotation.Bean;
  9 +import org.springframework.context.annotation.Configuration;
  10 +
  11 +// TODO 芋艿:临时实现;
  12 +@Configuration
  13 +public class TestConfiguration {
  14 +
  15 + @Bean
  16 + public DeviceDataApi deviceDataApi() {
  17 + return new DeviceDataApi() {
  18 +
  19 + @Override
  20 + public void saveDeviceData(DeviceDataCreateReqDTO createDTO) {
  21 + System.out.println("saveDeviceData");
  22 + }
  23 +
  24 + };
  25 + }
  26 +
  27 + // TODO @haohao:可能要看下,有没更好的方式
  28 + @Bean(initMethod = "start")
  29 + public HttpVertxPlugin HttpVertxPlugin() {
  30 + PluginWrapper pluginWrapper = new PluginWrapper(new DefaultPluginManager(), null, null, null);
  31 + return new HttpVertxPlugin(pluginWrapper);
  32 + }
  33 +
  34 +}
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/controller/RpcController.java deleted
1   -
2   -package cn.iocoder.yudao.module.iot.controller;
3   -
4   -import cn.iocoder.yudao.module.iot.mqttrpc.client.RpcClient;
5   -import lombok.RequiredArgsConstructor;
6   -import org.springframework.web.bind.annotation.PostMapping;
7   -import org.springframework.web.bind.annotation.RequestMapping;
8   -import org.springframework.web.bind.annotation.RequestParam;
9   -import org.springframework.web.bind.annotation.RestController;
10   -
11   -import javax.annotation.Resource;
12   -import java.util.concurrent.CompletableFuture;
13   -
14   -// TODO 芋艿:后续 review 下
15   -/**
16   - * 插件实例 RPC 接口
17   - *
18   - * @author 芋道源码
19   - */
20   -@RestController
21   -@RequestMapping("/rpc")
22   -@RequiredArgsConstructor
23   -public class RpcController {
24   -
25   - @Resource
26   - private RpcClient rpcClient;
27   -
28   - @PostMapping("/add")
29   - public CompletableFuture<Object> add(@RequestParam int a, @RequestParam int b) throws Exception {
30   - return rpcClient.call("add", new Object[]{a, b}, 10);
31   - }
32   -
33   - @PostMapping("/concat")
34   - public CompletableFuture<Object> concat(@RequestParam String str1, @RequestParam String str2) throws Exception {
35   - return rpcClient.call("concat", new Object[]{str1, str2}, 10);
36   - }
37   -
38   -}
39 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/client/RpcClient.java deleted
1   -package cn.iocoder.yudao.module.iot.mqttrpc.client;
2   -
3   -import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcRequest;
4   -import cn.iocoder.yudao.module.iot.mqttrpc.common.RpcResponse;
5   -import cn.iocoder.yudao.module.iot.mqttrpc.common.SerializationUtils;
6   -import cn.iocoder.yudao.module.iot.mqttrpc.config.MqttConfig;
7   -import lombok.extern.slf4j.Slf4j;
8   -import org.eclipse.paho.client.mqttv3.MqttClient;
9   -import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
10   -import org.eclipse.paho.client.mqttv3.MqttException;
11   -import org.eclipse.paho.client.mqttv3.MqttMessage;
12   -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
13   -import org.springframework.stereotype.Service;
14   -
15   -import javax.annotation.PostConstruct;
16   -import javax.annotation.PreDestroy;
17   -import java.util.UUID;
18   -import java.util.concurrent.*;
19   -
20   -// TODO @芋艿:需要考虑,怎么公用!
21   -@Service
22   -@Slf4j
23   -public class RpcClient {
24   -
25   - private final MqttConfig mqttConfig;
26   - private final MqttClient mqttClient;
27   - private final ConcurrentMap<String, CompletableFuture<RpcResponse>> pendingRequests = new ConcurrentHashMap<>();
28   - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
29   -
30   - public RpcClient(MqttConfig mqttConfig) throws MqttException {
31   - this.mqttConfig = mqttConfig;
32   - this.mqttClient = new MqttClient(mqttConfig.getBroker(), mqttConfig.getClientId(), new MemoryPersistence());
33   - MqttConnectOptions options = new MqttConnectOptions();
34   - options.setAutomaticReconnect(true);
35   - options.setCleanSession(true);
36   - options.setUserName(mqttConfig.getUsername());
37   - options.setPassword(mqttConfig.getPassword().toCharArray());
38   - this.mqttClient.connect(options);
39   - }
40   -
41   - @PostConstruct
42   - public void init() throws MqttException {
43   - mqttClient.subscribe(mqttConfig.getResponseTopicPrefix() + "#", this::handleResponse);
44   - log.info("RPC Client subscribed to topics: {}", mqttConfig.getResponseTopicPrefix() + "#");
45   - }
46   -
47   - private void handleResponse(String topic, MqttMessage message) {
48   - String correlationId = topic.substring(mqttConfig.getResponseTopicPrefix().length());
49   - RpcResponse response = SerializationUtils.deserialize(new String(message.getPayload()), RpcResponse.class);
50   - CompletableFuture<RpcResponse> future = pendingRequests.remove(correlationId);
51   - if (future != null) {
52   - if (response.getError() != null) {
53   - future.completeExceptionally(new RuntimeException(response.getError()));
54   - } else {
55   - future.complete(response);
56   - }
57   - } else {
58   - log.warn("Received response for unknown correlationId: {}", correlationId);
59   - }
60   - }
61   -
62   - public CompletableFuture<Object> call(String method, Object[] params, int timeoutSeconds) throws MqttException {
63   - String correlationId = UUID.randomUUID().toString();
64   - String replyTo = mqttConfig.getResponseTopicPrefix() + correlationId;
65   -
66   - RpcRequest request = new RpcRequest(method, params, correlationId, replyTo);
67   - String payload = SerializationUtils.serialize(request);
68   - MqttMessage message = new MqttMessage(payload.getBytes());
69   - message.setQos(1);
70   - mqttClient.publish(mqttConfig.getRequestTopic(), message);
71   -
72   - CompletableFuture<RpcResponse> futureResponse = new CompletableFuture<>();
73   - pendingRequests.put(correlationId, futureResponse);
74   -
75   - // 设置超时
76   - scheduler.schedule(() -> {
77   - CompletableFuture<RpcResponse> removed = pendingRequests.remove(correlationId);
78   - if (removed != null) {
79   - removed.completeExceptionally(new TimeoutException("RPC call timed out"));
80   - }
81   - }, timeoutSeconds, TimeUnit.SECONDS);
82   -
83   - // 返回最终的结果
84   - return futureResponse.thenApply(RpcResponse::getResult);
85   - }
86   -
87   - @PreDestroy
88   - public void cleanup() throws MqttException {
89   - mqttClient.disconnect();
90   - scheduler.shutdown();
91   - log.info("RPC Client disconnected");
92   - }
93   -}
94 0 \ No newline at end of file
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/config/MqttConfig.java deleted
1   -package cn.iocoder.yudao.module.iot.mqttrpc.config;
2   -
3   -import lombok.Data;
4   -import org.springframework.boot.context.properties.ConfigurationProperties;
5   -import org.springframework.context.annotation.Configuration;
6   -
7   -@Data
8   -@Configuration
9   -@ConfigurationProperties(prefix = "mqtt")
10   -public class MqttConfig {
11   -
12   - /**
13   - * MQTT 代理地址
14   - */
15   - private String broker;
16   -
17   - /**
18   - * MQTT 用户名
19   - */
20   - private String username;
21   -
22   - /**
23   - * MQTT 密码
24   - */
25   - private String password;
26   -
27   - /**
28   - * MQTT 客户端 ID
29   - */
30   - private String clientId;
31   -
32   - /**
33   - * MQTT 请求主题
34   - */
35   - private String requestTopic;
36   -
37   - /**
38   - * MQTT 响应主题前缀
39   - */
40   - private String responseTopicPrefix;
41   -}
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/HttpVertxPlugin.java
1 1 package cn.iocoder.yudao.module.iot.plugin;
2 2  
3   -import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
  3 +import cn.hutool.extra.spring.SpringUtil;
4 4 import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
5 5 import io.vertx.core.Vertx;
6 6 import io.vertx.ext.web.Router;
7 7 import io.vertx.ext.web.handler.BodyHandler;
  8 +import lombok.extern.slf4j.Slf4j;
8 9 import org.pf4j.PluginWrapper;
9 10 import org.pf4j.spring.SpringPlugin;
10 11 import org.springframework.context.ApplicationContext;
11 12 import org.springframework.context.annotation.AnnotationConfigApplicationContext;
12 13  
13   -import lombok.extern.slf4j.Slf4j;
14   -
15 14 @Slf4j
16 15 public class HttpVertxPlugin extends SpringPlugin {
17 16  
18 17 private static final int PORT = 8092;
19 18 private Vertx vertx;
  19 +
20 20 private DeviceDataApi deviceDataApi;
21 21  
22 22 public HttpVertxPlugin(PluginWrapper wrapper) {
... ... @@ -28,7 +28,7 @@ public class HttpVertxPlugin extends SpringPlugin {
28 28 log.info("HttpVertxPlugin.start()");
29 29  
30 30 // 获取 DeviceDataApi 实例
31   - deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
  31 + deviceDataApi = SpringUtil.getBean(DeviceDataApi.class);
32 32 if (deviceDataApi == null) {
33 33 log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
34 34 return;
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/resources/application.yml
1   -server:
2   - port: 8092
3   -
4 1 spring:
5 2 application:
6 3 name: yudao-module-iot-http-plugin
7   -
  4 +
8 5 # MQTT-RPC 配置
9 6 mqtt:
10 7 broker: tcp://chaojiniu.top:1883
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/plugin.properties
1 1 plugin.id=mqtt-plugin
  2 +plugin.description=Vert.x MQTT plugin
2 3 plugin.class=cn.iocoder.yudao.module.iot.plugin.MqttPlugin
3   -plugin.version=0.0.1
  4 +plugin.version=1.0.0
  5 +plugin.requires=
4 6 plugin.provider=ahh
5   -plugin.dependencies=
6   -plugin.description=mqtt-plugin-0.0.1
  7 +plugin.license=Apache-2.0
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/pom.xml
... ... @@ -145,10 +145,11 @@
145 145 <version>${lombok.version}</version>
146 146 <scope>provided</scope>
147 147 </dependency>
148   - <!-- MQTT -->
  148 + <!-- Vert.x MQTT -->
149 149 <dependency>
150   - <groupId>org.eclipse.paho</groupId>
151   - <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  150 + <groupId>io.vertx</groupId>
  151 + <artifactId>vertx-mqtt</artifactId>
  152 + <version>4.5.11</version>
152 153 </dependency>
153 154 </dependencies>
154 155 </project>
155 156 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttPlugin.java
1 1 package cn.iocoder.yudao.module.iot.plugin;
2 2  
3   -import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
4   -import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
5 3 import lombok.extern.slf4j.Slf4j;
6 4 import org.pf4j.Plugin;
7 5 import org.pf4j.PluginWrapper;
8 6  
9   -import javax.annotation.Resource;
10   -import java.util.concurrent.ExecutorService;
11   -import java.util.concurrent.Executors;
12   -
13 7 @Slf4j
14 8 public class MqttPlugin extends Plugin {
15 9  
16   - private ExecutorService executorService;
17   - @Resource
18   - private DeviceDataApi deviceDataApi;
  10 + private MqttServerExtension mqttServerExtension;
19 11  
20 12 public MqttPlugin(PluginWrapper wrapper) {
21 13 super(wrapper);
22   - this.executorService = Executors.newSingleThreadExecutor();
23 14 }
24 15  
25 16 @Override
26 17 public void start() {
27   - log.info("MqttPlugin.start()");
28   -
29   - if (executorService.isShutdown() || executorService.isTerminated()) {
30   - executorService = Executors.newSingleThreadExecutor();
31   - }
32   -
33   - deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
34   - if (deviceDataApi == null) {
35   - log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
36   - return;
37   - }
38   -
  18 + log.info("MQTT Plugin started.");
  19 + mqttServerExtension = new MqttServerExtension();
  20 + mqttServerExtension.startMqttServer();
39 21 }
40 22  
41 23 @Override
42 24 public void stop() {
43   - log.info("MqttPlugin.stop()");
  25 + log.info("MQTT Plugin stopped.");
  26 + if (mqttServerExtension != null) {
  27 + mqttServerExtension.stopMqttServer().onComplete(ar -> {
  28 + if (ar.succeeded()) {
  29 + log.info("Stopped MQTT Server successfully");
  30 + } else {
  31 + log.error("Failed to stop MQTT Server: {}", ar.cause().getMessage());
  32 + }
  33 + });
  34 + }
44 35 }
45   -}
46 36 \ No newline at end of file
  37 +}
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/MqttServerExtension.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.plugin;
  2 +
  3 +import io.netty.handler.codec.mqtt.MqttProperties;
  4 +import io.netty.handler.codec.mqtt.MqttQoS;
  5 +import io.vertx.core.Future;
  6 +import io.vertx.core.Vertx;
  7 +import io.vertx.core.buffer.Buffer;
  8 +import io.vertx.mqtt.MqttEndpoint;
  9 +import io.vertx.mqtt.MqttServer;
  10 +import io.vertx.mqtt.MqttServerOptions;
  11 +import io.vertx.mqtt.MqttTopicSubscription;
  12 +import io.vertx.mqtt.messages.MqttDisconnectMessage;
  13 +import io.vertx.mqtt.messages.MqttPublishMessage;
  14 +import io.vertx.mqtt.messages.MqttSubscribeMessage;
  15 +import io.vertx.mqtt.messages.MqttUnsubscribeMessage;
  16 +import io.vertx.mqtt.messages.codes.MqttSubAckReasonCode;
  17 +import lombok.extern.slf4j.Slf4j;
  18 +import org.pf4j.Extension;
  19 +
  20 +import java.nio.charset.Charset;
  21 +import java.util.ArrayList;
  22 +import java.util.List;
  23 +
  24 +/**
  25 + * 根据官方示例,整合常见 MQTT 功能到 PF4J 的 Extension 类中
  26 + */
  27 +@Slf4j
  28 +@Extension
  29 +public class MqttServerExtension {
  30 +
  31 + private Vertx vertx;
  32 + private MqttServer mqttServer;
  33 +
  34 + /**
  35 + * 启动 MQTT 服务端
  36 + * 可根据需要决定是否启用 SSL/TLS、WebSocket、多实例部署等
  37 + */
  38 + public void startMqttServer() {
  39 + // 初始化 Vert.x
  40 + vertx = Vertx.vertx();
  41 +
  42 + // ========== 如果需要 SSL/TLS,请参考下面注释,启用注释并替换端口、证书路径等 ==========
  43 + // MqttServerOptions options = new MqttServerOptions()
  44 + // .setPort(8883)
  45 + // .setKeyCertOptions(new PemKeyCertOptions()
  46 + // .setKeyPath("./src/test/resources/tls/server-key.pem")
  47 + // .setCertPath("./src/test/resources/tls/server-cert.pem"))
  48 + // .setSsl(true);
  49 +
  50 + // ========== 如果需要 WebSocket,请设置 setUseWebSocket(true) ==========
  51 + // options.setUseWebSocket(true);
  52 +
  53 + // ========== 默认不启用 SSL 的示例 ==========
  54 + MqttServerOptions options = new MqttServerOptions()
  55 + .setPort(1883)
  56 + .setHost("0.0.0.0")
  57 + .setUseWebSocket(false); // 如果需要 WebSocket,请改为 true
  58 +
  59 + mqttServer = MqttServer.create(vertx, options);
  60 +
  61 + // 指定 endpointHandler,处理客户端连接等
  62 + mqttServer.endpointHandler(endpoint -> {
  63 + handleClientConnect(endpoint);
  64 + handleDisconnect(endpoint);
  65 + handleSubscribe(endpoint);
  66 + handleUnsubscribe(endpoint);
  67 + handlePublish(endpoint);
  68 + handlePing(endpoint);
  69 + });
  70 +
  71 + // 启动监听
  72 + mqttServer.listen(ar -> {
  73 + if (ar.succeeded()) {
  74 + log.info("MQTT server is listening on port {}", mqttServer.actualPort());
  75 + } else {
  76 + log.error("Error on starting the server", ar.cause());
  77 + }
  78 + });
  79 + }
  80 +
  81 + /**
  82 + * 优雅关闭 MQTT 服务端
  83 + */
  84 + public Future<Void> stopMqttServer() {
  85 + if (mqttServer != null) {
  86 + return mqttServer.close().onComplete(ar -> {
  87 + if (ar.succeeded()) {
  88 + log.info("MQTT server closed.");
  89 + if (vertx != null) {
  90 + vertx.close();
  91 + log.info("Vert.x instance closed.");
  92 + }
  93 + } else {
  94 + log.error("Failed to close MQTT server: {}", ar.cause().getMessage());
  95 + }
  96 + });
  97 + }
  98 + return Future.succeededFuture();
  99 + }
  100 +
  101 + // ==================== 以下为官方示例中常见事件的处理封装 ====================
  102 +
  103 + /**
  104 + * 处理客户端连接 (CONNECT)
  105 + */
  106 + private void handleClientConnect(MqttEndpoint endpoint) {
  107 + // 打印 CONNECT 的主要信息
  108 + log.info("MQTT client [{}] request to connect, clean session = {}",
  109 + endpoint.clientIdentifier(), endpoint.isCleanSession());
  110 +
  111 + if (endpoint.auth() != null) {
  112 + log.info("[username = {}, password = {}]", endpoint.auth().getUsername(), endpoint.auth().getPassword());
  113 + }
  114 + log.info("[properties = {}]", endpoint.connectProperties());
  115 +
  116 + if (endpoint.will() != null) {
  117 + log.info("[will topic = {}, msg = {}, QoS = {}, isRetain = {}]",
  118 + endpoint.will().getWillTopic(),
  119 + new String(endpoint.will().getWillMessageBytes()),
  120 + endpoint.will().getWillQos(),
  121 + endpoint.will().isWillRetain());
  122 + }
  123 +
  124 + log.info("[keep alive timeout = {}]", endpoint.keepAliveTimeSeconds());
  125 +
  126 + // 接受远程客户端的连接
  127 + endpoint.accept(false);
  128 + }
  129 +
  130 + /**
  131 + * 处理客户端主动断开 (DISCONNECT)
  132 + */
  133 + private void handleDisconnect(MqttEndpoint endpoint) {
  134 + endpoint.disconnectMessageHandler((MqttDisconnectMessage disconnectMessage) -> {
  135 + log.info("Received disconnect from client [{}], reason code = {}",
  136 + endpoint.clientIdentifier(), disconnectMessage.code());
  137 + });
  138 + }
  139 +
  140 + /**
  141 + * 处理客户端订阅 (SUBSCRIBE)
  142 + */
  143 + private void handleSubscribe(MqttEndpoint endpoint) {
  144 + endpoint.subscribeHandler((MqttSubscribeMessage subscribe) -> {
  145 + List<MqttSubAckReasonCode> reasonCodes = new ArrayList<>();
  146 + for (MqttTopicSubscription s : subscribe.topicSubscriptions()) {
  147 + log.info("Subscription for {} with QoS {}", s.topicName(), s.qualityOfService());
  148 + // 将客户端请求的 QoS 转换为返回给客户端的 reason code(可能是错误码或实际 granted QoS)
  149 + reasonCodes.add(MqttSubAckReasonCode.qosGranted(s.qualityOfService()));
  150 + }
  151 + // 回复 SUBACK,MQTT 5.0 时可指定 reasonCodes、properties
  152 + endpoint.subscribeAcknowledge(subscribe.messageId(), reasonCodes, MqttProperties.NO_PROPERTIES);
  153 + });
  154 + }
  155 +
  156 + /**
  157 + * 处理客户端取消订阅 (UNSUBSCRIBE)
  158 + */
  159 + private void handleUnsubscribe(MqttEndpoint endpoint) {
  160 + endpoint.unsubscribeHandler((MqttUnsubscribeMessage unsubscribe) -> {
  161 + for (String topic : unsubscribe.topics()) {
  162 + log.info("Unsubscription for {}", topic);
  163 + }
  164 + // 回复 UNSUBACK,MQTT 5.0 时可指定 reasonCodes、properties
  165 + endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
  166 + });
  167 + }
  168 +
  169 + /**
  170 + * 处理客户端发布的消息 (PUBLISH)
  171 + */
  172 + private void handlePublish(MqttEndpoint endpoint) {
  173 + // 接收 PUBLISH 消息
  174 + endpoint.publishHandler((MqttPublishMessage message) -> {
  175 + String payload = message.payload().toString(Charset.defaultCharset());
  176 + log.info("Received message [{}] on topic [{}] with QoS [{}]",
  177 + payload, message.topicName(), message.qosLevel());
  178 +
  179 + // 根据不同 QoS,回复客户端
  180 + if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
  181 + endpoint.publishAcknowledge(message.messageId());
  182 + } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
  183 + endpoint.publishReceived(message.messageId());
  184 + }
  185 + });
  186 +
  187 + // 如果 QoS = 2,需要处理 PUBREL
  188 + endpoint.publishReleaseHandler(messageId -> {
  189 + endpoint.publishComplete(messageId);
  190 + });
  191 + }
  192 +
  193 + /**
  194 + * 处理客户端 PINGREQ
  195 + */
  196 + private void handlePing(MqttEndpoint endpoint) {
  197 + endpoint.pingHandler(v -> {
  198 + // 这里仅做日志, PINGRESP 已自动发送
  199 + log.info("Ping received from client [{}]", endpoint.clientIdentifier());
  200 + });
  201 + }
  202 +
  203 + // ==================== 如果需要服务端向客户端发布消息,可用以下示例 ====================
  204 +
  205 + /**
  206 + * 服务端主动向已连接的某个 endpoint 发布消息的示例
  207 + * 如果使用 MQTT 5.0,可以传递更多消息属性
  208 + */
  209 + public void publishToClient(MqttEndpoint endpoint, String topic, String content) {
  210 + endpoint.publish(topic,
  211 + Buffer.buffer(content),
  212 + MqttQoS.AT_LEAST_ONCE, // QoS 自行选择
  213 + false,
  214 + false);
  215 +
  216 + // 处理 QoS 1 和 QoS 2 的 ACK
  217 + endpoint.publishAcknowledgeHandler(messageId -> {
  218 + log.info("Received PUBACK from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId);
  219 + }).publishReceivedHandler(messageId -> {
  220 + endpoint.publishRelease(messageId);
  221 + }).publishCompletionHandler(messageId -> {
  222 + log.info("Received PUBCOMP from client [{}] for messageId = {}", endpoint.clientIdentifier(), messageId);
  223 + });
  224 + }
  225 +
  226 + // ==================== 如果需要多实例部署,用于多核扩展,可参考以下思路 ====================
  227 + // 例如,在宿主应用或插件中循环启动多个 MqttServerExtension 实例,或使用 Vert.x 的 deployVerticle:
  228 + // DeploymentOptions options = new DeploymentOptions().setInstances(10);
  229 + // vertx.deployVerticle(() -> new MyMqttVerticle(), options);
  230 +
  231 +}
... ...