style: 移除Python文件中的编码声明并优化代码格式

refactor: 重构前端组件和样式,添加AI助手功能

docs: 更新README文档,添加ruff代码检查说明

feat: 新增AI助手相关API和前端组件

chore: 更新.gitignore文件,添加ruff缓存配置

fix: 修复前端布局和设置相关的问题

perf: 优化代码结构和性能,移除冗余代码

test: 更新测试文件,移除编码声明

build: 更新依赖版本,调整requirements.txt
This commit is contained in:
zhangtao
2026-01-16 01:13:59 +08:00
parent 59c1ce1104
commit d50dd9dd1e
210 changed files with 5236 additions and 4037 deletions
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from .cache.controller import CacheRouter
-1
View File
@@ -1,2 +1 @@
# -*- coding: utf-8 -*-
+21 -22
View File
@@ -1,18 +1,17 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from redis.asyncio.client import Redis
from app.common.response import SuccessResponse
from app.core.exceptions import CustomException
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=["缓存监控"])
@@ -23,11 +22,11 @@ CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=["
description="获取缓存监控信息"
)
async def get_monitor_cache_info_controller(
redis: Redis = Depends(redis_getter)
redis: Annotated[Redis, Depends(redis_getter)]
) -> JSONResponse:
"""
获取缓存监控统计信息
返回:
- JSONResponse: 包含缓存监控统计信息的JSON响应
"""
@@ -45,7 +44,7 @@ async def get_monitor_cache_info_controller(
async def get_monitor_cache_name_controller() -> JSONResponse:
"""
获取缓存名称列表
返回:
- JSONResponse: 包含缓存名称列表的JSON响应
"""
@@ -62,14 +61,14 @@ async def get_monitor_cache_name_controller() -> JSONResponse:
)
async def get_monitor_cache_key_controller(
cache_name: str,
redis: Redis = Depends(redis_getter)
redis: Annotated[Redis, Depends(redis_getter)]
) -> JSONResponse:
"""
获取指定缓存名称下的键名列表
参数:
- cache_name (str): 缓存名称
返回:
- JSONResponse: 包含缓存键名列表的JSON响应
"""
@@ -85,17 +84,17 @@ async def get_monitor_cache_key_controller(
description="获取缓存值"
)
async def get_monitor_cache_value_controller(
cache_name: str,
cache_name: str,
cache_key: str,
redis: Redis = Depends(redis_getter)
)-> JSONResponse:
redis: Annotated[Redis, Depends(redis_getter)]
) -> JSONResponse:
"""
获取指定缓存键的值
参数:
- cache_name (str): 缓存名称
- cache_key (str): 缓存键
返回:
- JSONResponse: 包含缓存值的JSON响应
"""
@@ -112,14 +111,14 @@ async def get_monitor_cache_value_controller(
)
async def clear_monitor_cache_name_controller(
cache_name: str,
redis: Redis = Depends(redis_getter)
redis: Annotated[Redis, Depends(redis_getter)]
) -> JSONResponse:
"""
清除指定缓存名称下的所有缓存
参数:
- cache_name (str): 缓存名称
返回:
- JSONResponse: 包含清除结果的JSON响应
"""
@@ -138,14 +137,14 @@ async def clear_monitor_cache_name_controller(
)
async def clear_monitor_cache_key_controller(
cache_key: str,
redis: Redis = Depends(redis_getter)
redis: Annotated[Redis, Depends(redis_getter)]
) -> JSONResponse:
"""
清除指定缓存键
参数:
- cache_key (str): 缓存键
返回:
- JSONResponse: 包含清除结果的JSON响应
"""
@@ -163,11 +162,11 @@ async def clear_monitor_cache_key_controller(
description="清除所有缓存"
)
async def clear_monitor_cache_all_controller(
redis: Redis = Depends(redis_getter)
redis: Annotated[Redis, Depends(redis_getter)]
) -> JSONResponse:
"""
清除所有缓存
返回:
- JSONResponse: 包含清除结果的JSON响应
"""
+1 -2
View File
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
from typing import Any
from pydantic import BaseModel, ConfigDict, Field
+24 -30
View File
@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
from redis.asyncio.client import Redis
from app.common.enums import RedisInitKeyConfig
from app.core.redis_crud import RedisCURD
from .schema import CacheMonitorSchema, CacheInfoSchema
from .schema import CacheInfoSchema, CacheMonitorSchema
class CacheService:
@@ -14,13 +12,13 @@ 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) -> dict:
"""
获取缓存监控信息。
参数:
- redis (Redis): Redis 对象。
返回:
- dict: 缓存监控信息字典。
"""
@@ -29,42 +27,38 @@ class CacheService:
command_stats_dict = await RedisCURD(redis).commandstats()
command_stats = [
dict(name=key.split('_')[1], value=str(value.get('calls'))) for key, value in command_stats_dict.items()
{'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()
@classmethod
async def get_cache_monitor_cache_name_service(cls)->list:
async def get_cache_monitor_cache_name_service(cls) -> list:
"""
获取缓存名称列表信息。
返回:
- list: 缓存名称列表信息。
"""
name_list = []
for key_config in RedisInitKeyConfig:
name_list.append(
CacheInfoSchema(
name_list = [CacheInfoSchema(
cache_key='',
cache_name=key_config.key,
cache_value='',
remark=key_config.remark,
).model_dump()
)
).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:
async def get_cache_monitor_cache_key_service(cls, redis: Redis, cache_name: str) -> list:
"""
获取缓存键名列表信息。
参数:
- redis (Redis): Redis 对象。
- cache_name (str): 缓存名称。
返回:
- list: 缓存键名列表信息。
"""
@@ -74,15 +68,15 @@ class CacheService:
return cache_key_list
@classmethod
async def get_cache_monitor_cache_value_service(cls, redis: Redis, cache_name: str, cache_key: str)->dict:
async def get_cache_monitor_cache_value_service(cls, redis: Redis, cache_name: str, cache_key: str) -> dict:
"""
获取缓存内容信息。
参数:
- redis (Redis): Redis 对象。
- cache_name (str): 缓存名称。
- cache_key (str): 缓存键名。
返回:
- dict: 缓存内容信息字典。
"""
@@ -91,14 +85,14 @@ class CacheService:
return CacheInfoSchema(cache_key=cache_key, 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:
async def clear_cache_monitor_cache_name_service(cls, redis: Redis, cache_name: str) -> bool:
"""
清除指定缓存名称对应的所有键值。
参数:
- redis (Redis): Redis 对象。
- cache_name (str): 缓存名称。
返回:
- bool: 是否清理成功。
"""
@@ -109,14 +103,14 @@ class CacheService:
return True
@classmethod
async def clear_cache_monitor_cache_key_service(cls, redis: Redis, cache_key: str)->bool:
async def clear_cache_monitor_cache_key_service(cls, redis: Redis, cache_key: str) -> bool:
"""
清除匹配指定键名的所有键值。
参数:
- redis (Redis): Redis 对象。
- cache_key (str): 缓存键名。
返回:
- bool: 是否清理成功。
"""
@@ -127,13 +121,13 @@ class CacheService:
return True
@classmethod
async def clear_cache_monitor_all_service(cls, redis: Redis)->bool:
async def clear_cache_monitor_all_service(cls, redis: Redis) -> bool:
"""
清除所有缓存。
参数:
- redis (Redis): Redis 对象。
返回:
- bool: 是否清理成功。
"""
@@ -1,2 +1 @@
# -*- coding: utf-8 -*-
@@ -1,69 +1,68 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Body, Depends
from fastapi.responses import JSONResponse
from redis.asyncio.client import Redis
from app.common.request import PaginationService
from app.common.response import SuccessResponse,ErrorResponse
from app.core.dependencies import AuthPermission, redis_getter
from app.common.response import ErrorResponse, SuccessResponse
from app.core.base_params import PaginationQueryParam
from app.core.router_class import OperationLogRoute
from app.core.dependencies import AuthPermission, redis_getter
from app.core.logger import log
from app.core.router_class import OperationLogRoute
from .schema import OnlineQueryParam
from .service import OnlineService
OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["在线用户"])
@OnlineRouter.get(
'/list',
'/list',
dependencies=[Depends(AuthPermission(['module_monitor:online:query']))],
summary="获取在线用户列表",
description="获取在线用户列表"
)
async def get_online_list_controller(
redis: Redis = Depends(redis_getter),
paging_query: PaginationQueryParam = Depends(),
search: OnlineQueryParam = Depends()
)->JSONResponse:
redis: Annotated[Redis, Depends(redis_getter)],
paging_query: Annotated[PaginationQueryParam, Depends()],
search: Annotated[OnlineQueryParam, Depends()]
) -> JSONResponse:
"""
获取在线用户列表
参数:
- redis (Redis): Redis异步客户端实例。
- paging_query (PaginationQueryParam): 分页查询参数模型。
- search (OnlineQueryParam): 查询参数模型。
返回:
- JSONResponse: 包含在线用户列表的JSON响应。
"""
result_dict_list = await OnlineService.get_online_list_service(redis=redis, search=search)
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= paging_query.page_no, page_size = paging_query.page_size)
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=paging_query.page_no, page_size=paging_query.page_size)
log.info('获取成功')
return SuccessResponse(data=result_dict,msg='获取成功')
return SuccessResponse(data=result_dict, msg='获取成功')
@OnlineRouter.delete(
'/delete',
'/delete',
dependencies=[Depends(AuthPermission(['module_monitor:online:delete']))],
summary="强制下线",
description="强制下线"
)
async def delete_online_controller(
session_id: str = Body(..., description="会话编号"),
redis: Redis = Depends(redis_getter),
)->JSONResponse:
session_id: Annotated[str, Body(description="会话编号")],
redis: Annotated[Redis, Depends(redis_getter)],
) -> JSONResponse:
"""
强制下线指定在线用户
参数:
- session_id (str): 在线用户会话ID。
- redis (Redis): Redis异步客户端实例。
返回:
- JSONResponse: 包含操作结果的JSON响应。
"""
@@ -71,25 +70,25 @@ async def delete_online_controller(
if is_ok:
log.info("强制下线成功")
return SuccessResponse(msg="强制下线成功")
else:
log.info("强制下线失败")
return ErrorResponse(msg="强制下线失败")
log.info("强制下线失败")
return ErrorResponse(msg="强制下线失败")
@OnlineRouter.delete(
'/clear',
'/clear',
dependencies=[Depends(AuthPermission(['module_monitor:online:delete']))],
summary="清除所有在线用户",
description="清除所有在线用户"
)
async def clear_online_controller(
redis: Redis = Depends(redis_getter),
)->JSONResponse:
redis: Annotated[Redis, Depends(redis_getter)],
) -> JSONResponse:
"""
清除所有在线用户
参数:
- redis (Redis): Redis异步客户端实例。
返回:
- JSONResponse: 包含操作结果的JSON响应。
"""
@@ -97,6 +96,5 @@ async def clear_online_controller(
if is_ok:
log.info("清除所有在线用户成功")
return SuccessResponse(msg="清除所有在线用户成功")
else:
log.info("清除所有在线用户失败")
return ErrorResponse(msg="清除所有在线用户失败")
log.info("清除所有在线用户失败")
return ErrorResponse(msg="清除所有在线用户失败")
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field
from app.core.validator import DateTimeStr
@@ -30,11 +28,11 @@ class OnlineQueryParam:
def __init__(
self,
name: str | None = Query(None, description="登录名称"),
name: str | None = Query(None, description="登录名称"),
ipaddr: str | None = Query(None, description="登陆IP地址"),
login_location: str | None = Query(None, description="登录所属地"),
) -> None:
# 模糊查询字段
self.name = ("like", f"%{name}%") if name else None
self.login_location = ("like", f"%{login_location}%") if login_location else None
@@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
import json
from redis.asyncio.client import Redis
from app.common.enums import RedisInitKeyConfig
from app.core.logger import log
from app.core.redis_crud import RedisCURD
from app.core.security import decode_access_token
from app.core.logger import log
from .schema import OnlineQueryParam
@@ -18,11 +17,11 @@ class OnlineService:
async def get_online_list_service(cls, redis: Redis, search: OnlineQueryParam | None = None) -> list[dict]:
"""
获取在线用户列表信息(支持分页和搜索)
参数:
- redis (Redis): Redis异步客户端实例。
- search (OnlineQueryParam | None): 查询参数模型。
返回:
- list[dict]: 在线用户详情字典列表。
"""
@@ -36,7 +35,7 @@ class OnlineService:
continue
try:
payload = decode_access_token(token=token)
session_info = json.loads(payload.sub)
session_info = json.loads(payload.sub)
if cls._match_search_conditions(session_info, search):
online_users.append(session_info)
except Exception as e:
@@ -44,19 +43,18 @@ class OnlineService:
continue
# 按照 login_time 倒序排序
online_users.sort(key=lambda x: x.get('login_time', ''), reverse=True)
return online_users
return online_users
@classmethod
async def delete_online_service(cls, redis: Redis, session_id: str) -> bool:
"""
强制下线指定在线用户
参数:
- redis (Redis): Redis异步客户端实例。
- session_id (str): 在线用户会话ID。
返回:
- bool: 如果操作成功则返回True,否则返回False。
"""
@@ -64,18 +62,17 @@ class OnlineService:
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
@classmethod
async def clear_online_service(cls, redis: Redis) -> bool:
"""
强制下线所有在线用户
参数:
- redis (Redis): Redis异步客户端实例。
返回:
- bool: 如果操作成功则返回True,否则返回False。
"""
@@ -83,19 +80,18 @@ class OnlineService:
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*")
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:*")
log.info(f"清除所有在线用户会话成功")
log.info("清除所有在线用户会话成功")
return True
@staticmethod
def _match_search_conditions(online_info: dict, search: OnlineQueryParam | None = None) -> bool:
"""
检查是否匹配搜索条件
参数:
- online_info (dict): 在线用户信息字典。
- search (OnlineQueryParam | None): 查询参数模型。
返回:
- bool: 如果匹配则返回True,否则返回False。
"""
@@ -1 +0,0 @@
# -*- coding: utf-8 -*-
@@ -1,84 +1,84 @@
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Body, Depends, Query, Request, UploadFile, Form
from fastapi.responses import JSONResponse, StreamingResponse, FileResponse
from fastapi import APIRouter, Body, Depends, Form, Query, Request, UploadFile
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from app.common.response import StreamResponse, SuccessResponse
from app.common.request import PaginationService
from app.core.router_class import OperationLogRoute
from app.utils.common_util import bytes2file_response
from app.common.response import StreamResponse, SuccessResponse
from app.core.base_params import PaginationQueryParam
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 .service import ResourceService
from .schema import (
ResourceMoveSchema,
ResourceCopySchema,
ResourceRenameSchema,
ResourceCreateDirSchema,
ResourceSearchQueryParam
ResourceMoveSchema,
ResourceRenameSchema,
ResourceSearchQueryParam,
)
from .service import ResourceService
ResourceRouter = APIRouter(route_class=OperationLogRoute, prefix="/resource", tags=["资源管理"])
@ResourceRouter.get(
"/list",
summary="获取目录列表",
"/list",
summary="获取目录列表",
description="获取指定目录下的文件和子目录列表",
dependencies=[Depends(AuthPermission(["module_monitor:resource:query"]))]
)
async def get_directory_list_controller(
request: Request,
page: PaginationQueryParam = Depends(),
search: ResourceSearchQueryParam = Depends(),
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[ResourceSearchQueryParam, Depends()],
) -> JSONResponse:
"""
获取目录列表
参数:
- request (Request): FastAPI请求对象,用于获取基础URL。
- page (PaginationQueryParam): 分页查询参数模型。
- search (ResourceSearchQueryParam): 资源查询参数模型。
返回:
- JSONResponse: 包含目录列表的JSON响应。
"""
# 获取资源列表(与案例模块保持一致的分页实现)
result_dict_list = await ResourceService.get_resources_list_service(
search=search,
search=search,
base_url=str(request.base_url)
)
# 使用分页服务进行分页处理(与案例模块保持一致)
result_dict = await PaginationService.paginate(
data_list=result_dict_list,
page_no=page.page_no,
data_list=result_dict_list,
page_no=page.page_no,
page_size=page.page_size
)
log.info(f"获取目录列表成功: {getattr(search, 'name', None) or ''}")
return SuccessResponse(data=result_dict, msg="获取目录列表成功")
@ResourceRouter.post(
"/upload",
summary="上传文件",
"/upload",
summary="上传文件",
description="上传文件到指定目录",
dependencies=[Depends(AuthPermission(["module_monitor:resource:upload"]))])
async def upload_file_controller(
file: UploadFile,
request: Request,
target_path: str | None = Form(None, description="目标目录路径")
target_path: Annotated[str | None, Form(description="目标目录路径")] = None
) -> JSONResponse:
"""
上传文件
参数:
- file (UploadFile): 要上传的文件对象。
- request (Request): FastAPI请求对象,用于获取基础URL。
- target_path (str | None): 目标目录路径,默认为None。
返回:
- JSONResponse: 包含上传文件信息的JSON响应。
"""
@@ -92,22 +92,22 @@ async def upload_file_controller(
@ResourceRouter.get(
"/download",
summary="下载文件",
"/download",
summary="下载文件",
description="下载指定文件",
dependencies=[Depends(AuthPermission(["module_monitor:resource:download"]))]
)
async def download_file_controller(
request: Request,
path: str = Query(..., description="文件路径")
path: Annotated[str, Query(description="文件路径")]
) -> FileResponse:
"""
下载文件
参数:
- request (Request): FastAPI请求对象,用于获取基础URL。
- path (str): 文件路径。
返回:
- FileResponse: 包含文件内容的文件响应。
"""
@@ -115,11 +115,11 @@ async def download_file_controller(
file_path=path,
base_url=str(request.base_url)
)
# 获取文件名
import os
filename = os.path.basename(file_path)
log.info(f"下载文件成功: {filename}")
return FileResponse(
path=file_path,
@@ -129,20 +129,20 @@ async def download_file_controller(
@ResourceRouter.delete(
"/delete",
summary="删除文件",
"/delete",
summary="删除文件",
description="删除指定文件或目录",
dependencies=[Depends(AuthPermission(["module_monitor:resource:delete"]))]
)
async def delete_files_controller(
paths: list[str] = Body(..., description="文件路径列表")
paths: Annotated[list[str], Body(description="文件路径列表")]
) -> JSONResponse:
"""
删除文件
参数:
- paths (list[str]): 文件路径列表。
返回:
- JSONResponse: 包含删除结果的JSON响应。
"""
@@ -152,8 +152,8 @@ async def delete_files_controller(
@ResourceRouter.post(
"/move",
summary="移动文件",
"/move",
summary="移动文件",
description="移动文件或目录",
dependencies=[Depends(AuthPermission(["module_monitor:resource:move"]))]
)
@@ -162,10 +162,10 @@ async def move_file_controller(
) -> JSONResponse:
"""
移动文件
参数:
- data (ResourceMoveSchema): 移动文件参数模型。
返回:
- JSONResponse: 包含移动结果的JSON响应。
"""
@@ -175,8 +175,8 @@ async def move_file_controller(
@ResourceRouter.post(
"/copy",
summary="复制文件",
"/copy",
summary="复制文件",
description="复制文件或目录",
dependencies=[Depends(AuthPermission(["module_monitor:resource:copy"]))]
)
@@ -185,10 +185,10 @@ async def copy_file_controller(
) -> JSONResponse:
"""
复制文件
参数:
- data (ResourceCopySchema): 复制文件参数模型。
返回:
- JSONResponse: 包含复制结果的JSON响应。
"""
@@ -198,8 +198,8 @@ async def copy_file_controller(
@ResourceRouter.post(
"/rename",
summary="重命名文件",
"/rename",
summary="重命名文件",
description="重命名文件或目录",
dependencies=[Depends(AuthPermission(["module_monitor:resource:rename"]))]
)
@@ -208,10 +208,10 @@ async def rename_file_controller(
) -> JSONResponse:
"""
重命名文件
参数:
- data (ResourceRenameSchema): 重命名文件参数模型。
返回:
- JSONResponse: 包含重命名结果的JSON响应。
"""
@@ -221,8 +221,8 @@ async def rename_file_controller(
@ResourceRouter.post(
"/create-dir",
summary="创建目录",
"/create-dir",
summary="创建目录",
description="在指定路径创建新目录",
dependencies=[Depends(AuthPermission(["module_monitor:resource:create_dir"]))]
)
@@ -231,10 +231,10 @@ async def create_directory_controller(
) -> JSONResponse:
"""
创建目录
参数:
- data (ResourceCreateDirSchema): 创建目录参数模型。
返回:
- JSONResponse: 包含创建目录结果的JSON响应。
"""
@@ -244,22 +244,22 @@ async def create_directory_controller(
@ResourceRouter.post(
"/export",
summary="导出资源列表",
"/export",
summary="导出资源列表",
description="导出资源列表",
dependencies=[Depends(AuthPermission(["module_monitor:resource:export"]))]
)
async def export_resource_list_controller(
request: Request,
search: ResourceSearchQueryParam = Depends()
search: Annotated[ResourceSearchQueryParam, Depends()]
) -> StreamingResponse:
"""
导出资源列表
参数:
- request (Request): FastAPI请求对象,用于获取基础URL。
- search (ResourceSearchQueryParam): 资源查询参数模型。
返回:
- StreamingResponse: 包含导出资源列表的流式响应。
"""
@@ -269,7 +269,7 @@ async def export_resource_list_controller(
base_url=str(request.base_url)
)
export_result = await ResourceService.export_resource_service(data_list=result_dict_list)
log.info("导出资源列表成功")
return StreamResponse(
data=bytes2file_response(export_result),
@@ -277,4 +277,4 @@ async def export_resource_list_controller(
headers={
'Content-Disposition': 'attachment; filename=resource_list.xlsx'
}
)
)
@@ -1,15 +1,14 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from urllib.parse import urlparse
from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
class ResourceItemSchema(BaseModel):
"""资源项目模型"""
model_config = ConfigDict(from_attributes=True)
name: str = Field(..., description="文件名")
file_url: str = Field(..., description="文件URL路径")
relative_path: str = Field(..., description="相对路径")
@@ -51,7 +50,7 @@ class ResourceItemSchema(BaseModel):
class ResourceDirectorySchema(BaseModel):
"""资源目录模型"""
model_config = ConfigDict(from_attributes=True)
path: str = Field(..., description="目录路径")
name: str = Field(..., description="目录名称")
items: list[ResourceItemSchema] = Field(default_factory=list, description="目录项")
@@ -63,7 +62,7 @@ class ResourceDirectorySchema(BaseModel):
class ResourceUploadSchema(BaseModel):
"""资源上传响应模型"""
model_config = ConfigDict(from_attributes=True)
filename: str = Field(..., description="文件名")
file_url: str = Field(..., description="访问URL")
file_size: int = Field(..., description="文件大小")
@@ -73,11 +72,11 @@ class ResourceUploadSchema(BaseModel):
class ResourceMoveSchema(BaseModel):
"""资源移动模型"""
model_config = ConfigDict(from_attributes=True)
source_path: str = Field(..., description="源路径")
target_path: str = Field(..., description="目标路径")
overwrite: bool = Field(False, description="是否覆盖")
@field_validator('source_path', 'target_path')
@classmethod
def validate_paths(cls, value: str):
@@ -88,16 +87,15 @@ class ResourceMoveSchema(BaseModel):
class ResourceCopySchema(ResourceMoveSchema):
"""资源复制模型"""
pass
class ResourceRenameSchema(BaseModel):
"""资源重命名模型"""
model_config = ConfigDict(from_attributes=True)
old_path: str = Field(..., description="原路径")
new_name: str = Field(..., description="新名称")
@field_validator('old_path', 'new_name')
@classmethod
def validate_inputs(cls, value: str):
@@ -117,10 +115,10 @@ class ResourceRenameSchema(BaseModel):
class ResourceCreateDirSchema(BaseModel):
"""创建目录模型"""
model_config = ConfigDict(from_attributes=True)
parent_path: str = Field(..., description="父目录路径")
dir_name: str = Field(..., description="目录名称", max_length=255)
@field_validator('parent_path', 'dir_name')
@classmethod
def validate_inputs(cls, value: str, info):
@@ -135,7 +133,7 @@ class ResourceCreateDirSchema(BaseModel):
else: # 对于dir_name仍然严格检查
if not value or len(value.strip()) == 0:
raise ValueError("参数不能为空")
if '..' in value or value.startswith('/') or value.startswith('\\'):
if '..' in value or value.startswith(('/', '\\')):
raise ValueError("参数包含不安全字符")
return value.strip()
@@ -148,9 +146,9 @@ class ResourceSearchQueryParam:
name: str | None = Query(None, description="搜索关键词"),
path: str | None = Query(None, description="目录路径"),
) -> None:
# 模糊查询字段
self.name = ("like", name) if name else None
# 精确查询字段
self.path = path
self.path = path
@@ -1,26 +1,26 @@
# -*- coding: utf-8 -*-
import os
import shutil
from datetime import datetime
from pathlib import Path
from urllib.parse import urlparse
from fastapi import UploadFile
from app.config.setting import settings
from app.core.exceptions import CustomException
from app.core.logger import log
from app.utils.excel_util import ExcelUtil
from app.config.setting import settings
from .schema import (
ResourceItemSchema,
ResourceDirectorySchema,
ResourceUploadSchema,
ResourceMoveSchema,
ResourceCopySchema,
ResourceRenameSchema,
ResourceCreateDirSchema,
ResourceSearchQueryParam
ResourceDirectorySchema,
ResourceItemSchema,
ResourceMoveSchema,
ResourceRenameSchema,
ResourceSearchQueryParam,
ResourceUploadSchema,
)
@@ -28,90 +28,87 @@ class ResourceService:
"""
资源管理模块服务层 - 管理系统静态文件目录
"""
# 配置常量
MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB
MAX_SEARCH_RESULTS = 1000 # 最大搜索结果数
MAX_PATH_DEPTH = 20 # 最大路径深度
@classmethod
def _get_resource_root(cls) -> str:
"""
获取资源管理根目录
返回:
- str: 资源管理根目录路径。
"""
if not settings.STATIC_ENABLE:
raise CustomException(msg='静态文件服务未启用')
return str(settings.STATIC_ROOT)
@classmethod
def _get_safe_path(cls, path: str | None = None) -> str:
"""
获取安全的文件路径
参数:
- path (str | None): 原始文件路径。
返回:
- str: 安全的文件路径。
"""
resource_root = cls._get_resource_root()
if not path:
return resource_root
# 支持前端传递的完整URL或以STATIC_URL/ROOT_PATH+STATIC_URL开头的URL路径,转换为相对资源路径
if isinstance(path, str):
static_prefix = settings.STATIC_URL.rstrip('/')
root_prefix = settings.ROOT_PATH.rstrip('/') if getattr(settings, 'ROOT_PATH', '') else ''
root_static_prefix = f"{root_prefix}{static_prefix}" if root_prefix else static_prefix
def strip_prefix(p: str) -> str:
if p.startswith(root_static_prefix):
return p[len(root_static_prefix):].lstrip('/')
if p.startswith(static_prefix):
return p[len(static_prefix):].lstrip('/')
return p
if path.startswith('http://') or path.startswith('https://'):
if path.startswith(('http://', 'https://')):
parsed = urlparse(path)
url_path = parsed.path or ''
path = strip_prefix(url_path)
else:
path = strip_prefix(path)
# 清理路径,移除危险字符
path = path.strip().replace('..', '').replace('//', '/')
# 规范化路径
if os.path.isabs(path):
safe_path = os.path.normpath(path)
else:
safe_path = os.path.normpath(os.path.join(resource_root, path))
safe_path = os.path.normpath(path) if os.path.isabs(path) else os.path.normpath(os.path.join(resource_root, path))
# 检查路径是否在允许的范围内
resource_root_abs = os.path.normpath(os.path.abspath(resource_root))
safe_path_abs = os.path.normpath(os.path.abspath(safe_path))
if not safe_path_abs.startswith(resource_root_abs):
raise CustomException(msg=f'访问路径不在允许范围内: {path}')
# 防止路径遍历攻击
if '..' in safe_path or safe_path.count('/') > cls.MAX_PATH_DEPTH:
raise CustomException(msg=f'不安全的路径格式: {path}')
return safe_path
@classmethod
def _path_exists(cls, path: str) -> bool:
"""
检查路径是否存在
参数:
- path (str): 要检查的路径。
返回:
- bool: 如果路径存在则返回True,否则返回False。
"""
@@ -120,16 +117,16 @@ class ResourceService:
return os.path.exists(safe_path)
except:
return False
@classmethod
def _generate_http_url(cls, file_path: str, base_url: str | None = None) -> str:
"""
生成文件的HTTP URL
参数:
- file_path (str): 文件的绝对路径。
- base_url (str | None): 基础URL,用于生成完整URL。
返回:
- str: 文件的HTTP URL。
"""
@@ -141,7 +138,7 @@ class ResourceService:
except ValueError:
# 如果无法计算相对路径,使用文件名
url_path = os.path.basename(file_path)
# 如果提供了base_url,使用它生成完整URL,否则使用settings.STATIC_URL
if base_url:
# 修复URL生成逻辑
@@ -153,18 +150,18 @@ class ResourceService:
http_url = f"{base_part}{static_part}/{file_part}".replace('//', '/').replace(':/', '://')
else:
http_url = f"{settings.STATIC_URL}/{url_path}".replace('//', '/')
return http_url
@classmethod
def _get_file_info(cls, file_path: str, base_url: str | None = None) -> dict:
"""
获取文件或目录的详细信息,如名称、大小、创建时间、修改时间、路径、深度、HTTP URL、是否隐藏、是否为目录等。
参数:
- file_path (str): 文件或目录的路径。
- base_url (str | None): 基础URL,用于生成完整URL。
返回:
- dict: 文件或目录的详细信息字典。
"""
@@ -172,36 +169,36 @@ class ResourceService:
safe_path = cls._get_safe_path(file_path)
if not os.path.exists(safe_path):
return {}
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)
# 计算深度
try:
depth = len(Path(safe_path).relative_to(resource_root).parts)
len(Path(safe_path).relative_to(resource_root).parts)
except ValueError:
depth = 0
pass
# 生成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字段
@@ -214,19 +211,19 @@ class ResourceService:
'is_hidden': is_hidden
}
except Exception as e:
log.error(f'获取文件信息失败: {str(e)}')
log.error(f'获取文件信息失败: {e!s}')
return {}
@classmethod
async def get_directory_list_service(cls, path: str | None = None, include_hidden: bool = False, base_url: str | None = None) -> dict:
"""
获取目录列表
参数:
- path (str | None): 目录路径。如果未指定,将使用静态文件根目录。
- include_hidden (bool): 是否包含隐藏文件。
- base_url (str | None): 基础URL,用于生成完整URL。
返回:
- dict: 包含目录列表和统计信息的字典。
"""
@@ -238,39 +235,39 @@ class ResourceService:
else:
safe_path = cls._get_safe_path(path)
display_path = cls._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 = cls._get_file_info(item_path, base_url)
if file_info:
items.append(ResourceItemSchema(**file_info))
if file_info['is_file']:
total_files += 1
total_size += file_info.get('size', 0) or 0
elif file_info['is_dir']:
total_dirs += 1
except PermissionError:
raise CustomException(msg='没有权限访问此目录')
return ResourceDirectorySchema(
path=display_path, # 返回HTTP URL路径而不是文件系统路径
name=os.path.basename(safe_path),
@@ -279,85 +276,82 @@ class ResourceService:
total_dirs=total_dirs,
total_size=total_size
).model_dump()
except CustomException:
raise
except Exception as e:
log.error(f'获取目录列表失败: {str(e)}')
raise CustomException(msg=f'获取目录列表失败: {str(e)}')
log.error(f'获取目录列表失败: {e!s}')
raise CustomException(msg=f'获取目录列表失败: {e!s}')
@classmethod
async def get_resources_list_service(cls, search: ResourceSearchQueryParam | None = None, order_by: str | None = None, base_url: str | None = None) -> list[dict]:
"""
搜索资源列表(用于分页和导出)
参数:
- search (ResourceSearchQueryParam | None): 查询参数模型。
- order_by (str | None): 排序参数。
- base_url (str | None): 基础URL,用于生成完整URL。
返回:
- list[dict]: 资源详情字典列表。
"""
try:
# 确定搜索路径
if search and hasattr(search, 'path') and search.path:
resource_root = cls._get_safe_path(search.path)
else:
resource_root = cls._get_resource_root()
resource_root = cls._get_safe_path(search.path) if search and hasattr(search, 'path') and search.path else cls._get_resource_root()
# 检查路径是否存在
if not os.path.exists(resource_root):
raise CustomException(msg='目录不存在')
if not os.path.isdir(resource_root):
raise CustomException(msg='路径不是目录')
# 收集资源
all_resources = []
try:
for item_name in os.listdir(resource_root):
# 跳过隐藏文件
if item_name.startswith('.'):
continue
item_path = os.path.join(resource_root, item_name)
file_info = cls._get_file_info(item_path, base_url)
if file_info:
# 应用名称过滤
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():
continue
all_resources.append(file_info)
except PermissionError:
raise CustomException(msg='没有权限访问此目录')
# 应用排序
sorted_resources = cls._sort_results(all_resources, order_by)
# 限制最大结果数
if len(sorted_resources) > cls.MAX_SEARCH_RESULTS:
sorted_resources = sorted_resources[:cls.MAX_SEARCH_RESULTS]
return sorted_resources
except Exception as e:
log.error(f'搜索资源失败: {str(e)}')
raise CustomException(msg=f'搜索资源失败: {str(e)}')
log.error(f'搜索资源失败: {e!s}')
raise CustomException(msg=f'搜索资源失败: {e!s}')
@classmethod
async def export_resource_service(cls, data_list: list[dict]) -> bytes:
"""
导出资源列表
参数:
- data_list (list[dict]): 资源详情字典列表。
返回:
- bytes: Excel文件的二进制数据。
"""
@@ -372,7 +366,7 @@ class ResourceService:
# 复制数据并转换状态
export_data = data_list.copy()
# 格式化文件大小
for item in export_data:
if item.get('size'):
@@ -384,47 +378,47 @@ class ResourceService:
async def _get_directory_stats(cls, path: str, include_hidden: bool = False) -> dict[str, int]:
"""
递归获取目录统计信息
参数:
- path (str): 目录路径。
- include_hidden (bool): 是否包含隐藏文件。
返回:
- dict[str, int]: 包含文件数、目录数和总大小的字典。
"""
stats = {'files': 0, 'dirs': 0, 'size': 0}
try:
for root, dirs, files in os.walk(path):
# 过滤隐藏目录
if not include_hidden:
dirs[:] = [d for d in dirs if not d.startswith('.')]
files = [f for f in files if not f.startswith('.')]
stats['dirs'] += len(dirs)
stats['files'] += len(files)
for file in files:
file_path = os.path.join(root, file)
try:
stats['size'] += os.path.getsize(file_path)
except (OSError, IOError):
except OSError:
continue
except Exception:
pass
return stats
@classmethod
def _sort_results(cls, results: list[dict], order_by: str | None = None) -> list[dict]:
"""
排序搜索结果
参数:
- results (list[dict]): 资源详情字典列表。
- order_by (str | None): 排序参数。
返回:
- list[dict]: 排序后的资源详情字典列表。
"""
@@ -432,7 +426,7 @@ class ResourceService:
# 默认按名称升序排序
if not order_by:
return sorted(results, key=lambda x: x.get('name', ''), reverse=False)
# 解析order_by参数,格式: [{'field':'asc/desc'}]
try:
sort_conditions = eval(order_by)
@@ -442,7 +436,7 @@ class ResourceService:
keys = []
for cond in sort_conditions:
field = cond.get('field', 'name')
direction = cond.get('direction', 'asc')
cond.get('direction', 'asc')
# 获取字段值,默认为空字符串
value = item.get(field, '')
# 如果是日期字段,转换为可比较的格式
@@ -450,61 +444,58 @@ class ResourceService:
value = datetime.fromisoformat(value)
keys.append(value)
return keys
# 确定排序方向(这里只支持单一方向,多个条件时使用第一个条件的方向)
reverse = False
if sort_conditions and isinstance(sort_conditions[0], dict):
direction = sort_conditions[0].get('direction', '').lower()
reverse = direction == 'desc'
return sorted(results, key=sort_key, reverse=reverse)
except:
# 如果解析失败,使用默认排序
pass
return sorted(results, key=lambda x: x.get('name', ''), reverse=False)
except:
return results
@classmethod
async def upload_file_service(cls, file: UploadFile, target_path: str | None = None, base_url: str | None = None) -> dict:
async def upload_file_service(cls, file: UploadFile, target_path: str | None = None, base_url: str | None = None) -> dict:
"""
上传文件到指定目录
参数:
- file (UploadFile): 上传的文件对象。
- target_path (str | None): 目标目录路径。
- base_url (str | None): 基础URL,用于生成完整URL。
返回:
- dict: 包含文件信息的字典。
"""
if not file or not file.filename:
raise CustomException(msg="请选择要上传的文件")
# 文件名安全检查
if '..' in file.filename or '/' in file.filename or '\\' in file.filename:
raise CustomException(msg="文件名包含不安全字符")
try:
# 检查文件大小
content = await file.read()
if len(content) > cls.MAX_UPLOAD_SIZE:
raise CustomException(msg=f"文件太大,最大支持{cls.MAX_UPLOAD_SIZE // (1024*1024)}MB")
raise CustomException(msg=f"文件太大,最大支持{cls.MAX_UPLOAD_SIZE // (1024 * 1024)}MB")
# 确定上传目录,如果没有指定目标路径,使用静态文件根目录
if target_path is None:
safe_dir = cls._get_resource_root()
else:
safe_dir = cls._get_safe_path(target_path)
safe_dir = cls._get_resource_root() if target_path is None else cls._get_safe_path(target_path)
# 创建目录(如果不存在)
os.makedirs(safe_dir, exist_ok=True)
# 生成文件路径
filename = file.filename
file_path = os.path.join(safe_dir, filename)
# 检查文件是否已存在
if os.path.exists(file_path):
# 生成唯一文件名
@@ -515,119 +506,118 @@ class ResourceService:
file_path = os.path.join(safe_dir, new_filename)
counter += 1
filename = os.path.basename(file_path)
# 保存文件(使用已读取的内容)
with open(file_path, 'wb') as f:
f.write(content)
Path(file_path).write_bytes(content)
# 获取文件信息
file_info = cls._get_file_info(file_path, base_url)
# 生成文件URL
file_url = cls._generate_http_url(file_path, base_url)
log.info(f"文件上传成功: {filename}")
return ResourceUploadSchema(
filename=filename,
file_url=file_url,
file_size=file_info.get('size', 0),
upload_time=datetime.now()
).model_dump(mode='json')
except Exception as e:
log.error(f"文件上传失败: {str(e)}")
raise CustomException(msg=f"文件上传失败: {str(e)}")
log.error(f"文件上传失败: {e!s}")
raise CustomException(msg=f"文件上传失败: {e!s}")
@classmethod
async def download_file_service(cls, file_path: str, base_url: str | None = None) -> str:
"""
下载文件(返回本地文件系统路径)
参数:
- file_path (str): 文件路径(可为相对路径、绝对路径或完整URL)。
- base_url (str | None): 基础URL,用于生成完整URL(不再直接返回URL)。
返回:
- str: 本地文件系统路径。
"""
try:
safe_path = cls._get_safe_path(file_path)
if not os.path.exists(safe_path):
raise CustomException(msg='文件不存在')
if not os.path.isfile(safe_path):
raise CustomException(msg='路径不是文件')
# 返回本地文件路径给 FileResponse 使用
log.info(f"定位文件路径: {safe_path}")
return safe_path
except CustomException:
raise
except Exception as e:
log.error(f"下载文件失败: {str(e)}")
raise CustomException(msg=f"下载文件失败: {str(e)}")
log.error(f"下载文件失败: {e!s}")
raise CustomException(msg=f"下载文件失败: {e!s}")
@classmethod
async def delete_file_service(cls, paths: list[str]) -> None:
"""
删除文件或目录
参数:
- paths (list[str]): 文件或目录路径列表。
返回:
- None
"""
if not paths:
raise CustomException(msg='删除失败,删除路径不能为空')
for path in paths:
try:
safe_path = cls._get_safe_path(path)
if not os.path.exists(safe_path):
log.error(f"路径不存在,跳过: {path}")
continue
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}")
except Exception as e:
log.error(f"删除失败 {path}: {str(e)}")
raise CustomException(msg=f"删除失败 {path}: {str(e)}")
log.error(f"删除失败 {path}: {e!s}")
raise CustomException(msg=f"删除失败 {path}: {e!s}")
@classmethod
async def batch_delete_service(cls, paths: list[str]) -> dict[str, list[str]]:
"""
批量删除文件或目录
参数:
- paths (List[str]): 文件或目录路径列表。
返回:
- Dict[str, List[str]]: 包含成功删除路径和失败删除路径的字典。
"""
if not paths:
raise CustomException(msg='删除失败,删除路径不能为空')
success_paths = []
failed_paths = []
for path in paths:
try:
safe_path = cls._get_safe_path(path)
if not os.path.exists(safe_path):
failed_paths.append(path)
continue
if os.path.isfile(safe_path):
os.remove(safe_path)
success_paths.append(path)
@@ -636,11 +626,11 @@ class ResourceService:
shutil.rmtree(safe_path)
success_paths.append(path)
log.info(f"删除目录成功: {safe_path}")
except Exception as e:
log.error(f"删除失败 {path}: {str(e)}")
log.error(f"删除失败 {path}: {e!s}")
failed_paths.append(path)
return {
"success": success_paths,
"failed": failed_paths
@@ -650,177 +640,176 @@ class ResourceService:
async def move_file_service(cls, data: ResourceMoveSchema) -> None:
"""
移动文件或目录
参数:
- data (ResourceMoveSchema): 包含源路径和目标路径的模型。
返回:
- None
"""
try:
source_path = cls._get_safe_path(data.source_path)
target_path = cls._get_safe_path(data.target_path)
if not os.path.exists(source_path):
raise CustomException(msg='源路径不存在')
# 检查目标路径是否已存在
if os.path.exists(target_path):
if not data.overwrite:
raise CustomException(msg='目标路径已存在')
# 删除目标路径
if os.path.isfile(target_path):
os.remove(target_path)
else:
# 删除目标路径
if os.path.isfile(target_path):
os.remove(target_path)
else:
shutil.rmtree(target_path)
shutil.rmtree(target_path)
# 确保目标目录存在
target_dir = os.path.dirname(target_path)
os.makedirs(target_dir, exist_ok=True)
# 移动文件
shutil.move(source_path, target_path)
log.info(f"移动成功: {source_path} -> {target_path}")
except CustomException:
raise
except Exception as e:
log.error(f"移动失败: {str(e)}")
raise CustomException(msg=f"移动失败: {str(e)}")
log.error(f"移动失败: {e!s}")
raise CustomException(msg=f"移动失败: {e!s}")
@classmethod
async def copy_file_service(cls, data: ResourceCopySchema) -> None:
"""
复制文件或目录
参数:
- data (ResourceCopySchema): 包含源路径和目标路径的模型。
返回:
- None
"""
try:
source_path = cls._get_safe_path(data.source_path)
target_path = cls._get_safe_path(data.target_path)
if not os.path.exists(source_path):
raise CustomException(msg='源路径不存在')
# 检查目标路径是否已存在
if os.path.exists(target_path) and not data.overwrite:
raise CustomException(msg='目标路径已存在')
# 确保目标目录存在
target_dir = os.path.dirname(target_path)
os.makedirs(target_dir, exist_ok=True)
# 复制文件或目录
if os.path.isfile(source_path):
shutil.copy2(source_path, target_path)
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"复制失败: {str(e)}")
raise CustomException(msg=f"复制失败: {str(e)}")
log.error(f"复制失败: {e!s}")
raise CustomException(msg=f"复制失败: {e!s}")
@classmethod
async def rename_file_service(cls, data: ResourceRenameSchema) -> None:
"""
重命名文件或目录
参数:
- data (ResourceRenameSchema): 包含旧路径和新名称的模型。
返回:
- None
"""
try:
old_path = cls._get_safe_path(data.old_path)
if not os.path.exists(old_path):
raise CustomException(msg='文件或目录不存在')
# 生成新路径
parent_dir = os.path.dirname(old_path)
new_path = os.path.join(parent_dir, data.new_name)
if os.path.exists(new_path):
raise CustomException(msg='目标名称已存在')
# 重命名
os.rename(old_path, new_path)
log.info(f"重命名成功: {old_path} -> {new_path}")
except CustomException:
raise
except Exception as e:
log.error(f"重命名失败: {str(e)}")
raise CustomException(msg=f"重命名失败: {str(e)}")
log.error(f"重命名失败: {e!s}")
raise CustomException(msg=f"重命名失败: {e!s}")
@classmethod
async def create_directory_service(cls, data: ResourceCreateDirSchema) -> None:
"""
创建目录
参数:
- data (ResourceCreateDirSchema): 包含父目录路径和目录名称的模型。
返回:
- None
"""
try:
parent_path = cls._get_safe_path(data.parent_path)
if not os.path.exists(parent_path):
raise CustomException(msg='父目录不存在')
if not os.path.isdir(parent_path):
raise CustomException(msg='父路径不是目录')
# 生成新目录路径
new_dir_path = os.path.join(parent_path, data.dir_name)
# 安全检查:确保新目录名称不包含路径遍历字符
if '..' in data.dir_name or '/' in data.dir_name or '\\' in data.dir_name:
raise CustomException(msg='目录名称包含不安全字符')
if os.path.exists(new_dir_path):
raise CustomException(msg='目录已存在')
# 创建目录
os.makedirs(new_dir_path)
log.info(f"创建目录成功: {new_dir_path}")
except CustomException:
raise
except Exception as e:
log.error(f"创建目录失败: {str(e)}")
raise CustomException(msg=f"创建目录失败: {str(e)}")
log.error(f"创建目录失败: {e!s}")
raise CustomException(msg=f"创建目录失败: {e!s}")
@classmethod
def _format_file_size(cls, size_bytes: int) -> str:
"""
格式化文件大小
参数:
- size_bytes (int): 文件大小(字节)
返回:
- str: 格式化后的文件大小字符串(例如:"123.45MB"
"""
if size_bytes == 0:
return "0B"
size_names = ["B", "KB", "MB", "GB", "TB"]
i = 0
while size_bytes >= 1024 and i < len(size_names) - 1:
size_bytes = int(size_bytes / 1024)
i += 1
return f"{size_bytes:.2f}{size_names[i]}"
@@ -1,2 +1 @@
# -*- coding: utf-8 -*-
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends
@@ -11,9 +10,9 @@ from app.core.router_class import OperationLogRoute
from .service import ServerService
ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=["服务器监控"])
@ServerRouter.get(
'/info',
summary="查询服务器监控信息",
@@ -23,7 +22,7 @@ ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=[
async def get_monitor_server_info_controller() -> JSONResponse:
"""
查询服务器监控信息
返回:
- JSONResponse: 包含服务器监控信息的JSON响应。
"""
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field
@@ -1,21 +1,14 @@
# -*- coding: utf-8 -*-
import platform
import psutil
import socket
import time
from pathlib import Path
import psutil
from app.utils.common_util import bytes2human
from .schema import (
CpuInfoSchema,
MemoryInfoSchema,
PyInfoSchema,
ServerMonitorSchema,
DiskInfoSchema,
SysInfoSchema
)
from .schema import CpuInfoSchema, DiskInfoSchema, MemoryInfoSchema, PyInfoSchema, ServerMonitorSchema, SysInfoSchema
class ServerService:
@@ -25,7 +18,7 @@ class ServerService:
async def get_server_monitor_info_service(cls) -> dict:
"""
获取服务器监控信息
返回:
- Dict: 包含服务器监控信息的字典。
"""
@@ -41,12 +34,12 @@ class ServerService:
def _get_cpu_info(cls) -> CpuInfoSchema:
"""
获取CPU信息
返回:
- CpuInfoSchema: CPU信息模型。
"""
cpu_times = psutil.cpu_times_percent()
cpu_num=psutil.cpu_count(logical=True)
cpu_num = psutil.cpu_count(logical=True)
if not cpu_num:
cpu_num = 1
return CpuInfoSchema(
@@ -60,7 +53,7 @@ class ServerService:
def _get_memory_info(cls) -> MemoryInfoSchema:
"""
获取内存信息
返回:
- MemoryInfoSchema: 内存信息模型。
"""
@@ -76,7 +69,7 @@ class ServerService:
def _get_system_info(cls) -> SysInfoSchema:
"""
获取系统信息
返回:
- SysInfoSchema: 系统信息模型。
"""
@@ -93,17 +86,17 @@ class ServerService:
def _get_python_info(cls) -> PyInfoSchema:
"""
获取Python解释器信息
返回:
- PyInfoSchema: Python解释器信息模型。
"""
current_process = psutil.Process()
memory = psutil.virtual_memory()
process_memory = current_process.memory_info()
start_time = current_process.create_time()
run_time = ServerService._calculate_run_time(start_time)
return PyInfoSchema(
name=current_process.name(),
version=platform.python_version(),
@@ -120,7 +113,7 @@ class ServerService:
def _get_disk_info(cls) -> list[DiskInfoSchema]:
"""
获取磁盘信息
返回:
- list[DiskInfoSchema]: 磁盘信息模型列表。
"""
@@ -147,13 +140,13 @@ class ServerService:
return disk_info
@classmethod
def _calculate_run_time(cls,start_time: float) -> str:
def _calculate_run_time(cls, start_time: float) -> str:
"""
计算运行时间
参数:
- start_time (float): 进程启动时间(时间戳)
返回:
- str: 格式化后的运行时间字符串(例如:"1天2小时3分钟"
"""