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:
iven
2026-04-11 23:25:43 +08:00
parent 1fec5e2cf2
commit 5d6e1dc394
32 changed files with 549 additions and 184 deletions

View File

@@ -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,
}