refactor:重构配置热加载

This commit is contained in:
jxxghp
2025-06-03 20:56:21 +08:00
parent 5f18a21e86
commit b4ed2880f7
18 changed files with 291 additions and 80 deletions

View File

@@ -24,14 +24,13 @@ from app.db.models import User
from app.db.systemconfig_oper import SystemConfigOper
from app.db.user_oper import get_current_active_superuser
from app.helper.mediaserver import MediaServerHelper
from app.helper.message import MessageHelper, MessageQueueManager
from app.helper.message import MessageHelper
from app.helper.progress import ProgressHelper
from app.helper.rule import RuleHelper
from app.helper.sites import SitesHelper
from app.helper.subscribe import SubscribeHelper
from app.helper.system import SystemHelper
from app.log import logger
from app.monitor import Monitor
from app.scheduler import Scheduler
from app.schemas.types import SystemConfigKey
from app.utils.crypto import HashUtils
@@ -483,18 +482,6 @@ def restart_system(_: User = Depends(get_current_active_superuser)):
return schemas.Response(success=ret, message=msg)
@router.get("/reload", summary="重新加载模块", response_model=schemas.Response)
def reload_module(_: User = Depends(get_current_active_superuser)):
"""
重新加载模块(仅管理员)
"""
MessageQueueManager().init_config()
ModuleManager().reload()
Scheduler().init()
Monitor().init()
return schemas.Response(success=True)
@router.get("/runscheduler", summary="运行服务", response_model=schemas.Response)
def run_scheduler(jobid: str,
_: User = Depends(get_current_active_superuser)):

View File

