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:
@@ -101,8 +101,7 @@ where
|
||||
id,
|
||||
ctx.tenant_id,
|
||||
ctx.user_id,
|
||||
&req.name,
|
||||
&req.description,
|
||||
&req,
|
||||
&state.db,
|
||||
)
|
||||
.await?;
|
||||
@@ -113,11 +112,13 @@ where
|
||||
/// DELETE /api/v1/dictionaries/:id
|
||||
///
|
||||
/// 软删除字典,设置 deleted_at 时间戳。
|
||||
/// 需要请求体包含 version 字段用于乐观锁校验。
|
||||
/// 需要 `dictionary.delete` 权限。
|
||||
pub async fn delete_dictionary<S>(
|
||||
State(state): State<ConfigState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<DeleteVersionReq>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
ConfigState: FromRef<S>,
|
||||
@@ -125,8 +126,15 @@ where
|
||||
{
|
||||
require_permission(&ctx, "dictionary.delete")?;
|
||||
|
||||
DictionaryService::delete(id, ctx.tenant_id, ctx.user_id, &state.db, &state.event_bus)
|
||||
.await?;
|
||||
DictionaryService::delete(
|
||||
id,
|
||||
ctx.tenant_id,
|
||||
ctx.user_id,
|
||||
req.version,
|
||||
&state.db,
|
||||
&state.event_bus,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
@@ -228,11 +236,13 @@ where
|
||||
/// DELETE /api/v1/dictionaries/:dict_id/items/:item_id
|
||||
///
|
||||
/// 软删除字典项,设置 deleted_at 时间戳。
|
||||
/// 需要请求体包含 version 字段用于乐观锁校验。
|
||||
/// 需要 `dictionary.delete` 权限。
|
||||
pub async fn delete_item<S>(
|
||||
State(state): State<ConfigState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path((_dict_id, item_id)): Path<(Uuid, Uuid)>,
|
||||
Json(req): Json<DeleteVersionReq>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
ConfigState: FromRef<S>,
|
||||
@@ -240,7 +250,8 @@ where
|
||||
{
|
||||
require_permission(&ctx, "dictionary.delete")?;
|
||||
|
||||
DictionaryService::delete_item(item_id, ctx.tenant_id, ctx.user_id, &state.db).await?;
|
||||
DictionaryService::delete_item(item_id, ctx.tenant_id, ctx.user_id, req.version, &state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
@@ -254,3 +265,9 @@ where
|
||||
pub struct ItemsByCodeQuery {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
/// 删除操作的乐观锁版本号。
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct DeleteVersionReq {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ where
|
||||
scope: "platform".to_string(),
|
||||
scope_id: None,
|
||||
value,
|
||||
version: None,
|
||||
},
|
||||
ctx.tenant_id,
|
||||
ctx.user_id,
|
||||
|
||||
@@ -86,11 +86,12 @@ where
|
||||
|
||||
/// DELETE /api/v1/config/menus/{id}
|
||||
///
|
||||
/// 软删除单个菜单项。
|
||||
/// 软删除单个菜单项。需要请求体包含 version 字段用于乐观锁校验。
|
||||
pub async fn delete_menu<S>(
|
||||
State(state): State<ConfigState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<DeleteMenuVersionReq>,
|
||||
) -> Result<JsonResponse<ApiResponse<()>>, AppError>
|
||||
where
|
||||
ConfigState: FromRef<S>,
|
||||
@@ -98,7 +99,15 @@ where
|
||||
{
|
||||
require_permission(&ctx, "menu.update")?;
|
||||
|
||||
MenuService::delete(id, ctx.tenant_id, ctx.user_id, &state.db, &state.event_bus).await?;
|
||||
MenuService::delete(
|
||||
id,
|
||||
ctx.tenant_id,
|
||||
ctx.user_id,
|
||||
req.version,
|
||||
&state.db,
|
||||
&state.event_bus,
|
||||
)
|
||||
.await?;
|
||||
Ok(JsonResponse(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
@@ -122,6 +131,7 @@ where
|
||||
for item in &req.menus {
|
||||
match item.id {
|
||||
Some(id) => {
|
||||
let version = item.version.unwrap_or(0);
|
||||
let update_req = crate::dto::UpdateMenuReq {
|
||||
title: Some(item.title.clone()),
|
||||
path: item.path.clone(),
|
||||
@@ -130,6 +140,7 @@ where
|
||||
visible: item.visible,
|
||||
permission: item.permission.clone(),
|
||||
role_ids: item.role_ids.clone(),
|
||||
version,
|
||||
};
|
||||
MenuService::update(id, ctx.tenant_id, ctx.user_id, &update_req, &state.db)
|
||||
.await?;
|
||||
@@ -164,3 +175,9 @@ where
|
||||
message: Some("菜单批量保存成功".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// 删除菜单的乐观锁版本号请求体。
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct DeleteMenuVersionReq {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
@@ -121,11 +121,13 @@ where
|
||||
/// DELETE /api/v1/numbering-rules/:id
|
||||
///
|
||||
/// 软删除编号规则,设置 deleted_at 时间戳。
|
||||
/// 需要请求体包含 version 字段用于乐观锁校验。
|
||||
/// 需要 `numbering.delete` 权限。
|
||||
pub async fn delete_numbering_rule<S>(
|
||||
State(state): State<ConfigState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(req): Json<DeleteNumberingVersionReq>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
ConfigState: FromRef<S>,
|
||||
@@ -133,8 +135,15 @@ where
|
||||
{
|
||||
require_permission(&ctx, "numbering.delete")?;
|
||||
|
||||
NumberingService::delete(id, ctx.tenant_id, ctx.user_id, &state.db, &state.event_bus)
|
||||
.await?;
|
||||
NumberingService::delete(
|
||||
id,
|
||||
ctx.tenant_id,
|
||||
ctx.user_id,
|
||||
req.version,
|
||||
&state.db,
|
||||
&state.event_bus,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
@@ -142,3 +151,9 @@ where
|
||||
message: Some("编号规则已删除".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// 删除编号规则的乐观锁版本号请求体。
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct DeleteNumberingVersionReq {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ where
|
||||
scope: "tenant".to_string(),
|
||||
scope_id: None,
|
||||
value: req.setting_value,
|
||||
version: req.version,
|
||||
},
|
||||
ctx.tenant_id,
|
||||
ctx.user_id,
|
||||
@@ -80,12 +81,14 @@ pub struct SettingQuery {
|
||||
/// DELETE /api/v1/settings/:key
|
||||
///
|
||||
/// 软删除设置值,设置 deleted_at 时间戳。
|
||||
/// 需要请求体包含 version 字段用于乐观锁校验。
|
||||
/// 需要 `setting.delete` 权限。
|
||||
pub async fn delete_setting<S>(
|
||||
State(state): State<ConfigState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(key): Path<String>,
|
||||
Query(query): Query<SettingQuery>,
|
||||
Json(req): Json<DeleteSettingVersionReq>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where
|
||||
ConfigState: FromRef<S>,
|
||||
@@ -101,6 +104,7 @@ where
|
||||
&query.scope_id,
|
||||
ctx.tenant_id,
|
||||
ctx.user_id,
|
||||
req.version,
|
||||
&state.db,
|
||||
)
|
||||
.await?;
|
||||
@@ -111,3 +115,9 @@ where
|
||||
message: Some("设置已删除".to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// 删除设置的乐观锁版本号请求体。
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct DeleteSettingVersionReq {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ where
|
||||
scope: "tenant".to_string(),
|
||||
scope_id: None,
|
||||
value,
|
||||
version: None,
|
||||
},
|
||||
ctx.tenant_id,
|
||||
ctx.user_id,
|
||||
|
||||
Reference in New Issue
Block a user