Commit 7670ac19e5b4cb096653919d18b2343602b844c3

Authored by YunaiV
1 parent e650e752

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

Showing 16 changed files with 279 additions and 105 deletions
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; @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; 4 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
5 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; 5 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
6 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; 6 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
  7 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
7 import cn.iocoder.yudao.module.iot.enums.ApiConstants; 8 import cn.iocoder.yudao.module.iot.enums.ApiConstants;
8 import jakarta.validation.Valid; 9 import jakarta.validation.Valid;
9 import org.springframework.web.bind.annotation.PostMapping; 10 import org.springframework.web.bind.annotation.PostMapping;
@@ -20,6 +21,8 @@ public interface IotDeviceUpstreamApi { @@ -20,6 +21,8 @@ public interface IotDeviceUpstreamApi {
20 21
21 String PREFIX = ApiConstants.PREFIX + "/device/upstream"; 22 String PREFIX = ApiConstants.PREFIX + "/device/upstream";
22 23
  24 + // ========== 设备相关 ==========
  25 +
23 /** 26 /**
24 * 更新设备状态 27 * 更新设备状态
25 * 28 *
@@ -44,4 +47,14 @@ public interface IotDeviceUpstreamApi { @@ -44,4 +47,14 @@ public interface IotDeviceUpstreamApi {
44 @PostMapping(PREFIX + "/report-event") 47 @PostMapping(PREFIX + "/report-event")
45 CommonResult<Boolean> reportDeviceEvent(@Valid @RequestBody IotDeviceEventReportReqDTO reportReqDTO); 48 CommonResult<Boolean> reportDeviceEvent(@Valid @RequestBody IotDeviceEventReportReqDTO reportReqDTO);
46 49
  50 + // ========== 插件相关 ==========
  51 +
  52 + /**
  53 + * 心跳插件实例
  54 + *
  55 + * @param heartbeatReqDTO 心跳插件实例 DTO
  56 + */
  57 + @PostMapping(PREFIX + "/heartbeat-plugin-instance")
  58 + CommonResult<Boolean> heartbeatPluginInstance(@Valid @RequestBody IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO);
  59 +
47 } 60 }
48 \ No newline at end of file 61 \ No newline at end of file
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/api/device/dto/control/upstream/IotPluginInstanceHeartbeatReqDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.api.device.dto.control.upstream;
  2 +
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * IoT 插件实例心跳 Request DTO
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class IotPluginInstanceHeartbeatReqDTO {
  14 +
  15 + /**
  16 + * 请求编号
  17 + */
  18 + @NotEmpty(message = "请求编号不能为空")
  19 + private String processId;
  20 +
  21 + /**
  22 + * 插件包标识符
  23 + */
  24 + @NotEmpty(message = "插件包标识符不能为空")
  25 + private String pluginKey;
  26 +
  27 + /**
  28 + * 插件实例所在 IP
  29 + */
  30 + @NotEmpty(message = "插件实例所在 IP 不能为空")
  31 + private String hostIp;
  32 + /**
  33 + * 插件实例的进程编号
  34 + */
  35 + @NotNull(message = "插件实例的进程编号不能为空")
  36 + private Integer downstreamPort;
  37 +
  38 + /**
  39 + * 是否在线
  40 + */
  41 + @NotNull(message = "是否在线不能为空")
  42 + private Boolean online;
  43 +
  44 +}
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; @@ -4,7 +4,9 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; 4 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
5 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; 5 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
6 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; 6 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
  7 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
7 import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; 8 import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService;
  9 +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
