upload project source code
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from fastapi.responses import 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.core.base_params import PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.logger import log
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .tools.ap_scheduler import SchedulerUtil
|
||||
from .service import JobService, JobLogService
|
||||
from .schema import (
|
||||
JobCreateSchema,
|
||||
JobUpdateSchema,
|
||||
JobQueryParam,
|
||||
JobLogQueryParam
|
||||
)
|
||||
|
||||
|
||||
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(["module_application:job:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取定时任务详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含定时任务详情的JSON响应
|
||||
"""
|
||||
result_dict = await JobService.get_job_detail_service(id=id, auth=auth)
|
||||
log.info(f"获取定时任务详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取定时任务详情成功")
|
||||
|
||||
@JobRouter.get("/list", summary="查询定时任务", description="查询定时任务")
|
||||
async def get_obj_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: JobQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询定时任务
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数模型
|
||||
- search (JobQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含分页后的定时任务列表的JSON响应
|
||||
"""
|
||||
result_dict_list = await JobService.get_job_list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size)
|
||||
log.info(f"查询定时任务列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询定时任务列表成功")
|
||||
|
||||
@JobRouter.post("/create", summary="创建定时任务", description="创建定时任务")
|
||||
async def create_obj_controller(
|
||||
data: JobCreateSchema,
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:create"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
参数:
|
||||
- data (JobCreateSchema): 创建参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建定时任务结果的JSON响应
|
||||
"""
|
||||
result_dict = await JobService.create_job_service(auth=auth, data=data)
|
||||
log.info(f"创建定时任务成功: {result_dict}")
|
||||
return SuccessResponse(data=result_dict, msg="创建定时任务成功")
|
||||
|
||||
@JobRouter.put("/update/{id}", summary="修改定时任务", description="修改定时任务")
|
||||
async def update_obj_controller(
|
||||
data: JobUpdateSchema,
|
||||
id: int = Path(..., description="定时任务ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:update"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
修改定时任务
|
||||
|
||||
参数:
|
||||
- data (JobUpdateSchema): 更新参数模型
|
||||
- id (int): 定时任务ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含修改定时任务结果的JSON响应
|
||||
"""
|
||||
result_dict = await JobService.update_job_service(auth=auth, id=id, data=data)
|
||||
log.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(["module_application:job:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除定时任务
|
||||
|
||||
参数:
|
||||
- ids (list[int]): ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除定时任务结果的JSON响应
|
||||
"""
|
||||
await JobService.delete_job_service(auth=auth, ids=ids)
|
||||
log.info(f"删除定时任务成功: {ids}")
|
||||
return SuccessResponse(msg="删除定时任务成功")
|
||||
|
||||
@JobRouter.post('/export', summary="导出定时任务", description="导出定时任务")
|
||||
async def export_obj_list_controller(
|
||||
search: JobQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出定时任务
|
||||
|
||||
参数:
|
||||
- search (JobQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- 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)
|
||||
log.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(["module_application:job:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清空定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清空定时任务结果的JSON响应
|
||||
"""
|
||||
await JobService.clear_job_service(auth=auth)
|
||||
log.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(["module_application:job:update"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
暂停/恢复/重启定时任务
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- option (int): 操作类型 1: 暂停 2: 恢复 3: 重启
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含操作定时任务结果的JSON响应
|
||||
"""
|
||||
await JobService.option_job_service(auth=auth, id=id, option=option)
|
||||
log.info(f"操作定时任务成功: {id}")
|
||||
return SuccessResponse(msg="操作定时任务成功")
|
||||
|
||||
@JobRouter.get("/log", summary="获取定时任务日志", description="获取定时任务日志", dependencies=[Depends(AuthPermission(["module_application:job:query"]))])
|
||||
async def get_job_log_controller():
|
||||
"""
|
||||
获取定时任务日志
|
||||
|
||||
返回:
|
||||
- JSONResponse: 获取定时任务日志的JSON响应
|
||||
"""
|
||||
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_single_job_status(job_id=i.id)
|
||||
}
|
||||
for i in SchedulerUtil.get_all_jobs()
|
||||
]
|
||||
|
||||
return SuccessResponse(msg="获取定时任务日志成功", data=data)
|
||||
|
||||
|
||||
# 定时任务日志管理接口
|
||||
@JobRouter.get("/log/detail/{id}", summary="获取定时任务日志详情", description="获取定时任务日志详情")
|
||||
async def get_job_log_detail_controller(
|
||||
id: int = Path(..., description="定时任务日志ID"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务日志ID
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 获取定时任务日志详情的JSON响应
|
||||
"""
|
||||
result_dict = await JobLogService.get_job_log_detail_service(id=id, auth=auth)
|
||||
log.info(f"获取定时任务日志详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取定时任务日志详情成功")
|
||||
|
||||
|
||||
@JobRouter.get("/log/list", summary="查询定时任务日志", description="查询定时任务日志")
|
||||
async def get_job_log_list_controller(
|
||||
page: PaginationQueryParam = Depends(),
|
||||
search: JobLogQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:query"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
查询定时任务日志
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数模型
|
||||
- search (JobLogQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 查询定时任务日志列表的JSON响应
|
||||
"""
|
||||
order_by = [{"created_time": "desc"}]
|
||||
result_dict_list = await JobLogService.get_job_log_list_service(auth=auth, search=search, order_by=order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
log.info(f"查询定时任务日志列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询定时任务日志列表成功")
|
||||
|
||||
|
||||
@JobRouter.delete("/log/delete", summary="删除定时任务日志", description="删除定时任务日志")
|
||||
async def delete_job_log_controller(
|
||||
ids: list[int] = Body(..., description="ID列表"),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
删除定时任务日志
|
||||
|
||||
参数:
|
||||
- ids (list[int]): ID列表
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含删除定时任务日志结果的JSON响应
|
||||
"""
|
||||
await JobLogService.delete_job_log_service(auth=auth, ids=ids)
|
||||
log.info(f"删除定时任务日志成功: {ids}")
|
||||
return SuccessResponse(msg="删除定时任务日志成功")
|
||||
|
||||
|
||||
@JobRouter.delete("/log/clear", summary="清空定时任务日志", description="清空定时任务日志")
|
||||
async def clear_job_log_controller(
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:delete"]))
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
清空定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含清空定时任务日志结果的JSON响应
|
||||
"""
|
||||
await JobLogService.clear_job_log_service(auth=auth)
|
||||
log.info(f"清空定时任务日志成功")
|
||||
return SuccessResponse(msg="清空定时任务日志成功")
|
||||
|
||||
|
||||
@JobRouter.post('/log/export', summary="导出定时任务日志", description="导出定时任务日志")
|
||||
async def export_job_log_list_controller(
|
||||
search: JobLogQueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["module_application:job:export"]))
|
||||
) -> StreamingResponse:
|
||||
"""
|
||||
导出定时任务日志
|
||||
|
||||
参数:
|
||||
- search (JobLogQueryParam): 查询参数模型
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
|
||||
返回:
|
||||
- StreamingResponse: 包含导出定时任务日志结果的流式响应
|
||||
"""
|
||||
result_dict_list = await JobLogService.get_job_log_list_service(search=search, auth=auth)
|
||||
export_result = await JobLogService.export_job_log_service(data_list=result_dict_list)
|
||||
log.info('导出定时任务日志成功')
|
||||
|
||||
return StreamResponse(
|
||||
data=bytes2file_response(export_result),
|
||||
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={
|
||||
'Content-Disposition': 'attachment; filename=job_log.xlsx'
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,162 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Sequence, Any
|
||||
|
||||
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
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
self.auth = auth
|
||||
super().__init__(model=JobModel, auth=auth)
|
||||
|
||||
async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> JobModel | None:
|
||||
"""
|
||||
获取定时任务详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- JobModel | None: 定时任务模型,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_obj_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[JobModel]:
|
||||
"""
|
||||
获取定时任务列表
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[JobModel]: 定时任务模型序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_obj_crud(self, data: JobCreateSchema) -> JobModel | None:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
参数:
|
||||
- data (JobCreateSchema): 创建定时任务模型
|
||||
|
||||
返回:
|
||||
- JobModel | None: 创建的定时任务模型,如果创建失败则为None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_obj_crud(self, id: int, data: JobUpdateSchema) -> JobModel | None:
|
||||
"""
|
||||
更新定时任务
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务ID
|
||||
- data (JobUpdateSchema): 更新定时任务模型
|
||||
|
||||
返回:
|
||||
- JobModel | None: 更新后的定时任务模型,如果更新失败则为None
|
||||
"""
|
||||
return await self.update(id=id, data=data)
|
||||
|
||||
async def delete_obj_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def set_obj_field_crud(self, ids: list[int], **kwargs) -> None:
|
||||
"""
|
||||
设置定时任务的可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
- kwargs: 其他要设置的字段,例如 available=True 或 available=False
|
||||
"""
|
||||
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
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
self.auth = auth
|
||||
super().__init__(model=JobLogModel, auth=auth)
|
||||
|
||||
async def get_obj_log_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> JobLogModel | None:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
参数:
|
||||
- id (int): 定时任务日志ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- JobLogModel | None: 定时任务日志模型,如果不存在则为None
|
||||
"""
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def get_obj_log_list_crud(self, search: dict | None = None, order_by: list[dict[str, str]] | None = None, preload: list[str | Any] | None = None) -> Sequence[JobLogModel]:
|
||||
"""
|
||||
获取定时任务日志列表
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[JobLogModel]: 定时任务日志模型序列
|
||||
"""
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def delete_obj_log_crud(self, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务日志
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 定时任务日志ID列表
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def clear_obj_log_crud(self) -> None:
|
||||
"""
|
||||
清除定时任务日志
|
||||
|
||||
注意:
|
||||
- 此操作会删除所有定时任务日志,请谨慎操作
|
||||
"""
|
||||
return await self.clear()
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.logger import log
|
||||
|
||||
def job(*args, **kwargs) -> None:
|
||||
"""
|
||||
定时任务执行同步函数示例
|
||||
|
||||
参数:
|
||||
- args: 位置参数。
|
||||
- kwargs: 关键字参数。
|
||||
"""
|
||||
try:
|
||||
print(f"开始执行任务: {args}-{kwargs}")
|
||||
time.sleep(3)
|
||||
print(f'{datetime.now()}同步函数执行完成')
|
||||
except Exception as e:
|
||||
log.error(f"同步任务执行失败: {e}")
|
||||
raise
|
||||
|
||||
async def async_job(*args, **kwargs) -> None:
|
||||
"""
|
||||
定时任务执行异步函数示例
|
||||
|
||||
参数:
|
||||
- args: 位置参数。
|
||||
- kwargs: 关键字参数。
|
||||
"""
|
||||
try:
|
||||
print(f"开始执行任务: {args}-{kwargs}")
|
||||
time.sleep(3)
|
||||
print(f'{datetime.now()}异步函数执行完成')
|
||||
except Exception as e:
|
||||
log.error(f"异步任务执行失败: {e}")
|
||||
raise
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sqlalchemy import Boolean, String, Integer, Text, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class JobModel(ModelMixin, UserMixin):
|
||||
"""
|
||||
定时任务调度表
|
||||
- 0: 运行中
|
||||
- 1: 暂停中
|
||||
"""
|
||||
__tablename__: str = 'app_job'
|
||||
__table_args__: dict[str, str] = ({'comment': '定时任务调度表'})
|
||||
__loader_options__: list[str] = ["job_logs", "created_by", "updated_by"]
|
||||
|
||||
name: Mapped[str | None] = mapped_column(String(64), nullable=True, default='', comment='任务名称')
|
||||
jobstore: Mapped[str | None] = mapped_column(String(64), nullable=True, default='default', comment='存储器')
|
||||
executor: Mapped[str | None] = mapped_column(String(64), nullable=True, default='default', comment='执行器:将运行此作业的执行程序的名称')
|
||||
trigger: Mapped[str] = mapped_column(String(64), nullable=False, comment='触发器:控制此作业计划的 trigger 对象')
|
||||
trigger_args: Mapped[str | None] = mapped_column(Text, nullable=True, comment='触发器参数')
|
||||
func: Mapped[str] = mapped_column(Text, nullable=False, comment='任务函数')
|
||||
args: Mapped[str | None] = mapped_column(Text, nullable=True, comment='位置参数')
|
||||
kwargs: Mapped[str | None] = 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[str | None] = mapped_column(String(64), nullable=True, comment='开始时间')
|
||||
end_date: Mapped[str | None] = mapped_column(String(64), nullable=True, comment='结束时间')
|
||||
|
||||
# 关联关系
|
||||
job_logs: Mapped[list['JobLogModel'] | None] = relationship(
|
||||
back_populates="job",
|
||||
lazy="selectin"
|
||||
)
|
||||
|
||||
|
||||
class JobLogModel(ModelMixin):
|
||||
"""
|
||||
定时任务调度日志表
|
||||
"""
|
||||
__tablename__: str = 'app_job_log'
|
||||
__table_args__: dict[str, str] = ({'comment': '定时任务调度日志表'})
|
||||
__loader_options__: list[str] = ["job"]
|
||||
|
||||
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[str | None] = mapped_column(String(255), nullable=True, default='', comment='位置参数')
|
||||
job_kwargs: Mapped[str | None] = mapped_column(String(255), nullable=True, default='', comment='关键字参数')
|
||||
job_trigger: Mapped[str | None] = mapped_column(String(255), nullable=True, default='', comment='任务触发器')
|
||||
job_message: Mapped[str | None] = mapped_column(String(500), nullable=True, default='', comment='日志信息')
|
||||
exception_info: Mapped[str | None] = mapped_column(String(2000), nullable=True, default='', comment='异常信息')
|
||||
|
||||
# 任务关联
|
||||
job_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey('app_job.id', ondelete="CASCADE"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment='任务ID'
|
||||
)
|
||||
|
||||
job: Mapped["JobModel | None"] = relationship(
|
||||
back_populates="job_logs",
|
||||
lazy="selectin"
|
||||
)
|
||||
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from fastapi import Query
|
||||
|
||||
from app.core.base_schema import BaseSchema, UserBySchema
|
||||
from app.core.validator import DateTimeStr, datetime_validator
|
||||
|
||||
|
||||
class JobCreateSchema(BaseModel):
|
||||
"""
|
||||
定时任务调度表对应pydantic模型
|
||||
"""
|
||||
name: str = Field(..., max_length=64, description='任务名称')
|
||||
func: str = Field(..., description='任务函数')
|
||||
trigger: str = Field(..., description='触发器:控制此作业计划的 trigger 对象')
|
||||
args: str | None = Field(default=None, description='位置参数')
|
||||
kwargs: str | None = Field(default=None, description='关键字参数')
|
||||
coalesce: bool | None = Field(..., description='是否合并运行:是否在多个运行时间到期时仅运行作业一次')
|
||||
max_instances: int | None = Field(default=1, ge=1, description='最大实例数:允许的最大并发执行实例数')
|
||||
jobstore: str | None = Field(..., max_length=64, description='任务存储')
|
||||
executor: str | None = Field(..., max_length=64, description='任务执行器:将运行此作业的执行程序的名称')
|
||||
trigger_args: str | None = Field(default=None, description='触发器参数')
|
||||
start_date: str | None = Field(default=None, description='开始时间')
|
||||
end_date: str | None = Field(default=None, description='结束时间')
|
||||
description: str | None = Field(default=None, max_length=255, description='描述')
|
||||
status: str = Field(default='0', description='任务状态:启动,停止')
|
||||
|
||||
@field_validator('trigger')
|
||||
@classmethod
|
||||
def _validate_trigger(cls, v: str) -> str:
|
||||
allowed = {'cron', 'interval', 'date'}
|
||||
v = v.strip()
|
||||
if v not in allowed:
|
||||
raise ValueError('触发器必须为 cron/interval/date')
|
||||
return v
|
||||
|
||||
@model_validator(mode='after')
|
||||
def _validate_dates(self):
|
||||
"""跨字段校验:结束时间不得早于开始时间。"""
|
||||
if self.start_date and self.end_date:
|
||||
try:
|
||||
start = datetime_validator(self.start_date)
|
||||
end = datetime_validator(self.end_date)
|
||||
except Exception:
|
||||
raise ValueError('时间格式必须为 YYYY-MM-DD HH:MM:SS')
|
||||
if end < start:
|
||||
raise ValueError('结束时间不能早于开始时间')
|
||||
return self
|
||||
|
||||
|
||||
class JobUpdateSchema(JobCreateSchema):
|
||||
"""定时任务更新模型"""
|
||||
...
|
||||
|
||||
|
||||
class JobOutSchema(JobCreateSchema, BaseSchema, UserBySchema):
|
||||
"""定时任务响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
...
|
||||
|
||||
|
||||
class JobLogCreateSchema(BaseModel):
|
||||
"""
|
||||
定时任务调度日志表对应pydantic模型
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
job_name: str = Field(..., description='任务名称')
|
||||
job_group: str | None = Field(default=None, description='任务组名')
|
||||
job_executor: str | None = Field(default=None, description='任务执行器')
|
||||
invoke_target: str | None = Field(default=None, description='调用目标字符串')
|
||||
job_args: str | None = Field(default=None, description='位置参数')
|
||||
job_kwargs: str | None = Field(default=None, description='关键字参数')
|
||||
job_trigger: str | None = Field(default=None, description='任务触发器')
|
||||
job_message: str | None = Field(default=None, description='日志信息')
|
||||
exception_info: str | None = Field(default=None, description='异常信息')
|
||||
status: str = Field(default='0', description='任务状态:正常,失败')
|
||||
description: str | None = Field(default=None, max_length=255, description='描述')
|
||||
created_time: DateTimeStr | None = Field(default=None, description='创建时间')
|
||||
updated_time: DateTimeStr | None = Field(default=None, description='更新时间')
|
||||
|
||||
|
||||
class JobLogUpdateSchema(JobLogCreateSchema):
|
||||
"""定时任务调度日志表更新模型"""
|
||||
...
|
||||
id: int | None = Field(default=None, description='任务日志ID')
|
||||
|
||||
|
||||
class JobLogOutSchema(JobLogUpdateSchema, BaseSchema, UserBySchema):
|
||||
"""定时任务调度日志表响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
...
|
||||
|
||||
|
||||
class JobQueryParam:
|
||||
"""定时任务查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str | None = Query(None, description="任务名称"),
|
||||
status: str | None = Query(None, description="状态: 启动,停止"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
created_id: int | None = Query(None, description="创建人"),
|
||||
updated_id: int | None = Query(None, description="更新人"),
|
||||
) -> None:
|
||||
|
||||
# 模糊查询字段
|
||||
self.name = ("like", f"%{name}%") if name else None
|
||||
|
||||
# 精确查询字段
|
||||
self.created_id = created_id
|
||||
self.updated_id = updated_id
|
||||
self.status = status
|
||||
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
|
||||
|
||||
class JobLogQueryParam:
|
||||
"""定时任务查询参数"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
job_id: int | None = Query(None, description="定时任务ID"),
|
||||
job_name: str | None = Query(None, description="任务名称"),
|
||||
status: str | None = Query(None, description="状态: 正常,失败"),
|
||||
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
|
||||
) -> None:
|
||||
# 定时任务ID查询
|
||||
self.job_id = job_id
|
||||
# 模糊查询字段
|
||||
self.job_name = ("like", job_name)
|
||||
# 精确查询字段
|
||||
self.status = status
|
||||
# 时间范围查询
|
||||
if created_time and len(created_time) == 2:
|
||||
self.created_time = ("between", (created_time[0], created_time[1]))
|
||||
if updated_time and len(updated_time) == 2:
|
||||
self.updated_time = ("between", (updated_time[0], updated_time[1]))
|
||||
@@ -0,0 +1,307 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
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 .tools.ap_scheduler import SchedulerUtil
|
||||
from .crud import JobCRUD, JobLogCRUD
|
||||
from .schema import (
|
||||
JobCreateSchema,
|
||||
JobUpdateSchema,
|
||||
JobOutSchema,
|
||||
JobLogOutSchema,
|
||||
JobQueryParam,
|
||||
JobLogQueryParam
|
||||
)
|
||||
|
||||
|
||||
class JobService:
|
||||
"""
|
||||
定时任务管理模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_job_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取定时任务详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务ID
|
||||
|
||||
返回:
|
||||
- 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: JobQueryParam | None = None, order_by: list[dict[str, str]] | None = None) -> list[dict]:
|
||||
"""
|
||||
获取定时任务列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (JobQueryParam | None): 查询参数模型
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
|
||||
返回:
|
||||
- List[Dict]: 定时任务详情字典列表
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
创建定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- data (JobCreateSchema): 定时任务创建模型
|
||||
|
||||
返回:
|
||||
- Dict: 定时任务详情字典
|
||||
"""
|
||||
exist_obj = await JobCRUD(auth).get(name=data.name)
|
||||
if exist_obj:
|
||||
raise CustomException(msg='创建失败,该定时任务已存在')
|
||||
|
||||
obj = await JobCRUD(auth).create_obj_crud(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg='创建失败,该数据定时任务不存在')
|
||||
SchedulerUtil().add_job(job_info=obj)
|
||||
return JobOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def update_job_service(cls, auth: AuthSchema, id:int, data: JobUpdateSchema) -> dict:
|
||||
"""
|
||||
更新定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务ID
|
||||
- data (JobUpdateSchema): 定时任务更新模型
|
||||
|
||||
返回:
|
||||
- dict: 定时任务详情字典
|
||||
"""
|
||||
exist_obj = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg='更新失败,该定时任务不存在')
|
||||
if data.trigger == 'cron' and data.trigger_args and not CronUtil.validate_cron_expression(data.trigger_args):
|
||||
raise CustomException(msg=f'新增定时任务{data.name}失败, Cron表达式不正确')
|
||||
obj = await JobCRUD(auth).update_obj_crud(id=id, data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg='更新失败,该数据定时任务不存在')
|
||||
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:
|
||||
"""
|
||||
删除定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 定时任务ID列表
|
||||
"""
|
||||
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='删除失败,该数据定时任务不存在')
|
||||
obj = await JobLogCRUD(auth).get(job_id=id)
|
||||
if obj:
|
||||
raise CustomException(msg=f'删除失败,该定时任务存 {exist_obj.name} 在日志记录')
|
||||
|
||||
SchedulerUtil().remove_job(job_id=id)
|
||||
await JobCRUD(auth).delete_obj_crud(ids=ids)
|
||||
|
||||
|
||||
@classmethod
|
||||
async def clear_job_service(cls, auth: AuthSchema) -> None:
|
||||
"""
|
||||
清空所有定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
SchedulerUtil().clear_jobs()
|
||||
await JobLogCRUD(auth).clear_obj_log_crud()
|
||||
await JobCRUD(auth).clear_obj_crud()
|
||||
|
||||
@classmethod
|
||||
async def option_job_service(cls, auth: AuthSchema, id: int, option: int) -> None:
|
||||
"""
|
||||
操作定时任务
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务ID
|
||||
- option (int): 操作类型, 1: 暂停 2: 恢复 3: 重启
|
||||
"""
|
||||
# 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().remove_job(job_id=id)
|
||||
# 获取最新的任务配置
|
||||
updated_job = await JobCRUD(auth).get_obj_by_id_crud(id=id)
|
||||
if updated_job:
|
||||
# 重新添加任务
|
||||
SchedulerUtil.add_job(job_info=updated_job)
|
||||
# 设置状态为运行中
|
||||
await JobCRUD(auth).set_obj_field_crud(ids=[id], status=True)
|
||||
|
||||
@classmethod
|
||||
async def export_job_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出定时任务列表
|
||||
|
||||
参数:
|
||||
- data_list (list[dict]): 定时任务列表
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
"""
|
||||
mapping_dict = {
|
||||
'id': '编号',
|
||||
'name': '任务名称',
|
||||
'func': '任务函数',
|
||||
'trigger': '触发器',
|
||||
'args': '位置参数',
|
||||
'kwargs': '关键字参数',
|
||||
'coalesce': '是否合并运行',
|
||||
'max_instances': '最大实例数',
|
||||
'jobstore': '任务存储',
|
||||
'executor': '任务执行器',
|
||||
'trigger_args': '触发器参数',
|
||||
'status': '任务状态',
|
||||
'message': '日志信息',
|
||||
'description': '备注',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
'created_id': '创建者ID',
|
||||
'updated_id': '更新者ID',
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = data_list.copy()
|
||||
for item in data:
|
||||
item['status'] = '已完成' if item['status'] == '0' else '运行中' if item['status'] == '1' else '暂停'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
|
||||
class JobLogService:
|
||||
"""
|
||||
定时任务日志管理模块服务层
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
async def get_job_log_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
"""
|
||||
获取定时任务日志详情
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- id (int): 定时任务日志ID
|
||||
|
||||
返回:
|
||||
- dict: 定时任务日志详情字典
|
||||
"""
|
||||
obj = await JobLogCRUD(auth).get_obj_log_by_id_crud(id=id)
|
||||
return JobLogOutSchema.model_validate(obj).model_dump()
|
||||
|
||||
@classmethod
|
||||
async def get_job_log_list_service(cls, auth: AuthSchema, search: JobLogQueryParam | None = None, order_by: list[dict] | None = None) -> list[dict]:
|
||||
"""
|
||||
获取定时任务日志列表
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- search (JobLogQueryParam | None): 查询参数模型, 包含分页信息和查询条件
|
||||
- order_by (list[dict] | None): 排序参数列表, 每个元素为一个字典, 包含字段名和排序方向
|
||||
|
||||
返回:
|
||||
- list[dict]: 定时任务日志详情字典列表
|
||||
"""
|
||||
obj_list = await JobLogCRUD(auth).get_obj_log_list_crud(search=search.__dict__, order_by=order_by)
|
||||
return [JobLogOutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def delete_job_log_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
"""
|
||||
删除定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- ids (list[int]): 定时任务日志ID列表
|
||||
"""
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg='删除失败,删除对象不能为空')
|
||||
for id in ids:
|
||||
exist_obj = await JobLogCRUD(auth).get_obj_log_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg=f'删除失败,该定时任务日志ID为{id}的记录不存在')
|
||||
await JobLogCRUD(auth).delete_obj_log_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def clear_job_log_service(cls, auth: AuthSchema) -> None:
|
||||
"""
|
||||
清空定时任务日志
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
"""
|
||||
# 获取所有日志ID并批量删除
|
||||
all_logs = await JobLogCRUD(auth).get_obj_log_list_crud()
|
||||
if all_logs:
|
||||
ids = [log.id for log in all_logs]
|
||||
await JobLogCRUD(auth).delete_obj_log_crud(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def export_job_log_service(cls, data_list: list[dict]) -> bytes:
|
||||
"""
|
||||
导出定时任务日志列表
|
||||
|
||||
参数:
|
||||
- data_list (List[Dict[str, Any]]): 定时任务日志列表
|
||||
|
||||
返回:
|
||||
- bytes: Excel文件字节流
|
||||
"""
|
||||
mapping_dict = {
|
||||
'id': '编号',
|
||||
'job_name': '任务名称',
|
||||
'job_group': '任务组名',
|
||||
'job_executor': '任务执行器',
|
||||
'invoke_target': '调用目标字符串',
|
||||
'job_args': '位置参数',
|
||||
'job_kwargs': '关键字参数',
|
||||
'job_trigger': '任务触发器',
|
||||
'job_message': '日志信息',
|
||||
'exception_info': '异常信息',
|
||||
'status': '执行状态',
|
||||
'created_time': '创建时间',
|
||||
'updated_time': '更新时间',
|
||||
}
|
||||
|
||||
# 复制数据并转换状态
|
||||
data = data_list.copy()
|
||||
for item in data:
|
||||
item['status'] = '成功' if item.get('status') == '0' else '失败'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import importlib
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from asyncio import iscoroutinefunction
|
||||
from apscheduler.job import Job
|
||||
from apscheduler.events import JobExecutionEvent, EVENT_ALL, JobEvent
|
||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.executors.pool import ProcessPoolExecutor
|
||||
from apscheduler.jobstores.memory import MemoryJobStore
|
||||
from apscheduler.jobstores.redis import RedisJobStore
|
||||
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.database import engine, db_session, async_db_session
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.utils.cron_util import CronUtil
|
||||
|
||||
from app.api.v1.module_application.job.model import JobModel
|
||||
|
||||
job_stores = {
|
||||
'default': MemoryJobStore(),
|
||||
'sqlalchemy': SQLAlchemyJobStore(url=settings.DB_URI, engine=engine),
|
||||
'redis': RedisJobStore(
|
||||
host=settings.REDIS_HOST,
|
||||
port=int(settings.REDIS_PORT),
|
||||
username=settings.REDIS_USER,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=int(settings.REDIS_DB_NAME),
|
||||
),
|
||||
}
|
||||
# 配置执行器
|
||||
executors = {
|
||||
'default': AsyncIOExecutor(),
|
||||
'processpool': ProcessPoolExecutor(max_workers=1) # 减少进程数量以减少资源消耗
|
||||
}
|
||||
# 配置默认参数
|
||||
job_defaults = {
|
||||
'coalesce': True, # 合并执行错过的任务
|
||||
'max_instances': 1, # 最大实例数
|
||||
}
|
||||
# 配置调度器
|
||||
scheduler = AsyncIOScheduler()
|
||||
scheduler.configure(
|
||||
jobstores=job_stores,
|
||||
executors=executors,
|
||||
job_defaults=job_defaults,
|
||||
timezone='Asia/Shanghai'
|
||||
)
|
||||
|
||||
class SchedulerUtil:
|
||||
"""
|
||||
定时任务相关方法
|
||||
"""
|
||||
@classmethod
|
||||
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
|
||||
"""
|
||||
监听任务执行事件。
|
||||
|
||||
参数:
|
||||
- event (JobEvent | JobExecutionEvent): 任务事件对象。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 延迟导入避免循环导入
|
||||
from app.api.v1.module_application.job.model import JobLogModel
|
||||
|
||||
# 获取事件类型和任务ID
|
||||
event_type = event.__class__.__name__
|
||||
# 初始化任务状态
|
||||
status = True
|
||||
exception_info = ''
|
||||
if isinstance(event, JobExecutionEvent) and event.exception:
|
||||
exception_info = str(event.exception)
|
||||
status = False
|
||||
if hasattr(event, 'job_id'):
|
||||
job_id = event.job_id
|
||||
query_job = cls.get_job(job_id=job_id)
|
||||
if query_job:
|
||||
query_job_info = query_job.__getstate__()
|
||||
# 获取任务名称
|
||||
job_name = query_job_info.get('name')
|
||||
# 获取任务组名
|
||||
job_group = query_job._jobstore_alias
|
||||
# # 获取任务执行器
|
||||
job_executor = query_job_info.get('executor')
|
||||
# 获取调用目标字符串
|
||||
invoke_target = query_job_info.get('func')
|
||||
# 获取调用函数位置参数
|
||||
job_args = ','.join(map(str, query_job_info.get('args', [])))
|
||||
# 获取调用函数关键字参数
|
||||
job_kwargs = json.dumps(query_job_info.get('kwargs'))
|
||||
# 获取任务触发器
|
||||
job_trigger = str(query_job_info.get('trigger'))
|
||||
# 构造日志消息
|
||||
job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 任务名称: {job_name}, 状态: {status}, 任务组: {job_group}, 错误详情: {exception_info}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
# 创建ORM对象
|
||||
job_log = JobLogModel(
|
||||
job_name=job_name,
|
||||
job_group=job_group,
|
||||
job_executor=job_executor,
|
||||
invoke_target=invoke_target,
|
||||
job_args=job_args,
|
||||
job_kwargs=job_kwargs,
|
||||
job_trigger=job_trigger,
|
||||
job_message=job_message,
|
||||
status=status,
|
||||
exception_info=exception_info,
|
||||
created_time=datetime.now(),
|
||||
updated_time=datetime.now(),
|
||||
job_id=job_id,
|
||||
)
|
||||
|
||||
# 使用线程池执行操作以避免阻塞调度器和数据库锁定问题
|
||||
executor = ThreadPoolExecutor(max_workers=1)
|
||||
executor.submit(cls._save_job_log_async_wrapper, job_log)
|
||||
executor.shutdown(wait=False)
|
||||
|
||||
@classmethod
|
||||
def _save_job_log_async_wrapper(cls, job_log) -> None:
|
||||
"""
|
||||
异步保存任务日志的包装器函数,在独立线程中运行
|
||||
|
||||
参数:
|
||||
- job_log (JobLogModel): 任务日志对象
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
with db_session.begin() as session:
|
||||
try:
|
||||
session.add(job_log)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
log.error(f"保存任务日志失败: {str(e)}")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@classmethod
|
||||
async def init_system_scheduler(cls) -> None:
|
||||
"""
|
||||
应用启动时初始化定时任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
# 延迟导入避免循环导入
|
||||
from app.api.v1.module_application.job.crud import JobCRUD
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
log.info('🔎 开始启动定时任务...')
|
||||
|
||||
# 启动调度器
|
||||
scheduler.start()
|
||||
|
||||
# 添加事件监听器
|
||||
scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL)
|
||||
|
||||
async with async_db_session() as session:
|
||||
async with session.begin():
|
||||
auth = AuthSchema(db=session)
|
||||
job_list = await JobCRUD(auth).get_obj_list_crud()
|
||||
|
||||
# 只在一个实例上初始化任务
|
||||
# 使用Redis锁确保只有一个实例执行任务初始化
|
||||
import redis.asyncio as redis
|
||||
redis_client = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=int(settings.REDIS_PORT),
|
||||
username=settings.REDIS_USER,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=int(settings.REDIS_DB_NAME),
|
||||
)
|
||||
|
||||
# 尝试获取锁,过期时间10秒
|
||||
lock_key = "scheduler_init_lock"
|
||||
lock_acquired = await redis_client.set(lock_key, "1", ex=10, nx=True)
|
||||
|
||||
if lock_acquired:
|
||||
try:
|
||||
for item in job_list:
|
||||
# 检查任务是否已经存在
|
||||
existing_job = cls.get_job(job_id=item.id)
|
||||
if existing_job:
|
||||
cls.remove_job(job_id=item.id) # 删除旧任务
|
||||
|
||||
# 添加新任务
|
||||
cls.add_job(item)
|
||||
|
||||
# 根据数据库中保存的状态来设置任务状态
|
||||
if hasattr(item, 'status') and item.status == "1":
|
||||
# 如果任务状态为暂停,则立即暂停刚添加的任务
|
||||
cls.pause_job(job_id=item.id)
|
||||
log.info('✅️ 系统初始定时任务加载成功')
|
||||
finally:
|
||||
# 释放锁
|
||||
await redis_client.delete(lock_key)
|
||||
else:
|
||||
# 等待其他实例完成初始化
|
||||
import asyncio
|
||||
await asyncio.sleep(2)
|
||||
log.info('✅️ 定时任务已由其他实例初始化完成')
|
||||
|
||||
@classmethod
|
||||
async def close_system_scheduler(cls) -> None:
|
||||
"""
|
||||
关闭系统定时任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
try:
|
||||
# 移除所有任务
|
||||
scheduler.remove_all_jobs()
|
||||
# 等待所有任务完成后再关闭
|
||||
scheduler.shutdown(wait=True)
|
||||
log.info('✅️ 关闭定时任务成功')
|
||||
except Exception as e:
|
||||
log.error(f'关闭定时任务失败: {str(e)}')
|
||||
|
||||
@classmethod
|
||||
def get_job(cls, job_id: str | int) -> Job | None:
|
||||
"""
|
||||
根据任务ID获取任务对象。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- Job | None: 任务对象,未找到则为 None。
|
||||
"""
|
||||
return scheduler.get_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def get_all_jobs(cls) -> list[Job]:
|
||||
"""
|
||||
获取全部调度任务列表。
|
||||
|
||||
返回:
|
||||
- list[Job]: 任务列表。
|
||||
"""
|
||||
return scheduler.get_jobs()
|
||||
|
||||
@classmethod
|
||||
async def _task_wrapper(cls, job_id, func, *args, **kwargs):
|
||||
"""
|
||||
任务执行包装器,添加分布式锁防止同一任务被多个实例同时执行。
|
||||
|
||||
参数:
|
||||
- job_id: 任务ID
|
||||
- func: 实际要执行的任务函数
|
||||
- *args: 任务函数位置参数
|
||||
- **kwargs: 任务函数关键字参数
|
||||
|
||||
返回:
|
||||
- 任务函数的返回值
|
||||
"""
|
||||
import redis.asyncio as redis
|
||||
import asyncio
|
||||
from app.config.setting import settings
|
||||
|
||||
# 创建Redis客户端
|
||||
redis_client = redis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=int(settings.REDIS_PORT),
|
||||
username=settings.REDIS_USER,
|
||||
password=settings.REDIS_PASSWORD,
|
||||
db=int(settings.REDIS_DB_NAME),
|
||||
)
|
||||
|
||||
# 生成锁键
|
||||
lock_key = f"job_lock:{job_id}"
|
||||
|
||||
# 设置锁的过期时间(根据任务类型调整,这里设置为30秒)
|
||||
lock_expire = 30
|
||||
lock_acquired = False
|
||||
|
||||
try:
|
||||
# 尝试获取锁
|
||||
lock_acquired = await redis_client.set(lock_key, "1", ex=lock_expire, nx=True)
|
||||
|
||||
if lock_acquired:
|
||||
log.info(f"任务 {job_id} 获取执行锁成功")
|
||||
# 执行任务
|
||||
if iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
else:
|
||||
# 对于同步函数,使用线程池执行
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(None, func, *args, **kwargs)
|
||||
else:
|
||||
# 获取锁失败,记录日志
|
||||
log.info(f"任务 {job_id} 获取执行锁失败,跳过本次执行")
|
||||
return None
|
||||
finally:
|
||||
# 释放锁
|
||||
if lock_acquired:
|
||||
await redis_client.delete(lock_key)
|
||||
log.info(f"任务 {job_id} 释放执行锁")
|
||||
|
||||
@classmethod
|
||||
def add_job(cls, job_info: JobModel) -> Job:
|
||||
"""
|
||||
根据任务配置创建并添加调度任务。
|
||||
|
||||
参数:
|
||||
- job_info (JobModel): 任务对象信息(包含触发器、函数、参数等)。
|
||||
|
||||
返回:
|
||||
- Job: 新增的任务对象。
|
||||
"""
|
||||
# 动态导入模块
|
||||
# 1. 解析调用目标
|
||||
module_path, func_name = str(job_info.func).rsplit('.', 1)
|
||||
module_path = "app.api.v1.module_application.job.function_task." + module_path
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
job_func = getattr(module, func_name)
|
||||
|
||||
# 2. 确定任务存储器:优先使用redis,确保分布式环境中任务同步
|
||||
if job_info.jobstore is None:
|
||||
job_info.jobstore = 'redis' # 改为默认使用redis存储
|
||||
|
||||
# 3. 确定执行器
|
||||
job_executor = job_info.executor
|
||||
if job_executor is None:
|
||||
job_executor = 'default'
|
||||
|
||||
if job_info.trigger_args is None:
|
||||
raise ValueError("触发器缺少参数")
|
||||
|
||||
# 异步函数必须使用默认执行器
|
||||
if iscoroutinefunction(job_func):
|
||||
job_executor = 'default'
|
||||
|
||||
# 4. 创建触发器
|
||||
if job_info.trigger == 'date':
|
||||
trigger = DateTrigger(run_date=job_info.trigger_args)
|
||||
elif job_info.trigger == 'interval':
|
||||
# 将传入的 interval 表达式拆分为不同的字段
|
||||
fields = job_info.trigger_args.strip().split()
|
||||
if len(fields) != 5:
|
||||
raise ValueError("无效的 interval 表达式")
|
||||
second, minute, hour, day, week = tuple([int(field) if field != '*' else 0 for field in fields])
|
||||
# 秒、分、时、天、周(* * * * 1)
|
||||
trigger = IntervalTrigger(
|
||||
weeks=week,
|
||||
days=day,
|
||||
hours=hour,
|
||||
minutes=minute,
|
||||
seconds=second,
|
||||
start_date=job_info.start_date,
|
||||
end_date=job_info.end_date,
|
||||
timezone='Asia/Shanghai',
|
||||
jitter=None
|
||||
)
|
||||
elif job_info.trigger == 'cron':
|
||||
# 秒、分、时、天、月、星期几、年 ()
|
||||
fields = job_info.trigger_args.strip().split()
|
||||
if len(fields) not in (6, 7):
|
||||
raise ValueError("无效的 Cron 表达式")
|
||||
if not CronUtil.validate_cron_expression(job_info.trigger_args):
|
||||
raise ValueError(f'定时任务{job_info.name}, Cron表达式不正确')
|
||||
|
||||
parsed_fields = [None if field in ('*', '?') else field for field in fields]
|
||||
if len(fields) == 6:
|
||||
parsed_fields.append(None)
|
||||
|
||||
second, minute, hour, day, month, day_of_week, year = tuple(parsed_fields)
|
||||
trigger = CronTrigger(
|
||||
second=second,
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day=day,
|
||||
month=month,
|
||||
day_of_week=day_of_week,
|
||||
year=year,
|
||||
start_date=job_info.start_date,
|
||||
end_date=job_info.end_date,
|
||||
timezone='Asia/Shanghai'
|
||||
)
|
||||
else:
|
||||
raise ValueError("无效的 trigger 触发器")
|
||||
|
||||
# 5. 添加任务(使用包装器函数)
|
||||
job = scheduler.add_job(
|
||||
func=cls._task_wrapper,
|
||||
trigger=trigger,
|
||||
args=[str(job_info.id), job_func] + (str(job_info.args).split(',') if job_info.args else []),
|
||||
kwargs=json.loads(job_info.kwargs) if job_info.kwargs else {},
|
||||
id=str(job_info.id),
|
||||
name=job_info.name,
|
||||
coalesce=job_info.coalesce,
|
||||
max_instances=1, # 确保只有一个实例执行
|
||||
jobstore=job_info.jobstore,
|
||||
executor=job_executor,
|
||||
)
|
||||
log.info(f"任务 {job_info.id} 添加到 {job_info.jobstore} 存储器成功")
|
||||
return job
|
||||
except ModuleNotFoundError:
|
||||
raise ValueError(f"未找到该模块:{module_path}")
|
||||
except AttributeError:
|
||||
raise ValueError(f"未找到该模块下的方法:{func_name}")
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f"添加任务失败: {str(e)}")
|
||||
|
||||
@classmethod
|
||||
def remove_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
根据任务ID删除调度任务。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if query_job:
|
||||
scheduler.remove_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def clear_jobs(cls) -> None:
|
||||
"""
|
||||
删除所有调度任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
scheduler.remove_all_jobs()
|
||||
|
||||
@classmethod
|
||||
def modify_job(cls, job_id: str | int) -> Job:
|
||||
"""
|
||||
更新指定任务的配置(运行中的任务下次执行生效)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- Job: 更新后的任务对象。
|
||||
|
||||
异常:
|
||||
- CustomException: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise CustomException(msg=f"未找到该任务:{job_id}")
|
||||
return scheduler.modify_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def pause_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
暂停指定任务(仅运行中可暂停,已终止不可)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
|
||||
异常:
|
||||
- ValueError: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise ValueError(f"未找到该任务:{job_id}")
|
||||
scheduler.pause_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def resume_job(cls, job_id: str | int) -> None:
|
||||
"""
|
||||
恢复指定任务(仅暂停中可恢复,已终止不可)。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
|
||||
返回:
|
||||
- None
|
||||
|
||||
异常:
|
||||
- ValueError: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise ValueError(f"未找到该任务:{job_id}")
|
||||
scheduler.resume_job(job_id=str(job_id))
|
||||
|
||||
@classmethod
|
||||
def reschedule_job(cls, job_id: str | int, trigger=None, **trigger_args) -> Job | None:
|
||||
"""
|
||||
重启指定任务的触发器。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID。
|
||||
- trigger: 触发器类型
|
||||
- **trigger_args: 触发器参数
|
||||
|
||||
返回:
|
||||
- Job: 更新后的任务对象
|
||||
|
||||
异常:
|
||||
- CustomException: 当任务不存在时抛出。
|
||||
"""
|
||||
query_job = cls.get_job(job_id=str(job_id))
|
||||
if not query_job:
|
||||
raise CustomException(msg=f"未找到该任务:{job_id}")
|
||||
|
||||
# 如果没有提供新的触发器,则使用现有触发器
|
||||
if trigger is None:
|
||||
# 获取当前任务的触发器配置
|
||||
current_trigger = query_job.trigger
|
||||
# 重新调度任务,使用当前的触发器
|
||||
return scheduler.reschedule_job(job_id=str(job_id), trigger=current_trigger)
|
||||
else:
|
||||
# 使用新提供的触发器
|
||||
return scheduler.reschedule_job(job_id=str(job_id), trigger=trigger, **trigger_args)
|
||||
|
||||
@classmethod
|
||||
def get_single_job_status(cls, job_id: str | int) -> str:
|
||||
"""
|
||||
获取单个任务的当前状态。
|
||||
|
||||
参数:
|
||||
- job_id (str | int): 任务ID
|
||||
|
||||
返回:
|
||||
- str: 任务状态('running' | 'paused' | 'stopped' | 'unknown')
|
||||
"""
|
||||
job = cls.get_job(job_id=str(job_id))
|
||||
if not job:
|
||||
return 'unknown'
|
||||
|
||||
# 检查任务是否在暂停列表中
|
||||
if job_id in scheduler._jobstores[job._jobstore_alias]._paused_jobs:
|
||||
return 'paused'
|
||||
|
||||
# 检查调度器状态
|
||||
if scheduler.state == 0: # STATE_STOPPED
|
||||
return 'stopped'
|
||||
|
||||
return 'running'
|
||||
|
||||
@classmethod
|
||||
def print_jobs(cls,jobstore: Any | None = None, out: Any | None = None):
|
||||
"""
|
||||
打印调度任务列表。
|
||||
|
||||
参数:
|
||||
- jobstore (Any | None): 任务存储别名。
|
||||
- out (Any | None): 输出目标。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
scheduler.print_jobs(jobstore=jobstore, out=out)
|
||||
|
||||
@classmethod
|
||||
def get_job_status(cls) -> str:
|
||||
"""
|
||||
获取调度器当前状态。
|
||||
|
||||
返回:
|
||||
- str: 状态字符串('stopped' | 'running' | 'paused' | 'unknown')。
|
||||
"""
|
||||
#: constant indicating a scheduler's stopped state
|
||||
STATE_STOPPED = 0
|
||||
#: constant indicating a scheduler's running state (started and processing jobs)
|
||||
STATE_RUNNING = 1
|
||||
#: constant indicating a scheduler's paused state (started but not processing jobs)
|
||||
STATE_PAUSED = 2
|
||||
if scheduler.state == STATE_STOPPED:
|
||||
return 'stopped'
|
||||
elif scheduler.state == STATE_RUNNING:
|
||||
return 'running'
|
||||
elif scheduler.state == STATE_PAUSED:
|
||||
return 'paused'
|
||||
else:
|
||||
return 'unknown'
|
||||
Reference in New Issue
Block a user