init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""定时任务子包:调度器(job)与 APScheduler 节点(node)。"""
|
||||
@@ -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
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Security, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema, PaginationQueryParam
|
||||
from app.core.dependencies import AuthPermission, db_getter
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .schema import NodeCreateSchema, NodeExecuteSchema, NodeOutSchema, NodeQueryParam, NodeUpdateSchema
|
||||
from .service import NodeService
|
||||
|
||||
NodeRouter = APIRouter(route_class=OperationLogRoute, prefix="/cronjob/node", tags=["定时任务节点管理"])
|
||||
|
||||
|
||||
@NodeRouter.get("/options", summary="获取定时任务节点列表", response_model=ResponseSchema[list[dict]])
|
||||
async def get_node_options_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = NodeService(auth, db)
|
||||
result = await service.options()
|
||||
return SuccessResponse(data=result, msg="获取定时任务节点选项成功")
|
||||
|
||||
|
||||
@NodeRouter.get("/detail/{id}", summary="获取节点详情", response_model=ResponseSchema[NodeOutSchema])
|
||||
async def get_obj_detail_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:detail"]))],
|
||||
id: Annotated[int, Path(description="节点ID")],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = NodeService(auth, db)
|
||||
result_dict = await service.detail(id=id)
|
||||
return SuccessResponse(data=result_dict, msg="获取节点详情成功")
|
||||
|
||||
|
||||
@NodeRouter.get("/list", summary="查询节点", response_model=ResponseSchema[PageResultSchema[NodeOutSchema]])
|
||||
async def get_obj_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:query"]))],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[NodeQueryParam, Query()],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = NodeService(auth, db)
|
||||
result_dict = await service.page(
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
search=search,
|
||||
order_by=page.order_by,
|
||||
)
|
||||
return SuccessResponse(data=result_dict, msg="查询节点列表成功")
|
||||
|
||||
|
||||
@NodeRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建节点", response_model=ResponseSchema[NodeOutSchema])
|
||||
async def create_obj_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:create"]))],
|
||||
data: Annotated[NodeCreateSchema, Body(description="创建节点参数")],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = NodeService(auth, db)
|
||||
result_dict = await service.create(data=data)
|
||||
return SuccessResponse(data=result_dict, msg="创建节点成功")
|
||||
|
||||
|
||||
@NodeRouter.put("/update/{id}", summary="修改节点", response_model=ResponseSchema[NodeOutSchema])
|
||||
async def update_obj_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:update"]))],
|
||||
id: Annotated[int, Path(description="节点ID")],
|
||||
data: Annotated[NodeUpdateSchema, Body(description="修改节点参数")],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = NodeService(auth, db)
|
||||
result_dict = await service.update(id=id, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="修改节点成功")
|
||||
|
||||
|
||||
@NodeRouter.delete("/delete", summary="删除节点", response_model=ResponseSchema[None])
|
||||
async def delete_obj_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:delete"]))],
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = NodeService(auth, db)
|
||||
await service.delete(ids=ids)
|
||||
return SuccessResponse(msg="删除节点成功")
|
||||
|
||||
|
||||
@NodeRouter.delete("/clear", summary="清空节点", response_model=ResponseSchema[None])
|
||||
async def clear_obj_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:delete"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = NodeService(auth, db)
|
||||
await service.clear()
|
||||
return SuccessResponse(msg="清空节点成功")
|
||||
|
||||
|
||||
@NodeRouter.post("/execute/{id}", summary="调试节点", response_model=ResponseSchema[dict])
|
||||
async def execute_job_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:execute"]))],
|
||||
id: Annotated[int, Path(description="节点ID")],
|
||||
data: Annotated[NodeExecuteSchema, Body(description="调试节点参数")],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = NodeService(auth, db)
|
||||
result = await service.execute(id=id, execute_data=data)
|
||||
return SuccessResponse(data=result, msg="调试节点成功")
|
||||
|
||||
|
||||
@NodeRouter.patch("/status/batch", summary="批量设置节点状态", response_model=ResponseSchema[None])
|
||||
async def batch_set_status_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:cronjob:node:update"]))],
|
||||
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = NodeService(auth, db)
|
||||
await service.batch_set_status(ids=data.ids, status=data.status)
|
||||
return SuccessResponse(msg="批量设置节点状态成功")
|
||||
@@ -0,0 +1,110 @@
|
||||
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 NodeModel
|
||||
from .schema import NodeCreateSchema, NodeUpdateSchema
|
||||
|
||||
|
||||
class NodeCRUD(CRUDBase[NodeModel, NodeCreateSchema, NodeUpdateSchema]):
|
||||
"""节点数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
"""初始化节点CRUD
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息模型
|
||||
- db (AsyncSession): 数据库会话
|
||||
"""
|
||||
super().__init__(model=NodeModel, auth=auth, db=db)
|
||||
|
||||
async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> NodeModel | None:
|
||||
"""获取节点详情
|
||||
|
||||
参数:
|
||||
- id (int): 节点ID
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- NodeModel | 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[NodeModel]:
|
||||
"""获取节点列表
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询参数字典
|
||||
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||
- preload (list[str | Any] | None): 预加载关系,未提供时使用模型默认项
|
||||
|
||||
返回:
|
||||
- Sequence[NodeModel]: 节点模型序列
|
||||
"""
|
||||
return await self.get_list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_obj_crud(self, data: NodeCreateSchema) -> NodeModel | None:
|
||||
"""创建节点
|
||||
|
||||
参数:
|
||||
- data (NodeCreateSchema): 创建节点模型
|
||||
|
||||
返回:
|
||||
- NodeModel | None: 创建的节点模型,如果创建失败则为None
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_obj_crud(self, id: int, data: NodeUpdateSchema) -> NodeModel | None:
|
||||
"""更新节点
|
||||
|
||||
参数:
|
||||
- id (int): 节点ID
|
||||
- data (NodeUpdateSchema): 更新节点模型
|
||||
|
||||
返回:
|
||||
- NodeModel | 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 set_obj_field_crud(self, ids: list[int], **kwargs) -> None:
|
||||
"""设置节点的可用状态
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 节点ID列表
|
||||
- kwargs: 其他要设置的字段,例如 available=True 或 available=False
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.set(ids=ids, **kwargs)
|
||||
|
||||
async def clear_obj_crud(self) -> None:
|
||||
"""清除节点日志
|
||||
|
||||
注意:
|
||||
- 此操作会删除所有节点日志,请谨慎操作
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
return await self.clear()
|
||||
@@ -0,0 +1,49 @@
|
||||
"""示例处理器模块
|
||||
|
||||
提供简单的示例方法供节点执行函数调用
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def demo_handler(*args, **kwargs) -> dict:
|
||||
"""示例处理器(演示节点调用形态)。
|
||||
|
||||
返回:
|
||||
- dict: 包含 message、入参快照与时间戳。
|
||||
"""
|
||||
return {
|
||||
"message": "Hello from demo_handler!",
|
||||
"args": args,
|
||||
"kwargs": kwargs,
|
||||
"time": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
def process_data(data: list, operation: str = "sum") -> dict:
|
||||
"""简单数值列表聚合。
|
||||
|
||||
参数:
|
||||
- data (list): 数值列表。
|
||||
- operation (str): sum、avg、max、min、count 之一。
|
||||
|
||||
返回:
|
||||
- dict: 含 operation 与 result,或 error 说明。
|
||||
"""
|
||||
if not data:
|
||||
return {"error": "数据为空"}
|
||||
|
||||
if operation == "sum":
|
||||
result = sum(data)
|
||||
elif operation == "avg":
|
||||
result = sum(data) / len(data)
|
||||
elif operation == "max":
|
||||
result = max(data)
|
||||
elif operation == "min":
|
||||
result = min(data)
|
||||
elif operation == "count":
|
||||
result = len(data)
|
||||
else:
|
||||
return {"error": f"不支持的操作: {operation}"}
|
||||
|
||||
return {"operation": operation, "result": result}
|
||||
@@ -0,0 +1,28 @@
|
||||
from sqlalchemy import Boolean, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class NodeModel(ModelMixin, UserMixin):
|
||||
"""节点类型模型 - 动态定义节点类型
|
||||
"""
|
||||
|
||||
__tablename__: str = "task_node"
|
||||
__table_args__: dict[str, str] = {"comment": "节点类型表"}
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="节点名称")
|
||||
code: Mapped[str] = mapped_column(String(32), unique=True, nullable=False, 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 | None] = mapped_column(String(64), nullable=True, comment="触发器")
|
||||
trigger_args: Mapped[str | None] = mapped_column(Text, nullable=True, comment="触发器参数")
|
||||
func: Mapped[str | None] = mapped_column(Text, nullable=True, 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="结束时间")
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)")
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
||||
@@ -0,0 +1,107 @@
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
|
||||
from app.core.validator import datetime_validator
|
||||
|
||||
|
||||
class NodeCreateSchema(BaseModel):
|
||||
"""节点创建/编辑时只设置节点基本信息,节点参数在执行时设置
|
||||
"""
|
||||
|
||||
name: str = Field(..., max_length=64, description="任务名称")
|
||||
func: str | None = Field(default=None, description="代码块")
|
||||
args: str | None = Field(default=None, description="位置参数")
|
||||
kwargs: str | None = Field(default=None, description="关键字参数")
|
||||
coalesce: bool | None = Field(default=False, description="是否合并运行:是否在多个运行时间到期时仅运行作业一次")
|
||||
max_instances: int | None = Field(default=1, ge=1, description="最大实例数:允许的最大并发执行实例数")
|
||||
jobstore: str | None = Field(default="default", max_length=64, description="任务存储")
|
||||
executor: str | None = Field(default="default", max_length=64, description="任务执行器:将运行此作业的执行程序的名称")
|
||||
start_date: str | None = Field(default=None, description="开始时间")
|
||||
end_date: str | None = Field(default=None, description="结束时间")
|
||||
code: str | None = Field(default=None, max_length=32, description="节点编码")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
v = v.strip()
|
||||
if len(v) < 1 or len(v) > 64:
|
||||
raise ValueError("任务名称长度必须在1-64个字符之间")
|
||||
return v
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
def validate_code(cls, v: str | None) -> str | None:
|
||||
if v is None:
|
||||
return v
|
||||
v = v.strip()
|
||||
if len(v) < 2 or len(v) > 32:
|
||||
raise ValueError("节点编码长度必须在2-32个字符之间")
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_]*$", v):
|
||||
raise ValueError("节点编码必须以字母开头,仅允许字母、数字、下划线")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_func(self):
|
||||
if not self.func or not self.func.strip():
|
||||
raise ValueError("必须提供代码块(func)")
|
||||
return self
|
||||
|
||||
|
||||
class NodeUpdateSchema(NodeCreateSchema):
|
||||
"""节点更新模型"""
|
||||
|
||||
|
||||
class NodeOutSchema(NodeCreateSchema, BaseSchema, UserBySchema):
|
||||
"""节点响应模型"""
|
||||
|
||||
trigger: str | None = Field(default=None, description="触发器")
|
||||
trigger_args: str | None = Field(default=None, description="触发器参数")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class NodeQueryParam(BaseQueryParam, UserByQueryParam):
|
||||
"""节点查询参数"""
|
||||
|
||||
name: str | None = Field(None, description="节点名称", json_schema_extra={"q": "like"})
|
||||
status: int | None = Field(None, ge=0, le=1, description="状态(0:启动 1:停用)", json_schema_extra={"q": "eq"})
|
||||
|
||||
|
||||
class NodeExecuteSchema(BaseModel):
|
||||
"""节点执行参数"""
|
||||
|
||||
trigger: str = Field(default="now", description="触发方式: now/cron/interval/date")
|
||||
trigger_args: str | None = Field(default=None, description="触发器参数")
|
||||
start_date: str | None = Field(default=None, description="开始时间")
|
||||
end_date: str | None = Field(default=None, description="结束时间")
|
||||
|
||||
@field_validator("trigger")
|
||||
@classmethod
|
||||
def _validate_trigger(cls, v: str) -> str:
|
||||
allowed = {"now", "cron", "interval", "date"}
|
||||
v = v.strip()
|
||||
if v not in allowed:
|
||||
raise ValueError("触发器必须为 now/cron/interval/date")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_trigger_args(self):
|
||||
"""非立即执行时必须提供触发器参数"""
|
||||
if self.trigger != "now" and not self.trigger_args:
|
||||
raise ValueError("非立即执行时必须提供触发器参数")
|
||||
return self
|
||||
|
||||
@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
|
||||
@@ -0,0 +1,318 @@
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from apscheduler.job import Job
|
||||
from apscheduler.jobstores.base import ConflictingIdError, JobLookupError
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from apscheduler.triggers.date import DateTrigger
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from croniter import croniter
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.ap_scheduler import (
|
||||
SchedulerUtil,
|
||||
scheduler,
|
||||
)
|
||||
from app.core.base_schema import AuthSchema, PageResultSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import logger
|
||||
from app.utils.common_util import search_to_dict
|
||||
|
||||
from .crud import NodeCRUD
|
||||
from .model import NodeModel
|
||||
from .schema import (
|
||||
NodeCreateSchema,
|
||||
NodeExecuteSchema,
|
||||
NodeOutSchema,
|
||||
NodeQueryParam,
|
||||
NodeUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class NodeService:
|
||||
"""节点管理模块服务层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
self.auth = auth
|
||||
self.db = db
|
||||
|
||||
async def options(self) -> list[dict]:
|
||||
obj_list = await NodeCRUD(self.auth, self.db).get_obj_list_crud()
|
||||
return [
|
||||
{
|
||||
"id": obj.id,
|
||||
"name": obj.name,
|
||||
"code": obj.code,
|
||||
"func": obj.func,
|
||||
"args": obj.args,
|
||||
"kwargs": obj.kwargs,
|
||||
}
|
||||
for obj in obj_list
|
||||
]
|
||||
|
||||
async def detail(self, id: int) -> NodeOutSchema:
|
||||
obj = await NodeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id)
|
||||
return NodeOutSchema.model_validate(obj)
|
||||
|
||||
async def get_list(
|
||||
self,
|
||||
search: NodeQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> list[NodeOutSchema]:
|
||||
obj_list = await NodeCRUD(self.auth, self.db).get_obj_list_crud(search=search_to_dict(search, {}), order_by=order_by)
|
||||
return [NodeOutSchema.model_validate(obj) for obj in obj_list]
|
||||
|
||||
async def page(
|
||||
self,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: NodeQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> PageResultSchema[NodeOutSchema]:
|
||||
offset = (page_no - 1) * page_size
|
||||
return await NodeCRUD(self.auth, self.db).page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "asc"}],
|
||||
search=search_to_dict(search, {}),
|
||||
out_schema=NodeOutSchema,
|
||||
)
|
||||
|
||||
async def create(self, data: NodeCreateSchema) -> NodeOutSchema:
|
||||
exist_obj = await NodeCRUD(self.auth, self.db).get(name=data.name)
|
||||
if exist_obj:
|
||||
raise CustomException(msg="创建失败,该节点已存在")
|
||||
|
||||
obj = await NodeCRUD(self.auth, self.db).create_obj_crud(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建失败")
|
||||
return NodeOutSchema.model_validate(obj)
|
||||
|
||||
async def update(self, id: int, data: NodeUpdateSchema) -> NodeOutSchema:
|
||||
exist_obj = await NodeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg="更新失败,该节点不存在")
|
||||
|
||||
obj = await NodeCRUD(self.auth, self.db).update_obj_crud(id=id, data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="更新失败")
|
||||
return NodeOutSchema.model_validate(obj)
|
||||
|
||||
async def delete(self, ids: list[int]) -> None:
|
||||
if not ids:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
for mid in ids:
|
||||
exist_obj = await NodeCRUD(self.auth, self.db).get_obj_by_id_crud(id=mid)
|
||||
if not exist_obj:
|
||||
raise CustomException(msg="删除失败,该节点不存在")
|
||||
try:
|
||||
SchedulerUtil.remove_job(job_id=mid)
|
||||
except JobLookupError:
|
||||
pass
|
||||
await NodeCRUD(self.auth, self.db).delete_obj_crud(ids=ids)
|
||||
|
||||
async def clear(self) -> None:
|
||||
SchedulerUtil.clear_jobs()
|
||||
await NodeCRUD(self.auth, self.db).clear_obj_crud()
|
||||
|
||||
async def execute(self, id: int, execute_data: NodeExecuteSchema) -> dict:
|
||||
obj = await NodeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="调试失败,该节点不存在")
|
||||
|
||||
trigger = execute_data.trigger
|
||||
trigger_args = execute_data.trigger_args
|
||||
start_date = execute_data.start_date
|
||||
end_date = execute_data.end_date
|
||||
|
||||
if trigger == "now":
|
||||
add_and_run_job_now(job_info=obj)
|
||||
elif trigger == "cron":
|
||||
if not trigger_args:
|
||||
raise CustomException(msg="Cron执行需要提供Cron表达式")
|
||||
try:
|
||||
croniter(trigger_args)
|
||||
except (KeyError, ValueError):
|
||||
raise CustomException(msg=f"Cron表达式不正确: {trigger_args}")
|
||||
add_cron_job(
|
||||
job_info=obj,
|
||||
trigger_args=trigger_args,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
elif trigger == "interval":
|
||||
if not trigger_args:
|
||||
raise CustomException(msg="间隔执行需要提供间隔参数")
|
||||
add_interval_job(
|
||||
job_info=obj,
|
||||
trigger_args=trigger_args,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
)
|
||||
elif trigger == "date":
|
||||
if not trigger_args:
|
||||
raise CustomException(msg="指定时间执行需要提供执行时间")
|
||||
add_date_job(job_info=obj, run_date=trigger_args)
|
||||
else:
|
||||
raise CustomException(msg=f"不支持的触发方式: {trigger}")
|
||||
|
||||
return {"job_id": id, "status": "executed", "trigger": trigger}
|
||||
|
||||
async def batch_set_status(self, ids: list[int], status: int) -> None:
|
||||
if not ids:
|
||||
raise CustomException(msg="请选择要操作的数据")
|
||||
|
||||
await NodeCRUD(self.auth, self.db).set(
|
||||
ids=ids,
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
# ── NodeModel 封装的任务添加方法 ────────────────────────────
|
||||
|
||||
|
||||
def _add_job_with_trigger(job_info: NodeModel, trigger) -> Job:
|
||||
"""将 NodeModel 封装的任务添加到 APScheduler 调度器。"""
|
||||
code_block = job_info.func
|
||||
if not code_block or not code_block.strip():
|
||||
raise ValueError("任务代码块不能为空")
|
||||
|
||||
jobstore = job_info.jobstore or "sqlalchemy"
|
||||
executor = job_info.executor or "threadpool"
|
||||
|
||||
job_args = []
|
||||
if job_info.args:
|
||||
args_str = str(job_info.args).strip()
|
||||
if args_str:
|
||||
job_args = [arg.strip() for arg in args_str.split(",") if arg.strip()]
|
||||
|
||||
job_kwargs = {}
|
||||
if job_info.kwargs:
|
||||
kwargs_str = str(job_info.kwargs).strip()
|
||||
if kwargs_str:
|
||||
try:
|
||||
job_kwargs = json.loads(kwargs_str)
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"关键字参数JSON格式无效: {kwargs_str}")
|
||||
|
||||
SchedulerUtil.job_name_cache[str(job_info.id)] = job_info.name or ""
|
||||
|
||||
try:
|
||||
job = scheduler.add_job(
|
||||
func=SchedulerUtil._task_wrapper,
|
||||
trigger=trigger,
|
||||
args=[str(job_info.id), code_block, *job_args],
|
||||
kwargs=job_kwargs,
|
||||
id=str(job_info.id),
|
||||
name=job_info.name,
|
||||
coalesce=job_info.coalesce,
|
||||
max_instances=1,
|
||||
jobstore=jobstore,
|
||||
executor=executor,
|
||||
)
|
||||
logger.info(f"任务 {job_info.id} 添加到 {jobstore} 存储器成功")
|
||||
return job
|
||||
except ConflictingIdError:
|
||||
scheduler.remove_job(job_id=str(job_info.id), jobstore=jobstore)
|
||||
job = scheduler.add_job(
|
||||
func=SchedulerUtil._task_wrapper,
|
||||
trigger=trigger,
|
||||
args=[str(job_info.id), code_block, *job_args],
|
||||
kwargs=job_kwargs,
|
||||
id=str(job_info.id),
|
||||
name=job_info.name,
|
||||
coalesce=job_info.coalesce,
|
||||
max_instances=1,
|
||||
jobstore=jobstore,
|
||||
executor=executor,
|
||||
)
|
||||
logger.info(f"任务 {job_info.id} 已存在,已移除旧任务并重新添加")
|
||||
return job
|
||||
|
||||
|
||||
def add_and_run_job_now(job_info: NodeModel) -> Job:
|
||||
"""立即执行任务(加入调度器并尽快触发一次)。"""
|
||||
trigger = DateTrigger(run_date=datetime.now() + timedelta(seconds=0.1))
|
||||
return _add_job_with_trigger(job_info, trigger)
|
||||
|
||||
|
||||
def add_cron_job(
|
||||
job_info: NodeModel,
|
||||
trigger_args: str | None = None,
|
||||
start_date: str | None = None,
|
||||
end_date: str | None = None,
|
||||
) -> Job:
|
||||
"""创建 Cron 定时任务。"""
|
||||
cron_expr = trigger_args or job_info.trigger_args
|
||||
if not cron_expr:
|
||||
raise ValueError("Cron触发器缺少参数")
|
||||
|
||||
fields = cron_expr.strip().split()
|
||||
if len(fields) not in (6, 7):
|
||||
raise ValueError("无效的 Cron 表达式")
|
||||
try:
|
||||
croniter(cron_expr)
|
||||
except (KeyError, ValueError):
|
||||
raise ValueError(f"Cron表达式不正确: {cron_expr}")
|
||||
|
||||
parsed_fields = [field if field != "?" else "*" for field in fields]
|
||||
if len(fields) == 6:
|
||||
parsed_fields.append("*")
|
||||
|
||||
second, minute, hour, day, month, day_of_week, year = tuple(parsed_fields)
|
||||
|
||||
if second == "*" and minute == "*" and hour == "*" and day == "*" and month == "*" and day_of_week in ("*", "?"):
|
||||
raise ValueError("Cron表达式不允许每秒执行,请至少指定秒数(如:0 * * * * ? * 表示每分钟执行)")
|
||||
|
||||
trigger = CronTrigger(
|
||||
second=second,
|
||||
minute=minute,
|
||||
hour=hour,
|
||||
day=day,
|
||||
month=month,
|
||||
day_of_week=day_of_week,
|
||||
year=year,
|
||||
start_date=start_date or job_info.start_date,
|
||||
end_date=end_date or job_info.end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
)
|
||||
return _add_job_with_trigger(job_info, trigger)
|
||||
|
||||
|
||||
def add_interval_job(
|
||||
job_info: NodeModel,
|
||||
trigger_args: str | None = None,
|
||||
start_date: str | None = None,
|
||||
end_date: str | None = None,
|
||||
) -> Job:
|
||||
"""创建间隔执行任务。"""
|
||||
interval_args = trigger_args or job_info.trigger_args
|
||||
if not interval_args:
|
||||
raise ValueError("interval触发器缺少参数")
|
||||
|
||||
fields = interval_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)
|
||||
trigger = IntervalTrigger(
|
||||
weeks=week,
|
||||
days=day,
|
||||
hours=hour,
|
||||
minutes=minute,
|
||||
seconds=second,
|
||||
start_date=start_date or job_info.start_date,
|
||||
end_date=end_date or job_info.end_date,
|
||||
timezone="Asia/Shanghai",
|
||||
)
|
||||
return _add_job_with_trigger(job_info, trigger)
|
||||
|
||||
|
||||
def add_date_job(job_info: NodeModel, run_date: str | None = None) -> Job:
|
||||
"""创建指定时刻执行一次的任务。"""
|
||||
date_str = run_date or job_info.trigger_args
|
||||
if not date_str:
|
||||
raise ValueError("date触发器缺少执行时间参数")
|
||||
|
||||
trigger = DateTrigger(run_date=date_str, timezone="Asia/Shanghai")
|
||||
return _add_job_with_trigger(job_info, trigger)
|
||||
Reference in New Issue
Block a user