Explorar el Código

【代码新增】IoT:增加 plugin 插件的心跳机制,以及 Job 超时离线

YunaiV hace 6 meses
padre
commit
7670ac19e5
Se han modificado 16 ficheros con 279 adiciones y 105 borrados
  1. 13 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java
  2. 44 0
      yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java
  3. 14 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java
  4. 1 1
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java
  5. 4 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInfoMapper.java
  6. 11 6
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java
  7. 37 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java
  8. 0 30
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/PluginInstancesJob.java
  9. 4 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInfoService.java
  10. 6 0
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInfoServiceImpl.java
  11. 14 3
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java
  12. 64 65
      yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java
  13. 9 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java
  14. 49 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeta/IotPluginInstanceHeartbeatJob.java
  15. 7 0
      yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java
  16. 2 0
      yudao-server/src/main/resources/application.yaml

+ 13 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/IotDeviceUpstreamApi.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
+import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
 import cn.iocoder.yudao.module.iot.enums.ApiConstants;
 import jakarta.validation.Valid;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -20,6 +21,8 @@ public interface IotDeviceUpstreamApi {
 
     String PREFIX = ApiConstants.PREFIX + "/device/upstream";
 
+    // ========== 设备相关 ==========
+
     /**
      * 更新设备状态
      *
@@ -44,4 +47,14 @@ public interface IotDeviceUpstreamApi {
     @PostMapping(PREFIX + "/report-event")
     CommonResult<Boolean> reportDeviceEvent(@Valid @RequestBody IotDeviceEventReportReqDTO reportReqDTO);
 
+    // ========== 插件相关 ==========
+
+    /**
+     * 心跳插件实例
+     *
+     * @param heartbeatReqDTO 心跳插件实例 DTO
+     */
+    @PostMapping(PREFIX + "/heartbeat-plugin-instance")
+    CommonResult<Boolean> heartbeatPluginInstance(@Valid @RequestBody IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO);
+
 }

+ 44 - 0
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java

@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * IoT 插件实例心跳 Request DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class IotPluginInstanceHeartbeatReqDTO {
+
+    /**
+     * 请求编号
+     */
+    @NotEmpty(message = "请求编号不能为空")
+    private String processId;
+
+    /**
+     * 插件包标识符
+     */
+    @NotEmpty(message = "插件包标识符不能为空")
+    private String pluginKey;
+
+    /**
+     * 插件实例所在 IP
+     */
+    @NotEmpty(message = "插件实例所在 IP 不能为空")
+    private String hostIp;
+    /**
+     * 插件实例的进程编号
+     */
+    @NotNull(message = "插件实例的进程编号不能为空")
+    private Integer downstreamPort;
+
+    /**
+     * 是否在线
+     */
+    @NotNull(message = "是否在线不能为空")
+    private Boolean online;
+
+}

+ 14 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceUpstreamApiImpl.java

@@ -4,7 +4,9 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
+import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
 import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService;
+import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -21,6 +23,10 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
 
     @Resource
     private IotDeviceUpstreamService deviceUpstreamService;
+    @Resource
+    private IotPluginInstanceService pluginInstanceService;
+
+    // ========== 设备相关 ==========
 
     @Override
     public CommonResult<Boolean> updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) {
@@ -40,4 +46,12 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
         return success(true);
     }
 
+    // ========== 插件相关 ==========
+
+    @Override
+    public CommonResult<Boolean> heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) {
+        pluginInstanceService.heartbeatPluginInstance(heartbeatReqDTO);
+        return success(true);
+    }
+
 }

+ 1 - 1
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/plugin/IotPluginInstanceDO.java

@@ -67,6 +67,6 @@ public class IotPluginInstanceDO extends BaseDO {
      *
      * 目的:心路时间超过一定时间后,会被进行下线处理
      */
-    private Long heartbeatTime;
+    private LocalDateTime heartbeatTime;
 
 }

+ 4 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInfoMapper.java

