Commit f1d887d0e0755c09372e19ed524719357115be9f

Authored by alwayssuper
2 parents d9dda54c deab8c1c

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

 Conflicts:
	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogDataMapper.java
	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceLogDataService.java
	yudao-server/src/main/resources/application-local.yaml
Showing 73 changed files with 1617 additions and 518 deletions
.gitignore
... ... @@ -51,3 +51,4 @@ rebel.xml
51 51 application-my.yaml
52 52  
53 53 /yudao-ui-app/unpackage/
  54 +**/.DS_Store
... ...
plugins/disabled.txt deleted
plugins/enabled.txt deleted
1   -http-plugin
2   -http-plugin@0.0.1
plugins/yudao-module-iot-http-plugin-2.2.0-snapshot.jar
No preview for this file type
yudao-dependencies/pom.xml
... ... @@ -67,6 +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 71 <!-- 三方云服务相关 -->
71 72 <okio.version>3.5.0</okio.version>
72 73 <okhttp3.version>4.11.0</okhttp3.version>
... ... @@ -613,6 +614,19 @@
613 614 <version>${pf4j-spring.version}</version>
614 615 </dependency>
615 616  
  617 + <!-- Vert.x 核心依赖 -->
  618 + <dependency>
  619 + <groupId>io.vertx</groupId>
  620 + <artifactId>vertx-core</artifactId>
  621 + <version>${vertx.version}</version>
  622 + </dependency>
  623 + <!-- Vert.x Web 模块 -->
  624 + <dependency>
  625 + <groupId>io.vertx</groupId>
  626 + <artifactId>vertx-web</artifactId>
  627 + <version>${vertx.version}</version>
  628 + </dependency>
  629 +
616 630 </dependencies>
617 631 </dependencyManagement>
618 632  
... ...
yudao-module-iot/yudao-module-iot-api/pom.xml
... ... @@ -33,6 +33,13 @@
33 33 </exclusion>
34 34 </exclusions>
35 35 </dependency>
  36 +
  37 + <!-- 参数校验 -->
  38 + <dependency>
  39 + <groupId>org.springframework.boot</groupId>
  40 + <artifactId>spring-boot-starter-validation</artifactId>
  41 + <optional>true</optional>
  42 + </dependency>
36 43 </dependencies>
37 44  
38 45 </project>
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/ServiceRegistry.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.api;
3 3 import java.util.HashMap;
4 4 import java.util.Map;
5 5  
  6 +// TODO 芋艿:纠结下
6 7 /**
7 8 * 服务注册表 - 插架模块使用,无法使用 Spring 注入
8 9 */
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/DeviceDataApi.java
1 1 package cn.iocoder.yudao.module.iot.api.device;
2 2  
  3 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
  4 +import jakarta.validation.Valid;
  5 +
3 6 /**
4 7 * 设备数据 API
  8 + *
  9 + * @author haohao
5 10 */
6 11 public interface DeviceDataApi {
7 12  
8 13 /**
9 14 * 保存设备数据
10 15 *
11   - * @param productKey 产品 key
12   - * @param deviceName 设备名称
13   - * @param message 消息
  16 + * @param createDTO 设备数据
14 17 */
15   - void saveDeviceData(String productKey, String deviceName, String message);
  18 + void saveDeviceData(@Valid DeviceDataCreateReqDTO createDTO);
16 19  
17 20 }
18 21 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/DeviceDataCreateReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.api.device.dto;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Builder;
  5 +import lombok.Data;
  6 +import lombok.NoArgsConstructor;
  7 +import jakarta.validation.constraints.NotNull;
  8 +
  9 +@Data
  10 +@NoArgsConstructor
  11 +@AllArgsConstructor
  12 +@Builder
  13 +public class DeviceDataCreateReqDTO {
  14 +
  15 + /**
  16 + * 产品标识
  17 + */
  18 + @NotNull(message = "产品标识不能为空")
  19 + private String productKey;
  20 + /**
  21 + * 设备名称
  22 + */
  23 + @NotNull(message = "设备名称不能为空")
  24 + private String deviceName;
  25 + /**
  26 + * 消息
  27 + */
  28 + @NotNull(message = "消息不能为空")
  29 + private String message;
  30 +
  31 +}
0 32 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/plugin/IotPluginDeployTypeEnum.java
... ... @@ -13,8 +13,8 @@ import java.util.Arrays;
13 13 @Getter
14 14 public enum IotPluginDeployTypeEnum implements IntArrayValuable {
15 15  
16   - UPLOAD(0, "上传 jar"), // TODO @haohao:UPLOAD 和 ALONE 感觉有点冲突,前者是部署方式,后者是运行方式。这个后续再讨论下哈
17   - ALONE(1, "独立运行");
  16 + JAR(0, "JAR 部署"),
  17 + STANDALONE(1, "独立部署");
18 18  
19 19 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotPluginDeployTypeEnum::getDeployType).toArray();
20 20  
... ... @@ -48,4 +48,5 @@ public enum IotPluginDeployTypeEnum implements IntArrayValuable {
48 48 public int[] array() {
49 49 return ARRAYS;
50 50 }
  51 +
51 52 }
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/common/RpcRequest.java 0 → 100644
  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 0 → 100644
  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 0 → 100644
  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 +}
0 20 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/pom.xml
... ... @@ -64,6 +64,16 @@
64 64 <artifactId>yudao-spring-boot-starter-excel</artifactId>
65 65 </dependency>
66 66  
  67 + <!-- Vert.x 核心依赖 -->
  68 + <dependency>
  69 + <groupId>io.vertx</groupId>
  70 + <artifactId>vertx-core</artifactId>
  71 + </dependency>
  72 + <!-- Vert.x Web 模块 -->
  73 + <dependency>
  74 + <groupId>io.vertx</groupId>
  75 + <artifactId>vertx-web</artifactId>
  76 + </dependency>
67 77 <!-- MQTT -->
68 78 <dependency>
69 79 <groupId>org.eclipse.paho</groupId>
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/DeviceDataApiImpl.java
1 1 package cn.iocoder.yudao.module.iot.api.device;
2 2  
  3 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
