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 @@
"""工作流定义:画布、发布、执行(HTTP 入口在 controller)。"""
@@ -0,0 +1,92 @@
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, PageResultSchema, PaginationQueryParam
from app.core.dependencies import AuthPermission, db_getter
from app.core.router_class import OperationLogRoute
from .schema import WorkflowCreateSchema, WorkflowExecuteResultSchema, WorkflowExecuteSchema, WorkflowOutSchema, WorkflowQueryParam, WorkflowUpdateSchema
from .service import WorkflowService
WorkflowRouter = APIRouter(route_class=OperationLogRoute, prefix="/workflow/flow", tags=["流程编排"])
@WorkflowRouter.get("/detail/{id}", summary="工作流详情", response_model=ResponseSchema[WorkflowOutSchema])
async def get_workflow_detail_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:detail"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="工作流ID")],
) -> JSONResponse:
result_dict = await WorkflowService(auth, db).get_workflow_detail(id=id)
return SuccessResponse(data=result_dict, msg="获取工作流详情成功")
@WorkflowRouter.get("/list", summary="工作流列表", response_model=ResponseSchema[PageResultSchema[WorkflowOutSchema]])
async def get_workflow_list_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
page: Annotated[PaginationQueryParam, Depends()],
search: Annotated[WorkflowQueryParam, Query()],
) -> JSONResponse:
result_dict = await WorkflowService(auth, db).get_workflow_page(
page_no=page.page_no,
page_size=page.page_size,
search=search,
order_by=page.order_by,
)
return SuccessResponse(data=result_dict, msg="查询工作流列表成功")
@WorkflowRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建工作流", response_model=ResponseSchema[WorkflowOutSchema])
async def create_workflow_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:create"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
data: Annotated[WorkflowCreateSchema, Body(description="创建工作流参数")],
) -> JSONResponse:
result_dict = await WorkflowService(auth, db).create_workflow(data=data)
return SuccessResponse(data=result_dict, msg="创建工作流成功")
@WorkflowRouter.put("/update/{id}", summary="更新工作流", response_model=ResponseSchema[WorkflowOutSchema])
async def update_workflow_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="工作流ID")],
data: Annotated[WorkflowUpdateSchema, Body(description="更新工作流参数")],
) -> JSONResponse:
result_dict = await WorkflowService(auth, db).update_workflow(id=id, data=data)
return SuccessResponse(data=result_dict, msg="更新工作流成功")
@WorkflowRouter.delete("/delete", summary="删除工作流", response_model=ResponseSchema[None])
async def delete_workflow_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
ids: Annotated[list[int], Body(description="ID列表")],
) -> JSONResponse:
await WorkflowService(auth, db).delete_workflow(ids=ids)
return SuccessResponse(msg="删除工作流成功")
@WorkflowRouter.post("/publish/{id}", summary="发布工作流", response_model=ResponseSchema[WorkflowOutSchema])
async def publish_workflow_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
id: Annotated[int, Path(description="工作流ID")],
) -> JSONResponse:
result_dict = await WorkflowService(auth, db).publish_workflow(id=id)
return SuccessResponse(data=result_dict, msg="发布工作流成功")
@WorkflowRouter.post("/execute", summary="执行工作流", response_model=ResponseSchema[WorkflowExecuteResultSchema])
async def execute_workflow_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:flow:execute"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
body: Annotated[WorkflowExecuteSchema, Body(description="执行工作流参数")],
) -> JSONResponse:
result_dict = await WorkflowService(auth, db).execute_workflow(body=body)
return SuccessResponse(data=result_dict, msg="执行工作流完成")
@@ -0,0 +1,90 @@
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 WorkflowModel
from .schema import WorkflowCreateSchema, WorkflowUpdateSchema
class WorkflowCRUD(CRUDBase[WorkflowModel, WorkflowCreateSchema, WorkflowUpdateSchema]):
"""工作流数据层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
"""初始化工作流 CRUD。
参数:
- auth (AuthSchema): 认证信息。
- db (AsyncSession): 数据库会话。
返回:
- None
"""
super().__init__(model=WorkflowModel, auth=auth, db=db)
async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> WorkflowModel | None:
"""按主键查询工作流。
参数:
- id (int): 工作流 ID。
- preload (list[str | Any] | None): 预加载关系。
返回:
- WorkflowModel | 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[WorkflowModel]:
"""条件列表查询工作流。
参数:
- search (dict | None): 查询条件。
- order_by (list[dict[str, str]] | None): 排序。
- preload (list[str | Any] | None): 预加载关系。
返回:
- Sequence[WorkflowModel]: 工作流列表。
"""
return await self.get_list(search=search, order_by=order_by, preload=preload)
async def create_obj_crud(self, data: WorkflowCreateSchema) -> WorkflowModel | None:
"""创建工作流。
参数:
- data (WorkflowCreateSchema): 创建模型。
返回:
- WorkflowModel | None: 新建实体或 None。
"""
return await self.create(data=data)
async def update_obj_crud(self, id: int, data: WorkflowUpdateSchema) -> WorkflowModel | None:
"""更新工作流。
参数:
- id (int): 工作流 ID。
- data (WorkflowUpdateSchema): 更新模型。
返回:
- WorkflowModel | 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
"""
await self.delete(ids=ids)
@@ -0,0 +1,9 @@
"""工作流执行引擎(DAG 校验、拓扑排序、分层并行执行)。"""
from .workflow_engine import run_workflow_sync, utc_now_iso, validate_workflow_graph
__all__ = [
"run_workflow_sync",
"utc_now_iso",
"validate_workflow_graph",
]
@@ -0,0 +1,132 @@
import json
from collections import defaultdict, deque
from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime
from typing import Any
from app.core.ap_scheduler import SchedulerUtil
from app.core.logger import logger
def _parse_args(args_str: str | None) -> list[Any]:
if not args_str or not str(args_str).strip():
return []
return [a.strip() for a in str(args_str).split(",") if a.strip()]
def _parse_kwargs(kwargs_str: str | None) -> dict[str, Any]:
if not kwargs_str or not str(kwargs_str).strip():
return {}
try:
return json.loads(kwargs_str)
except json.JSONDecodeError:
return {}
def validate_workflow_graph(nodes: list[dict], edges: list[dict]) -> None:
if not nodes:
raise ValueError("工作流至少需要一个节点")
ids = {n["id"] for n in nodes}
for e in edges:
if e.get("source") not in ids or e.get("target") not in ids:
raise ValueError("连线引用了不存在的节点")
in_degree: dict[str, int] = dict.fromkeys(ids, 0)
adj: dict[str, list[str]] = defaultdict(list)
for e in edges:
adj[e["source"]].append(e["target"])
in_degree[e["target"]] += 1
q: deque[str] = deque([nid for nid in ids if in_degree[nid] == 0])
visited = 0
while q:
u = q.popleft()
visited += 1
for v in adj[u]:
in_degree[v] -= 1
if in_degree[v] == 0:
q.append(v)
if visited != len(ids):
raise ValueError("工作流图存在环路,无法执行")
def _topological_levels(nodes: list[dict], edges: list[dict]) -> list[list[dict]]:
id_to_node = {n["id"]: n for n in nodes}
in_degree: dict[str, int] = {n["id"]: 0 for n in nodes}
adj: dict[str, list[str]] = defaultdict(list)
for e in edges:
adj[e["source"]].append(e["target"])
in_degree[e["target"]] += 1
levels: list[list[dict]] = []
current = [nid for nid in in_degree if in_degree[nid] == 0]
while current:
levels.append([id_to_node[nid] for nid in current])
next_level: list[str] = []
for nid in current:
for target in adj[nid]:
in_degree[target] -= 1
if in_degree[target] == 0:
next_level.append(target)
current = next_level
return levels
def _execute_node(
vue_node_id: str,
node_type_code: str,
code_block: str,
args_str: str | None,
kwargs_str: str | None,
upstream: dict[str, Any],
flow_variables: dict[str, Any],
) -> Any:
job_id = f"wfnode-{vue_node_id}"
args = _parse_args(args_str)
kw = _parse_kwargs(kwargs_str)
kw.setdefault("upstream", upstream)
kw.setdefault("variables", flow_variables)
return SchedulerUtil._task_wrapper(job_id, code_block, *args, **kw)
def run_workflow_sync(
nodes: list[dict],
edges: list[dict],
node_templates: dict[str, dict[str, Any]],
flow_variables: dict[str, Any],
) -> dict[str, Any]:
"""同步执行工作流:按拓扑层级分组,同层节点并行执行。"""
validate_workflow_graph(nodes, edges)
levels = _topological_levels(nodes, edges)
results: dict[str, Any] = {}
for level in levels:
with ThreadPoolExecutor(max_workers=len(level)) as executor:
futures: dict[str, Any] = {}
for node in level:
nid = node["id"]
ntype = node.get("type") or ""
tpl = node_templates.get(ntype)
if not tpl or not tpl.get("func"):
raise ValueError(f"未知或未配置节点类型: {ntype}")
data = node.get("data") or {}
args_str = data.get("args") if data.get("args") is not None else tpl.get("args")
kwargs_str = data.get("kwargs") if data.get("kwargs") is not None else tpl.get("kwargs")
upstream: dict[str, Any] = {}
for e in edges:
if e.get("target") == nid and e.get("source") in results:
upstream[e["source"]] = results[e["source"]]
futures[nid] = executor.submit(
_execute_node,
nid,
ntype,
tpl["func"],
args_str,
kwargs_str,
upstream,
flow_variables,
)
for nid, fut in futures.items():
results[nid] = fut.result()
logger.info("工作流执行完成: nodes={}", list(results.keys()))
return {"node_results": results, "status": 1}
def utc_now_iso() -> str:
return datetime.now(UTC).isoformat()
@@ -0,0 +1,18 @@
from sqlalchemy import JSON, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.base_model import ModelMixin, UserMixin
class WorkflowModel(ModelMixin, UserMixin):
"""工作流定义:Vue Flow 画布序列化 + 拓扑分层并行执行"""
__tablename__: str = "task_workflow"
__table_args__: dict[str, str] = {"comment": "工作流定义表"}
name: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="流程名称")
code: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, comment="流程编码")
nodes: Mapped[list | None] = mapped_column(JSON, nullable=True, comment="VueFlow节点")
edges: Mapped[list | None] = mapped_column(JSON, nullable=True, comment="VueFlow连接线")
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:草稿 1:已发布 2:已归档)")
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
@@ -0,0 +1,119 @@
import re
from typing import Any
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 DateTimeStr
class WorkflowCreateSchema(BaseModel):
"""创建工作流"""
name: str = Field(..., max_length=128, description="流程名称")
code: str = Field(..., max_length=64, description="流程编码")
description: str | None = Field(default=None, description="描述")
nodes: list | None = Field(default=None, description="Vue Flow nodes")
edges: list | None = Field(default=None, description="Vue Flow edges")
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
v = v.strip()
if len(v) < 1 or len(v) > 128:
raise ValueError("流程名称长度必须在1-128个字符之间")
return v
@field_validator("code")
@classmethod
def validate_code(cls, v: str) -> str:
v = v.strip()
if len(v) < 2 or len(v) > 64:
raise ValueError("流程编码长度必须在2-64个字符之间")
if not re.match(r"^[A-Za-z][A-Za-z0-9_]*$", v):
raise ValueError("流程编码必须以字母开头,仅允许字母、数字、下划线")
return v
class WorkflowUpdateSchema(WorkflowCreateSchema):
"""更新工作流"""
workflow_status: int | None = Field(default=None, description="0:草稿 / 1:已发布 / 2:已归档")
@field_validator("workflow_status")
@classmethod
def validate_workflow_status(cls, v: int | None) -> int | None:
if v is None:
return v
allowed = {0, 1, 2}
if v not in allowed:
raise ValueError(f"流程状态必须为 {sorted(allowed)}")
return v
class WorkflowOutSchema(BaseSchema, UserBySchema):
"""工作流输出(status 表示流程状态 draft/published/archived,与 ModelMixin.status 区分)"""
model_config = ConfigDict(from_attributes=True)
id: int | None = Field(default=None, description="主键ID")
uuid: str | None = Field(default=None, description="UUID")
description: str | None = Field(default=None, description="描述")
created_time: DateTimeStr | None = Field(default=None, description="创建时间")
updated_time: DateTimeStr | None = Field(default=None, description="更新时间")
name: str = Field(description="流程名称")
code: str = Field(description="流程编码")
status: int = Field(description="流程状态 0:草稿 / 1:已发布 / 2:已归档")
nodes: list | None = Field(default=None, description="节点")
edges: list | None = Field(default=None, description="连线")
@model_validator(mode="before")
@classmethod
def _map_workflow_status(cls, data: Any) -> Any:
from .model import WorkflowModel
if isinstance(data, WorkflowModel):
return {
"id": data.id,
"uuid": data.uuid,
"description": data.description,
"created_time": data.created_time,
"updated_time": data.updated_time,
"created_id": data.created_id,
"updated_id": data.updated_id,
"name": data.name,
"code": data.code,
"status": data.status,
"nodes": data.nodes,
"edges": data.edges,
}
return data
class WorkflowQueryParam(BaseQueryParam, UserByQueryParam):
"""工作流查询"""
name: str | None = Field(None, description="流程名称", json_schema_extra={"q": "like"})
code: str | None = Field(None, description="流程编码", json_schema_extra={"q": "eq"})
class WorkflowExecuteSchema(BaseModel):
"""执行工作流"""
workflow_id: int = Field(..., description="工作流ID")
variables: dict | None = Field(default=None, description="注入到各节点的 variables 上下文")
business_key: str | None = Field(default=None, description="业务键")
job_id: int | None = Field(default=None, description="关联任务ID")
class WorkflowExecuteResultSchema(BaseModel):
"""执行结果"""
workflow_id: int = Field(..., description="工作流ID")
workflow_name: str = Field(..., description="工作流名称")
status: int = Field(description="执行状态 0:失败 / 1:已完成")
start_time: str | None = Field(default=None, description="开始时间")
end_time: str | None = Field(default=None, description="结束时间")
variables: dict | None = Field(default=None, description="变量")
node_results: dict | None = Field(default=None, description="节点结果")
error: str | None = Field(default=None, description="错误信息")
@@ -0,0 +1,199 @@
import asyncio
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, PageResultSchema
from app.core.exceptions import CustomException
from app.utils.common_util import search_to_dict
from ..node_type.crud import WorkflowNodeTypeCRUD
from .crud import WorkflowCRUD
from .handlers.workflow_engine import run_workflow_sync, utc_now_iso, validate_workflow_graph
from .schema import (
WorkflowCreateSchema,
WorkflowExecuteResultSchema,
WorkflowExecuteSchema,
WorkflowOutSchema,
WorkflowQueryParam,
WorkflowUpdateSchema,
)
# 工作流状态常量(与 WorkflowModel.status 保持一致:0:草稿 1:已发布 2:已归档)
WORKFLOW_STATUS_DRAFT = 0
WORKFLOW_STATUS_PUBLISHED = 1
WORKFLOW_STATUS_ARCHIVED = 2
# 工作流执行结果状态(0:失败 1:已完成)
WORKFLOW_EXEC_STATUS_FAILED = 0
WORKFLOW_EXEC_STATUS_COMPLETED = 1
class WorkflowService:
"""工作流:画布存储 + 发布校验 + 分层并行执行"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
def _out(self, obj: Any) -> WorkflowOutSchema:
return WorkflowOutSchema.model_validate(obj)
async def get_workflow_detail(self, id: int) -> WorkflowOutSchema:
obj = await WorkflowCRUD(self.auth, self.db).get_obj_by_id_crud(id=id)
if not obj:
raise CustomException(msg="工作流不存在")
return self._out(obj)
async def get_workflow_list(
self,
search: WorkflowQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[WorkflowOutSchema]:
if order_by is None:
order_by = [{"updated_time": "desc"}]
obj_list = await WorkflowCRUD(self.auth, self.db).get_obj_list_crud(
search=search_to_dict(search, {}),
order_by=order_by,
)
return [self._out(o) for o in obj_list]
async def get_workflow_page(
self,
page_no: int,
page_size: int,
search: WorkflowQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> PageResultSchema[WorkflowOutSchema]:
offset = (page_no - 1) * page_size
order = order_by or [{"updated_time": "desc"}]
result = await WorkflowCRUD(self.auth, self.db).page(
offset=offset,
limit=page_size,
order_by=order,
search=search_to_dict(search, {}),
out_schema=WorkflowOutSchema,
)
return result
async def create_workflow(self, data: WorkflowCreateSchema) -> WorkflowOutSchema:
exist = await WorkflowCRUD(self.auth, self.db).get(code=data.code)
if exist:
raise CustomException(msg="流程编码已存在")
obj = await WorkflowCRUD(self.auth, self.db).create_obj_crud(data=data)
if not obj:
raise CustomException(msg="创建工作流失败")
return self._out(obj)
async def update_workflow(self, id: int, data: WorkflowUpdateSchema) -> WorkflowOutSchema:
exist = await WorkflowCRUD(self.auth, self.db).get_obj_by_id_crud(id=id)
if not exist:
raise CustomException(msg="工作流不存在")
if exist.code != data.code:
other = await WorkflowCRUD(self.auth, self.db).get(code=data.code)
if other:
raise CustomException(msg="流程编码已存在")
obj = await WorkflowCRUD(self.auth, self.db).update_obj_crud(id=id, data=data)
if not obj:
raise CustomException(msg="更新工作流失败")
return self._out(obj)
async def delete_workflow(self, ids: list[int]) -> None:
if not ids:
raise CustomException(msg="删除ID不能为空")
await WorkflowCRUD(self.auth, self.db).delete_obj_crud(ids=ids)
async def publish_workflow(self, id: int) -> WorkflowOutSchema:
obj = await WorkflowCRUD(self.auth, self.db).get_obj_by_id_crud(id=id)
if not obj:
raise CustomException(msg="工作流不存在")
nodes = obj.nodes or []
edges = obj.edges or []
try:
validate_workflow_graph(nodes, edges)
except ValueError as e:
raise CustomException(msg=str(e)) from e
data = WorkflowUpdateSchema(
name=obj.name,
code=obj.code,
description=obj.description,
nodes=obj.nodes,
edges=obj.edges,
workflow_status=WORKFLOW_STATUS_PUBLISHED,
)
updated = await WorkflowCRUD(self.auth, self.db).update_obj_crud(id=id, data=data)
if not updated:
raise CustomException(msg="发布失败")
return self._out(updated)
async def execute_workflow(self, body: WorkflowExecuteSchema) -> WorkflowExecuteResultSchema:
obj = await WorkflowCRUD(self.auth, self.db).get_obj_by_id_crud(id=body.workflow_id)
if not obj:
raise CustomException(msg="工作流不存在")
if obj.status != WORKFLOW_STATUS_PUBLISHED:
raise CustomException(msg="仅已发布的工作流可执行")
nodes = obj.nodes or []
edges = obj.edges or []
if not nodes:
raise CustomException(msg="工作流没有节点")
codes_set = {n.get("type") for n in nodes if n.get("type")}
code_list = list(codes_set)
templates: dict[str, dict[str, Any]] = {}
type_objs = await WorkflowNodeTypeCRUD(self.auth, self.db).get_obj_list_crud(search={"code": ("in", code_list)})
type_map = {t.code: t for t in type_objs}
for code in codes_set:
node_type = type_map.get(code)
if not node_type:
raise CustomException(msg=f"节点类型未注册(请在「工作流节点类型」中维护,非定时任务节点): {code}")
if not node_type.func or not str(node_type.func).strip():
raise CustomException(msg=f"节点类型未配置 func 代码块: {code}")
templates[code] = {
"func": node_type.func,
"args": node_type.args,
"kwargs": node_type.kwargs,
}
variables = body.variables or {}
start = utc_now_iso()
try:
raw = await asyncio.to_thread(
run_workflow_sync,
nodes,
edges,
templates,
variables,
)
except ValueError as e:
raise CustomException(msg=str(e)) from e
except CustomException:
raise
except Exception as e:
end = utc_now_iso()
err = WorkflowExecuteResultSchema(
workflow_id=obj.id,
workflow_name=obj.name,
status=WORKFLOW_EXEC_STATUS_FAILED,
start_time=start,
end_time=end,
variables=variables,
node_results=None,
error=str(e),
)
return err
end = utc_now_iso()
ok = WorkflowExecuteResultSchema(
workflow_id=obj.id,
workflow_name=obj.name,
status=WORKFLOW_EXEC_STATUS_COMPLETED,
start_time=start,
end_time=end,
variables=variables,
node_results=raw.get("node_results"),
error=None,
)
return ok