refactor(api): 重构API路由及模块结构,清理废弃模型代码

- 修改alembic配置以使用异步数据库URI和更新Base类引用
- 新增数据库Schema优化脚本,统一字段长度,添加索引,规范外键策略
- 重构API路由管理,按模块类型分组并统一前缀
- 删除示例、监控及系统各子模块的模型定义,减少冗余代码
- 将mcp_server相关代码迁移到module_ai模块下,规范模块目录结构
- 迁移example控制器至module_application.application模块,并重命名相关服务和参数名
- 优化示例控制器中的依赖和响应结构,统一命名规范
This commit is contained in:
zhangtao
2025-09-04 02:50:29 +08:00
parent 35fa896a74
commit 3b04534b32
233 changed files with 6809 additions and 1788 deletions
+2
View File
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
+129
View File
@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
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.router_class import OperationLogRoute
from app.core.dependencies import AuthPermission, redis_getter
from app.core.logger import logger
from .service import CacheService
CacheRouter = APIRouter(route_class=OperationLogRoute, prefix="/cache", tags=["缓存监控"])
@CacheRouter.get(
'/info',
dependencies=[Depends(AuthPermission(permissions=['monitor:cache:query']))],
summary="获取缓存监控信息",
description="获取缓存监控信息"
)
async def get_monitor_cache_info_controller(
redis: Redis = Depends(redis_getter)
) -> JSONResponse:
"""获取缓存监控统计信息"""
result = await CacheService.get_cache_monitor_statistical_info_service(redis=redis)
logger.info('获取缓存监控信息成功')
return SuccessResponse(data=result, msg='获取缓存监控信息成功')
@CacheRouter.get(
'/get/names',
dependencies=[Depends(AuthPermission(permissions=['monitor:cache:query']))],
summary="获取缓存名称列表",
description="获取缓存名称列表"
)
async def get_monitor_cache_name_controller() -> JSONResponse:
"""获取缓存名称列表"""
result = await CacheService.get_cache_monitor_cache_name_service()
logger.info('获取缓存名称列表成功')
return SuccessResponse(data=result, msg='获取缓存名称列表成功')
@CacheRouter.get(
'/get/keys/{cache_name}',
dependencies=[Depends(AuthPermission(permissions=['monitor:cache:query']))],
summary="获取缓存键名列表",
description="获取缓存键名列表"
)
async def get_monitor_cache_key_controller(
cache_name: str,
redis: Redis = Depends(redis_getter)
) -> JSONResponse:
"""获取指定缓存名称下的键名列表"""
result = await CacheService.get_cache_monitor_cache_key_service(redis=redis, cache_name=cache_name)
logger.info(f'获取缓存{cache_name}的键名列表成功')
return SuccessResponse(data=result, msg=f'获取缓存{cache_name}的键名列表成功')
@CacheRouter.get(
'/get/value/{cache_name}/{cache_key}',
dependencies=[Depends(AuthPermission(permissions=['monitor:cache:query']))],
summary="获取缓存值",
description="获取缓存值"
)
async def get_monitor_cache_value_controller(
cache_name: str,
cache_key: str,
redis: Redis = Depends(redis_getter)
)-> JSONResponse:
"""获取指定缓存键的值"""
result = await CacheService.get_cache_monitor_cache_value_service(redis=redis, cache_name=cache_name, cache_key=cache_key)
logger.info(f'获取缓存{cache_name}:{cache_key}的值成功')
return SuccessResponse(data=result, msg=f'获取缓存{cache_name}:{cache_key}的值成功')
@CacheRouter.delete(
'/delete/name/{cache_name}',
dependencies=[Depends(AuthPermission(permissions=['monitor:cache:delete']))],
summary="清除指定缓存名称的所有缓存",
description="清除指定缓存名称的所有缓存"
)
async def clear_monitor_cache_name_controller(
cache_name: str,
redis: Redis = Depends(redis_getter)
) -> JSONResponse:
"""清除指定缓存名称下的所有缓存"""
result = await CacheService.clear_cache_monitor_cache_name_service(redis=redis, cache_name=cache_name)
if not result:
raise CustomException(message='清除缓存失败', data=result)
logger.info(f'清除缓存{cache_name}成功')
return SuccessResponse(msg=f'{cache_name}对应键值清除成功', data=result)
@CacheRouter.delete(
'/delete/key/{cache_key}',
dependencies=[Depends(AuthPermission(permissions=['monitor:cache:delete']))],
summary="清除指定缓存键",
description="清除指定缓存键"
)
async def clear_monitor_cache_key_controller(
cache_key: str,
redis: Redis = Depends(redis_getter)
) -> JSONResponse:
"""清除指定缓存键"""
result = await CacheService.clear_cache_monitor_cache_key_service(redis=redis, cache_key=cache_key)
if not result:
raise CustomException(message='清除缓存失败', data=result)
logger.info(f'清除缓存键{cache_key}成功')
return SuccessResponse(msg=f'{cache_key}清除成功', data=result)
@CacheRouter.delete(
'/delete/all',
dependencies=[Depends(AuthPermission(permissions=['monitor:cache:delete']))],
summary="清除所有缓存",
description="清除所有缓存"
)
async def clear_monitor_cache_all_controller(
redis: Redis = Depends(redis_getter)
) -> JSONResponse:
"""清除所有缓存"""
result = await CacheService.clear_cache_monitor_all_service(redis=redis)
if not result:
raise CustomException(message='清除缓存失败', data=result)
logger.info('清除所有缓存成功')
return SuccessResponse(msg='所有缓存清除成功', data=result)
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field
from typing import Any, Dict, List, Optional
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[str, Any] = Field(default_factory=dict, description='Redis服务器信息')
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='缓存值')
remark: Optional[str] = Field(default=None, description='备注说明')
+127
View File
@@ -0,0 +1,127 @@
# -*- 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
class CacheService:
"""
缓存监控模块服务层
"""
@classmethod
async def get_cache_monitor_statistical_info_service(cls, redis: Redis)->dict:
"""
获取缓存监控信息service
:param redis: Redis对象
:return: 缓存监控信息
"""
info = await RedisCURD(redis).info()
db_size = await RedisCURD(redis).db_size()
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()
]
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:
"""
获取缓存名称列表信息service
:return: 缓存名称列表信息
"""
name_list = []
for key_config in RedisInitKeyConfig:
name_list.append(
CacheInfoSchema(
cache_key='',
cache_name=key_config.key,
cache_value='',
remark=key_config.remark,
).model_dump()
)
return name_list
@classmethod
async def get_cache_monitor_cache_key_service(cls, redis: Redis, cache_name: str)->list:
"""
获取缓存键名列表信息service
:param redis: Redis对象
:param cache_name: 缓存名称
:return: 缓存键名列表信息
"""
cache_keys = await RedisCURD(redis).get_keys(f'{cache_name}*')
cache_key_list = [key.split(':', 1)[1] for key in cache_keys if key.startswith(f'{cache_name}:')]
return cache_key_list
@classmethod
async def get_cache_monitor_cache_value_service(cls, redis: Redis, cache_name: str, cache_key: str)->dict:
"""
获取缓存内容信息service
:param redis: Redis对象
:param cache_name: 缓存名称
:param cache_key: 缓存键名
:return: 缓存内容信息
"""
cache_value = await RedisCURD(redis).get(f'{cache_name}:{cache_key}')
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:
"""
清除缓存名称对应所有键值service
:param redis: Redis对象
:param cache_name: 缓存名称
:return: 操作缓存响应信息
"""
cache_keys = await RedisCURD(redis).get_keys(f'{cache_name}*')
if cache_keys:
await RedisCURD(redis).delete(*cache_keys)
return True
@classmethod
async def clear_cache_monitor_cache_key_service(cls, redis: Redis, cache_key: str)->bool:
"""
清除缓存名称对应所有键值service
:param redis: Redis对象
:param cache_key: 缓存键名
:return: 操作缓存响应信息
"""
cache_keys = await RedisCURD(redis).get_keys(f'*{cache_key}')
if cache_keys:
await RedisCURD(redis).delete(*cache_keys)
return True
@classmethod
async def clear_cache_monitor_all_service(cls, redis: Redis)->bool:
"""
清除所有缓存service
:param redis: Redis对象
:return: 操作缓存响应信息
"""
cache_keys = await RedisCURD(redis).get_keys()
if cache_keys:
await RedisCURD(redis).delete(*cache_keys)
return True
# 避免清除所有的缓存,而采用上面的方式,只清除本系统内指定的所有缓存
# return await RedisCURD(redis).clear()
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Body, Depends, Path, Query
from fastapi.responses import JSONResponse, StreamingResponse
from app.common.response import StreamResponse, SuccessResponse
from app.common.request import PaginationService
from app.utils.common_util import bytes2file_response
from app.core.base_params import PaginationQueryParams
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.core.logger import logger
from app.api.v1.module_system.auth.schema import AuthSchema
from .param import JobQueryParams
from .service import JobService
from .schema import (
JobCreateSchema,
JobUpdateSchema
)
from app.core.ap_scheduler import SchedulerUtil
JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/job", tags=["定时任务"])
@JobRouter.get("/detail/{id}", summary="获取定时任务详情", description="获取定时任务详情")
async def get_obj_detail_controller(
id: int = Path(..., description="定时任务ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:query"]))
) -> JSONResponse:
result_dict = await JobService.get_job_detail_service(id=id, auth=auth)
logger.info(f"获取定时任务详情成功 {id}")
return SuccessResponse(data=result_dict, msg="获取定时任务详情成功")
@JobRouter.get("/list", summary="查询定时任务", description="查询定时任务")
async def get_obj_list_controller(
page: PaginationQueryParams = Depends(),
search: JobQueryParams = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:query"]))
) -> JSONResponse:
result_dict_list = await JobService.get_job_list_service(auth=auth, search=search, order_by=page.order_by)
result_dict = await PaginationService.get_page_obj(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
logger.info(f"查询定时任务列表成功")
return SuccessResponse(data=result_dict, msg="查询定时任务列表成功")
@JobRouter.post("/create", summary="创建定时任务", description="创建定时任务")
async def create_obj_controller(
data: JobCreateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:create"]))
) -> JSONResponse:
result_dict = await JobService.create_job_service(auth=auth, data=data)
logger.info(f"创建定时任务成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建定时任务成功")
@JobRouter.put("/update", summary="修改定时任务", description="修改定时任务")
async def update_obj_controller(
data: JobUpdateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:update"]))
) -> JSONResponse:
result_dict = await JobService.update_job_service(auth=auth, data=data)
logger.info(f"修改定时任务成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="修改定时任务成功")
@JobRouter.delete("/delete", summary="删除定时任务", description="删除定时任务")
async def delete_obj_controller(
ids: list[int] = Body(..., description="ID列表"),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:delete"]))
) -> JSONResponse:
await JobService.delete_job_service(auth=auth, ids=ids)
logger.info(f"删除定时任务成功: {id}")
return SuccessResponse(msg="删除定时任务成功")
@JobRouter.post('/export', summary="导出定时任务", description="导出定时任务")
async def export_obj_list_controller(
search: JobQueryParams = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:export"]))
) -> StreamingResponse:
# 获取全量数据
result_dict_list = await JobService.get_job_list_service(search=search, auth=auth)
export_result = await JobService.export_job_service(data_list=result_dict_list)
logger.info('导出定时任务成功')
return StreamResponse(
data=bytes2file_response(export_result),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers = {
'Content-Disposition': 'attachment; filename=job.xlsx'
}
)
@JobRouter.delete("/clear", summary="清空定时任务日志", description="清空定时任务日志")
async def clear_obj_log_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:delete"]))
) -> JSONResponse:
await JobService.clear_job_service(auth=auth)
logger.info(f"清空定时任务成功")
return SuccessResponse(msg="清空定时任务成功")
@JobRouter.put("/option", summary="暂停/恢复/重启定时任务", description="暂停/恢复/重启定时任务")
async def option_obj_controller(
id: int = Body(..., description="定时任务ID"),
option: int = Body(..., description="操作类型 1: 暂停 2: 恢复 3: 重启"),
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:update"]))
) -> JSONResponse:
await JobService.option_job_service(auth=auth, id=id, option=option)
logger.info(f"操作定时任务成功: {id}")
return SuccessResponse(msg="操作定时任务成功")
@JobRouter.get("/log", summary="获取定时任务日志", description="获取定时任务日志", dependencies=[Depends(AuthPermission(permissions=["monitor:job:query"]))])
async def get_job_log_controller():
data = [
{
"id": i.id,
"name": i.name,
"trigger": i.trigger.__class__.__name__,
"executor": i.executor,
"func": i.func,
"func_ref": i.func_ref,
"args": i.args,
"kwargs": i.kwargs,
"misfire_grace_time": i.misfire_grace_time,
"coalesce": i.coalesce,
"max_instances": i.max_instances,
"next_run_time": i.next_run_time,
"state": SchedulerUtil.get_job_status()
}
for i in SchedulerUtil.get_all_jobs()
]
return SuccessResponse(msg="获取定时任务日志成功", data=data)
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
from typing import Dict, List, Optional, Sequence
from app.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema
from .model import JobModel, JobLogModel
from .schema import JobCreateSchema,JobUpdateSchema,JobLogCreateSchema,JobLogUpdateSchema
class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
"""定时任务数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""初始化定时任务CRUD"""
self.auth = auth
super().__init__(model=JobModel, auth=auth)
async def get_obj_by_id_crud(self, id: int) -> Optional[JobModel]:
"""获取定时任务详情"""
return await self.get(id=id)
async def get_obj_list_crud(self, search: Dict = None, order_by: List[Dict[str, str]] = None) -> Sequence[JobModel]:
"""获取定时任务列表"""
return await self.list(search=search, order_by=order_by)
async def create_obj_crud(self, data: JobCreateSchema) -> Optional[JobModel]:
"""创定时任务"""
return await self.create(data=data)
async def update_obj_crud(self, id: int, data: JobUpdateSchema) -> Optional[JobModel]:
"""更新定时任务"""
return await self.update(id=id, data=data)
async def delete_obj_crud(self, ids: List[int]) -> None:
"""删除定时任务"""
return await self.delete(ids=ids)
async def set_obj_field_crud(self, ids: List[int], **kwargs) -> None:
"""设置定时任务的可用状态"""
return await self.set(ids=ids, **kwargs)
async def clear_obj_crud(self) -> None:
"""清除定时任务日志"""
return await self.clear()
class JobLogCRUD(CRUDBase[JobLogModel, JobLogCreateSchema, JobLogUpdateSchema]):
"""定时任务日志数据层"""
def __init__(self, auth: AuthSchema) -> None:
"""初始化定时任务日志CRUD"""
self.auth = auth
super().__init__(model=JobLogModel, auth=auth)
async def get_obj_log_by_id_crud(self, id: int) -> Optional[JobLogModel]:
"""获取定时任务日志详情"""
return await self.get(id=id)
async def get_obj_log_list_crud(self, search: Dict = None, order_by: List[Dict[str, str]] = None) -> Sequence[JobLogModel]:
"""获取定时任务日志列表"""
return await self.list(search=search, order_by=order_by)
async def create_obj_log_crud(self, data: JobLogCreateSchema) -> Optional[JobLogModel]:
"""创建定时任务日志"""
return await self.create(data=data)
async def delete_obj_log_crud(self, ids: List[int]) -> None:
"""删除定时任务日志"""
return await self.delete(ids=ids)
@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from typing import Optional
from sqlalchemy import Boolean, String, Integer, Text, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.base_model import CreatorMixin, MappedBase
class JobModel(CreatorMixin):
"""
定时任务调度表
"""
__tablename__ = 'monitor_job'
__table_args__ = ({'comment': '定时任务调度表'})
name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, default='', comment='任务名称')
jobstore: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, default='default', comment='存储器')
executor: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, default='default', comment='执行器:将运行此作业的执行程序的名称')
trigger: Mapped[str] = mapped_column(String(64), nullable=False, comment='触发器:控制此作业计划的 trigger 对象')
trigger_args: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment='触发器参数')
func: Mapped[str] = mapped_column(Text, nullable=False, comment='任务函数')
args: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment='位置参数')
kwargs: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment='关键字参数')
coalesce: Mapped[bool] = mapped_column(Boolean, nullable=True, default=False, comment='是否合并运行:是否在多个运行时间到期时仅运行作业一次')
max_instances: Mapped[int] = mapped_column(Integer, nullable=True, default=1, comment='最大实例数:允许的最大并发执行实例数 工作')
start_date: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment='开始时间')
end_date: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment='结束时间')
job_logs: Mapped[Optional[list['JobLogModel']]] = relationship(back_populates="job", lazy="select")
class JobLogModel(MappedBase):
"""
定时任务调度日志表
"""
__tablename__ = 'monitor_job_log'
__table_args__ = ({'comment': '定时任务调度日志表'})
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment='主键ID')
job_name: Mapped[str] = mapped_column(String(64),nullable=False,comment='任务名称')
job_group: Mapped[str] = mapped_column(String(64),nullable=False,comment='任务组名')
job_executor: Mapped[str] = mapped_column(String(64),nullable=False,comment='任务执行器')
invoke_target: Mapped[str] = mapped_column(String(500),nullable=False,comment='调用目标字符串')
job_args: Mapped[Optional[str]] = mapped_column(String(255),nullable=True,default='',comment='位置参数')
job_kwargs: Mapped[Optional[str]] = mapped_column(String(255),nullable=True,default='',comment='关键字参数')
job_trigger: Mapped[Optional[str]] = mapped_column(String(255),nullable=True,default='',comment='任务触发器')
job_message: Mapped[Optional[str]] = mapped_column(String(500),nullable=True,default='',comment='日志信息')
exception_info: Mapped[Optional[str]] = mapped_column(String(2000),nullable=True,default='',comment='异常信息')
job_id: Mapped[Optional[int]] = mapped_column(ForeignKey('monitor_job.id'), comment='任务ID')
# 任务关联关系
job: Mapped[Optional["JobModel"]] = relationship(back_populates="job_logs", lazy="select")
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from typing import Optional
from fastapi import Query
from datetime import datetime
from app.core.validator import DateTimeStr
class JobQueryParams:
"""定时任务查询参数"""
def __init__(
self,
name: Optional[str] = Query(None, description="任务名称"),
status: Optional[bool] = Query(None, description="状态: 启动,停止"),
creator: Optional[int] = Query(None, description="创建人"),
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"),
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"),
) -> None:
super().__init__()
# 模糊查询字段
self.name = ("like", f"%{name}%") if name else None
# 精确查询字段
self.creator_id = creator
self.status = status
# 时间范围查询
if start_time and end_time:
start_datetime = datetime.strptime(str(start_time), '%Y-%m-%d %H:%M:%S')
end_datetime = datetime.strptime(str(end_time), '%Y-%m-%d %H:%M:%S')
self.created_at = ("between", (start_datetime, end_datetime))
class JobLogQueryParams:
"""定时任务查询参数"""
def __init__(
self,
status: Optional[bool] = Query(None, description="状态: 正常,失败"),
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"),
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"),
) -> None:
super().__init__()
# 精确查询字段
self.status = status
# 时间范围查询
if start_time and end_time:
start_datetime = datetime.strptime(str(start_time), '%Y-%m-%d %H:%M:%S')
end_datetime = datetime.strptime(str(end_time), '%Y-%m-%d %H:%M:%S')
self.created_at = ("between", (start_datetime, end_datetime))
@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field
from typing import Optional
from app.core.base_schema import BaseSchema
from app.core.validator import DateTimeStr
class JobCreateSchema(BaseModel):
"""
定时任务调度表对应pydantic模型
"""
name: Optional[str] = Field(..., max_length=64, description='任务名称')
func: str = Field(..., description='任务函数')
trigger: str = Field(..., description='触发器:控制此作业计划的 trigger 对象')
args: Optional[str] = Field(default=None, description='位置参数')
kwargs: Optional[str] = Field(default=None, description='关键字参数')
coalesce: Optional[bool] = Field(..., description='是否合并运行:是否在多个运行时间到期时仅运行作业一次')
max_instances: Optional[int] = Field(default=1, ge=1, description='最大实例数:允许的最大并发执行实例数')
jobstore: Optional[str] = Field(..., max_length=64, description='任务存储')
executor: Optional[str] = Field(..., max_length=64, description='任务执行器:将运行此作业的执行程序的名称')
trigger_args: Optional[str] = Field(default=None, description='触发器参数')
start_date: Optional[str] = Field(default=None, description='开始时间')
end_date: Optional[str] = Field(default=None, description='结束时间')
description: Optional[str] = Field(default=None, description='备注说明')
status: Optional[bool] = Field(default=False, description='任务状态:启动,停止')
message: Optional[str] = Field(default=None, max_length=500, description='日志信息')
class JobUpdateSchema(JobCreateSchema):
"""定时任务更新模型"""
id: int = Field(..., gt=0, description="ID")
class JobOutSchema(JobCreateSchema, BaseSchema):
"""定时任务响应模型"""
model_config = ConfigDict(from_attributes=True)
...
class JobLogCreateSchema(BaseModel):
"""
定时任务调度日志表对应pydantic模型
"""
model_config = ConfigDict(from_attributes=True)
job_name: Optional[str] = Field(default=None, description='任务名称')
job_group: Optional[str] = Field(default=None, description='任务组名')
job_executor: Optional[str] = Field(default=None, description='任务执行器')
invoke_target: Optional[str] = Field(default=None, description='调用目标字符串')
job_args: Optional[str] = Field(default=None, description='位置参数')
job_kwargs: Optional[str] = Field(default=None, description='关键字参数')
job_trigger: Optional[str] = Field(default=None, description='任务触发器')
job_message: Optional[str] = Field(default=None, description='日志信息')
status: Optional[bool] = Field(default=None, description='任务状态:正常,失败')
exception_info: Optional[str] = Field(default=None, description='异常信息')
create_time: Optional[DateTimeStr] = Field(default=None, description='创建时间')
class JobLogUpdateSchema(JobLogCreateSchema):
"""定时任务调度日志表更新模型"""
...
job_log_id: Optional[int] = Field(default=None, description='任务日志ID')
class JobLogOutSchema(JobLogCreateSchema):
"""定时任务调度日志表响应模型"""
model_config = ConfigDict(from_attributes=True)
...
@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
from typing import Any, List, Dict
from app.core.ap_scheduler import SchedulerUtil
from app.core.exceptions import CustomException
from app.utils.cron_util import CronUtil
from app.utils.excel_util import ExcelUtil
from app.api.v1.module_system.auth.schema import AuthSchema
from .schema import JobCreateSchema, JobUpdateSchema, JobOutSchema
from .param import JobQueryParams
from .crud import JobCRUD
class JobService:
"""
定时任务管理模块服务层
"""
@classmethod
async def get_job_detail_service(cls, auth: AuthSchema, id: int) -> Dict:
obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
return JobOutSchema.model_validate(obj).model_dump()
@classmethod
async def get_job_list_service(cls, auth: AuthSchema, search: JobQueryParams = None, order_by: List[Dict[str, str]] = None) -> List[Dict]:
if order_by:
order_by = eval(order_by)
obj_list = await JobCRUD(auth).get_obj_list_crud(search=search.__dict__, order_by=order_by)
return [JobOutSchema.model_validate(obj).model_dump() for obj in obj_list]
@classmethod
async def create_job_service(cls, auth: AuthSchema, data: JobCreateSchema) -> Dict:
exist_obj = await JobCRUD(auth).get(name=data.name)
if exist_obj:
raise CustomException(msg='创建失败,该定时任务已存在')
if data.trigger == 'cron' and not CronUtil.validate_cron_expression(data.trigger_args):
raise CustomException(msg=f'新增定时任务{data.name}失败, Cron表达式不正确')
obj = await JobCRUD(auth).create_obj_crud(data=data)
SchedulerUtil().add_job(job_info=obj)
return JobOutSchema.model_validate(obj).model_dump()
@classmethod
async def update_job_service(cls, auth: AuthSchema, data: JobUpdateSchema) -> Dict:
exist_obj = await JobCRUD(auth).get_obj_by_id_crud(id=data.id)
if not exist_obj:
raise CustomException(msg='更新失败,该定时任务不存在')
if data.trigger == 'cron' and not CronUtil.validate_cron_expression(data.trigger_args):
raise CustomException(msg=f'新增定时任务{data.name}失败, Cron表达式不正确')
obj = await JobCRUD(auth).update_obj_crud(id=data.id, data=data)
SchedulerUtil().modify_job(job_id=obj.id)
return JobOutSchema.model_validate(obj).model_dump()
@classmethod
async def delete_job_service(cls, auth: AuthSchema, ids: list[int]) -> None:
if len(ids) < 1:
raise CustomException(msg='删除失败,删除对象不能为空')
for id in ids:
exist_obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
if not exist_obj:
raise CustomException(msg='删除失败,该数据定时任务不存在')
SchedulerUtil.remove_job(job_id=id)
await JobCRUD(auth).delete_obj_crud(ids=ids)
@classmethod
async def clear_job_service(cls, auth: AuthSchema) -> None:
SchedulerUtil().clear_jobs()
await JobCRUD(auth).clear_obj_crud()
@classmethod
async def option_job_service(cls, auth: AuthSchema, id: int, option: int) -> None:
# 1: 暂停 2: 恢复 3: 重启
obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
if not obj:
raise CustomException(msg='操作失败,该数据定时任务不存在')
if option == 1:
SchedulerUtil().pause_job(job_id=id)
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=False)
elif option == 2:
SchedulerUtil().resume_job(job_id=id)
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=True)
# elif option == 3:
# SchedulerUtil().reschedule_job(job_id=id)
# await JobCRUD(auth).set_obj_field_crud(ids=[id], status=False)
@classmethod
async def export_job_service(cls, data_list: List[Dict[str, Any]]) -> bytes:
"""导出公告列表"""
mapping_dict = {
'id': '编号',
'name': '任务名称',
'func': '任务函数',
'trigger': '触发器',
'args': '位置参数',
'kwargs': '关键字参数',
'coalesce': '是否合并运行',
'max_instances': '最大实例数',
'jobstore': '任务存储',
'executor': '任务执行器',
'trigger_args': '触发器参数',
'status': '任务状态',
'message': '日志信息',
'description': '备注',
'created_at': '创建时间',
'updated_at': '更新时间',
'creator_id': '创建者ID',
'creator': '创建者',
}
# 复制数据并转换状态
data = data_list.copy()
for item in data:
item['status'] = '已完成' if item['status'] == 0 else '运行中' if item['status'] == 1 else '暂停'
item['creator'] = item.get('creator', {}).get('name', '未知') if isinstance(item.get('creator'), dict) else '未知'
return ExcelUtil.export_list2excel(list_data=data_list, mapping_dict=mapping_dict)
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
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.core.base_params import PaginationQueryParams
from app.core.router_class import OperationLogRoute
from app.core.logger import logger
from .param import OnlineQueryParams
from .service import OnlineService
OnlineRouter = APIRouter(route_class=OperationLogRoute, prefix="/online", tags=["在线用户"])
@OnlineRouter.get(
'/list',
dependencies=[Depends(AuthPermission(permissions=['monitor:online:query']))],
summary="获取在线用户列表",
description="获取在线用户列表"
)
async def get_online_list_controller(
redis: Redis = Depends(redis_getter),
paging_query: PaginationQueryParams = Depends(),
search: OnlineQueryParams = Depends()
)->JSONResponse:
# 获取全量数据
result_dict_list = await OnlineService.get_online_list_service(redis=redis, search=search)
result_dict = await PaginationService.get_page_obj(data_list= result_dict_list, page_no= paging_query.page_no, page_size = paging_query.page_size)
logger.info('获取成功')
return SuccessResponse(data=result_dict,msg='获取成功')
@OnlineRouter.delete(
'/delete',
dependencies=[Depends(AuthPermission(permissions=['monitor:online:delete']))],
summary="强制下线",
description="强制下线"
)
async def delete_online_controller(
session_id: str = Body(..., description="会话编号"),
redis: Redis = Depends(redis_getter),
)->JSONResponse:
is_ok = await OnlineService.delete_online_service(redis=redis, session_id=session_id)
if is_ok:
logger.info("强制下线成功")
return SuccessResponse(msg="强制下线成功")
else:
logger.info("强制下线失败")
return ErrorResponse(msg="强制下线失败")
@OnlineRouter.delete(
'/clear',
dependencies=[Depends(AuthPermission(permissions=['monitor:online:delete']))],
summary="清除所有在线用户",
description="清除所有在线用户"
)
async def clear_online_controller(
redis: Redis = Depends(redis_getter),
)->JSONResponse:
is_ok = await OnlineService.clear_online_service(redis=redis)
if is_ok:
logger.info("清除所有在线用户成功")
return SuccessResponse(msg="清除所有在线用户成功")
else:
logger.info("清除所有在线用户失败")
return ErrorResponse(msg="清除所有在线用户失败")
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from typing import Optional
from fastapi import Query
class OnlineQueryParams:
"""在线用户查询参数"""
def __init__(
self,
name: Optional[str] = Query(None, description="登录名称"),
ipaddr: Optional[str] = Query(None, description="登陆IP地址"),
login_location: Optional[str] = Query(None, description="登录所属地"),
) -> None:
super().__init__()
# 模糊查询字段
self.name = ("like", f"%{name}%") if name else None
self.login_location = ("like", f"%{login_location}%") if login_location else None
self.ipaddr = ("like", f"%{ipaddr}%") if ipaddr else None
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field
from typing import Optional
from app.core.validator import DateTimeStr
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')
user_name: str = Field(..., description='用户名')
ipaddr: Optional[str] = Field(default=None, description='登陆IP地址')
login_location: Optional[str] = Field(default=None, description='登录所属地')
os: Optional[str] = Field(default=None, description='操作系统')
browser: Optional[str] = Field(default=None, description='浏览器')
login_time: Optional[DateTimeStr] = Field(default=None, description='登录时间')
login_type: Optional[str] = Field(default=None, description='登录类型 PC端 | 移动端')
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
import json
from typing import Dict, List, Optional
from redis.asyncio.client import Redis
from app.common.enums import RedisInitKeyConfig
from app.core.redis_crud import RedisCURD
from app.core.security import decode_access_token
from app.core.logger import logger
from .param import OnlineQueryParams
from .schema import OnlineOutSchema
class OnlineService:
"""在线用户管理模块服务层"""
@classmethod
async def get_online_list_service(cls, redis: Redis, search: Optional[OnlineQueryParams] = None) -> List[Dict]:
"""
获取在线用户列表信息(支持分页和搜索)
"""
keys = await RedisCURD(redis).get_keys(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*")
tokens = await RedisCURD(redis).mget(*keys)
online_users = []
for token in tokens:
if not token:
continue
try:
payload = decode_access_token(token=token)
session_info = json.loads(payload.sub)
if cls._match_search_conditions(session_info, search):
online_users.append(session_info)
except Exception as e:
logger.error(f"解析在线用户数据失败: {e}")
continue
# 按照 login_time 倒序排序
online_users.sort(key=lambda x: x.get('login_time', ''), reverse=True)
return online_users
@classmethod
async def delete_online_service(cls, redis: Redis, session_id: str) -> bool:
"""强制下线在线用户"""
# 删除 token
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}")
await RedisCURD(redis).delete(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}")
logger.info(f"强制下线用户会话: {session_id}")
return True
@classmethod
async def clear_online_service(cls, redis: Redis) -> bool:
"""强制下线在线用户"""
# 删除 token
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:*")
await RedisCURD(redis).clear(f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:*")
logger.info(f"清除所有在线用户会话成功")
return True
@staticmethod
def _match_search_conditions(online_info: Dict, search: Optional[OnlineQueryParams]) -> bool:
"""检查是否匹配搜索条件"""
if not search:
return True
if search.name and search.name[1]:
keyword = search.name[1].strip('%')
if keyword.lower() not in online_info.get("name", "").lower():
return False
if search.ipaddr and search.ipaddr[1]:
keyword = search.ipaddr[1].strip('%')
if keyword not in online_info.get("ipaddr", ""):
return False
if search.login_location and search.login_location[1]:
keyword = search.login_location[1].strip('%')
if keyword.lower() not in online_info.get("login_location", "").lower():
return False
return True
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from app.common.response import SuccessResponse
from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute
from app.core.logger import logger
from .service import ServerService
ServerRouter = APIRouter(route_class=OperationLogRoute, prefix="/server", tags=["服务器监控"])
@ServerRouter.get(
'/info',
summary="查询服务器监控信息",
description="查询服务器监控信息",
dependencies=[Depends(AuthPermission(permissions=["monitor:server:query"]))]
)
async def get_monitor_server_info_controller() -> JSONResponse:
# 获取全量数据
result_dict = await ServerService.get_server_monitor_info_service()
logger.info(f'获取服务器监控信息成功: {result_dict}')
return SuccessResponse(data=result_dict, msg='获取服务器监控信息成功')
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field
from typing import List
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系统使用率(%)")
free: float = Field(ge=0, le=100, description="CPU空闲率(%)")
class MemoryInfoSchema(BaseModel):
"""内存信息模型"""
model_config = ConfigDict(from_attributes=True)
total: str = Field(description="内存总量")
used: str = Field(description="已用内存")
free: str = Field(description="剩余内存")
usage: float = Field(ge=0, le=100, description="使用率(%)")
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="系统架构")
os_name: str = Field(description="操作系统")
user_dir: str = Field(description="项目路径")
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="启动时间")
run_time: str = Field(description="运行时长")
home: str = Field(description="安装路径")
memory_used: str = Field(description="内存占用")
memory_usage: float = Field(ge=0, le=100, description="内存使用率(%)")
memory_total: str = Field(description="总内存")
memory_free: str = Field(description="剩余内存")
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="磁盘类型")
total: str = Field(description="总容量")
used: str = Field(description="已用容量")
free: str = Field(description="可用容量")
usage: float = Field(ge=0, le=100, description="使用率(%)")
class ServerMonitorSchema(BaseModel):
"""服务器监控信息模型"""
model_config = ConfigDict(from_attributes=True)
cpu: CpuInfoSchema = Field(description="CPU信息")
mem: MemoryInfoSchema = Field(description="内存信息")
py: PyInfoSchema = Field(description="Python运行信息")
sys: SysInfoSchema = Field(description="系统信息")
disks: List[DiskInfoSchema] = Field(default_factory=list, description="磁盘信息")
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
import platform
import psutil
import socket
import time
from pathlib import Path
from typing import List, Dict
from app.utils.common_util import bytes2human
from .schema import (
CpuInfoSchema,
MemoryInfoSchema,
PyInfoSchema,
ServerMonitorSchema,
DiskInfoSchema,
SysInfoSchema
)
class ServerService:
"""服务监控模块服务层"""
@classmethod
async def get_server_monitor_info_service(cls) -> Dict:
"""获取服务器监控信息"""
return ServerMonitorSchema(
cpu=cls._get_cpu_info().model_dump(),
mem=cls._get_memory_info().model_dump(),
sys=cls._get_system_info().model_dump(),
py=cls._get_python_info().model_dump(),
disks=cls._get_disk_info()
).model_dump()
@classmethod
def _get_cpu_info(cls) -> CpuInfoSchema:
"""获取CPU信息"""
cpu_times = psutil.cpu_times_percent()
return CpuInfoSchema(
cpu_num=psutil.cpu_count(logical=True),
used=cpu_times.user,
sys=cpu_times.system,
free=cpu_times.idle
)
@classmethod
def _get_memory_info(cls) -> MemoryInfoSchema:
"""获取内存信息"""
memory = psutil.virtual_memory()
return MemoryInfoSchema(
total=bytes2human(memory.total),
used=bytes2human(memory.used),
free=bytes2human(memory.free),
usage=memory.percent
)
@classmethod
def _get_system_info(cls) -> SysInfoSchema:
"""获取系统信息"""
hostname = socket.gethostname()
return SysInfoSchema(
computer_ip=socket.gethostbyname(hostname),
computer_name=platform.node(),
os_arch=platform.machine(),
os_name=platform.platform(),
user_dir=str(Path.cwd())
)
@classmethod
def _get_python_info(cls) -> 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(),
start_time=time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time)),
run_time=run_time,
home=str(Path(current_process.exe())),
memory_total=bytes2human(memory.available),
memory_used=bytes2human(process_memory.rss),
memory_free=bytes2human(memory.available - process_memory.rss),
memory_usage=round((process_memory.rss / memory.available) * 100, 2)
)
@classmethod
def _get_disk_info(cls) -> List[Dict]:
"""获取磁盘信息"""
disk_info = []
for partition in psutil.disk_partitions():
try:
# 使用mountpoint而不是device来获取磁盘使用情况
usage = psutil.disk_usage(partition.mountpoint)
mount_point = str(Path(partition.mountpoint))
disk_info.append(
DiskInfoSchema(
dir_name=mount_point, # 使用mountpoint替代device
sys_type_name=partition.fstype,
type_name=f'本地固定磁盘({mount_point}',
total=bytes2human(usage.total),
used=bytes2human(usage.used),
free=bytes2human(usage.free),
usage=usage.percent # 直接使用数字而不是字符串
).model_dump()
)
except (PermissionError, FileNotFoundError):
# 明确指定可能的异常
continue
return disk_info
@classmethod
def _calculate_run_time(cls,start_time: float) -> str:
"""计算运行时间"""
difference = time.time() - start_time
days = int(difference // (24 * 60 * 60))
hours = int((difference % (24 * 60 * 60)) // (60 * 60))
minutes = int((difference % (60 * 60)) // 60)
return f'{days}{hours}小时{minutes}分钟'