Commit cde6ebf92189969ace8739724bc6b9ea925d9e1d

Authored by 安浩浩
1 parent b5856c4c

【功能完善】IoT: 更新设备数据 API,重构保存设备数据方法以使用 DTO,新增参数校验依赖,优化插件管理功能,添加插件实例上报和状态更新接口,同时更新插件信息获取逻辑,删除不再使用的文件和配置。

Showing 21 changed files with 360 additions and 243 deletions
.gitignore
@@ -51,3 +51,4 @@ rebel.xml @@ -51,3 +51,4 @@ rebel.xml
51 application-my.yaml 51 application-my.yaml
52 52
53 /yudao-ui-app/unpackage/ 53 /yudao-ui-app/unpackage/
  54 +**/.DS_Store
.vscode/settings.json 0 → 100644
  1 +{
  2 + "java.compile.nullAnalysis.mode": "automatic",
  3 + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable",
  4 + "java.configuration.updateBuildConfiguration": "interactive"
  5 +}
0 \ No newline at end of file 6 \ No newline at end of file
plugins/disabled.txt deleted
plugins/enabled.txt deleted
1 -http-plugin  
plugins/yudao-module-iot-http-plugin-2.2.0-snapshot.jar
No preview for this file type
yudao-module-iot/yudao-module-iot-api/pom.xml
@@ -33,6 +33,13 @@ @@ -33,6 +33,13 @@
33 </exclusion> 33 </exclusion>
34 </exclusions> 34 </exclusions>
35 </dependency> 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 </dependencies> 43 </dependencies>
37 44
38 </project> 45 </project>
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/DeviceDataApi.java
1 package cn.iocoder.yudao.module.iot.api.device; 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 * 设备数据 API 7 * 设备数据 API
5 * 8 *
@@ -7,14 +10,11 @@ package cn.iocoder.yudao.module.iot.api.device; @@ -7,14 +10,11 @@ package cn.iocoder.yudao.module.iot.api.device;
7 */ 10 */
8 public interface DeviceDataApi { 11 public interface DeviceDataApi {
9 12
10 - // TODO @haohao:最好搞成 dto 哈!  
11 /** 13 /**
12 * 保存设备数据 14 * 保存设备数据
13 * 15 *
14 - * @param productKey 产品 key  
15 - * @param deviceName 设备名称  
16 - * @param message 消息 16 + * @param createDTO 设备数据
17 */ 17 */
18 - void saveDeviceData(String productKey, String deviceName, String message); 18 + void saveDeviceData(@Valid DeviceDataCreateReqDTO createDTO);
19 19
20 } 20 }
21 \ No newline at end of file 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 \ No newline at end of file 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,8 +13,8 @@ import java.util.Arrays;
13 @Getter 13 @Getter
14 public enum IotPluginDeployTypeEnum implements IntArrayValuable { 14 public enum IotPluginDeployTypeEnum implements IntArrayValuable {
15 15
16 - UPLOAD(0, "上传 jar"), // TODO @haohao:UPLOAD 和 ALONE 感觉有点冲突,前者是部署方式,后者是运行方式。这个后续再讨论下哈  
17 - ALONE(1, "独立运行"); 16 + DEPLOY_VIA_JAR(0, "通过 jar 部署"), // TODO @haohao:UPLOAD 和 ALONE 感觉有点冲突,前者是部署方式,后者是运行方式。这个后续再讨论下哈
  17 + DEPLOY_STANDALONE(1, "独立部署");
18 18
19 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotPluginDeployTypeEnum::getDeployType).toArray(); 19 public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotPluginDeployTypeEnum::getDeployType).toArray();
20 20
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/DeviceDataApiImpl.java
1 package cn.iocoder.yudao.module.iot.api.device; 1 package cn.iocoder.yudao.module.iot.api.device;
2 2
  3 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
3 import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService; 4 import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
4 import org.springframework.stereotype.Service; 5 import org.springframework.stereotype.Service;
5 import org.springframework.validation.annotation.Validated; 6 import org.springframework.validation.annotation.Validated;
@@ -17,8 +18,8 @@ public class DeviceDataApiImpl implements DeviceDataApi { @@ -17,8 +18,8 @@ public class DeviceDataApiImpl implements DeviceDataApi {
17 private IotDevicePropertyDataService deviceDataService; 18 private IotDevicePropertyDataService deviceDataService;
18 19
19 @Override 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 \ No newline at end of file 26 \ 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 package cn.iocoder.yudao.module.iot.controller.admin.plugin.vo; 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 import io.swagger.v3.oas.annotations.media.Schema; 5 import io.swagger.v3.oas.annotations.media.Schema;
4 -import lombok.*; 6 +import lombok.Data;
5 7
6 @Schema(description = "管理后台 - IoT 插件信息新增/修改 Request VO") 8 @Schema(description = "管理后台 - IoT 插件信息新增/修改 Request VO")
7 @Data 9 @Data
@@ -39,6 +41,7 @@ public class PluginInfoSaveReqVO { @@ -39,6 +41,7 @@ public class PluginInfoSaveReqVO {
39 private String protocol; 41 private String protocol;
40 42
41 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) 43 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED)
  44 + @InEnum(IotPluginStatusEnum.class)
42 private Integer status; 45 private Integer status;
43 46
44 @Schema(description = "插件配置项描述信息") 47 @Schema(description = "插件配置项描述信息")
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/emq/service/EmqxServiceImpl.java
1 package cn.iocoder.yudao.module.iot.emq.service; 1 package cn.iocoder.yudao.module.iot.emq.service;
2 2
  3 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
3 import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService; 4 import cn.iocoder.yudao.module.iot.service.device.IotDevicePropertyDataService;
4 import jakarta.annotation.Resource; 5 import jakarta.annotation.Resource;
5 import lombok.extern.slf4j.Slf4j; 6 import lombok.extern.slf4j.Slf4j;
6 import org.eclipse.paho.client.mqttv3.MqttClient; 7 import org.eclipse.paho.client.mqttv3.MqttClient;
7 import org.eclipse.paho.client.mqttv3.MqttMessage; 8 import org.eclipse.paho.client.mqttv3.MqttMessage;
8 import org.springframework.scheduling.annotation.Async; 9 import org.springframework.scheduling.annotation.Async;
9 -import org.springframework.stereotype.Service;  
10 10
11 // TODO @芋艿:在瞅瞅 11 // TODO @芋艿:在瞅瞅
12 12
@@ -16,7 +16,7 @@ import org.springframework.stereotype.Service; @@ -16,7 +16,7 @@ import org.springframework.stereotype.Service;
16 * @author ahh 16 * @author ahh
17 */ 17 */
18 @Slf4j 18 @Slf4j
19 -//@Service 19 +// @Service
20 public class EmqxServiceImpl implements EmqxService { 20 public class EmqxServiceImpl implements EmqxService {
21 21
22 @Resource 22 @Resource
@@ -34,7 +34,12 @@ public class EmqxServiceImpl implements EmqxService { @@ -34,7 +34,12 @@ public class EmqxServiceImpl implements EmqxService {
34 String productKey = topic.split("/")[2]; 34 String productKey = topic.split("/")[2];
35 String deviceName = topic.split("/")[3]; 35 String deviceName = topic.split("/")[3];
36 String message = new String(mqttMessage.getPayload()); 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/job/plugin/PluginInstancesJob.java
@@ -22,7 +22,7 @@ public class PluginInstancesJob { @@ -22,7 +22,7 @@ public class PluginInstancesJob {
22 @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) 22 @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
23 public void updatePluginInstances() { 23 public void updatePluginInstances() {
24 TenantUtils.executeIgnore(() -> { 24 TenantUtils.executeIgnore(() -> {
25 - pluginInstanceService.updatePluginInstances(); 25 + pluginInstanceService.reportPluginInstances();
26 }); 26 });
27 } 27 }
28 28
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDevicePropertyDataService.java
1 package cn.iocoder.yudao.module.iot.service.device; 1 package cn.iocoder.yudao.module.iot.service.device;
2 2
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 3 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
4 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataPageReqVO; 5 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataPageReqVO;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDataDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDataDO;
6 import jakarta.validation.Valid; 7 import jakarta.validation.Valid;
@@ -25,12 +26,9 @@ public interface IotDevicePropertyDataService { @@ -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 +6,7 @@ import cn.hutool.core.map.MapUtil;
6 import cn.hutool.core.util.StrUtil; 6 import cn.hutool.core.util.StrUtil;
7 import cn.hutool.json.JSONObject; 7 import cn.hutool.json.JSONObject;
8 import cn.iocoder.yudao.framework.common.pojo.PageResult; 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  9 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
9 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataPageReqVO; 10 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.deviceData.IotDeviceDataPageReqVO;
10 import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; 11 import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
11 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
@@ -14,8 +15,8 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; @@ -14,8 +15,8 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
14 import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.SelectVisualDO; 15 import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.SelectVisualDO;
15 import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.ThingModelMessage; 16 import cn.iocoder.yudao.module.iot.dal.dataobject.tdengine.ThingModelMessage;
16 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; 17 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
17 -import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyDataMapper;  
18 import cn.iocoder.yudao.module.iot.dal.redis.deviceData.DeviceDataRedisDAO; 18 import cn.iocoder.yudao.module.iot.dal.redis.deviceData.DeviceDataRedisDAO;
  19 +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyDataMapper;
19 import cn.iocoder.yudao.module.iot.dal.tdengine.TdEngineDMLMapper; 20 import cn.iocoder.yudao.module.iot.dal.tdengine.TdEngineDMLMapper;
20 import cn.iocoder.yudao.module.iot.enums.IotConstants; 21 import cn.iocoder.yudao.module.iot.enums.IotConstants;
21 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; 22 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
@@ -56,7 +57,7 @@ public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataSe @@ -56,7 +57,7 @@ public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataSe
56 .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT) 57 .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT)
57 .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE) 58 .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE)
58 .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明? 59 .put(IotDataSpecsDataTypeEnum.ENUM.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明?
59 - .put( IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明? 60 + .put(IotDataSpecsDataTypeEnum.BOOL.getDataType(), TDengineTableField.TYPE_TINYINT) // TODO 芋艿:为什么要映射为 TINYINT 的说明?
60 .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_NCHAR) 61 .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_NCHAR)
61 .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP) 62 .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP)
62 .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!! 63 .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!!
@@ -128,20 +129,20 @@ public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataSe @@ -128,20 +129,20 @@ public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataSe
128 } 129 }
129 130
130 @Override 131 @Override
131 - public void saveDeviceData(String productKey, String deviceName, String message) { 132 + public void saveDeviceData(DeviceDataCreateReqDTO createDTO) {
132 // 1. 根据产品 key 和设备名称,获得设备信息 133 // 1. 根据产品 key 和设备名称,获得设备信息
133 - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceName(productKey, deviceName); 134 + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceName(createDTO.getProductKey(), createDTO.getDeviceName());
134 // 2. 解析消息,保存数据 135 // 2. 解析消息,保存数据
135 - JSONObject jsonObject = new JSONObject(message);  
136 - log.info("[saveDeviceData][productKey({}) deviceName({}) data({})]", productKey, deviceName, jsonObject); 136 + JSONObject jsonObject = new JSONObject(createDTO.getMessage());
  137 + log.info("[saveDeviceData][productKey({}) deviceName({}) data({})]", createDTO.getProductKey(), createDTO.getDeviceName(), jsonObject);
137 ThingModelMessage thingModelMessage = ThingModelMessage.builder() 138 ThingModelMessage thingModelMessage = ThingModelMessage.builder()
138 .id(jsonObject.getStr("id")) 139 .id(jsonObject.getStr("id"))
139 .sys(jsonObject.get("sys")) 140 .sys(jsonObject.get("sys"))
140 .method(jsonObject.getStr("method")) 141 .method(jsonObject.getStr("method"))
141 .params(jsonObject.get("params")) 142 .params(jsonObject.get("params"))
142 .time(jsonObject.getLong("time") == null ? System.currentTimeMillis() : jsonObject.getLong("time")) 143 .time(jsonObject.getLong("time") == null ? System.currentTimeMillis() : jsonObject.getLong("time"))
143 - .productKey(productKey)  
144 - .deviceName(deviceName) 144 + .productKey(createDTO.getProductKey())
  145 + .deviceName(createDTO.getDeviceName())
145 .deviceKey(device.getDeviceKey()) 146 .deviceKey(device.getDeviceKey())
146 .build(); 147 .build();
147 thingModelMessageService.saveThingModelMessage(device, thingModelMessage); 148 thingModelMessageService.saveThingModelMessage(device, thingModelMessage);
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/PluginInfoService.java
@@ -78,9 +78,10 @@ public interface PluginInfoService { @@ -78,9 +78,10 @@ public interface PluginInfoService {
78 List<PluginInfoDO> getPluginInfoList(); 78 List<PluginInfoDO> getPluginInfoList();
79 79
80 /** 80 /**
81 - * 获得运行状态的插件信息列表 81 + * 根据状态获得插件信息列表
82 * 82 *
83 - * @return 运行状态的插件信息列表 83 + * @param status 状态
  84 + * @return 插件信息列表
84 */ 85 */
85 - List<PluginInfoDO> getRunningPluginInfoList(); 86 + List<PluginInfoDO> getPluginInfoListByStatus(Integer status);
86 } 87 }
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,25 +9,16 @@ import cn.iocoder.yudao.module.iot.dal.mysql.plugin.PluginInfoMapper;
9 import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; 9 import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
10 import jakarta.annotation.Resource; 10 import jakarta.annotation.Resource;
11 import lombok.extern.slf4j.Slf4j; 11 import lombok.extern.slf4j.Slf4j;
12 -import org.pf4j.PluginDescriptor;  
13 -import org.pf4j.PluginState;  
14 -import org.pf4j.PluginWrapper;  
15 import org.pf4j.spring.SpringPluginManager; 12 import org.pf4j.spring.SpringPluginManager;
16 -import org.springframework.beans.factory.annotation.Value;  
17 import org.springframework.stereotype.Service; 13 import org.springframework.stereotype.Service;
18 import org.springframework.validation.annotation.Validated; 14 import org.springframework.validation.annotation.Validated;
19 import org.springframework.web.multipart.MultipartFile; 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 import java.util.List; 17 import java.util.List;
26 -import java.util.concurrent.Executors;  
27 -import java.util.concurrent.TimeUnit;  
28 18
29 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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 * IoT 插件信息 Service 实现类 24 * IoT 插件信息 Service 实现类
@@ -43,11 +34,10 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -43,11 +34,10 @@ public class PluginInfoServiceImpl implements PluginInfoService {
43 private PluginInfoMapper pluginInfoMapper; 34 private PluginInfoMapper pluginInfoMapper;
44 35
45 @Resource 36 @Resource
46 - private SpringPluginManager pluginManager; 37 + private PluginInstanceService pluginInstanceService;
47 38
48 - // TODO @芋艿:要不要换位置  
49 - @Value("${pf4j.pluginsDir}")  
50 - private String pluginsDir; 39 + @Resource
  40 + private SpringPluginManager pluginManager;
51 41
52 @Override 42 @Override
53 public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) { 43 public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
@@ -75,32 +65,13 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -75,32 +65,13 @@ public class PluginInfoServiceImpl implements PluginInfoService {
75 } 65 }
76 66
77 // 2. 卸载插件 67 // 2. 卸载插件
78 - // TODO @haohao:可以复用 stopAndUnloadPlugin  
79 - PluginWrapper plugin = pluginManager.getPlugin(pluginInfoDO.getPluginKey());  
80 - if (plugin != null) {  
81 - // 停止插件  
82 - if (plugin.getPluginState().equals(PluginState.STARTED)) {  
83 - pluginManager.stopPlugin(plugin.getPluginId());  
84 - }  
85 - // 卸载插件  
86 - pluginManager.unloadPlugin(plugin.getPluginId());  
87 - } 68 + pluginInstanceService.stopAndUnloadPlugin(pluginInfoDO.getPluginKey());
  69 +
  70 + // 3. 删除插件文件
  71 + pluginInstanceService.deletePluginFile(pluginInfoDO);
88 72
89 - // 3.1 删除 73 + // 4. 删除插件信息
90 pluginInfoMapper.deleteById(id); 74 pluginInfoMapper.deleteById(id);
91 - // 3.2 删除插件文件  
92 - // TODO @haohao:这个直接主线程 sleep 就好了,不用单独开线程池哈。原因是,低频操作;另外,只有存在的时候,才 sleep + 删除;  
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 - });  
104 } 75 }
105 76
106 private PluginInfoDO validatePluginInfoExists(Long id) { 77 private PluginInfoDO validatePluginInfoExists(Long id) {
@@ -127,144 +98,52 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -127,144 +98,52 @@ public class PluginInfoServiceImpl implements PluginInfoService {
127 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id); 98 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
128 99
129 // 2. 停止并卸载旧的插件 100 // 2. 停止并卸载旧的插件
130 - stopAndUnloadPlugin(pluginInfoDo.getPluginKey()); 101 + pluginInstanceService.stopAndUnloadPlugin(pluginInfoDo.getPluginKey());
131 102
132 - // 3.1 上传新的插件文件  
133 - String pluginKeyNew = uploadAndLoadNewPlugin(file);  
134 - // 3.2 更新插件启用状态文件  
135 - updatePluginStatusFile(pluginKeyNew, false); 103 + // 3 上传新的插件文件,更新插件启用状态文件
  104 + String pluginKeyNew = pluginInstanceService.uploadAndLoadNewPlugin(file);
  105 + pluginInstanceService.updatePluginStatusFile(pluginKeyNew, false);
136 106
137 // 4. 更新插件信息 107 // 4. 更新插件信息
138 updatePluginInfo(pluginInfoDo, pluginKeyNew, file); 108 updatePluginInfo(pluginInfoDo, pluginKeyNew, file);
139 } 109 }
140 110
141 - // TODO @haohao:注释的格式  
142 - // 停止并卸载旧的插件  
143 - private void stopAndUnloadPlugin(String pluginKey) {  
144 - PluginWrapper plugin = pluginManager.getPlugin(pluginKey);  
145 - if (plugin != null) {  
146 - if (plugin.getPluginState().equals(PluginState.STARTED)) {  
147 - pluginManager.stopPlugin(pluginKey); // 停止插件  
148 - }  
149 - pluginManager.unloadPlugin(pluginKey); // 卸载插件  
150 - }  
151 - }  
152 -  
153 - // TODO @haohao:注释的格式  
154 - // 上传并加载新的插件文件  
155 - private String uploadAndLoadNewPlugin(MultipartFile file) {  
156 - // TODO @haohao:多节点,是不是要上传 s3 之类的存储器;然后定时去加载  
157 - Path pluginsPath = Paths.get(pluginsDir);  
158 - try {  
159 - // TODO @haohao:可以使用 FileUtil 简化?  
160 - if (!Files.exists(pluginsPath)) {  
161 - Files.createDirectories(pluginsPath); // 创建插件目录  
162 - }  
163 - String filename = file.getOriginalFilename();  
164 - if (filename != null) {  
165 - Path jarPath = pluginsPath.resolve(filename);  
166 - Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件  
167 - return pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件  
168 - }  
169 - throw exception(PLUGIN_INSTALL_FAILED); // TODO @haohao:这么抛的话,貌似会被 catch (Exception e) {  
170 - } catch (Exception e) {  
171 - // TODO @haohao:打个 error log,方便排查  
172 - throw exception(PLUGIN_INSTALL_FAILED);  
173 - }  
174 - }  
175 -  
176 - // TODO @haohao:注释的格式  
177 - // 更新插件状态文件  
178 - private void updatePluginStatusFile(String pluginKeyNew, boolean isEnabled) {  
179 - // TODO @haohao:疑问,这里写 enabled.txt 和 disabled.txt 的目的是啥哈?  
180 - Path enabledFilePath = Paths.get(pluginsDir, "enabled.txt");  
181 - Path disabledFilePath = Paths.get(pluginsDir, "disabled.txt");  
182 - Path targetFilePath = isEnabled ? enabledFilePath : disabledFilePath;  
183 - Path oppositeFilePath = isEnabled ? disabledFilePath : enabledFilePath;  
184 -  
185 - try {  
186 - PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginKeyNew);  
187 - if (pluginWrapper == null) {  
188 - throw exception(PLUGIN_INSTALL_FAILED);  
189 - }  
190 - List<String> targetLines = Files.exists(targetFilePath) ? Files.readAllLines(targetFilePath) : new ArrayList<>();  
191 - List<String> oppositeLines = Files.exists(oppositeFilePath) ? Files.readAllLines(oppositeFilePath) : new ArrayList<>();  
192 -  
193 - if (!targetLines.contains(pluginKeyNew)) {  
194 - targetLines.add(pluginKeyNew);  
195 - Files.write(targetFilePath, targetLines, StandardOpenOption.CREATE,  
196 - StandardOpenOption.TRUNCATE_EXISTING);  
197 - }  
198 -  
199 - if (oppositeLines.contains(pluginKeyNew)) {  
200 - oppositeLines.remove(pluginKeyNew);  
201 - Files.write(oppositeFilePath, oppositeLines, StandardOpenOption.CREATE,  
202 - StandardOpenOption.TRUNCATE_EXISTING);  
203 - }  
204 - } catch (IOException e) {  
205 - throw exception(PLUGIN_INSTALL_FAILED);  
206 - }  
207 - }  
208 -  
209 - // TODO @haohao:注释的格式  
210 - // 更新插件信息 111 + /**
  112 + * 更新插件信息
  113 + *
  114 + * @param pluginInfoDo 插件信息
  115 + * @param pluginKeyNew 插件标识符
  116 + * @param file 文件
  117 + */