@@ -25,4 +25,8 @@ public interface IotPluginInfoMapper extends BaseMapperX<IotPluginInfoDO> {
                 .orderByAsc(IotPluginInfoDO::getId));
     }
 
+    default IotPluginInfoDO selectByPluginKey(String pluginKey) {
+        return selectOne(IotPluginInfoDO::getPluginKey, pluginKey);
+    }
+
 }

+ 11 - 6
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java

@@ -1,18 +1,23 @@
 package cn.iocoder.yudao.module.iot.dal.mysql.plugin;
 
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
+import java.time.LocalDateTime;
+import java.util.List;
+
 @Mapper
 public interface IotPluginInstanceMapper extends BaseMapperX<IotPluginInstanceDO> {
 
-    // TODO @芋艿:方法名,重构
-    default IotPluginInstanceDO selectByMainIdAndPluginId(String mainId, Long pluginId) {
-        return selectOne(new LambdaQueryWrapperX<IotPluginInstanceDO>()
-                .eq(IotPluginInstanceDO::getProcessId, mainId)
-                .eq(IotPluginInstanceDO::getPluginId, pluginId));
+    default IotPluginInstanceDO selectByProcessId(String processId) {
+        return selectOne(IotPluginInstanceDO::getProcessId, processId);
+    }
+
+    default List<IotPluginInstanceDO> selectListByHeartbeatTimeLt(LocalDateTime heartbeatTime) {
+        return selectList(new LambdaQueryWrapper<IotPluginInstanceDO>()
+                .lt(IotPluginInstanceDO::getHeartbeatTime, heartbeatTime));
     }
 
 }

+ 37 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.iot.job.plugin;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+/**
+ * IoT 插件实例离线检查 Job
+ *
+ * @author 芋道源码
+ */
+@Component
+public class IotPluginInstancesJob implements JobHandler {
+
+    /**
+     * 插件离线超时时间
+     *
+     * TODO 芋艿:暂定 10 分钟,后续看要不要做配置
+     */
+    public static final Duration OFFLINE_TIMEOUT = Duration.ofMinutes(10);
+
+    @Resource
+    private IotPluginInstanceService pluginInstanceService;
+
+    @Override
+    public String execute(String param) {
+        int count = pluginInstanceService.offlineTimeoutPluginInstance(
+                LocalDateTime.now().minus(OFFLINE_TIMEOUT));
+        return StrUtil.format("离线超时插件实例数量为: {}", count);
+    }
+
+}

+ 0 - 30
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/PluginInstancesJob.java

@@ -1,30 +0,0 @@
-package cn.iocoder.yudao.module.iot.job.plugin;
-
-import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
-import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-import javax.annotation.Resource;
-import java.util.concurrent.TimeUnit;
-
-// TODO 芋艿:后续再看看
-/**
- * 插件实例 Job
- *
- * @author 芋道源码
- */
-@Component
-public class PluginInstancesJob {
-
-    @Resource
-    private IotPluginInstanceService pluginInstanceService;
-
-    @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
-    public void updatePluginInstances() {
-        TenantUtils.executeIgnore(() -> {
-            pluginInstanceService.reportPluginInstances();
-        });
-    }
-
-}

+ 4 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInfoService.java

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.info.PluginInfoSav
 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO;
 import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
 import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.util.List;
@@ -85,4 +86,7 @@ public interface IotPluginInfoService {
      * @return 插件信息列表
      */
     List<IotPluginInfoDO> getPluginInfoListByStatus(Integer status);
+
+    IotPluginInfoDO getPluginInfoByPluginKey(@NotEmpty(message = "插件包标识符不能为空") String pluginKey);
+
 }

+ 6 - 0
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInfoServiceImpl.java

@@ -41,6 +41,7 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService {
 
     @Override
     public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
+        // TODO @haohao:pluginKey 唯一值
         IotPluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, IotPluginInfoDO.class);
         pluginInfoMapper.insert(pluginInfo);
         return pluginInfo.getId();
@@ -155,4 +156,9 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService {
         return pluginInfoMapper.selectListByStatus(status);
     }
 
+    @Override
+    public IotPluginInfoDO getPluginInfoByPluginKey(String pluginKey) {
+        return pluginInfoMapper.selectByPluginKey(pluginKey);
+    }
+
 }