3 4 import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
4 5 import org.springframework.stereotype.Service;
5 6 import org.springframework.validation.annotation.Validated;
... ... @@ -17,8 +18,8 @@ public class DeviceDataApiImpl implements DeviceDataApi {
17 18 private IotDevicePropertyDataService deviceDataService;
18 19  
19 20 @Override
20   - public void saveDeviceData(String productKey, String deviceName, String message) {
21   - deviceDataService.saveDeviceData(productKey, deviceName, message);
  21 + public void saveDeviceData(DeviceDataCreateReqDTO createDTO) {
  22 + deviceDataService.saveDeviceData(createDTO);
22 23 }
23 24  
24 25 }
25 26 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceDataController.java
... ... @@ -3,19 +3,15 @@ package cn.iocoder.yudao.module.iot.controller.admin.device;
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6   -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceSaveReqVO;
7   -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataPageReqVO;
8   -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataRespVO;
9   -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataSimulatorSaveReqVO;
  6 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.*;
10 7 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDataDO;
11   -import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotTimeDataRespVO;
  8 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO;
12 9 import cn.iocoder.yudao.module.iot.service.device.IotDeviceLogDataService;
13 10 import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
14 11 import io.swagger.v3.oas.annotations.Operation;
15 12 import io.swagger.v3.oas.annotations.tags.Tag;
16 13 import jakarta.annotation.Resource;
17 14 import jakarta.validation.Valid;
18   -import org.springframework.security.access.prepost.PreAuthorize;
19 15 import org.springframework.validation.annotation.Validated;
20 16 import org.springframework.web.bind.annotation.*;
21 17  
... ... @@ -36,6 +32,9 @@ public class IotDeviceDataController {
36 32 @Resource
37 33 private IotDeviceLogDataService iotDeviceLogDataService;
38 34  
  35 + @Resource // TODO @super:service 之间,不用空行;原因是,这样更简洁;空行,主要是为了“间隔”,提升可读性
  36 + private IotDeviceLogDataService deviceLogDataService;
  37 +
39 38 // TODO @浩浩:这里的 /latest-list,包括方法名。
40 39 @GetMapping("/latest")
41 40 @Operation(summary = "获取设备属性最新数据")
... ... @@ -52,12 +51,22 @@ public class IotDeviceDataController {
52 51 return success(BeanUtils.toBean(list, IotTimeDataRespVO.class));
53 52 }
54 53  
  54 + // TODO:数据权限
55 55 @PostMapping("/simulator")
56 56 @Operation(summary = "模拟设备")
57 57 public CommonResult<Boolean> simulatorDevice(@Valid @RequestBody IotDeviceDataSimulatorSaveReqVO simulatorReqVO) {
58   - //TODO:先生成一下日志 后续完善模拟设备代码逻辑
  58 + //TODO:先生成一下设备日志 后续完善模拟设备代码逻辑
  59 + // TODO @super:应该 deviceDataService 里面有个 simulatorDevice,然后里面去 insert 日志!
59 60 iotDeviceLogDataService.createDeviceLog(simulatorReqVO);
60 61 return success(true);
61 62 }
62 63  
  64 + // TODO:数据权限
  65 + @GetMapping("/log/page")
  66 + @Operation(summary = "获得设备日志分页")
  67 + public CommonResult<PageResult<IotDeviceLogRespVO>> getDeviceLogPage(@Valid IotDeviceLogPageReqVO pageReqVO) {
  68 + PageResult<IotDeviceLogDO> pageResult = deviceLogDataService.getDeviceLogPage(pageReqVO);
  69 + return success(BeanUtils.toBean(pageResult, IotDeviceLogRespVO.class));
  70 + }
  71 +
63 72 }
64 73 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceSaveReqVO.java
... ... @@ -14,7 +14,7 @@ public class IotDeviceSaveReqVO {
14 14 private Long id;
15 15  
16 16 @Schema(description = "设备编号", requiredMode = Schema.RequiredMode.AUTO, example = "177")
17   - @Size(max = 50, message = "设备编号长度不能超过50个字符")
  17 + @Size(max = 50, message = "设备编号长度不能超过 50 个字符")
18 18 private String deviceKey;
19 19  
20 20 @Schema(description = "设备名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/deviceData/IotDeviceDataSimulatorSaveReqVO.java
... ... @@ -4,24 +4,26 @@ import io.swagger.v3.oas.annotations.media.Schema;
4 4 import jakarta.validation.constraints.NotEmpty;
5 5 import lombok.Data;
6 6  
7   -
8   -import java.time.LocalDateTime;
9   -
  7 +// TODO super: SaveReqVO => ReqVO
10 8 @Schema(description = "管理后台 - IoT 模拟设备数据 Request VO")
11 9 @Data
12 10 public class IotDeviceDataSimulatorSaveReqVO {
13 11  
14   - @Schema(description = "消息ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "msg123")
  12 + // TODO @super:感觉后端随机更合适?
  13 + @Schema(description = "消息 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "msg123")
15 14 private String id;
16 15  
  16 + // TODO @super:不用传递 productKey,因为 deviceKey 可以推导出来
17 17 @Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "product123")
18 18 @NotEmpty(message = "产品ID不能为空")
19 19 private String productKey;
20 20  
  21 + // TODO @super:中文写作规范,中英文之间,要有空格。例如说,设备 ID。ps:这里应该是设备标识
21 22 @Schema(description = "设备ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "device123")
22 23 @NotEmpty(message = "设备ID不能为空")
23 24 private String deviceKey;
24 25  
  26 + // TODO @super:type、subType,是不是不用传递,因为模拟只有属性???
25 27 @Schema(description = "消息/日志类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property")
26 28 @NotEmpty(message = "消息类型不能为空")
27 29 private String type;
... ... @@ -34,7 +36,8 @@ public class IotDeviceDataSimulatorSaveReqVO {
34 36 @NotEmpty(message = "数据内容不能为空")
35 37 private String content;
36 38  
  39 + // TODO @芋艿:需要讨论下,reportTime 到底以那个为准!
37 40 @Schema(description = "上报时间", requiredMode = Schema.RequiredMode.REQUIRED)
38   - private LocalDateTime reportTime;
  41 + private Long reportTime;
39 42  
40 43 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/deviceData/IotDeviceLogPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import jakarta.validation.constraints.NotEmpty;
  6 +import lombok.Data;
  7 +import org.springframework.format.annotation.DateTimeFormat;
  8 +
  9 +import java.time.LocalDateTime;
  10 +
  11 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  12 +
  13 +@Schema(description = "管理后台 - IoT 设备日志分页查询 Request VO")
  14 +@Data
  15 +public class IotDeviceLogPageReqVO extends PageParam {
  16 +
  17 + @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "device123")
  18 + @NotEmpty(message = "设备标识不能为空")
  19 + private String deviceKey;
  20 +
  21 + // TODO @super:对应的枚举类
  22 + @Schema(description = "消息类型", example = "property")
  23 + private String type;
  24 +
  25 + @Schema(description = "标识符", example = "temperature")
  26 + // TODO @super:对应的枚举类
  27 + private String subType;
  28 +
  29 + @Schema(description = "创建时间")
  30 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  31 + private LocalDateTime[] createTime;
  32 +
  33 +}
0 34 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/deviceData/IotDeviceLogRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - IoT 设备日志 Response VO")
  9 +@Data
  10 +public class IotDeviceLogRespVO {
  11 +
  12 + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
  13 + private String id;
  14 +
  15 + @Schema(description = "产品标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "product123")
  16 + private String productKey;
  17 +
  18 + @Schema(description = "设备标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "device123")
  19 + private String deviceKey;
  20 +
  21 + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "property")
  22 + private String type;
  23 +
  24 + @Schema(description = "标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "temperature")
  25 + private String subType;
  26 +
  27 + @Schema(description = "日志内容", requiredMode = Schema.RequiredMode.REQUIRED)
  28 + private String content;
  29 +
  30 + @Schema(description = "上报时间", requiredMode = Schema.RequiredMode.REQUIRED)
  31 + private LocalDateTime reportTime;
  32 +
  33 + @Schema(description = "记录时间戳", requiredMode = Schema.RequiredMode.REQUIRED)
  34 + private LocalDateTime ts;
  35 +
  36 +}
0 37 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/PluginInfoImportReqVO.java
... ... @@ -9,7 +9,7 @@ import org.springframework.web.multipart.MultipartFile;
9 9 @Data
10 10 public class PluginInfoImportReqVO {
11 11  
12   - @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
  12 + @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
13 13 private Long id;
14 14  
15 15 @Schema(description = "插件文件", requiredMode = Schema.RequiredMode.REQUIRED)
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/PluginInfoPageReqVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
4 6 import io.swagger.v3.oas.annotations.media.Schema;
5 7 import lombok.Data;
6 8  
... ... @@ -11,7 +13,8 @@ public class PluginInfoPageReqVO extends PageParam {
11 13 @Schema(description = "插件名称", example = "http")
12 14 private String name;
13 15  
14   - @Schema(description = "状态")
  16 + @Schema(description = "状态", example = "1")
  17 + @InEnum(IotPluginStatusEnum.class)
15 18 private Integer status;
16 19  
17 20 }
18 21 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/PluginInfoRespVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
2 2  
3   -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
4   -import com.alibaba.excel.annotation.ExcelProperty;
5 3 import io.swagger.v3.oas.annotations.media.Schema;
6 4 import lombok.Data;
7 5  
... ... @@ -9,63 +7,48 @@ import java.time.LocalDateTime;
9 7  
10 8 @Schema(description = "管理后台 - IoT 插件信息 Response VO")
11 9 @Data
12   -@ExcelIgnoreUnannotated
13 10 public class PluginInfoRespVO {
14 11  
15 12 @Schema(description = "主键 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
16   - @ExcelProperty("主键 ID")
17 13 private Long id;
18 14  
19 15 @Schema(description = "插件包标识符", requiredMode = Schema.RequiredMode.REQUIRED, example = "24627")
20   - @ExcelProperty("插件包标识符")
21 16 private String pluginKey;
22 17  
23 18 @Schema(description = "插件名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六")
24   - @ExcelProperty("插件名称")
25 19 private String name;
26 20  
27 21 @Schema(description = "描述", example = "你猜")
28   - @ExcelProperty("描述")
29 22 private String description;
30 23  
31 24 @Schema(description = "部署方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
32   - @ExcelProperty("部署方式")
33 25 private Integer deployType;
34 26  
35 27 @Schema(description = "插件包文件名", requiredMode = Schema.RequiredMode.REQUIRED)
36   - @ExcelProperty("插件包文件名")
37 28 private String fileName;
38 29  
39 30 @Schema(description = "插件版本", requiredMode = Schema.RequiredMode.REQUIRED)
40   - @ExcelProperty("插件版本")
41 31 private String version;
42 32  
43 33 @Schema(description = "插件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
44   - @ExcelProperty("插件类型")
45 34 private Integer type;
46 35  
47 36 @Schema(description = "设备插件协议类型")
48   - @ExcelProperty("设备插件协议类型")
49 37 private String protocol;
50 38  
51 39 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED)
52   - @ExcelProperty("状态")
53 40 private Integer status;
54 41  
55 42 @Schema(description = "插件配置项描述信息")
56   - @ExcelProperty("插件配置项描述信息")
57 43 private String configSchema;
58 44  
59 45 @Schema(description = "插件配置信息")
60   - @ExcelProperty("插件配置信息")
61 46 private String config;
62 47  
63 48 @Schema(description = "插件脚本")
64   - @ExcelProperty("插件脚本")
65 49 private String script;
66 50  
67 51 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
68   - @ExcelProperty("创建时间")
69 52 private LocalDateTime createTime;
70 53  
71 54 }
72 55 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/plugin/vo/PluginInfoSaveReqVO.java
1 1 package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo;
2 2  
  3 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  4 +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
3 5 import io.swagger.v3.oas.annotations.media.Schema;
4   -import lombok.*;
  6 +import lombok.Data;
5 7  
6 8 @Schema(description = "管理后台 - IoT 插件信息新增/修改 Request VO")
7 9 @Data
8 10 public class PluginInfoSaveReqVO {
9 11  
  12 + // TODO @haohao:新增的字段有点多,每个都需要哇?
  13 +
  14 + // TODO @haohao:一些枚举字段,需要加枚举校验。例如说,deployType、status、type 等
  15 +
10 16 @Schema(description = "主键ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "11546")
11 17 private Long id;
12 18  
... ... @@ -35,6 +41,7 @@ public class PluginInfoSaveReqVO {
35 41 private String protocol;
36 42  
37 43 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED)
  44 + @InEnum(IotPluginStatusEnum.class)
38 45 private Integer status;
39 46  
40 47 @Schema(description = "插件配置项描述信息")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/IotThingModelController.java
... ... @@ -74,6 +74,7 @@ public class IotThingModelController {
74 74 return success(IotThingModelConvert.INSTANCE.convertList(list));
75 75 }
76 76  
  77 + // TODO @puhui @super:getThingModelListByProductId 和 getThingModelListByProductId 可以融合么?
77 78 @GetMapping("/list")
78 79 @Operation(summary = "获得产品物模型列表")
79 80 @PreAuthorize("@ss.hasPermission('iot:thing-model:query')")
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelListReqVO.java
... ... @@ -6,11 +6,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
6 6 import jakarta.validation.constraints.NotNull;
7 7 import lombok.Data;
8 8  
9   -
10   -
11 9 @Schema(description = "管理后台 - IoT 产品物模型List Request VO")
12 10 @Data
13 11 public class IotThingModelListReqVO {
  12 +
14 13 @Schema(description = "功能标识")
15 14 private String identifier;
16 15  
... ... @@ -21,7 +20,8 @@ public class IotThingModelListReqVO {
21 20 @InEnum(IotThingModelTypeEnum.class)
22 21 private Integer type;
23 22  
24   - @Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED)
25   - @NotNull(message = "产品ID不能为空")
  23 + @Schema(description = "产品 ID", requiredMode = Schema.RequiredMode.REQUIRED)
  24 + @NotNull(message = "产品 ID 不能为空")
26 25 private Long productId;
  26 +
27 27 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/device/IotDeviceLogDO.java
1 1 package cn.iocoder.yudao.module.iot.dal.dataobject.device;
2 2  
3   -import cn.hutool.core.date.DateTime;
4 3 import lombok.AllArgsConstructor;
5 4 import lombok.Builder;
6 5 import lombok.Data;
7 6 import lombok.NoArgsConstructor;
8 7  
9   -import java.time.LocalDateTime;
10   -
11 8 /**
12 9 * IoT 设备日志数据 DO
13 10 *
  11 + * 目前使用 TDengine 存储
  12 + *
14 13 * @author alwayssuper
15 14 */
16 15 @Data
... ... @@ -18,45 +17,52 @@ import java.time.LocalDateTime;
18 17 @NoArgsConstructor
19 18 @AllArgsConstructor
20 19 public class IotDeviceLogDO {
  20 +
  21 + // TODO @芋艿:消息 ID 的生成逻辑
21 22 /**
22   - * 消息ID
  23 + * 消息 ID
23 24 */
24 25 private String id;
25 26  
  27 + // TODO @super:关联要 @下
26 28 /**
27   - * 产品ID
  29 + * 产品标识
28 30 */
29 31 private String productKey;
30 32  
  33 + // TODO @super:关联要 @下
31 34 /**
32   - * 设备ID
  35 + * 设备标识
33 36 */
34 37 private String deviceKey;
35 38  
  39 + // TODO @super:枚举类
36 40 /**
37   - * 消息/日志类型
  41 + * 日志类型
38 42 */
39 43 private String type;
40 44  
  45 + // TODO @super:枚举类
41 46 /**
42 47 * 标识符:用于标识具体的属性、事件或服务
43 48 */
44 49 private String subType;
45 50  
46 51 /**
47   - * 数据内容:存储具体的消息数据内容,通常是JSON格式
  52 + * 数据内容
  53 + *
  54 + * 存储具体的消息数据内容,通常是 JSON 格式
48 55 */
49 56 private String content;
50 57  
51 58 /**
52 59 * 上报时间戳
53 60 */
54   - private DateTime reportTime;
  61 + private Long reportTime;
55 62  
56 63 /**
57 64 * 时序时间
58 65 */
59   - private DateTime ts;
60   -
  66 + private Long ts;
61 67  
62 68 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugininstance/PluginInstanceDO.java
... ... @@ -33,19 +33,22 @@ public class PluginInstanceDO extends BaseDO {
33 33 */
34 34 private String mainId;
35 35 /**
36   - * 插件id
  36 + * 插件 ID
37 37 * <p>
38 38 * 关联 {@link PluginInfoDO#getId()}
39 39 */
40 40 private Long pluginId;
  41 +
41 42 /**
42   - * 插件主程序所在ip
  43 + * 插件主程序所在 IP
43 44 */
44 45 private String ip;
45 46 /**
46 47 * 插件主程序端口
47 48 */
48 49 private Integer port;
  50 +
  51 + // TODO @haohao:字段改成 heartbeatTime,LocalDateTime
49 52 /**
50 53 * 心跳时间,心路时间超过 30 秒需要剔除
51 54 */
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/tdengine/ThingModelMessageDO.java
... ... @@ -6,7 +6,7 @@ import lombok.Data;
6 6 import lombok.NoArgsConstructor;
7 7  
8 8 // TODO @芋艿:纠结下字段
9   -@Deprecated
  9 +@Deprecated // TODO @super:看看啥时候删除下哈。
10 10 /**
11 11 * TD 物模型消息日志的数据库
12 12 */
... ... @@ -25,7 +25,7 @@ public class ThingModelMessageDO {
25 25  
26 26 /**
27 27 * 系统扩展参数
28   - *
  28 + *
29 29 * 例如:设备状态、系统时间、固件版本等系统级信息
30 30 */
31 31 private Object system;
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/PluginInfoMapper.java
... ... @@ -4,6 +4,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
  7 +
  8 +import java.util.List;
  9 +
7 10 import org.apache.ibatis.annotations.Mapper;
8 11 import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.*;
9 12  
... ... @@ -22,4 +25,10 @@ public interface PluginInfoMapper extends BaseMapperX&lt;PluginInfoDO&gt; {
22 25 .orderByDesc(PluginInfoDO::getId));
23 26 }
24 27  
  28 + default List<PluginInfoDO> selectListByStatus(Integer status) {
  29 + return selectList(new LambdaQueryWrapperX<PluginInfoDO>()
  30 + .eq(PluginInfoDO::getStatus, status)
  31 + .orderByAsc(PluginInfoDO::getId));
  32 + }
  33 +
25 34 }
26 35 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/PluginInstanceMapper.java
... ... @@ -21,6 +21,7 @@ public interface PluginInstanceMapper extends BaseMapperX&lt;PluginInstanceDO&gt; {
21 21 .eq(PluginInstanceDO::getPluginId, pluginId));
22 22 }
23 23  
  24 + // TODO @haohao:这个还需要么?相关不用的 VO 可以删除
24 25 default PageResult<PluginInstanceDO> selectPage(PluginInstancePageReqVO reqVO) {
25 26 return selectPage(reqVO, new LambdaQueryWrapperX<PluginInstanceDO>()
26 27 .eqIfPresent(PluginInstanceDO::getMainId, reqVO.getMainId())
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/IotDeviceLogDataMapper.java
1 1 package cn.iocoder.yudao.module.iot.dal.tdengine;
2 2  
  3 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceLogPageReqVO;
3 4 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO;
4 5 import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS;
5 6 import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
6 7 import org.apache.ibatis.annotations.Mapper;
7 8 import org.apache.ibatis.annotations.Param;
8 9  
  10 +import java.util.List;
  11 +
9 12 /**
10   - * IoT 设备日志 Mapper
  13 + * IOT 设备日志数据 Mapper 接口
11 14 *
12   - * @author alwayssuper
  15 + * 基于 TDengine 实现设备日志的存储
13 16 */
14 17 @Mapper
15 18 @TDengineDS
... ... @@ -18,22 +21,58 @@ public interface IotDeviceLogDataMapper {
18 21  
19 22 /**
20 23 * 创建设备日志超级表
  24 +<<<<<<< HEAD
21 25 * 初始化只创建一次
22 26 */
23 27 void createDeviceLogSTable();
24 28  
  29 +=======
  30 + *
  31 + * 注意:初始化时只需创建一次
  32 + */
  33 + void createDeviceLogSTable();
  34 +
  35 + // TODO @super:是不是删除哈
  36 +>>>>>>> deab8c1cc6bb7864d9c40e0c369f649f6f9bfa41
25 37 /**
26 38 * 创建设备日志子表
27 39 *
28 40 * @param deviceKey 设备标识
29 41 */
  42 +<<<<<<< HEAD
30 43 void createDeviceLogTable( @Param("deviceKey") String deviceKey);
31 44  
32 45 /**
33 46 * 插入设备日志数据
34 47 *
  48 +=======
  49 + void createDeviceLogTable(@Param("deviceKey") String deviceKey);
  50 +
  51 + // TODO @super:单个参数,不用加 @Param
  52 + /**
  53 + * 插入设备日志数据
  54 + *
  55 + * 如果子表不存在,会自动创建子表
  56 + *
  57 +>>>>>>> deab8c1cc6bb7864d9c40e0c369f649f6f9bfa41
35 58 * @param log 设备日志数据
36 59 */
37 60 void insert(@Param("log") IotDeviceLogDO log);
38 61  
  62 + /**
  63 + * 获得设备日志分页
  64 + *
  65 + * @param reqVO 分页查询条件
  66 + * @return 设备日志列表
  67 + */
  68 + List<IotDeviceLogDO> selectPage(@Param("reqVO") IotDeviceLogPageReqVO reqVO);
  69 +
  70 + /**
  71 + * 获得设备日志总数
  72 + *
  73 + * @param reqVO 查询条件
  74 + * @return 日志总数
  75 + */
  76 + Long selectCount(@Param("reqVO") IotDeviceLogPageReqVO reqVO);
  77 +
39 78 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/tdengine/TdThingModelMessageMapper.java
1 1 package cn.iocoder.yudao.module.iot.dal.tdengine;
2 2  
3   -import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
4   -import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.ThingModelMessage;
5   -import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.ThingModelMessageDO;
6 3 import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS;
7   -import com.baomidou.dynamic.datasource.annotation.DS;
8 4 import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
9 5 import org.apache.ibatis.annotations.Mapper;
10 6 import org.apache.ibatis.annotations.Param;
... ... @@ -13,7 +9,7 @@ import org.apache.ibatis.annotations.Param;
13 9 * 处理 TD 中物模型消息日志的操作
14 10 */
15 11 @Mapper
16   -@Deprecated
  12 +@Deprecated // TODO super:什么时候,删除下哈。
17 13 @TDengineDS
18 14 @InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错
19 15 public interface TdThingModelMessageMapper {
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/callback/EmqxCallback.java
... ... @@ -17,7 +17,7 @@ import org.springframework.stereotype.Component;
17 17 * @author ahh
18 18 */
19 19 @Slf4j
20   -@Component
  20 +//@Component
21 21 public class EmqxCallback implements MqttCallbackExtended {
22 22  
23 23 @Lazy
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/client/EmqxClient.java
... ... @@ -19,7 +19,7 @@ import org.springframework.stereotype.Component;
19 19 */
20 20 @Slf4j
21 21 @Data
22   -@Component
  22 +//@Component
23 23 public class EmqxClient {
24 24  
25 25 @Resource
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/config/MqttConfig.java
... ... @@ -12,8 +12,8 @@ import org.springframework.stereotype.Component;
12 12 * @author ahh
13 13 */
14 14 @Data
15   -@Component
16   -@ConfigurationProperties("iot.emq")
  15 +//@Component
  16 +//@ConfigurationProperties("iot.emq")
17 17 public class MqttConfig {
18 18  
19 19 /**
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxServiceImpl.java
1 1 package cn.iocoder.yudao.module.iot.emq.service;
2 2  
  3 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
3 4 import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
4 5 import jakarta.annotation.Resource;
5 6 import lombok.extern.slf4j.Slf4j;
6 7 import org.eclipse.paho.client.mqttv3.MqttClient;
7 8 import org.eclipse.paho.client.mqttv3.MqttMessage;
8 9 import org.springframework.scheduling.annotation.Async;
9   -import org.springframework.stereotype.Service;
10 10  
11 11 // TODO @芋艿:在瞅瞅
12 12  
... ... @@ -16,7 +16,7 @@ import org.springframework.stereotype.Service;
16 16 * @author ahh
17 17 */
18 18 @Slf4j
19   -@Service
  19 +// @Service
20 20 public class EmqxServiceImpl implements EmqxService {
21 21  
22 22 @Resource
... ... @@ -34,7 +34,12 @@ public class EmqxServiceImpl implements EmqxService {
34 34 String productKey = topic.split("/")[2];
35 35 String deviceName = topic.split("/")[3];
36 36 String message = new String(mqttMessage.getPayload());
37   - iotDeviceDataService.saveDeviceData(productKey, deviceName, message);
  37 + DeviceDataCreateReqDTO createDTO = DeviceDataCreateReqDTO.builder()
  38 + .productKey(productKey)
  39 + .deviceName(deviceName)
  40 + .message(message)
  41 + .build();
  42 + iotDeviceDataService.saveDeviceData(createDTO);
38 43 }
39 44 }
40 45  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/start/EmqxStart.java
... ... @@ -13,7 +13,7 @@ import org.springframework.stereotype.Component;
13 13 *
14 14 * @author ahh
15 15 */
16   -@Component
  16 +//@Component
17 17 public class EmqxStart implements ApplicationRunner {
18 18  
19 19 @Resource
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/PluginStart.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.framework.plugin;
  2 +
  3 +import java.util.List;
  4 +
  5 +import javax.annotation.Resource;
  6 +
  7 +import org.pf4j.spring.SpringPluginManager;
  8 +import org.springframework.boot.ApplicationArguments;
  9 +import org.springframework.boot.ApplicationRunner;
  10 +import org.springframework.stereotype.Component;
  11 +
  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;
  19 +
  20 +@Component
  21 +@Slf4j
  22 +public class PluginStart implements ApplicationRunner {
  23 +
  24 + @Resource
  25 + private PluginInfoService pluginInfoService;
  26 +
  27 + @Resource
  28 + private SpringPluginManager pluginManager;
  29 +
  30 + @Override
  31 + public void run(ApplicationArguments args) {
  32 + TenantUtils.executeIgnore(() -> { // 1. 忽略租户上下文执行
  33 + List<PluginInfoDO> pluginInfoList = pluginInfoService
  34 + .getPluginInfoListByStatus(IotPluginStatusEnum.RUNNING.getStatus()); // 2. 获取运行中的插件列表
  35 + if (CollUtil.isEmpty(pluginInfoList)) { // 3. 检查插件列表是否为空
  36 + log.info("[run] 没有需要启动的插件"); // 4. 日志记录没有插件需要启动
  37 + return;
  38 + }
  39 + pluginInfoList.forEach(pluginInfo -> { // 5. 使用lambda表达式遍历插件列表
  40 + try {
  41 + log.info("[run][启动插件] pluginKey = {}", pluginInfo.getPluginKey()); // 6. 日志记录插件启动信息
  42 + pluginManager.startPlugin(pluginInfo.getPluginKey()); // 7. 启动插件
  43 + } catch (Exception e) {
  44 + log.error("[run][启动插件失败] pluginKey = {}", pluginInfo.getPluginKey(), e); // 8. 记录启动失败的日志
  45 + }
  46 + });
  47 + });
  48 +
  49 + }
  50 +
  51 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/plugin/UnifiedConfiguration.java
... ... @@ -31,7 +31,13 @@ public class UnifiedConfiguration {
31 31 @DependsOn(SERVICE_REGISTRY_INITIALIZED_MARKER)
32 32 public SpringPluginManager pluginManager() {
33 33 log.info("[init][实例化 SpringPluginManager]");
34   - SpringPluginManager springPluginManager = new SpringPluginManager();
  34 + SpringPluginManager springPluginManager = new SpringPluginManager() {
  35 + @Override
  36 + public void startPlugins() {
  37 + // 禁用插件启动,避免插件启动时,启动所有插件
  38 + log.info("[init][禁用默认启动所有插件]");
  39 + }
  40 + };
35 41 springPluginManager.addPluginStateListener(new CustomPluginStateListener());
36 42 return springPluginManager;
37 43 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/config/TDengineTableInitConfiguration.java
... ... @@ -16,7 +16,7 @@ import org.springframework.core.annotation.Order;
16 16 @Slf4j
17 17 @RequiredArgsConstructor
18 18 @Configuration
19   -@Order(Integer.MAX_VALUE) // 保证在最后执行
  19 +@Order
20 20 public class TDengineTableInitConfiguration implements ApplicationRunner {
21 21  
22 22 private final IotDeviceLogDataService deviceLogService;
... ... @@ -26,15 +26,18 @@ public class TDengineTableInitConfiguration implements ApplicationRunner {
26 26 try {
27 27 // 初始化设备日志表
28 28 deviceLogService.initTDengineSTable();
29   - log.info("初始化 设备日志表 TDengine 表结构成功");
  29 + // TODO @super:这个日志,是不是不用打,不然重复啦!!!
  30 + log.info("[run]初始化 设备日志表 TDengine 表结构成功");
30 31 } catch (Exception ex) {
  32 + // TODO @super:初始化失败,打印 error 日志,退出系统。。不然跑起来,就初始啦!!!
31 33 if (ex.getMessage().contains("Table already exists")) {
32 34 log.info("TDengine 设备日志超级表已存在,跳过创建");
33 35 return;
34   - }else{
  36 + } else{
35 37 log.error("初始化 设备日志表 TDengine 表结构失败", ex);
36 38 }
37 39 throw ex;
38 40 }
39 41 }
  42 +
40 43 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/PluginInstancesJob.java
1 1 package cn.iocoder.yudao.module.iot.job.plugin;
2 2  
3   -
4 3 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
5 4 import cn.iocoder.yudao.module.iot.service.plugin.PluginInstanceService;
6 5 import org.springframework.scheduling.annotation.Scheduled;
... ... @@ -23,7 +22,8 @@ public class PluginInstancesJob {
23 22 @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
24 23 public void updatePluginInstances() {
25 24 TenantUtils.executeIgnore(() -> {
26   - pluginInstanceService.updatePluginInstances();
  25 + pluginInstanceService.reportPluginInstances();
27 26 });
28 27 }
  28 +
29 29 }
30 30 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mqttrpc/config/MqttConfig.java 0 → 100644
  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 0 → 100644
  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 +}
0 101 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceLogDataService.java
1 1 package cn.iocoder.yudao.module.iot.service.device;
2 2  
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 4 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataSimulatorSaveReqVO;
  5 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceLogPageReqVO;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO;
4 7  
5 8 /**
6 9 * IoT 设备日志数据 Service 接口
... ... @@ -10,13 +13,27 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDevi
10 13 public interface IotDeviceLogDataService {
11 14  
12 15 /**
13   - * 初始化 TDengine 表
  16 + * 初始化 TDengine 超级表
  17 + *
  18 + *系统启动时,会自动初始化一次
14 19 */
15 20 void initTDengineSTable();
16 21  
17 22 /**
18   - * 模拟设备创建设备日志
19   - * @param simulatorReqVO 模拟设备信息
  23 + * 插入设备日志
  24 + *
  25 + * 当该设备第一次插入日志时,自动创建该设备的设备日志子表
  26 + *
  27 + * @param simulatorReqVO 设备日志模拟数据
20 28 */
21 29 void createDeviceLog(IotDeviceDataSimulatorSaveReqVO simulatorReqVO);
  30 +
  31 + /**
  32 + * 获得设备日志分页
  33 + *
  34 + * @param pageReqVO 分页查询
  35 + * @return 设备日志分页
  36 + */
  37 + PageResult<IotDeviceLogDO> getDeviceLogPage(IotDeviceLogPageReqVO pageReqVO);
  38 +
22 39 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceLogDataServiceImpl.java
1 1 package cn.iocoder.yudao.module.iot.service.device;
2 2  
3   -import cn.hutool.core.date.DateTime;
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 5 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataSimulatorSaveReqVO;
  6 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceLogPageReqVO;
6 7 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO;
7 8 import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceLogDataMapper;
8 9 import jakarta.annotation.Resource;
... ... @@ -10,7 +11,7 @@ import lombok.extern.slf4j.Slf4j;
10 11 import org.springframework.stereotype.Service;
11 12 import org.springframework.validation.annotation.Validated;
12 13  
13   -import java.time.LocalDateTime;
  14 +import java.util.List;
14 15  
15 16 /**
16 17 * IoT 设备日志数据 Service 实现了
... ... @@ -19,27 +20,43 @@ import java.time.LocalDateTime;
19 20 */
20 21 @Service
21 22 @Slf4j
  23 +@Validated
22 24 public class IotDeviceLogDataServiceImpl implements IotDeviceLogDataService{
23 25  
24 26 @Resource
25 27 private IotDeviceLogDataMapper iotDeviceLogDataMapper;
26 28  
27   -
  29 + // TODO @super:方法名。defineDeviceLog。。未来,有可能别人使用别的记录日志,例如说 es 之类的。
28 30 @Override
29 31 public void initTDengineSTable() {
30   - try {
31   - // 创建设备日志超级表
32   - iotDeviceLogDataMapper.createDeviceLogSTable();
33   - log.info("创建设备日志超级表成功");
34   - } catch (Exception ex) {
35   - throw ex;
36   - }
  32 + // TODO @super:改成不存在才创建。
  33 + iotDeviceLogDataMapper.createDeviceLogSTable();
37 34 }
38 35  
39 36 @Override
40 37 public void createDeviceLog(IotDeviceDataSimulatorSaveReqVO simulatorReqVO) {
  38 + // 1. 转换请求对象为 DO
41 39 IotDeviceLogDO iotDeviceLogDO = BeanUtils.toBean(simulatorReqVO, IotDeviceLogDO.class);
42   - iotDeviceLogDO.setTs(DateTime.now());
  40 +
  41 + // 2. 处理时间字段
  42 + // TODO @super:一次性的字段,不用单独给个变量
  43 + long currentTime = System.currentTimeMillis();
  44 + // 2.1 设置时序时间为当前时间
  45 + iotDeviceLogDO.setTs(currentTime); // TODO @super:TS在SQL中直接NOW 咱们的TS数据获取是走哪一种;走 now()
  46 +
  47 + // 3. 插入数据
  48 + // TODO @super:不要直接调用对方的 IotDeviceLogDataMapper,通过 service 哈!
43 49 iotDeviceLogDataMapper.insert(iotDeviceLogDO);
44 50 }
  51 +
  52 + // TODO @super:在 iotDeviceLogDataService 写
  53 + @Override
  54 + public PageResult<IotDeviceLogDO> getDeviceLogPage(IotDeviceLogPageReqVO pageReqVO) {
  55 + // 查询数据
  56 + List<IotDeviceLogDO> list = iotDeviceLogDataMapper.selectPage(pageReqVO);
  57 + Long total = iotDeviceLogDataMapper.selectCount(pageReqVO);
  58 + // 构造分页结果
  59 + return new PageResult<>(list, total);
  60 + }
  61 +
45 62 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDevicePropertyDataService.java
1 1 package cn.iocoder.yudao.module.iot.service.device;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
4 5 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataPageReqVO;
5 6 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDataDO;
6 7 import jakarta.validation.Valid;
... ... @@ -25,12 +26,9 @@ public interface IotDevicePropertyDataService {
25 26 /**
26 27 * 保存设备数据
27 28 *
28   - * @param productKey 产品 key
29   - * @param deviceName 设备名称
30   - * @param message 消息
31   - * <p>参见 <a href="https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services?spm=a2c4g.11186623.0.0.3a3335aeUdzkz2#concept-mvc-4tw-y2b">JSON 格式</a>
  29 + * @param createDTO 设备数据
32 30 */
33   - void saveDeviceData(String productKey, String deviceName, String message);
  31 + void saveDeviceData(DeviceDataCreateReqDTO createDTO);
34 32  
35 33 /**
36 34 * 获得设备属性最新数据
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDevicePropertyDataServiceImpl.java
... ... @@ -6,6 +6,7 @@ import cn.hutool.core.map.MapUtil;
6 6 import cn.hutool.core.util.StrUtil;
7 7 import cn.hutool.json.JSONObject;
8 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  9 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
9 10 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataPageReqVO;
10 11 import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
11 12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
... ... @@ -14,10 +15,9 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
14 15 import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.SelectVisualDO;
15 16 import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.ThingModelMessage;
16 17 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
17   -import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyDataMapper;
18 18 import cn.iocoder.yudao.module.iot.dal.redis.deviceData.DeviceDataRedisDAO;
  19 +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyDataMapper;
19 20 import cn.iocoder.yudao.module.iot.dal.tdengine.TdEngineDMLMapper;
20   -import cn.iocoder.yudao.module.iot.dal.tdengine.TdThingModelMessageMapper;
21 21 import cn.iocoder.yudao.module.iot.enums.IotConstants;
22 22 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
23 23 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
... ... @@ -57,7 +57,7 @@ public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataSe
57 57 .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT)
58 58 .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE)
59 59 .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明?
60   - .put( IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明?
  60 + .put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明?
61 61 .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_NCHAR)
62 62 .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP)
63 63 .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!!
... ... @@ -110,7 +110,6 @@ public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataSe
110 110 return;
111 111 }
112 112 newFields.add(0, new TDengineTableField(TDengineTableField.FIELD_TS, TDengineTableField.TYPE_TIMESTAMP));
113   - // 2.1.1 创建产品超级表
114 113 devicePropertyDataMapper.createProductPropertySTable(product.getProductKey(), newFields);
115 114 return;
116 115 }
... ... @@ -131,20 +130,20 @@ public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataSe
131 130 }
132 131  
133 132 @Override
134   - public void saveDeviceData(String productKey, String deviceName, String message) {
  133 + public void saveDeviceData(DeviceDataCreateReqDTO createDTO) {
135 134 // 1. 根据产品 key 和设备名称,获得设备信息
136   - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceName(productKey, deviceName);
  135 + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceName(createDTO.getProductKey(), createDTO.getDeviceName());
137 136 // 2. 解析消息,保存数据
138   - JSONObject jsonObject = new JSONObject(message);
139   - log.info("[saveDeviceData][productKey({}) deviceName({}) data({})]", productKey, deviceName, jsonObject);
  137 + JSONObject jsonObject = new JSONObject(createDTO.getMessage());
  138 + log.info("[saveDeviceData][productKey({}) deviceName({}) data({})]", createDTO.getProductKey(), createDTO.getDeviceName(), jsonObject);
140 139 ThingModelMessage thingModelMessage = ThingModelMessage.builder()
141 140 .id(jsonObject.getStr("id"))
142 141 .sys(jsonObject.get("sys"))
143 142 .method(jsonObject.getStr("method"))
144 143 .params(jsonObject.get("params"))
145 144 .time(jsonObject.getLong("time") == null ? System.currentTimeMillis() : jsonObject.getLong("time"))
146   - .productKey(productKey)
147   - .deviceName(deviceName)
  145 + .productKey(createDTO.getProductKey())
  146 + .deviceName(createDTO.getDeviceName())
148 147 .deviceKey(device.getDeviceKey())
149 148 .build();
150 149 thingModelMessageService.saveThingModelMessage(device, thingModelMessage);
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/ExampleService.java 0 → 100644
  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 +}
0 44 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/PluginInfoService.java
... ... @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoPageReqVO;
5 5 import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.PluginInfoSaveReqVO;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
  7 +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
7 8 import jakarta.validation.Valid;
8 9 import org.springframework.web.multipart.MultipartFile;
9 10  
... ... @@ -66,7 +67,7 @@ public interface PluginInfoService {
66 67 * 更新插件的状态
67 68 *
68 69 * @param id 插件id
69   - * @param status 状态
  70 + * @param status 状态 {@link IotPluginStatusEnum}
70 71 */
71 72 void updatePluginStatus(Long id, Integer status);
72 73  
... ... @@ -76,4 +77,12 @@ public interface PluginInfoService {
76 77 * @return 插件信息列表
77 78 */
78 79 List<PluginInfoDO> getPluginInfoList();
79   -}
80 80 \ No newline at end of file
  81 +
  82 + /**
  83 + * 根据状态获得插件信息列表
  84 + *
  85 + * @param status 状态 {@link IotPluginStatusEnum}
  86 + * @return 插件信息列表
  87 + */
  88 + List<PluginInfoDO> getPluginInfoListByStatus(Integer status);
  89 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/PluginInfoServiceImpl.java
... ... @@ -9,25 +9,16 @@ import cn.iocoder.yudao.module.iot.dal.mysql.plugin.PluginInfoMapper;
9 9 import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
10 10 import jakarta.annotation.Resource;
11 11 import lombok.extern.slf4j.Slf4j;
12   -import org.pf4j.PluginDescriptor;
13   -import org.pf4j.PluginState;
14   -import org.pf4j.PluginWrapper;
15 12 import org.pf4j.spring.SpringPluginManager;
16   -import org.springframework.beans.factory.annotation.Value;
17 13 import org.springframework.stereotype.Service;
18 14 import org.springframework.validation.annotation.Validated;
19 15 import org.springframework.web.multipart.MultipartFile;
20 16  
21   -import java.io.File;
22   -import java.io.IOException;
23   -import java.nio.file.*;
24   -import java.util.ArrayList;
25 17 import java.util.List;
26   -import java.util.concurrent.Executors;
27   -import java.util.concurrent.TimeUnit;
28 18  
29 19 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
30   -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
  20 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PLUGIN_INFO_DELETE_FAILED_RUNNING;
  21 +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PLUGIN_INFO_NOT_EXISTS;
31 22  
32 23 /**
33 24 * IoT 插件信息 Service 实现类
... ... @@ -41,18 +32,17 @@ public class PluginInfoServiceImpl implements PluginInfoService {
41 32  
42 33 @Resource
43 34 private PluginInfoMapper pluginInfoMapper;
  35 +
44 36 @Resource
45   - private SpringPluginManager pluginManager;
  37 + private PluginInstanceService pluginInstanceService;
46 38  
47   - @Value("${pf4j.pluginsDir}")
48   - private String pluginsDir;
  39 + @Resource
  40 + private SpringPluginManager pluginManager;
49 41  
50 42 @Override
51 43 public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
52   - // 插入
53 44 PluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, PluginInfoDO.class);
54 45 pluginInfoMapper.insert(pluginInfo);
55   - // 返回
56 46 return pluginInfo.getId();
57 47 }
58 48  
... ... @@ -67,41 +57,21 @@ public class PluginInfoServiceImpl implements PluginInfoService {
67 57  
68 58 @Override
69 59 public void deletePluginInfo(Long id) {
70   - // 校验存在
  60 + // 1.1 校验存在
71 61 PluginInfoDO pluginInfoDO = validatePluginInfoExists(id);
72   -
73   - // 停止插件
  62 + // 1.2 停止插件
74 63 if (IotPluginStatusEnum.RUNNING.getStatus().equals(pluginInfoDO.getStatus())) {
75 64 throw exception(PLUGIN_INFO_DELETE_FAILED_RUNNING);
76 65 }
77 66  
78   - // 卸载插件
79   - PluginWrapper plugin = pluginManager.getPlugin(pluginInfoDO.getPluginKey());
80   - if (plugin != null) {
81   - // 查询插件是否是启动状态
82   - if (plugin.getPluginState().equals(PluginState.STARTED)) {
83   - // 停止插件
84   - pluginManager.stopPlugin(plugin.getPluginId());
85   - }
86   - // 卸载插件
87   - pluginManager.unloadPlugin(plugin.getPluginId());
88   - }
  67 + // 2. 卸载插件
  68 + pluginInstanceService.stopAndUnloadPlugin(pluginInfoDO.getPluginKey());
89 69  
90   - // 删除
91   - pluginInfoMapper.deleteById(id);
92   - // 删除插件文件
93   - Executors.newSingleThreadExecutor().submit(() -> {
94   - try {
95   - TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕
96   - File file = new File(pluginsDir, pluginInfoDO.getFileName());
97   - if (file.exists() && !file.delete()) {
98   - log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName());
99   - }
100   - } catch (InterruptedException e) {
101   - log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName(), e);
102   - }
103   - });
  70 + // 3. 删除插件文件
  71 + pluginInstanceService.deletePluginFile(pluginInfoDO);
104 72  
  73 + // 4. 删除插件信息
  74 + pluginInfoMapper.deleteById(id);
105 75 }
106 76  
107 77 private PluginInfoDO validatePluginInfoExists(Long id) {
... ... @@ -127,99 +97,37 @@ public class PluginInfoServiceImpl implements PluginInfoService {
127 97 // 1. 校验插件信息是否存在
128 98 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
129 99  
130   - // 2. 获取插件标识
131   - String pluginKey = pluginInfoDo.getPluginKey();
132   -
133   - // 3. 停止并卸载旧的插件
134   - stopAndUnloadPlugin(pluginKey);
  100 + // 2. 停止并卸载旧的插件
  101 + pluginInstanceService.stopAndUnloadPlugin(pluginInfoDo.getPluginKey());
135 102  
136   - // 4. 上传新的插件文件
137   - String pluginKeyNew = uploadAndLoadNewPlugin(file);
  103 + // 3 上传新的插件文件,更新插件启用状态文件
  104 + String pluginKeyNew = pluginInstanceService.uploadAndLoadNewPlugin(file);
138 105  
139   - // 5. 更新插件启用状态文件
140   - updatePluginStatusFile(pluginKeyNew, false);
141   -
142   - // 6. 更新插件信息
  106 + // 4. 更新插件信息
143 107 updatePluginInfo(pluginInfoDo, pluginKeyNew, file);
144 108 }
145 109  
146   - // 停止并卸载旧的插件
147   - private void stopAndUnloadPlugin(String pluginKey) {
148   - PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
149   - if (plugin != null) {
150   - if (plugin.getPluginState().equals(PluginState.STARTED)) {
151   - pluginManager.stopPlugin(pluginKey); // 停止插件
152   - }
153   - pluginManager.unloadPlugin(pluginKey); // 卸载插件
154   - }
155   - }
156   -
157   - // 上传并加载新的插件文件
158   - private String uploadAndLoadNewPlugin(MultipartFile file) {
159   - Path pluginsPath = Paths.get(pluginsDir);
160   - try {
161   - if (!Files.exists(pluginsPath)) {
162   - Files.createDirectories(pluginsPath); // 创建插件目录
163   - }
164   - String filename = file.getOriginalFilename();
165   - if (filename != null) {
166   - Path jarPath = pluginsPath.resolve(filename);
167   - Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件
168   - return pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件
169   - } else {
170   - throw exception(PLUGIN_INSTALL_FAILED);
171   - }
172   - } catch (Exception e) {
173   - throw exception(PLUGIN_INSTALL_FAILED);
174   - }
175   - }
176   -
177   - // 更新插件状态文件
178   - private void updatePluginStatusFile(String pluginKeyNew, boolean isEnabled) {
179   - Path enabledFilePath = Paths.get(pluginsDir, "enabled.txt");
180   - Path disabledFilePath = Paths.get(pluginsDir, "disabled.txt");
181   - Path targetFilePath = isEnabled ? enabledFilePath : disabledFilePath;
182   - Path oppositeFilePath = isEnabled ? disabledFilePath : enabledFilePath;
183   -
184   - try {
185   - PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginKeyNew);
186   - if (pluginWrapper == null) {
187   - throw exception(PLUGIN_INSTALL_FAILED);
188   - }
189   - String pluginInfo = pluginKeyNew + "@" + pluginWrapper.getDescriptor().getVersion();
190   - List<String> targetLines = Files.exists(targetFilePath) ? Files.readAllLines(targetFilePath)
191   - : new ArrayList<>();
192   - List<String> oppositeLines = Files.exists(oppositeFilePath) ? Files.readAllLines(oppositeFilePath)
193   - : new ArrayList<>();
194   -
195   - if (!targetLines.contains(pluginInfo)) {
196   - targetLines.add(pluginInfo);
197   - Files.write(targetFilePath, targetLines, StandardOpenOption.CREATE,
198   - StandardOpenOption.TRUNCATE_EXISTING);
199   - }
200   -
201   - if (oppositeLines.contains(pluginInfo)) {
202   - oppositeLines.remove(pluginInfo);
203   - Files.write(oppositeFilePath, oppositeLines, StandardOpenOption.CREATE,
204   - StandardOpenOption.TRUNCATE_EXISTING);
205   - }
206   - } catch (IOException e) {
207   - throw exception(PLUGIN_INSTALL_FAILED);
208   - }
209   - }
210   -
211   - // 更新插件信息
  110 + /**
  111 + * 更新插件信息
  112 + *
  113 + * @param pluginInfoDo 插件信息
  114 + * @param pluginKeyNew 插件标识符
  115 + * @param file 文件
  116 + */
212 117 private void updatePluginInfo(PluginInfoDO pluginInfoDo, String pluginKeyNew, MultipartFile file) {
213   - pluginInfoDo.setPluginKey(pluginKeyNew);
214   - pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus());
215   - pluginInfoDo.setFileName(file.getOriginalFilename());
216   - pluginInfoDo.setScript("");
217   -
218   - PluginDescriptor pluginDescriptor = pluginManager.getPlugin(pluginKeyNew).getDescriptor();
219   - pluginInfoDo.setConfigSchema(pluginDescriptor.getPluginDescription());
220   - pluginInfoDo.setVersion(pluginDescriptor.getVersion());
221   - pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription());
222   - pluginInfoMapper.updateById(pluginInfoDo);
  118 + // 创建新的插件信息对象并链式设置属性
  119 + PluginInfoDO updatedPluginInfo = new PluginInfoDO()
  120 + .setId(pluginInfoDo.getId())
  121 + .setPluginKey(pluginKeyNew)
  122 + .setStatus(IotPluginStatusEnum.STOPPED.getStatus())
  123 + .setFileName(file.getOriginalFilename())
  124 + .setScript("")
  125 + .setConfigSchema(pluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription())
  126 + .setVersion(pluginManager.getPlugin(pluginKeyNew).getDescriptor().getVersion())
  127 + .setDescription(pluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription());
  128 +
  129 + // 执行更新
  130 + pluginInfoMapper.updateById(updatedPluginInfo);
223 131 }
224 132  
225 133 @Override
... ... @@ -227,44 +135,24 @@ public class PluginInfoServiceImpl implements PluginInfoService {
227 135 // 1. 校验插件信息是否存在
228 136 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
229 137  
230   - // 2. 校验插件状态是否有效
231   - if (!IotPluginStatusEnum.contains(status)) {
232   - throw exception(PLUGIN_STATUS_INVALID);
233   - }
234   -
235   - // 3. 获取插件标识和插件实例
236   - String pluginKey = pluginInfoDo.getPluginKey();
237   - PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
238   -
239   - // 4. 根据状态更新插件
240   - if (plugin != null) {
241   - // 4.1 如果目标状态是运行且插件未启动,则启动插件
242   - if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
243   - && plugin.getPluginState() != PluginState.STARTED) {
244   - pluginManager.startPlugin(pluginKey);
245   - updatePluginStatusFile(pluginKey, true); // 更新插件状态文件为启用
246   - }
247   - // 4.2 如果目标状态是停止且插件已启动,则停止插件
248   - else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
249   - && plugin.getPluginState() == PluginState.STARTED) {
250   - pluginManager.stopPlugin(pluginKey);
251   - updatePluginStatusFile(pluginKey, false); // 更新插件状态文件为禁用
252   - }
253   - } else {
254   - // 5. 插件不存在且状态为停止,抛出异常
255   - if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {
256   - throw exception(PLUGIN_STATUS_INVALID);
257   - }
258   - }
  138 + // 2. 更新插件状态
  139 + pluginInstanceService.updatePluginStatus(pluginInfoDo, status);
259 140  
260   - // 6. 更新数据库中的插件状态
261   - pluginInfoDo.setStatus(status);
262   - pluginInfoMapper.updateById(pluginInfoDo);
  141 + // 3. 更新数据库中的插件状态
  142 + PluginInfoDO updatedPluginInfo = new PluginInfoDO();
  143 + updatedPluginInfo.setId(id);
  144 + updatedPluginInfo.setStatus(status);
  145 + pluginInfoMapper.updateById(updatedPluginInfo);
263 146 }
264 147  
265 148 @Override
266 149 public List<PluginInfoDO> getPluginInfoList() {
267   - return pluginInfoMapper.selectList(null);
  150 + return pluginInfoMapper.selectList();
  151 + }
  152 +
  153 + @Override
  154 + public List<PluginInfoDO> getPluginInfoListByStatus(Integer status) {
  155 + return pluginInfoMapper.selectListByStatus(status);
268 156 }
269 157  
270 158 }
271 159 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/PluginInstanceService.java
1 1 package cn.iocoder.yudao.module.iot.service.plugin;
2 2  
  3 +import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
  4 +import org.springframework.web.multipart.MultipartFile;
  5 +
3 6 /**
4 7 * IoT 插件实例 Service 接口
5 8 *
... ... @@ -8,8 +11,38 @@ package cn.iocoder.yudao.module.iot.service.plugin;
8 11 public interface PluginInstanceService {
9 12  
10 13 /**
11   - * 更新IoT 插件实例
  14 + * 上报插件实例(心跳)
  15 + */
  16 + void reportPluginInstances();
  17 +
  18 + /**
  19 + * 停止并卸载插件
  20 + *
  21 + * @param pluginKey 插件标识符
  22 + */
  23 + void stopAndUnloadPlugin(String pluginKey);
  24 +
  25 + /**
  26 + * 删除插件文件
  27 + *
  28 + * @param pluginInfoDo 插件信息
  29 + */
  30 + void deletePluginFile(PluginInfoDO pluginInfoDo);
  31 +
  32 + /**
  33 + * 上传并加载新的插件文件
  34 + *
  35 + * @param file 插件文件
  36 + * @return 插件标识符
  37 + */
  38 + String uploadAndLoadNewPlugin(MultipartFile file);
  39 +
  40 + /**
  41 + * 更新插件状态
  42 + *
  43 + * @param pluginInfoDo 插件信息
  44 + * @param status 新状态
12 45 */
13   - void updatePluginInstances();
  46 + void updatePluginStatus(PluginInfoDO pluginInfoDo, Integer status);
14 47  
15 48 }
16 49 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/PluginInstanceServiceImpl.java
1 1 package cn.iocoder.yudao.module.iot.service.plugin;
2 2  
  3 +import cn.hutool.core.io.FileUtil;
3 4 import cn.hutool.core.net.NetUtil;
4 5 import cn.hutool.core.util.IdUtil;
5 6 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
6 7 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
  8 +import cn.iocoder.yudao.module.iot.dal.mysql.plugin.PluginInfoMapper;
7 9 import cn.iocoder.yudao.module.iot.dal.mysql.plugin.PluginInstanceMapper;
  10 +import cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants;
  11 +import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
8 12 import jakarta.annotation.Resource;
9 13 import lombok.extern.slf4j.Slf4j;
  14 +import org.pf4j.PluginState;
10 15 import org.pf4j.PluginWrapper;
11 16 import org.pf4j.spring.SpringPluginManager;
12 17 import org.springframework.beans.factory.annotation.Value;
13 18 import org.springframework.stereotype.Service;
14 19 import org.springframework.validation.annotation.Validated;
  20 +import org.springframework.web.multipart.MultipartFile;
15 21  
  22 +import java.io.File;
  23 +import java.io.IOException;
  24 +import java.nio.file.*;
16 25 import java.util.List;
  26 +import java.util.Map;
  27 +import java.util.concurrent.TimeUnit;
  28 +import java.util.function.Function;
  29 +import java.util.stream.Collectors;
  30 +
  31 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
17 32  
18 33 /**
19 34 * IoT 插件实例 Service 实现类
... ... @@ -25,76 +40,149 @@ import java.util.List;
25 40 @Slf4j
26 41 public class PluginInstanceServiceImpl implements PluginInstanceService {
27 42  
28   - /**
29   - * 主程序id
30   - */
  43 + // TODO @haohao:这个可以后续确认下,有没更合适的标识。例如说 mac 地址之类的
  44 + // 简化的 UUID + mac 地址 会不会好一些,一台机子有可能会部署多个插件;
  45 + // 那就 mac@uuid ?
31 46 public static final String MAIN_ID = IdUtil.fastSimpleUUID();
32 47  
33 48 @Resource
34   - private PluginInfoService pluginInfoService;
  49 + private PluginInfoMapper pluginInfoMapper;
35 50 @Resource
36 51 private PluginInstanceMapper pluginInstanceMapper;
37 52 @Resource
38 53 private SpringPluginManager pluginManager;
39 54  
  55 + @Value("${pf4j.pluginsDir}")
  56 + private String pluginsDir;
40 57 @Value("${server.port:48080}")
41 58 private int port;
42 59  
  60 + @Override
  61 + public void stopAndUnloadPlugin(String pluginKey) {
  62 + PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
  63 + // TODO @haohao:改成 if return 会更简洁一点;
  64 + if (plugin != null) {
  65 + if (plugin.getPluginState().equals(PluginState.STARTED)) {
  66 + pluginManager.stopPlugin(pluginKey); // 停止插件
  67 + log.info("已停止插件: {}", pluginKey);
  68 + }
  69 + pluginManager.unloadPlugin(pluginKey); // 卸载插件
  70 + log.info("已卸载插件: {}", pluginKey);
  71 + } else {
  72 + log.warn("插件不存在或已卸载: {}", pluginKey);
  73 + }
  74 + }
  75 +
  76 + @Override
  77 + public void deletePluginFile(PluginInfoDO pluginInfoDO) {
  78 + File file = new File(pluginsDir, pluginInfoDO.getFileName());
  79 + // TODO @haohao:改成 if return 会更简洁一点;
  80 + if (file.exists()) {
  81 + try {
  82 + TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕
  83 + if (!file.delete()) {
  84 + log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName());
  85 + }
  86 + } catch (InterruptedException e) {
  87 + log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName(), e);
  88 + }
  89 + }
  90 + }
  91 +
  92 + @Override
  93 + public String uploadAndLoadNewPlugin(MultipartFile file) {
  94 + String pluginKeyNew;
  95 + // TODO @haohao:多节点,是不是要上传 s3 之类的存储器;然后定时去加载
  96 + Path pluginsPath = Paths.get(pluginsDir);
  97 + try {
  98 + FileUtil.mkdir(pluginsPath.toFile()); // 创建插件目录
  99 + String filename = file.getOriginalFilename();
  100 + if (filename != null) {
  101 + Path jarPath = pluginsPath.resolve(filename);
  102 + Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件
  103 + pluginKeyNew = pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件
  104 + log.info("已加载插件: {}", pluginKeyNew);
  105 + } else {
  106 + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED);
  107 + }
  108 + } catch (IOException e) {
  109 + log.error("[uploadAndLoadNewPlugin][上传插件文件失败]", e);
  110 + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e);
  111 + } catch (Exception e) {
  112 + log.error("[uploadAndLoadNewPlugin][加载插件失败]", e);
  113 + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e);
  114 + }
  115 + return pluginKeyNew;
  116 + }
  117 +
  118 + @Override
  119 + public void updatePluginStatus(PluginInfoDO pluginInfoDo, Integer status) {
  120 + String pluginKey = pluginInfoDo.getPluginKey();
  121 + PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
  122 +
  123 + // TODO @haohao:改成 if return 会更简洁一点;
  124 + if (plugin != null) {
  125 + // 启动插件
  126 + if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
  127 + && plugin.getPluginState() != PluginState.STARTED) {
  128 + pluginManager.startPlugin(pluginKey);
  129 + log.info("已启动插件: {}", pluginKey);
  130 + }
  131 + // 停止插件
  132 + else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
  133 + && plugin.getPluginState() == PluginState.STARTED) {
  134 + pluginManager.stopPlugin(pluginKey);
  135 + log.info("已停止插件: {}", pluginKey);
  136 + }
  137 + } else {
  138 + // 插件不存在且状态为停止,抛出异常
  139 + if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {
  140 + throw exception(ErrorCodeConstants.PLUGIN_STATUS_INVALID);
  141 + }
  142 + }
  143 + }
43 144  
44 145 @Override
45   - public void updatePluginInstances() {
46   - // 1. 查询 pf4j 插件列表
  146 + public void reportPluginInstances() {
  147 + // 1.1 获取 pf4j 插件列表
47 148 List<PluginWrapper> plugins = pluginManager.getPlugins();
48 149  
49   - // 2. 查询插件信息列表
50   - List<PluginInfoDO> pluginInfos = pluginInfoService.getPluginInfoList();
  150 + // 1.2 获取插件信息列表并转换为 Map 以便快速查找
  151 + List<PluginInfoDO> pluginInfos = pluginInfoMapper.selectList();
  152 + Map<String, PluginInfoDO> pluginInfoMap = pluginInfos.stream()
  153 + .collect(Collectors.toMap(PluginInfoDO::getPluginKey, Function.identity()));
51 154  
52   - // 动态获取主程序的 IP 和端口
53   - String mainIp = getLocalIpAddress();
  155 + // 1.3 获取本机 IP 和 MAC 地址
  156 + String ip = NetUtil.getLocalhostStr();
  157 + String mac = NetUtil.getLocalMacAddress();
  158 + String mainId = MAIN_ID + "-" + mac;
54 159  
55   - // 3. 遍历插件列表,并保存为插件实例
  160 + // 2. 遍历插件列表,并保存为插件实例
56 161 for (PluginWrapper plugin : plugins) {
57 162 String pluginKey = plugin.getPluginId();
58   - PluginInfoDO pluginInfo = pluginInfos.stream()
59   - .filter(pluginInfoDO -> pluginInfoDO.getPluginKey().equals(pluginKey))
60   - .findFirst()
61   - .orElse(null);
62 163  
63   - // 4. 如果插件信息不存在,则跳过
  164 + // 2.1 查找插件信息
  165 + PluginInfoDO pluginInfo = pluginInfoMap.get(pluginKey);
64 166 if (pluginInfo == null) {
  167 + log.error("插件信息不存在,pluginKey = {}", pluginKey);
65 168 continue;
66 169 }
67 170  
68   - // 5. 查询插件实例
69   - PluginInstanceDO pluginInstance = pluginInstanceMapper.selectByMainIdAndPluginId(MAIN_ID, pluginInfo.getId());
70   -
71   - // 6. 如果插件实例不存在,则创建
  171 + // 2.2 情况一:如果插件实例不存在,则创建
  172 + PluginInstanceDO pluginInstance = pluginInstanceMapper.selectByMainIdAndPluginId(mainId,
  173 + pluginInfo.getId());
72 174 if (pluginInstance == null) {
73   - pluginInstance = new PluginInstanceDO();
74   - pluginInstance.setPluginId(pluginInfo.getId());
75   - pluginInstance.setMainId(MAIN_ID);
76   - pluginInstance.setIp(mainIp);
77   - pluginInstance.setPort(port);
78   - pluginInstance.setHeartbeatAt(System.currentTimeMillis());
  175 + // 4.4 如果插件实例不存在,则创建
  176 + pluginInstance = PluginInstanceDO.builder().pluginId(pluginInfo.getId()).mainId(MAIN_ID + "-" + mac)
  177 + .ip(ip).port(port).heartbeatAt(System.currentTimeMillis()).build();
79 178 pluginInstanceMapper.insert(pluginInstance);
80 179 } else {
81   - // 7. 如果插件实例存在,则更新
  180 + // 2.2 情况二:如果存在,则更新 heartbeatAt
  181 + // TODO @haohao:这里最好 new 去 update;避免并发更新(虽然目前没有)
82 182 pluginInstance.setHeartbeatAt(System.currentTimeMillis());
83 183 pluginInstanceMapper.updateById(pluginInstance);
84 184 }
85 185 }
86 186 }
87 187  
88   - private String getLocalIpAddress() {
89   - try {
90   - List<String> ipList = NetUtil.localIpv4s().stream()
91   - .filter(ip -> !ip.startsWith("0.0") && !ip.startsWith("127.") && !ip.startsWith("169.254") && !ip.startsWith("255.255.255.255"))
92   - .toList();
93   - return ipList.isEmpty() ? "127.0.0.1" : ipList.get(0);
94   - } catch (Exception e) {
95   - log.error("获取本地IP地址失败", e);
96   - return "127.0.0.1"; // 默认值
97   - }
98   - }
99   -
100 188 }
101 189 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
... ... @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
9 9 import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductMapper;
10 10 import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum;
11 11 import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
12   -import cn.iocoder.yudao.module.iot.service.tdengine.IotThingModelMessageService;
13 12 import com.baomidou.dynamic.datasource.annotation.DSTransactional;
14 13 import jakarta.annotation.Resource;
15 14 import org.springframework.context.annotation.Lazy;
... ... @@ -116,14 +115,15 @@ public class IotProductServiceImpl implements IotProductService {
116 115 public void updateProductStatus(Long id, Integer status) {
117 116 // 1. 校验存在
118 117 validateProductExists(id);
119   - // 2. 更新
120   - IotProductDO updateObj = IotProductDO.builder().id(id).status(status).build();
121   - // 3. 产品是发布状态
  118 +
  119 + // 2. 产品是发布状态
122 120 if (Objects.equals(status, IotProductStatusEnum.PUBLISHED.getStatus())) {
123   - // 3.1 创建产品超级表数据模型
  121 + // 创建产品超级表数据模型
124 122 devicePropertyDataService.defineDevicePropertyData(id);
125   -
126 123 }
  124 +
  125 + // 3. 更新
  126 + IotProductDO updateObj = IotProductDO.builder().id(id).status(status).build();
127 127 productMapper.updateById(updateObj);
128 128 }
129 129  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/tdengine/IotThingModelMessageServiceImpl.java
... ... @@ -7,20 +7,20 @@ import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
7 7 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceStatusUpdateReqVO;
8 8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
9 9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDataDO;
10   -import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.*;
  10 +import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.FieldParser;
  11 +import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.TdFieldDO;
  12 +import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.TdTableDO;
  13 +import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.ThingModelMessage;
11 14 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
12   -import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
13 15 import cn.iocoder.yudao.module.iot.dal.redis.deviceData.DeviceDataRedisDAO;
14 16 import cn.iocoder.yudao.module.iot.dal.tdengine.TdEngineDDLMapper;
15 17 import cn.iocoder.yudao.module.iot.dal.tdengine.TdEngineDMLMapper;
16   -import cn.iocoder.yudao.module.iot.dal.tdengine.TdThingModelMessageMapper;
17 18 import cn.iocoder.yudao.module.iot.enums.IotConstants;
18 19 import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum;
19 20 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
20 21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
21   -import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
22 22 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
23   -import cn.iocoder.yudao.module.iot.util.IotTdDatabaseUtils;
  23 +import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
24 24 import jakarta.annotation.Resource;
25 25 import lombok.extern.slf4j.Slf4j;
26 26 import org.springframework.beans.factory.annotation.Value;
... ... @@ -62,12 +62,8 @@ public class IotThingModelMessageServiceImpl implements IotThingModelMessageServ
62 62 private TdEngineDMLMapper tdEngineDMLMapper;
63 63  
64 64 @Resource
65   - private TdThingModelMessageMapper tdThingModelMessageMapper;
66   -
67   - @Resource
68 65 private DeviceDataRedisDAO deviceDataRedisDAO;
69   -
70   -
  66 +
71 67 // TODO @haohao:这个方法,可以考虑加下 1. 2. 3. 更有层次感
72 68 @Override
73 69 @TenantIgnore
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDeviceLogDataMapper.xml
... ... @@ -6,20 +6,20 @@
6 6  
7 7 <!-- 创建设备日志超级表 初始化只创建一次-->
8 8 <update id="createDeviceLogSTable">
9   - CREATE STABLE device_log(
10   - ts TIMESTAMP,
11   - id NCHAR(50),
12   - product_key NCHAR(50),
13   - type NCHAR(50),
14   - subType NCHAR(50),
15   - content NCHAR(1024),
16   - report_time TIMESTAMP
17   - )TAGS (
18   - device_key NCHAR(50)
19   - )
  9 + CREATE STABLE device_log (
  10 + ts TIMESTAMP,
  11 + id NCHAR(50),
  12 + product_key NCHAR(50),
  13 + type NCHAR(50),
  14 + <!-- TODO @super:下划线 sub_type -->
  15 + subType NCHAR(50),
  16 + content NCHAR(1024),
  17 + report_time TIMESTAMP
  18 + ) TAGS (
  19 + device_key NCHAR(50)
  20 + )
20 21 </update>
21 22  
22   -
23 23 <!-- 创建设备日志子表 讨论:TDengine 在子表不存在的情况下 可在数据插入时 自动建表 要不要去掉创建子表的逻辑 由第一次插入数据时自动创建-->
24 24 <update id="createDeviceLogTable">
25 25 CREATE TABLE device_log_${deviceKey} USING device_log TAGS('${deviceKey}')
... ... @@ -41,4 +41,38 @@
41 41 )
42 42 </insert>
43 43  
  44 + <select id="selectPage" resultType="cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceLogDO">
  45 + SELECT ts, id, device_key, product_key, type, subType, content, report_time
  46 + FROM device_log_${reqVO.deviceKey}
  47 + <where>
  48 + <if test="reqVO.type != null and reqVO.type != ''">
  49 + AND type = #{reqVO.type}
  50 + </if>
  51 + <if test="reqVO.subType != null and reqVO.subType != ''">
  52 + AND subType = #{reqVO.subType}
  53 + </if>
  54 + <if test="reqVO.createTime != null">
  55 + AND ts BETWEEN #{reqVO.createTime[0]} AND #{reqVO.createTime[1]}
  56 + </if>
  57 + </where>
  58 + ORDER BY ts DESC
  59 + LIMIT #{reqVO.pageSize} OFFSET #{reqVO.pageNo}
  60 + </select>
  61 +
  62 + <select id="selectCount" resultType="Long">
  63 + SELECT COUNT(*)
  64 + FROM device_log_${reqVO.deviceKey}
  65 + <where>
  66 + <if test="reqVO.type != null and reqVO.type != ''">
  67 + AND type = #{reqVO.type}
  68 + </if>
  69 + <if test="reqVO.subType != null and reqVO.subType != ''">
  70 + AND subType = #{reqVO.subType}
  71 + </if>
  72 + <if test="reqVO.createTime != null">
  73 + AND ts BETWEEN #{reqVO.createTime[0]} AND #{reqVO.createTime[1]}
  74 + </if>
  75 + </where>
  76 + </select>
  77 +
44 78 </mapper>
45 79 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/pom.xml
... ... @@ -2,11 +2,6 @@
2 2 <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 3 xmlns="http://maven.apache.org/POM/4.0.0"
4 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5   -<!-- <modelVersion>4.0.0</modelVersion>-->
6   -<!-- <groupId>cn.iocoder.boot</groupId>-->
7   -<!-- <artifactId>yudao-module-iot-plugin</artifactId>-->
8   -<!-- <version>0.0.1</version>-->
9   -<!-- <packaging>pom</packaging>-->
10 5 <parent>
11 6 <artifactId>yudao-module-iot</artifactId>
12 7 <groupId>cn.iocoder.boot</groupId>
... ... @@ -15,6 +10,7 @@
15 10 <modules>
16 11 <module>yudao-module-iot-demo-plugin</module>
17 12 <module>yudao-module-iot-http-plugin</module>
  13 + <module>yudao-module-iot-mqtt-plugin</module>
18 14 </modules>
19 15  
20 16 <modelVersion>4.0.0</modelVersion>
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/dependency-reduced-pom.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<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">
  3 + <parent>
  4 + <artifactId>yudao-module-iot-plugin</artifactId>
  5 + <groupId>cn.iocoder.boot</groupId>
  6 + <version>2.2.0-snapshot</version>
  7 + </parent>
  8 + <modelVersion>4.0.0</modelVersion>
  9 + <artifactId>yudao-module-iot-http-plugin</artifactId>
  10 + <name>${project.artifactId}</name>
  11 + <version>2.2.0-snapshot</version>
  12 + <description>物联网 插件模块 - http 插件</description>
  13 + <build>
  14 + <plugins>
  15 + <plugin>
  16 + <artifactId>maven-jar-plugin</artifactId>
  17 + <version>2.4</version>
  18 + <configuration>
  19 + <archive>
  20 + <manifestEntries>
  21 + <Plugin-Id>${plugin.id}</Plugin-Id>
  22 + <Plugin-Class>${plugin.class}</Plugin-Class>
  23 + <Plugin-Version>${plugin.version}</Plugin-Version>
  24 + <Plugin-Provider>${plugin.provider}</Plugin-Provider>
  25 + <Plugin-Description>${plugin.description}</Plugin-Description>
  26 + <Plugin-Dependencies>${plugin.dependencies}</Plugin-Dependencies>
  27 + </manifestEntries>
  28 + </archive>
  29 + </configuration>
  30 + </plugin>
  31 + <plugin>
  32 + <artifactId>maven-deploy-plugin</artifactId>
  33 + <configuration>
  34 + <skip>true</skip>
  35 + </configuration>
  36 + </plugin>
  37 + <plugin>
  38 + <artifactId>maven-shade-plugin</artifactId>
  39 + <version>3.4.1</version>
  40 + <executions>
  41 + <execution>
  42 + <phase>package</phase>
  43 + <goals>
  44 + <goal>shade</goal>
  45 + </goals>
  46 + <configuration>
  47 + <shadedArtifactAttached>true</shadedArtifactAttached>
  48 + <shadedClassifierName>shaded</shadedClassifierName>
  49 + <transformers>
  50 + <transformer>
  51 + <mainClass>cn.iocoder.yudao.module.iot.HttpPluginSpringbootApplication</mainClass>
  52 + </transformer>
  53 + </transformers>
  54 + </configuration>
  55 + </execution>
  56 + </executions>
  57 + </plugin>
  58 + </plugins>
  59 + </build>
  60 + <dependencies>
  61 + <dependency>
  62 + <groupId>org.pf4j</groupId>
  63 + <artifactId>pf4j-spring</artifactId>
  64 + <version>0.9.0</version>
  65 + <scope>provided</scope>
  66 + </dependency>
  67 + <dependency>
  68 + <groupId>org.projectlombok</groupId>
  69 + <artifactId>lombok</artifactId>
  70 + <version>1.18.34</version>
  71 + <scope>provided</scope>
  72 + </dependency>
  73 + </dependencies>
  74 + <properties>
  75 + <plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpVertxPlugin</plugin.class>
  76 + <plugin.version>0.0.1</plugin.version>
  77 + <plugin.id>http-plugin</plugin.id>
  78 + <plugin.description>http-plugin-0.0.1</plugin.description>
  79 + <plugin.provider>ahh</plugin.provider>
  80 + </properties>
  81 +</project>
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/plugin.properties
1 1 plugin.id=http-plugin
2   -plugin.class=cn.iocoder.yudao.module.iot.plugin.HttpPlugin
  2 +plugin.class=cn.iocoder.yudao.module.iot.plugin.HttpVertxPlugin
3 3 plugin.version=0.0.1
4 4 plugin.provider=ahh
5 5 plugin.dependencies=
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/pom.xml
... ... @@ -21,7 +21,7 @@
21 21 <properties>
22 22 <!-- 插件相关 -->
23 23 <plugin.id>http-plugin</plugin.id>
24   - <plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpPlugin</plugin.class>
  24 + <plugin.class>cn.iocoder.yudao.module.iot.plugin.HttpVertxPlugin</plugin.class>
25 25 <plugin.version>0.0.1</plugin.version>
26 26 <plugin.provider>ahh</plugin.provider>
27 27 <plugin.description>http-plugin-0.0.1</plugin.description>
... ... @@ -30,27 +30,6 @@
30 30  
31 31 <build>
32 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 33 <plugin>
55 34 <groupId>org.apache.maven.plugins</groupId>
56 35 <artifactId>maven-antrun-plugin</artifactId>
... ... @@ -118,6 +97,29 @@
118 97 <skip>true</skip>
119 98 </configuration>
120 99 </plugin>
  100 +
  101 + <!-- <plugin>-->
  102 + <!-- <groupId>org.apache.maven.plugins</groupId>-->
  103 + <!-- <artifactId>maven-shade-plugin</artifactId>-->
  104 + <!-- <version>3.4.1</version>-->
  105 + <!-- <executions>-->
  106 + <!-- <execution>-->
  107 + <!-- <phase>package</phase>-->
  108 + <!-- <goals>-->
  109 + <!-- <goal>shade</goal>-->
  110 + <!-- </goals>-->
  111 + <!-- <configuration>-->
  112 + <!-- <shadedArtifactAttached>true</shadedArtifactAttached>-->
  113 + <!-- <shadedClassifierName>shaded</shadedClassifierName>-->
  114 + <!-- <transformers>-->
  115 + <!-- <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">-->
  116 + <!-- <mainClass>cn.iocoder.yudao.module.iot.HttpPluginSpringbootApplication</mainClass>-->
  117 + <!-- </transformer>-->
  118 + <!-- </transformers>-->
  119 + <!-- </configuration>-->
  120 + <!-- </execution>-->
  121 + <!-- </executions>-->
  122 + <!-- </plugin>-->
121 123 </plugins>
122 124 </build>
123 125  
... ... @@ -145,10 +147,20 @@
145 147 <version>${lombok.version}</version>
146 148 <scope>provided</scope>
147 149 </dependency>
  150 + <!-- Vert.x 核心依赖 -->
  151 + <dependency>
  152 + <groupId>io.vertx</groupId>
  153 + <artifactId>vertx-core</artifactId>
  154 + </dependency>
  155 + <!-- Vert.x Web 模块 -->
  156 + <dependency>
  157 + <groupId>io.vertx</groupId>
  158 + <artifactId>vertx-web</artifactId>
  159 + </dependency>
  160 + <!-- MQTT -->
148 161 <dependency>
149   - <groupId>io.netty</groupId>
150   - <artifactId>netty-all</artifactId>
151   - <version>4.1.63.Final</version> <!-- 版本可根据需要调整 -->
  162 + <groupId>org.eclipse.paho</groupId>
  163 + <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
152 164 </dependency>
153 165 </dependencies>
154 166 </project>
155 167 \ 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 0 → 100644
  1 +package cn.iocoder.yudao.module.iot;
  2 +
  3 +import org.springframework.boot.SpringApplication;
  4 +import org.springframework.boot.autoconfigure.SpringBootApplication;
  5 +
  6 +@SpringBootApplication
  7 +public class HttpPluginSpringbootApplication {
  8 + public static void main(String[] args) {
  9 + SpringApplication.run(HttpPluginSpringbootApplication.class, args);
  10 + }
  11 +}
0 12 \ 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/controller/RpcController.java 0 → 100644
  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 +}
0 39 \ 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 0 → 100644
  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 +}
0 94 \ 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 0 → 100644
  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/HttpHandler.java renamed to yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/HttpVertxHandler.java
... ... @@ -3,63 +3,30 @@ package cn.iocoder.yudao.module.iot.plugin;
3 3 import cn.hutool.json.JSONObject;
4 4 import cn.hutool.json.JSONUtil;
5 5 import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
6   -import io.netty.buffer.Unpooled;
7   -import io.netty.channel.ChannelFutureListener;
8   -import io.netty.channel.ChannelHandlerContext;
9   -import io.netty.channel.SimpleChannelInboundHandler;
10   -import io.netty.handler.codec.http.*;
11   -import io.netty.util.CharsetUtil;
  6 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
  7 +import io.vertx.core.Handler;
  8 +import io.vertx.ext.web.RequestBody;
  9 +import io.vertx.ext.web.RoutingContext;
  10 +import lombok.extern.slf4j.Slf4j;
12 11  
13   -/**
14   - * 基于 Netty 的 HTTP 处理器,用于接收设备上报的数据并调用主程序的 DeviceDataApi 接口进行处理。
15   - *
16   - * 1. 请求格式:JSON 格式,地址为 POST /sys/{productKey}/{deviceName}/thing/event/property/post
17   - * 2. 返回结果:JSON 格式,包含统一的 code、data、id、message、method、version 字段
18   - */
19   -public class HttpHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
  12 +@Slf4j
  13 +public class HttpVertxHandler implements Handler<RoutingContext> {
20 14  
21 15 private final DeviceDataApi deviceDataApi;
22 16  
23   - public HttpHandler(DeviceDataApi deviceDataApi) {
  17 + public HttpVertxHandler(DeviceDataApi deviceDataApi) {
24 18 this.deviceDataApi = deviceDataApi;
25 19 }
26 20  
27 21 @Override
28   - protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
29   - // 期望的路径格式: /sys/{productKey}/{deviceName}/thing/event/property/post
30   - // 使用 "/" 拆分路径
31   - String uri = request.uri();
32   - String[] parts = uri.split("/");
  22 + public void handle(RoutingContext ctx) {
  23 + String productKey = ctx.pathParam("productKey");
  24 + String deviceName = ctx.pathParam("deviceName");
  25 + RequestBody requestBody = ctx.body();
33 26  
34   - /*
35   - 拆分结果示例:
36   - parts[0] = ""
37   - parts[1] = "sys"
38   - parts[2] = productKey
39   - parts[3] = deviceName
40   - parts[4] = "thing"
41   - parts[5] = "event"
42   - parts[6] = "property"
43   - parts[7] = "post"
44   - */
45   - boolean isCorrectPath = parts.length == 8
46   - && "sys".equals(parts[1])
47   - && "thing".equals(parts[4])
48   - && "event".equals(parts[5])
49   - && "property".equals(parts[6])
50   - && "post".equals(parts[7]);
51   - if (!isCorrectPath) {
52   - writeResponse(ctx, HttpResponseStatus.NOT_FOUND, "Not Found");
53   - return;
54   - }
55   - String productKey = parts[2];
56   - String deviceName = parts[3];
57   -
58   - // 从请求中获取原始数据,尝试解析请求数据为 JSON 对象
59   - String requestBody = request.content().toString(CharsetUtil.UTF_8);
60 27 JSONObject jsonData;
61 28 try {
62   - jsonData = JSONUtil.parseObj(requestBody);
  29 + jsonData = JSONUtil.parseObj(requestBody.asJsonObject());
63 30 } catch (Exception e) {
64 31 JSONObject res = createResponseJson(
65 32 400,
... ... @@ -67,16 +34,24 @@ public class HttpHandler extends SimpleChannelInboundHandler&lt;FullHttpRequest&gt; {
67 34 null,
68 35 "请求数据不是合法的 JSON 格式: " + e.getMessage(),
69 36 "thing.event.property.post",
70   - "1.0"
71   - );
72   - writeResponse(ctx, HttpResponseStatus.BAD_REQUEST, res.toString());
  37 + "1.0");
  38 + ctx.response()
  39 + .setStatusCode(400)
  40 + .putHeader("Content-Type", "application/json; charset=UTF-8")
  41 + .end(res.toString());
73 42 return;
74 43 }
  44 +
75 45 String id = jsonData.getStr("id", null);
76 46  
77 47 try {
78 48 // 调用主程序的接口保存数据
79   - deviceDataApi.saveDeviceData(productKey, deviceName, jsonData.toString());
  49 + DeviceDataCreateReqDTO createDTO = DeviceDataCreateReqDTO.builder()
  50 + .productKey(productKey)
  51 + .deviceName(deviceName)
  52 + .message(jsonData.toString())
  53 + .build();
  54 + deviceDataApi.saveDeviceData(createDTO);
80 55  
81 56 // 构造成功响应内容
82 57 JSONObject successRes = createResponseJson(
... ... @@ -85,9 +60,11 @@ public class HttpHandler extends SimpleChannelInboundHandler&lt;FullHttpRequest&gt; {
85 60 id,
86 61 "success",
87 62 "thing.event.property.post",
88   - "1.0"
89   - );
90   - writeResponse(ctx, HttpResponseStatus.OK, successRes.toString());
  63 + "1.0");
  64 + ctx.response()
  65 + .setStatusCode(200)
  66 + .putHeader("Content-Type", "application/json; charset=UTF-8")
  67 + .end(successRes.toString());
91 68 } catch (Exception e) {
92 69 JSONObject errorRes = createResponseJson(
93 70 500,
... ... @@ -95,9 +72,11 @@ public class HttpHandler extends SimpleChannelInboundHandler&lt;FullHttpRequest&gt; {
95 72 id,
96 73 "The format of result is error!",
97 74 "thing.event.property.post",
98   - "1.0"
99   - );
100   - writeResponse(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR, errorRes.toString());
  75 + "1.0");
  76 + ctx.response()
  77 + .setStatusCode(500)
  78 + .putHeader("Content-Type", "application/json; charset=UTF-8")
  79 + .end(errorRes.toString());
101 80 }
102 81 }
103 82  
... ... @@ -112,7 +91,8 @@ public class HttpHandler extends SimpleChannelInboundHandler&lt;FullHttpRequest&gt; {
112 91 * @param version 返回的版本号
113 92 * @return 构造好的 JSON 对象
114 93 */
115   - private JSONObject createResponseJson(int code, JSONObject data, String id, String message, String method, String version) {
  94 + private JSONObject createResponseJson(int code, JSONObject data, String id, String message, String method,
  95 + String version) {
116 96 JSONObject res = new JSONObject();
117 97 res.set("code", code);
118 98 res.set("data", data != null ? data : new JSONObject());
... ... @@ -122,26 +102,4 @@ public class HttpHandler extends SimpleChannelInboundHandler&lt;FullHttpRequest&gt; {
122 102 res.set("version", version);
123 103 return res;
124 104 }
125   -
126   - /**
127   - * 向客户端返回 HTTP 响应的辅助方法
128   - *
129   - * @param ctx 通道上下文
130   - * @param status HTTP 响应状态码(网络层面的)
131   - * @param content 响应内容(JSON 字符串或其他文本)
132   - */
133   - private void writeResponse(ChannelHandlerContext ctx, HttpResponseStatus status, String content) {
134   - // 设置响应头为 JSON 类型和正确的编码
135   - FullHttpResponse response = new DefaultFullHttpResponse(
136   - HttpVersion.HTTP_1_1,
137   - status,
138   - Unpooled.copiedBuffer(content, CharsetUtil.UTF_8)
139   - );
140   - response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8");
141   - response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
142   -
143   - // 发送响应并在发送完成后关闭连接
144   - ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
145   - }
146   -
147   -}
148 105 \ No newline at end of file
  106 +}
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/HttpVertxPlugin.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 io.vertx.core.Vertx;
  6 +import io.vertx.ext.web.Router;
  7 +import io.vertx.ext.web.handler.BodyHandler;
  8 +import org.pf4j.PluginWrapper;
  9 +import org.pf4j.spring.SpringPlugin;
  10 +import org.springframework.context.ApplicationContext;
  11 +import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  12 +
  13 +import lombok.extern.slf4j.Slf4j;
  14 +
  15 +@Slf4j
  16 +public class HttpVertxPlugin extends SpringPlugin {
  17 +
  18 + private static final int PORT = 8092;
  19 + private Vertx vertx;
  20 + private DeviceDataApi deviceDataApi;
  21 +
  22 + public HttpVertxPlugin(PluginWrapper wrapper) {
  23 + super(wrapper);
  24 + }
  25 +
  26 + @Override
  27 + public void start() {
  28 + log.info("HttpVertxPlugin.start()");
  29 +
  30 + // 获取 DeviceDataApi 实例
  31 + deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
  32 + if (deviceDataApi == null) {
  33 + log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
  34 + return;
  35 + }
  36 +
  37 + // 初始化 Vert.x
  38 + vertx = Vertx.vertx();
  39 + Router router = Router.router(vertx);
  40 +
  41 + // 处理 Body
  42 + router.route().handler(BodyHandler.create());
  43 +
  44 + // 设置路由
  45 + router.post("/sys/:productKey/:deviceName/thing/event/property/post")
  46 + .handler(new HttpVertxHandler(deviceDataApi));
  47 +
  48 + // 启动 HTTP 服务器
  49 + vertx.createHttpServer()
  50 + .requestHandler(router)
  51 + .listen(PORT, http -> {
  52 + if (http.succeeded()) {
  53 + log.info("HTTP 服务器启动成功,端口为: {}", PORT);
  54 + } else {
  55 + log.error("HTTP 服务器启动失败", http.cause());
  56 + }
  57 + });
  58 + }
  59 +
  60 + @Override
  61 + public void stop() {
  62 + log.info("HttpVertxPlugin.stop()");
  63 + if (vertx != null) {
  64 + vertx.close(ar -> {
  65 + if (ar.succeeded()) {
  66 + log.info("Vert.x 关闭成功");
  67 + } else {
  68 + log.error("Vert.x 关闭失败", ar.cause());
  69 + }
  70 + });
  71 + }
  72 + }
  73 +
  74 + @Override
  75 + protected ApplicationContext createApplicationContext() {
  76 + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
  77 + applicationContext.setClassLoader(getWrapper().getPluginClassLoader());
  78 + applicationContext.refresh();
  79 +
  80 + return applicationContext;
  81 + }
  82 +}
0 83 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-http-plugin/src/main/resources/application.yml 0 → 100644
  1 +server:
  2 + port: 8092
  3 +
  4 +spring:
  5 + application:
  6 + name: yudao-module-iot-http-plugin
  7 +
  8 +# MQTT-RPC 配置
  9 +mqtt:
  10 + broker: tcp://chaojiniu.top:1883
  11 + username: haohao
  12 + password: ahh@123456
  13 + clientId: mqtt-rpc-client-${random.int}
  14 + requestTopic: rpc/request
  15 + responseTopicPrefix: rpc/response/
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-plugin/plugin.properties 0 → 100644
  1 +plugin.id=mqtt-plugin
  2 +plugin.class=cn.iocoder.yudao.module.iot.plugin.MqttPlugin
  3 +plugin.version=0.0.1
  4 +plugin.provider=ahh
  5 +plugin.dependencies=
  6 +plugin.description=mqtt-plugin-0.0.1
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-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-mqtt-plugin</artifactId>
  15 +
  16 + <name>${project.artifactId}</name>
  17 + <description>
  18 + 物联网 插件模块 - mqtt 插件
  19 + </description>
  20 +
  21 + <properties>
  22 + <!-- 插件相关 -->
  23 + <plugin.id>mqtt-plugin</plugin.id>
  24 + <plugin.class>cn.iocoder.yudao.module.iot.plugin.MqttPlugin</plugin.class>
  25 + <plugin.version>0.0.1</plugin.version>
  26 + <plugin.provider>ahh</plugin.provider>
  27 + <plugin.description>mqtt-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 + <!-- MQTT -->
  149 + <dependency>
  150 + <groupId>org.eclipse.paho</groupId>
  151 + <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  152 + </dependency>
  153 + </dependencies>
  154 +</project>
0 155 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-plugin/yudao-module-iot-mqtt-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-http-plugin/src/main/java/cn/iocoder/yudao/module/iot/plugin/HttpPlugin.java renamed to 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.device.DeviceDataApi;
4 3 import cn.iocoder.yudao.module.iot.api.ServiceRegistry;
5   -import io.netty.bootstrap.ServerBootstrap;
6   -import io.netty.channel.*;
7   -import io.netty.channel.nio.NioEventLoopGroup;
8   -import io.netty.channel.socket.nio.NioServerSocketChannel;
9   -import io.netty.handler.codec.http.*;
  4 +import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
10 5 import lombok.extern.slf4j.Slf4j;
11   -import org.pf4j.PluginWrapper;
12 6 import org.pf4j.Plugin;
  7 +import org.pf4j.PluginWrapper;
13 8  
  9 +import javax.annotation.Resource;
14 10 import java.util.concurrent.ExecutorService;
15 11 import java.util.concurrent.Executors;
16 12  
17 13 @Slf4j
18   -public class HttpPlugin extends Plugin {
19   -
20   - private static final int PORT = 8092;
  14 +public class MqttPlugin extends Plugin {
21 15  
22 16 private ExecutorService executorService;
  17 + @Resource
23 18 private DeviceDataApi deviceDataApi;
24 19  
25   - public HttpPlugin(PluginWrapper wrapper) {
  20 + public MqttPlugin(PluginWrapper wrapper) {
26 21 super(wrapper);
27   - // 初始化线程池
28 22 this.executorService = Executors.newSingleThreadExecutor();
29 23 }
30 24  
31 25 @Override
32 26 public void start() {
33   - log.info("HttpPlugin.start()");
  27 + log.info("MqttPlugin.start()");
34 28  
35   - // 重新初始化线程池,确保它是活跃的
36 29 if (executorService.isShutdown() || executorService.isTerminated()) {
37 30 executorService = Executors.newSingleThreadExecutor();
38 31 }
39 32  
40   - // 从 ServiceRegistry 中获取主程序暴露的 DeviceDataApi 接口实例
41 33 deviceDataApi = ServiceRegistry.getService(DeviceDataApi.class);
42 34 if (deviceDataApi == null) {
43 35 log.error("未能从 ServiceRegistry 获取 DeviceDataApi 实例,请确保主程序已正确注册!");
44 36 return;
45 37 }
46 38  
47   - // 异步启动 Netty 服务器
48   - executorService.submit(this::startHttpServer);
49 39 }
50 40  
51 41 @Override
52 42 public void stop() {
53   - log.info("HttpPlugin.stop()");
54   - // 停止线程池
55   - executorService.shutdownNow();
  43 + log.info("MqttPlugin.stop()");
56 44 }
57   -
58   - /**
59   - * 启动 HTTP 服务
60   - */
61   - private void startHttpServer() {
62   - EventLoopGroup bossGroup = new NioEventLoopGroup(1);
63   - EventLoopGroup workerGroup = new NioEventLoopGroup();
64   -
65   - try {
66   - ServerBootstrap bootstrap = new ServerBootstrap();
67   - bootstrap.group(bossGroup, workerGroup)
68   - .channel(NioServerSocketChannel.class)
69   - .childHandler(new ChannelInitializer<>() {
70   -
71   - @Override
72   - protected void initChannel(Channel channel) {
73   - channel.pipeline().addLast(new HttpServerCodec());
74   - channel.pipeline().addLast(new HttpObjectAggregator(65536));
75   - // 将从 ServiceRegistry 获取的 deviceDataApi 传入处理器
76   - channel.pipeline().addLast(new HttpHandler(deviceDataApi));
77   - }
78   -
79   - });
80   -
81   - // 绑定端口并启动服务器
82   - ChannelFuture future = bootstrap.bind(PORT).sync();
83   - log.info("HTTP 服务器启动成功,端口为: {}", PORT);
84   - future.channel().closeFuture().sync();
85   - } catch (InterruptedException e) {
86   - Thread.currentThread().interrupt();
87   - log.warn("HTTP 服务启动被中断", e);
88   - } finally {
89   - bossGroup.shutdownGracefully();
90   - workerGroup.shutdownGracefully();
91   - }
92   - }
93   -
94 45 }
95 46 \ No newline at end of file
... ...
yudao-server/src/main/resources/application-dev.yaml
... ... @@ -212,4 +212,9 @@ iot:
212 212 # 保持连接
213 213 keepalive: 60
214 214 # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)
215   - clearSession: true
216 215 \ No newline at end of file
  216 + clearSession: true
  217 +
  218 +
  219 +# 插件配置
  220 +pf4j:
  221 + pluginsDir: ${user.home}/plugins # 插件目录
217 222 \ No newline at end of file
... ...
yudao-server/src/main/resources/application-local.yaml
... ... @@ -183,10 +183,8 @@ logging:
183 183 cn.iocoder.yudao.module.crm.dal.mysql: debug
184 184 cn.iocoder.yudao.module.erp.dal.mysql: debug
185 185 cn.iocoder.yudao.module.iot.dal.mysql: debug
186   - cn.iocoder.yudao.module.iot.dal.tdengine: DEBUG
187 186 cn.iocoder.yudao.module.ai.dal.mysql: debug
188 187 org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
189   - com.taosdata: DEBUG # TDengine 的日志级别
190 188  
191 189 debug: false
192 190  
... ... @@ -283,3 +281,16 @@ iot:
283 281 # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)
284 282 clearSession: true
285 283  
  284 +# MQTT-RPC 配置
  285 +mqtt:
  286 + broker: tcp://127.0.0.1:1883
  287 + username: root
  288 + password: 123456
  289 + clientId: mqtt-rpc-server-${random.int}
  290 + requestTopic: rpc/request
  291 + responseTopicPrefix: rpc/response/
  292 +
  293 +
  294 +# 插件配置
  295 +pf4j:
  296 + pluginsDir: /Users/anhaohao/code/gitee/ruoyi-vue-pro/plugins # 插件目录
286 297 \ No newline at end of file
... ...