init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
"""工作流编排子包(plugin.module_task.workflow):
|
||||
|
||||
- ``flow``: 工作流定义(画布 CRUD、发布、执行 API)
|
||||
- ``nodes``: 节点类型(palette / 与 task_node 分离)
|
||||
- ``engine``: 拓扑分层并行执行引擎
|
||||
|
||||
动态路由仍统一挂在 ``/task`` 下(见各子包 ``controller.py`` 的 ``prefix``)。
|
||||
"""
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
"""节点类型(palette、CRUD;与定时任务 task_node 无关)。"""
|
||||
@@ -0,0 +1,97 @@
|
||||
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 WorkflowNodeTypeCreateSchema, WorkflowNodeTypeOutSchema, WorkflowNodeTypeQueryParam, WorkflowNodeTypeUpdateSchema
|
||||
from .service import WorkflowNodeTypeService
|
||||
|
||||
WorkflowNodeTypeRouter = APIRouter(route_class=OperationLogRoute, prefix="/workflow/nodes", tags=["工作流节点"])
|
||||
|
||||
|
||||
@WorkflowNodeTypeRouter.get("/options", summary="节点选项", response_model=ResponseSchema[list[dict]])
|
||||
async def get_workflow_node_type_options_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = WorkflowNodeTypeService(auth, db)
|
||||
result = await service.get_options()
|
||||
return SuccessResponse(data=result, msg="获取节点选项成功")
|
||||
|
||||
|
||||
@WorkflowNodeTypeRouter.get("/detail/{id}", summary="节点详情", response_model=ResponseSchema[WorkflowNodeTypeOutSchema])
|
||||
async def get_workflow_node_type_detail_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
id: Annotated[int, Path(description="ID")],
|
||||
) -> JSONResponse:
|
||||
service = WorkflowNodeTypeService(auth, db)
|
||||
result_dict = await service.get_detail(id=id)
|
||||
return SuccessResponse(data=result_dict, msg="获取节点详情成功")
|
||||
|
||||
|
||||
@WorkflowNodeTypeRouter.get("/list", summary="节点列表", response_model=ResponseSchema[PageResultSchema[WorkflowNodeTypeOutSchema]])
|
||||
async def get_workflow_node_type_list_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[WorkflowNodeTypeQueryParam, Query()],
|
||||
) -> JSONResponse:
|
||||
service = WorkflowNodeTypeService(auth, db)
|
||||
result_dict = await service.get_page(
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
search=search,
|
||||
order_by=page.order_by,
|
||||
)
|
||||
return SuccessResponse(data=result_dict, msg="查询节点列表成功")
|
||||
|
||||
|
||||
@WorkflowNodeTypeRouter.post("/create", status_code=status.HTTP_201_CREATED, summary="创建节点", response_model=ResponseSchema[WorkflowNodeTypeOutSchema])
|
||||
async def create_workflow_node_type_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:create"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
data: Annotated[WorkflowNodeTypeCreateSchema, Body(description="创建节点参数")],
|
||||
) -> JSONResponse:
|
||||
service = WorkflowNodeTypeService(auth, db)
|
||||
result_dict = await service.create(data=data)
|
||||
return SuccessResponse(data=result_dict, msg="创建节点成功")
|
||||
|
||||
|
||||
@WorkflowNodeTypeRouter.put("/update/{id}", summary="更新节点", response_model=ResponseSchema[WorkflowNodeTypeOutSchema])
|
||||
async def update_workflow_node_type_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:update"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
id: Annotated[int, Path(description="节点ID")],
|
||||
data: Annotated[WorkflowNodeTypeUpdateSchema, Body(description="更新节点参数")],
|
||||
) -> JSONResponse:
|
||||
service = WorkflowNodeTypeService(auth, db)
|
||||
result_dict = await service.update(id=id, data=data)
|
||||
return SuccessResponse(data=result_dict, msg="更新节点成功")
|
||||
|
||||
|
||||
@WorkflowNodeTypeRouter.delete("/delete", summary="删除节点", response_model=ResponseSchema[None])
|
||||
async def delete_workflow_node_type_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:delete"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
) -> JSONResponse:
|
||||
service = WorkflowNodeTypeService(auth, db)
|
||||
await service.delete(ids=ids)
|
||||
return SuccessResponse(msg="删除节点成功")
|
||||
|
||||
|
||||
@WorkflowNodeTypeRouter.get("/select", summary="节点选择列表", response_model=ResponseSchema[list[dict]])
|
||||
async def get_workflow_node_type_select_controller(
|
||||
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:workflow:nodes:query"]))],
|
||||
db: Annotated[AsyncSession, Depends(db_getter)],
|
||||
) -> JSONResponse:
|
||||
service = WorkflowNodeTypeService(auth, db)
|
||||
result = await service.get_select()
|
||||
return SuccessResponse(data=result, msg="获取节点选择列表成功")
|
||||
@@ -0,0 +1,102 @@
|
||||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.common.enums import QueueEnum
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.core.base_schema import AuthSchema
|
||||
|
||||
from .model import WorkflowNodeTypeModel
|
||||
from .schema import WorkflowNodeTypeCreateSchema, WorkflowNodeTypeUpdateSchema
|
||||
|
||||
|
||||
class WorkflowNodeTypeCRUD(CRUDBase[WorkflowNodeTypeModel, WorkflowNodeTypeCreateSchema, WorkflowNodeTypeUpdateSchema]):
|
||||
"""节点类型 CRUD"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
"""初始化节点类型 CRUD。
|
||||
|
||||
参数:
|
||||
- auth (AuthSchema): 认证信息。
|
||||
- db (AsyncSession): 数据库会话。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
super().__init__(model=WorkflowNodeTypeModel, auth=auth, db=db)
|
||||
|
||||
async def get_obj_by_id_crud(self, id: int, preload: list[str | Any] | None = None) -> WorkflowNodeTypeModel | None:
|
||||
"""按主键查询节点类型。
|
||||
|
||||
参数:
|
||||
- id (int): 主键。
|
||||
- preload (list[str | Any] | None): 预加载关系。
|
||||
|
||||
返回:
|
||||
- WorkflowNodeTypeModel | 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[WorkflowNodeTypeModel]:
|
||||
"""条件列表查询节点类型。
|
||||
|
||||
参数:
|
||||
- search (dict | None): 查询条件。
|
||||
- order_by (list[dict[str, str]] | None): 排序。
|
||||
- preload (list[str | Any] | None): 预加载关系。
|
||||
|
||||
返回:
|
||||
- Sequence[WorkflowNodeTypeModel]: 列表。
|
||||
"""
|
||||
return await self.get_list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_obj_crud(self, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeModel | None:
|
||||
"""创建节点类型。
|
||||
|
||||
参数:
|
||||
- data (WorkflowNodeTypeCreateSchema): 创建模型。
|
||||
|
||||
返回:
|
||||
- WorkflowNodeTypeModel | None: 新建实体或 None。
|
||||
"""
|
||||
return await self.create(data=data)
|
||||
|
||||
async def update_obj_crud(self, id: int, data: WorkflowNodeTypeUpdateSchema) -> WorkflowNodeTypeModel | None:
|
||||
"""更新节点类型。
|
||||
|
||||
参数:
|
||||
- id (int): 主键。
|
||||
- data (WorkflowNodeTypeUpdateSchema): 更新模型。
|
||||
|
||||
返回:
|
||||
- WorkflowNodeTypeModel | 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)
|
||||
|
||||
async def list_active_options_crud(self) -> Sequence[WorkflowNodeTypeModel]:
|
||||
"""画布用:仅启用的类型,按 sort_order、id 排序。
|
||||
|
||||
返回:
|
||||
- Sequence[WorkflowNodeTypeModel]: 启用中的节点类型列表。
|
||||
"""
|
||||
return await self.get_obj_list_crud(
|
||||
search={"is_active": (QueueEnum.eq.value, True)},
|
||||
order_by=[{"sort_order": "asc"}, {"id": "asc"}],
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
from sqlalchemy import Boolean, Integer, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.base_model import ModelMixin, UserMixin
|
||||
|
||||
|
||||
class WorkflowNodeTypeModel(ModelMixin, UserMixin):
|
||||
"""节点类型:用于 Vue Flow 左侧 palette 与执行引擎解析。"""
|
||||
|
||||
__tablename__: str = "task_workflow_node_type"
|
||||
__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="节点编码,对应画布 node.type")
|
||||
category: Mapped[str] = mapped_column(String(32), nullable=False, default="action", comment="分类: trigger/action/condition/control")
|
||||
func: Mapped[str] = mapped_column(Text, nullable=False, comment="Python 代码块,须定义 handler(*args,**kwargs)")
|
||||
args: Mapped[str | None] = mapped_column(Text, nullable=True, comment="默认位置参数,逗号分隔")
|
||||
kwargs: Mapped[str | None] = mapped_column(Text, nullable=True, comment="默认关键字参数 JSON")
|
||||
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="排序")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=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,71 @@
|
||||
import re
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
|
||||
|
||||
|
||||
class WorkflowNodeTypeCreateSchema(BaseModel):
|
||||
"""创建节点类型"""
|
||||
|
||||
name: str = Field(..., max_length=128, description="显示名称")
|
||||
code: str = Field(..., max_length=64, description="节点编码")
|
||||
category: str = Field(default="action", max_length=32, description="trigger/action/condition/control")
|
||||
func: str = Field(..., description="代码块,须定义 handler")
|
||||
args: str | None = Field(default=None, description="默认位置参数")
|
||||
kwargs: str | None = Field(default=None, description="默认 kwargs JSON")
|
||||
sort_order: int = Field(default=0, ge=0, description="排序")
|
||||
is_active: bool = Field(default=True, description="是否启用")
|
||||
|
||||
@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
|
||||
|
||||
@field_validator("category")
|
||||
@classmethod
|
||||
def _cat(cls, v: str) -> str:
|
||||
allowed = {"trigger", "action", "condition", "control"}
|
||||
v = v.strip()
|
||||
if v not in allowed:
|
||||
raise ValueError(f"category 须为: {allowed}")
|
||||
return v
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _func_nonempty(self):
|
||||
if not self.func or not str(self.func).strip():
|
||||
raise ValueError("必须提供 func 代码块")
|
||||
return self
|
||||
|
||||
|
||||
class WorkflowNodeTypeUpdateSchema(WorkflowNodeTypeCreateSchema):
|
||||
"""更新节点类型"""
|
||||
|
||||
|
||||
class WorkflowNodeTypeOutSchema(WorkflowNodeTypeCreateSchema, BaseSchema, UserBySchema):
|
||||
"""输出(含审计与用户信息)"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class WorkflowNodeTypeQueryParam(BaseQueryParam, UserByQueryParam):
|
||||
"""查询"""
|
||||
|
||||
name: str | None = Field(None, description="名称", json_schema_extra={"q": "like"})
|
||||
code: str | None = Field(None, description="编码", json_schema_extra={"q": "eq"})
|
||||
category: str | None = Field(None, description="分类", json_schema_extra={"q": "eq"})
|
||||
is_active: bool | None = Field(None, description="是否启用", json_schema_extra={"q": "eq"})
|
||||
status: int | None = Field(None, ge=0, le=1, description="状态(0:启动 1:停用)", json_schema_extra={"q": "eq"})
|
||||
@@ -0,0 +1,107 @@
|
||||
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 .crud import WorkflowNodeTypeCRUD
|
||||
from .schema import (
|
||||
WorkflowNodeTypeCreateSchema,
|
||||
WorkflowNodeTypeOutSchema,
|
||||
WorkflowNodeTypeQueryParam,
|
||||
WorkflowNodeTypeUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class WorkflowNodeTypeService:
|
||||
"""工作流节点类型(与定时任务 task_node 无关)"""
|
||||
|
||||
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
|
||||
self.auth = auth
|
||||
self.db = db
|
||||
|
||||
@staticmethod
|
||||
def _out(obj) -> WorkflowNodeTypeOutSchema:
|
||||
return WorkflowNodeTypeOutSchema.model_validate(obj)
|
||||
|
||||
async def get_options(self) -> list[dict]:
|
||||
objs = await WorkflowNodeTypeCRUD(self.auth, self.db).list_active_options_crud()
|
||||
return [
|
||||
{
|
||||
"id": o.id,
|
||||
"code": o.code,
|
||||
"name": o.name,
|
||||
"category": o.category,
|
||||
"args": o.args or "",
|
||||
"kwargs": o.kwargs or "{}",
|
||||
}
|
||||
for o in objs
|
||||
]
|
||||
|
||||
async def get_detail(self, id: int) -> WorkflowNodeTypeOutSchema:
|
||||
obj = await WorkflowNodeTypeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="节点类型不存在")
|
||||
return self._out(obj)
|
||||
|
||||
async def get_list(
|
||||
self,
|
||||
search: WorkflowNodeTypeQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> list[WorkflowNodeTypeOutSchema]:
|
||||
if order_by is None:
|
||||
order_by = [{"sort_order": "asc"}, {"id": "asc"}]
|
||||
obj_list = await WorkflowNodeTypeCRUD(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_page(
|
||||
self,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: WorkflowNodeTypeQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> PageResultSchema[WorkflowNodeTypeOutSchema]:
|
||||
offset = (page_no - 1) * page_size
|
||||
order = order_by or [{"sort_order": "asc"}, {"id": "asc"}]
|
||||
result = await WorkflowNodeTypeCRUD(self.auth, self.db).page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order,
|
||||
search=search_to_dict(search, {}),
|
||||
out_schema=WorkflowNodeTypeOutSchema,
|
||||
)
|
||||
return result
|
||||
|
||||
async def create(self, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeOutSchema:
|
||||
exist = await WorkflowNodeTypeCRUD(self.auth, self.db).get(code=data.code)
|
||||
if exist:
|
||||
raise CustomException(msg="节点编码已存在")
|
||||
obj = await WorkflowNodeTypeCRUD(self.auth, self.db).create_obj_crud(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建失败")
|
||||
return self._out(obj)
|
||||
|
||||
async def update(self, id: int, data: WorkflowNodeTypeUpdateSchema) -> WorkflowNodeTypeOutSchema:
|
||||
exist = await WorkflowNodeTypeCRUD(self.auth, self.db).get_obj_by_id_crud(id=id)
|
||||
if not exist:
|
||||
raise CustomException(msg="节点类型不存在")
|
||||
if exist.code != data.code:
|
||||
other = await WorkflowNodeTypeCRUD(self.auth, self.db).get(code=data.code)
|
||||
if other:
|
||||
raise CustomException(msg="节点编码已存在")
|
||||
obj = await WorkflowNodeTypeCRUD(self.auth, self.db).update_obj_crud(id=id, data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="更新失败")
|
||||
return self._out(obj)
|
||||
|
||||
async def delete(self, ids: list[int]) -> None:
|
||||
if not ids:
|
||||
raise CustomException(msg="删除ID不能为空")
|
||||
await WorkflowNodeTypeCRUD(self.auth, self.db).delete_obj_crud(ids=ids)
|
||||
|
||||
async def get_select(self) -> list[dict]:
|
||||
objs = await WorkflowNodeTypeCRUD(self.auth, self.db).get_obj_list_crud()
|
||||
return [{"id": o.id, "name": o.name} for o in objs]
|
||||
Reference in New Issue
Block a user