8 import org.springframework.validation.annotation.Validated; 10 import org.springframework.validation.annotation.Validated;
9 import org.springframework.web.bind.annotation.RestController; 11 import org.springframework.web.bind.annotation.RestController;
10 12
@@ -21,6 +23,10 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { @@ -21,6 +23,10 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
21 23
22 @Resource 24 @Resource
23 private IotDeviceUpstreamService deviceUpstreamService; 25 private IotDeviceUpstreamService deviceUpstreamService;
  26 + @Resource
  27 + private IotPluginInstanceService pluginInstanceService;
  28 +
  29 + // ========== 设备相关 ==========
24 30
25 @Override 31 @Override
26 public CommonResult<Boolean> updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { 32 public CommonResult<Boolean> updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) {
@@ -40,4 +46,12 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { @@ -40,4 +46,12 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi {
40 return success(true); 46 return success(true);
41 } 47 }
42 48
  49 + // ========== 插件相关 ==========
  50 +
  51 + @Override
  52 + public CommonResult<Boolean> heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) {
  53 + pluginInstanceService.heartbeatPluginInstance(heartbeatReqDTO);
  54 + return success(true);
  55 + }
  56 +
43 } 57 }
44 \ No newline at end of file 58 \ No newline at end of file
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 { @@ -67,6 +67,6 @@ public class IotPluginInstanceDO extends BaseDO {
67 * 67 *
68 * 目的:心路时间超过一定时间后,会被进行下线处理 68 * 目的:心路时间超过一定时间后,会被进行下线处理
69 */ 69 */
70 - private Long heartbeatTime; 70 + private LocalDateTime heartbeatTime;
71 71
72 } 72 }
73 \ No newline at end of file 73 \ No newline at end of file
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&lt;IotPluginInfoDO&gt; { @@ -25,4 +25,8 @@ public interface IotPluginInfoMapper extends BaseMapperX&lt;IotPluginInfoDO&gt; {
25 .orderByAsc(IotPluginInfoDO::getId)); 25 .orderByAsc(IotPluginInfoDO::getId));
26 } 26 }
27 27
  28 + default IotPluginInfoDO selectByPluginKey(String pluginKey) {
  29 + return selectOne(IotPluginInfoDO::getPluginKey, pluginKey);
  30 + }
  31 +
28 } 32 }
29 \ No newline at end of file 33 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/plugin/IotPluginInstanceMapper.java
1 package cn.iocoder.yudao.module.iot.dal.mysql.plugin; 1 package cn.iocoder.yudao.module.iot.dal.mysql.plugin;
2 2
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;  
5 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO;
  5 +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
6 import org.apache.ibatis.annotations.Mapper; 6 import org.apache.ibatis.annotations.Mapper;
7 7
  8 +import java.time.LocalDateTime;
  9 +import java.util.List;
  10 +
8 @Mapper 11 @Mapper
9 public interface IotPluginInstanceMapper extends BaseMapperX<IotPluginInstanceDO> { 12 public interface IotPluginInstanceMapper extends BaseMapperX<IotPluginInstanceDO> {
10 13
11 - // TODO @芋艿:方法名,重构  
12 - default IotPluginInstanceDO selectByMainIdAndPluginId(String mainId, Long pluginId) {  
13 - return selectOne(new LambdaQueryWrapperX<IotPluginInstanceDO>()  
14 - .eq(IotPluginInstanceDO::getProcessId, mainId)  
15 - .eq(IotPluginInstanceDO::getPluginId, pluginId)); 14 + default IotPluginInstanceDO selectByProcessId(String processId) {
  15 + return selectOne(IotPluginInstanceDO::getProcessId, processId);
  16 + }
  17 +
  18 + default List<IotPluginInstanceDO> selectListByHeartbeatTimeLt(LocalDateTime heartbeatTime) {
  19 + return selectList(new LambdaQueryWrapper<IotPluginInstanceDO>()
  20 + .lt(IotPluginInstanceDO::getHeartbeatTime, heartbeatTime));
16 } 21 }
17 22
18 } 23 }
19 \ No newline at end of file 24 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/IotPluginInstancesJob.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.job.plugin;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
  5 +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;
  6 +import org.springframework.stereotype.Component;
  7 +
  8 +import javax.annotation.Resource;
  9 +import java.time.Duration;
  10 +import java.time.LocalDateTime;
  11 +
  12 +/**
  13 + * IoT 插件实例离线检查 Job
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +@Component
  18 +public class IotPluginInstancesJob implements JobHandler {
  19 +
  20 + /**
  21 + * 插件离线超时时间
  22 + *
  23 + * TODO 芋艿:暂定 10 分钟,后续看要不要做配置
  24 + */
  25 + public static final Duration OFFLINE_TIMEOUT = Duration.ofMinutes(10);
  26 +
  27 + @Resource
  28 + private IotPluginInstanceService pluginInstanceService;
  29 +
  30 + @Override
  31 + public String execute(String param) {
  32 + int count = pluginInstanceService.offlineTimeoutPluginInstance(
  33 + LocalDateTime.now().minus(OFFLINE_TIMEOUT));
  34 + return StrUtil.format("离线超时插件实例数量为: {}", count);
  35 + }
  36 +
  37 +}
