mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 05:02:57 +00:00
chore: 清理冗余代码与配置,优化项目结构
1. 删除无用文件与废弃代码:移除locale枚举、element-plus插件、sse路由、api token模块等 2. 简化类型导入与依赖:移除大量未使用的类型导入,统一echarts导入方式 3. 优化配置与样式:调整gitignore、样式引入顺序,新增列表动画样式 4. 修复接口与模型:修正接口返回类型、查询参数配置,更新部门模型字段 5. 优化性能与体验:添加图片懒加载,优化加载逻辑与表格渲染 6. 调整环境配置:新增并更新开发/生产环境配置文件
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Query, Security
|
||||
from fastapi import APIRouter, Body, Depends, Security
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi_cache import FastAPICache
|
||||
from fastapi_cache.decorator import cache
|
||||
from redis.asyncio.client import Redis
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -18,14 +16,12 @@ from .service import OnlineService
|
||||
|
||||
OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["在线用户"])
|
||||
|
||||
_STATS_NS = "online_stats"
|
||||
|
||||
|
||||
@OnlineRouter.get("/list", summary="获取在线用户列表", response_model=ResponseSchema[list[OnlineOutSchema]], dependencies=[Security(AuthPermission(["module_monitor:online:query"]))])
|
||||
async def get_online_list_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[OnlineQueryParam, Query()],
|
||||
search: Annotated[OnlineQueryParam, Depends()],
|
||||
) -> JSONResponse:
|
||||
result_dict_list = await OnlineService.get_online_list(redis=redis, search=search)
|
||||
result_dict = await PaginationService.paginate(
|
||||
@@ -36,13 +32,21 @@ async def get_online_list_controller(
|
||||
return SuccessResponse(data=result_dict, msg="获取成功")
|
||||
|
||||
|
||||
@OnlineRouter.get("/current", summary="获取当前用户的在线会话", response_model=ResponseSchema[list[OnlineOutSchema]], dependencies=[Depends(get_current_user)])
|
||||
async def get_current_online_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
) -> JSONResponse:
|
||||
sessions = await OnlineService.get_current_user_sessions(redis=redis, user_id=auth.user.id)
|
||||
return SuccessResponse(data=sessions, msg="获取当前用户在线会话成功")
|
||||
|
||||
|
||||
@OnlineRouter.delete("/delete", summary="强制下线", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_monitor:online:delete"]))])
|
||||
async def delete_online_controller(
|
||||
session_id: Annotated[str, Body(description="会话编号")],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
) -> JSONResponse:
|
||||
await OnlineService.delete_online(redis=redis, session_id=session_id)
|
||||
await FastAPICache.clear(namespace=_STATS_NS)
|
||||
return SuccessResponse(msg="强制下线成功")
|
||||
|
||||
|
||||
@@ -51,16 +55,14 @@ async def clear_online_controller(
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
) -> JSONResponse:
|
||||
await OnlineService.clear_online(redis=redis)
|
||||
await FastAPICache.clear(namespace=_STATS_NS)
|
||||
return SuccessResponse(msg="清除所有在线用户成功")
|
||||
|
||||
|
||||
@OnlineRouter.get("/stats", summary="获取仪表盘统计数据", response_model=ResponseSchema[DashboardStatsSchema])
|
||||
@cache(expire=15, namespace=_STATS_NS)
|
||||
async def get_dashboard_stats_controller(
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
redis: Annotated[Redis, Depends(redis_getter)],
|
||||
_auth: Annotated[AuthSchema, Depends(get_current_user)],
|
||||
_auth: Annotated[AuthSchema, Security(AuthPermission(["module_monitor:dashboard:query"]))],
|
||||
) -> JSONResponse:
|
||||
data = await OnlineService.get_dashboard_stats(db=db, redis=redis)
|
||||
return SuccessResponse(data=data, msg="获取仪表盘统计成功")
|
||||
|
||||
@@ -24,7 +24,7 @@ class OnlineService:
|
||||
tokens = await RedisCURD(redis).mget(keys)
|
||||
|
||||
online_users = []
|
||||
for token in tokens:
|
||||
for key, token in zip(keys, tokens, strict=True):
|
||||
if not token:
|
||||
continue
|
||||
try:
|
||||
@@ -53,13 +53,24 @@ class OnlineService:
|
||||
continue
|
||||
|
||||
online_users.append(session_info)
|
||||
except Exception as e:
|
||||
logger.error(f"解析在线用户数据失败: {e}")
|
||||
except Exception:
|
||||
# token 已过期或无效,清理 Redis 中的脏数据
|
||||
key_str = key.decode() if isinstance(key, bytes) else key
|
||||
session_id = key_str.split(":")[-1]
|
||||
await RedisCURD(redis).delete(key_str)
|
||||
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}")
|
||||
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.USER_SESSION.key}:{session_id}")
|
||||
continue
|
||||
|
||||
online_users.sort(key=lambda x: x.get("login_time", ""), reverse=True)
|
||||
return online_users
|
||||
|
||||
@staticmethod
|
||||
async def get_current_user_sessions(redis: Redis, user_id: int) -> list[dict]:
|
||||
"""获取当前用户的在线会话列表"""
|
||||
all_online = await OnlineService.get_online_list(redis)
|
||||
return [s for s in all_online if s.get("user_id") == user_id]
|
||||
|
||||
@staticmethod
|
||||
async def delete_online(redis: Redis, session_id: str) -> None:
|
||||
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}")
|
||||
|
||||
@@ -21,7 +21,7 @@ ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", ta
|
||||
async def get_directory_list_controller(
|
||||
request: Request,
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[ResourceSearchQueryParam, Query()],
|
||||
search: Annotated[ResourceSearchQueryParam, Depends()],
|
||||
) -> JSONResponse:
|
||||
result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url))
|
||||
result_dict = await PaginationService.paginate(
|
||||
@@ -111,7 +111,7 @@ async def create_directory_controller(
|
||||
@ResourceRouter.post("/export", summary="导出资源列表", dependencies=[Security(AuthPermission(["module_monitor:resource:export"]))])
|
||||
async def export_resource_list_controller(
|
||||
request: Request,
|
||||
search: Annotated[ResourceSearchQueryParam, Query()],
|
||||
search: Annotated[ResourceSearchQueryParam, Depends()],
|
||||
) -> StreamingResponse:
|
||||
result_dict_list = await ResourceService.get_resources_list(search=search, base_url=str(request.base_url))
|
||||
export_result = await ResourceService.export_resource(data_list=result_dict_list)
|
||||
|
||||
@@ -186,5 +186,6 @@ class ResourceSearchQueryParam(BaseModel):
|
||||
|
||||
name: str | None = Field(None, description="搜索关键词")
|
||||
path: str | None = Field(None, description="目录路径")
|
||||
include_hidden: bool = Field(False, description="是否包含隐藏文件")
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -8,8 +7,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi_cache import FastAPICache
|
||||
|
||||
from app.config.path_conf import STATIC_DIR
|
||||
from app.config.setting import settings
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import logger
|
||||
@@ -18,7 +16,6 @@ from app.utils.excel_util import ExcelUtil
|
||||
from .schema import (
|
||||
ResourceCopySchema,
|
||||
ResourceCreateDirSchema,
|
||||
ResourceDirectorySchema,
|
||||
ResourceItemSchema,
|
||||
ResourceMoveSchema,
|
||||
ResourceRenameSchema,
|
||||
@@ -35,9 +32,7 @@ class ResourceService:
|
||||
|
||||
@staticmethod
|
||||
def _get_resource_root() -> str:
|
||||
if not settings.STATIC_ENABLE:
|
||||
raise CustomException(msg="静态文件服务未启用")
|
||||
resource_root = os.path.join(str(settings.STATIC_ROOT), "upload", "resource")
|
||||
resource_root = os.path.join(str(STATIC_DIR), "upload")
|
||||
os.makedirs(resource_root, exist_ok=True)
|
||||
return resource_root
|
||||
|
||||
@@ -161,7 +156,7 @@ class ResourceService:
|
||||
|
||||
@staticmethod
|
||||
def _generate_http_url(file_path: str, base_url: str | None = None) -> str:
|
||||
static_root = str(settings.STATIC_ROOT)
|
||||
static_root = str(STATIC_DIR)
|
||||
try:
|
||||
relative_path = os.path.relpath(file_path, static_root)
|
||||
url_path = relative_path.replace(os.sep, "/")
|
||||
@@ -212,75 +207,6 @@ class ResourceService:
|
||||
logger.error(f"获取文件信息失败: {e!s}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def get_directory_list(
|
||||
path: str | None = None,
|
||||
include_hidden: bool = False,
|
||||
base_url: str | None = None,
|
||||
) -> ResourceDirectorySchema:
|
||||
# 进程级缓存(目录内容变更极低频,30s 过期)
|
||||
_RESOURCE_DIR_TTL = 30
|
||||
cache_key = f"resource_dir:{path or 'root'}:{include_hidden}"
|
||||
_backend = FastAPICache.get_backend()
|
||||
cached = await _backend.get(cache_key)
|
||||
if cached:
|
||||
return ResourceDirectorySchema(**json.loads(cached.decode()))
|
||||
|
||||
try:
|
||||
if path is None:
|
||||
safe_path = ResourceService._get_resource_root()
|
||||
display_path = ResourceService._generate_http_url(safe_path, base_url)
|
||||
else:
|
||||
safe_path = ResourceService._get_safe_path(path)
|
||||
display_path = ResourceService._generate_http_url(safe_path, base_url)
|
||||
|
||||
if not os.path.exists(safe_path):
|
||||
raise CustomException(msg="目录不存在")
|
||||
|
||||
if not os.path.isdir(safe_path):
|
||||
raise CustomException(msg="路径不是目录")
|
||||
|
||||
items = []
|
||||
total_files = 0
|
||||
total_dirs = 0
|
||||
total_size = 0
|
||||
|
||||
try:
|
||||
for item_name in os.listdir(safe_path):
|
||||
if not include_hidden and item_name.startswith("."):
|
||||
continue
|
||||
|
||||
item_path = os.path.join(safe_path, item_name)
|
||||
file_info = ResourceService._get_file_info(item_path, base_url)
|
||||
|
||||
if file_info:
|
||||
items.append(file_info)
|
||||
if file_info.is_file:
|
||||
total_files += 1
|
||||
total_size += file_info.size or 0
|
||||
elif file_info.is_dir:
|
||||
total_dirs += 1
|
||||
|
||||
except PermissionError:
|
||||
raise CustomException(msg="没有权限访问此目录")
|
||||
|
||||
result = ResourceDirectorySchema(
|
||||
path=display_path,
|
||||
name=os.path.basename(safe_path),
|
||||
items=items,
|
||||
total_files=total_files,
|
||||
total_dirs=total_dirs,
|
||||
total_size=total_size,
|
||||
)
|
||||
await _backend.set(cache_key, json.dumps(result.model_dump()).encode(), expire=_RESOURCE_DIR_TTL)
|
||||
return result
|
||||
|
||||
except CustomException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"获取目录列表失败: {e!s}")
|
||||
raise CustomException(msg=f"获取目录列表失败: {e!s}")
|
||||
|
||||
@staticmethod
|
||||
async def get_resources_list(
|
||||
search: ResourceSearchQueryParam | None = None,
|
||||
@@ -302,8 +228,10 @@ class ResourceService:
|
||||
all_resources = []
|
||||
|
||||
try:
|
||||
include_hidden = search.include_hidden if search and hasattr(search, "include_hidden") else False
|
||||
|
||||
for item_name in os.listdir(resource_root):
|
||||
if item_name.startswith("."):
|
||||
if item_name.startswith(".") and not include_hidden:
|
||||
continue
|
||||
|
||||
item_path = os.path.join(resource_root, item_name)
|
||||
|
||||
@@ -1,45 +1,17 @@
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Security
|
||||
from fastapi import APIRouter, Security
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.sse import EventSourceResponse, ServerSentEvent
|
||||
|
||||
from app.api.v1.module_monitor.server.schema import ServerMonitorSchema
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core.base_schema import AuthSchema
|
||||
from app.core.dependencies import AuthPermission, get_current_user_ws
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .service import ServerService
|
||||
|
||||
ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=["服务器监控"])
|
||||
|
||||
# ── 服务器监控推送间隔 ──
|
||||
_SERVER_STREAM_INTERVAL = 5 # 秒
|
||||
|
||||
|
||||
@ServerRouter.get("/info", summary="查询服务器监控信息", response_model=ResponseSchema[ServerMonitorSchema], dependencies=[Security(AuthPermission(["module_monitor:server:query"]))])
|
||||
async def get_monitor_server_info_controller() -> JSONResponse:
|
||||
result_dict = await ServerService.get_server_monitor_info()
|
||||
return SuccessResponse(data=result_dict, msg="获取服务器监控信息成功")
|
||||
|
||||
|
||||
@ServerRouter.get("/stream", summary="服务器资源实时推送", response_class=EventSourceResponse)
|
||||
async def server_monitor_stream(
|
||||
token: Annotated[str, Query(..., description="认证 token")],
|
||||
auth: Annotated[AuthSchema, Depends(get_current_user_ws)],
|
||||
) -> AsyncIterable[ServerSentEvent]:
|
||||
"""SSE 实时推送服务器资源使用情况,每 5 秒推送一次。
|
||||
|
||||
由于 EventSource 不支持自定义请求头,通过查询参数 token 传递认证信息。
|
||||
"""
|
||||
# 手动验证权限(SSE 端点通过查询参数 token 认证,无法使用 Security(AuthPermission))
|
||||
perm_check = AuthPermission(["module_monitor:server:query"])
|
||||
await perm_check(auth)
|
||||
|
||||
while True:
|
||||
data = await ServerService.get_server_monitor_info()
|
||||
yield ServerSentEvent(data=data, event="server_status")
|
||||
await asyncio.sleep(_SERVER_STREAM_INTERVAL)
|
||||
|
||||
Reference in New Issue
Block a user