【功能完善】IoT: 集成 Vert.x 支持,重构 HTTP 插件为 Vert.x 插件
This commit is contained in:
parent
cde6ebf921
commit
77b89aad77
|
@ -67,6 +67,7 @@
|
||||||
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
|
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
|
||||||
<mqtt.version>1.2.5</mqtt.version>
|
<mqtt.version>1.2.5</mqtt.version>
|
||||||
<pf4j-spring.version>0.9.0</pf4j-spring.version>
|
<pf4j-spring.version>0.9.0</pf4j-spring.version>
|
||||||
|
<vertx.version>4.4.0</vertx.version>
|
||||||
<!-- 三方云服务相关 -->
|
<!-- 三方云服务相关 -->
|
||||||
<okio.version>3.5.0</okio.version>
|
<okio.version>3.5.0</okio.version>
|
||||||
<okhttp3.version>4.11.0</okhttp3.version>
|
<okhttp3.version>4.11.0</okhttp3.version>
|
||||||
|
@ -613,6 +614,19 @@
|
||||||
<version>${pf4j-spring.version}</version>
|
<version>${pf4j-spring.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Vert.x 核心依赖 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vertx</groupId>
|
||||||
|
<artifactId>vertx-core</artifactId>
|
||||||
|
<version>${vertx.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- Vert.x Web 模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vertx</groupId>
|
||||||
|
<artifactId>vertx-web</artifactId>
|
||||||
|
<version>${vertx.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,16 @@
|
||||||
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
<artifactId>yudao-spring-boot-starter-excel</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Vert.x 核心依赖 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vertx</groupId>
|
||||||
|
<artifactId>vertx-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Vert.x Web 模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vertx</groupId>
|
||||||
|
<artifactId>vertx-web</artifactId>
|
||||||
|
</dependency>
|
||||||
<!-- MQTT -->
|
<!-- MQTT -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.paho</groupId>
|
<groupId>org.eclipse.paho</groupId>
|
||||||
|
|
|
@ -196,10 +196,8 @@ public class PluginInstanceServiceImpl implements PluginInstanceService {
|
||||||
.collect(Collectors.toMap(PluginInfoDO::getPluginKey, Function.identity()));
|
.collect(Collectors.toMap(PluginInfoDO::getPluginKey, Function.identity()));
|
||||||
|
|
||||||
// 3. 获取本机 IP 和 MAC 地址
|
// 3. 获取本机 IP 和 MAC 地址
|
||||||
LinkedHashSet<InetAddress> localAddressList = NetUtil.localAddressList(t -> t instanceof Inet4Address);
|
String ip = NetUtil.getLocalhostStr();
|
||||||
LinkedHashSet<String> ipList = NetUtil.toIpList(localAddressList);
|
String mac = NetUtil.getLocalMacAddress();
|
||||||
String ip = ipList.stream().findFirst().orElse("127.0.0.1");
|
|
||||||
String mac = NetUtil.getMacAddress(localAddressList.stream().findFirst().orElse(null));
|
|
||||||
String mainId = MAIN_ID + "-" + mac;
|
String mainId = MAIN_ID + "-" + mac;
|
||||||
|
|
||||||
// 4. 遍历插件列表,并保存为插件实例
|
// 4. 遍历插件列表,并保存为插件实例
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<parent>
|
||||||
|
<artifactId>yudao-module-iot-plugin</artifactId>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<version>2.2.0-snapshot</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>yudao-module-iot-http-plugin</artifactId>
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<version>2.2.0-snapshot</version>
|
||||||
|
<description>物联网 插件模块 - http 插件</description>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>2.4</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifestEntries>
|
||||||
|
<Plugin-Id>${plugin.id}</Plugin-Id>
|
||||||
|
<Plugin-Class>${plugin.class}</Plugin-Class>
|
||||||
|
<Plugin-Version>${plugin.version}</Plugin-Version>
|
||||||
|
<Plugin-Provider>${plugin.provider}</Plugin-Provider>
|
||||||
|
<Plugin-Description>${plugin.description}</Plugin-Description>
|
||||||
|
<Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
|
||||||
|
</manifestEntries>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-deploy-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<skip>true</skip>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.4.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<shadedArtifactAttached>true</shadedArtifactAttached>
|
||||||
|
<shadedClassifierName>shaded</shadedClassifierName>
|
||||||
|
<transformers>
|
||||||
|
<transformer>
|
||||||
|
<mainClass>cn.iocoder.yudao.module.iot.HttpPluginSpringbootApplication</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.pf4j</groupId>
|
||||||
|
<artifactId>pf4j-spring</artifactId>
|
||||||
|
<version>0.9.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.34</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<properties>
|
||||||
|
<plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpVertxPlugin</plugin.class>
|
||||||
|
<plugin.version>0.0.1</plugin.version>
|
||||||
|
<plugin.id>http-plugin</plugin.id>
|
||||||
|
<plugin.description>http-plugin-0.0.1</plugin.description>
|
||||||
|
<plugin.provider>ahh</plugin.provider>
|
||||||
|
</properties>
|
||||||
|
</project>
|
|
@ -1,5 +1,5 @@
|
||||||
plugin.id=http-plugin
|
plugin.id=http-plugin
|
||||||
plugin.class=cn.iocoder.yudao.module.iot.plugin.HttpPlugin
|
plugin.class=cn.iocoder.yudao.module.iot.plugin.HttpVertxPlugin
|
||||||
plugin.version=0.0.1
|
plugin.version=0.0.1
|
||||||
plugin.provider=ahh
|
plugin.provider=ahh
|
||||||
plugin.dependencies=
|
plugin.dependencies=
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<properties>
|
<properties>
|
||||||
<!-- 插件相关 -->
|
<!-- 插件相关 -->
|
||||||
<plugin.id>http-plugin</plugin.id>
|
<plugin.id>http-plugin</plugin.id>
|
||||||
<plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpPlugin</plugin.class>
|
<plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpVertxPlugin</plugin.class>
|
||||||
<plugin.version>0.0.1</plugin.version>
|
<plugin.version>0.0.1</plugin.version>
|
||||||
<plugin.provider>ahh</plugin.provider>
|
<plugin.provider>ahh</plugin.provider>
|
||||||
<plugin.description>http-plugin-0.0.1</plugin.description>
|
<plugin.description>http-plugin-0.0.1</plugin.description>
|
||||||
|
@ -30,27 +30,6 @@
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<!-- DOESN'T WORK WITH MAVEN 3 (I defined the plugin metadata in properties section)
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.codehaus.mojo</groupId>
|
|
||||||
<artifactId>properties-maven-plugin</artifactId>
|
|
||||||
<version>1.0-alpha-2</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>initialize</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>read-project-properties</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<files>
|
|
||||||
<file>plugin.properties</file>
|
|
||||||
</files>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-antrun-plugin</artifactId>
|
<artifactId>maven-antrun-plugin</artifactId>
|
||||||
|
@ -118,6 +97,29 @@
|
||||||
<skip>true</skip>
|
<skip>true</skip>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<!-- <plugin>-->
|
||||||
|
<!-- <groupId>org.apache.maven.plugins</groupId>-->
|
||||||
|
<!-- <artifactId>maven-shade-plugin</artifactId>-->
|
||||||
|
<!-- <version>3.4.1</version>-->
|
||||||
|
<!-- <executions>-->
|
||||||
|
<!-- <execution>-->
|
||||||
|
<!-- <phase>package</phase>-->
|
||||||
|
<!-- <goals>-->
|
||||||
|
<!-- <goal>shade</goal>-->
|
||||||
|
<!-- </goals>-->
|
||||||
|
<!-- <configuration>-->
|
||||||
|
<!-- <shadedArtifactAttached>true</shadedArtifactAttached>-->
|
||||||
|
<!-- <shadedClassifierName>shaded</shadedClassifierName>-->
|
||||||
|
<!-- <transformers>-->
|
||||||
|
<!-- <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">-->
|
||||||
|
<!-- <mainClass>cn.iocoder.yudao.module.iot.HttpPluginSpringbootApplication</mainClass>-->
|
||||||
|
<!-- </transformer>-->
|
||||||
|
<!-- </transformers>-->
|
||||||
|
<!-- </configuration>-->
|
||||||
|
<!-- </execution>-->
|
||||||
|
<!-- </executions>-->
|
||||||
|
<!-- </plugin>-->
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
@ -145,10 +147,15 @@
|
||||||
<version>${lombok.version}</version>
|
<version>${lombok.version}</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Vert.x 核心依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.netty</groupId>
|
<groupId>io.vertx</groupId>
|
||||||
<artifactId>netty-all</artifactId>
|
<artifactId>vertx-core</artifactId>
|
||||||
<version>4.1.63.Final</version> <!-- 版本可根据需要调整 -->
|
</dependency>
|
||||||
|
<!-- Vert.x Web 模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vertx</groupId>
|
||||||
|
<artifactId>vertx-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- MQTT -->
|
<!-- MQTT -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
package cn.iocoder.yudao.module.iot.plugin;
|
|
||||||
|
|
||||||
import cn.hutool.json.JSONObject;
|
|
||||||
import cn.hutool.json.JSONUtil;
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
|
|
||||||
import io.netty.buffer.Unpooled;
|
|
||||||
import io.netty.channel.ChannelFutureListener;
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
|
||||||
import io.netty.channel.SimpleChannelInboundHandler;
|
|
||||||
import io.netty.handler.codec.http.*;
|
|
||||||
import io.netty.util.CharsetUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 基于 Netty 的 HTTP 处理器,用于接收设备上报的数据并调用主程序的 DeviceDataApi 接口进行处理。
|
|
||||||
* <p>
|
|
||||||
* 1. 请求格式:JSON 格式,地址为 POST /sys/{productKey}/{deviceName}/thing/event/property/post
|
|
||||||
* 2. 返回结果:JSON 格式,包含统一的 code、data、id、message、method、version 字段
|
|
||||||
*/
|
|
||||||
public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
|
|
||||||
|
|
||||||
private final DeviceDataApi deviceDataApi;
|
|
||||||
|
|
||||||
public HttpHandler(DeviceDataApi deviceDataApi) {
|
|
||||||
this.deviceDataApi = deviceDataApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
|
|
||||||
// 期望的路径格式: /sys/{productKey}/{deviceName}/thing/event/property/post
|
|
||||||
// 使用 "/" 拆分路径
|
|
||||||
String uri = request.uri();
|
|
||||||
String[] parts = uri.split("/");
|
|
||||||
|
|
||||||
/*
|
|
||||||
拆分结果示例:
|
|
||||||
parts[0] = ""
|
|
||||||
parts[1] = "sys"
|
|
||||||
parts[2] = productKey
|
|
||||||
parts[3] = deviceName
|
|
||||||
parts[4] = "thing"
|
|
||||||
parts[5] = "event"
|
|
||||||
parts[6] = "property"
|
|
||||||
parts[7] = "post"
|
|
||||||
*/
|
|
||||||
boolean isCorrectPath = parts.length == 8
|
|
||||||
&& "sys".equals(parts[1])
|
|
||||||
&& "thing".equals(parts[4])
|
|
||||||
&& "event".equals(parts[5])
|
|
||||||
&& "property".equals(parts[6])
|
|
||||||
&& "post".equals(parts[7]);
|
|
||||||
if (!isCorrectPath) {
|
|
||||||
writeResponse(ctx, HttpResponseStatus.NOT_FOUND, "Not Found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String productKey = parts[2];
|
|
||||||
String deviceName = parts[3];
|
|
||||||
|
|
||||||
// 从请求中获取原始数据,尝试解析请求数据为 JSON 对象
|
|
||||||
String requestBody = request.content().toString(CharsetUtil.UTF_8);
|
|
||||||
JSONObject jsonData;
|
|
||||||
try {
|
|
||||||
jsonData = JSONUtil.parseObj(requestBody);
|
|
||||||
} catch (Exception e) {
|
|
||||||
JSONObject res = createResponseJson(
|
|
||||||
400,
|
|
||||||
new JSONObject(),
|
|
||||||
null,
|
|
||||||
"请求数据不是合法的 JSON 格式: " + e.getMessage(),
|
|
||||||
"thing.event.property.post",
|
|
||||||
"1.0"
|
|
||||||
);
|
|
||||||
writeResponse(ctx, HttpResponseStatus.BAD_REQUEST, res.toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String id = jsonData.getStr("id", null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 调用主程序的接口保存数据
|
|
||||||
DeviceDataCreateReqDTO createDTO = DeviceDataCreateReqDTO.builder()
|
|
||||||
.productKey(productKey)
|
|
||||||
.deviceName(deviceName)
|
|
||||||
.message(jsonData.toString())
|
|
||||||
.build();
|
|
||||||
deviceDataApi.saveDeviceData(createDTO);
|
|
||||||
|
|
||||||
// 构造成功响应内容
|
|
||||||
JSONObject successRes = createResponseJson(
|
|
||||||
200,
|
|
||||||
new JSONObject(),
|
|
||||||
id,
|
|
||||||
"success",
|
|
||||||
"thing.event.property.post",
|
|
||||||
"1.0"
|
|
||||||
);
|
|
||||||
writeResponse(ctx, HttpResponseStatus.OK, successRes.toString());
|
|
||||||
} catch (Exception e) {
|
|
||||||
JSONObject errorRes = createResponseJson(
|
|
||||||
500,
|
|
||||||
new JSONObject(),
|
|
||||||
id,
|
|
||||||
"The format of result is error!",
|
|
||||||
"thing.event.property.post",
|
|
||||||
"1.0"
|
|
||||||
);
|
|
||||||
writeResponse(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorRes.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建标准化的响应 JSON 对象
|
|
||||||
*
|
|
||||||
* @param code 响应状态码(业务层面的)
|
|
||||||
* @param data 返回的数据对象(JSON)
|
|
||||||
* @param id 请求的 id(可选)
|
|
||||||
* @param message 返回的提示信息
|
|
||||||
* @param method 返回的 method 标识
|
|
||||||
* @param version 返回的版本号
|
|
||||||
* @return 构造好的 JSON 对象
|
|
||||||
*/
|
|
||||||
private JSONObject createResponseJson(int code, JSONObject data, String id, String message, String method, String version) {
|
|
||||||
JSONObject res = new JSONObject();
|
|
||||||
res.set("code", code);
|
|
||||||
res.set("data", data != null ? data : new JSONObject());
|
|
||||||
res.set("id", id);
|
|
||||||
res.set("message", message);
|
|
||||||
res.set("method", method);
|
|
||||||
res.set("version", version);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 向客户端返回 HTTP 响应的辅助方法
|
|
||||||
*
|
|
||||||
* @param ctx 通道上下文
|
|
||||||
* @param status HTTP 响应状态码(网络层面的)
|
|
||||||
* @param content 响应内容(JSON 字符串或其他文本)
|
|
||||||
*/
|
|
||||||
private void writeResponse(ChannelHandlerContext ctx, HttpResponseStatus status, String content) {
|
|
||||||
// 设置响应头为 JSON 类型和正确的编码
|
|
||||||
FullHttpResponse response = new DefaultFullHttpResponse(
|
|
||||||
HttpVersion.HTTP_1_1,
|
|
||||||
status,
|
|
||||||
Unpooled.copiedBuffer(content, CharsetUtil.UTF_8)
|
|
||||||
);
|
|
||||||
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
|
|
||||||
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
|
|
||||||
|
|
||||||
// 发送响应并在发送完成后关闭连接
|
|
||||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
package cn.iocoder.yudao.module.iot.plugin;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
|
|
||||||
import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
|
|
||||||
import io.netty.bootstrap.ServerBootstrap;
|
|
||||||
import io.netty.channel.*;
|
|
||||||
import io.netty.channel.nio.NioEventLoopGroup;
|
|
||||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
|
||||||
import io.netty.handler.codec.http.*;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.pf4j.PluginWrapper;
|
|
||||||
import org.pf4j.Plugin;
|
|
||||||
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class HttpPlugin extends Plugin {
|
|
||||||
|
|
||||||
private static final int PORT = 8092;
|
|
||||||
|
|
||||||
private ExecutorService executorService;
|
|
||||||
private DeviceDataApi deviceDataApi;
|
|
||||||
|
|
||||||
public HttpPlugin(PluginWrapper wrapper) {
|
|
||||||
super(wrapper);
|
|
||||||
// 初始化线程池
|
|
||||||
this.executorService = Executors.newSingleThreadExecutor();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
log.info("HttpPlugin.start()");
|
|
||||||
|
|
||||||
// 重新初始化线程池,确保它是活跃的
|
|
||||||
if (executorService.isShutdown() || executorService.isTerminated()) {
|
|
||||||
executorService = Executors.newSingleThreadExecutor();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从 ServiceRegistry 中获取主程序暴露的 DeviceDataApi 接口实例
|
|
||||||
deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
|
|
||||||
if (deviceDataApi == null) {
|
|
||||||
log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 异步启动 Netty 服务器
|
|
||||||
executorService.submit(this::startHttpServer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
log.info("HttpPlugin.stop()");
|
|
||||||
// 停止线程池
|
|
||||||
executorService.shutdownNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动 HTTP 服务
|
|
||||||
*/
|
|
||||||
private void startHttpServer() {
|
|
||||||
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
|
|
||||||
EventLoopGroup workerGroup = new NioEventLoopGroup();
|
|
||||||
|
|
||||||
try {
|
|
||||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
|
||||||
bootstrap.group(bossGroup, workerGroup)
|
|
||||||
.channel(NioServerSocketChannel.class)
|
|
||||||
.childHandler(new ChannelInitializer<>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void initChannel(Channel channel) {
|
|
||||||
channel.pipeline().addLast(new HttpServerCodec());
|
|
||||||
channel.pipeline().addLast(new HttpObjectAggregator(65536));
|
|
||||||
// 将从 ServiceRegistry 获取的 deviceDataApi 传入处理器
|
|
||||||
channel.pipeline().addLast(new HttpHandler(deviceDataApi));
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
// 绑定端口并启动服务器
|
|
||||||
ChannelFuture future = bootstrap.bind(PORT).sync();
|
|
||||||
log.info("HTTP 服务器启动成功,端口为: {}", PORT);
|
|
||||||
future.channel().closeFuture().sync();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
log.warn("HTTP 服务启动被中断", e);
|
|
||||||
} finally {
|
|
||||||
bossGroup.shutdownGracefully();
|
|
||||||
workerGroup.shutdownGracefully();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
package cn.iocoder.yudao.module.iot.plugin;
|
||||||
|
|
||||||
|
import cn.hutool.json.JSONObject;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
|
||||||
|
import io.vertx.core.Handler;
|
||||||
|
import io.vertx.ext.web.RequestBody;
|
||||||
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class HttpVertxHandler implements Handler<RoutingContext> {
|
||||||
|
|
||||||
|
private final DeviceDataApi deviceDataApi;
|
||||||
|
|
||||||
|
public HttpVertxHandler(DeviceDataApi deviceDataApi) {
|
||||||
|
this.deviceDataApi = deviceDataApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(RoutingContext ctx) {
|
||||||
|
String productKey = ctx.pathParam("productKey");
|
||||||
|
String deviceName = ctx.pathParam("deviceName");
|
||||||
|
RequestBody requestBody = ctx.body();
|
||||||
|
|
||||||
|
JSONObject jsonData;
|
||||||
|
try {
|
||||||
|
jsonData = JSONUtil.parseObj(requestBody.asJsonObject());
|
||||||
|
} catch (Exception e) {
|
||||||
|
JSONObject res = createResponseJson(
|
||||||
|
400,
|
||||||
|
new JSONObject(),
|
||||||
|
null,
|
||||||
|
"请求数据不是合法的 JSON 格式: " + e.getMessage(),
|
||||||
|
"thing.event.property.post",
|
||||||
|
"1.0");
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(400)
|
||||||
|
.putHeader("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.end(res.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String id = jsonData.getStr("id", null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用主程序的接口保存数据
|
||||||
|
DeviceDataCreateReqDTO createDTO = DeviceDataCreateReqDTO.builder()
|
||||||
|
.productKey(productKey)
|
||||||
|
.deviceName(deviceName)
|
||||||
|
.message(jsonData.toString())
|
||||||
|
.build();
|
||||||
|
deviceDataApi.saveDeviceData(createDTO);
|
||||||
|
|
||||||
|
// 构造成功响应内容
|
||||||
|
JSONObject successRes = createResponseJson(
|
||||||
|
200,
|
||||||
|
new JSONObject(),
|
||||||
|
id,
|
||||||
|
"success",
|
||||||
|
"thing.event.property.post",
|
||||||
|
"1.0");
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(200)
|
||||||
|
.putHeader("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.end(successRes.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
JSONObject errorRes = createResponseJson(
|
||||||
|
500,
|
||||||
|
new JSONObject(),
|
||||||
|
id,
|
||||||
|
"The format of result is error!",
|
||||||
|
"thing.event.property.post",
|
||||||
|
"1.0");
|
||||||
|
ctx.response()
|
||||||
|
.setStatusCode(500)
|
||||||
|
.putHeader("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.end(errorRes.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建标准化的响应 JSON 对象
|
||||||
|
*
|
||||||
|
* @param code 响应状态码(业务层面的)
|
||||||
|
* @param data 返回的数据对象(JSON)
|
||||||
|
* @param id 请求的 id(可选)
|
||||||
|
* @param message 返回的提示信息
|
||||||
|
* @param method 返回的 method 标识
|
||||||
|
* @param version 返回的版本号
|
||||||
|
* @return 构造好的 JSON 对象
|
||||||
|
*/
|
||||||
|
private JSONObject createResponseJson(int code, JSONObject data, String id, String message, String method,
|
||||||
|
String version) {
|
||||||
|
JSONObject res = new JSONObject();
|
||||||
|
res.set("code", code);
|
||||||
|
res.set("data", data != null ? data : new JSONObject());
|
||||||
|
res.set("id", id);
|
||||||
|
res.set("message", message);
|
||||||
|
res.set("method", method);
|
||||||
|
res.set("version", version);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package cn.iocoder.yudao.module.iot.plugin;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
|
||||||
|
import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
|
||||||
|
import io.vertx.core.Vertx;
|
||||||
|
import io.vertx.ext.web.Router;
|
||||||
|
import io.vertx.ext.web.handler.BodyHandler;
|
||||||
|
import org.pf4j.Plugin;
|
||||||
|
import org.pf4j.PluginWrapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class HttpVertxPlugin extends Plugin {
|
||||||
|
|
||||||
|
private static final int PORT = 8092;
|
||||||
|
private Vertx vertx;
|
||||||
|
private DeviceDataApi deviceDataApi;
|
||||||
|
|
||||||
|
public HttpVertxPlugin(PluginWrapper wrapper) {
|
||||||
|
super(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
log.info("HttpVertxPlugin.start()");
|
||||||
|
|
||||||
|
// 获取 DeviceDataApi 实例
|
||||||
|
deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
|
||||||
|
if (deviceDataApi == null) {
|
||||||
|
log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 Vert.x
|
||||||
|
vertx = Vertx.vertx();
|
||||||
|
Router router = Router.router(vertx);
|
||||||
|
|
||||||
|
// 处理 Body
|
||||||
|
router.route().handler(BodyHandler.create());
|
||||||
|
|
||||||
|
// 设置路由
|
||||||
|
router.post("/sys/:productKey/:deviceName/thing/event/property/post")
|
||||||
|
.handler(new HttpVertxHandler(deviceDataApi));
|
||||||
|
|
||||||
|
// 启动 HTTP 服务器
|
||||||
|
vertx.createHttpServer()
|
||||||
|
.requestHandler(router)
|
||||||
|
.listen(PORT, http -> {
|
||||||
|
if (http.succeeded()) {
|
||||||
|
log.info("HTTP 服务器启动成功,端口为: {}", PORT);
|
||||||
|
} else {
|
||||||
|
log.error("HTTP 服务器启动失败", http.cause());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
log.info("HttpVertxPlugin.stop()");
|
||||||
|
if (vertx != null) {
|
||||||
|
vertx.close(ar -> {
|
||||||
|
if (ar.succeeded()) {
|
||||||
|
log.info("Vert.x 关闭成功");
|
||||||
|
} else {
|
||||||
|
log.error("Vert.x 关闭失败", ar.cause());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -212,4 +212,9 @@ iot:
|
||||||
# 保持连接
|
# 保持连接
|
||||||
keepalive: 60
|
keepalive: 60
|
||||||
# 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)
|
# 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)
|
||||||
clearSession: true
|
clearSession: true
|
||||||
|
|
||||||
|
|
||||||
|
# 插件配置
|
||||||
|
pf4j:
|
||||||
|
pluginsDir: ${user.home}/plugins # 插件目录
|
|
@ -45,7 +45,7 @@ spring:
|
||||||
primary: master
|
primary: master
|
||||||
datasource:
|
datasource:
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://127.0.0.1:3307/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://chaojiniu.top:23306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
|
||||||
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例
|
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例
|
||||||
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
||||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||||
|
@ -53,8 +53,8 @@ spring:
|
||||||
# url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
# url: jdbc:dm://127.0.0.1:5236?schema=RUOYI_VUE_PRO # DM 连接的示例
|
||||||
# url: jdbc:kingbase8://127.0.0.1:54321/test # 人大金仓 KingbaseES 连接的示例
|
# url: jdbc:kingbase8://127.0.0.1:54321/test # 人大金仓 KingbaseES 连接的示例
|
||||||
# url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss 连接的示例
|
# url: jdbc:postgresql://127.0.0.1:5432/postgres # OpenGauss 连接的示例
|
||||||
username: root
|
username: ruoyi-vue-pro
|
||||||
password: ahh@123456
|
password: ruoyi-@h2ju02hebp
|
||||||
# username: sa # SQL Server 连接的示例
|
# username: sa # SQL Server 连接的示例
|
||||||
# password: Yudao@2024 # SQL Server 连接的示例
|
# password: Yudao@2024 # SQL Server 连接的示例
|
||||||
# username: SYSDBA # DM 连接的示例
|
# username: SYSDBA # DM 连接的示例
|
||||||
|
@ -63,17 +63,25 @@ spring:
|
||||||
# password: Yudao@2024 # OpenGauss 连接的示例
|
# password: Yudao@2024 # OpenGauss 连接的示例
|
||||||
slave: # 模拟从库,可根据自己需要修改
|
slave: # 模拟从库,可根据自己需要修改
|
||||||
lazy: true # 开启懒加载,保证启动速度
|
lazy: true # 开启懒加载,保证启动速度
|
||||||
url: jdbc:mysql://127.0.0.1:3307/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
url: jdbc:mysql://chaojiniu.top:23306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||||
|
username: ruoyi-vue-pro
|
||||||
|
password: ruoyi-@h2ju02hebp
|
||||||
|
tdengine: # IOT 数据库
|
||||||
|
# lazy: true # 开启懒加载,保证启动速度
|
||||||
|
url: jdbc:TAOS-RS://chaojiniu.top:6041/ruoyi_vue_pro
|
||||||
|
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
|
||||||
username: root
|
username: root
|
||||||
password: ahh@123456
|
password: taosdata
|
||||||
|
druid:
|
||||||
|
validation-query: SELECT SERVER_STATUS() # TDengine 数据源的有效性检查 SQL
|
||||||
|
|
||||||
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: 127.0.0.1 # 地址
|
host: chaojiniu.top # 地址
|
||||||
port: 6379 # 端口
|
port: 6379 # 端口
|
||||||
database: 0 # 数据库索引
|
database: 15 # 数据库索引
|
||||||
# password: dev # 密码,建议生产环境开启
|
password: fsknKD7UvQYZsyf2hXXn # 密码,建议生产环境开启
|
||||||
|
|
||||||
--- #################### 定时任务相关配置 ####################
|
--- #################### 定时任务相关配置 ####################
|
||||||
|
|
||||||
|
@ -175,8 +183,10 @@ logging:
|
||||||
cn.iocoder.yudao.module.crm.dal.mysql: debug
|
cn.iocoder.yudao.module.crm.dal.mysql: debug
|
||||||
cn.iocoder.yudao.module.erp.dal.mysql: debug
|
cn.iocoder.yudao.module.erp.dal.mysql: debug
|
||||||
cn.iocoder.yudao.module.iot.dal.mysql: debug
|
cn.iocoder.yudao.module.iot.dal.mysql: debug
|
||||||
|
cn.iocoder.yudao.module.iot.dal.tdengine: DEBUG
|
||||||
cn.iocoder.yudao.module.ai.dal.mysql: debug
|
cn.iocoder.yudao.module.ai.dal.mysql: debug
|
||||||
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
|
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
|
||||||
|
com.taosdata: DEBUG # TDengine 的日志级别
|
||||||
|
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
|
@ -259,7 +269,7 @@ justauth:
|
||||||
iot:
|
iot:
|
||||||
emq:
|
emq:
|
||||||
# 账号
|
# 账号
|
||||||
username: anhaohao
|
username: haohao
|
||||||
# 密码
|
# 密码
|
||||||
password: ahh@123456
|
password: ahh@123456
|
||||||
# 主机地址
|
# 主机地址
|
||||||
|
@ -271,4 +281,18 @@ iot:
|
||||||
# 保持连接
|
# 保持连接
|
||||||
keepalive: 60
|
keepalive: 60
|
||||||
# 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)
|
# 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)
|
||||||
clearSession: true
|
clearSession: true
|
||||||
|
|
||||||
|
# MQTT-RPC 配置
|
||||||
|
mqtt:
|
||||||
|
broker: tcp://chaojiniu.top:1883
|
||||||
|
username: haohao
|
||||||
|
password: ahh@123456
|
||||||
|
clientId: mqtt-rpc-server-${random.int}
|
||||||
|
requestTopic: rpc/request
|
||||||
|
responseTopicPrefix: rpc/response/
|
||||||
|
|
||||||
|
|
||||||
|
# 插件配置
|
||||||
|
pf4j:
|
||||||
|
pluginsDir: /Users/anhaohao/code/gitee/ruoyi-vue-pro/plugins # 插件目录
|
Loading…
Reference in New Issue