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,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<IotPluginInfoDO> { | @@ -25,4 +25,8 @@ public interface IotPluginInfoMapper extends BaseMapperX<IotPluginInfoDO> { | ||
| 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 |