Commit 7670ac19e5b4cb096653919d18b2343602b844c3
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 | 4 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; |
| 6 | 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 | 8 | import cn.iocoder.yudao.module.iot.enums.ApiConstants; |
| 8 | 9 | import jakarta.validation.Valid; |
| 9 | 10 | import org.springframework.web.bind.annotation.PostMapping; |
| ... | ... | @@ -20,6 +21,8 @@ public interface IotDeviceUpstreamApi { |
| 20 | 21 | |
| 21 | 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 | 47 | @PostMapping(PREFIX + "/report-event") |
| 45 | 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 | 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 | 4 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; |
| 6 | 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 | 8 | import cn.iocoder.yudao.module.iot.service.device.control.IotDeviceUpstreamService; |
| 9 | +import cn.iocoder.yudao.module.iot.service.plugin.IotPluginInstanceService; | |
| 8 | 10 | import org.springframework.validation.annotation.Validated; |
| 9 | 11 | import org.springframework.web.bind.annotation.RestController; |
| 10 | 12 | |
| ... | ... | @@ -21,6 +23,10 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { |
| 21 | 23 | |
| 22 | 24 | @Resource |
| 23 | 25 | private IotDeviceUpstreamService deviceUpstreamService; |
| 26 | + @Resource | |
| 27 | + private IotPluginInstanceService pluginInstanceService; | |
| 28 | + | |
| 29 | + // ========== 设备相关 ========== | |
| 24 | 30 | |
| 25 | 31 | @Override |
| 26 | 32 | public CommonResult<Boolean> updateDeviceState(IotDeviceStateUpdateReqDTO updateReqDTO) { |
| ... | ... | @@ -40,4 +46,12 @@ public class IoTDeviceUpstreamApiImpl implements IotDeviceUpstreamApi { |
| 40 | 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 | 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
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> { |
| 25 | 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 | 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 | 1 | package cn.iocoder.yudao.module.iot.dal.mysql.plugin; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 4 | -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | |
| 5 | 4 | import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; |
| 5 | +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | |
| 6 | 6 | import org.apache.ibatis.annotations.Mapper; |
| 7 | 7 | |
| 8 | +import java.time.LocalDateTime; | |
| 9 | +import java.util.List; | |
| 10 | + | |
| 8 | 11 | @Mapper |
| 9 | 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 | 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 | 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 | 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 | import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.enums.plugin.IotPluginStatusEnum; |
| 8 | 8 | import jakarta.validation.Valid; |
| 9 | +import jakarta.validation.constraints.NotEmpty; | |
| 9 | 10 | import org.springframework.web.multipart.MultipartFile; |
| 10 | 11 | |
| 11 | 12 | import java.util.List; |
| ... | ... | @@ -85,4 +86,7 @@ public interface IotPluginInfoService { |
| 85 | 86 | * @return 插件信息列表 |
| 86 | 87 | */ |
| 87 | 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 | 41 | |
| 42 | 42 | @Override |
| 43 | 43 | public Long createPluginInfo(PluginInfoSaveReqVO createReqVO) { |
| 44 | + // TODO @haohao:pluginKey 唯一值 | |
| 44 | 45 | IotPluginInfoDO pluginInfo = BeanUtils.toBean(createReqVO, IotPluginInfoDO.class); |
| 45 | 46 | pluginInfoMapper.insert(pluginInfo); |
| 46 | 47 | return pluginInfo.getId(); |
| ... | ... | @@ -155,4 +156,9 @@ public class IotPluginInfoServiceImpl implements IotPluginInfoService { |
| 155 | 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 | 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 | 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 | 4 | import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO; |
| 4 | 5 | import org.springframework.web.multipart.MultipartFile; |
| 5 | 6 | |
| 7 | +import java.time.LocalDateTime; | |
| 8 | + | |
| 6 | 9 | /** |
| 7 | 10 | * IoT 插件实例 Service 接口 |
| 8 | 11 | * |
| ... | ... | @@ -10,11 +13,19 @@ import org.springframework.web.multipart.MultipartFile; |
| 10 | 13 | */ |
| 11 | 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 | 1 | package cn.iocoder.yudao.module.iot.service.plugin; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 3 | 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 | 6 | import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInfoDO; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.dal.dataobject.plugin.IotPluginInstanceDO; |
| 8 | -import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInfoMapper; | |
| 9 | 8 | import cn.iocoder.yudao.module.iot.dal.mysql.plugin.IotPluginInstanceMapper; |
| 10 | 9 | import cn.iocoder.yudao.module.iot.dal.redis.plugin.DevicePluginProcessIdRedisDAO; |
| 11 | 10 | import cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants; |
| ... | ... | @@ -16,18 +15,20 @@ import org.pf4j.PluginState; |
| 16 | 15 | import org.pf4j.PluginWrapper; |
| 17 | 16 | import org.pf4j.spring.SpringPluginManager; |
| 18 | 17 | import org.springframework.beans.factory.annotation.Value; |
| 18 | +import org.springframework.context.annotation.Lazy; | |
| 19 | 19 | import org.springframework.stereotype.Service; |
| 20 | 20 | import org.springframework.validation.annotation.Validated; |
| 21 | 21 | import org.springframework.web.multipart.MultipartFile; |
| 22 | 22 | |
| 23 | 23 | import java.io.File; |
| 24 | 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 | 30 | import java.util.List; |
| 27 | -import java.util.Map; | |
| 28 | 31 | import java.util.concurrent.TimeUnit; |
| 29 | -import java.util.function.Function; | |
| 30 | -import java.util.stream.Collectors; | |
| 31 | 32 | |
| 32 | 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 | 42 | @Slf4j |
| 42 | 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 | 45 | @Resource |
| 49 | - private IotPluginInfoMapper pluginInfoMapper; | |
| 46 | + @Lazy // 延迟加载,避免循环依赖 | |
| 47 | + private IotPluginInfoService pluginInfoService; | |
| 50 | 48 | |
| 51 | 49 | @Resource |
| 52 | 50 | private IotPluginInstanceMapper pluginInstanceMapper; |
| ... | ... | @@ -59,8 +57,60 @@ public class IotPluginInstanceServiceImpl implements IotPluginInstanceService { |
| 59 | 57 | |
| 60 | 58 | @Value("${pf4j.pluginsDir}") |
| 61 | 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 | 115 | @Override |
| 66 | 116 | public void stopAndUnloadPlugin(String pluginKey) { |
| ... | ... | @@ -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 | 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 | 3 | import cn.iocoder.yudao.module.iot.api.device.IotDeviceUpstreamApi; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamServer; |
| 6 | +import cn.iocoder.yudao.module.iot.plugin.common.heartbeta.IotPluginInstanceHeartbeatJob; | |
| 6 | 7 | import cn.iocoder.yudao.module.iot.plugin.common.upstream.IotDeviceUpstreamClient; |
| 7 | 8 | import org.springframework.beans.factory.annotation.Value; |
| 8 | 9 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 9 | 10 | import org.springframework.boot.web.client.RestTemplateBuilder; |
| 10 | 11 | import org.springframework.context.annotation.Bean; |
| 12 | +import org.springframework.scheduling.annotation.EnableScheduling; | |
| 11 | 13 | import org.springframework.web.client.RestTemplate; |
| 12 | 14 | |
| 13 | 15 | import java.time.Duration; |
| ... | ... | @@ -18,6 +20,7 @@ import java.time.Duration; |
| 18 | 20 | * @author haohao |
| 19 | 21 | */ |
| 20 | 22 | @AutoConfiguration |
| 23 | +@EnableScheduling // 开启定时任务,因为 IotPluginInstanceHeartbeatJob 是一个定时任务 | |
| 21 | 24 | public class IotPluginCommonAutoConfiguration { |
| 22 | 25 | |
| 23 | 26 | // TODO @haohao:这个要不搞个配置类哈 |
| ... | ... | @@ -55,4 +58,10 @@ public class IotPluginCommonAutoConfiguration { |
| 55 | 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 | 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 | 5 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDeviceEventReportReqDTO; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.api.device.dto.control.upstream.IotDevicePropertyReportReqDTO; |
| 7 | 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 | 9 | import lombok.extern.slf4j.Slf4j; |
| 9 | 10 | import org.springframework.web.client.RestTemplate; |
| 10 | 11 | |
| ... | ... | @@ -51,6 +52,12 @@ public class IotDeviceUpstreamClient implements IotDeviceUpstreamApi { |
| 51 | 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 | 61 | // TODO @haohao:未来可能有 get 类型哈 |
| 55 | 62 | /** |
| 56 | 63 | * 将与远程服务交互的通用逻辑抽取成一个私有方法 | ... | ... |
yudao-server/src/main/resources/application.yaml