0 \ No newline at end of file 38 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/job/plugin/PluginInstancesJob.java deleted
1 -package cn.iocoder.yudao.module.iot.job.plugin;  
2 -  
3 -import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;  
4 -import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService;  
5 -import org.springframework.scheduling.annotation.Scheduled;  
6 -import org.springframework.stereotype.Component;  
7 -  
8 -import javax.annotation.Resource;  
9 -import java.util.concurrent.TimeUnit;  
10 -  
11 -// TODO 芋艿:后续再看看  
12 -/**  
13 - * 插件实例 Job  
14 - *  
15 - * @author 芋道源码  
16 - */  
17 -@Component  
18 -public class PluginInstancesJob {  
19 -  
20 - @Resource  
21 - private IotPluginInstanceService pluginInstanceService;  
22 -  
23 - @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)  
24 - public void updatePluginInstances() {  
25 - TenantUtils.executeIgnore(() -> {  
26 - pluginInstanceService.reportPluginInstances();  
27 - });  
28 - }  
29 -  
30 -}  
31 \ No newline at end of file 0 \ No newline at end of file
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 @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.plugin.vo.info.PluginInfoSav
6 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO;
7 import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; 7 import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum;
8 import jakarta.validation.Valid; 8 import jakarta.validation.Valid;
  9 +import jakarta.validation.constraints.NotEmpty;
9 import org.springframework.web.multipart.MultipartFile; 10 import org.springframework.web.multipart.MultipartFile;
10 11
11 import java.util.List; 12 import java.util.List;
@@ -85,4 +86,7 @@ public interface IotPluginInfoService { @@ -85,4 +86,7 @@ public interface IotPluginInfoService {
85 * @return 插件信息列表 86 * @return 插件信息列表
86 */ 87 */
87 List<IotPluginInfoDO> getPluginInfoListByStatus(Integer status); 88 List<IotPluginInfoDO> getPluginInfoListByStatus(Integer status);
  89 +
  90 + IotPluginInfoDO getPluginInfoByPluginKey(@NotEmpty(message = "插件包标识符不能为空") String pluginKey);
  91 +
88 } 92 }
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 { @@ -41,6 +41,7 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService {
41 41
42 @Override 42 @Override
43 public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) { 43 public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) {
  44 + // TODO @haohao:pluginKey 唯一值
44 IotPluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, IotPluginInfoDO.class); 45 IotPluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, IotPluginInfoDO.class);
45 pluginInfoMapper.insert(pluginInfo); 46 pluginInfoMapper.insert(pluginInfo);
46 return pluginInfo.getId(); 47 return pluginInfo.getId();
@@ -155,4 +156,9 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService { @@ -155,4 +156,9 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService {
155 return pluginInfoMapper.selectListByStatus(status); 156 return pluginInfoMapper.selectListByStatus(status);
156 } 157 }
157 158
  159 + @Override
  160 + public IotPluginInfoDO getPluginInfoByPluginKey(String pluginKey) {
  161 + return pluginInfoMapper.selectByPluginKey(pluginKey);
  162 + }
  163 +
158 } 164 }
159 \ No newline at end of file 165 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceService.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.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
3 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO;
4 import org.springframework.web.multipart.MultipartFile; 5 import org.springframework.web.multipart.MultipartFile;
5 6
  7 +import java.time.LocalDateTime;
  8 +