@@ -1,21 +1,120 @@
import copy
import json
import os
import re
import secrets
import sys
import threading
from collections import defaultdict
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Type
from typing import Any, Dict, List, Optional, Tuple, Type, Callable
from dotenv import set_key
from pydantic import BaseModel, BaseSettings, validator, Field
from app.log import logger, log_settings, LogConfigModel
from app.utils.object import ObjectUtils
from app.utils.system import SystemUtils
from app.utils.url import UrlUtils
class ConfigChangeType(Enum):
"""
配置变更类型
"""
ADD = "add"
UPDATE = "update"
DELETE = "delete"
class ConfigChangeEvent:
"""
配置变更事件
"""
def __init__(self, key: str, old_value: Any, new_value: Any,
change_type: ConfigChangeType = ConfigChangeType.UPDATE):
self.key = key
self.old_value = old_value
self.new_value = new_value
self.change_type = change_type
self.timestamp = threading.Event()
class ConfigObserver:
"""
配置观察者接口
"""
def on_config_changed(self, event: ConfigChangeEvent):
"""
配置变更回调
"""
pass
class ConfigNotifier:
"""
配置变更通知器
"""
def __init__(self):
self._observers: Dict[str, List[ConfigObserver]] = defaultdict(list)
self._global_observers: List[ConfigObserver] = []
self._lock = threading.RLock()
def add_observer(self, observer: ConfigObserver, config_keys: Optional[List[str]] = None):
"""
添加观察者
:param observer: 观察者对象
:param config_keys: 监听的配置键列表为None时监听所有配置变更
"""
with self._lock:
if config_keys is None:
self._global_observers.append(observer)
else:
for key in config_keys:
self._observers[key].append(observer)
def remove_observer(self, observer: ConfigObserver, config_keys: Optional[List[str]] = None):
"""
移除观察者
:param observer: 观察者对象
:param config_keys: 监听的配置键列表为None时移除全局观察者
"""
with self._lock:
if config_keys is None:
if observer in self._global_observers:
self._global_observers.remove(observer)
else:
for key in config_keys:
if observer in self._observers[key]:
self._observers[key].remove(observer)
def notify(self, event: ConfigChangeEvent):
"""
通知观察者配置变更
"""
with self._lock:
# 通知全局观察者
for observer in self._global_observers:
try:
observer.on_config_changed(event)
except Exception as e:
logger.error(f"配置观察者 {observer} 处理配置变更时出错: {e}")
# 通知特定配置键的观察者
for observer in self._observers.get(event.key, []):
try:
observer.on_config_changed(event)
except Exception as e:
logger.error(f"配置观察者 {observer} 处理配置变更 {event.key} 时出错: {e}")
# 全局配置通知器
config_notifier = ConfigNotifier()
class ConfigModel(BaseModel):
"""
Pydantic 配置模型,描述所有配置项及其类型和默认值
@@ -255,30 +354,26 @@ class ConfigModel(BaseModel):
# 编码探测的最低置信度阈值
ENCODING_DETECTION_MIN_CONFIDENCE: float = 0.8
# 允许的图片缓存域名
SECURITY_IMAGE_DOMAINS: List[str] = Field(
default_factory=lambda: ["image.tmdb.org",
"static-mdb.v.geilijiasu.com",
"bing.com",
"doubanio.com",
"lain.bgm.tv",
"raw.githubusercontent.com",
"github.com",
"thetvdb.com",
"cctvpic.com",
"iqiyipic.com",
"hdslb.com",
"cmvideo.cn",
"ykimg.com",
"qpic.cn"]
)
SECURITY_IMAGE_DOMAINS: list = Field(default=[
"image.tmdb.org",
"static-mdb.v.geilijiasu.com",
"bing.com",
"doubanio.com",
"lain.bgm.tv",
"raw.githubusercontent.com",
"github.com",
"thetvdb.com",
"cctvpic.com",
"iqiyipic.com",
"hdslb.com",
"cmvideo.cn",
"ykimg.com",
"qpic.cn"
])
# 允许的图片文件后缀格式
SECURITY_IMAGE_SUFFIXES: List[str] = Field(
default_factory=lambda: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg", ".avif"]
)
SECURITY_IMAGE_SUFFIXES: list = Field(default=[".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg", ".avif"])
# 重命名时支持的S0别名
RENAME_FORMAT_S0_NAMES: List[str] = Field(
default_factory=lambda: ["Specials", "SPs"]
)
RENAME_FORMAT_S0_NAMES: list = Field(default=["Specials", "SPs"])
# 启用分词搜索
TOKENIZED_SEARCH: bool = False
# 为指定默认字幕添加.default后缀
@@ -333,6 +428,7 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
raise_exception: bool = False) -> Tuple[Any, bool]:
"""
通用类型转换函数,根据预期类型转换值。如果转换失败,返回默认值
:return: 元组 (转换后的值, 是否需要更新)
"""
if isinstance(value, (list, dict, set)):
value = copy.deepcopy(value)
@@ -373,12 +469,8 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
converted = float(value)
return converted, str(converted) != str(original_value)
elif expected_type is str:
# 清理 value 中所有空白字符的字段
fields_not_keep_spaces = {"AUTO_DOWNLOAD_USER", "REPO_GITHUB_TOKEN", "PLUGIN_MARKET"}
if field_name in fields_not_keep_spaces:
value = re.sub(r"\s+", "", value)
return value, str(value) != str(original_value)
# 支持 list 类型的处理
converted = str(value).strip()
return converted, converted != str(original_value)
elif expected_type is list:
if isinstance(value, list):
return value, str(value) != str(original_value)
@@ -388,7 +480,6 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
return items, items != original_value
else:
return items, str(items) != str(original_value)
# 可根据需要添加更多类型处理
else:
return value, str(value) != str(original_value)
except (ValueError, TypeError) as e:
@@ -462,9 +553,14 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
success, message = self.update_env_config(field, value, converted_value)
# 仅成功更新配置时,才更新内存
if success:
old_value = getattr(self, key)
setattr(self, key, converted_value)
if hasattr(log_settings, key):
setattr(log_settings, key, converted_value)
# 发送配置变更通知
event = ConfigChangeEvent(key, old_value, converted_value)
config_notifier.notify(event)
return success, message
return True, ""
except Exception as e:
@@ -475,21 +571,8 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
更新多个配置项
"""
results = {}
log_updated, plugin_monitor_updated = False, False
for k, v in env.items():
results[k] = self.update_setting(k, v)
if hasattr(log_settings, k):
log_updated = True
if k in ["PLUGIN_AUTO_RELOAD", "DEV"]:
plugin_monitor_updated = True
# 本次更新存在日志配置项更新,需要重新加载日志配置
if log_updated:
logger.update_loggers()
# 本次更新存在插件监控配置项更新,需要重新加载插件监控
if plugin_monitor_updated:
# 解决顶层循环导入问题
from app.core.plugin import PluginManager
PluginManager().reload_monitor()
return results
@property
@@ -645,6 +728,10 @@ class Settings(BaseSettings, ConfigModel, LogConfigModel):
return UrlUtils.combine_url(host=self.APP_DOMAIN, path=url)
# 实例化配置
settings = Settings()
class GlobalVar(object):
"""
全局标识
@@ -702,8 +789,93 @@ class GlobalVar(object):
return self.is_system_stopped or workflow_id in self.EMERGENCY_STOP_WORKFLOWS
# 实例化配置
settings = Settings()
# 全局标识
global_vars = GlobalVar()
class HotReloadManager(ConfigObserver):
"""
配置热更新管理器
"""
def __init__(self):
self._reload_handlers: Dict[str, Callable] = {}
# 注册为全局配置观察者
config_notifier.add_observer(self)
def register_handler(self, config_keys: List[str], handler: Callable[[Any, Any], None]):
"""
注册配置变更处理器
:param config_keys: 配置键列表
:param handler: 处理函数,接收 (old_value, new_value) 参数
"""
for key in config_keys:
self._reload_handlers[key] = handler
@staticmethod
def __get_callable(name: str):
"""
根据类名获取类实例,首先检查全局变量中是否存在该类,如果不存在则尝试动态导入模块。
:param name: 方法名/类名.方法名
:return: 类的实例
"""
# 检查类是否在全局变量中
if name in globals():
try:
class_obj = globals()[name]()
return class_obj
except Exception as e:
logger.error(str(e))
return None
# TODO 如果类不在全局变量中,尝试动态导入模块并创建实例
return None
def on_config_changed(self, event: ConfigChangeEvent):
"""
处理配置变更事件
"""
if event.key in self._reload_handlers:
try:
handler = self._reload_handlers[event.key]
# 可执行函数
func = self.__get_callable(handler.__qualname__)
# 参数数量
args_num = ObjectUtils.arguments(func)
if args_num < 2:
func()
else:
func(event.old_value, event.new_value)
logger.info(f"配置 {event.key} 热更新成功:{func}")
except Exception as e:
logger.error(f"配置 {event.key} 热更新失败: {e}")
# 初始化热更新管理器
hot_reload_manager = HotReloadManager()
def on_config_change(config_keys: List[str]):
"""
装饰器:用于注册配置变更处理函数
使用示例:
@on_config_change(['PROXY_HOST', 'TMDB_API_KEY'])
def handle_config_change(old_value, new_value):
pass
"""
def decorator(func: Callable[[Any, Any], None]):
hot_reload_manager.register_handler(config_keys, func)
return func
return decorator
@on_config_change(['DEBUG', 'LOG_LEVEL'])
def handle_logger_change():
"""
默认的配置变更处理函数
"""
logger.update_loggers()

View File

@@ -15,7 +15,7 @@ from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from app import schemas
from app.core.config import settings
from app.core.config import settings, on_config_change
from app.core.event import eventmanager
from app.db.plugindata_oper import PluginDataOper
from app.db.systemconfig_oper import SystemConfigOper
@@ -241,6 +241,13 @@ class PluginManager(metaclass=Singleton):
"""
return self._plugins
@on_config_change(['PLUGIN_AUTO_RELOAD', 'DEV'])
def handle_config_change(self):
"""
处理配置变更事件,重新加载插件监测
"""
self.reload_monitor()
def reload_monitor(self):
"""
重新加载插件文件修改监测

View File

@@ -1,5 +1,6 @@
from typing import Any, Union
from app.core.config import ConfigChangeEvent, config_notifier, ConfigChangeType
from app.db import DbOper
from app.db.models.systemconfig import SystemConfig
from app.schemas.types import SystemConfigKey
@@ -24,17 +25,32 @@ class SystemConfigOper(DbOper, metaclass=Singleton):
"""
if isinstance(key, SystemConfigKey):
key = key.value
# 旧值
old_value = self.__SYSTEMCONF.get(key)
# 更新内存
self.__SYSTEMCONF[key] = value
conf = SystemConfig.get_by_key(self._db, key)
if conf:
if value:
conf.update(self._db, {"value": value})
# 发送配置变更通知
if old_value != value:
event = ConfigChangeEvent(key, old_value=old_value, new_value=value,
change_type=ConfigChangeType.UPDATE)
config_notifier.notify(event)
else:
conf.delete(self._db, conf.id)
# 发送配置删除通知
event = ConfigChangeEvent(key, old_value=old_value, new_value=None,
change_type=ConfigChangeType.DELETE)
config_notifier.notify(event)
else:
conf = SystemConfig(key=key, value=value)
conf.create(self._db)
# 发送配置变更通知
event = ConfigChangeEvent(key, old_value=None, new_value=value,
change_type=ConfigChangeType.ADD)
config_notifier.notify(event)
def get(self, key: Union[str, SystemConfigKey] = None) -> Any:
"""
@@ -59,11 +75,15 @@ class SystemConfigOper(DbOper, metaclass=Singleton):
if isinstance(key, SystemConfigKey):
key = key.value
# 更新内存
self.__SYSTEMCONF.pop(key, None)
old_value = self.__SYSTEMCONF.pop(key, None)
# 写入数据库
conf = SystemConfig.get_by_key(self._db, key)
if conf:
conf.delete(self._db, conf.id)
# 发送配置变更通知
event = ConfigChangeEvent(key, old_value=old_value, new_value=None,
change_type=ConfigChangeType.ADD)
config_notifier.notify(event)
return True
def __del__(self):

View File

@@ -1,16 +1,18 @@
from typing import Any, Generator, List, Optional, Tuple, Union
from app import schemas
from app.core.config import on_config_change
from app.core.context import MediaInfo
from app.core.event import eventmanager
from app.log import logger
from app.modules import _MediaServerBase, _ModuleBase
from app.modules.emby.emby import Emby
from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType
from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType, SystemConfigKey
class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
@on_config_change([SystemConfigKey.MediaServers.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -1,17 +1,19 @@
from typing import Any, Generator, List, Optional, Tuple, Union
from app import schemas
from app.core.config import on_config_change
from app.core.context import MediaInfo
from app.core.event import eventmanager
from app.log import logger
from app.modules import _MediaServerBase, _ModuleBase
from app.modules.jellyfin.jellyfin import Jellyfin
from app.schemas import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType
from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType, SystemConfigKey
class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
@on_config_change([SystemConfigKey.MediaServers.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -1,17 +1,19 @@
from typing import Optional, Tuple, Union, Any, List, Generator
from app import schemas
from app.core.config import on_config_change
from app.core.context import MediaInfo
from app.core.event import eventmanager
from app.log import logger
from app.modules import _ModuleBase, _MediaServerBase
from app.modules.plex.plex import Plex
from app.schemas import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType
from app.schemas.types import MediaType, ModuleType, ChainEventType, MediaServerType, SystemConfigKey
class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
@on_config_change([SystemConfigKey.MediaServers.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -5,18 +5,19 @@ from qbittorrentapi import TorrentFilesList
from torrentool.torrent import Torrent
from app import schemas
from app.core.config import settings
from app.core.config import settings, on_config_change
from app.core.metainfo import MetaInfo
from app.log import logger
from app.modules import _ModuleBase, _DownloaderBase
from app.modules.qbittorrent.qbittorrent import Qbittorrent
from app.schemas import TransferTorrent, DownloadingTorrent
from app.schemas.types import TorrentStatus, ModuleType, DownloaderType
from app.schemas.types import TorrentStatus, ModuleType, DownloaderType, SystemConfigKey
from app.utils.string import StringUtils
class QbittorrentModule(_ModuleBase, _DownloaderBase[Qbittorrent]):
@on_config_change([SystemConfigKey.Downloaders.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -2,16 +2,18 @@ import json
import re
from typing import Optional, Union, List, Tuple, Any
from app.core.config import on_config_change
from app.core.context import MediaInfo, Context
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.slack.slack import Slack
from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
from app.schemas.types import ModuleType, SystemConfigKey
class SlackModule(_ModuleBase, _MessageBase[Slack]):
@on_config_change([SystemConfigKey.Notifications.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -1,15 +1,17 @@
from typing import Optional, Union, List, Tuple, Any
from app.core.config import on_config_change
from app.core.context import MediaInfo, Context
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.synologychat.synologychat import SynologyChat
from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
from app.schemas.types import ModuleType, SystemConfigKey
class SynologyChatModule(_ModuleBase, _MessageBase[SynologyChat]):
@on_config_change([SystemConfigKey.Notifications.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -2,18 +2,20 @@ import copy
import json
from typing import Optional, Union, List, Tuple, Any, Dict
from app.core.config import on_config_change
from app.core.context import MediaInfo, Context
from app.core.event import eventmanager
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.telegram.telegram import Telegram
from app.schemas import MessageChannel, CommingMessage, Notification, CommandRegisterEventData
from app.schemas.types import ModuleType, ChainEventType
from app.schemas.types import ModuleType, ChainEventType, SystemConfigKey
from app.utils.structures import DictUtils
class TelegramModule(_ModuleBase, _MessageBase[Telegram]):
@on_config_change([SystemConfigKey.Notifications.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -5,18 +5,19 @@ from torrentool.torrent import Torrent
from transmission_rpc import File
from app import schemas
from app.core.config import settings
from app.core.config import settings, on_config_change
from app.core.metainfo import MetaInfo
from app.log import logger
from app.modules import _ModuleBase, _DownloaderBase
from app.modules.transmission.transmission import Transmission
from app.schemas import TransferTorrent, DownloadingTorrent
from app.schemas.types import TorrentStatus, ModuleType, DownloaderType
from app.schemas.types import TorrentStatus, ModuleType, DownloaderType, SystemConfigKey
from app.utils.string import StringUtils
class TransmissionModule(_ModuleBase, _DownloaderBase[Transmission]):
@on_config_change([SystemConfigKey.Downloaders.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -1,17 +1,19 @@
from typing import Any, Generator, List, Optional, Tuple, Union
from app import schemas
from app.core.config import on_config_change
from app.core.context import MediaInfo
from app.core.event import eventmanager
from app.log import logger
from app.modules import _MediaServerBase, _ModuleBase
from app.modules.trimemedia.trimemedia import TrimeMedia
from app.schemas import AuthCredentials, AuthInterceptCredentials
from app.schemas.types import ChainEventType, MediaServerType, MediaType, ModuleType
from app.schemas.types import ChainEventType, MediaServerType, MediaType, ModuleType, SystemConfigKey
class TrimeMediaModule(_ModuleBase, _MediaServerBase[TrimeMedia]):
@on_config_change([SystemConfigKey.MediaServers.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -1,16 +1,18 @@
import json
from typing import Optional, Union, List, Tuple, Any, Dict
from app.core.config import on_config_change
from app.core.context import Context, MediaInfo
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.modules.vocechat.vocechat import VoceChat
from app.schemas import MessageChannel, CommingMessage, Notification
from app.schemas.types import ModuleType
from app.schemas.types import ModuleType, SystemConfigKey
class VoceChatModule(_ModuleBase, _MessageBase[VoceChat]):
@on_config_change([SystemConfigKey.Notifications.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -3,15 +3,16 @@ from typing import Union, Tuple
from pywebpush import webpush, WebPushException
from app.core.config import global_vars, settings
from app.core.config import global_vars, settings, on_config_change
from app.log import logger
from app.modules import _ModuleBase, _MessageBase
from app.schemas import Notification
from app.schemas.types import ModuleType, MessageChannel
from app.schemas.types import ModuleType, MessageChannel, SystemConfigKey
class WebPushModule(_ModuleBase, _MessageBase):
@on_config_change([SystemConfigKey.Notifications.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -2,6 +2,7 @@ import copy
import xml.dom.minidom
from typing import Optional, Union, List, Tuple, Any, Dict
from app.core.config import on_config_change
from app.core.context import Context, MediaInfo
from app.core.event import eventmanager
from app.log import logger
@@ -9,13 +10,14 @@ from app.modules import _ModuleBase, _MessageBase
from app.modules.wechat.WXBizMsgCrypt3 import WXBizMsgCrypt
from app.modules.wechat.wechat import WeChat
from app.schemas import MessageChannel, CommingMessage, Notification, CommandRegisterEventData
from app.schemas.types import ModuleType, ChainEventType
from app.schemas.types import ModuleType, ChainEventType, SystemConfigKey
from app.utils.dom import DomUtils
from app.utils.structures import DictUtils
class WechatModule(_ModuleBase, _MessageBase[WeChat]):
@on_config_change([SystemConfigKey.Notifications.value])
def init_module(self) -> None:
"""
初始化模块

