init: 初始化 dpb 桃育种系统代码库

前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
34047007@qq.com
2026-08-06 00:17:49 +08:00
commit b95053c52c
1469 changed files with 322298 additions and 0 deletions
@@ -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
]