Add multi level caching and optimize caching (#1054)

* Add multi level caching and optimize caching

* optimize current caching

* improved decorators

* improved local

* add pub/sub

* fix serialize

* Improve pub and sub

* Add comment

* Fix lru_cache maxsize

* Fix lint

* Fix config warmup
This commit is contained in:
Wu Clan
2026-02-02 17:18:49 +08:00
committed by GitHub
parent 093788acea
commit 6f1c27786d
24 changed files with 636 additions and 43 deletions
@@ -7,8 +7,6 @@ from backend.app.admin.crud.crud_dept import dept_dao
from backend.app.admin.model import Dept
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.redis import redis_client
from backend.utils.build_tree import get_tree_data
@@ -115,8 +113,6 @@ class DeptService:
if children:
raise errors.ConflictError(msg='部门下存在子部门,无法删除')
count = await dept_dao.delete(db, pk)
for user in dept.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
+1 -1
View File
@@ -75,7 +75,7 @@ class PluginService:
bacup_dir = PLUGIN_DIR / f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup'
shutil.move(plugin_dir, bacup_dir)
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'true')
@staticmethod
async def update_status(*, plugin: str) -> None:
@@ -57,11 +57,19 @@ class UserPasswordHistoryService:
failure_count = await redis_client.get(f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}')
failure_count = int(failure_count) if failure_count else 0
failure_count += 1
await redis_client.set(f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}', str(failure_count))
await redis_client.setex(
f'{settings.LOGIN_FAILURE_PREFIX}:{user_id}',
settings.USER_LOCK_SECONDS,
str(failure_count),
)
if failure_count >= settings.USER_LOCK_THRESHOLD:
locked_until = timezone.now() + timedelta(seconds=settings.USER_LOCK_SECONDS)
await redis_client.set(f'{settings.USER_LOCK_REDIS_PREFIX}:{user_id}', timezone.to_str(locked_until))
await redis_client.setex(
f'{settings.USER_LOCK_REDIS_PREFIX}:{user_id}',
settings.USER_LOCK_SECONDS,
timezone.to_str(locked_until),
)
raise errors.AuthorizationError(msg='登录失败次数过多,账号已被锁定')
@staticmethod
+2 -1
View File
@@ -212,7 +212,7 @@ class UserService:
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
]
for prefix in key_prefix:
await redis_client.delete(prefix)
await redis_client.delete_prefix(prefix)
return count
@staticmethod
@@ -314,6 +314,7 @@ class UserService:
key_prefix = [
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
]
for key in key_prefix:
await redis_client.delete_prefix(key)
View File
+226
View File
@@ -0,0 +1,226 @@
import functools
from collections.abc import Callable, Sequence
from typing import Any, ParamSpec, TypeVar
from cachebox import make_hash_key
from msgspec import json
from backend.common.cache.local import local_cache_manager
from backend.common.cache.pubsub import cache_pubsub_manager
from backend.common.context import ctx
from backend.common.exception import errors
from backend.common.log import log
from backend.core.conf import settings
from backend.database.redis import redis_client
from backend.utils.serializers import select_columns_serialize, select_list_serialize
P = ParamSpec('P')
T = TypeVar('T')
# 哈希缓存键排除参数
_EXCLUDE_PARAMS = frozenset({'db', 'session', 'self', 'cls', 'request', 'response'})
def build_cache_key(
name: str,
key: str | None,
key_builder: Callable[..., str] | None,
*args: Any,
**kwargs: Any,
) -> str:
"""构建缓存 Key"""
if key_builder:
return f'{name}:{key_builder(*args, **kwargs)}'
if key:
value = kwargs.get(key)
if value is None:
raise errors.ServerError(msg=f'缓存键构建失败,参数 "{key}" 不存在或值为空')
return f'{name}:{value}'
filtered = {k: v for k, v in kwargs.items() if k not in _EXCLUDE_PARAMS and v is not None}
if filtered:
hash_suffix = make_hash_key(*args, **kwargs)
return f'{name}:{hash_suffix}'
return name
def user_key_builder() -> str:
"""基于当前用户 ID 生成缓存 Key"""
user_id = ctx.user_id
if user_id is None:
raise errors.ServerError(msg='用户缓存键构建失败')
return str(user_id)
def _serialize_result(result: Any) -> bytes:
"""
序列化缓存结果
:param result: 需要进行序列化的结果
:return:
"""
# SQLAlchemy 查询表
if hasattr(result, '__table__'):
return json.encode(select_columns_serialize(result))
# SQLAlchemy 查询列表
if (
isinstance(result, Sequence)
and not isinstance(result, (str, bytes))
and len(result) > 0
and hasattr(result[0], '__table__')
):
return json.encode(select_list_serialize(result))
# 基本类型
return json.encode(result)
def _deserialize_result(value: bytes) -> Any:
"""
反序列化缓存结果
:param value: 缓存结果
:return:
"""
try:
return json.decode(value)
except Exception:
return value
def cached( # noqa: C901
name: str,
*,
key: str | None = None,
key_builder: Callable[..., str] | None = None,
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""
缓存装饰器
:param name: 缓存名称(通常为缓存 Key 前缀)
:param key: 从方法参数中获取指定参数名的值作为缓存 Key,与 key_builder 互斥
:param key_builder: 自定义 Key 生成函数,与 key 互斥
:return:
"""
if key is not None and key_builder is not None:
raise errors.ServerError(msg='缓存 key 和 key_builder 不能同时使用')
def decorator(func: Callable[P, T]) -> Callable[P, T]: # noqa: C901
@functools.wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
cache_key = build_cache_key(name, key, key_builder, *args, **kwargs)
# L1: 本地缓存
if settings.CACHE_LOCAL_ENABLED:
local_value = local_cache_manager.get(cache_key)
if local_value is not None:
return local_value
# L2: Redis 缓存
try:
redis_value = await redis_client.get(cache_key)
if redis_value is not None:
result = _deserialize_result(redis_value)
# 回填 L1
if settings.CACHE_LOCAL_ENABLED:
local_cache_manager.set(cache_key, result)
return result
except Exception as e:
log.warning(f'[Cache] GET error: {e}')
# 缓存未命中
result = await func(*args, **kwargs)
if result is not None:
try:
# 回填 L1
if settings.CACHE_LOCAL_ENABLED:
local_cache_manager.set(cache_key, result)
# 回填 L2
serialized_result = _serialize_result(result)
if settings.CACHE_REDIS_TTL:
await redis_client.setex(cache_key, settings.CACHE_REDIS_TTL, serialized_result)
else:
await redis_client.set(cache_key, serialized_result)
except Exception as e:
log.warning(f'[Cache] SET error: {e}')
return result
return wrapper
return decorator
def cache_invalidate( # noqa: C901
name: str,
*,
key: str | None = None,
key_builder: Callable[..., str] | None = None,
atomic: bool = True,
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""
缓存失效装饰器
:param name: 缓存名称(通常为缓存 Key 前缀)
:param key: 从方法参数中获取指定参数名的值作为缓存 Key,与 key_builder 互斥
:param key_builder: 自定义 Key 生成函数,与 key 互斥
:param atomic: 是否保证缓存原子性
:return:
"""
if key is not None and key_builder is not None:
raise errors.ServerError(msg='缓存 key 和 key_builder 不能同时使用')
def decorator(func: Callable[P, T]) -> Callable[P, T]:
@functools.wraps(func)
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
result = await func(*args, **kwargs)
# 尝试失效缓存
invalidate_success = False
invalidate_error = None
try:
invalidate_key = build_cache_key(name, key, key_builder, *args, **kwargs)
# L2 缓存失效
if invalidate_key == name:
await redis_client.delete(invalidate_key)
else:
await redis_client.delete_prefix(invalidate_key)
# L1 缓存失效
if settings.CACHE_LOCAL_ENABLED:
if invalidate_key == name:
local_cache_manager.delete(invalidate_key)
else:
local_cache_manager.delete_prefix(invalidate_key)
# 广播失效消息(通知其他节点清除本地缓存)
if settings.CACHE_LOCAL_ENABLED:
if invalidate_key == name:
await cache_pubsub_manager.publish_invalidation(invalidate_key)
else:
await cache_pubsub_manager.publish_invalidation(invalidate_key, is_delete_prefix=True)
except Exception as e:
log.error(f'[Cache] INVALIDATE error: {e}')
invalidate_error = e
else:
invalidate_success = True
# 原子性检查
if atomic and not invalidate_success:
raise errors.ServerError(msg='缓存失效失败,数据可能不一致', data=invalidate_error)
return result
return wrapper
return decorator
+56
View File
@@ -0,0 +1,56 @@
from typing import Any
import cachebox
from backend.core.conf import settings
class LocalCacheManager:
"""本地缓存管理器"""
def __init__(self) -> None:
self.hot_cache: cachebox.TTLCache = cachebox.TTLCache(
settings.CACHE_LOCAL_MAXSIZE, ttl=settings.CACHE_LOCAL_TTL
)
def get(self, key: str) -> Any:
"""获取缓存"""
try:
return self.hot_cache[key]
except KeyError:
return None
def set(self, key: str, value: Any) -> None:
"""设置缓存"""
self.hot_cache[key] = value
def delete(self, key: str) -> bool:
"""删除缓存"""
try:
del self.hot_cache[key]
except KeyError:
return False
return True
def clear(self) -> None:
"""清空缓存"""
self.hot_cache.clear()
def delete_prefix(self, prefix: str, exclude: str | list[str] | None = None) -> None:
"""
删除指定前缀的缓存
:param prefix: 要删除的键前缀
:param exclude: 要排除的键或键列表
:return:
"""
exclude_set = set(exclude) if isinstance(exclude, list) else {exclude} if isinstance(exclude, str) else set()
for key in list(self.hot_cache.keys()):
if key.startswith(prefix) and key not in exclude_set:
try:
del self.hot_cache[key]
except KeyError:
pass
local_cache_manager = LocalCacheManager()
+112
View File
@@ -0,0 +1,112 @@
import asyncio
import json
from backend.common.cache.local import local_cache_manager
from backend.common.log import log
from backend.core.conf import settings
from backend.database.redis import RedisCli, redis_client
class CachePubSubManager:
"""缓存 Pub/Sub 管理器"""
_pubsub_task: asyncio.Task | None = None
@staticmethod
async def publish_invalidation(key: str, *, is_delete_prefix: bool) -> None:
"""
发布缓存失效通知
:param key: 缓存键
:param is_delete_prefix: 是否删除符合前缀的所有缓存
:return:
"""
try:
message = json.dumps({'key': key, 'is_delete_prefix': is_delete_prefix})
await redis_client.publish(settings.CACHE_PUBSUB_CHANNEL, message)
except Exception as e:
log.warning(f'[CachePubSub] 发布通知失败: {e}')
@staticmethod
async def subscribe_and_listen() -> None: # noqa: C901
"""订阅并监听缓存失效通知"""
reconnect_attempts = 0
while reconnect_attempts < settings.CACHE_PUBSUB_MAX_RECONNECT_ATTEMPTS:
pubsub_client: RedisCli | None = None
pubsub = None
try:
# 使用独立连接
pubsub_client = RedisCli()
pubsub = pubsub_client.pubsub()
await pubsub.subscribe(settings.CACHE_PUBSUB_CHANNEL)
# 发布订阅成功
reconnect_attempts = 0
async for message in pubsub.listen():
if message['type'] == 'message':
try:
data = json.loads(message['data'])
key = data['key']
if not data['is_delete_prefix']:
local_cache_manager.delete(key)
else:
local_cache_manager.delete_prefix(key)
except json.JSONDecodeError as e:
log.warning(f'[CachePubSub] 消息格式错误 {e}')
except Exception as e:
log.error(f'[CachePubSub] 处理通知失败: {e}')
except asyncio.CancelledError:
break
except Exception as e:
reconnect_attempts += 1
log.error(
f'[CachePubSub] 订阅异常 ({reconnect_attempts}/{settings.CACHE_PUBSUB_MAX_RECONNECT_ATTEMPTS}): {e}'
)
if reconnect_attempts >= settings.CACHE_PUBSUB_MAX_RECONNECT_ATTEMPTS:
log.error('[CachePubSub] 达到最大重连次数,停止订阅')
break
await asyncio.sleep(settings.CACHE_PUBSUB_RECONNECT_DELAY)
finally:
if pubsub_client:
try:
await pubsub_client.aclose()
except Exception:
pass
if pubsub:
try:
await pubsub.aclose()
except Exception:
pass
@classmethod
def start_listener(cls) -> None:
"""启动缓存 Pub/Sub 监听器"""
if not settings.CACHE_LOCAL_ENABLED:
return
if cls._pubsub_task is None or cls._pubsub_task.done():
cls._pubsub_task = asyncio.create_task(cls.subscribe_and_listen())
@classmethod
async def stop_listener(cls) -> None:
"""停止缓存 Pub/Sub 监听器"""
if cls._pubsub_task is None:
return
if not cls._pubsub_task.done():
cls._pubsub_task.cancel()
try:
await cls._pubsub_task
except asyncio.CancelledError:
pass
cls._pubsub_task = None
cache_pubsub_manager = CachePubSubManager()
+36
View File
@@ -0,0 +1,36 @@
from backend.common.log import log
from backend.database.db import async_db_session
from backend.plugin.config.enums import ConfigType
async def cache_warmup() -> None:
"""缓存预热"""
await _warmup_config()
await _warmup_dict()
async def _warmup_config() -> None:
"""预热参数配置缓存"""
try:
from backend.plugin.config.service.config_service import config_service
async with async_db_session() as db:
for type in ConfigType.get_member_values():
await config_service.get_all(db=db, type=type)
except ImportError:
pass
except Exception as e:
log.warning(f'[Warmup] 参数配置缓存预热失败: {e}')
async def _warmup_dict() -> None:
"""预热数据字典缓存"""
try:
from backend.plugin.dict.service.dict_data_service import dict_data_service
async with async_db_session() as db:
await dict_data_service.get_all(db=db)
except ImportError:
pass
except Exception as e:
log.warning(f'[Warmup] 数据字典缓存预热失败: {e}')
+2
View File
@@ -21,6 +21,8 @@ class TypedContextProtocol(Protocol):
permission: str | None
language: str
user_id: int | None
class TypedContext(TypedContextProtocol, _Context):
def __getattr__(self, name: str) -> Any:
+2 -1
View File
@@ -40,7 +40,8 @@ class RequestPermission:
if settings.RBAC_ROLE_MENU_MODE:
if not isinstance(self.value, str):
raise errors.ServerError
# 附加权限标识到请求状态
# 设置权限标识到上下文
ctx.permission = self.value
+14 -2
View File
@@ -1,6 +1,6 @@
import shutil
from functools import lru_cache
from functools import cache
from re import Pattern
from typing import Any, Literal
@@ -68,6 +68,17 @@ class Settings(BaseSettings):
# Redis
REDIS_TIMEOUT: int = 5
# 缓存
CACHE_LOCAL_ENABLED: bool = True
CACHE_LOCAL_MAXSIZE: int = 100000
CACHE_LOCAL_TTL: int = 60 * 60 * 2 # 2 小时
CACHE_REDIS_TTL: int = 60 * 60 * 2 # 2 小时
CACHE_CONFIG_REDIS_PREFIX: str = 'fba:cache:config'
CACHE_DICT_REDIS_PREFIX: str = 'fba:cache:dict'
CACHE_PUBSUB_CHANNEL: str = 'fba:cache:invalidate'
CACHE_PUBSUB_RECONNECT_DELAY: int = 5 # 重连延迟(秒)
CACHE_PUBSUB_MAX_RECONNECT_ATTEMPTS: int = 10 # 最大重连次数
# .env Snowflake
SNOWFLAKE_DATACENTER_ID: int | None = None
SNOWFLAKE_WORKER_ID: int | None = None
@@ -213,6 +224,7 @@ class Settings(BaseSettings):
'new_password',
'confirm_password',
]
OPERA_LOG_QUEUE_MAXSIZE: int = 100000
OPERA_LOG_QUEUE_BATCH_CONSUME_SIZE: int = 100
OPERA_LOG_QUEUE_TIMEOUT: int = 60 # 1 分钟
@@ -303,7 +315,7 @@ class Settings(BaseSettings):
return values
@lru_cache
@cache
def get_settings() -> Settings:
"""获取全局配置单例"""
if not ENV_FILE_PATH.exists():
+11
View File
@@ -17,6 +17,8 @@ from starlette_context.middleware import ContextMiddleware
from starlette_context.plugins import RequestIdPlugin
from backend import __version__
from backend.common.cache.pubsub import cache_pubsub_manager
from backend.common.cache.warmup import cache_warmup
from backend.common.exception.exception_handler import register_exception
from backend.common.log import set_custom_logfile, setup_logging
from backend.common.response.response_code import StandardResponseCode
@@ -66,8 +68,17 @@ async def register_init(app: FastAPI) -> AsyncGenerator[None, None]:
# 创建操作日志任务
create_task(OperaLogMiddleware.consumer())
# 缓存预热
await cache_warmup()
# 启动缓存 Pub/Sub 监听器
cache_pubsub_manager.start_listener()
yield
# 停止缓存 Pub/Sub 监听器
await cache_pubsub_manager.stop_listener()
# 释放 snowflake 节点
await snowflake.shutdown()
@@ -7,6 +7,7 @@ from starlette.authentication import AuthenticationError as StarletteAuthenticat
from starlette.requests import HTTPConnection
from backend.app.admin.schema.user import GetUserInfoWithRelationDetail
from backend.common.context import ctx
from backend.common.exception.errors import TokenError
from backend.common.log import log
from backend.common.security.jwt import jwt_authentication
@@ -95,6 +96,9 @@ class JwtAuthMiddleware(AuthenticationBackend):
log.exception(f'JWT 授权异常:{e}')
raise AuthenticationError(code=getattr(e, 'code', 500), msg=getattr(e, 'msg', 'Internal Server Error'))
# 设置用户 ID 到上下文
ctx.user_id = user.id
# 请注意,此返回使用非标准模式,所以在认证通过时,将丢失某些标准特性
# 标准返回模式请查看:https://www.starlette.io/authentication/
return AuthCredentials(['authenticated']), user
+1 -1
View File
@@ -31,7 +31,7 @@ from backend.utils.trace_id import get_request_trace_id
class OperaLogMiddleware(BaseHTTPMiddleware):
"""操作日志中间件"""
opera_log_queue: Queue = Queue(maxsize=100000)
opera_log_queue: Queue = Queue(maxsize=settings.OPERA_LOG_QUEUE_MAXSIZE)
async def dispatch(self, request: Request, call_next: Any) -> Response:
"""
@@ -3,8 +3,10 @@ from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from backend.common.cache.decorator import cache_invalidate, cached
from backend.common.exception import errors
from backend.common.pagination import paging_data
from backend.core.conf import settings
from backend.plugin.config.crud.crud_config import config_dao
from backend.plugin.config.model import Config
from backend.plugin.config.schema.config import (
@@ -26,13 +28,16 @@ class ConfigService:
:param pk: 参数配置 ID
:return:
"""
config = await config_dao.get(db, pk)
if not config:
raise errors.NotFoundError(msg='参数配置不存在')
return config
@staticmethod
@cached(
settings.CACHE_CONFIG_REDIS_PREFIX,
key_builder=lambda *, db, type: f'type:{type}',
)
async def get_all(*, db: AsyncSession, type: str | None) -> Sequence[Config | None]:
"""
获取所有参数配置
@@ -41,7 +46,6 @@ class ConfigService:
:param type: 参数配置类型
:return:
"""
return await config_dao.get_all(db, type)
@staticmethod
@@ -58,6 +62,7 @@ class ConfigService:
return await paging_data(db, config_select)
@staticmethod
@cache_invalidate(settings.CACHE_CONFIG_REDIS_PREFIX)
async def create(*, db: AsyncSession, obj: CreateConfigParam) -> None:
"""
创建参数配置
@@ -66,13 +71,13 @@ class ConfigService:
:param obj: 参数配置创建参数
:return:
"""
config = await config_dao.get_by_key(db, obj.key)
if config:
raise errors.ConflictError(msg=f'参数配置 {obj.key} 已存在')
await config_dao.create(db, obj)
@staticmethod
@cache_invalidate(settings.CACHE_CONFIG_REDIS_PREFIX)
async def update(*, db: AsyncSession, pk: int, obj: UpdateConfigParam) -> int:
"""
更新参数配置
@@ -82,7 +87,6 @@ class ConfigService:
:param obj: 参数配置更新参数
:return:
"""
config = await config_dao.get(db, pk)
if not config:
raise errors.NotFoundError(msg='参数配置不存在')
@@ -94,6 +98,7 @@ class ConfigService:
return count
@staticmethod
@cache_invalidate(settings.CACHE_CONFIG_REDIS_PREFIX)
async def bulk_update(*, db: AsyncSession, objs: list[UpdateConfigsParam]) -> int:
"""
批量更新参数配置
@@ -102,7 +107,6 @@ class ConfigService:
:param objs: 参数配置批量更新参数
:return:
"""
for _batch in range(0, len(objs), 1000):
for obj in objs:
config = await config_dao.get(db, obj.id)
@@ -116,6 +120,7 @@ class ConfigService:
return count
@staticmethod
@cache_invalidate(settings.CACHE_CONFIG_REDIS_PREFIX)
async def delete(*, db: AsyncSession, pks: list[int]) -> int:
"""
批量删除参数配置
@@ -124,7 +129,6 @@ class ConfigService:
:param pks: 参数配置 ID 列表
:return:
"""
count = await config_dao.delete(db, pks)
return count
+4 -4
View File
@@ -22,8 +22,8 @@ from backend.utils.async_helper import run_await
from backend.utils.dynamic_import import get_model_objects, import_module_cached
@lru_cache
def get_plugins() -> list[str]:
@lru_cache(maxsize=128)
def get_plugins() -> tuple[str, ...]:
"""获取插件列表"""
plugin_packages = []
@@ -37,7 +37,7 @@ def get_plugins() -> list[str]:
if os.path.isdir(item_path) and '__init__.py' in os.listdir(item_path):
plugin_packages.append(item)
return plugin_packages
return tuple(plugin_packages)
def get_plugin_models() -> list[object]:
@@ -104,7 +104,7 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
plugins = get_plugins()
# 使用独立单例,避免与主线程冲突
# 使用独立连接
current_redis_client = RedisCli()
run_await(current_redis_client.init)()
@@ -3,8 +3,10 @@ from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from backend.common.cache.decorator import cache_invalidate, cached
from backend.common.exception import errors
from backend.common.pagination import paging_data
from backend.core.conf import settings
from backend.plugin.dict.crud.crud_dict_data import dict_data_dao
from backend.plugin.dict.crud.crud_dict_type import dict_type_dao
from backend.plugin.dict.model import DictData
@@ -23,13 +25,16 @@ class DictDataService:
:param pk: 字典数据 ID
:return:
"""
dict_data = await dict_data_dao.get(db, pk)
if not dict_data:
raise errors.NotFoundError(msg='字典数据不存在')
return dict_data
@staticmethod
@cached(
settings.CACHE_DICT_REDIS_PREFIX,
key_builder=lambda *, db, code: f'type:{code}',
)
async def get_by_type_code(*, db: AsyncSession, code: str) -> Sequence[DictData]:
"""
获取字典数据详情
@@ -38,13 +43,16 @@ class DictDataService:
:param code: 字典类型编码
:return:
"""
dict_datas = await dict_data_dao.get_by_type_code(db, code)
if not dict_datas:
raise errors.NotFoundError(msg='字典数据不存在')
return dict_datas
@staticmethod
@cached(
settings.CACHE_DICT_REDIS_PREFIX,
key_builder=lambda *, db: 'all',
)
async def get_all(*, db: AsyncSession) -> Sequence[DictData]:
"""
获取所有字典数据
@@ -86,6 +94,7 @@ class DictDataService:
return await paging_data(db, dict_data_select)
@staticmethod
@cache_invalidate(settings.CACHE_DICT_REDIS_PREFIX)
async def create(*, db: AsyncSession, obj: CreateDictDataParam) -> None:
"""
创建字典数据
@@ -103,6 +112,7 @@ class DictDataService:
await dict_data_dao.create(db, obj, dict_type.code)
@staticmethod
@cache_invalidate(settings.CACHE_DICT_REDIS_PREFIX)
async def update(*, db: AsyncSession, pk: int, obj: UpdateDictDataParam) -> int:
"""
更新字典数据
@@ -112,7 +122,6 @@ class DictDataService:
:param obj: 字典数据更新参数
:return:
"""
dict_data = await dict_data_dao.get(db, pk)
if not dict_data:
raise errors.NotFoundError(msg='字典数据不存在')
@@ -127,6 +136,7 @@ class DictDataService:
return count
@staticmethod
@cache_invalidate(settings.CACHE_DICT_REDIS_PREFIX)
async def delete(*, db: AsyncSession, obj: DeleteDictDataParam) -> int:
"""
批量删除字典数据
@@ -135,7 +145,6 @@ class DictDataService:
:param obj: 字典数据 ID 列表
:return:
"""
count = await dict_data_dao.delete(db, obj.pks)
return count
+2 -2
View File
@@ -104,7 +104,7 @@ async def install_zip_plugin(file: UploadFile | str) -> str:
await _append_env_example(full_plugin_path)
await install_requirements_async(plugin_dir_name)
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'true')
return plugin_name
@@ -133,6 +133,6 @@ async def install_git_plugin(repo_url: str) -> str:
await _append_env_example(path)
await install_requirements_async(repo_name)
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'true')
return repo_name
+1 -11
View File
@@ -9,20 +9,10 @@ from starlette.concurrency import run_in_threadpool
from backend.core.conf import settings
from backend.core.path_conf import PLUGIN_DIR
from backend.plugin.core import get_plugins
from backend.plugin.errors import PluginInstallError
def get_plugins() -> list[str]:
"""
获取插件列表
注意此函数从 backend.plugin.core 导入以避免循环依赖
"""
from backend.plugin.core import get_plugins as _get_plugins
return _get_plugins()
def _is_in_virtualenv() -> bool:
"""检测当前是否在虚拟环境中运行"""
return hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
+4 -4
View File
@@ -13,7 +13,7 @@ from backend.common.log import log
T = TypeVar('T')
@lru_cache(maxsize=512)
@lru_cache(maxsize=128)
def import_module_cached(module_path: str) -> Any:
"""
缓存导入模块
@@ -84,9 +84,9 @@ def get_app_models() -> list[object]:
return objs
@lru_cache
def get_all_models() -> list[object]:
@lru_cache(256)
def get_all_models() -> tuple[object, ...]:
"""获取所有模型类"""
from backend.plugin.core import get_plugin_models
return get_app_models() + get_plugin_models()
return tuple(get_app_models() + get_plugin_models())