feat(workflow): add workflow engine module (Phase 4)
Implement complete workflow engine with BPMN subset support: Backend (erp-workflow crate): - Token-driven execution engine with exclusive/parallel gateway support - BPMN parser with flow graph validation - Expression evaluator for conditional branching - Process definition CRUD with draft/publish lifecycle - Process instance management (start, suspend, terminate) - Task service (pending, complete, delegate) - PostgreSQL advisory locks for concurrent safety - 5 database tables: process_definitions, process_instances, tokens, tasks, process_variables - 13 API endpoints with RBAC protection - Timeout checker framework (placeholder) Frontend: - Workflow page with 4 tabs (definitions, pending, completed, monitor) - React Flow visual process designer (@xyflow/react) - Process viewer with active node highlighting - 3 API client modules for workflow endpoints - Sidebar menu integration
This commit is contained in:
211
crates/erp-workflow/src/dto.rs
Normal file
211
crates/erp-workflow/src/dto.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
// --- 流程图节点/边定义 ---
|
||||
|
||||
/// BPMN 节点类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum NodeType {
|
||||
StartEvent,
|
||||
EndEvent,
|
||||
UserTask,
|
||||
ServiceTask,
|
||||
ExclusiveGateway,
|
||||
ParallelGateway,
|
||||
}
|
||||
|
||||
/// 流程图节点定义
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NodeDef {
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub node_type: NodeType,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub assignee_id: Option<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub candidate_groups: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub service_type: Option<String>,
|
||||
/// 前端渲染位置
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub position: Option<NodePosition>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NodePosition {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
/// 流程图连线定义
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EdgeDef {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub target: String,
|
||||
/// 条件表达式(排他网关分支)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub condition: Option<String>,
|
||||
/// 前端渲染标签
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
/// 完整流程图
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FlowDiagram {
|
||||
pub nodes: Vec<NodeDef>,
|
||||
pub edges: Vec<EdgeDef>,
|
||||
}
|
||||
|
||||
// --- 流程定义 DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ProcessDefinitionResp {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub key: String,
|
||||
pub version: i32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub category: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub nodes: serde_json::Value,
|
||||
pub edges: serde_json::Value,
|
||||
pub status: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateProcessDefinitionReq {
|
||||
#[validate(length(min = 1, max = 200, message = "流程名称不能为空"))]
|
||||
pub name: String,
|
||||
#[validate(length(min = 1, max = 100, message = "流程编码不能为空"))]
|
||||
pub key: String,
|
||||
pub category: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub nodes: Vec<NodeDef>,
|
||||
pub edges: Vec<EdgeDef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateProcessDefinitionReq {
|
||||
pub name: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub nodes: Option<Vec<NodeDef>>,
|
||||
pub edges: Option<Vec<EdgeDef>>,
|
||||
}
|
||||
|
||||
// --- 流程实例 DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ProcessInstanceResp {
|
||||
pub id: Uuid,
|
||||
pub definition_id: Uuid,
|
||||
pub definition_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub business_key: Option<String>,
|
||||
pub status: String,
|
||||
pub started_by: Uuid,
|
||||
pub started_at: DateTime<Utc>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 当前活跃的 token 位置
|
||||
pub active_tokens: Vec<TokenResp>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct StartInstanceReq {
|
||||
pub definition_id: Uuid,
|
||||
pub business_key: Option<String>,
|
||||
/// 初始流程变量
|
||||
pub variables: Option<Vec<SetVariableReq>>,
|
||||
}
|
||||
|
||||
// --- Token DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct TokenResp {
|
||||
pub id: Uuid,
|
||||
pub node_id: String,
|
||||
pub status: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// --- 任务 DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct TaskResp {
|
||||
pub id: Uuid,
|
||||
pub instance_id: Uuid,
|
||||
pub token_id: Uuid,
|
||||
pub node_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub node_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub assignee_id: Option<Uuid>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub candidate_groups: Option<serde_json::Value>,
|
||||
pub status: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub outcome: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub form_data: Option<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub due_date: Option<DateTime<Utc>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 流程定义名称(用于列表展示)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub definition_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub business_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct CompleteTaskReq {
|
||||
pub outcome: String,
|
||||
pub form_data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct DelegateTaskReq {
|
||||
pub delegate_to: Uuid,
|
||||
}
|
||||
|
||||
// --- 流程变量 DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ProcessVariableResp {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub var_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value_string: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value_number: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value_boolean: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value_date: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, ToSchema)]
|
||||
pub struct SetVariableReq {
|
||||
pub name: String,
|
||||
pub var_type: Option<String>,
|
||||
pub value: serde_json::Value,
|
||||
}
|
||||
Reference in New Issue
Block a user