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:
336
crates/erp-workflow/src/service/task_service.rs
Normal file
336
crates/erp-workflow/src/service/task_service.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::Utc;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set,
|
||||
TransactionTrait,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::dto::{CompleteTaskReq, DelegateTaskReq, TaskResp};
|
||||
use crate::engine::executor::FlowExecutor;
|
||||
use crate::engine::parser;
|
||||
use crate::entity::{process_definition, process_instance, task};
|
||||
use crate::error::{WorkflowError, WorkflowResult};
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::types::Pagination;
|
||||
|
||||
/// 任务服务。
|
||||
pub struct TaskService;
|
||||
|
||||
impl TaskService {
|
||||
/// 查询当前用户的待办任务。
|
||||
pub async fn list_pending(
|
||||
tenant_id: Uuid,
|
||||
assignee_id: Uuid,
|
||||
pagination: &Pagination,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> WorkflowResult<(Vec<TaskResp>, u64)> {
|
||||
let paginator = task::Entity::find()
|
||||
.filter(task::Column::TenantId.eq(tenant_id))
|
||||
.filter(task::Column::AssigneeId.eq(assignee_id))
|
||||
.filter(task::Column::Status.eq("pending"))
|
||||
.filter(task::Column::DeletedAt.is_null())
|
||||
.paginate(db, pagination.limit());
|
||||
|
||||
let total = paginator
|
||||
.num_items()
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
let page_index = pagination.page.unwrap_or(1).saturating_sub(1) as u64;
|
||||
let models = paginator
|
||||
.fetch_page(page_index)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
let mut resps = Vec::new();
|
||||
for m in &models {
|
||||
let mut resp = Self::model_to_resp(m);
|
||||
// 附加实例信息
|
||||
if let Some(inst) = process_instance::Entity::find_by_id(m.instance_id)
|
||||
.one(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
resp.business_key = inst.business_key;
|
||||
if let Some(def) = process_definition::Entity::find_by_id(inst.definition_id)
|
||||
.one(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
resp.definition_name = Some(def.name);
|
||||
}
|
||||
}
|
||||
resps.push(resp);
|
||||
}
|
||||
|
||||
Ok((resps, total))
|
||||
}
|
||||
|
||||
/// 查询当前用户的已办任务。
|
||||
pub async fn list_completed(
|
||||
tenant_id: Uuid,
|
||||
assignee_id: Uuid,
|
||||
pagination: &Pagination,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> WorkflowResult<(Vec<TaskResp>, u64)> {
|
||||
let paginator = task::Entity::find()
|
||||
.filter(task::Column::TenantId.eq(tenant_id))
|
||||
.filter(task::Column::AssigneeId.eq(assignee_id))
|
||||
.filter(task::Column::Status.is_in(["approved", "rejected", "delegated"]))
|
||||
.filter(task::Column::DeletedAt.is_null())
|
||||
.paginate(db, pagination.limit());
|
||||
|
||||
let total = paginator
|
||||
.num_items()
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
let page_index = pagination.page.unwrap_or(1).saturating_sub(1) as u64;
|
||||
let models = paginator
|
||||
.fetch_page(page_index)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
let mut resps = Vec::new();
|
||||
for m in &models {
|
||||
let mut resp = Self::model_to_resp(m);
|
||||
if let Some(inst) = process_instance::Entity::find_by_id(m.instance_id)
|
||||
.one(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
resp.business_key = inst.business_key;
|
||||
if let Some(def) = process_definition::Entity::find_by_id(inst.definition_id)
|
||||
.one(db)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
resp.definition_name = Some(def.name);
|
||||
}
|
||||
}
|
||||
resps.push(resp);
|
||||
}
|
||||
|
||||
Ok((resps, total))
|
||||
}
|
||||
|
||||
/// 完成任务:更新任务状态 + 推进 token。
|
||||
pub async fn complete(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Uuid,
|
||||
req: &CompleteTaskReq,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
event_bus: &EventBus,
|
||||
) -> WorkflowResult<TaskResp> {
|
||||
let task_model = task::Entity::find_by_id(id)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||||
.filter(|t| t.tenant_id == tenant_id && t.deleted_at.is_none())
|
||||
.ok_or_else(|| WorkflowError::NotFound(format!("任务不存在: {id}")))?;
|
||||
|
||||
if task_model.status != "pending" {
|
||||
return Err(WorkflowError::InvalidState(
|
||||
"任务状态不是 pending,无法完成".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let instance_id = task_model.instance_id;
|
||||
let token_id = task_model.token_id;
|
||||
|
||||
// 获取流程定义和流程图
|
||||
let instance = process_instance::Entity::find_by_id(instance_id)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||||
.filter(|i| i.tenant_id == tenant_id && i.deleted_at.is_none())
|
||||
.ok_or_else(|| WorkflowError::NotFound(format!("流程实例不存在: {instance_id}")))?;
|
||||
|
||||
let definition = process_definition::Entity::find_by_id(instance.definition_id)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||||
.filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none())
|
||||
.ok_or_else(|| {
|
||||
WorkflowError::NotFound(format!("流程定义不存在: {}", instance.definition_id))
|
||||
})?;
|
||||
|
||||
let nodes: Vec<crate::dto::NodeDef> =
|
||||
serde_json::from_value(definition.nodes.clone()).map_err(|e| {
|
||||
WorkflowError::InvalidDiagram(format!("节点数据无效: {e}"))
|
||||
})?;
|
||||
let edges: Vec<crate::dto::EdgeDef> =
|
||||
serde_json::from_value(definition.edges.clone()).map_err(|e| {
|
||||
WorkflowError::InvalidDiagram(format!("连线数据无效: {e}"))
|
||||
})?;
|
||||
let graph = parser::parse_and_validate(&nodes, &edges)?;
|
||||
|
||||
// 准备变量(从 req.form_data 中提取)
|
||||
let mut variables = HashMap::new();
|
||||
if let Some(form) = &req.form_data {
|
||||
if let Some(obj) = form.as_object() {
|
||||
for (k, v) in obj {
|
||||
variables.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在事务中更新任务 + 推进 token
|
||||
let now = Utc::now();
|
||||
let outcome = req.outcome.clone();
|
||||
let form_data = req.form_data.clone();
|
||||
db.transaction::<_, (), WorkflowError>(|txn| {
|
||||
let graph = graph.clone();
|
||||
let variables = variables.clone();
|
||||
let task_model = task_model.clone();
|
||||
Box::pin(async move {
|
||||
// 更新任务状态
|
||||
let mut active: task::ActiveModel = task_model.clone().into();
|
||||
active.status = Set("completed".to_string());
|
||||
active.outcome = Set(Some(outcome));
|
||||
active.form_data = Set(form_data);
|
||||
active.completed_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active
|
||||
.update(txn)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
// 推进 token
|
||||
FlowExecutor::advance(
|
||||
token_id,
|
||||
instance_id,
|
||||
tenant_id,
|
||||
&graph,
|
||||
&variables,
|
||||
txn,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
event_bus.publish(erp_core::events::DomainEvent::new(
|
||||
"task.completed",
|
||||
tenant_id,
|
||||
serde_json::json!({ "task_id": id, "outcome": req.outcome }),
|
||||
));
|
||||
|
||||
// 重新查询任务
|
||||
let updated = task::Entity::find_by_id(id)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||||
.ok_or_else(|| WorkflowError::NotFound(format!("任务不存在: {id}")))?;
|
||||
|
||||
Ok(Self::model_to_resp(&updated))
|
||||
}
|
||||
|
||||
/// 委派任务给其他人。
|
||||
pub async fn delegate(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Uuid,
|
||||
req: &DelegateTaskReq,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> WorkflowResult<TaskResp> {
|
||||
let task_model = task::Entity::find_by_id(id)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||||
.filter(|t| t.tenant_id == tenant_id && t.deleted_at.is_none())
|
||||
.ok_or_else(|| WorkflowError::NotFound(format!("任务不存在: {id}")))?;
|
||||
|
||||
if task_model.status != "pending" {
|
||||
return Err(WorkflowError::InvalidState(
|
||||
"任务状态不是 pending,无法委派".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut active: task::ActiveModel = task_model.into();
|
||||
active.assignee_id = Set(Some(req.delegate_to));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
|
||||
let updated = active
|
||||
.update(db)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
Ok(Self::model_to_resp(&updated))
|
||||
}
|
||||
|
||||
/// 创建任务记录(由执行引擎调用)。
|
||||
pub async fn create_task(
|
||||
instance_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
token_id: Uuid,
|
||||
node_id: &str,
|
||||
node_name: Option<&str>,
|
||||
assignee_id: Option<Uuid>,
|
||||
candidate_groups: Option<Vec<String>>,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> WorkflowResult<Uuid> {
|
||||
let id = Uuid::now_v7();
|
||||
let now = Utc::now();
|
||||
let system_user = Uuid::nil();
|
||||
|
||||
let model = task::ActiveModel {
|
||||
id: Set(id),
|
||||
tenant_id: Set(tenant_id),
|
||||
instance_id: Set(instance_id),
|
||||
token_id: Set(token_id),
|
||||
node_id: Set(node_id.to_string()),
|
||||
node_name: Set(node_name.map(|s| s.to_string())),
|
||||
assignee_id: Set(assignee_id),
|
||||
candidate_groups: Set(candidate_groups.map(|g| serde_json::to_value(g).unwrap_or_default())),
|
||||
status: Set("pending".to_string()),
|
||||
outcome: Set(None),
|
||||
form_data: Set(None),
|
||||
due_date: Set(None),
|
||||
completed_at: Set(None),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(system_user),
|
||||
updated_by: Set(system_user),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
model
|
||||
.insert(db)
|
||||
.await
|
||||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn model_to_resp(m: &task::Model) -> TaskResp {
|
||||
TaskResp {
|
||||
id: m.id,
|
||||
instance_id: m.instance_id,
|
||||
token_id: m.token_id,
|
||||
node_id: m.node_id.clone(),
|
||||
node_name: m.node_name.clone(),
|
||||
assignee_id: m.assignee_id,
|
||||
candidate_groups: m.candidate_groups.clone(),
|
||||
status: m.status.clone(),
|
||||
outcome: m.outcome.clone(),
|
||||
form_data: m.form_data.clone(),
|
||||
due_date: m.due_date,
|
||||
completed_at: m.completed_at,
|
||||
created_at: m.created_at,
|
||||
definition_name: None,
|
||||
business_key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user