refactor: 完成项目大规模代码重构与依赖清理

这是一次综合性的重构更新,包含以下主要变更:
1.  升级Python版本到3.12,更新依赖配置
2.  替换旧的.j2模板为.jinja2格式,新增代码生成模板
3.  重构权限过滤策略,更新权限枚举与模型配置
4.  移除Prefect依赖,替换为自研拓扑并行执行引擎
5.  重构认证与上下文管理,拆分租户/请求上下文
6.  简化响应模型、CRUD与服务层代码
7.  清理废弃的支付网关模块,重构订单定时任务
8.  更新在线用户、监控等模块的接口与路由
9.  优化邮件模板与工具类,新增邮件模板文件
10. 修复数据库会话配置与类型提示
This commit is contained in:
zhangtao
2026-06-21 06:02:05 +08:00
parent 4e2b668d7b
commit 211fddd6e0
121 changed files with 2869 additions and 11379 deletions
@@ -3,7 +3,7 @@
- ``definition``: 工作流定义(画布 CRUD、发布、执行 API)
- ``node_type``: 编排节点类型(palette / 与 task_node 分离)
- ``engine``: Prefect DAG 执行引擎
- ``engine``: 拓扑分层并行执行引擎
动态路由仍统一挂在 ``/task`` 下(见各子包 ``controller.py`` 的 ``prefix``)。
"""
@@ -19,7 +19,7 @@ from .schema import (
)
from .service import WorkflowService
WorkflowRouter = APIRouter(route_class=OperationLogRoute, prefix="/workflow/definition", tags=["工作流"])
WorkflowRouter = APIRouter(route_class=OperationLogRoute, prefix="/workflow/definition", tags=["任务调度", "工作流"])
@WorkflowRouter.get(
@@ -31,17 +31,7 @@ async def get_workflow_detail_controller(
id: Annotated[int, Path(description="工作流ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:detail"]))],
) -> JSONResponse:
"""
根据 ID 获取工作流详情(含画布 nodes/edges)。
参数:
- id (int): 工作流 ID。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为详情字典。
"""
result_dict = await WorkflowService.get_workflow_detail_service(auth=auth, id=id)
result_dict = await WorkflowService(auth).get_workflow_detail(id=id)
return SuccessResponse(data=result_dict, msg="获取工作流详情成功")
@@ -55,19 +45,7 @@ async def get_workflow_list_controller(
search: Annotated[WorkflowQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:query"]))],
) -> JSONResponse:
"""
分页查询工作流列表。
参数:
- page (PaginationQueryParam): 分页与排序参数。
- search (WorkflowQueryParam): 查询条件。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为分页结果。
"""
result_dict = await WorkflowService.get_workflow_page_service(
auth=auth,
result_dict = await WorkflowService(auth).get_workflow_page(
page_no=page.page_no,
page_size=page.page_size,
search=search,
@@ -85,17 +63,7 @@ async def create_workflow_controller(
data: WorkflowCreateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:create"]))],
) -> JSONResponse:
"""
创建草稿工作流。
参数:
- data (WorkflowCreateSchema): 创建体。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为新建工作流。
"""
result_dict = await WorkflowService.create_workflow_service(auth=auth, data=data)
result_dict = await WorkflowService(auth).create_workflow(data=data)
return SuccessResponse(data=result_dict, msg="创建工作流成功")
@@ -109,18 +77,7 @@ async def update_workflow_controller(
data: WorkflowUpdateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:update"]))],
) -> JSONResponse:
"""
更新工作流及画布。
参数:
- id (int): 工作流 ID。
- data (WorkflowUpdateSchema): 更新体。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为更新后的工作流。
"""
result_dict = await WorkflowService.update_workflow_service(auth=auth, id=id, data=data)
result_dict = await WorkflowService(auth).update_workflow(id=id, data=data)
return SuccessResponse(data=result_dict, msg="更新工作流成功")
@@ -133,17 +90,7 @@ async def delete_workflow_controller(
ids: Annotated[list[int], Body(description="ID列表")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:delete"]))],
) -> JSONResponse:
"""
批量删除工作流。
参数:
- ids (list[int]): 工作流 ID 列表。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功提示响应。
"""
await WorkflowService.delete_workflow_service(auth=auth, ids=ids)
await WorkflowService(auth).delete_workflow(ids=ids)
return SuccessResponse(msg="删除工作流成功")
@@ -156,17 +103,7 @@ async def publish_workflow_controller(
id: Annotated[int, Path(description="工作流ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:update"]))],
) -> JSONResponse:
"""
校验 DAG 无环后发布工作流。
参数:
- id (int): 工作流 ID。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为发布后工作流。
"""
result_dict = await WorkflowService.publish_workflow_service(auth=auth, id=id)
result_dict = await WorkflowService(auth).publish_workflow(id=id)
return SuccessResponse(data=result_dict, msg="发布工作流成功")
@@ -179,15 +116,5 @@ async def execute_workflow_controller(
body: WorkflowExecuteSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:execute"]))],
) -> JSONResponse:
"""
使用 Prefect 按拓扑顺序执行已发布工作流。
参数:
- body (WorkflowExecuteSchema): 工作流 ID 与变量等。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为执行结果摘要。
"""
result_dict = await WorkflowService.execute_workflow_service(auth=auth, body=body)
result_dict = await WorkflowService(auth).execute_workflow(body=body)
return SuccessResponse(data=result_dict, msg="执行工作流完成")
@@ -6,7 +6,7 @@ from app.core.base_model import ModelMixin, TenantMixin, UserMixin
class WorkflowModel(ModelMixin, TenantMixin, UserMixin):
"""
工作流定义:Vue Flow 画布序列化 + Prefect 运行时执行
工作流定义:Vue Flow 画布序列化 + 拓扑分层并行执行
"""
__tablename__: str = "task_workflow"
@@ -4,8 +4,8 @@ from typing import Any
from app.core.base_schema import AuthSchema
from app.core.exceptions import CustomException
from ..engine.prefect_engine import run_prefect_workflow_sync, utc_now_iso, validate_workflow_graph
from ..node_type.crud import WorkflowNodeTypeCRUD
from ..handlers.workflow_engine import run_workflow_sync, utc_now_iso, validate_workflow_graph
from ..nodes.crud import WorkflowNodeTypeCRUD
from .crud import WorkflowCRUD
from .schema import (
WorkflowCreateSchema,
@@ -18,83 +18,43 @@ from .schema import (
class WorkflowService:
"""工作流:画布存储 + 发布校验 + Prefect 执行"""
"""工作流:画布存储 + 发布校验 + 分层并行执行"""
@classmethod
def _out(cls, obj: Any) -> WorkflowOutSchema:
def __init__(self, auth: AuthSchema) -> None:
self.auth = auth
def _out(self, obj: Any) -> WorkflowOutSchema:
return WorkflowOutSchema.model_validate(obj)
@classmethod
async def get_workflow_detail_service(cls, auth: AuthSchema, id: int) -> WorkflowOutSchema:
"""
获取工作流详情。
参数:
- auth (AuthSchema): 认证信息。
- id (int): 工作流 ID。
返回:
- dict: 序列化后的工作流详情。
异常:
- CustomException: 不存在时抛出。
"""
obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id)
async def get_workflow_detail(self, id: int) -> WorkflowOutSchema:
obj = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=id)
if not obj:
raise CustomException(msg="工作流不存在")
return cls._out(obj)
return self._out(obj)
@classmethod
async def get_workflow_list_service(
cls,
auth: AuthSchema,
async def get_workflow_list(
self,
search: WorkflowQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[WorkflowOutSchema]:
"""
获取工作流列表(非分页)。
参数:
- auth (AuthSchema): 认证信息。
- search (WorkflowQueryParam | None): 查询条件。
- order_by (list[dict[str, str]] | None): 排序。
返回:
- list[dict]: 工作流字典列表。
"""
if order_by is None:
order_by = [{"updated_time": "desc"}]
obj_list = await WorkflowCRUD(auth).get_obj_list_crud(
obj_list = await WorkflowCRUD(self.auth).get_obj_list_crud(
search=vars(search) if search else None,
order_by=order_by,
)
return [cls._out(o) for o in obj_list]
return [self._out(o) for o in obj_list]
@classmethod
async def get_workflow_page_service(
cls,
auth: AuthSchema,
async def get_workflow_page(
self,
page_no: int,
page_size: int,
search: WorkflowQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> dict:
"""
分页查询工作流(数据库 OFFSET/LIMIT)。
参数:
- auth (AuthSchema): 认证信息。
- page_no (int): 页码。
- page_size (int): 每页条数。
- search (WorkflowQueryParam | None): 查询条件。
- order_by (list[dict[str, str]] | None): 排序。
返回:
- dict: 分页结果(items 已 JSON 友好序列化)。
"""
offset = (page_no - 1) * page_size
order = order_by or [{"updated_time": "desc"}]
result = await WorkflowCRUD(auth).page(
result = await WorkflowCRUD(self.auth).page(
offset=offset,
limit=page_size,
order_by=order,
@@ -104,92 +64,35 @@ class WorkflowService:
result.items = [WorkflowOutSchema.model_validate(item).model_dump(mode="json") for item in result.items]
return result
@classmethod
async def create_workflow_service(cls, auth: AuthSchema, data: WorkflowCreateSchema) -> WorkflowOutSchema:
"""
创建工作流草稿。
参数:
- auth (AuthSchema): 认证信息。
- data (WorkflowCreateSchema): 创建体。
返回:
- dict: 新建工作流字典。
异常:
- CustomException: 编码重复或创建失败。
"""
exist = await WorkflowCRUD(auth).get(code=data.code)
async def create_workflow(self, data: WorkflowCreateSchema) -> WorkflowOutSchema:
exist = await WorkflowCRUD(self.auth).get(code=data.code)
if exist:
raise CustomException(msg="流程编码已存在")
obj = await WorkflowCRUD(auth).create_obj_crud(data=data)
obj = await WorkflowCRUD(self.auth).create_obj_crud(data=data)
if not obj:
raise CustomException(msg="创建工作流失败")
return cls._out(obj)
return self._out(obj)
@classmethod
async def update_workflow_service(cls, auth: AuthSchema, id: int, data: WorkflowUpdateSchema) -> WorkflowOutSchema:
"""
更新工作流。
参数:
- auth (AuthSchema): 认证信息。
- id (int): 工作流 ID。
- data (WorkflowUpdateSchema): 更新体。
返回:
- dict: 更新后工作流字典。
异常:
- CustomException: 不存在、编码冲突或更新失败。
"""
exist = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id)
async def update_workflow(self, id: int, data: WorkflowUpdateSchema) -> WorkflowOutSchema:
exist = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=id)
if not exist:
raise CustomException(msg="工作流不存在")
if exist.code != data.code:
other = await WorkflowCRUD(auth).get(code=data.code)
other = await WorkflowCRUD(self.auth).get(code=data.code)
if other:
raise CustomException(msg="流程编码已存在")
obj = await WorkflowCRUD(auth).update_obj_crud(id=id, data=data)
obj = await WorkflowCRUD(self.auth).update_obj_crud(id=id, data=data)
if not obj:
raise CustomException(msg="更新工作流失败")
return cls._out(obj)
return self._out(obj)
@classmethod
async def delete_workflow_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
批量删除工作流。
参数:
- auth (AuthSchema): 认证信息。
- ids (list[int]): ID 列表。
返回:
- None
异常:
- CustomException: ID 为空时抛出。
"""
async def delete_workflow(self, ids: list[int]) -> None:
if not ids:
raise CustomException(msg="删除ID不能为空")
await WorkflowCRUD(auth).delete_obj_crud(ids=ids)
await WorkflowCRUD(self.auth).delete_obj_crud(ids=ids)
@classmethod
async def publish_workflow_service(cls, auth: AuthSchema, id: int) -> WorkflowOutSchema:
"""
校验 DAG 后发布工作流。
参数:
- auth (AuthSchema): 认证信息。
- id (int): 工作流 ID。
返回:
- dict: 发布后工作流字典。
异常:
- CustomException: 不存在、图无效或发布失败。
"""
obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id)
async def publish_workflow(self, id: int) -> WorkflowOutSchema:
obj = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=id)
if not obj:
raise CustomException(msg="工作流不存在")
nodes = obj.nodes or []
@@ -208,27 +111,13 @@ class WorkflowService:
edges=obj.edges,
workflow_status="published",
)
updated = await WorkflowCRUD(auth).update_obj_crud(id=id, data=data)
updated = await WorkflowCRUD(self.auth).update_obj_crud(id=id, data=data)
if not updated:
raise CustomException(msg="发布失败")
return cls._out(updated)
return self._out(updated)
@classmethod
async def execute_workflow_service(cls, auth: AuthSchema, body: WorkflowExecuteSchema) -> WorkflowExecuteResultSchema:
"""
执行已发布工作流(Prefect 同步入口在线程池中运行)。
参数:
- auth (AuthSchema): 认证信息。
- body (WorkflowExecuteSchema): 工作流 ID 与变量。
返回:
- dict: 执行结果摘要(成功或失败结构)。
异常:
- CustomException: 未发布、缺节点、节点类型未注册等。
"""
obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=body.workflow_id)
async def execute_workflow(self, body: WorkflowExecuteSchema) -> WorkflowExecuteResultSchema:
obj = await WorkflowCRUD(self.auth).get_obj_by_id_crud(id=body.workflow_id)
if not obj:
raise CustomException(msg="工作流不存在")
if obj.workflow_status != "published":
@@ -242,7 +131,7 @@ class WorkflowService:
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(auth).get_obj_list_crud(search={"code": ("in", code_list)})
type_objs = await WorkflowNodeTypeCRUD(self.auth).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)
@@ -260,7 +149,7 @@ class WorkflowService:
start = utc_now_iso()
try:
raw = await asyncio.to_thread(
run_prefect_workflow_sync,
run_workflow_sync,
nodes,
edges,
templates,
@@ -1,9 +1,9 @@
"""Prefect 编排执行DAG 校验、拓扑排序、Flow/Task)。"""
"""工作流执行引擎DAG 校验、拓扑排序、分层并行执行)。"""
from .prefect_engine import run_prefect_workflow_sync, utc_now_iso, validate_workflow_graph
from .workflow_engine import run_workflow_sync, utc_now_iso, validate_workflow_graph
__all__ = [
"run_prefect_workflow_sync",
"run_workflow_sync",
"utc_now_iso",
"validate_workflow_graph",
]
@@ -1,232 +0,0 @@
"""
将 Vue Flow 画布(nodes/edges)转为 DAG,按拓扑顺序用 Prefect 编排执行。
画布节点 `type` 对应表 task_workflow_node_type.code(与定时任务 task_node 无关),
执行时加载该类型的 `func` 代码块,经 SchedulerUtil._task_wrapper 运行。
"""
from __future__ import annotations
import json
from collections import defaultdict, deque
from datetime import datetime, timezone
from typing import Any
from prefect import flow, task
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:
"""
校验画布图有效且无环。
参数:
- nodes (list[dict]): 节点列表(须含 id)。
- edges (list[dict]): 边列表(source/target)。
返回:
- None
异常:
- ValueError: 图为空、边引用非法或存在环。
"""
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]]:
"""
按拓扑层级分组节点:同一层级内节点互不依赖,可并行执行。
参数:
- 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
@task(name="workflow-node", retries=0)
def prefect_node_task(
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:
"""
单个画布节点的 Prefect Task:通过 SchedulerUtil 执行用户代码块。
参数:
- vue_node_id (str): 画布节点 id。
- node_type_code (str): 节点类型编码。
- code_block (str): 可执行代码字符串。
- args_str (str | None): 逗号分隔位置参数说明。
- kwargs_str (str | None): JSON 关键字参数。
- 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)
@flow(name="workflow-run", log_prints=True)
def run_workflow_prefect_flow(
ordered_nodes: list[dict],
edges: list[dict],
node_templates: dict[str, dict[str, Any]],
flow_variables: dict[str, Any],
) -> dict[str, Any]:
"""
Prefect Flow:按拓扑层级并行提交,层间串行收集结果。
同层级节点互不依赖,使用 submit() 批量提交后统一收集,
避免 .submit() → .result() 逐节点串行阻塞。
参数:
- ordered_nodes (list[dict]): 已排序节点列表。
- edges (list[dict]): 边列表。
- node_templates (dict[str, dict[str, Any]]): 类型编码到 {func, args, kwargs}。
- flow_variables (dict[str, Any]): 流程变量。
返回:
- dict[str, Any]: 含 node_results、status 等。
"""
levels = _topological_levels(ordered_nodes, edges)
results: dict[str, Any] = {}
for level in levels:
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] = prefect_node_task.submit(
nid,
ntype,
tpl["func"],
args_str,
kwargs_str,
upstream,
flow_variables,
)
for nid, fut in futures.items():
results[nid] = fut.result()
logger.info(
"Prefect workflow 完成: nodes=%s",
list(results.keys()),
)
return {
"node_results": results,
"status": "completed",
}
def run_prefect_workflow_sync(
nodes: list[dict],
edges: list[dict],
node_templates: dict[str, dict[str, Any]],
flow_variables: dict[str, Any],
) -> dict[str, Any]:
"""
同步入口:校验 DAG 后执行 Prefect FlowFlow 内部按层级并行调度)。
参数:
- nodes (list[dict]): 画布节点。
- edges (list[dict]): 画布边。
- node_templates (dict[str, dict[str, Any]]): 节点类型模板。
- flow_variables (dict[str, Any]): 流程变量。
返回:
- dict[str, Any]: Flow 执行汇总结果。
"""
validate_workflow_graph(nodes, edges)
return run_workflow_prefect_flow(
ordered_nodes=nodes,
edges=edges,
node_templates=node_templates,
flow_variables=flow_variables or {},
)
def utc_now_iso() -> str:
"""
当前 UTC 时间的 ISO 8601 字符串。
返回:
- str: ISO 格式时间戳。
"""
return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,136 @@
"""工作流 DAG 执行引擎 — 拓扑分层 + 并行执行"""
from __future__ import annotations
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=%s", list(results.keys()))
return {"node_results": results, "status": "completed"}
def utc_now_iso() -> str:
return datetime.now(UTC).isoformat()
@@ -32,16 +32,8 @@ WorkflowNodeTypeRouter = APIRouter(
async def get_workflow_node_type_options_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
) -> JSONResponse:
"""
获取画布用编排节点类型选项(仅启用项)。
参数:
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为选项列表。
"""
result = await WorkflowNodeTypeService.get_options_service(auth=auth)
service = WorkflowNodeTypeService(auth)
result = await service.get_options()
return SuccessResponse(data=result, msg="获取编排节点类型选项成功")
@@ -54,17 +46,8 @@ async def get_workflow_node_type_detail_controller(
id: Annotated[int, Path(description="ID")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
) -> JSONResponse:
"""
获取编排节点类型详情。
参数:
- id (int): 主键。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为详情。
"""
result_dict = await WorkflowNodeTypeService.get_detail_service(auth=auth, id=id)
service = WorkflowNodeTypeService(auth)
result_dict = await service.get_detail(id=id)
return SuccessResponse(data=result_dict, msg="获取编排节点类型详情成功")
@@ -78,22 +61,11 @@ async def get_workflow_node_type_list_controller(
search: Annotated[WorkflowNodeTypeQueryParam, Depends()],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
) -> JSONResponse:
"""
分页查询编排节点类型列表。
参数:
- page (PaginationQueryParam): 分页与排序。
- search (WorkflowNodeTypeQueryParam): 查询条件。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为分页结果。
"""
order_by = [{"sort_order": "asc"}, {"id": "asc"}]
if page.order_by:
order_by = page.order_by
result_dict = await WorkflowNodeTypeService.get_page_service(
auth=auth,
service = WorkflowNodeTypeService(auth)
result_dict = await service.get_page(
page_no=page.page_no,
page_size=page.page_size,
search=search,
@@ -111,17 +83,8 @@ async def create_workflow_node_type_controller(
data: WorkflowNodeTypeCreateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:create"]))],
) -> JSONResponse:
"""
创建编排节点类型。
参数:
- data (WorkflowNodeTypeCreateSchema): 创建体。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为新记录。
"""
result_dict = await WorkflowNodeTypeService.create_service(auth=auth, data=data)
service = WorkflowNodeTypeService(auth)
result_dict = await service.create(data=data)
return SuccessResponse(data=result_dict, msg="创建编排节点类型成功")
@@ -135,18 +98,8 @@ async def update_workflow_node_type_controller(
data: WorkflowNodeTypeUpdateSchema,
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:update"]))],
) -> JSONResponse:
"""
更新编排节点类型。
参数:
- id (int): 主键。
- data (WorkflowNodeTypeUpdateSchema): 更新体。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为更新后记录。
"""
result_dict = await WorkflowNodeTypeService.update_service(auth=auth, id=id, data=data)
service = WorkflowNodeTypeService(auth)
result_dict = await service.update(id=id, data=data)
return SuccessResponse(data=result_dict, msg="更新编排节点类型成功")
@@ -159,17 +112,8 @@ async def delete_workflow_node_type_controller(
ids: Annotated[list[int], Body(description="ID列表")],
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:delete"]))],
) -> JSONResponse:
"""
批量删除编排节点类型。
参数:
- ids (list[int]): ID 列表。
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功提示响应。
"""
await WorkflowNodeTypeService.delete_service(auth=auth, ids=ids)
service = WorkflowNodeTypeService(auth)
await service.delete(ids=ids)
return SuccessResponse(msg="删除编排节点类型成功")
@@ -181,14 +125,6 @@ async def delete_workflow_node_type_controller(
async def get_workflow_node_type_select_controller(
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
) -> JSONResponse:
"""
获取编排节点类型选择列表。
参数:
- auth (AuthSchema): 认证信息。
返回:
- JSONResponse: 成功响应,data 为选择列表。
"""
result = await WorkflowNodeTypeService.get_select_service(auth=auth)
service = WorkflowNodeTypeService(auth)
result = await service.get_select()
return SuccessResponse(data=result, msg="获取编排节点类型选择列表成功")
@@ -6,7 +6,7 @@ from app.core.base_model import ModelMixin, TenantMixin, UserMixin
class WorkflowNodeTypeModel(ModelMixin, TenantMixin, UserMixin):
"""
编排节点类型:用于 Vue Flow 左侧 palette 与 Prefect 运行时解析。
编排节点类型:用于 Vue Flow 左侧 palette 与执行引擎解析。
"""
__tablename__: str = "task_workflow_node_type"
@@ -13,22 +13,15 @@ from .schema import (
class WorkflowNodeTypeService:
"""工作流编排节点类型(与定时任务 task_node 无关)"""
@classmethod
def _out(cls, obj) -> WorkflowNodeTypeOutSchema:
def __init__(self, auth: AuthSchema) -> None:
self.auth = auth
@staticmethod
def _out(obj) -> WorkflowNodeTypeOutSchema:
return WorkflowNodeTypeOutSchema.model_validate(obj)
@classmethod
async def get_options_service(cls, auth: AuthSchema) -> list[dict]:
"""
画布左侧 palette:仅返回启用项,结构与前端 Node options 对齐。
参数:
- auth (AuthSchema): 认证信息。
返回:
- list[dict]: 选项字典列表。
"""
objs = await WorkflowNodeTypeCRUD(auth).list_active_options_crud()
async def get_options(self) -> list[dict]:
objs = await WorkflowNodeTypeCRUD(self.auth).list_active_options_crud()
return [
{
"id": o.id,
@@ -41,77 +34,35 @@ class WorkflowNodeTypeService:
for o in objs
]
@classmethod
async def get_detail_service(cls, auth: AuthSchema, id: int) -> WorkflowNodeTypeOutSchema:
"""
获取编排节点类型详情。
参数:
- auth (AuthSchema): 认证信息。
- id (int): 主键。
返回:
- dict: 序列化后的详情。
异常:
- CustomException: 不存在时抛出。
"""
obj = await WorkflowNodeTypeCRUD(auth).get_obj_by_id_crud(id=id)
async def get_detail(self, id: int) -> WorkflowNodeTypeOutSchema:
obj = await WorkflowNodeTypeCRUD(self.auth).get_obj_by_id_crud(id=id)
if not obj:
raise CustomException(msg="编排节点类型不存在")
return cls._out(obj)
return self._out(obj)
@classmethod
async def get_list_service(
cls,
auth: AuthSchema,
async def get_list(
self,
search: WorkflowNodeTypeQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[WorkflowNodeTypeOutSchema]:
"""
获取编排节点类型列表(非分页)。
参数:
- auth (AuthSchema): 认证信息。
- search (WorkflowNodeTypeQueryParam | None): 查询条件。
- order_by (list[dict[str, str]] | None): 排序。
返回:
- list[dict]: 字典列表。
"""
if order_by is None:
order_by = [{"sort_order": "asc"}, {"id": "asc"}]
obj_list = await WorkflowNodeTypeCRUD(auth).get_obj_list_crud(
obj_list = await WorkflowNodeTypeCRUD(self.auth).get_obj_list_crud(
search=vars(search) if search else None,
order_by=order_by,
)
return [cls._out(o) for o in obj_list]
return [self._out(o) for o in obj_list]
@classmethod
async def get_page_service(
cls,
auth: AuthSchema,
async def get_page(
self,
page_no: int,
page_size: int,
search: WorkflowNodeTypeQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> dict:
"""
分页查询编排节点类型(数据库 OFFSET/LIMIT)。
参数:
- auth (AuthSchema): 认证信息。
- page_no (int): 页码。
- page_size (int): 每页条数。
- search (WorkflowNodeTypeQueryParam | None): 查询条件。
- order_by (list[dict[str, str]] | None): 排序。
返回:
- dict: 分页结果(items 已 JSON 友好序列化)。
"""
offset = (page_no - 1) * page_size
order = order_by or [{"sort_order": "asc"}, {"id": "asc"}]
result = await WorkflowNodeTypeCRUD(auth).page(
result = await WorkflowNodeTypeCRUD(self.auth).page(
offset=offset,
limit=page_size,
order_by=order,
@@ -121,86 +72,33 @@ class WorkflowNodeTypeService:
result.items = [WorkflowNodeTypeOutSchema.model_validate(item).model_dump(mode="json") for item in result.items]
return result
@classmethod
async def create_service(cls, auth: AuthSchema, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeOutSchema:
"""
创建编排节点类型。
参数:
- auth (AuthSchema): 认证信息。
- data (WorkflowNodeTypeCreateSchema): 创建体。
返回:
- dict: 新建记录字典。
异常:
- CustomException: 编码重复或创建失败。
"""
exist = await WorkflowNodeTypeCRUD(auth).get(code=data.code)
async def create(self, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeOutSchema:
exist = await WorkflowNodeTypeCRUD(self.auth).get(code=data.code)
if exist:
raise CustomException(msg="节点编码已存在")
obj = await WorkflowNodeTypeCRUD(auth).create_obj_crud(data=data)
obj = await WorkflowNodeTypeCRUD(self.auth).create_obj_crud(data=data)
if not obj:
raise CustomException(msg="创建失败")
return cls._out(obj)
return self._out(obj)
@classmethod
async def update_service(cls, auth: AuthSchema, id: int, data: WorkflowNodeTypeUpdateSchema) -> WorkflowNodeTypeOutSchema:
"""
更新编排节点类型。
参数:
- auth (AuthSchema): 认证信息。
- id (int): 主键。
- data (WorkflowNodeTypeUpdateSchema): 更新体。
返回:
- dict: 更新后字典。
异常:
- CustomException: 不存在、编码冲突或更新失败。
"""
exist = await WorkflowNodeTypeCRUD(auth).get_obj_by_id_crud(id=id)
async def update(self, id: int, data: WorkflowNodeTypeUpdateSchema) -> WorkflowNodeTypeOutSchema:
exist = await WorkflowNodeTypeCRUD(self.auth).get_obj_by_id_crud(id=id)
if not exist:
raise CustomException(msg="编排节点类型不存在")
if exist.code != data.code:
other = await WorkflowNodeTypeCRUD(auth).get(code=data.code)
other = await WorkflowNodeTypeCRUD(self.auth).get(code=data.code)
if other:
raise CustomException(msg="节点编码已存在")
obj = await WorkflowNodeTypeCRUD(auth).update_obj_crud(id=id, data=data)
obj = await WorkflowNodeTypeCRUD(self.auth).update_obj_crud(id=id, data=data)
if not obj:
raise CustomException(msg="更新失败")
return cls._out(obj)
return self._out(obj)
@classmethod
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
"""
批量删除编排节点类型。
参数:
- auth (AuthSchema): 认证信息。
- ids (list[int]): ID 列表。
返回:
- None
异常:
- CustomException: ID 为空时抛出。
"""
async def delete(self, ids: list[int]) -> None:
if not ids:
raise CustomException(msg="删除ID不能为空")
await WorkflowNodeTypeCRUD(auth).delete_obj_crud(ids=ids)
await WorkflowNodeTypeCRUD(self.auth).delete_obj_crud(ids=ids)
@classmethod
async def get_select_service(cls, auth: AuthSchema) -> list[dict]:
"""
获取编排节点类型选择列表。
参数:
- auth (AuthSchema): 认证信息。
返回:
- list[dict]: 选择列表,包含 id 和 name。
"""
objs = await WorkflowNodeTypeCRUD(auth).get_obj_list_crud()
async def get_select(self) -> list[dict]:
objs = await WorkflowNodeTypeCRUD(self.auth).get_obj_list_crud()
return [{"id": o.id, "name": o.name} for o in objs]