Phase 1 安全热修复: - P0-1: /uploads 文件服务添加 JWT 认证中间件(支持 header + query param) - P0-2: analytics/batch 路由从 public 移到 protected_routes - P0-3: plugin engine SQL 注入修复(format! → 参数化查询) - P0-new: stats_service compute_avg_field 字段白名单 + FLOAT8 类型转换 Phase 2 数据完整性: - P0-4: 组织删除级联检查(添加部门存在性校验) - P0-5: 部门删除级联检查(添加岗位 + 用户存在性校验) - P0-8: workflow on_tenant_deleted 实现 5 实体批量删除 - P0-7: 并行网关 race condition 修复(consumed → completed 原子转换) Phase 3 P1 后端 Bug: - P1-12: plugin host 表名消毒(使用 sanitize_identifier) - P1-10: workflow deprecated 状态转换(published → deprecated) - P1-11: workflow 更新验证条件(nodes/edges 任一变化即验证) - P0-9: 小程序 .gitignore 添加 .env/.env.*/日志 - P1-19: 小程序加密密钥替换为 64 字符强密钥 Phase 4 消息模块: - P1-5: 通知偏好 GET 路由 + handler - P1-4: 消息模板 update/delete CRUD + version - P2-8: mark_all_read SQL 添加 version + 1 - P2-7: markAsRead 改为乐观更新 + 失败回滚 Phase 5 前端修复: - P2-9: 通知面板点击导航到 /messages - P2-1: 随访任务患者名批量 ID 解析(替代 UUID 显示) - P2-5: AppointmentList 分离 patient_id/doctor_id 分别调用 API - P2-17: PluginMarket installed 字段修正(name → id) - P3-3: 路由标题 fallback 改为模式匹配(支持 :id 动态路径) - P2-15: workflow updateDefinition 添加 version 字段 - P3-9: Kanban 版本使用记录实际 version - P2-21: secure-storage 生产环境无密钥时阻止存储 - P3-11: destroyOnHidden → destroyOnClose - P3-13: PendingTasks 深色模式 Tag 颜色适配 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
407 lines
14 KiB
Rust
407 lines
14 KiB
Rust
use chrono::Utc;
|
||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
|
||
use uuid::Uuid;
|
||
|
||
use crate::dto::{CreateProcessDefinitionReq, ProcessDefinitionResp, UpdateProcessDefinitionReq};
|
||
use crate::engine::parser;
|
||
use crate::entity::process_definition;
|
||
use crate::error::{WorkflowError, WorkflowResult};
|
||
use erp_core::audit::AuditLog;
|
||
use erp_core::audit_service;
|
||
use erp_core::error::check_version;
|
||
use erp_core::events::EventBus;
|
||
use erp_core::types::Pagination;
|
||
|
||
/// 流程定义 CRUD 服务。
|
||
pub struct DefinitionService;
|
||
|
||
impl DefinitionService {
|
||
/// 分页查询流程定义列表。
|
||
pub async fn list(
|
||
tenant_id: Uuid,
|
||
pagination: &Pagination,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> WorkflowResult<(Vec<ProcessDefinitionResp>, u64)> {
|
||
let paginator = process_definition::Entity::find()
|
||
.filter(process_definition::Column::TenantId.eq(tenant_id))
|
||
.filter(process_definition::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);
|
||
let models = paginator
|
||
.fetch_page(page_index)
|
||
.await
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||
|
||
let resps: Vec<ProcessDefinitionResp> = models.iter().map(Self::model_to_resp).collect();
|
||
Ok((resps, total))
|
||
}
|
||
|
||
/// 获取单个流程定义。
|
||
pub async fn get_by_id(
|
||
id: Uuid,
|
||
tenant_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> WorkflowResult<ProcessDefinitionResp> {
|
||
let model = process_definition::Entity::find_by_id(id)
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
|
||
.ok_or_else(|| WorkflowError::NotFound(format!("流程定义不存在: {id}")))?;
|
||
|
||
Ok(Self::model_to_resp(&model))
|
||
}
|
||
|
||
/// 创建流程定义。
|
||
pub async fn create(
|
||
tenant_id: Uuid,
|
||
operator_id: Uuid,
|
||
req: &CreateProcessDefinitionReq,
|
||
db: &sea_orm::DatabaseConnection,
|
||
event_bus: &EventBus,
|
||
) -> WorkflowResult<ProcessDefinitionResp> {
|
||
// 验证流程图合法性
|
||
parser::parse_and_validate(&req.nodes, &req.edges)?;
|
||
|
||
let now = Utc::now();
|
||
let id = Uuid::now_v7();
|
||
let nodes_json = serde_json::to_value(&req.nodes)
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||
let edges_json = serde_json::to_value(&req.edges)
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||
|
||
let model = process_definition::ActiveModel {
|
||
id: Set(id),
|
||
tenant_id: Set(tenant_id),
|
||
name: Set(req.name.clone()),
|
||
key: Set(req.key.clone()),
|
||
version: Set(1),
|
||
category: Set(req.category.clone()),
|
||
description: Set(req.description.clone()),
|
||
nodes: Set(nodes_json),
|
||
edges: Set(edges_json),
|
||
status: Set("draft".to_string()),
|
||
created_at: Set(now),
|
||
updated_at: Set(now),
|
||
created_by: Set(operator_id),
|
||
updated_by: Set(operator_id),
|
||
deleted_at: Set(None),
|
||
version_field: Set(1),
|
||
};
|
||
model
|
||
.insert(db)
|
||
.await
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||
|
||
event_bus
|
||
.publish(
|
||
erp_core::events::DomainEvent::new(
|
||
"process_definition.created",
|
||
tenant_id,
|
||
serde_json::json!({ "definition_id": id, "key": req.key }),
|
||
),
|
||
db,
|
||
)
|
||
.await;
|
||
|
||
audit_service::record(
|
||
AuditLog::new(
|
||
tenant_id,
|
||
Some(operator_id),
|
||
"process_definition.create",
|
||
"process_definition",
|
||
)
|
||
.with_resource_id(id),
|
||
db,
|
||
)
|
||
.await;
|
||
|
||
Ok(ProcessDefinitionResp {
|
||
id,
|
||
name: req.name.clone(),
|
||
key: req.key.clone(),
|
||
version: 1,
|
||
category: req.category.clone(),
|
||
description: req.description.clone(),
|
||
nodes: serde_json::to_value(&req.nodes).unwrap_or_default(),
|
||
edges: serde_json::to_value(&req.edges).unwrap_or_default(),
|
||
status: "draft".to_string(),
|
||
created_at: now,
|
||
updated_at: now,
|
||
lock_version: 1,
|
||
})
|
||
}
|
||
|
||
/// 更新流程定义(仅 draft 状态可编辑)。
|
||
pub async fn update(
|
||
id: Uuid,
|
||
tenant_id: Uuid,
|
||
operator_id: Uuid,
|
||
req: &UpdateProcessDefinitionReq,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> WorkflowResult<ProcessDefinitionResp> {
|
||
let model = process_definition::Entity::find_by_id(id)
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
|
||
.ok_or_else(|| WorkflowError::NotFound(format!("流程定义不存在: {id}")))?;
|
||
|
||
if model.status != "draft" {
|
||
return Err(WorkflowError::InvalidState(
|
||
"只有 draft 状态的流程定义可以编辑".to_string(),
|
||
));
|
||
}
|
||
|
||
let current_version = model.version_field;
|
||
let mut active: process_definition::ActiveModel = model.into();
|
||
|
||
if let Some(name) = &req.name {
|
||
active.name = Set(name.clone());
|
||
}
|
||
if let Some(category) = &req.category {
|
||
active.category = Set(Some(category.clone()));
|
||
}
|
||
if let Some(description) = &req.description {
|
||
active.description = Set(Some(description.clone()));
|
||
}
|
||
// 当 nodes 或 edges 任一存在时,取最终值验证流程图完整性
|
||
let final_nodes = req.nodes.as_ref().or_else(|| {
|
||
serde_json::from_value::<Vec<crate::dto::NodeDef>>(active.nodes.as_ref().clone()).ok().as_ref().map(|_| unreachable!())
|
||
});
|
||
// 简化:如果提供了 nodes 或 edges,将两者合并后验证
|
||
if req.nodes.is_some() || req.edges.is_some() {
|
||
let nodes_val = req.nodes.as_ref().map(|n| serde_json::to_value(n).unwrap()).unwrap_or(active.nodes.as_ref().clone());
|
||
let edges_val = req.edges.as_ref().map(|e| serde_json::to_value(e).unwrap()).unwrap_or(active.edges.as_ref().clone());
|
||
let nodes: Vec<crate::dto::NodeDef> = serde_json::from_value(nodes_val)
|
||
.map_err(|e| WorkflowError::Validation(format!("节点数据无效: {e}")))?;
|
||
let edges: Vec<crate::dto::EdgeDef> = serde_json::from_value(edges_val)
|
||
.map_err(|e| WorkflowError::Validation(format!("连线数据无效: {e}")))?;
|
||
parser::parse_and_validate(&nodes, &edges)?;
|
||
}
|
||
if let Some(nodes) = &req.nodes {
|
||
let nodes_json = serde_json::to_value(nodes)
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||
active.nodes = Set(nodes_json);
|
||
}
|
||
if let Some(edges) = &req.edges {
|
||
let edges_json = serde_json::to_value(edges)
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||
active.edges = Set(edges_json);
|
||
}
|
||
|
||
let next_ver = check_version(req.version, current_version)
|
||
.map_err(|_| WorkflowError::VersionMismatch)?;
|
||
active.version_field = Set(next_ver);
|
||
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()))?;
|
||
|
||
audit_service::record(
|
||
AuditLog::new(
|
||
tenant_id,
|
||
Some(operator_id),
|
||
"process_definition.update",
|
||
"process_definition",
|
||
)
|
||
.with_resource_id(id),
|
||
db,
|
||
)
|
||
.await;
|
||
|
||
Ok(Self::model_to_resp(&updated))
|
||
}
|
||
|
||
/// 发布流程定义(draft → published)。
|
||
pub async fn publish(
|
||
id: Uuid,
|
||
tenant_id: Uuid,
|
||
operator_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
event_bus: &EventBus,
|
||
) -> WorkflowResult<ProcessDefinitionResp> {
|
||
let model = process_definition::Entity::find_by_id(id)
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
|
||
.ok_or_else(|| WorkflowError::NotFound(format!("流程定义不存在: {id}")))?;
|
||
|
||
if model.status != "draft" {
|
||
return Err(WorkflowError::InvalidState(
|
||
"只有 draft 状态的流程定义可以发布".to_string(),
|
||
));
|
||
}
|
||
|
||
// 验证流程图
|
||
let nodes: Vec<crate::dto::NodeDef> = serde_json::from_value(model.nodes.clone())
|
||
.map_err(|e| WorkflowError::InvalidDiagram(format!("节点数据无效: {e}")))?;
|
||
let edges: Vec<crate::dto::EdgeDef> = serde_json::from_value(model.edges.clone())
|
||
.map_err(|e| WorkflowError::InvalidDiagram(format!("连线数据无效: {e}")))?;
|
||
parser::parse_and_validate(&nodes, &edges)?;
|
||
|
||
let current_version = model.version_field;
|
||
let mut active: process_definition::ActiveModel = model.into();
|
||
active.status = Set("published".to_string());
|
||
active.version_field = Set(current_version + 1);
|
||
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()))?;
|
||
|
||
event_bus
|
||
.publish(
|
||
erp_core::events::DomainEvent::new(
|
||
"process_definition.published",
|
||
tenant_id,
|
||
serde_json::json!({ "definition_id": id }),
|
||
),
|
||
db,
|
||
)
|
||
.await;
|
||
|
||
audit_service::record(
|
||
AuditLog::new(
|
||
tenant_id,
|
||
Some(operator_id),
|
||
"process_definition.publish",
|
||
"process_definition",
|
||
)
|
||
.with_resource_id(id),
|
||
db,
|
||
)
|
||
.await;
|
||
|
||
Ok(Self::model_to_resp(&updated))
|
||
}
|
||
|
||
/// 将已发布的流程定义标记为 deprecated。
|
||
pub async fn deprecate(
|
||
id: Uuid,
|
||
tenant_id: Uuid,
|
||
operator_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
event_bus: &EventBus,
|
||
) -> WorkflowResult<ProcessDefinitionResp> {
|
||
let model = process_definition::Entity::find_by_id(id)
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
|
||
.ok_or_else(|| WorkflowError::NotFound(format!("流程定义不存在: {id}")))?;
|
||
|
||
if model.status != "published" {
|
||
return Err(WorkflowError::InvalidState(
|
||
"只有 published 状态的流程定义可以废弃".to_string(),
|
||
));
|
||
}
|
||
|
||
let current_version = model.version_field;
|
||
let mut active: process_definition::ActiveModel = model.into();
|
||
active.status = Set("deprecated".to_string());
|
||
active.version_field = Set(current_version + 1);
|
||
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()))?;
|
||
|
||
event_bus
|
||
.publish(
|
||
erp_core::events::DomainEvent::new(
|
||
"process_definition.deprecated",
|
||
tenant_id,
|
||
serde_json::json!({ "definition_id": id }),
|
||
),
|
||
db,
|
||
)
|
||
.await;
|
||
|
||
audit_service::record(
|
||
AuditLog::new(
|
||
tenant_id,
|
||
Some(operator_id),
|
||
"process_definition.deprecate",
|
||
"process_definition",
|
||
)
|
||
.with_resource_id(id),
|
||
db,
|
||
)
|
||
.await;
|
||
|
||
Ok(Self::model_to_resp(&updated))
|
||
}
|
||
|
||
/// 软删除流程定义。
|
||
pub async fn delete(
|
||
id: Uuid,
|
||
tenant_id: Uuid,
|
||
operator_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> WorkflowResult<()> {
|
||
let model = process_definition::Entity::find_by_id(id)
|
||
.one(db)
|
||
.await
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?
|
||
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
|
||
.ok_or_else(|| WorkflowError::NotFound(format!("流程定义不存在: {id}")))?;
|
||
|
||
let current_version = model.version_field;
|
||
let mut active: process_definition::ActiveModel = model.into();
|
||
active.version_field = Set(current_version + 1);
|
||
active.deleted_at = Set(Some(Utc::now()));
|
||
active.updated_at = Set(Utc::now());
|
||
active.updated_by = Set(operator_id);
|
||
active
|
||
.update(db)
|
||
.await
|
||
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
|
||
|
||
audit_service::record(
|
||
AuditLog::new(
|
||
tenant_id,
|
||
Some(operator_id),
|
||
"process_definition.delete",
|
||
"process_definition",
|
||
)
|
||
.with_resource_id(id),
|
||
db,
|
||
)
|
||
.await;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn model_to_resp(m: &process_definition::Model) -> ProcessDefinitionResp {
|
||
ProcessDefinitionResp {
|
||
id: m.id,
|
||
name: m.name.clone(),
|
||
key: m.key.clone(),
|
||
version: m.version,
|
||
category: m.category.clone(),
|
||
description: m.description.clone(),
|
||
nodes: m.nodes.clone(),
|
||
edges: m.edges.clone(),
|
||
status: m.status.clone(),
|
||
created_at: m.created_at,
|
||
updated_at: m.updated_at,
|
||
lock_version: m.version_field,
|
||
}
|
||
}
|
||
}
|