+ 14 - 3
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.java

@@ -1,8 +1,11 @@
 package cn.iocoder.yudao.module.iot.service.plugin;
 
+import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.time.LocalDateTime;
+
 /**
  * IoT 插件实例 Service 接口
  *
@@ -10,11 +13,19 @@ import org.springframework.web.multipart.MultipartFile;
  */
 public interface IotPluginInstanceService {
 
-    // TODO @芋艿:这个是否应该放到 plugin 主动心跳,而是 server 自己心跳
     /**
-     * 上报插件实例(心跳)
+     * 心跳插件实例
+     *
+     * @param heartbeatReqDTO 心跳插件实例 DTO
+     */
+    void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO);
+
+    /**
+     * 离线超时插件实例
+     *
+     * @param maxHeartbeatTime 最大心跳时间
      */
-    void reportPluginInstances();
+    int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime);
 
     /**
      * 停止并卸载插件

+ 64 - 65
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.java

@@ -1,11 +1,10 @@
 package cn.iocoder.yudao.module.iot.service.plugin;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.net.NetUtil;
-import cn.hutool.core.util.IdUtil;
+import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO;
 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO;
-import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInfoMapper;
 import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInstanceMapper;
 import cn.iocoder.yudao.module.iot.dal.redis.plugin.DevicePluginProcessIdRedisDAO;
 import cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants;
@@ -16,18 +15,20 @@ import org.pf4j.PluginState;
 import org.pf4j.PluginWrapper;
 import org.pf4j.spring.SpringPluginManager;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.file.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.time.LocalDateTime;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 
@@ -41,12 +42,9 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
 @Slf4j
 public class IotPluginInstanceServiceImpl implements IotPluginInstanceService {
 
-    // TODO @haohao:mac@uuid
-    public static final String MAIN_ID = IdUtil.fastSimpleUUID();
-
-    // TODO @haohao:不直接操作,通过 Service 哈
     @Resource
-    private IotPluginInfoMapper pluginInfoMapper;
+    @Lazy // 延迟加载,避免循环依赖
+    private IotPluginInfoService pluginInfoService;
 
     @Resource
     private IotPluginInstanceMapper pluginInstanceMapper;
@@ -59,8 +57,60 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService {
 
     @Value("${pf4j.pluginsDir}")
     private String pluginsDir;
-    @Value("${server.port:48080}")
-    private int port;
+
+    @Override
+    public void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) {
+        // 情况一:已存在,则进行更新
+        IotPluginInstanceDO instance = pluginInstanceMapper.selectByProcessId(heartbeatReqDTO.getProcessId());
+        if (instance != null) {
+            IotPluginInstanceDO.IotPluginInstanceDOBuilder updateObj = IotPluginInstanceDO.builder().id(instance.getId())
+                    .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort())
+                    .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now());
+            if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) {
+                if (Boolean.FALSE.equals(instance.getOnline())) { // 当前处于离线时,才需要更新上线时间
+                    updateObj.onlineTime(LocalDateTime.now());
+                }
+            } else {
+                updateObj.offlineTime(LocalDateTime.now());
+            }
+            pluginInstanceMapper.updateById(updateObj.build());
+            return;
+        }
+
+        // 情况二:不存在,则创建
+        IotPluginInfoDO info = pluginInfoService.getPluginInfoByPluginKey(heartbeatReqDTO.getPluginKey());
+        if (info == null) {
+            log.error("[heartbeatPluginInstance][心跳({}) 对应的插件不存在]", heartbeatReqDTO);
+            return;
+        }
+        IotPluginInstanceDO.IotPluginInstanceDOBuilder insertObj = IotPluginInstanceDO.builder()
+                .pluginId(info.getId()).processId(heartbeatReqDTO.getProcessId())
+                .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort())
+                .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now());
+        if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) {
+            insertObj.onlineTime(LocalDateTime.now());
+        } else {
+            insertObj.offlineTime(LocalDateTime.now());
+        }
+        pluginInstanceMapper.insert(insertObj.build());
+    }
+
+    @Override
+    public int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime) {
+        List<IotPluginInstanceDO> list = pluginInstanceMapper.selectListByHeartbeatTimeLt(maxHeartbeatTime);
+        if (CollUtil.isEmpty(list)) {
+            return 0;
+        }
+
+        // 更新插件实例为离线
+        int count = 0;
+        for (IotPluginInstanceDO instance : list) {
+            pluginInstanceMapper.updateById(IotPluginInstanceDO.builder().id(instance.getId())
+                    .online(false).offlineTime(LocalDateTime.now()).build());
+            count++;
+        }
+        return count;
+    }
 
     @Override
     public void stopAndUnloadPlugin(String pluginKey) {
@@ -146,57 +196,6 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService {
         }
     }
 
-    @Override
-    public void reportPluginInstances() {
-        // 1.1 获取 pf4j 插件列表
-        List<PluginWrapper> plugins = pluginManager.getPlugins();
-
-        // 1.2 获取插件信息列表并转换为 Map 以便快速查找
-        List<IotPluginInfoDO> pluginInfos = pluginInfoMapper.selectList();
-        Map<String, IotPluginInfoDO> pluginInfoMap = pluginInfos.stream()
-                .collect(Collectors.toMap(IotPluginInfoDO::getPluginKey, Function.identity()));
-
-        // 1.3 获取本机 IP 和 MAC 地址,mac@uuid
-        String ip = NetUtil.getLocalhostStr();
-        String mac = NetUtil.getLocalMacAddress();
-        String mainId = mac + "@" + MAIN_ID;
-
-        // 2. 遍历插件列表,并保存为插件实例
-        for (PluginWrapper plugin : plugins) {
-            String pluginKey = plugin.getPluginId();
-
-            // 2.1 查找插件信息
-            IotPluginInfoDO pluginInfo = pluginInfoMap.get(pluginKey);
-            if (pluginInfo == null) {
-                log.error("插件信息不存在,pluginKey = {}", pluginKey);
-                continue;
-            }
-
-            // 2.2 情况一:如果插件实例不存在,则创建
-            IotPluginInstanceDO pluginInstance = pluginInstanceMapper.selectByMainIdAndPluginId(mainId,
-                    pluginInfo.getId());
-            // TODO @芋艿:稍后优化;
-//            if (pluginInstance == null) {
-//                // 4.4 如果插件实例不存在,则创建
-//                pluginInstance = PluginInstanceDO.builder()
-//                        .pluginId(pluginInfo.getId())
-//                        .mainId(MAIN_ID + "-" + mac)
-//                        .hostIp(ip)
-//                        .port(port)
-//                        .heartbeatAt(System.currentTimeMillis())
-//                        .build();
-//                pluginInstanceMapper.insert(pluginInstance);
-//            } else {
-//                // 2.2 情况二:如果存在,则更新 heartbeatAt
-//                PluginInstanceDO updatePluginInstance = PluginInstanceDO.builder()
-//                        .id(pluginInstance.getId())
-//                        .heartbeatAt(System.currentTimeMillis())
-//                        .build();
-//                pluginInstanceMapper.updateById(updatePluginInstance);
-//            }
-        }
-    }
-
     // ========== 设备与插件的映射操作 ==========
 
     @Override

+ 9 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/config/IotPluginCommonAutoConfiguration.java

@@ -3,11 +3,13 @@ package cn.iocoder.yudao.module.iot.plugin.common.config;
 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler;
 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer;
+import cn.iocoder.yudao.module.iot.plugin.common.heartbeta.IotPluginInstanceHeartbeatJob;
 import cn.iocoder.yudao.module.iot.plugin.common.upstream.IotDeviceUpstreamClient;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.web.client.RestTemplateBuilder;
 import org.springframework.context.annotation.Bean;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.web.client.RestTemplate;
 
 import java.time.Duration;
@@ -18,6 +20,7 @@ import java.time.Duration;
  * @author haohao
  */
 @AutoConfiguration
