Files
FastapiAdmin/backend/app/core/event_bus.py
T
zhangtao a222cd9e43 refactor: 移除多租户相关代码,重构为单租户架构
此次提交进行了大规模的架构重构:
1.  移除所有平台租户相关模块和代码,包括租户管理、套餐、订单、发票等功能
2.  将菜单模块从platform迁移到system模块,统一系统功能入口
3.  移除租户隔离相关的模型混入、中间件和配置
4.  简化文件上传、SSE事件总线、定时任务等模块的租户逻辑
5.  重构所有业务schema和模型,移除租户相关字段和关联
6.  清理初始化脚本、模板和常量中的租户相关代码
7.  简化认证和权限控制逻辑,移除数据范围检查相关代码
2026-07-16 23:22:45 +08:00

90 lines
2.7 KiB
Python

"""异步事件总线 — SSE 通知推送的核心组件
职责:
- 维护每个用户的 asyncio.Queue(用户退出后自动清理)
- 提供 publish / subscribe / unsubscribe 接口
使用方:
- SSE 端点 → subscribe / unsubscribe
- 各业务服务 → publish
"""
from __future__ import annotations
import asyncio
import json
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from app.core.logger import logger
@dataclass
class _Subscriber:
"""订阅者信息"""
user_id: int
queue: asyncio.Queue[str] = field(default_factory=lambda: asyncio.Queue(maxsize=256))
class EventBus:
"""异步事件总线(全局单例)"""
_subscribers: dict[int, _Subscriber] = {}
@classmethod
def subscribe(cls, user_id: int) -> asyncio.Queue[str]:
"""为用户创建一个事件队列(已存在则返回现有队列)"""
sub = cls._subscribers.get(user_id)
if sub:
return sub.queue
sub = _Subscriber(user_id=user_id)
cls._subscribers[user_id] = sub
logger.debug(f"SSE 订阅: user_id={user_id}")
return sub.queue
@classmethod
def unsubscribe(cls, user_id: int) -> None:
"""移除用户的事件队列"""
cls._subscribers.pop(user_id, None)
logger.debug(f"SSE 取消订阅: user_id={user_id}")
@classmethod
async def publish(cls, user_id: int, event: dict[str, Any]) -> None:
"""向指定用户推送事件(用户不在线则静默丢弃)"""
sub = cls._subscribers.get(user_id)
if sub is None:
return
payload = _build_sse_payload(event)
try:
await asyncio.wait_for(sub.queue.put(payload), timeout=2)
except (TimeoutError, asyncio.QueueFull):
logger.warning(f"SSE 推送超时或队列满: user_id={user_id}, event={event.get('type')}")
@classmethod
async def publish_all(cls, event: dict[str, Any]) -> None:
"""向所有在线用户广播事件"""
payload = _build_sse_payload(event)
tasks = [_put(sub.queue, payload) for sub in cls._subscribers.values()]
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
@classmethod
def online_count(cls) -> int:
"""当前在线 SSE 连接数"""
return len(cls._subscribers)
def _build_sse_payload(event: dict[str, Any]) -> str:
"""将事件字典序列化为 SSE data 行"""
event.setdefault("timestamp", datetime.now().isoformat())
return json.dumps(event, ensure_ascii=False)
async def _put(queue: asyncio.Queue[str], payload: str) -> None:
try:
await asyncio.wait_for(queue.put(payload), timeout=1)
except (TimeoutError, asyncio.QueueFull):
pass