Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"python.analysis.autoImportCompletions": false
"python.analysis.autoImportCompletions": true
}
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
- 🔍 时间范围审计日志: 从时间范围获取交易记录
- 🚀 导出数据: 支持从 Json 文件导入/导出到 Json 文件
- 🔧 依赖注入: 支持依赖注入模式调用
- ⚡️ 高性能: LRU淘汰策略应用层缓存

### 快速开始

Expand Down Expand Up @@ -74,6 +75,12 @@ plugins = ["nonebot_plugin_value"]

### [API Docs](https://docs.suggar.top/project/value/docs/api)

### 配置项

```dotenv
VALUE_PRE_BUILD_CACHE = true # 是否在启动时预构建缓存
```

### 更新迁移

1. 升级 nonebot_plugin_value 到最新版本
Expand Down
11 changes: 9 additions & 2 deletions nonebot_plugin_value/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from nonebot import get_driver
from nonebot import get_driver, get_plugin_config, logger
from nonebot.plugin import PluginMetadata, require

require("nonebot_plugin_orm")
require("nonebot_plugin_localstore")

from .api import api_balance, api_currency, api_transaction
from .api.api_currency import get_or_create_currency
from .api.api_currency import get_or_create_currency, list_currencies
from .api.depends import factory
from .config import Config
from .hook import context, hooks_manager, hooks_type
from .models import currency
from .pyd_models import balance_pyd, base_pyd, currency_pyd
Expand Down Expand Up @@ -44,3 +45,9 @@ async def init_db():
初始化数据库
"""
await get_or_create_currency(CurrencyData(id=DEFAULT_CURRENCY_UUID.hex))
if get_plugin_config(Config).value_pre_build_cache:
logger.info("正在初始化缓存...")
logger.info("正在初始化货币缓存...")
await list_currencies()
logger.info("正在初始化账户缓存...")
await api_balance.list_accounts()
151 changes: 151 additions & 0 deletions nonebot_plugin_value/_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from __future__ import annotations

import enum
from asyncio import Lock
from collections import OrderedDict
from collections.abc import Hashable
from dataclasses import dataclass, field
from functools import lru_cache
from typing import Any, Generic, TypeVar

from typing_extensions import Self

from .pyd_models.balance_pyd import UserAccountData
from .pyd_models.base_pyd import BaseData
from .pyd_models.currency_pyd import CurrencyData
from .pyd_models.transaction_pyd import TransactionData


class CacheCategoryEnum(str, enum.Enum):
CURRENCY = "currency"
ACCOUNT = "account"
TRANSACTION = "transaction"


T = TypeVar("T", BaseData, CurrencyData, TransactionData, UserAccountData)


@dataclass
class Cache(Generic[T]):
"""Cache存储模型"""

# 默认缓存最大条目数
max_size: int = 1000

# LRU实现
_cache: OrderedDict[str, BaseData] = field(default_factory=lambda: OrderedDict())

def __post_init__(self):
if self.max_size <= 0:
self.max_size = 1000

async def update(self, *, data: BaseData) -> bool:
data_id = data.uni_id if isinstance(data, UserAccountData) else data.id
async with self._lock(data_id):
if existing := self._cache.get(data_id):
existing.model_validate(data, from_attributes=True)
self._cache.move_to_end(data_id)
return True

# 添加新数据
self._cache[data_id] = data
self._cache.move_to_end(data_id)

# 如果超出最大大小,删除最久未使用的项(第一个)
if len(self._cache) > self.max_size:
self._cache.popitem(last=False)

return False

async def get(self, *, data_id: str) -> BaseData | None:
async with self._lock(data_id):
item = self._cache.get(data_id)
if item is not None:
# 访问后移到末尾(标记为最近使用)
self._cache.move_to_end(data_id)
return item

async def get_all(self) -> list[BaseData]:
async with self._lock():
# 返回所有缓存项的副本
return list(self._cache.values())

async def delete(self, *, data_id: str):
async with self._lock(data_id):
self._cache.pop(data_id, None)

async def clear(self):
async with self._lock(0):
self._cache.clear()

@staticmethod
@lru_cache(1024)
def _lock(*args: Hashable) -> Lock:
return Lock()


class CacheManager:
_instance = None
_cached: dict[CacheCategoryEnum, Cache[Any]]

def __new__(cls) -> Self:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._cached = {}
return cls._instance

async def get_cache(
self, category: CacheCategoryEnum, max_size: int = 1000
) -> Cache[Any]:
# 为不同类别创建具有不同大小的缓存
if category not in self._cached:
self._cached[category] = Cache(max_size=max_size)
return self._cached[category]

async def update_cache(
self,
*,
category: CacheCategoryEnum,
data: BaseData,
max_size: int = 1000,
) -> Self:
"""更新缓存

Args:
category (CacheCategoryEnum): 缓存板块
data (BaseData): 数据
max_size (int): 缓存最大大小
"""
async with self._get_lock(category):
cache = await self.get_cache(category, max_size)
await cache.update(data=data)
return self

async def expire_cache(
self, *, category: CacheCategoryEnum, data_id: str | None = None
) -> Self:
"""使缓存过期(当数据库操作中该条删除时使用)

Args:
category (CacheCategoryEnum): 缓存板块
data_id (str | None, optional): 数据ID. Defaults to None.
"""
async with self._get_lock(category):
if category in self._cached:
if data_id is not None:
cache = await self.get_cache(category)
await cache.delete(data_id=data_id)
else:
self._cached.pop(category, None)
return self

async def expire_all_cache(self) -> Self:
"""使所有缓存过期"""

self._cached.clear()
return self

@staticmethod
@lru_cache(1024)
def _get_lock(*args: Hashable) -> Lock:
return Lock()
Loading
Loading