+@EnableScheduling // 开启定时任务,因为 IotPluginInstanceHeartbeatJob 是一个定时任务
 public class IotPluginCommonAutoConfiguration {
 
     // TODO @haohao:这个要不搞个配置类哈
@@ -55,4 +58,10 @@ public class IotPluginCommonAutoConfiguration {
         return new IotDeviceDownstreamServer(deviceDownstreamHandler);
     }
 
+    @Bean(initMethod = "init", destroyMethod = "stop")
+    public IotPluginInstanceHeartbeatJob pluginInstanceHeartbeatJob(
+            IotDeviceUpstreamApi deviceDataApi, IotDeviceDownstreamServer deviceDownstreamServer) {
+        return new IotPluginInstanceHeartbeatJob(deviceDataApi, deviceDownstreamServer);
+    }
+
 }

+ 49 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/heartbeta/IotPluginInstanceHeartbeatJob.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.iot.plugin.common.heartbeta;
+
+import cn.hutool.system.SystemUtil;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
+import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
+import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer;
+import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * IoT 插件实例心跳 Job
+ *
+ * 用于定时发送心跳给服务端
+ */
+@RequiredArgsConstructor
+public class IotPluginInstanceHeartbeatJob {
+
+    private final IotDeviceUpstreamApi deviceUpstreamApi;
+    private final IotDeviceDownstreamServer deviceDownstreamServer;
+
+    public void init() {
+        CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true));
+        // TODO @芋艿:结果的处理
+    }
+
+    public void stop() {
+        CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(false));
+        // TODO @芋艿:结果的处理
+    }
+
+    @Scheduled(initialDelay = 3, fixedRate = 3, timeUnit = TimeUnit.MINUTES) // 3 分钟执行一次
+    public void execute() {
+        CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true));
+        // TODO @芋艿:结果的处理
+    }
+
+    private IotPluginInstanceHeartbeatReqDTO buildPluginInstanceHeartbeatReqDTO(Boolean online) {
+        // TODO @芋艿:pluginKey 的获取???
+        return new IotPluginInstanceHeartbeatReqDTO()
+                .setPluginKey("yudao-module-iot-plugin-http").setProcessId(IotPluginCommonUtils.getProcessId())
+                .setHostIp(SystemUtil.getHostInfo().getAddress()).setDownstreamPort(deviceDownstreamServer.getPort())
+                .setOnline(online);
+    }
+
+}

+ 7 - 0
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/upstream/IotDeviceUpstreamClient.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
+import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.client.RestTemplate;
 
@@ -51,6 +52,12 @@ public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi {
         return doPost(url, reportReqDTO, "reportDevicePropertyData");
     }
 
+    @Override
+    public CommonResult<Boolean> heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) {
+        String url = deviceDataUrl + URL_PREFIX + "/heartbeat-plugin-instance";
+        return doPost(url, heartbeatReqDTO, "heartbeatPluginInstance");
+    }
+
     // TODO @haohao:未来可能有 get 类型哈
     /**
      * 将与远程服务交互的通用逻辑抽取成一个私有方法

+ 2 - 0
yudao-server/src/main/resources/application.yaml

@@ -284,6 +284,8 @@ yudao:
       - infra_job
       - infra_job_log
       - infra_job_log
+      - iot_plugin_info
+      - iot_plugin_instance
       - infra_data_source_config
       - jimu_dict
       - jimu_dict_item