refactor: 大规模代码整理与功能优化

1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码
2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑
3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件
4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
This commit is contained in:
zhangtao
2026-06-17 01:56:31 +08:00
parent 17b3cd0a4c
commit 73f2823692
500 changed files with 40763 additions and 26616 deletions
+1 -16
View File
@@ -8,19 +8,17 @@ from app.api.v1.module_monitor.cache.schema import CacheInfoSchema, CacheMonitor
from app.common.response import ResponseSchema, SuccessResponse
from app.core.dependencies import AuthPermission, redis_getter
from app.core.exceptions import CustomException
from app.core.logger import log
from app.core.router_class import OperationLogRoute
from .service import CacheService
CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=["缓存监控"])
CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=["系统监控/缓存监控"])
@CacheRouter.get(
"/info",
dependencies=[Depends(AuthPermission(["module_monitor:cache:query"]))],
summary="获取缓存监控信息",
description="获取缓存监控信息",
response_model=ResponseSchema[CacheMonitorSchema],
)
async def get_monitor_cache_info_controller(
@@ -36,7 +34,6 @@ async def get_monitor_cache_info_controller(
- JSONResponse: 包含缓存监控统计信息的JSON响应
"""
result = await CacheService.get_cache_monitor_statistical_info_service(redis=redis)
log.info("获取缓存监控信息成功")
return SuccessResponse(data=result, msg="获取缓存监控信息成功")
@@ -44,7 +41,6 @@ async def get_monitor_cache_info_controller(
"/get/names",
dependencies=[Depends(AuthPermission(["module_monitor:cache:query"]))],
summary="获取缓存名称列表",
description="获取缓存名称列表",
response_model=ResponseSchema[list[CacheInfoSchema]],
)
async def get_monitor_cache_name_controller() -> JSONResponse:
@@ -55,7 +51,6 @@ async def get_monitor_cache_name_controller() -> JSONResponse:
- JSONResponse: 包含缓存名称列表的JSON响应
"""
result = await CacheService.get_cache_monitor_cache_name_service()
log.info("获取缓存名称列表成功")
return SuccessResponse(data=result, msg="获取缓存名称列表成功")
@@ -63,7 +58,6 @@ async def get_monitor_cache_name_controller() -> JSONResponse:
"/get/keys/{cache_name}",
dependencies=[Depends(AuthPermission(["module_monitor:cache:query"]))],
summary="获取缓存键名列表",
description="获取缓存键名列表",
response_model=ResponseSchema[list[CacheInfoSchema]],
)
async def get_monitor_cache_key_controller(
@@ -81,7 +75,6 @@ async def get_monitor_cache_key_controller(
result = await CacheService.get_cache_monitor_cache_key_service(
redis=redis, cache_name=cache_name
)
log.info(f"获取缓存{cache_name}的键名列表成功")
return SuccessResponse(data=result, msg=f"获取缓存{cache_name}的键名列表成功")
@@ -89,7 +82,6 @@ async def get_monitor_cache_key_controller(
"/get/value/{cache_name}/{cache_key}",
dependencies=[Depends(AuthPermission(["module_monitor:cache:query"]))],
summary="获取缓存值",
description="获取缓存值",
response_model=ResponseSchema[CacheInfoSchema],
)
async def get_monitor_cache_value_controller(
@@ -110,7 +102,6 @@ async def get_monitor_cache_value_controller(
result = await CacheService.get_cache_monitor_cache_value_service(
redis=redis, cache_name=cache_name, cache_key=cache_key
)
log.info(f"获取缓存{cache_name}:{cache_key}的值成功")
return SuccessResponse(data=result, msg=f"获取缓存{cache_name}:{cache_key}的值成功")
@@ -118,7 +109,6 @@ async def get_monitor_cache_value_controller(
"/delete/name/{cache_name}",
dependencies=[Depends(AuthPermission(["module_monitor:cache:delete"]))],
summary="清除指定缓存名称的所有缓存",
description="清除指定缓存名称的所有缓存",
response_model=ResponseSchema[None],
)
async def clear_monitor_cache_name_controller(
@@ -138,7 +128,6 @@ async def clear_monitor_cache_name_controller(
)
if not result:
raise CustomException(msg="清除缓存失败", data=result)
log.info(f"清除缓存{cache_name}成功")
return SuccessResponse(msg=f"{cache_name}对应键值清除成功", data=result)
@@ -146,7 +135,6 @@ async def clear_monitor_cache_name_controller(
"/delete/key/{cache_key}",
dependencies=[Depends(AuthPermission(["module_monitor:cache:delete"]))],
summary="清除指定缓存键",
description="清除指定缓存键",
response_model=ResponseSchema[None],
)
async def clear_monitor_cache_key_controller(
@@ -166,7 +154,6 @@ async def clear_monitor_cache_key_controller(
)
if not result:
raise CustomException(msg="清除缓存失败", data=result)
log.info(f"清除缓存键{cache_key}成功")
return SuccessResponse(msg=f"{cache_key}清除成功", data=result)
@@ -174,7 +161,6 @@ async def clear_monitor_cache_key_controller(
"/delete/all",
dependencies=[Depends(AuthPermission(["module_monitor:cache:delete"]))],
summary="清除所有缓存",
description="清除所有缓存",
response_model=ResponseSchema[None],
)
async def clear_monitor_cache_all_controller(
@@ -192,5 +178,4 @@ async def clear_monitor_cache_all_controller(
result = await CacheService.clear_cache_monitor_all_service(redis=redis)
if not result:
raise CustomException(msg="清除缓存失败", data=result)
log.info("清除所有缓存成功")
return SuccessResponse(msg="所有缓存清除成功", data=result)
+2 -10
View File
@@ -1,13 +1,8 @@
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field
class CacheMonitorSchema(BaseModel):
"""缓存监控信息模型"""
model_config = ConfigDict(from_attributes=True)
command_stats: list[dict] = Field(default_factory=list, description="Redis命令统计信息")
db_size: int = Field(default=0, description="Redis数据库中的Key总数")
info: dict = Field(default_factory=dict, description="Redis服务器信息")
@@ -15,10 +10,7 @@ class CacheMonitorSchema(BaseModel):
class CacheInfoSchema(BaseModel):
"""缓存对象信息模型"""
model_config = ConfigDict(from_attributes=True)
cache_key: str = Field(..., description="缓存键名")
cache_name: str = Field(..., description="缓存名称")
cache_value: Any = Field(default=None, description="缓存值")
cache_value: str | None = Field(default=None, description="缓存值")
remark: str | None = Field(default=None, description="备注说明")
+7 -11
View File
@@ -12,7 +12,7 @@ class CacheService:
"""
@classmethod
async def get_cache_monitor_statistical_info_service(cls, redis: Redis) -> dict:
async def get_cache_monitor_statistical_info_service(cls, redis: Redis) -> CacheMonitorSchema:
"""
获取缓存监控信息。
@@ -30,30 +30,26 @@ class CacheService:
{"name": key.split("_")[1], "value": str(value.get("calls"))}
for key, value in command_stats_dict.items()
]
result = CacheMonitorSchema(command_stats=command_stats, db_size=db_size, info=info)
return result.model_dump()
return CacheMonitorSchema(command_stats=command_stats, db_size=db_size, info=info)
@classmethod
async def get_cache_monitor_cache_name_service(cls) -> list:
async def get_cache_monitor_cache_name_service(cls) -> list[CacheInfoSchema]:
"""
获取缓存名称列表信息。
返回:
- list: 缓存名称列表信息。
"""
name_list = [
return [
CacheInfoSchema(
cache_key="",
cache_name=key_config.key,
cache_value="",
remark=key_config.remark,
).model_dump()
)
for key_config in RedisInitKeyConfig
]
return name_list
@classmethod
async def get_cache_monitor_cache_key_service(cls, redis: Redis, cache_name: str) -> list:
"""
@@ -76,7 +72,7 @@ class CacheService:
@classmethod
async def get_cache_monitor_cache_value_service(
cls, redis: Redis, cache_name: str, cache_key: str
) -> dict:
) -> CacheInfoSchema:
"""
获取缓存内容信息。
@@ -95,7 +91,7 @@ class CacheService:
cache_name=cache_name,
cache_value=cache_value,
remark="",
).model_dump()
)
@classmethod
async def clear_cache_monitor_cache_name_service(cls, redis: Redis, cache_name: str) -> bool:
@@ -5,23 +5,21 @@ from fastapi.responses import JSONResponse
from redis.asyncio.client import Redis
from app.common.request import PaginationService
from app.common.response import ErrorResponse, ResponseSchema, SuccessResponse
from app.common.response import ResponseSchema, SuccessResponse
from app.core.base_params import PaginationQueryParam
from app.core.dependencies import AuthPermission, redis_getter
from app.core.logger import log
from app.core.router_class import OperationLogRoute
from .schema import OnlineOutSchema, OnlineQueryParam
from .service import OnlineService
OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["在线用户"])
OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["系统监控/在线用户"])
@OnlineRouter.get(
"/list",
dependencies=[Depends(AuthPermission(["module_monitor:online:query"]))],
summary="获取在线用户列表",
description="获取在线用户列表",
response_model=ResponseSchema[list[OnlineOutSchema]],
)
async def get_online_list_controller(
@@ -47,7 +45,6 @@ async def get_online_list_controller(
page_no=paging_query.page_no,
page_size=paging_query.page_size,
)
log.info("获取成功")
return SuccessResponse(data=result_dict, msg="获取成功")
@@ -56,7 +53,6 @@ async def get_online_list_controller(
"/delete",
dependencies=[Depends(AuthPermission(["module_monitor:online:delete"]))],
summary="强制下线",
description="强制下线",
response_model=ResponseSchema[None],
)
async def delete_online_controller(
@@ -73,19 +69,14 @@ async def delete_online_controller(
返回:
- JSONResponse: 包含操作结果的JSON响应。
"""
is_ok = await OnlineService.delete_online_service(redis=redis, session_id=session_id)
if is_ok:
log.info("强制下线成功")
return SuccessResponse(msg="强制下线成功")
log.info("强制下线失败")
return ErrorResponse(msg="强制下线失败")
await OnlineService.delete_online_service(redis=redis, session_id=session_id)
return SuccessResponse(msg="强制下线成功")
@OnlineRouter.delete(
"/clear",
dependencies=[Depends(AuthPermission(["module_monitor:online:delete"]))],
summary="清除所有在线用户",
description="清除所有在线用户",
response_model=ResponseSchema[None],
)
async def clear_online_controller(
@@ -100,9 +91,5 @@ async def clear_online_controller(
返回:
- JSONResponse: 包含操作结果的JSON响应。
"""
is_ok = await OnlineService.clear_online_service(redis=redis)
if is_ok:
log.info("清除所有在线用户成功")
return SuccessResponse(msg="清除所有在线用户成功")
log.info("清除所有在线用户失败")
return ErrorResponse(msg="清除所有在线用户失败")
await OnlineService.clear_online_service(redis=redis)
return SuccessResponse(msg="清除所有在线用户成功")
@@ -1,5 +1,5 @@
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field
from app.common.enums import QueueEnum
from app.core.validator import DateTimeStr
@@ -10,8 +10,6 @@ class OnlineOutSchema(BaseModel):
在线用户对应pydantic模型
"""
model_config = ConfigDict(from_attributes=True)
name: str = Field(..., description="用户名称")
session_id: str = Field(..., description="会话编号")
user_id: int = Field(..., description="用户ID")
@@ -3,7 +3,7 @@ import json
from redis.asyncio.client import Redis
from app.common.enums import RedisInitKeyConfig
from app.core.logger import log
from app.core.logger import logger
from app.core.redis_crud import RedisCURD
from app.core.security import decode_access_token
@@ -41,7 +41,7 @@ class OnlineService:
if cls._match_search_conditions(session_info, search):
online_users.append(session_info)
except Exception as e:
log.error(f"解析在线用户数据失败: {e}")
logger.error(f"解析在线用户数据失败: {e}")
continue
# 按照 login_time 倒序排序
online_users.sort(key=lambda x: x.get("login_time", ""), reverse=True)
@@ -49,41 +49,33 @@ class OnlineService:
return online_users
@classmethod
async def delete_online_service(cls, redis: Redis, session_id: str) -> bool:
async def delete_online_service(cls, redis: Redis, session_id: str) -> None:
"""
强制下线指定在线用户
参数:
- redis (Redis): Redis异步客户端实例。
- session_id (str): 在线用户会话ID。
返回:
- bool: 如果操作成功则返回True,否则返回False。
"""
# 删除 token
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}")
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}")
log.info(f"强制下线用户会话: {session_id}")
return True
logger.info(f"强制下线用户会话: {session_id}")
@classmethod
async def clear_online_service(cls, redis: Redis) -> bool:
async def clear_online_service(cls, redis: Redis) -> None:
"""
强制下线所有在线用户
参数:
- redis (Redis): Redis异步客户端实例。
返回:
- bool: 如果操作成功则返回True,否则返回False。
"""
# 删除 token
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*")
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:*")
log.info("清除所有在线用户会话成功")
return True
logger.info("清除所有在线用户会话成功")
@staticmethod
def _match_search_conditions(online_info: dict, search: OnlineQueryParam | None = None) -> bool:
@@ -3,31 +3,32 @@ from typing import Annotated
from fastapi import APIRouter, Body, Depends, Form, Query, Request, UploadFile
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from app.api.v1.module_common.file.service import FileService
from app.common.request import PaginationService
from app.common.response import ResponseSchema, StreamResponse, SuccessResponse, UploadFileResponse
from app.core.base_params import PaginationQueryParam
from app.core.base_schema import UploadResponseSchema
from app.core.dependencies import AuthPermission
from app.core.logger import log
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from .schema import (
ResourceCopySchema,
ResourceCreateDirSchema,
ResourceItemSchema,
ResourceMoveSchema,
ResourceRenameSchema,
ResourceSearchQueryParam,
)
from .service import ResourceService
ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["资源管理"])
ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["系统监控/资源管理"])
@ResourceRouter.get(
"/list",
summary="获取目录列表",
description="获取指定目录下的文件和子目录列表",
response_model=ResponseSchema[list[dict]],
response_model=ResponseSchema[list[ResourceItemSchema]],
dependencies=[Depends(AuthPermission(["module_monitor:resource:query"]))],
)
async def get_directory_list_controller(
@@ -57,15 +58,13 @@ async def get_directory_list_controller(
page_size=page.page_size,
)
log.info(f"获取目录列表成功: {getattr(search, 'name', None) or ''}")
return SuccessResponse(data=result_dict, msg="获取目录列表成功")
@ResourceRouter.post(
"/upload",
summary="上传文件",
description="上传文件到指定目录(调用统一上传接口)",
response_model=ResponseSchema[dict],
response_model=ResponseSchema[UploadResponseSchema],
dependencies=[Depends(AuthPermission(["module_monitor:resource:upload"]))],
)
async def upload_file_controller(
@@ -85,20 +84,18 @@ async def upload_file_controller(
- JSONResponse: 包含上传文件信息的JSON响应。
"""
# 调用统一上传接口,使用 resource 类型
result_dict = await FileService.upload_service(
result = await FileService.upload_service(
base_url=str(request.base_url),
file=file,
upload_type="resource",
target_path=target_path,
)
log.info(f"上传文件成功: {result_dict['file_name']}")
return SuccessResponse(data=result_dict, msg="上传文件成功")
return SuccessResponse(data=result, msg="上传文件成功")
@ResourceRouter.get(
"/download",
summary="下载文件",
description="下载指定文件",
dependencies=[Depends(AuthPermission(["module_monitor:resource:download"]))],
)
async def download_file_controller(
@@ -120,7 +117,6 @@ async def download_file_controller(
filename = os.path.basename(file_path)
log.info(f"下载文件成功: {filename}")
return UploadFileResponse(
file_path=file_path,
filename=filename,
@@ -131,7 +127,6 @@ async def download_file_controller(
@ResourceRouter.delete(
"/delete",
summary="删除文件",
description="删除指定文件或目录",
response_model=ResponseSchema[None],
dependencies=[Depends(AuthPermission(["module_monitor:resource:delete"]))],
)
@@ -148,14 +143,12 @@ async def delete_files_controller(
- JSONResponse: 包含删除结果的JSON响应。
"""
await ResourceService.delete_file_service(paths=paths)
log.info(f"删除文件成功: {paths}")
return SuccessResponse(msg="删除文件成功")
@ResourceRouter.post(
"/move",
summary="移动文件",
description="移动文件或目录",
response_model=ResponseSchema[None],
dependencies=[Depends(AuthPermission(["module_monitor:resource:move"]))],
)
@@ -170,14 +163,12 @@ async def move_file_controller(data: ResourceMoveSchema) -> JSONResponse:
- JSONResponse: 包含移动结果的JSON响应。
"""
await ResourceService.move_file_service(data=data)
log.info(f"移动文件成功: {data.source_path} -> {data.target_path}")
return SuccessResponse(msg="移动文件成功")
@ResourceRouter.post(
"/copy",
summary="复制文件",
description="复制文件或目录",
response_model=ResponseSchema[None],
dependencies=[Depends(AuthPermission(["module_monitor:resource:copy"]))],
)
@@ -192,14 +183,12 @@ async def copy_file_controller(data: ResourceCopySchema) -> JSONResponse:
- JSONResponse: 包含复制结果的JSON响应。
"""
await ResourceService.copy_file_service(data=data)
log.info(f"复制文件成功: {data.source_path} -> {data.target_path}")
return SuccessResponse(msg="复制文件成功")
@ResourceRouter.post(
"/rename",
summary="重命名文件",
description="重命名文件或目录",
response_model=ResponseSchema[None],
dependencies=[Depends(AuthPermission(["module_monitor:resource:rename"]))],
)
@@ -214,14 +203,12 @@ async def rename_file_controller(data: ResourceRenameSchema) -> JSONResponse:
- JSONResponse: 包含重命名结果的JSON响应。
"""
await ResourceService.rename_file_service(data=data)
log.info(f"重命名文件成功: {data.old_path} -> {data.new_name}")
return SuccessResponse(msg="重命名文件成功")
@ResourceRouter.post(
"/create-dir",
summary="创建目录",
description="在指定路径创建新目录",
response_model=ResponseSchema[None],
dependencies=[Depends(AuthPermission(["module_monitor:resource:create_dir"]))],
)
@@ -238,14 +225,12 @@ async def create_directory_controller(
- JSONResponse: 包含创建目录结果的JSON响应。
"""
await ResourceService.create_directory_service(data=data)
log.info(f"创建目录成功: {data.parent_path}/{data.dir_name}")
return SuccessResponse(msg="创建目录成功")
@ResourceRouter.post(
"/export",
summary="导出资源列表",
description="导出资源列表",
response_model=ResponseSchema[None],
dependencies=[Depends(AuthPermission(["module_monitor:resource:export"]))],
)
@@ -268,7 +253,6 @@ async def export_resource_list_controller(
)
export_result = await ResourceService.export_resource_service(data_list=result_dict_list)
log.info("导出资源列表成功")
return StreamResponse(
data=bytes2file_response(export_result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
@@ -1,15 +1,15 @@
import ast
import os
import re
import shutil
import urllib.parse
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
from app.config.setting import settings
from app.core.exceptions import CustomException
from app.core.logger import log
from app.core.logger import logger
from app.utils.excel_util import ExcelUtil
from .schema import (
@@ -36,18 +36,18 @@ class ResourceService:
@classmethod
def _get_resource_root(cls) -> str:
"""
获取资源管理根目录(仅允许访问 upload 目录)
获取资源管理根目录(仅允许访问 upload/resource 目录)
返回:
- str: 资源管理根目录路径(upload 目录)。
- str: 资源管理根目录路径(upload/resource 目录)。
"""
if not settings.STATIC_ENABLE:
raise CustomException(msg="静态文件服务未启用")
# 限制只能管理 upload 目录
upload_root = os.path.join(str(settings.STATIC_ROOT), "upload")
# 确保 upload 目录存在
os.makedirs(upload_root, exist_ok=True)
return upload_root
# 限制只能管理 upload/resource 目录(与 upload_type="resource" 保持一致)
resource_root = os.path.join(str(settings.STATIC_ROOT), "upload", "resource")
# 确保 resource 目录存在
os.makedirs(resource_root, exist_ok=True)
return resource_root
@classmethod
def _get_safe_path(cls, path: str | None = None) -> str:
@@ -107,13 +107,13 @@ class ResourceService:
# 检查路径遍历攻击
if ".." in path or "\x00" in path:
log.error(f"检测到路径遍历攻击尝试: {path}")
logger.error(f"检测到路径遍历攻击尝试: {path}")
raise CustomException(msg="非法的路径格式")
# URL 解码检查
decoded_path = urllib.parse.unquote(path)
if ".." in decoded_path:
log.error(f"检测到编码后的路径遍历攻击: {path}")
logger.error(f"检测到编码后的路径遍历攻击: {path}")
raise CustomException(msg="非法的路径格式")
# 构建完整路径
@@ -128,7 +128,7 @@ class ResourceService:
not safe_path_abs.startswith(resource_root_abs + os.sep)
and safe_path_abs != resource_root_abs
):
log.error(
logger.error(
f"路径遍历攻击被阻止: 尝试访问 {safe_path_abs}, 但根目录是 {resource_root_abs}"
)
raise CustomException(msg="访问路径不在允许范围内")
@@ -185,7 +185,7 @@ class ResourceService:
]
for pattern in dangerous_patterns:
if re.search(pattern, filename, re.IGNORECASE):
log.error(f"检测到文件名路径遍历攻击: {filename}")
logger.error(f"检测到文件名路径遍历攻击: {filename}")
# 返回安全文件名,不包含原始文件名
return f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}"
@@ -194,7 +194,7 @@ class ResourceService:
decoded_twice = urllib.parse.unquote(decoded)
for check in [decoded, decoded_twice]:
if ".." in check or "/" in check or "\\" in check:
log.error(f"检测到编码后的文件名攻击: {filename}")
logger.error(f"检测到编码后的文件名攻击: {filename}")
return f"file_{datetime.now().strftime('%Y%m%d%H%M%S')}"
# 使用 os.path.basename 提取纯文件名(移除路径)
@@ -281,60 +281,48 @@ class ResourceService:
return http_url
@classmethod
def _get_file_info(cls, file_path: str, base_url: str | None = None) -> dict:
def _get_file_info(cls, file_path: str, base_url: str | None = None) -> ResourceItemSchema | None:
"""
获取文件或目录的详细信息,如名称、大小、创建时间、修改时间、路径、深度、HTTP URL、是否隐藏、是否为目录等
获取文件或目录的详细信息。
参数:
- file_path (str): 文件或目录的路径(必须是绝对路径)。
- base_url (str | None): 基础URL,用于生成完整URL。
返回:
- dict: 文件或目录的详细信息字典
- ResourceItemSchema | None: 文件或目录的详细信息,路径不存在时返回 None
"""
try:
# 直接使用传入的路径(已经是绝对路径)
safe_path = file_path
if not os.path.exists(safe_path):
return {}
return None
stat = os.stat(safe_path)
path_obj = Path(safe_path)
resource_root = cls._get_resource_root()
# 计算相对路径(相对于资源根目录)
try:
relative_path = os.path.relpath(safe_path, resource_root)
except ValueError:
relative_path = os.path.basename(safe_path)
# 生成HTTP URL路径
http_url = cls._generate_http_url(safe_path, base_url)
# 检查是否为隐藏文件(文件名以点开头)
is_hidden = path_obj.name.startswith(".")
# 对于目录,设置is_directory字段(兼容前端)
is_directory = os.path.isdir(safe_path)
# 将datetime对象转换为ISO格式的字符串,确保JSON序列化成功
created_time = datetime.fromtimestamp(stat.st_ctime).isoformat()
modified_time = datetime.fromtimestamp(stat.st_mtime).isoformat()
return {
"name": path_obj.name,
"file_url": http_url, # 统一使用file_url字段
"relative_path": relative_path,
"is_file": os.path.isfile(safe_path),
"is_dir": is_directory,
"size": stat.st_size if os.path.isfile(safe_path) else None,
"created_time": created_time,
"modified_time": modified_time,
"is_hidden": is_hidden,
}
return ResourceItemSchema(
name=path_obj.name,
file_url=http_url,
relative_path=relative_path,
is_file=os.path.isfile(safe_path),
is_dir=os.path.isdir(safe_path),
size=stat.st_size if os.path.isfile(safe_path) else None,
created_time=datetime.fromtimestamp(stat.st_ctime),
modified_time=datetime.fromtimestamp(stat.st_mtime),
is_hidden=is_hidden,
)
except Exception as e:
log.error(f"获取文件信息失败: {e!s}")
return {}
logger.error(f"获取文件信息失败: {e!s}")
return None
@classmethod
async def get_directory_list_service(
@@ -342,7 +330,7 @@ class ResourceService:
path: str | None = None,
include_hidden: bool = False,
base_url: str | None = None,
) -> dict:
) -> ResourceDirectorySchema:
"""
获取目录列表
@@ -384,30 +372,30 @@ class ResourceService:
file_info = cls._get_file_info(item_path, base_url)
if file_info:
items.append(ResourceItemSchema(**file_info))
items.append(file_info)
if file_info["is_file"]:
if file_info.is_file:
total_files += 1
total_size += file_info.get("size", 0) or 0
elif file_info["is_dir"]:
total_size += file_info.size or 0
elif file_info.is_dir:
total_dirs += 1
except PermissionError:
raise CustomException(msg="没有权限访问此目录")
return ResourceDirectorySchema(
path=display_path, # 返回HTTP URL路径而不是文件系统路径
path=display_path,
name=os.path.basename(safe_path),
items=items,
total_files=total_files,
total_dirs=total_dirs,
total_size=total_size,
).model_dump()
)
except CustomException:
raise
except Exception as e:
log.error(f"获取目录列表失败: {e!s}")
logger.error(f"获取目录列表失败: {e!s}")
raise CustomException(msg=f"获取目录列表失败: {e!s}")
@classmethod
@@ -416,7 +404,7 @@ class ResourceService:
search: ResourceSearchQueryParam | None = None,
order_by: str | None = None,
base_url: str | None = None,
) -> list[dict]:
) -> list[ResourceItemSchema]:
"""
搜索资源列表(用于分页和导出)
@@ -426,7 +414,7 @@ class ResourceService:
- base_url (str | None): 基础URL,用于生成完整URL。
返回:
- list[dict]: 资源详情字典列表。
- list[ResourceItemSchema]: 资源详情列表。
"""
try:
# 确定搜索路径
@@ -458,7 +446,7 @@ class ResourceService:
# 应用名称过滤
if search and hasattr(search, "name") and search.name and search.name[1]:
search_keyword = search.name[1].lower()
if search_keyword not in file_info.get("name", "").lower():
if search_keyword not in file_info.name.lower():
continue
all_resources.append(file_info)
@@ -476,16 +464,16 @@ class ResourceService:
return sorted_resources
except Exception as e:
log.error(f"搜索资源失败: {e!s}")
logger.error(f"搜索资源失败: {e!s}")
raise CustomException(msg=f"搜索资源失败: {e!s}")
@classmethod
async def export_resource_service(cls, data_list: list[dict]) -> bytes:
async def export_resource_service(cls, data_list: list[ResourceItemSchema]) -> bytes:
"""
导出资源列表
参数:
- data_list (list[dict]): 资源详情字典列表。
- data_list (list[ResourceItemSchema]): 资源详情列表。
返回:
- bytes: Excel文件的二进制数据。
@@ -500,7 +488,7 @@ class ResourceService:
}
# 复制数据并转换状态
export_data = data_list.copy()
export_data = [item.model_dump() for item in data_list]
# 格式化文件大小
for item in export_data:
@@ -547,55 +535,38 @@ class ResourceService:
@classmethod
def _sort_results(
cls, results: list[dict], order_by: str | None = None
) -> list[dict[Any, Any]]:
cls, results: list[ResourceItemSchema], order_by: str | None = None
) -> list[ResourceItemSchema]:
"""
排序搜索结果
参数:
- results (list[dict]): 资源详情字典列表。
- results (list[ResourceItemSchema]): 资源详情列表。
- order_by (str | None): 排序参数。
返回:
- list[dict]: 排序后的资源详情字典列表。
- list[ResourceItemSchema]: 排序后的资源详情列表。
"""
try:
# 默认按名称升序排序
if not order_by:
return sorted(results, key=lambda x: x.get("name", ""), reverse=False)
return sorted(results, key=lambda x: x.name, reverse=False)
# 解析order_by参数,格式: [{'field':'asc/desc'}]
sort_conditions = eval(order_by)
sort_conditions = ast.literal_eval(order_by)
if isinstance(sort_conditions, list):
# 构建排序键函数
def sort_key(item):
"""
按多条排序条件从资源项中抽取比较键(支持时间字段转 datetime)。
参数:
- item (dict): 单条资源详情字典。
返回:
- list: 用于 `sorted` 的多字段键列表。
"""
keys = []
for cond in sort_conditions:
field = cond.get("field", "name")
cond.get("direction", "asc")
# 获取字段值,默认为空字符串
value = item.get(field, "")
# 如果是日期字段,转换为可比较的格式
value = getattr(item, field, "")
if (
field
in [
"created_time",
"modified_time",
"accessed_time",
]
in ["created_time", "modified_time", "accessed_time"]
and value
):
value = datetime.fromisoformat(value)
if isinstance(value, str):
value = datetime.fromisoformat(value)
keys.append(value)
return keys
@@ -634,13 +605,12 @@ class ResourceService:
raise CustomException(msg="路径不是文件")
# 返回本地文件路径给 FileResponse 使用
log.info(f"定位文件路径: {safe_path}")
return safe_path
except CustomException:
raise
except Exception as e:
log.error(f"下载文件失败: {e!s}")
logger.error(f"下载文件失败: {e!s}")
raise CustomException(msg=f"下载文件失败: {e!s}")
@classmethod
@@ -660,15 +630,13 @@ class ResourceService:
safe_path = cls._get_safe_path(path)
if not os.path.exists(safe_path):
log.error(f"路径不存在,跳过: {path}")
logger.error(f"路径不存在,跳过: {path}")
raise CustomException(msg=f"路径不存在: {path}")
if os.path.isfile(safe_path):
os.remove(safe_path)
log.info(f"删除文件成功: {safe_path}")
elif os.path.isdir(safe_path):
shutil.rmtree(safe_path)
log.info(f"删除目录成功: {safe_path}")
@classmethod
async def delete_file_service(cls, paths: list[str]) -> None:
@@ -692,7 +660,7 @@ class ResourceService:
try:
cls._delete_single_path(path)
except Exception as e:
log.error(f"删除失败 {path}: {e!s}")
logger.error(f"删除失败 {path}: {e!s}")
raise CustomException(msg=f"删除失败 {path}: {e!s}")
@classmethod
@@ -755,12 +723,11 @@ class ResourceService:
# 移动文件
shutil.move(source_path, target_path)
log.info(f"移动成功: {source_path} -> {target_path}")
except CustomException:
raise
except Exception as e:
log.error(f"移动失败: {e!s}")
logger.error(f"移动失败: {e!s}")
raise CustomException(msg=f"移动失败: {e!s}")
@classmethod
@@ -795,12 +762,10 @@ class ResourceService:
else:
shutil.copytree(source_path, target_path, dirs_exist_ok=data.overwrite)
log.info(f"复制成功: {source_path} -> {target_path}")
except CustomException:
raise
except Exception as e:
log.error(f"复制失败: {e!s}")
logger.error(f"复制失败: {e!s}")
raise CustomException(msg=f"复制失败: {e!s}")
@classmethod
@@ -826,7 +791,7 @@ class ResourceService:
# 如果新名称被重置,说明检测到攻击
if safe_new_name.startswith("file_") and safe_new_name != data.new_name:
log.error(f"重命名时检测到路径遍历攻击,原始名称: {data.new_name}")
logger.error(f"重命名时检测到路径遍历攻击,原始名称: {data.new_name}")
raise CustomException(msg="新名称包含非法字符")
# 生成新路径
@@ -841,7 +806,7 @@ class ResourceService:
not new_path_abs.startswith(resource_root_abs + os.sep)
and new_path_abs != resource_root_abs
):
log.error(f"重命名时检测到越权访问: {new_path_abs}")
logger.error(f"重命名时检测到越权访问: {new_path_abs}")
raise CustomException(msg="目标路径不在允许范围内")
if os.path.exists(new_path):
@@ -849,12 +814,11 @@ class ResourceService:
# 重命名
os.rename(old_path, new_path)
log.info(f"重命名成功: {old_path} -> {new_path}")
except CustomException:
raise
except Exception as e:
log.error(f"重命名失败: {e!s}")
logger.error(f"重命名失败: {e!s}")
raise CustomException(msg=f"重命名失败: {e!s}")
@classmethod
@@ -882,7 +846,7 @@ class ResourceService:
# 如果目录名被重置,说明检测到攻击
if safe_dir_name.startswith("file_") and safe_dir_name != data.dir_name:
log.error(f"创建目录时检测到路径遍历攻击,原始名称: {data.dir_name}")
logger.error(f"创建目录时检测到路径遍历攻击,原始名称: {data.dir_name}")
raise CustomException(msg="目录名称包含非法字符")
# 生成新目录路径
@@ -896,7 +860,7 @@ class ResourceService:
not new_dir_path_abs.startswith(resource_root_abs + os.sep)
and new_dir_path_abs != resource_root_abs
):
log.error(f"创建目录时检测到越权访问: {new_dir_path_abs}")
logger.error(f"创建目录时检测到越权访问: {new_dir_path_abs}")
raise CustomException(msg="目标路径不在允许范围内")
if os.path.exists(new_dir_path):
@@ -904,12 +868,11 @@ class ResourceService:
# 创建目录
os.makedirs(new_dir_path)
log.info(f"创建目录成功: {new_dir_path}")
except CustomException:
raise
except Exception as e:
log.error(f"创建目录失败: {e!s}")
logger.error(f"创建目录失败: {e!s}")
raise CustomException(msg=f"创建目录失败: {e!s}")
@classmethod
@@ -4,18 +4,16 @@ from fastapi.responses import JSONResponse
from app.api.v1.module_monitor.server.schema import ServerMonitorSchema
from app.common.response import ResponseSchema, SuccessResponse
from app.core.dependencies import AuthPermission
from app.core.logger import log
from app.core.router_class import OperationLogRoute
from .service import ServerService
ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=["服务器监控"])
ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=["系统监控/服务器监控"])
@ServerRouter.get(
"/info",
summary="查询服务器监控信息",
description="查询服务器监控信息",
dependencies=[Depends(AuthPermission(["module_monitor:server:query"]))],
response_model=ResponseSchema[ServerMonitorSchema],
)
@@ -27,6 +25,5 @@ async def get_monitor_server_info_controller() -> JSONResponse:
- JSONResponse: 包含服务器监控信息的JSON响应。
"""
result_dict = await ServerService.get_server_monitor_info_service()
log.info(f"获取服务器监控信息成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="获取服务器监控信息成功")
@@ -1,11 +1,9 @@
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field
class CpuInfoSchema(BaseModel):
"""CPU信息模型"""
model_config = ConfigDict(from_attributes=True)
cpu_num: int = Field(description="CPU核心数")
used: float = Field(ge=0, le=100, description="CPU用户使用率(%)")
sys: float = Field(ge=0, le=100, description="CPU系统使用率(%)")
@@ -15,8 +13,6 @@ class CpuInfoSchema(BaseModel):
class MemoryInfoSchema(BaseModel):
"""内存信息模型"""
model_config = ConfigDict(from_attributes=True)
total: str = Field(description="内存总量")
used: str = Field(description="已用内存")
free: str = Field(description="剩余内存")
@@ -26,8 +22,6 @@ class MemoryInfoSchema(BaseModel):
class SysInfoSchema(BaseModel):
"""系统信息模型"""
model_config = ConfigDict(from_attributes=True)
computer_ip: str = Field(description="服务器IP")
computer_name: str = Field(description="服务器名称")
os_arch: str = Field(description="系统架构")
@@ -38,8 +32,6 @@ class SysInfoSchema(BaseModel):
class PyInfoSchema(BaseModel):
"""Python运行信息模型"""
model_config = ConfigDict(from_attributes=True)
name: str = Field(description="Python名称")
version: str = Field(description="Python版本")
start_time: str = Field(description="启动时间")
@@ -54,8 +46,6 @@ class PyInfoSchema(BaseModel):
class DiskInfoSchema(BaseModel):
"""磁盘信息模型"""
model_config = ConfigDict(from_attributes=True)
dir_name: str = Field(description="磁盘路径")
sys_type_name: str = Field(description="文件系统类型")
type_name: str = Field(description="磁盘类型")
@@ -68,8 +58,6 @@ class DiskInfoSchema(BaseModel):
class ServerMonitorSchema(BaseModel):
"""服务器监控信息模型"""
model_config = ConfigDict(from_attributes=True)
cpu: CpuInfoSchema = Field(description="CPU信息")
mem: MemoryInfoSchema = Field(description="内存信息")
py: PyInfoSchema = Field(description="Python运行信息")
@@ -21,7 +21,7 @@ class ServerService:
"""服务监控模块服务层"""
@classmethod
async def get_server_monitor_info_service(cls) -> dict:
async def get_server_monitor_info_service(cls) -> ServerMonitorSchema:
"""
获取服务器监控信息
@@ -34,7 +34,7 @@ class ServerService:
sys=cls._get_system_info(),
py=cls._get_python_info(),
disks=cls._get_disk_info(),
).model_dump()
)
@classmethod
def _get_cpu_info(cls) -> CpuInfoSchema: