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 51 application-my.yaml
52 52  
53 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 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 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/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
5 8 *
... ... @@ -7,14 +10,11 @@ package cn.iocoder.yudao.module.iot.api.device;
7 10 */
8 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 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 + DEPLOY_VIA_JAR(0, "通过 jar 部署"), // TODO @haohao:UPLOAD 和 ALONE 感觉有点冲突,前者是部署方式,后者是运行方式。这个后续再讨论下哈
  17 + DEPLOY_STANDALONE(1, "独立部署");
18 18  
19 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 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/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
... ... @@ -39,6 +41,7 @@ public class PluginInfoSaveReqVO {
39 41 private String protocol;
40 42  
41 43 @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED)
  44 + @InEnum(IotPluginStatusEnum.class)
42 45 private Integer status;
43 46  
44 47 @Schema(description = "插件配置项描述信息")
... ...
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/job/plugin/PluginInstancesJob.java
... ... @@ -22,7 +22,7 @@ public class PluginInstancesJob {
22 22 @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
23 23 public void updatePluginInstances() {
24 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 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,8 +15,8 @@ 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 21 import cn.iocoder.yudao.module.iot.enums.IotConstants;
21 22 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
... ... @@ -56,7 +57,7 @@ public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataSe
56 57 .put(IotDataSpecsDataTypeEnum.FLOAT.getDataType(), TDengineTableField.TYPE_FLOAT)
57 58 .put(IotDataSpecsDataTypeEnum.DOUBLE.getDataType(), TDengineTableField.TYPE_DOUBLE)
58 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 61 .put(IotDataSpecsDataTypeEnum.TEXT.getDataType(), TDengineTableField.TYPE_NCHAR)
61 62 .put(IotDataSpecsDataTypeEnum.DATE.getDataType(), TDengineTableField.TYPE_TIMESTAMP)
62 63 .put(IotDataSpecsDataTypeEnum.STRUCT.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿:怎么映射!!!!
... ... @@ -128,20 +129,20 @@ public class IotDevicePropertyDataServiceImpl implements IotDevicePropertyDataSe
128 129 }
129 130  
130 131 @Override
131   - public void saveDeviceData(String productKey, String deviceName, String message) {
  132 + public void saveDeviceData(DeviceDataCreateReqDTO createDTO) {
132 133 // 1. 根据产品 key 和设备名称,获得设备信息
133   - IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceName(productKey, deviceName);
  134 + IotDeviceDO device = deviceService.getDeviceByProductKeyAndDeviceName(createDTO.getProductKey(), createDTO.getDeviceName());
134 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 138 ThingModelMessage thingModelMessage = ThingModelMessage.builder()
138 139 .id(jsonObject.getStr("id"))
139 140 .sys(jsonObject.get("sys"))
140 141 .method(jsonObject.getStr("method"))
141 142 .params(jsonObject.get("params"))
142 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 146 .deviceKey(device.getDeviceKey())
146 147 .build();
147 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 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 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 实现类
... ... @@ -43,11 +34,10 @@ public class PluginInfoServiceImpl implements PluginInfoService {
43 34 private PluginInfoMapper pluginInfoMapper;
44 35  
45 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 42 @Override
53 43 public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
... ... @@ -75,32 +65,13 @@ public class PluginInfoServiceImpl implements PluginInfoService {
75 65 }
76 66  
77 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 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 77 private PluginInfoDO validatePluginInfoExists(Long id) {
... ... @@ -127,144 +98,52 @@ public class PluginInfoServiceImpl implements PluginInfoService {
127 98 PluginInfoDO pluginInfoDo = validatePluginInfoExists(id);
128 99  
129 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 107 // 4. 更新插件信息
138 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 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 134 @Override
230 135 public void updatePluginStatus(Long id, Integer status) {
231 136 // 1. 校验插件信息是否存在
232 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 149 @Override
... ... @@ -272,10 +151,9 @@ public class PluginInfoServiceImpl implements PluginInfoService {
272 151 return pluginInfoMapper.selectList();
273 152 }
274 153  
275   - // TODO @haohao:可以改成 getPluginInfoListByStatus 更通用哈。
276 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 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 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,46 @@ 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 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 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 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.net.Inet4Address;
  25 +import java.net.InetAddress;
  26 +import java.nio.file.*;
  27 +import java.util.ArrayList;
  28 +import java.util.LinkedHashSet;
16 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 38 * IoT 插件实例 Service 实现类
... ... @@ -25,79 +44,195 @@ import java.util.List;
25 44 @Slf4j
26 45 public class PluginInstanceServiceImpl implements PluginInstanceService {
27 46  
28   - /**
29   - * 主程序 ID
30   - */
31 47 // TODO @haohao:这个可以后续确认下,有没更合适的标识。例如说 mac 地址之类的
  48 + // 简化的UUID + mac 地址 会不会好一些,一台机子有可能会部署多个插件
32 49 public static final String MAIN_ID = IdUtil.fastSimpleUUID();
33 50  
34 51 @Resource
35   - private PluginInfoService pluginInfoService;
  52 + private PluginInfoMapper pluginInfoMapper;
36 53 @Resource
37 54 private PluginInstanceMapper pluginInstanceMapper;
38 55 @Resource
39 56 private SpringPluginManager pluginManager;
40 57  
  58 + @Value("${pf4j.pluginsDir}")
  59 + private String pluginsDir;
  60 +
41 61 @Value("${server.port:48080}")
42 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 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 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 206 for (PluginWrapper plugin : plugins) {
58   - // 2.1 查找插件信息
59 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 211 if (pluginInfo == null) {
66   - // TODO @haohao:建议打个 error log
  212 + // 4.2 插件信息不存在,记录错误并跳过
  213 + log.error("插件信息不存在,插件包标识符 = {}", pluginKey);
67 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 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 229 pluginInstanceMapper.insert(pluginInstance);
82 230 } else {
83   - // 2.3.2 如果插件实例存在,则更新
  231 + // 4.5 如果插件实例存在,则更新心跳时间
84 232 pluginInstance.setHeartbeatAt(System.currentTimeMillis());
85 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 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 11 import javax.annotation.Resource;
12 12 import java.util.concurrent.CompletableFuture;
13 13  
  14 +/**
  15 + * 插件实例 RPC 接口
  16 + *
  17 + * @author 芋道源码
  18 + */
14 19 @RestController
15 20 @RequestMapping("/rpc")
16 21 @RequiredArgsConstructor
... ... @@ -29,4 +34,4 @@ public class RpcController {
29 34 return rpcClient.call("concat", new Object[]{str1, str2}, 10);
30 35 }
31 36  
32 37 -}
  38 +}
33 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 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 cn.iocoder.yudao.module.iot.api.device.dto.DeviceDataCreateReqDTO;
6 7 import io.netty.buffer.Unpooled;
7 8 import io.netty.channel.ChannelFutureListener;
8 9 import io.netty.channel.ChannelHandlerContext;
... ... @@ -12,7 +13,7 @@ import io.netty.util.CharsetUtil;
12 13  
13 14 /**
14 15 * 基于 Netty 的 HTTP 处理器,用于接收设备上报的数据并调用主程序的 DeviceDataApi 接口进行处理。
15   - *
  16 + * <p>
16 17 * 1. 请求格式:JSON 格式,地址为 POST /sys/{productKey}/{deviceName}/thing/event/property/post
17 18 * 2. 返回结果:JSON 格式,包含统一的 code、data、id、message、method、version 字段
18 19 */
... ... @@ -76,7 +77,12 @@ public class HttpHandler extends SimpleChannelInboundHandler&lt;FullHttpRequest&gt; {
76 77  
77 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 88 JSONObject successRes = createResponseJson(
... ...