211 private void updatePluginInfo(PluginInfoDO pluginInfoDo, String pluginKeyNew, MultipartFile file) { 118 private void updatePluginInfo(PluginInfoDO pluginInfoDo, String pluginKeyNew, MultipartFile file) {
212 - // TODO @haohao:更新实体的时候,最好 new 一个新的!  
213 - // TODO @haohao:可以链式调用,简化下代码;  
214 - pluginInfoDo.setPluginKey(pluginKeyNew);  
215 - pluginInfoDo.setStatus(IotPluginStatusEnum.STOPPED.getStatus());  
216 - pluginInfoDo.setFileName(file.getOriginalFilename());  
217 - pluginInfoDo.setScript("");  
218 - // 解析 pf4j 插件  
219 - PluginDescriptor pluginDescriptor = pluginManager.getPlugin(pluginKeyNew).getDescriptor();  
220 - pluginInfoDo.setConfigSchema(pluginDescriptor.getPluginDescription());  
221 - pluginInfoDo.setVersion(pluginDescriptor.getVersion());  
222 - pluginInfoDo.setDescription(pluginDescriptor.getPluginDescription()); 119 + // 创建新的插件信息对象并链式设置属性
  120 + PluginInfoDO updatedPluginInfo = new PluginInfoDO()
  121 + .setId(pluginInfoDo.getId())
  122 + .setPluginKey(pluginKeyNew)
  123 + .setStatus(IotPluginStatusEnum.STOPPED.getStatus())
  124 + .setFileName(file.getOriginalFilename())
  125 + .setScript("")
  126 + .setConfigSchema(pluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription())
  127 + .setVersion(pluginManager.getPlugin(pluginKeyNew).getDescriptor().getVersion())
  128 + .setDescription(pluginManager.getPlugin(pluginKeyNew).getDescriptor().getPluginDescription());
223 129
224 // 执行更新 130 // 执行更新
225 - pluginInfoMapper.updateById(pluginInfoDo); 131 + pluginInfoMapper.updateById(updatedPluginInfo);
226 } 132 }
227 133
228 - // TODO @haohao:status、state 字段命名,要统一下~  
229 @Override 134 @Override
230 public void updatePluginStatus(Long id, Integer status) { 135 public void updatePluginStatus(Long id, Integer status) {
231 // 1. 校验插件信息是否存在 136 // 1. 校验插件信息是否存在
232 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id); 137 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
233 138
234 - // 2. 校验插件状态是否有效  
235 - // TODO @haohao:直接参数校验掉。通过 @InEnum  
236 - if (!IotPluginStatusEnum.contains(status)) {  
237 - throw exception(PLUGIN_STATUS_INVALID);  
238 - }  
239 -  
240 - // 3. 获取插件标识和插件实例  
241 - String pluginKey = pluginInfoDo.getPluginKey();  
242 - PluginWrapper plugin = pluginManager.getPlugin(pluginKey);  
243 -  
244 - // 4. 根据状态更新插件  
245 - if (plugin != null) {  
246 - // 4.1 启动:如果目标状态是运行且插件未启动,则启动插件  
247 - if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())  
248 - && plugin.getPluginState() != PluginState.STARTED) {  
249 - pluginManager.startPlugin(pluginKey);  
250 - updatePluginStatusFile(pluginKey, true);  
251 - // 4.2 停止:如果目标状态是停止且插件已启动,则停止插件  
252 - } else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())  
253 - && plugin.getPluginState() == PluginState.STARTED) {  
254 - pluginManager.stopPlugin(pluginKey);  
255 - updatePluginStatusFile(pluginKey, false);  
256 - }  
257 - } else {  
258 - // 5. 插件不存在且状态为停止,抛出异常  
259 - if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {  
260 - throw exception(PLUGIN_STATUS_INVALID);  
261 - }  
262 - } 139 + // 2. 更新插件状态
  140 + pluginInstanceService.updatePluginStatus(pluginInfoDo, status);
