# backend/app/api/v2/websocket.py """ WebSocket API 接口 支持连接状态查询、订阅管理、推送统计 """ from typing import Optional from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, Query, HTTPException from sqlalchemy.orm import Session from app.db.base import get_db from app.middleware.auth import get_current_user from app.models.user import User from app.websocket.connection_manager import connection_manager, websocket_handler from app.services.push_service import push_service from app.services.auth_service import verify_token import logging logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/v2/ws", tags=["WebSocket 服务"]) # ============== WebSocket 连接路由 ============== @router.websocket("/quote") async def websocket_quote_endpoint( websocket: WebSocket, token: str = Query(..., description="认证 Token"), db: Session = Depends(get_db) ): """ WebSocket 行情推送连接 连接地址: WS /api/v2/ws/quote?token={token} 客户端操作: - subscribe: 订阅品种 {"action": "subscribe", "symbols": ["IF2406"]} - unsubscribe: 取消订阅 {"action": "unsubscribe", "symbols": ["IF2406"]} - heartbeat: 心跳 {"action": "heartbeat"} - query: 查询订阅 {"action": "query"} 服务端推送: - quote: 行情推送 {"type": "quote", "symbol": "IF2406", "data": {...}} - kline: K 线推送 {"type": "kline", "symbol": "IF2406", "period": "1m", "data": {...}} - system: 系统消息 {"type": "system", "event": "connected", ...} """ # 认证 user = await verify_token(token) if not user: await websocket.close(code=4001, reason="认证失败") return # 处理 WebSocket 消息 await websocket_handler(websocket, user.id) # ============== HTTP API(WebSocket 管理) ============== @router.get("/connections", summary="查询连接统计") async def get_connection_statistics( current_user: User = Depends(get_current_user) ): """ 查询 WebSocket 连接统计 包括连接数、用户数、订阅数、消息数等 """ try: stats = connection_manager.get_statistics() return { "connections": stats, "push_service": push_service.get_statistics() } except Exception as e: logger.error(f"❌ 查询连接统计失败: {e}") raise HTTPException(status_code=500, detail=f"查询连接统计失败: {str(e)}") @router.get("/user/{user_id}/subscriptions", summary="查询用户订阅") async def get_user_subscriptions( user_id: int, current_user: User = Depends(get_current_user) ): """ 查询用户的 WebSocket 订阅列表 仅管理员或用户本人可查询 """ try: # 权限检查 if current_user.id != user_id and current_user.role != "admin": raise HTTPException(status_code=403, detail="无权限查询其他用户的订阅") subscriptions = connection_manager.get_user_subscriptions(user_id) return { "user_id": user_id, "subscriptions": subscriptions, "count": len(subscriptions) } except HTTPException: raise except Exception as e: logger.error(f"❌ 查询用户订阅失败: {e}") raise HTTPException(status_code=500, detail=f"查询用户订阅失败: {str(e)}") @router.get("/symbol/{symbol}/subscribers", summary="查询品种订阅用户") async def get_symbol_subscribers( symbol: str, current_user: User = Depends(get_current_user) ): """ 查询订阅某品种的用户列表 仅管理员可查询 """ try: # 权限检查 if current_user.role != "admin": raise HTTPException(status_code=403, detail="无权限查询品种订阅用户") stats = connection_manager.get_statistics() symbol_subscribers = stats.get("symbol_subscribers", {}) user_count = symbol_subscribers.get(symbol, 0) return { "symbol": symbol, "subscriber_count": user_count } except HTTPException: raise except Exception as e: logger.error(f"❌ 查询品种订阅用户失败: {e}") raise HTTPException(status_code=500, detail=f"查询品种订阅用户失败: {str(e)}") @router.post("/broadcast", summary="广播系统消息") async def broadcast_system_message( message: dict, current_user: User = Depends(get_current_user) ): """ 广播系统消息 仅管理员可操作 """ try: # 权限检查 if current_user.role != "admin": raise HTTPException(status_code=403, detail="无权限广播消息") # 广播消息 await connection_manager.broadcast({ "type": "system", "event": "broadcast", "message": message, "time": datetime.now().isoformat() }) logger.info(f"✅ 管理员 {current_user.id} 广播消息: {message}") return {"status": "success", "message": "消息已广播"} except HTTPException: raise except Exception as e: logger.error(f"❌ 广播消息失败: {e}") raise HTTPException(status_code=500, detail=f"广播消息失败: {str(e)}") @router.post("/publish/quote", summary="发布行情更新") async def publish_quote_update( symbol: str = Query(..., description="品种代码"), quote_data: dict = None, current_user: User = Depends(get_current_user) ): """ 发布行情更新(手动触发) 仅管理员可操作 """ try: # 权限检查 if current_user.role != "admin": raise HTTPException(status_code=403, detail="无权限发布行情") # 发布行情 await push_service.publish_quote(symbol, quote_data) logger.info(f"✅ 管理员 {current_user.id} 发布行情: {symbol}") return {"status": "success", "message": f"行情已发布: {symbol}"} except HTTPException: raise except Exception as e: logger.error(f"❌ 发布行情失败: {e}") raise HTTPException(status_code=500, detail=f"发布行情失败: {str(e)}") # ============== 导入依赖 ============== from datetime import datetime