Commit f1d887d0e0755c09372e19ed524719357115be9f
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
plugins/disabled.txt deleted
plugins/enabled.txt deleted
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
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<PluginInfoDO> { |
| 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<PluginInstanceDO> { |
| 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
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/client/EmqxClient.java
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/config/MqttConfig.java
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
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
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<FullHttpRequest> { |
| 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<FullHttpRequest> { |
| 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<FullHttpRequest> { |
| 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<FullHttpRequest> { |
| 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<FullHttpRequest> { |
| 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
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 | ... | ... |