263 141
264 - // 6. 更新数据库中的插件状态  
265 - // TODO @haohao:新建新建 pluginInfoDo 哈!  
266 - pluginInfoDo.setStatus(status);  
267 - pluginInfoMapper.updateById(pluginInfoDo); 142 + // 3. 更新数据库中的插件状态
  143 + PluginInfoDO updatedPluginInfo = new PluginInfoDO();
  144 + updatedPluginInfo.setId(id);
  145 + updatedPluginInfo.setStatus(status);
  146 + pluginInfoMapper.updateById(updatedPluginInfo);
268 } 147 }
269 148
270 @Override 149 @Override
@@ -272,10 +151,9 @@ public class PluginInfoServiceImpl implements PluginInfoService { @@ -272,10 +151,9 @@ public class PluginInfoServiceImpl implements PluginInfoService {
272 return pluginInfoMapper.selectList(); 151 return pluginInfoMapper.selectList();
273 } 152 }
274 153
275 - // TODO @haohao:可以改成 getPluginInfoListByStatus 更通用哈。  
276 @Override 154 @Override
277 - public List<PluginInfoDO> getRunningPluginInfoList() {  
278 - return pluginInfoMapper.selectListByStatus(IotPluginStatusEnum.RUNNING.getStatus()); 155 + public List<PluginInfoDO> getPluginInfoListByStatus(Integer status) {
  156 + return pluginInfoMapper.selectListByStatus(status);
279 } 157 }
280 158
281 } 159 }
282 \ No newline at end of file 160 \ 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 package cn.iocoder.yudao.module.iot.service.plugin; 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 * IoT 插件实例 Service 接口 7 * IoT 插件实例 Service 接口
5 * 8 *
@@ -8,8 +11,46 @@ package cn.iocoder.yudao.module.iot.service.plugin; @@ -8,8 +11,46 @@ package cn.iocoder.yudao.module.iot.service.plugin;
8 public interface PluginInstanceService { 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 pluginKeyNew 插件标识符
  44 + * @param isEnabled 是否启用
  45 + */
  46 + void updatePluginStatusFile(String pluginKeyNew, boolean isEnabled);
  47 +
  48 + /**
  49 + * 更新插件状态
  50 + *
  51 + * @param pluginInfoDo 插件信息
  52 + * @param status 新状态
12 */ 53 */
13 - void updatePluginInstances(); 54 + void updatePluginStatus(PluginInfoDO pluginInfoDo, Integer status);
14 55
15 } 56 }
16 \ No newline at end of file 57 \ 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 package cn.iocoder.yudao.module.iot.service.plugin; 1 package cn.iocoder.yudao.module.iot.service.plugin;
2 2
  3 +import cn.hutool.core.io.FileUtil;
