from fastapi import APIRouter, Depends, Query from app.core.auth import get_current_admin_user from app.core.response import create_api_response from app.core.database import get_db_connection from app.services.jwt_service import jwt_service from app.core.config import AUDIO_DIR, REDIS_CONFIG from datetime import datetime from typing import Dict, List import os import redis router = APIRouter() # Redis 客户端 redis_client = redis.Redis(**REDIS_CONFIG) # 常量定义 AUDIO_FILE_EXTENSIONS = ('.wav', '.mp3', '.m4a', '.aac', '.flac', '.ogg') BYTES_TO_GB = 1024 ** 3 def _build_status_condition(status: str) -> str: """构建任务状态查询条件""" if status == 'running': return "AND (t.status = 'pending' OR t.status = 'processing')" elif status == 'completed': return "AND t.status = 'completed'" elif status == 'failed': return "AND t.status = 'failed'" return "" def _get_task_stats_query() -> str: """获取任务统计的 SQL 查询""" return """ SELECT COUNT(*) as total, SUM(CASE WHEN status = 'pending' OR status = 'processing' THEN 1 ELSE 0 END) as running, SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed, SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed """ def _get_online_user_count(redis_client) -> int: """从 Redis 获取在线用户数""" try: token_keys = redis_client.keys("token:*") user_ids = set() for key in token_keys: parts = key.split(':') if len(parts) >= 2: user_ids.add(parts[1]) return len(user_ids) except Exception as e: print(f"获取在线用户数失败: {e}") return 0 def _calculate_audio_storage() -> Dict[str, float]: """计算音频文件存储统计""" audio_files_count = 0 audio_total_size = 0 try: if os.path.exists(AUDIO_DIR): for root, _, files in os.walk(AUDIO_DIR): for file in files: if file.endswith(AUDIO_FILE_EXTENSIONS): audio_files_count += 1 file_path = os.path.join(root, file) try: audio_total_size += os.path.getsize(file_path) except OSError: continue except Exception as e: print(f"统计音频文件失败: {e}") return { "audio_files_count": audio_files_count, "audio_total_size_gb": round(audio_total_size / BYTES_TO_GB, 2) } @router.get("/admin/dashboard/stats") async def get_dashboard_stats(current_user=Depends(get_current_admin_user)): """获取管理员 Dashboard 统计数据""" try: with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) # 1. 用户统计 today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) cursor.execute("SELECT COUNT(*) as total FROM users") total_users = cursor.fetchone()['total'] cursor.execute( "SELECT COUNT(*) as count FROM users WHERE created_at >= %s", (today_start,) ) today_new_users = cursor.fetchone()['count'] online_users = _get_online_user_count(redis_client) # 2. 会议统计 cursor.execute("SELECT COUNT(*) as total FROM meetings") total_meetings = cursor.fetchone()['total'] cursor.execute( "SELECT COUNT(*) as count FROM meetings WHERE created_at >= %s", (today_start,) ) today_new_meetings = cursor.fetchone()['count'] # 3. 任务统计 task_stats_query = _get_task_stats_query() # 转录任务 cursor.execute(f"{task_stats_query} FROM transcript_tasks") transcription_stats = cursor.fetchone() or {'total': 0, 'running': 0, 'completed': 0, 'failed': 0} # 总结任务 cursor.execute(f"{task_stats_query} FROM llm_tasks") summary_stats = cursor.fetchone() or {'total': 0, 'running': 0, 'completed': 0, 'failed': 0} # 知识库任务 cursor.execute(f"{task_stats_query} FROM knowledge_base_tasks") kb_stats = cursor.fetchone() or {'total': 0, 'running': 0, 'completed': 0, 'failed': 0} # 4. 音频存储统计 storage_stats = _calculate_audio_storage() # 组装返回数据 stats = { "users": { "total": total_users, "today_new": today_new_users, "online": online_users }, "meetings": { "total": total_meetings, "today_new": today_new_meetings }, "tasks": { "transcription": { "total": transcription_stats['total'] or 0, "running": transcription_stats['running'] or 0, "completed": transcription_stats['completed'] or 0, "failed": transcription_stats['failed'] or 0 }, "summary": { "total": summary_stats['total'] or 0, "running": summary_stats['running'] or 0, "completed": summary_stats['completed'] or 0, "failed": summary_stats['failed'] or 0 }, "knowledge_base": { "total": kb_stats['total'] or 0, "running": kb_stats['running'] or 0, "completed": kb_stats['completed'] or 0, "failed": kb_stats['failed'] or 0 } }, "storage": storage_stats } return create_api_response(code="200", message="获取统计数据成功", data=stats) except Exception as e: print(f"获取Dashboard统计数据失败: {e}") return create_api_response(code="500", message=f"获取统计数据失败: {str(e)}") @router.get("/admin/online-users") async def get_online_users(current_user=Depends(get_current_admin_user)): """获取在线用户列表""" try: token_keys = redis_client.keys("token:*") # 提取用户ID并去重 user_tokens = {} for key in token_keys: parts = key.split(':') if len(parts) >= 3: user_id = int(parts[1]) token = parts[2] if user_id not in user_tokens: user_tokens[user_id] = [] user_tokens[user_id].append({'token': token, 'key': key}) # 查询用户信息 with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) online_users_list = [] for user_id, tokens in user_tokens.items(): cursor.execute( "SELECT user_id, username, caption, email, role_id FROM users WHERE user_id = %s", (user_id,) ) user = cursor.fetchone() if user: ttl_seconds = redis_client.ttl(tokens[0]['key']) online_users_list.append({ **user, 'token_count': len(tokens), 'ttl_seconds': ttl_seconds, 'ttl_hours': round(ttl_seconds / 3600, 1) if ttl_seconds > 0 else 0 }) # 按用户ID排序 online_users_list.sort(key=lambda x: x['user_id']) return create_api_response( code="200", message="获取在线用户列表成功", data={"users": online_users_list, "total": len(online_users_list)} ) except Exception as e: print(f"获取在线用户列表失败: {e}") return create_api_response(code="500", message=f"获取在线用户列表失败: {str(e)}") @router.post("/admin/kick-user/{user_id}") async def kick_user(user_id: int, current_user=Depends(get_current_admin_user)): """踢出用户(撤销该用户的所有 token)""" try: revoked_count = jwt_service.revoke_all_user_tokens(user_id) if revoked_count > 0: return create_api_response( code="200", message=f"已踢出用户,撤销了 {revoked_count} 个 token", data={"user_id": user_id, "revoked_count": revoked_count} ) else: return create_api_response( code="404", message="该用户当前不在线或未找到 token" ) except Exception as e: print(f"踢出用户失败: {e}") return create_api_response(code="500", message=f"踢出用户失败: {str(e)}") @router.get("/admin/tasks/monitor") async def monitor_tasks( task_type: str = Query('all', description="任务类型: all, transcription, summary, knowledge_base"), status: str = Query('all', description="任务状态: all, running, completed, failed"), limit: int = Query(20, ge=1, le=100, description="返回数量限制"), current_user=Depends(get_current_admin_user) ): """监控任务进度""" try: with get_db_connection() as connection: cursor = connection.cursor(dictionary=True) tasks = [] status_condition = _build_status_condition(status) # 转录任务 if task_type in ['all', 'transcription']: query = f""" SELECT t.task_id, 'transcription' as task_type, t.meeting_id, m.title as meeting_title, t.status, t.progress, t.error_message, t.created_at, t.completed_at, u.username as creator_name FROM transcript_tasks t LEFT JOIN meetings m ON t.meeting_id = m.meeting_id LEFT JOIN users u ON m.user_id = u.user_id WHERE 1=1 {status_condition} ORDER BY t.created_at DESC LIMIT %s """ cursor.execute(query, (limit,)) tasks.extend(cursor.fetchall()) # 总结任务 if task_type in ['all', 'summary']: query = f""" SELECT t.task_id, 'summary' as task_type, t.meeting_id, m.title as meeting_title, t.status, NULL as progress, t.error_message, t.created_at, t.completed_at, u.username as creator_name FROM llm_tasks t LEFT JOIN meetings m ON t.meeting_id = m.meeting_id LEFT JOIN users u ON m.user_id = u.user_id WHERE 1=1 {status_condition} ORDER BY t.created_at DESC LIMIT %s """ cursor.execute(query, (limit,)) tasks.extend(cursor.fetchall()) # 知识库任务 if task_type in ['all', 'knowledge_base']: query = f""" SELECT t.task_id, 'knowledge_base' as task_type, t.kb_id as meeting_id, k.title as meeting_title, t.status, t.progress, t.error_message, t.created_at, t.updated_at, u.username as creator_name FROM knowledge_base_tasks t LEFT JOIN knowledge_bases k ON t.kb_id = k.kb_id LEFT JOIN users u ON k.creator_id = u.user_id WHERE 1=1 {status_condition} ORDER BY t.created_at DESC LIMIT %s """ cursor.execute(query, (limit,)) tasks.extend(cursor.fetchall()) # 按创建时间排序并限制返回数量 tasks.sort(key=lambda x: x['created_at'], reverse=True) tasks = tasks[:limit] return create_api_response( code="200", message="获取任务监控数据成功", data={"tasks": tasks, "total": len(tasks)} ) except Exception as e: print(f"获取任务监控数据失败: {e}") import traceback traceback.print_exc() return create_api_response(code="500", message=f"获取任务监控数据失败: {str(e)}") @router.get("/admin/system/resources") async def get_system_resources(current_user=Depends(get_current_admin_user)): """获取服务器资源使用情况""" try: import psutil # CPU 使用率 cpu_percent = psutil.cpu_percent(interval=1) cpu_count = psutil.cpu_count() # 内存使用情况 memory = psutil.virtual_memory() memory_total_gb = round(memory.total / BYTES_TO_GB, 2) memory_used_gb = round(memory.used / BYTES_TO_GB, 2) # 磁盘使用情况 disk = psutil.disk_usage('/') disk_total_gb = round(disk.total / BYTES_TO_GB, 2) disk_used_gb = round(disk.used / BYTES_TO_GB, 2) resources = { "cpu": { "percent": cpu_percent, "count": cpu_count }, "memory": { "total_gb": memory_total_gb, "used_gb": memory_used_gb, "percent": memory.percent }, "disk": { "total_gb": disk_total_gb, "used_gb": disk_used_gb, "percent": disk.percent }, "timestamp": datetime.now().isoformat() } return create_api_response(code="200", message="获取系统资源成功", data=resources) except ImportError: return create_api_response( code="500", message="psutil 库未安装,请运行: pip install psutil" ) except Exception as e: print(f"获取系统资源失败: {e}") return create_api_response(code="500", message=f"获取系统资源失败: {str(e)}")