6 /** 9 /**
7 * IoT 插件实例 Service 接口 10 * IoT 插件实例 Service 接口
8 * 11 *
@@ -10,11 +13,19 @@ import org.springframework.web.multipart.MultipartFile; @@ -10,11 +13,19 @@ import org.springframework.web.multipart.MultipartFile;
10 */ 13 */
11 public interface IotPluginInstanceService { 14 public interface IotPluginInstanceService {
12 15
13 - // TODO @芋艿:这个是否应该放到 plugin 主动心跳,而是 server 自己心跳  
14 /** 16 /**
15 - * 上报插件实例(心跳) 17 + * 心跳插件实例
  18 + *
  19 + * @param heartbeatReqDTO 心跳插件实例 DTO
  20 + */
  21 + void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO);
  22 +
  23 + /**
  24 + * 离线超时插件实例
  25 + *
  26 + * @param maxHeartbeatTime 最大心跳时间
16 */ 27 */
17 - void reportPluginInstances(); 28 + int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime);
18 29
19 /** 30 /**
20 * 停止并卸载插件 31 * 停止并卸载插件
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/plugin/IotPluginInstanceServiceImpl.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.collection.CollUtil;
3 import cn.hutool.core.io.FileUtil; 4 import cn.hutool.core.io.FileUtil;
4 -import cn.hutool.core.net.NetUtil;  
5 -import cn.hutool.core.util.IdUtil; 5 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; 7 import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO;
8 -import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInfoMapper;  
9 import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInstanceMapper; 8 import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInstanceMapper;
10 import cn.iocoder.yudao.module.iot.dal.redis.plugin.DevicePluginProcessIdRedisDAO; 9 import cn.iocoder.yudao.module.iot.dal.redis.plugin.DevicePluginProcessIdRedisDAO;
11 import cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants; 10 import cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants;
@@ -16,18 +15,20 @@ import org.pf4j.PluginState; @@ -16,18 +15,20 @@ import org.pf4j.PluginState;
16 import org.pf4j.PluginWrapper; 15 import org.pf4j.PluginWrapper;
17 import org.pf4j.spring.SpringPluginManager; 16 import org.pf4j.spring.SpringPluginManager;
18 import org.springframework.beans.factory.annotation.Value; 17 import org.springframework.beans.factory.annotation.Value;
  18 +import org.springframework.context.annotation.Lazy;
19 import org.springframework.stereotype.Service; 19 import org.springframework.stereotype.Service;
20 import org.springframework.validation.annotation.Validated; 20 import org.springframework.validation.annotation.Validated;
21 import org.springframework.web.multipart.MultipartFile; 21 import org.springframework.web.multipart.MultipartFile;
22 22
23 import java.io.File; 23 import java.io.File;
24 import java.io.IOException; 24 import java.io.IOException;
25 -import java.nio.file.*; 25 +import java.nio.file.Files;
  26 +import java.nio.file.Path;
  27 +import java.nio.file.Paths;
  28 +import java.nio.file.StandardCopyOption;
  29 +import java.time.LocalDateTime;
26 import java.util.List; 30 import java.util.List;
27 -import java.util.Map;  
28 import java.util.concurrent.TimeUnit; 31 import java.util.concurrent.TimeUnit;
29 -import java.util.function.Function;  
30 -import java.util.stream.Collectors;  
31 32
32 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 33 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
33 34
@@ -41,12 +42,9 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU @@ -41,12 +42,9 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
41 @Slf4j 42 @Slf4j
42 public class IotPluginInstanceServiceImpl implements IotPluginInstanceService { 43 public class IotPluginInstanceServiceImpl implements IotPluginInstanceService {
43 44
44 - // TODO @haohao:mac@uuid  
45 - public static final String MAIN_ID = IdUtil.fastSimpleUUID();  
46 -  
47 - // TODO @haohao:不直接操作,通过 Service 哈  
48 @Resource 45 @Resource
49 - private IotPluginInfoMapper pluginInfoMapper; 46 + @Lazy // 延迟加载,避免循环依赖
  47 + private IotPluginInfoService pluginInfoService;
50 48
51 @Resource 49 @Resource
52 private IotPluginInstanceMapper pluginInstanceMapper; 50 private IotPluginInstanceMapper pluginInstanceMapper;
@@ -59,8 +57,60 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService { @@ -59,8 +57,60 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService {
59 57
60 @Value("${pf4j.pluginsDir}") 58 @Value("${pf4j.pluginsDir}")
61 private String pluginsDir; 59 private String pluginsDir;
62 - @Value("${server.port:48080}")  
63 - private int port; 60 +
  61 + @Override
  62 + public void heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) {
  63 + // 情况一:已存在,则进行更新
  64 + IotPluginInstanceDO instance = pluginInstanceMapper.selectByProcessId(heartbeatReqDTO.getProcessId());
  65 + if (instance != null) {
  66 + IotPluginInstanceDO.IotPluginInstanceDOBuilder updateObj = IotPluginInstanceDO.builder().id(instance.getId())
  67 + .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort())
  68 + .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now());
  69 + if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) {
  70 + if (Boolean.FALSE.equals(instance.getOnline())) { // 当前处于离线时,才需要更新上线时间
  71 + updateObj.onlineTime(LocalDateTime.now());
  72 + }
  73 + } else {
  74 + updateObj.offlineTime(LocalDateTime.now());
  75 + }
  76 + pluginInstanceMapper.updateById(updateObj.build());
  77 + return;
  78 + }
  79 +
  80 + // 情况二:不存在,则创建
  81 + IotPluginInfoDO info = pluginInfoService.getPluginInfoByPluginKey(heartbeatReqDTO.getPluginKey());
  82 + if (info == null) {
  83 + log.error("[heartbeatPluginInstance][心跳({}) 对应的插件不存在]", heartbeatReqDTO);
  84 + return;
  85 + }
  86 + IotPluginInstanceDO.IotPluginInstanceDOBuilder insertObj = IotPluginInstanceDO.builder()
  87 + .pluginId(info.getId()).processId(heartbeatReqDTO.getProcessId())
  88 + .hostIp(heartbeatReqDTO.getHostIp()).downstreamPort(heartbeatReqDTO.getDownstreamPort())
  89 + .online(heartbeatReqDTO.getOnline()).heartbeatTime(LocalDateTime.now());
  90 + if (Boolean.TRUE.equals(heartbeatReqDTO.getOnline())) {
  91 + insertObj.onlineTime(LocalDateTime.now());
  92 + } else {
  93 + insertObj.offlineTime(LocalDateTime.now());
  94 + }
  95 + pluginInstanceMapper.insert(insertObj.build());
  96 + }
  97 +
  98 + @Override
  99 + public int offlineTimeoutPluginInstance(LocalDateTime maxHeartbeatTime) {
  100 + List<IotPluginInstanceDO> list = pluginInstanceMapper.selectListByHeartbeatTimeLt(maxHeartbeatTime);
  101 + if (CollUtil.isEmpty(list)) {
  102 + return 0;
  103 + }
  104 +
  105 + // 更新插件实例为离线
  106 + int count = 0;
  107 + for (IotPluginInstanceDO instance : list) {
  108 + pluginInstanceMapper.updateById(IotPluginInstanceDO.builder().id(instance.getId())
  109 + .online(false).offlineTime(LocalDateTime.now()).build());
  110 + count++;
  111 + }
  112 + return count;
  113 + }
64 114
65 @Override 115 @Override
66 public void stopAndUnloadPlugin(String pluginKey) { 116 public void stopAndUnloadPlugin(String pluginKey) {
@@ -146,57 +196,6 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService { @@ -146,57 +196,6 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService {
146 } 196 }
147 } 197 }
148 198
149 - @Override  
150 - public void reportPluginInstances() {  
151 - // 1.1 获取 pf4j 插件列表  
152 - List<PluginWrapper> plugins = pluginManager.getPlugins();  
153 -  
154 - // 1.2 获取插件信息列表并转换为 Map 以便快速查找  
155 - List<IotPluginInfoDO> pluginInfos = pluginInfoMapper.selectList();  
156 - Map<String, IotPluginInfoDO> pluginInfoMap = pluginInfos.stream()  
157 - .collect(Collectors.toMap(IotPluginInfoDO::getPluginKey, Function.identity()));  
158 -  
159 - // 1.3 获取本机 IP 和 MAC 地址,mac@uuid  
160 - String ip = NetUtil.getLocalhostStr();  
161 - String mac = NetUtil.getLocalMacAddress();  
162 - String mainId = mac + "@" + MAIN_ID;  
163 -  
164 - // 2. 遍历插件列表,并保存为插件实例  
165 - for (PluginWrapper plugin : plugins) {  
166 - String pluginKey = plugin.getPluginId();  
167 -  
168 - // 2.1 查找插件信息  
169 - IotPluginInfoDO pluginInfo = pluginInfoMap.get(pluginKey);  
170 - if (pluginInfo == null) {  
171 - log.error("插件信息不存在,pluginKey = {}", pluginKey);  
172 - continue;  
173 - }  
174 -  
175 - // 2.2 情况一:如果插件实例不存在,则创建  
176 - IotPluginInstanceDO pluginInstance = pluginInstanceMapper.selectByMainIdAndPluginId(mainId,  
177 - pluginInfo.getId());  
178 - // TODO @芋艿:稍后优化;  
179 -// if (pluginInstance == null) {  
180 -// // 4.4 如果插件实例不存在,则创建  
181 -// pluginInstance = PluginInstanceDO.builder()  
182 -// .pluginId(pluginInfo.getId())  
183 -// .mainId(MAIN_ID + "-" + mac)  
184 -// .hostIp(ip)  
185 -// .port(port)  
186 -// .heartbeatAt(System.currentTimeMillis())  
187 -// .build();  
188 -// pluginInstanceMapper.insert(pluginInstance);  
189 -// } else {  
190 -// // 2.2 情况二:如果存在,则更新 heartbeatAt  
191 -// PluginInstanceDO updatePluginInstance = PluginInstanceDO.builder()  
192 -// .id(pluginInstance.getId())  
193 -// .heartbeatAt(System.currentTimeMillis())  
194 -// .build();  
195 -// pluginInstanceMapper.updateById(updatePluginInstance);  
196 -// }  
197 - }  
198 - }  
199 -  
200 // ========== 设备与插件的映射操作 ========== 199 // ========== 设备与插件的映射操作 ==========
201 200
202 @Override 201 @Override
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; @@ -3,11 +3,13 @@ package cn.iocoder.yudao.module.iot.plugin.common.config;
3 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; 3 import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
4 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; 4 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler;
5 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; 5 import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer;
  6 +import cn.iocoder.yudao.module.iot.plugin.common.heartbeta.IotPluginInstanceHeartbeatJob;
6 import cn.iocoder.yudao.module.iot.plugin.common.upstream.IotDeviceUpstreamClient; 7 import cn.iocoder.yudao.module.iot.plugin.common.upstream.IotDeviceUpstreamClient;
7 import org.springframework.beans.factory.annotation.Value; 8 import org.springframework.beans.factory.annotation.Value;
8 import org.springframework.boot.autoconfigure.AutoConfiguration; 9 import org.springframework.boot.autoconfigure.AutoConfiguration;
9 import org.springframework.boot.web.client.RestTemplateBuilder; 10 import org.springframework.boot.web.client.RestTemplateBuilder;
10 import org.springframework.context.annotation.Bean; 11 import org.springframework.context.annotation.Bean;
  12 +import org.springframework.scheduling.annotation.EnableScheduling;
11 import org.springframework.web.client.RestTemplate; 13 import org.springframework.web.client.RestTemplate;
12 14
13 import java.time.Duration; 15 import java.time.Duration;
@@ -18,6 +20,7 @@ import java.time.Duration; @@ -18,6 +20,7 @@ import java.time.Duration;
18 * @author haohao 20 * @author haohao
19 */ 21 */
20 @AutoConfiguration 22 @AutoConfiguration
  23 +@EnableScheduling // 开启定时任务,因为 IotPluginInstanceHeartbeatJob 是一个定时任务