3 import cn.hutool.core.net.NetUtil; 4 import cn.hutool.core.net.NetUtil;
4 import cn.hutool.core.util.IdUtil; 5 import cn.hutool.core.util.IdUtil;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininfo.PluginInfoDO;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO; 7 import cn.iocoder.yudao.module.iot.dal.dataobject.plugininstance.PluginInstanceDO;
  8 +import cn.iocoder.yudao.module.iot.dal.mysql.plugin.PluginInfoMapper;
7 import cn.iocoder.yudao.module.iot.dal.mysql.plugin.PluginInstanceMapper; 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 import jakarta.annotation.Resource; 12 import jakarta.annotation.Resource;
9 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
  14 +import org.pf4j.PluginState;
10 import org.pf4j.PluginWrapper; 15 import org.pf4j.PluginWrapper;
11 import org.pf4j.spring.SpringPluginManager; 16 import org.pf4j.spring.SpringPluginManager;
12 import org.springframework.beans.factory.annotation.Value; 17 import org.springframework.beans.factory.annotation.Value;
13 import org.springframework.stereotype.Service; 18 import org.springframework.stereotype.Service;
14 import org.springframework.validation.annotation.Validated; 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.net.Inet4Address;
  25 +import java.net.InetAddress;
  26 +import java.nio.file.*;
  27 +import java.util.ArrayList;
  28 +import java.util.LinkedHashSet;
