"""Redis Pub/Sub 消息通道模块 提供基于 Redis 的发布/订阅机制,用于实时推送提醒消息到 WebSocket 客户端。 主要功能: - publish: 发布提醒消息到指定用户的频道 - subscribe: 订阅用户的提醒频道 - unsubscribe: 取消订阅 依赖: - redis.asyncio: Redis 异步客户端 """ import json import logging from typing import AsyncGenerator, Optional import redis.asyncio as redis from backend.app.core.config import get_settings logger = logging.getLogger(__name__) def _get_redis_url() -> str: """获取 Redis 连接 URL""" settings = get_settings() password_part = f":{settings.redis_password}@" if settings.redis_password else "" return f"redis://{password_part}{settings.redis_host}:{settings.redis_port}/{settings.redis_db}" class ReminderPubSub: """提醒消息的发布/订阅管理器""" CHANNEL_PREFIX = "reminder" def __init__(self): self._redis: Optional[redis.Redis] = None async def get_redis(self) -> redis.Redis: """获取 Redis 连接(懒加载)""" if self._redis is None: try: redis_url = _get_redis_url() self._redis = redis.from_url(redis_url, decode_responses=True) await self._redis.ping() logger.info("Redis Pub/Sub 连接成功") except Exception as e: logger.error(f"Redis Pub/Sub 连接失败: {e}") raise return self._redis def _get_channel(self, user_id: int) -> str: """获取用户专属的频道名称""" return f"{self.CHANNEL_PREFIX}:{user_id}" async def publish(self, user_id: int, reminder: dict) -> bool: """发布提醒消息到指定用户的频道 Args: user_id: 目标用户 ID reminder: 提醒数据字典 Returns: bool: 是否发布成功 """ try: r = await self.get_redis() channel = self._get_channel(user_id) message = json.dumps(reminder, ensure_ascii=False) await r.publish(channel, message) logger.info(f"发布提醒到用户 {user_id}: {reminder.get('title', '')}") return True except Exception as e: logger.error(f"发布提醒失败: {e}") return False async def subscribe(self, user_id: int) -> redis.client.PubSub: """订阅用户的提醒频道 Args: user_id: 用户 ID Returns: PubSub 对象,可用于接收消息 """ r = await self.get_redis() pubsub = r.pubsub() channel = self._get_channel(user_id) await pubsub.subscribe(channel) logger.info(f"用户 {user_id} 订阅频道 {channel}") return pubsub async def unsubscribe(self, pubsub: redis.client.PubSub, user_id: int) -> None: """取消订阅 Args: pubsub: PubSub 对象 user_id: 用户 ID """ try: channel = self._get_channel(user_id) await pubsub.unsubscribe(channel) await pubsub.close() logger.info(f"用户 {user_id} 取消订阅频道 {channel}") except Exception as e: logger.error(f"取消订阅失败: {e}") async def close(self) -> None: """关闭 Redis 连接""" if self._redis: await self._redis.close() self._redis = None # 全局单例 reminder_pubsub = ReminderPubSub()