mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 05:02:57 +00:00
此次提交进行了大规模的架构重构: 1. 移除所有平台租户相关模块和代码,包括租户管理、套餐、订单、发票等功能 2. 将菜单模块从platform迁移到system模块,统一系统功能入口 3. 移除租户隔离相关的模型混入、中间件和配置 4. 简化文件上传、SSE事件总线、定时任务等模块的租户逻辑 5. 重构所有业务schema和模型,移除租户相关字段和关联 6. 清理初始化脚本、模板和常量中的租户相关代码 7. 简化认证和权限控制逻辑,移除数据范围检查相关代码
90 lines
2.7 KiB
Python
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
|