16 import java.util.List; 29 import java.util.List;
  30 +import java.util.Map;
  31 +import java.util.concurrent.TimeUnit;
  32 +import java.util.function.Function;
  33 +import java.util.stream.Collectors;
  34 +
  35 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
17 36
18 /** 37 /**
19 * IoT 插件实例 Service 实现类 38 * IoT 插件实例 Service 实现类
@@ -25,79 +44,195 @@ import java.util.List; @@ -25,79 +44,195 @@ import java.util.List;
25 @Slf4j 44 @Slf4j
26 public class PluginInstanceServiceImpl implements PluginInstanceService { 45 public class PluginInstanceServiceImpl implements PluginInstanceService {
27 46
28 - /**  
29 - * 主程序 ID  
30 - */  
31 // TODO @haohao:这个可以后续确认下,有没更合适的标识。例如说 mac 地址之类的 47 // TODO @haohao:这个可以后续确认下,有没更合适的标识。例如说 mac 地址之类的
  48 + // 简化的UUID + mac 地址 会不会好一些,一台机子有可能会部署多个插件
32 public static final String MAIN_ID = IdUtil.fastSimpleUUID(); 49 public static final String MAIN_ID = IdUtil.fastSimpleUUID();
33 50
34 @Resource 51 @Resource
35 - private PluginInfoService pluginInfoService; 52 + private PluginInfoMapper pluginInfoMapper;
36 @Resource 53 @Resource
37 private PluginInstanceMapper pluginInstanceMapper; 54 private PluginInstanceMapper pluginInstanceMapper;
38 @Resource 55 @Resource
39 private SpringPluginManager pluginManager; 56 private SpringPluginManager pluginManager;
40 57
  58 + @Value("${pf4j.pluginsDir}")
  59 + private String pluginsDir;
  60 +
41 @Value("${server.port:48080}") 61 @Value("${server.port:48080}")
42 private int port; 62 private int port;
43 63
44 - // TODO @haohao:建议把 PluginInfoServiceImpl 里面,和 instance 相关的逻辑拿过来,可能会更好。info 处理信息,instance 处理实例 64 + @Override
  65 + public void stopAndUnloadPlugin(String pluginKey) {
  66 + PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
  67 + if (plugin != null) {
  68 + if (plugin.getPluginState().equals(PluginState.STARTED)) {
  69 + pluginManager.stopPlugin(pluginKey); // 停止插件
  70 + log.info("已停止插件: {}", pluginKey);
  71 + }
  72 + pluginManager.unloadPlugin(pluginKey); // 卸载插件
  73 + log.info("已卸载插件: {}", pluginKey);
  74 + } else {
  75 + log.warn("插件不存在或已卸载: {}", pluginKey);
  76 + }
  77 + }
  78 +
  79 + @Override
  80 + public void deletePluginFile(PluginInfoDO pluginInfoDO) {
  81 + File file = new File(pluginsDir, pluginInfoDO.getFileName());
  82 + if (file.exists()) {
  83 + try {
  84 + TimeUnit.SECONDS.sleep(1); // 等待 1 秒,避免插件未卸载完毕
  85 + if (!file.delete()) {
  86 + log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName());
  87 + }
  88 + } catch (InterruptedException e) {
  89 + log.error("[deletePluginInfo][删除插件文件({}) 失败]", pluginInfoDO.getFileName(),
  90 + e);
  91 + Thread.currentThread().interrupt(); // 恢复中断状态
  92 + }
  93 + }
  94 + }
  95 +
  96 + @Override
  97 + public String uploadAndLoadNewPlugin(MultipartFile file) {
  98 + String pluginKeyNew;
  99 + // TODO @haohao:多节点,是不是要上传 s3 之类的存储器;然后定时去加载
  100 + Path pluginsPath = Paths.get(pluginsDir);
  101 + try {
  102 + FileUtil.mkdir(pluginsPath.toFile()); // 创建插件目录
  103 + String filename = file.getOriginalFilename();
  104 + if (filename != null) {
  105 + Path jarPath = pluginsPath.resolve(filename);
  106 + Files.copy(file.getInputStream(), jarPath, StandardCopyOption.REPLACE_EXISTING); // 保存上传的 JAR 文件
  107 + pluginKeyNew = pluginManager.loadPlugin(jarPath.toAbsolutePath()); // 加载插件
  108 + log.info("已加载插件: {}", pluginKeyNew);
  109 + } else {
  110 + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED);
  111 + }
  112 + } catch (IOException e) {
  113 + log.error("[uploadAndLoadNewPlugin][上传插件文件失败]", e);
  114 + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e);
  115 + } catch (Exception e) {
  116 + log.error("[uploadAndLoadNewPlugin][加载插件失败]", e);
  117 + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e);
  118 + }
  119 + return pluginKeyNew;
  120 + }