21 public class IotPluginCommonAutoConfiguration { 24 public class IotPluginCommonAutoConfiguration {
22 25
23 // TODO @haohao:这个要不搞个配置类哈 26 // TODO @haohao:这个要不搞个配置类哈
@@ -55,4 +58,10 @@ public class IotPluginCommonAutoConfiguration { @@ -55,4 +58,10 @@ public class IotPluginCommonAutoConfiguration {
55 return new IotDeviceDownstreamServer(deviceDownstreamHandler); 58 return new IotDeviceDownstreamServer(deviceDownstreamHandler);
56 } 59 }
57 60
  61 + @Bean(initMethod = "init", destroyMethod = "stop")
  62 + public IotPluginInstanceHeartbeatJob pluginInstanceHeartbeatJob(
  63 + IotDeviceUpstreamApi deviceDataApi, IotDeviceDownstreamServer deviceDownstreamServer) {
  64 + return new IotPluginInstanceHeartbeatJob(deviceDataApi, deviceDownstreamServer);
  65 + }
  66 +
58 } 67 }
59 \ No newline at end of file 68 \ No newline at end of file
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 → 100644
  1 +package cn.iocoder.yudao.module.iot.plugin.common.heartbeta;
  2 +
  3 +import cn.hutool.system.SystemUtil;
  4 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
  6 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
  7 +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer;
  8 +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils;
  9 +import lombok.RequiredArgsConstructor;
  10 +import org.springframework.scheduling.annotation.Scheduled;
  11 +
  12 +import java.util.concurrent.TimeUnit;
  13 +
  14 +/**
  15 + * IoT 插件实例心跳 Job
  16 + *
  17 + * 用于定时发送心跳给服务端
  18 + */
  19 +@RequiredArgsConstructor
  20 +public class IotPluginInstanceHeartbeatJob {
  21 +
  22 + private final IotDeviceUpstreamApi deviceUpstreamApi;
  23 + private final IotDeviceDownstreamServer deviceDownstreamServer;
  24 +
  25 + public void init() {
  26 + CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true));
  27 + // TODO @芋艿:结果的处理
  28 + }
  29 +
  30 + public void stop() {
  31 + CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(false));
  32 + // TODO @芋艿:结果的处理
  33 + }
  34 +
  35 + @Scheduled(initialDelay = 3, fixedRate = 3, timeUnit = TimeUnit.MINUTES) // 3 分钟执行一次
  36 + public void execute() {
  37 + CommonResult<Boolean> result = deviceUpstreamApi.heartbeatPluginInstance(buildPluginInstanceHeartbeatReqDTO(true));
  38 + // TODO @芋艿:结果的处理
  39 + }
  40 +
  41 + private IotPluginInstanceHeartbeatReqDTO buildPluginInstanceHeartbeatReqDTO(Boolean online) {
  42 + // TODO @芋艿:pluginKey 的获取???
  43 + return new IotPluginInstanceHeartbeatReqDTO()
  44 + .setPluginKey("yudao-module-iot-plugin-http").setProcessId(IotPluginCommonUtils.getProcessId())
  45 + .setHostIp(SystemUtil.getHostInfo().getAddress()).setDownstreamPort(deviceDownstreamServer.getPort())
  46 + .setOnline(online);
  47 + }
  48 +
  49 +}
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; @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi;
5 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; 5 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO;
6 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; 6 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO;
7 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO; 7 import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceStateUpdateReqDTO;
  8 +import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotPluginInstanceHeartbeatReqDTO;
8 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
9 import org.springframework.web.client.RestTemplate; 10 import org.springframework.web.client.RestTemplate;
10 11
@@ -51,6 +52,12 @@ public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi { @@ -51,6 +52,12 @@ public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi {
51 return doPost(url, reportReqDTO, "reportDevicePropertyData"); 52 return doPost(url, reportReqDTO, "reportDevicePropertyData");
52 } 53 }
53 54
  55 + @Override
  56 + public CommonResult<Boolean> heartbeatPluginInstance(IotPluginInstanceHeartbeatReqDTO heartbeatReqDTO) {
  57 + String url = deviceDataUrl + URL_PREFIX + "/heartbeat-plugin-instance";
  58 + return doPost(url, heartbeatReqDTO, "heartbeatPluginInstance");
  59 + }
  60 +
54 // TODO @haohao:未来可能有 get 类型哈 61 // TODO @haohao:未来可能有 get 类型哈
55 /** 62 /**
56 * 将与远程服务交互的通用逻辑抽取成一个私有方法 63 * 将与远程服务交互的通用逻辑抽取成一个私有方法
yudao-server/src/main/resources/application.yaml
@@ -284,6 +284,8 @@ yudao: @@ -284,6 +284,8 @@ yudao:
284 - infra_job 284 - infra_job
285 - infra_job_log 285 - infra_job_log
286 - infra_job_log 286 - infra_job_log
  287 + - iot_plugin_info
  288 + - iot_plugin_instance
287 - infra_data_source_config 289 - infra_data_source_config
288 - jimu_dict 290 - jimu_dict
289 - jimu_dict_item 291 - jimu_dict_item