feat(core): implement optimistic locking across all entities
Add VersionMismatch error variant and check_version() helper to erp-core. All 13 mutable entities now enforce version checking on update/delete: - erp-auth: user, role, organization, department, position - erp-config: dictionary, dictionary_item, menu, setting, numbering_rule - erp-workflow: process_definition, process_instance, task - erp-message: message, message_subscription Update DTOs to expose version in responses and require version in update requests. HTTP 409 Conflict returned on version mismatch.
This commit is contained in:
@@ -79,6 +79,7 @@ pub struct ProcessDefinitionResp {
|
||||
pub status: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub lock_version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
@@ -101,6 +102,7 @@ pub struct UpdateProcessDefinitionReq {
|
||||
pub description: Option<String>,
|
||||
pub nodes: Option<Vec<NodeDef>>,
|
||||
pub edges: Option<Vec<EdgeDef>>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
// --- 流程实例 DTOs ---
|
||||
@@ -120,6 +122,7 @@ pub struct ProcessInstanceResp {
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 当前活跃的 token 位置
|
||||
pub active_tokens: Vec<TokenResp>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
@@ -169,6 +172,7 @@ pub struct TaskResp {
|
||||
pub definition_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub business_key: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
|
||||
@@ -20,6 +20,9 @@ pub enum WorkflowError {
|
||||
|
||||
#[error("表达式求值失败: {0}")]
|
||||
ExpressionError(String),
|
||||
|
||||
#[error("版本冲突: 数据已被其他操作修改,请刷新后重试")]
|
||||
VersionMismatch,
|
||||
}
|
||||
|
||||
impl From<sea_orm::TransactionError<WorkflowError>> for WorkflowError {
|
||||
@@ -42,6 +45,7 @@ impl From<WorkflowError> for AppError {
|
||||
WorkflowError::InvalidDiagram(s) => AppError::Validation(s),
|
||||
WorkflowError::InvalidState(s) => AppError::Validation(s),
|
||||
WorkflowError::ExpressionError(s) => AppError::Validation(s),
|
||||
WorkflowError::VersionMismatch => AppError::VersionMismatch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::dto::{
|
||||
use crate::engine::parser;
|
||||
use crate::entity::process_definition;
|
||||
use crate::error::{WorkflowError, WorkflowResult};
|
||||
use erp_core::error::check_version;
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::types::Pagination;
|
||||
|
||||
@@ -118,6 +119,7 @@ impl DefinitionService {
|
||||
status: "draft".to_string(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
lock_version: 1,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -142,6 +144,7 @@ impl DefinitionService {
|
||||
));
|
||||
}
|
||||
|
||||
let current_version = model.version_field;
|
||||
let mut active: process_definition::ActiveModel = model.into();
|
||||
|
||||
if let Some(name) = &req.name {
|
||||
@@ -168,6 +171,9 @@ impl DefinitionService {
|
||||
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);
|
||||
|
||||
@@ -207,8 +213,10 @@ impl DefinitionService {
|
||||
.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);
|
||||
|
||||
@@ -240,7 +248,9 @@ impl DefinitionService {
|
||||
.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);
|
||||
@@ -265,6 +275,7 @@ impl DefinitionService {
|
||||
status: m.status.clone(),
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
lock_version: m.version_field,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ impl InstanceService {
|
||||
completed_at: instance.completed_at,
|
||||
created_at: instance.created_at,
|
||||
active_tokens,
|
||||
version: instance.version,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,6 +196,7 @@ impl InstanceService {
|
||||
completed_at: m.completed_at,
|
||||
created_at: m.created_at,
|
||||
active_tokens,
|
||||
version: m.version,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -234,6 +236,7 @@ impl InstanceService {
|
||||
completed_at: instance.completed_at,
|
||||
created_at: instance.created_at,
|
||||
active_tokens,
|
||||
version: instance.version,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -289,8 +292,10 @@ impl InstanceService {
|
||||
)));
|
||||
}
|
||||
|
||||
let current_version = instance.version;
|
||||
let mut active: process_instance::ActiveModel = instance.into();
|
||||
active.status = Set(to_status.to_string());
|
||||
active.version = Set(current_version + 1);
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active
|
||||
|
||||
@@ -206,11 +206,13 @@ impl TaskService {
|
||||
let task_model = task_model.clone();
|
||||
Box::pin(async move {
|
||||
// 更新任务状态
|
||||
let current_version = task_model.version;
|
||||
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.version = Set(current_version + 1);
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active
|
||||
@@ -297,8 +299,10 @@ impl TaskService {
|
||||
));
|
||||
}
|
||||
|
||||
let current_version = task_model.version;
|
||||
let mut active: task::ActiveModel = task_model.into();
|
||||
active.assignee_id = Set(Some(req.delegate_to));
|
||||
active.version = Set(current_version + 1);
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
|
||||
@@ -372,6 +376,7 @@ impl TaskService {
|
||||
created_at: m.created_at,
|
||||
definition_name: None,
|
||||
business_key: None,
|
||||
version: m.version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user