45 121
46 - // TODO @haohao:这个改成 reportPluginInstance 会不会更合适哈。  
47 @Override 122 @Override
48 - public void updatePluginInstances() {  
49 - // 1.1 查询 pf4j 插件列表 123 + public void updatePluginStatusFile(String pluginKeyNew, boolean isEnabled) {
  124 + // TODO @haohao:疑问,这里写 enabled.txt 和 disabled.txt 的目的是啥哈?
  125 + // pf4j 的插件状态文件,需要 2 个文件,一个 enabled.txt 一个 disabled.txt
  126 + Path enabledFilePath = Paths.get(pluginsDir, "enabled.txt");
  127 + Path disabledFilePath = Paths.get(pluginsDir, "disabled.txt");
  128 + Path targetFilePath = isEnabled ? enabledFilePath : disabledFilePath;
  129 + Path oppositeFilePath = isEnabled ? disabledFilePath : enabledFilePath;
  130 +
  131 + try {
  132 + PluginWrapper pluginWrapper = pluginManager.getPlugin(pluginKeyNew);
  133 + if (pluginWrapper == null) {
  134 + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED);
  135 + }
  136 + List<String> targetLines = Files.exists(targetFilePath) ? Files.readAllLines(targetFilePath)
  137 + : new ArrayList<>();
  138 + List<String> oppositeLines = Files.exists(oppositeFilePath) ? Files.readAllLines(oppositeFilePath)
  139 + : new ArrayList<>();
  140 +
  141 + if (!targetLines.contains(pluginKeyNew)) {
  142 + targetLines.add(pluginKeyNew);
  143 + Files.write(targetFilePath, targetLines, StandardOpenOption.CREATE,
  144 + StandardOpenOption.TRUNCATE_EXISTING);
  145 + log.info("已添加插件 {} 到 {}", pluginKeyNew, targetFilePath.getFileName());
  146 + }
  147 +
  148 + if (oppositeLines.contains(pluginKeyNew)) {
  149 + oppositeLines.remove(pluginKeyNew);
  150 + Files.write(oppositeFilePath, oppositeLines, StandardOpenOption.CREATE,
  151 + StandardOpenOption.TRUNCATE_EXISTING);
  152 + log.info("已从 {} 移除插件 {}", oppositeFilePath.getFileName(), pluginKeyNew);
  153 + }
  154 + } catch (IOException e) {
  155 + log.error("[updatePluginStatusFile][更新插件状态文件失败]", e);
  156 + throw exception(ErrorCodeConstants.PLUGIN_INSTALL_FAILED, e);
  157 + }
  158 + }
  159 +
  160 + @Override
  161 + public void updatePluginStatus(PluginInfoDO pluginInfoDo, Integer status) {
  162 + String pluginKey = pluginInfoDo.getPluginKey();
  163 + PluginWrapper plugin = pluginManager.getPlugin(pluginKey);
  164 +
  165 + if (plugin != null) {
  166 + // 启动插件
  167 + if (status.equals(IotPluginStatusEnum.RUNNING.getStatus())
  168 + && plugin.getPluginState() != PluginState.STARTED) {
  169 + pluginManager.startPlugin(pluginKey);
  170 + updatePluginStatusFile(pluginKey, true);
  171 + log.info("已启动插件: {}", pluginKey);
  172 + }
  173 + // 停止插件
  174 + else if (status.equals(IotPluginStatusEnum.STOPPED.getStatus())
  175 + && plugin.getPluginState() == PluginState.STARTED) {
  176 + pluginManager.stopPlugin(pluginKey);
  177 + updatePluginStatusFile(pluginKey, false);
  178 + log.info("已停止插件: {}", pluginKey);
  179 + }
  180 + } else {
  181 + // 插件不存在且状态为停止,抛出异常
  182 + if (IotPluginStatusEnum.STOPPED.getStatus().equals(pluginInfoDo.getStatus())) {
  183 + throw exception(ErrorCodeConstants.PLUGIN_STATUS_INVALID);
  184 + }
  185 + }
  186 + }
  187 +
  188 + @Override
  189 + public void reportPluginInstances() {
  190 + // 1. 获取 pf4j 插件列表
50 List<PluginWrapper> plugins = pluginManager.getPlugins(); 191 List<PluginWrapper> plugins = pluginManager.getPlugins();
51 - // 1.2 查询插件信息列表  
52 - List<PluginInfoDO> pluginInfos = pluginInfoService.getPluginInfoList();  
53 - // 1.3 动态获取主程序的 IP 和端口  
54 - String mainIp = getLocalIpAddress();  
55 192
56 - // 2. 遍历插件列表,并保存为插件实例 193 + // 2. 获取插件信息列表并转换为 Map 以便快速查找
  194 + List<PluginInfoDO> pluginInfos = pluginInfoMapper.selectList();
  195 + Map<String, PluginInfoDO> pluginInfoMap = pluginInfos.stream()
  196 + .collect(Collectors.toMap(PluginInfoDO::getPluginKey, Function.identity()));
  197 +
  198 + // 3. 获取本机 IP 和 MAC 地址
  199 + LinkedHashSet<InetAddress> localAddressList = NetUtil.localAddressList(t -> t instanceof Inet4Address);
  200 + LinkedHashSet<String> ipList = NetUtil.toIpList(localAddressList);
  201 + String ip = ipList.stream().findFirst().orElse("127.0.0.1");
  202 + String mac = NetUtil.getMacAddress(localAddressList.stream().findFirst().orElse(null));
  203 + String mainId = MAIN_ID + "-" + mac;
  204 +
  205 + // 4. 遍历插件列表,并保存为插件实例
57 for (PluginWrapper plugin : plugins) { 206 for (PluginWrapper plugin : plugins) {
58 - // 2.1 查找插件信息  
59 String pluginKey = plugin.getPluginId(); 207 String pluginKey = plugin.getPluginId();
60 - // TODO @haohao:CollUtil.findOne() 简化  
61 - PluginInfoDO pluginInfo = pluginInfos.stream()  
62 - .filter(pluginInfoDO -> pluginInfoDO.getPluginKey().equals(pluginKey))  
63 - .findFirst()  
64 - .orElse(null); 208 +
  209 + // 4.1 查找插件信息
  210 + PluginInfoDO pluginInfo = pluginInfoMap.get(pluginKey);
65 if (pluginInfo == null) { 211 if (pluginInfo == null) {
66 - // TODO @haohao:建议打个 error log 212 + // 4.2 插件信息不存在,记录错误并跳过
  213 + log.error("插件信息不存在,插件包标识符 = {}", pluginKey);
67 continue; 214 continue;
68 } 215 }
69 216
70 - // 2.2 查询插件实例  
71 - PluginInstanceDO pluginInstance = pluginInstanceMapper.selectByMainIdAndPluginId(MAIN_ID, pluginInfo.getId());  
72 - // 2.3.1 如果插件实例不存在,则创建 217 + // 4.3 查询插件实例
  218 + PluginInstanceDO pluginInstance = pluginInstanceMapper.selectByMainIdAndPluginId(mainId,
  219 + pluginInfo.getId());
73 if (pluginInstance == null) { 220 if (pluginInstance == null) {
74 - // TODO @haohao:可以链式调用;建议新建一个!  
75 - pluginInstance = new PluginInstanceDO();  
76 - pluginInstance.setPluginId(pluginInfo.getId());  
77 - pluginInstance.setMainId(MAIN_ID);  
78 - pluginInstance.setIp(mainIp);  
79 - pluginInstance.setPort(port);  
80 - pluginInstance.setHeartbeatAt(System.currentTimeMillis()); 221 + // 4.4 如果插件实例不存在,则创建
  222 + pluginInstance = PluginInstanceDO.builder()
  223 + .pluginId(pluginInfo.getId())
  224 + .mainId(MAIN_ID + "-" + mac)
  225 + .ip(ip)
  226 + .port(port)
  227 + .heartbeatAt(System.currentTimeMillis())
  228 + .build();
81 pluginInstanceMapper.insert(pluginInstance); 229 pluginInstanceMapper.insert(pluginInstance);
82 } else { 230 } else {
83 - // 2.3.2 如果插件实例存在,则更新 231 + // 4.5 如果插件实例存在,则更新心跳时间
84 pluginInstance.setHeartbeatAt(System.currentTimeMillis()); 232 pluginInstance.setHeartbeatAt(System.currentTimeMillis());
85 pluginInstanceMapper.updateById(pluginInstance); 233 pluginInstanceMapper.updateById(pluginInstance);
86 } 234 }
87 } 235 }
88 } 236 }
89 237
90 - // TODO @haohao:这个目的是,获取到第一个有效 ip 是哇?  
91 - private String getLocalIpAddress() {  
92 - try {  
93 - List<String> ipList = NetUtil.localIpv4s().stream()  
94 - .filter(ip -> !ip.startsWith("0.0") && !ip.startsWith("127.") && !ip.startsWith("169.254") && !ip.startsWith("255.255.255.255"))  
95 - .toList();  
96 - return ipList.isEmpty() ? "127.0.0.1" : ipList.get(0);  
97 - } catch (Exception e) {  
98 - log.error("获取本地IP地址失败", e);  
99 - return "127.0.0.1"; // 默认值  
100 - }  
101 - }  
102 -  
103 } 238 }
104 \ No newline at end of file 239 \ 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
@@ -11,6 +11,11 @@ import org.springframework.web.bind.annotation.RestController; @@ -11,6 +11,11 @@ import org.springframework.web.bind.annotation.RestController;
11 import javax.annotation.Resource; 11 import javax.annotation.Resource;
12 import java.util.concurrent.CompletableFuture; 12 import java.util.concurrent.CompletableFuture;
13 13
  14 +/**
  15 + * 插件实例 RPC 接口
  16 + *
  17 + * @author 芋道源码
  18 + */