View File

@@ -14,12 +14,13 @@ from watchdog.observers.polling import PollingObserver
from app.chain import ChainBase
from app.chain.storage import StorageChain
from app.chain.transfer import TransferChain
from app.core.config import settings
from app.core.config import settings, on_config_change
from app.db.systemconfig_oper import SystemConfigOper
from app.helper.directory import DirectoryHelper
from app.helper.message import MessageHelper
from app.log import logger
from app.schemas import FileItem
from app.schemas.types import SystemConfigKey
from app.utils.singleton import Singleton
lock = Lock()
@@ -85,6 +86,7 @@ class Monitor(metaclass=Singleton):
# 启动目录监控和文件整理
self.init()
@on_config_change([SystemConfigKey.Directories.value])
def init(self):
"""
启动监控

View File

@@ -18,7 +18,7 @@ from app.chain.subscribe import SubscribeChain
from app.chain.tmdb import TmdbChain
from app.chain.transfer import TransferChain
from app.chain.workflow import WorkflowChain
from app.core.config import settings
from app.core.config import settings, on_config_change
from app.core.event import EventManager
from app.core.plugin import PluginManager
from app.db.systemconfig_oper import SystemConfigOper
@@ -57,6 +57,8 @@ class Scheduler(metaclass=Singleton):
def __init__(self):
self.init()
@on_config_change(['DEV', 'COOKIECLOUD_INTERVAL', 'MEDIASERVER_SYNC_INTERVAL', 'SUBSCRIBE_SEARCH',
'SUBSCRIBE_MODE', 'SUBSCRIBE_RSS_INTERVAL', 'SITEDATA_REFRESH_INTERVAL'])
def init(self):
"""
初始化定时服务