chore: 清理冗余代码与配置,优化项目结构

1.  删除无用文件与废弃代码:移除locale枚举、element-plus插件、sse路由、api token模块等
2.  简化类型导入与依赖:移除大量未使用的类型导入,统一echarts导入方式
3.  优化配置与样式:调整gitignore、样式引入顺序,新增列表动画样式
4.  修复接口与模型:修正接口返回类型、查询参数配置,更新部门模型字段
5.  优化性能与体验:添加图片懒加载,优化加载逻辑与表格渲染
6.  调整环境配置:新增并更新开发/生产环境配置文件
This commit is contained in:
zhangtao
2026-07-23 20:49:52 +08:00
parent 096e2216d6
commit 0ce31936aa
360 changed files with 8072 additions and 22122 deletions
@@ -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)