14 @RestController 19 @RestController
15 @RequestMapping("/rpc") 20 @RequestMapping("/rpc")
16 @RequiredArgsConstructor 21 @RequiredArgsConstructor
@@ -29,4 +34,4 @@ public class RpcController { @@ -29,4 +34,4 @@ public class RpcController {
29 return rpcClient.call("concat", new Object[]{str1, str2}, 10); 34 return rpcClient.call("concat", new Object[]{str1, str2}, 10);
30 } 35 }
31 36
32 -} 37 -}
  38 +}
33 \ No newline at end of file 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/plugin/HttpHandler.java
@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.plugin; @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.plugin;
3 import cn.hutool.json.JSONObject; 3 import cn.hutool.json.JSONObject;
4 import cn.hutool.json.JSONUtil; 4 import cn.hutool.json.JSONUtil;
5 import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi; 5 import cn.iocoder.yudao.module.iot.api.device.DeviceDataApi;
  6 +import cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
6 import io.netty.buffer.Unpooled; 7 import io.netty.buffer.Unpooled;
7 import io.netty.channel.ChannelFutureListener; 8 import io.netty.channel.ChannelFutureListener;
8 import io.netty.channel.ChannelHandlerContext; 9 import io.netty.channel.ChannelHandlerContext;
@@ -12,7 +13,7 @@ import io.netty.util.CharsetUtil; @@ -12,7 +13,7 @@ import io.netty.util.CharsetUtil;
12 13
13 /** 14 /**
14 * 基于 Netty 的 HTTP 处理器,用于接收设备上报的数据并调用主程序的 DeviceDataApi 接口进行处理。 15 * 基于 Netty 的 HTTP 处理器,用于接收设备上报的数据并调用主程序的 DeviceDataApi 接口进行处理。
15 - * 16 + * <p>
16 * 1. 请求格式:JSON 格式,地址为 POST /sys/{productKey}/{deviceName}/thing/event/property/post 17 * 1. 请求格式:JSON 格式,地址为 POST /sys/{productKey}/{deviceName}/thing/event/property/post
17 * 2. 返回结果:JSON 格式,包含统一的 code、data、id、message、method、version 字段 18 * 2. 返回结果:JSON 格式,包含统一的 code、data、id、message、method、version 字段
18 */ 19 */
@@ -76,7 +77,12 @@ public class HttpHandler extends SimpleChannelInboundHandler&lt;FullHttpRequest&gt; { @@ -76,7 +77,12 @@ public class HttpHandler extends SimpleChannelInboundHandler&lt;FullHttpRequest&gt; {
76 77
77 try { 78 try {
78 // 调用主程序的接口保存数据 79 // 调用主程序的接口保存数据
79 - deviceDataApi.saveDeviceData(productKey, deviceName, jsonData.toString()); 80 + DeviceDataCreateReqDTO createDTO = DeviceDataCreateReqDTO.builder()
  81 + .productKey(productKey)
  82 + .deviceName(deviceName)
  83 + .message(jsonData.toString())
  84 + .build();
  85 + deviceDataApi.saveDeviceData(createDTO);
80 86
81 // 构造成功响应内容 87 // 构造成功响应内容
82 JSONObject successRes = createResponseJson( 88 JSONObject successRes = createResponseJson(