feat(channel): 新增缓存绑定仓储实现

新增带缓存的渠道绑定仓储类,通过装饰代理仓储实现缓存读写、失效逻辑,缓存失效时间默认60秒,空缓存10秒
This commit is contained in:
Kris 2026-05-31 22:19:38 +08:00
commit cf723fcc44

View File

@ -0,0 +1,128 @@
from __future__ import annotations
import json
import logging
from yuxi.channel.domain.exception.cache_error import CacheError
from yuxi.channel.domain.model.binding.channel_binding import ChannelBinding
from yuxi.channel.domain.port import CachePort
from yuxi.channel.domain.repository.binding_repository import BindingRepositoryPort
logger = logging.getLogger(__name__)
_CACHE_TTL = 60
_NULL_CACHE_TTL = 10
_NULL_MARKER = json.dumps({"__null__": True})
_CACHE_VERSION = "v2"
class CachingBindingRepository(BindingRepositoryPort):
def __init__(self, delegate: BindingRepositoryPort, cache_port: CachePort):
self._delegate = delegate
self._cache = cache_port
def _cache_key(self, channel_type: str, account_id: str, group_id: str) -> str:
return f"channel:binding:{_CACHE_VERSION}:{channel_type}:{account_id}:{group_id}"
async def find_active_binding(self, *, channel_type: str, account_id: str, group_id: str) -> ChannelBinding | None:
try:
cache_key = self._cache_key(channel_type, account_id, group_id)
cached = await self._cache.get(cache_key)
if cached:
data = json.loads(cached)
if data.get("__null__"):
return None
return ChannelBinding(
id=data.get("id", 0),
channel_type=data.get("channel_type", ""),
account_id=data.get("account_id", ""),
group_id=data.get("group_id", ""),
agent_config_id=data.get("agent_config_id", 0),
session_key_strategy=data.get("session_key_strategy", "auto"),
is_enabled=data.get("is_enabled", True),
created_by=data.get("created_by"),
updated_by=data.get("updated_by"),
created_at=data.get("created_at"),
updated_at=data.get("updated_at"),
)
except CacheError:
logger.warning("Redis cache read failed, falling back to DB")
except (json.JSONDecodeError, KeyError, TypeError):
logger.warning("Cache data corrupted for key %s:%s:%s, falling back to DB", channel_type, account_id, group_id)
result = await self._delegate.find_active_binding(
channel_type=channel_type, account_id=account_id, group_id=group_id
)
try:
cache_key = self._cache_key(channel_type, account_id, group_id)
if result is None:
await self._cache.set(cache_key, _NULL_MARKER, ex=_NULL_CACHE_TTL)
else:
await self._cache.set(
cache_key,
json.dumps(result.__dict__, ensure_ascii=False, default=str),
ex=_CACHE_TTL,
)
except CacheError:
logger.warning("Redis cache write failed, skipping cache")
return result
async def create_binding(
self,
*,
channel_type: str,
account_id: str,
group_id: str,
agent_config_id: int,
created_by: str | None = None,
) -> ChannelBinding:
result = await self._delegate.create_binding(
channel_type=channel_type,
account_id=account_id,
group_id=group_id,
agent_config_id=agent_config_id,
created_by=created_by,
)
await self._invalidate(channel_type, account_id, group_id)
return result
async def update_binding(
self,
binding_id: int,
*,
agent_config_id: int | None = None,
is_enabled: bool | None = None,
updated_by: str | None = None,
) -> ChannelBinding | None:
result = await self._delegate.update_binding(
binding_id,
agent_config_id=agent_config_id,
is_enabled=is_enabled,
updated_by=updated_by,
)
if result:
await self._invalidate(result.channel_type, result.account_id, result.group_id)
return result
async def delete_binding(self, binding_id: int, *, updated_by: str | None = None) -> ChannelBinding | None:
result = await self._delegate.delete_binding(binding_id, updated_by=updated_by)
if result:
await self._invalidate(result.channel_type, result.account_id, result.group_id)
return result
async def get_binding(self, binding_id: int) -> ChannelBinding | None:
return await self._delegate.get_binding(binding_id)
async def list_bindings(
self, *, channel_type: str | None = None, offset: int = 0, limit: int = 50
) -> tuple[list[ChannelBinding], int]:
return await self._delegate.list_bindings(channel_type=channel_type, offset=offset, limit=limit)
async def _invalidate(self, channel_type: str, account_id: str, group_id: str) -> None:
try:
cache_key = self._cache_key(channel_type, account_id, group_id)
await self._cache.delete(cache_key)
except CacheError:
logger.warning("Redis cache invalidation failed, cache will expire via TTL")