init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core.ap_scheduler import SchedulerUtil
|
||||
from app.core.base_schema import AuthSchema, PageResultSchema, PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .schema import JobOutSchema, JobQueryParam
|
||||
from .service import JobService
|
||||
|
||||
JobRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/job", tags=["定时任务管理"])
|
||||
|
||||
|
||||
@JobRouter.get("/scheduler/status", summary="获取调度器状态", response_model=ResponseSchema[dict], dependencies=[Security(AuthPermission(["module_task:cronjob:job:query"]))])
|
||||
async def get_scheduler_status_controller() -> JSONResponse:
|
||||
data = JobService.get_scheduler_status()
|
||||
return SuccessResponse(data=data, msg="获取调度器状态成功")
|
||||
|
||||
|
||||
@JobRouter.get("/scheduler/jobs", summary="获取调度器任务列表", response_model=ResponseSchema[list[dict]], dependencies=[Security(AuthPermission(["module_task:cronjob:job:query"]))])
|
||||
async def get_scheduler_jobs_controller() -> JSONResponse:
|
||||
data = JobService.get_scheduler_jobs()
|
||||
return SuccessResponse(data=data, msg="获取调度器任务列表成功")
|
||||
|
||||
|
||||
@JobRouter.post("/scheduler/start", summary="启动调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))])
|
||||
async def start_scheduler_controller() -> JSONResponse:
|
||||
SchedulerUtil.start()
|
||||
return SuccessResponse(msg="调度器已启动")
|
||||
|
||||
|
||||
@JobRouter.post("/scheduler/pause", summary="暂停调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))])
|
||||
async def pause_scheduler_controller() -> JSONResponse:
|
||||
SchedulerUtil.pause()
|
||||
return SuccessResponse(msg="调度器已暂停")
|
||||
|
||||
|
||||
@JobRouter.post("/scheduler/resume", summary="恢复调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))])
|
||||
async def resume_scheduler_controller() -> JSONResponse:
|
||||
SchedulerUtil.resume()
|
||||
return SuccessResponse(msg="调度器已恢复")
|
||||
|
||||
|
||||
@JobRouter.post("/scheduler/shutdown", summary="关闭调度器", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:scheduler"]))])
|
||||
async def shutdown_scheduler_controller() -> JSONResponse:
|
||||
SchedulerUtil.shutdown()
|
||||
return SuccessResponse(msg="调度器已关闭")
|
||||
|
||||
|
||||
@JobRouter.delete("/scheduler/jobs/clear", summary="清空所有任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
|
||||
async def clear_jobs_controller() -> JSONResponse:
|
||||
SchedulerUtil.clear_jobs()
|
||||
return SuccessResponse(msg="已清空所有任务")
|
||||
|
||||
|
||||
@JobRouter.get("/scheduler/console", summary="获取调度器控制台信息", response_model=ResponseSchema[str], dependencies=[Security(AuthPermission(["module_task:cronjob:job:query"]))])
|
||||
async def get_scheduler_console_controller() -> JSONResponse:
|
||||
console_output = SchedulerUtil.print_jobs()
|
||||
return SuccessResponse(data=console_output, msg="获取控制台信息成功")
|
||||
|
||||
|
||||
@JobRouter.post("/task/pause/{job_id}", summary="暂停任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
|
||||
async def pause_job_controller(
|
||||
job_id: Annotated[str, Path(description="调度器任务ID")],
|
||||
) -> JSONResponse:
|
||||
SchedulerUtil.pause_job(job_id=job_id)
|
||||
return SuccessResponse(msg="暂停任务成功")
|
||||
|
||||
|
||||
@JobRouter.post("/task/resume/{job_id}", summary="恢复任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
|
||||
async def resume_job_controller(
|
||||
job_id: Annotated[str, Path(description="调度器任务ID")],
|
||||
) -> JSONResponse:
|
||||
SchedulerUtil.resume_job(job_id=job_id)
|
||||
return SuccessResponse(msg="恢复任务成功")
|
||||
|
||||
|
||||
@JobRouter.post("/task/run/{job_id}", summary="立即执行任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
|
||||
async def run_job_controller(
|
||||
job_id: Annotated[str, Path(description="调度器任务ID")],
|
||||
) -> JSONResponse:
|
||||
SchedulerUtil.run_job_now(job_id=job_id)
|
||||
return SuccessResponse(msg="立即执行任务成功")
|
||||
|
||||
|
||||
@JobRouter.put("/task/modify/{job_id}", summary="修改任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:task"]))])
|
||||
async def modify_job_controller(
|
||||
job_id: Annotated[str, Path(description="调度器任务ID")],
|
||||
changes: Annotated[dict, Body(description="要修改的任务属性,如 name、coalesce、max_instances 等")],
|
||||
) -> JSONResponse:
|
||||
SchedulerUtil.modify_job(job_id=job_id, **changes)
|
||||
return SuccessResponse(msg="修改任务成功")
|
||||
|
||||
|
||||
@JobRouter.delete("/task/remove/{job_id}", summary="移除任务", response_model=ResponseSchema[None], dependencies=[Security(AuthPermission(["module_task:cronjob:job:delete"]))])
|
||||
async def remove_job_controller(
|
||||
job_id: Annotated[str, Path(description="调度器任务ID")],
|
||||
) -> JSONResponse:
|
||||
SchedulerUtil.remove_job(job_id=job_id)
|
||||
return SuccessResponse(msg="移除任务成功")
|
||||
|
||||
|
||||
@JobRouter.get("/log/list", summary="查询执行日志列表", response_model=ResponseSchema[PageResultSchema[JobOutSchema]])
|
||||
async def get_job_log_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:query"]))],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[JobQueryParam, Query()],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
result_dict = await JobService(auth, db).get_job_log_page(
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
search=search,
|
||||
order_by=page.order_by,
|
||||
)
|
||||
return SuccessResponse(data=result_dict, msg="查询执行日志列表成功")
|
||||
|
||||
|
||||
@JobRouter.get("/log/detail/{id}", summary="获取执行日志详情", response_model=ResponseSchema[JobOutSchema])
|
||||
async def get_job_log_detail_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:detail"]))],
|
||||
id: Annotated[int, Path(description="日志ID")],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
result_dict = await JobService(auth, db).get_job_log_detail(id=id)
|
||||
return SuccessResponse(data=result_dict, msg="获取执行日志详情成功")
|
||||
|
||||
|
||||
@JobRouter.delete("/log/delete", summary="删除执行日志", response_model=ResponseSchema[None])
|
||||
async def delete_job_log_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:job:delete"]))],
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
await JobService(auth, db).delete_job_log(ids=ids)
|
||||
return SuccessResponse(msg="删除执行日志成功")
|
||||
@@ -0,0 +1,95 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.core.base_schema import AuthSchema
|
||||
|
||||
from .model import JobModel
|
||||
from .schema import JobCreateSchema, JobUpdateSchema
|
||||
|
||||
|
||||
class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
||||
"""任务执行日志数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
"""初始化任务执行日志CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- db (AsyncSession): 数据库会话
|
||||
"""
|
||||
super().__init__(model=JobModel, auth=auth, db=db)
|
||||
|
||||
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.get_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列表
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.delete(ids=ids)
|
||||
|
||||
async def clear_obj_crud(self) -> None:
|
||||
"""清空所有执行日志。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.clear()
|
||||
@@ -0,0 +1,22 @@
|
||||
from sqlalchemy import Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin
|
||||
|
||||
|
||||
class JobModel(ModelMixin):
|
||||
"""任务执行日志表
|
||||
"""
|
||||
|
||||
__tablename__: str = "task_job"
|
||||
__table_args__: dict[str, str] = {"comment": "任务执行日志表"}
|
||||
|
||||
job_id: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="任务ID")
|
||||
job_name: Mapped[str | None] = mapped_column(String(128), nullable=True, comment="任务名称")
|
||||
trigger_type: Mapped[str | None] = mapped_column(String(32), nullable=True, comment="触发方式: cron/interval/date/manual")
|
||||
next_run_time: Mapped[str | None] = mapped_column(String(64), nullable=True, comment="下次执行时间")
|
||||
job_state: Mapped[str | None] = mapped_column(Text, nullable=True, comment="任务状态信息")
|
||||
result: Mapped[str | None] = mapped_column(Text, nullable=True, comment="执行结果")
|
||||
error: Mapped[str | None] = mapped_column(Text, nullable=True, comment="错误信息")
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="执行状态(0:待执行 1:执行中 2:成功 3:失败 4:超时 5:已取消)")
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
||||
@@ -0,0 +1,65 @@
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
field_validator,
|
||||
)
|
||||
|
||||
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
|
||||
|
||||
|
||||
class JobCreateSchema(BaseModel):
|
||||
"""执行日志创建模型"""
|
||||
|
||||
job_id: str = Field(..., max_length=64, description="任务ID")
|
||||
job_name: str | None = Field(default=None, max_length=128, description="任务名称")
|
||||
trigger_type: str | None = Field(default=None, max_length=32, description="触发方式")
|
||||
status: int = Field(default=0, ge=0, le=5, description="执行状态(0:待执行 1:执行中 2:成功 3:失败 4:超时 5:已取消)")
|
||||
next_run_time: str | None = Field(default=None, description="下次执行时间")
|
||||
job_state: str | None = Field(default=None, description="任务状态信息")
|
||||
result: str | None = Field(default=None, description="执行结果")
|
||||
error: str | None = Field(default=None, description="错误信息")
|
||||
|
||||
@field_validator("job_id")
|
||||
@classmethod
|
||||
def validate_job_id(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if len(v) < 1 or len(v) > 64:
|
||||
raise ValueError("任务ID长度必须在1-64个字符之间")
|
||||
return v
|
||||
|
||||
@field_validator("trigger_type")
|
||||
@classmethod
|
||||
def validate_trigger_type(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
allowed = {"cron", "interval", "date", "manual"}
|
||||
v = v.strip()
|
||||
if v not in allowed:
|
||||
raise ValueError(f"触发方式必须为 {allowed}")
|
||||
return v
|
||||
|
||||
|
||||
class JobUpdateSchema(BaseModel):
|
||||
"""执行日志更新模型"""
|
||||
|
||||
status: int | None = Field(default=None, ge=0, le=5, description="执行状态(0:待执行 1:执行中 2:成功 3:失败 4:超时 5:已取消)")
|
||||
next_run_time: str | None = Field(default=None, description="下次执行时间")
|
||||
job_state: str | None = Field(default=None, description="任务状态信息")
|
||||
result: str | None = Field(default=None, description="执行结果")
|
||||
error: str | None = Field(default=None, description="错误信息")
|
||||
|
||||
|
||||
class JobOutSchema(JobCreateSchema, BaseSchema, UserBySchema):
|
||||
"""执行日志响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class JobQueryParam(BaseQueryParam, UserByQueryParam):
|
||||
"""执行日志查询参数"""
|
||||
|
||||
job_id: str | None = Field(None, description="任务ID", json_schema_extra={"q": "eq"})
|
||||
job_name: str | None = Field(None, description="任务名称", json_schema_extra={"q": "like"})
|
||||
trigger_type: str | None = Field(None, description="触发方式", json_schema_extra={"q": "eq"})
|
||||
status: int | None = Field(None, ge=0, le=5, description="执行状态(0:待执行 1:执行中 2:成功 3:失败 4:超时 5:已取消)", json_schema_extra={"q": "eq"})
|
||||
@@ -0,0 +1,120 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.ap_scheduler import SchedulerUtil
|
||||
from app.core.base_schema import AuthSchema, PageResultSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.common_util import search_to_dict
|
||||
|
||||
from .crud import JobCRUD
|
||||
from .schema import JobCreateSchema, JobOutSchema, JobQueryParam, JobUpdateSchema
|
||||
|
||||
|
||||
class JobService:
|
||||
"""调度器监控模块服务层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
self.auth = auth
|
||||
self.db = db
|
||||
|
||||
async def get_job_log_detail(self, id: int) -> JobOutSchema:
|
||||
obj = await JobCRUD(self.auth, self.db).get_obj_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="执行日志不存在")
|
||||
return JobOutSchema.model_validate(obj)
|
||||
|
||||
async def get_job_log_list(
|
||||
self,
|
||||
search: JobQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> list[JobOutSchema]:
|
||||
if order_by is None:
|
||||
order_by = [{"created_time": "desc"}]
|
||||
obj_list = await JobCRUD(self.auth, self.db).get_obj_list_crud(search=search_to_dict(search, {}), order_by=order_by)
|
||||
return [JobOutSchema.model_validate(obj) for obj in obj_list]
|
||||
|
||||
async def get_job_log_page(
|
||||
self,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: JobQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> PageResultSchema[JobOutSchema]:
|
||||
offset = (page_no - 1) * page_size
|
||||
ob = order_by or [{"created_time": "desc"}]
|
||||
return await JobCRUD(self.auth, self.db).page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=ob,
|
||||
search=search_to_dict(search, {}),
|
||||
out_schema=JobOutSchema,
|
||||
)
|
||||
|
||||
async def create_job_log(
|
||||
self,
|
||||
job_id: str,
|
||||
job_name: str | None = None,
|
||||
trigger_type: str | None = None,
|
||||
) -> JobOutSchema:
|
||||
data = JobCreateSchema(
|
||||
job_id=job_id,
|
||||
job_name=job_name,
|
||||
trigger_type=trigger_type,
|
||||
status=1, # 执行中
|
||||
)
|
||||
obj = await JobCRUD(self.auth, self.db).create_obj_crud(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建执行日志失败")
|
||||
return JobOutSchema.model_validate(obj)
|
||||
|
||||
async def update_job_log(
|
||||
self,
|
||||
id: int,
|
||||
status: int,
|
||||
result: str | None = None,
|
||||
error: str | None = None,
|
||||
) -> JobOutSchema:
|
||||
data = JobUpdateSchema(
|
||||
status=status,
|
||||
result=result,
|
||||
error=error,
|
||||
)
|
||||
obj = await JobCRUD(self.auth, self.db).update_obj_crud(id=id, data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="更新执行日志失败")
|
||||
return JobOutSchema.model_validate(obj)
|
||||
|
||||
async def delete_job_log(self, ids: list[int]) -> None:
|
||||
if not ids:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
await JobCRUD(self.auth, self.db).delete_obj_crud(ids=ids)
|
||||
|
||||
async def clear_job_log(self) -> None:
|
||||
await JobCRUD(self.auth, self.db).clear_obj_crud()
|
||||
|
||||
# APScheduler 状态常量: 0=停止, 1=运行中, 2=暂停
|
||||
_SCHEDULER_STATE_MAP: dict[int, str] = {0: "停止", 1: "运行中", 2: "暂停"}
|
||||
|
||||
@staticmethod
|
||||
def get_scheduler_status() -> dict:
|
||||
state = SchedulerUtil.get_scheduler_state()
|
||||
is_running = SchedulerUtil.is_running()
|
||||
jobs = SchedulerUtil.get_jobs()
|
||||
return {
|
||||
"status": JobService._SCHEDULER_STATE_MAP.get(state, "未知"),
|
||||
"is_running": is_running,
|
||||
"job_count": len(jobs),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_scheduler_jobs() -> list[dict]:
|
||||
jobs = SchedulerUtil.get_jobs()
|
||||
return [
|
||||
{
|
||||
"id": job.id,
|
||||
"name": job.name,
|
||||
"trigger": str(job.trigger),
|
||||
"next_run_time": str(job.next_run_time) if job.next_run_time else None,
|
||||
"status": SchedulerUtil.get_job_status(job_id=job.id),
|
||||
}
|
||||
for job in jobs
|
||||
]
|
||||
Reference in New Issue
Block a user