减少数据库查询次数,提高数据库查询方面性能 (#2030)
Some checks failed
检查bot是否运行正常 / bot check (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Sequential Lint and Type Check / ruff-call (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Force Sync to Aliyun / sync (push) Has been cancelled
Update Version / update-version (push) Has been cancelled
Sequential Lint and Type Check / pyright-call (push) Has been cancelled

* 🐛 修复数据库超时问题

* 🔧 移除帮助图片清理功能.

*  更新插件商店功能,允许在添加插件时指定源类型为 None。优化插件 ID 查找逻辑,增强代码可读性。新增 zhenxun/ui 模块导入。

* 🔧 优化数据访问和数据库上下文逻辑,移除不必要的全局变量和日志信息,调整日志级别为调试,提升代码可读性和性能。
This commit is contained in:
HibiKier 2025-08-26 16:53:14 +08:00 committed by GitHub
parent b12168b6b9
commit d9e65057cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 42 additions and 43 deletions

View File

@ -219,7 +219,7 @@ class StoreManager:
return plugin_info, is_external return plugin_info, is_external
@classmethod @classmethod
async def add_plugin(cls, index_or_module: str, source: str | None) -> str: async def add_plugin(cls, index_or_module: str, source: str | None = None) -> str:
"""添加插件 """添加插件
参数: 参数:
@ -518,11 +518,11 @@ class StoreManager:
raise PluginStoreException("插件ID不存在...") raise PluginStoreException("插件ID不存在...")
return all_plugin_list[idx].module return all_plugin_list[idx].module
elif isinstance(plugin_id, str): elif isinstance(plugin_id, str):
result = ( if plugin_id in [v.module for v in all_plugin_list]:
None return plugin_id
if plugin_id not in [v.module for v in all_plugin_list]
else plugin_id for plugin_info in all_plugin_list:
) or next(v for v in all_plugin_list if v.name == plugin_id).module if plugin_info.name.lower() == plugin_id.lower():
if not result: return plugin_info.module
raise PluginStoreException("插件 Module / 名称 不存在...")
return result raise PluginStoreException("插件 Module / 名称 不存在...")

View File

@ -16,7 +16,7 @@ from zhenxun.utils.platform import PlatformUtils
from ....base_model import Result from ....base_model import Result
from ....config import QueryDateType from ....config import QueryDateType
from ....utils import authentication, clear_help_image, get_system_status from ....utils import authentication, get_system_status
from .data_source import ApiDataSource from .data_source import ApiDataSource
from .model import ( from .model import (
ActiveGroup, ActiveGroup,
@ -234,7 +234,6 @@ async def _(param: BotManageUpdateParam):
bot_data.block_plugins = CommonUtils.convert_module_format(param.block_plugins) bot_data.block_plugins = CommonUtils.convert_module_format(param.block_plugins)
bot_data.block_tasks = CommonUtils.convert_module_format(param.block_tasks) bot_data.block_tasks = CommonUtils.convert_module_format(param.block_tasks)
await bot_data.save(update_fields=["block_plugins", "block_tasks"]) await bot_data.save(update_fields=["block_plugins", "block_tasks"])
clear_help_image()
return Result.ok() return Result.ok()
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/update_bot_manage 调用错误", "WebUi", e=e) logger.error(f"{router.prefix}/update_bot_manage 调用错误", "WebUi", e=e)

View File

@ -7,7 +7,7 @@ from zhenxun.utils.enum import BlockType, PluginType
from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager from zhenxun.utils.manager.virtual_env_package_manager import VirtualEnvPackageManager
from ....base_model import Result from ....base_model import Result
from ....utils import authentication, clear_help_image from ....utils import authentication
from .data_source import ApiDataSource from .data_source import ApiDataSource
from .model import ( from .model import (
BatchUpdatePlugins, BatchUpdatePlugins,
@ -82,7 +82,6 @@ async def _() -> Result[PluginCount]:
async def _(param: UpdatePlugin) -> Result: async def _(param: UpdatePlugin) -> Result:
try: try:
await ApiDataSource.update_plugin(param) await ApiDataSource.update_plugin(param)
clear_help_image()
return Result.ok(info="已经帮你写好啦!") return Result.ok(info="已经帮你写好啦!")
except (ValueError, KeyError): except (ValueError, KeyError):
return Result.fail("插件数据不存在...") return Result.fail("插件数据不存在...")
@ -110,7 +109,6 @@ async def _(param: PluginSwitch) -> Result:
db_plugin.block_type = None db_plugin.block_type = None
db_plugin.status = True db_plugin.status = True
await db_plugin.save() await db_plugin.save()
clear_help_image()
return Result.ok(info="成功改变了开关状态!") return Result.ok(info="成功改变了开关状态!")
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/change_switch 调用错误", "WebUi", e=e) logger.error(f"{router.prefix}/change_switch 调用错误", "WebUi", e=e)
@ -177,7 +175,6 @@ async def _(
updated_count=result_dict["updated_count"], updated_count=result_dict["updated_count"],
errors=result_dict["errors"], errors=result_dict["errors"],
) )
clear_help_image()
return Result.ok(result_model, "插件配置更新完成") return Result.ok(result_model, "插件配置更新完成")
except Exception as e: except Exception as e:
logger.error(f"{router.prefix}/plugins/batch_update 调用错误", "WebUi", e=e) logger.error(f"{router.prefix}/plugins/batch_update 调用错误", "WebUi", e=e)
@ -197,7 +194,6 @@ async def _(payload: RenameMenuTypePayload) -> Result[str]:
old_name=payload.old_name, new_name=payload.new_name old_name=payload.old_name, new_name=payload.new_name
) )
if result.get("success"): if result.get("success"):
clear_help_image()
return Result.ok( return Result.ok(
info=result.get( info=result.get(
"info", "info",

View File

@ -12,7 +12,7 @@ import psutil
import ujson as json import ujson as json
from zhenxun.configs.config import Config from zhenxun.configs.config import Config
from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH from zhenxun.configs.path_config import DATA_PATH
from .base_model import SystemFolderSize, SystemStatus, User from .base_model import SystemFolderSize, SystemStatus, User
@ -68,22 +68,6 @@ def validate_path(path_str: str | None) -> tuple[Path | None, str | None]:
return None, f"路径验证失败: {e!s}" return None, f"路径验证失败: {e!s}"
GROUP_HELP_PATH = DATA_PATH / "group_help"
SIMPLE_HELP_IMAGE = IMAGE_PATH / "SIMPLE_HELP.png"
SIMPLE_DETAIL_HELP_IMAGE = IMAGE_PATH / "SIMPLE_DETAIL_HELP.png"
def clear_help_image():
"""清理帮助图片"""
if SIMPLE_HELP_IMAGE.exists():
SIMPLE_HELP_IMAGE.unlink()
if SIMPLE_DETAIL_HELP_IMAGE.exists():
SIMPLE_DETAIL_HELP_IMAGE.unlink()
for file in GROUP_HELP_PATH.iterdir():
if file.is_file():
file.unlink()
def get_user(uname: str) -> User | None: def get_user(uname: str) -> User | None:
"""获取账号密码 """获取账号密码

View File

@ -4,6 +4,7 @@ from typing_extensions import Self
from tortoise import fields from tortoise import fields
from zhenxun.services.data_access import DataAccess
from zhenxun.services.db_context import Model from zhenxun.services.db_context import Model
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.enum import CacheType, DbLockType from zhenxun.utils.enum import CacheType, DbLockType
@ -57,14 +58,15 @@ class BanConsole(Model):
""" """
if not user_id and not group_id: if not user_id and not group_id:
raise UserAndGroupIsNone() raise UserAndGroupIsNone()
dao = DataAccess(cls)
if user_id: if user_id:
return ( return (
await cls.safe_get_or_none(user_id=user_id, group_id=group_id) await dao.safe_get_or_none(user_id=user_id, group_id=group_id)
if group_id if group_id
else await cls.safe_get_or_none(user_id=user_id, group_id__isnull=True) else await dao.safe_get_or_none(user_id=user_id, group_id__isnull=True)
) )
else: else:
return await cls.safe_get_or_none(user_id="", group_id=group_id) return await dao.safe_get_or_none(user_id="", group_id=group_id)
@classmethod @classmethod
async def check_ban_level( async def check_ban_level(

View File

@ -7,6 +7,7 @@ from tortoise.backends.base.client import BaseDBAsyncClient
from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.task_info import TaskInfo from zhenxun.models.task_info import TaskInfo
from zhenxun.services.cache import CacheRoot from zhenxun.services.cache import CacheRoot
from zhenxun.services.data_access import DataAccess
from zhenxun.services.db_context import Model from zhenxun.services.db_context import Model
from zhenxun.utils.enum import CacheType, DbLockType, PluginType from zhenxun.utils.enum import CacheType, DbLockType, PluginType
@ -254,13 +255,14 @@ class GroupConsole(Model):
返回: 返回:
Self: GroupConsole Self: GroupConsole
""" """
dao = DataAccess(cls)
if channel_id: if channel_id:
return await cls.safe_get_or_none( return await dao.safe_get_or_none(
group_id=group_id, group_id=group_id,
channel_id=channel_id, channel_id=channel_id,
clean_duplicates=clean_duplicates, clean_duplicates=clean_duplicates,
) )
return await cls.safe_get_or_none( return await dao.safe_get_or_none(
group_id=group_id, group_id=group_id,
channel_id__isnull=True, channel_id__isnull=True,
clean_duplicates=clean_duplicates, clean_duplicates=clean_duplicates,

View File

@ -37,7 +37,7 @@ class CacheDict(Generic[T]):
return 0 return 0
return data.expire_time return data.expire_time
def __getitem__(self, key: str) -> T | None: def __getitem__(self, key: str) -> T:
"""获取字典项 """获取字典项
参数: 参数:
@ -47,8 +47,10 @@ class CacheDict(Generic[T]):
T: 字典值 T: 字典值
""" """
if value := self._data.get(key): if value := self._data.get(key):
return value.value if self.expire_time(key) else None if self.expire_time(key):
return None raise KeyError(f"{key} 已过期")
return value.value
raise KeyError(f"{key} 不存在")
def __setitem__(self, key: str, value: T) -> None: def __setitem__(self, key: str, value: T) -> None:
"""设置字典项 """设置字典项

View File

@ -7,6 +7,8 @@ from zhenxun.services.log import logger
T = TypeVar("T", bound=Model) T = TypeVar("T", bound=Model)
cache = CacheRoot.cache_dict("DB_TEST_BAN", 10, int)
class DataAccess(Generic[T]): class DataAccess(Generic[T]):
"""数据访问层,根据配置决定是否使用缓存 """数据访问层,根据配置决定是否使用缓存
@ -167,6 +169,7 @@ class DataAccess(Generic[T]):
return await with_db_timeout( return await with_db_timeout(
db_query_func(*args, **kwargs), db_query_func(*args, **kwargs),
operation=f"{self.model_cls.__name__}.{db_query_func.__name__}", operation=f"{self.model_cls.__name__}.{db_query_func.__name__}",
source="DataAccess",
) )
# 尝试从缓存获取 # 尝试从缓存获取
@ -179,9 +182,10 @@ class DataAccess(Generic[T]):
if cache_key is not None: if cache_key is not None:
data = await self.cache.get(cache_key) data = await self.cache.get(cache_key)
logger.debug( logger.debug(
f"{self.model_cls.__name__} self.cache.get(cache_key)" f"{self.model_cls.__name__} key: {cache_key}"
f" 从缓存获取到的数据 {type(data)}: {data}" f" 从缓存获取到的数据 {type(data)}: {data}"
) )
if data == self._NULL_RESULT: if data == self._NULL_RESULT:
# 空结果缓存命中 # 空结果缓存命中
self._cache_stats[self.cache_type]["null_hits"] += 1 self._cache_stats[self.cache_type]["null_hits"] += 1

View File

@ -227,6 +227,7 @@ class Model(TortoiseModel):
return await with_db_timeout( return await with_db_timeout(
cls.get_or_none(*args, using_db=using_db, **kwargs), cls.get_or_none(*args, using_db=using_db, **kwargs),
operation=f"{cls.__name__}.get_or_none", operation=f"{cls.__name__}.get_or_none",
source="DataBaseModel",
) )
except MultipleObjectsReturned: except MultipleObjectsReturned:
# 如果出现多个记录的情况,进行特殊处理 # 如果出现多个记录的情况,进行特殊处理
@ -239,6 +240,7 @@ class Model(TortoiseModel):
records = await with_db_timeout( records = await with_db_timeout(
cls.filter(*args, **kwargs).all(), cls.filter(*args, **kwargs).all(),
operation=f"{cls.__name__}.filter.all", operation=f"{cls.__name__}.filter.all",
source="DataBaseModel",
) )
if not records: if not records:
@ -255,6 +257,7 @@ class Model(TortoiseModel):
await with_db_timeout( await with_db_timeout(
record.delete(), record.delete(),
operation=f"{cls.__name__}.delete_duplicate", operation=f"{cls.__name__}.delete_duplicate",
source="DataBaseModel",
) )
logger.info( logger.info(
f"{cls.__name__} 删除重复记录:" f"{cls.__name__} 删除重复记录:"
@ -269,11 +272,13 @@ class Model(TortoiseModel):
return await with_db_timeout( return await with_db_timeout(
cls.filter(*args, **kwargs).order_by("-id").first(), cls.filter(*args, **kwargs).order_by("-id").first(),
operation=f"{cls.__name__}.filter.order_by.first", operation=f"{cls.__name__}.filter.order_by.first",
source="DataBaseModel",
) )
# 如果没有 id 字段,则返回第一个记录 # 如果没有 id 字段,则返回第一个记录
return await with_db_timeout( return await with_db_timeout(
cls.filter(*args, **kwargs).first(), cls.filter(*args, **kwargs).first(),
operation=f"{cls.__name__}.filter.first", operation=f"{cls.__name__}.filter.first",
source="DataBaseModel",
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.error( logger.error(

View File

@ -11,11 +11,15 @@ from .config import (
async def with_db_timeout( async def with_db_timeout(
coro, timeout: float = DB_TIMEOUT_SECONDS, operation: str | None = None coro,
timeout: float = DB_TIMEOUT_SECONDS,
operation: str | None = None,
source: str | None = None,
): ):
"""带超时控制的数据库操作""" """带超时控制的数据库操作"""
start_time = time.time() start_time = time.time()
try: try:
logger.debug(f"开始执行数据库操作: {operation} 来源: {source}")
result = await asyncio.wait_for(coro, timeout=timeout) result = await asyncio.wait_for(coro, timeout=timeout)
elapsed = time.time() - start_time elapsed = time.time() - start_time
if elapsed > SLOW_QUERY_THRESHOLD and operation: if elapsed > SLOW_QUERY_THRESHOLD and operation:

View File

@ -50,6 +50,7 @@ class ZhenxunRepoConfig:
"zhenxun/utils", "zhenxun/utils",
"zhenxun/models", "zhenxun/models",
"zhenxun/configs", "zhenxun/configs",
"zhenxun/ui",
] ]
ZHENXUN_BOT_VERSION_FILE_STRING = "__version__" ZHENXUN_BOT_VERSION_FILE_STRING = "__version__"
ZHENXUN_BOT_VERSION_FILE = Path() / ZHENXUN_BOT_VERSION_FILE_STRING ZHENXUN_BOT_VERSION_FILE = Path() / ZHENXUN_BOT_VERSION_FILE_STRING