dingdanquanliucheng/backend/app/core/pubsub.py
2026-06-16 21:42:48 +08:00

118 lines
3.5 KiB
Python

"""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()