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:
@@ -14,6 +14,7 @@ pub struct DictionaryItemResp {
|
||||
pub sort_order: i32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
@@ -24,6 +25,7 @@ pub struct DictionaryResp {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub items: Vec<DictionaryItemResp>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
@@ -39,6 +41,7 @@ pub struct CreateDictionaryReq {
|
||||
pub struct UpdateDictionaryReq {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
@@ -57,6 +60,7 @@ pub struct UpdateDictionaryItemReq {
|
||||
pub value: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
pub color: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
// --- Menu DTOs ---
|
||||
@@ -77,6 +81,7 @@ pub struct MenuResp {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub permission: Option<String>,
|
||||
pub children: Vec<MenuResp>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
@@ -103,6 +108,7 @@ pub struct UpdateMenuReq {
|
||||
pub visible: Option<bool>,
|
||||
pub permission: Option<String>,
|
||||
pub role_ids: Option<Vec<Uuid>>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
@@ -124,6 +130,8 @@ pub struct MenuItemReq {
|
||||
pub menu_type: Option<String>,
|
||||
pub permission: Option<String>,
|
||||
pub role_ids: Option<Vec<Uuid>>,
|
||||
/// 乐观锁版本号。更新已有菜单时必填。
|
||||
pub version: Option<i32>,
|
||||
}
|
||||
|
||||
// --- Setting DTOs ---
|
||||
@@ -136,11 +144,14 @@ pub struct SettingResp {
|
||||
pub scope_id: Option<Uuid>,
|
||||
pub setting_key: String,
|
||||
pub setting_value: serde_json::Value,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateSettingReq {
|
||||
pub setting_value: serde_json::Value,
|
||||
/// 乐观锁版本号。更新已有设置时必填,创建新设置时忽略。
|
||||
pub version: Option<i32>,
|
||||
}
|
||||
|
||||
/// 内部参数结构体,用于减少 SettingService::set 的参数数量。
|
||||
@@ -149,6 +160,8 @@ pub struct SetSettingParams {
|
||||
pub scope: String,
|
||||
pub scope_id: Option<Uuid>,
|
||||
pub value: serde_json::Value,
|
||||
/// 乐观锁版本号。更新已有设置时用于校验。
|
||||
pub version: Option<i32>,
|
||||
}
|
||||
|
||||
// --- Numbering Rule DTOs ---
|
||||
@@ -168,6 +181,7 @@ pub struct NumberingRuleResp {
|
||||
pub reset_cycle: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_reset_date: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
@@ -192,6 +206,7 @@ pub struct UpdateNumberingRuleReq {
|
||||
pub seq_length: Option<i32>,
|
||||
pub separator: Option<String>,
|
||||
pub reset_cycle: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
|
||||
@@ -14,6 +14,9 @@ pub enum ConfigError {
|
||||
|
||||
#[error("编号序列耗尽: {0}")]
|
||||
NumberingExhausted(String),
|
||||
|
||||
#[error("版本冲突: 数据已被其他操作修改,请刷新后重试")]
|
||||
VersionMismatch,
|
||||
}
|
||||
|
||||
impl From<sea_orm::TransactionError<ConfigError>> for ConfigError {
|
||||
@@ -34,6 +37,7 @@ impl From<ConfigError> for AppError {
|
||||
ConfigError::NotFound(s) => AppError::NotFound(s),
|
||||
ConfigError::DuplicateKey(s) => AppError::Conflict(s),
|
||||
ConfigError::NumberingExhausted(s) => AppError::Internal(s),
|
||||
ConfigError::VersionMismatch => AppError::VersionMismatch,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,6 +7,7 @@ use uuid::Uuid;
|
||||
use crate::dto::{DictionaryItemResp, DictionaryResp};
|
||||
use crate::entity::{dictionary, dictionary_item};
|
||||
use crate::error::{ConfigError, ConfigResult};
|
||||
use erp_core::error::check_version;
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::types::Pagination;
|
||||
|
||||
@@ -51,6 +52,7 @@ impl DictionaryService {
|
||||
code: m.code.clone(),
|
||||
description: m.description.clone(),
|
||||
items,
|
||||
version: m.version,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,6 +82,7 @@ impl DictionaryService {
|
||||
code: model.code.clone(),
|
||||
description: model.description.clone(),
|
||||
items,
|
||||
version: model.version,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,18 +143,19 @@ impl DictionaryService {
|
||||
code: code.to_string(),
|
||||
description: description.clone(),
|
||||
items: vec![],
|
||||
version: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update editable dictionary fields (name and description).
|
||||
///
|
||||
/// Code cannot be changed after creation.
|
||||
/// Performs optimistic locking via version check.
|
||||
pub async fn update(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Uuid,
|
||||
name: &Option<String>,
|
||||
description: &Option<String>,
|
||||
req: &crate::dto::UpdateDictionaryReq,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> ConfigResult<DictionaryResp> {
|
||||
let model = dictionary::Entity::find_by_id(id)
|
||||
@@ -161,17 +165,21 @@ impl DictionaryService {
|
||||
.filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none())
|
||||
.ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?;
|
||||
|
||||
let next_version =
|
||||
check_version(req.version, model.version).map_err(|_| ConfigError::VersionMismatch)?;
|
||||
|
||||
let mut active: dictionary::ActiveModel = model.into();
|
||||
|
||||
if let Some(n) = name {
|
||||
if let Some(n) = &req.name {
|
||||
active.name = Set(n.clone());
|
||||
}
|
||||
if let Some(d) = description {
|
||||
if let Some(d) = &req.description {
|
||||
active.description = Set(Some(d.clone()));
|
||||
}
|
||||
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
|
||||
let updated = active
|
||||
.update(db)
|
||||
@@ -186,14 +194,17 @@ impl DictionaryService {
|
||||
code: updated.code.clone(),
|
||||
description: updated.description.clone(),
|
||||
items,
|
||||
version: updated.version,
|
||||
})
|
||||
}
|
||||
|
||||
/// Soft-delete a dictionary by setting the `deleted_at` timestamp.
|
||||
/// Performs optimistic locking via version check.
|
||||
pub async fn delete(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Uuid,
|
||||
version: i32,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
event_bus: &EventBus,
|
||||
) -> ConfigResult<()> {
|
||||
@@ -204,10 +215,14 @@ impl DictionaryService {
|
||||
.filter(|d| d.tenant_id == tenant_id && d.deleted_at.is_none())
|
||||
.ok_or_else(|| ConfigError::NotFound("字典不存在".to_string()))?;
|
||||
|
||||
let next_version =
|
||||
check_version(version, model.version).map_err(|_| ConfigError::VersionMismatch)?;
|
||||
|
||||
let mut active: dictionary::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
active
|
||||
.update(db)
|
||||
.await
|
||||
@@ -283,10 +298,12 @@ impl DictionaryService {
|
||||
value: req.value.clone(),
|
||||
sort_order,
|
||||
color: req.color.clone(),
|
||||
version: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update editable dictionary item fields (label, value, sort_order, color).
|
||||
/// Performs optimistic locking via version check.
|
||||
pub async fn update_item(
|
||||
item_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
@@ -301,6 +318,9 @@ impl DictionaryService {
|
||||
.filter(|i| i.tenant_id == tenant_id && i.deleted_at.is_none())
|
||||
.ok_or_else(|| ConfigError::NotFound("字典项不存在".to_string()))?;
|
||||
|
||||
let next_version =
|
||||
check_version(req.version, model.version).map_err(|_| ConfigError::VersionMismatch)?;
|
||||
|
||||
let mut active: dictionary_item::ActiveModel = model.into();
|
||||
|
||||
if let Some(l) = &req.label {
|
||||
@@ -318,6 +338,7 @@ impl DictionaryService {
|
||||
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
|
||||
let updated = active
|
||||
.update(db)
|
||||
@@ -331,14 +352,17 @@ impl DictionaryService {
|
||||
value: updated.value.clone(),
|
||||
sort_order: updated.sort_order,
|
||||
color: updated.color.clone(),
|
||||
version: updated.version,
|
||||
})
|
||||
}
|
||||
|
||||
/// Soft-delete a dictionary item by setting the `deleted_at` timestamp.
|
||||
/// Performs optimistic locking via version check.
|
||||
pub async fn delete_item(
|
||||
item_id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Uuid,
|
||||
version: i32,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> ConfigResult<()> {
|
||||
let model = dictionary_item::Entity::find_by_id(item_id)
|
||||
@@ -348,10 +372,14 @@ impl DictionaryService {
|
||||
.filter(|i| i.tenant_id == tenant_id && i.deleted_at.is_none())
|
||||
.ok_or_else(|| ConfigError::NotFound("字典项不存在".to_string()))?;
|
||||
|
||||
let next_version =
|
||||
check_version(version, model.version).map_err(|_| ConfigError::VersionMismatch)?;
|
||||
|
||||
let mut active: dictionary_item::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
active
|
||||
.update(db)
|
||||
.await
|
||||
@@ -405,6 +433,7 @@ impl DictionaryService {
|
||||
value: i.value.clone(),
|
||||
sort_order: i.sort_order,
|
||||
color: i.color.clone(),
|
||||
version: i.version,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use uuid::Uuid;
|
||||
use crate::dto::{CreateMenuReq, MenuResp};
|
||||
use crate::entity::{menu, menu_role};
|
||||
use crate::error::{ConfigError, ConfigResult};
|
||||
use erp_core::error::check_version;
|
||||
use erp_core::events::EventBus;
|
||||
|
||||
/// 菜单 CRUD 服务 -- 创建、查询(树形/平铺)、更新、软删除菜单,
|
||||
@@ -103,6 +104,7 @@ impl MenuService {
|
||||
menu_type: m.menu_type.clone(),
|
||||
permission: m.permission.clone(),
|
||||
children: vec![],
|
||||
version: m.version,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
@@ -165,10 +167,12 @@ impl MenuService {
|
||||
menu_type: req.menu_type.clone().unwrap_or_else(|| "menu".to_string()),
|
||||
permission: req.permission.clone(),
|
||||
children: vec![],
|
||||
version: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新菜单字段,并可选地重新关联角色。
|
||||
/// 使用乐观锁校验版本。
|
||||
pub async fn update(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
@@ -183,6 +187,9 @@ impl MenuService {
|
||||
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
|
||||
.ok_or_else(|| ConfigError::NotFound(format!("菜单不存在: {id}")))?;
|
||||
|
||||
let next_version =
|
||||
check_version(req.version, model.version).map_err(|_| ConfigError::VersionMismatch)?;
|
||||
|
||||
let mut active: menu::ActiveModel = model.into();
|
||||
|
||||
if let Some(title) = &req.title {
|
||||
@@ -206,6 +213,7 @@ impl MenuService {
|
||||
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
|
||||
let updated = active
|
||||
.update(db)
|
||||
@@ -228,14 +236,16 @@ impl MenuService {
|
||||
menu_type: updated.menu_type.clone(),
|
||||
permission: updated.permission.clone(),
|
||||
children: vec![],
|
||||
version: updated.version,
|
||||
})
|
||||
}
|
||||
|
||||
/// 软删除菜单。
|
||||
/// 软删除菜单。使用乐观锁校验版本。
|
||||
pub async fn delete(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Uuid,
|
||||
version: i32,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
event_bus: &EventBus,
|
||||
) -> ConfigResult<()> {
|
||||
@@ -246,10 +256,14 @@ impl MenuService {
|
||||
.filter(|m| m.tenant_id == tenant_id && m.deleted_at.is_none())
|
||||
.ok_or_else(|| ConfigError::NotFound(format!("菜单不存在: {id}")))?;
|
||||
|
||||
let next_version =
|
||||
check_version(version, model.version).map_err(|_| ConfigError::VersionMismatch)?;
|
||||
|
||||
let mut active: menu::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
active
|
||||
.update(db)
|
||||
.await
|
||||
@@ -348,6 +362,7 @@ impl MenuService {
|
||||
menu_type: m.menu_type.clone(),
|
||||
permission: m.permission.clone(),
|
||||
children: Self::build_tree(&children, children_map),
|
||||
version: m.version,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -8,6 +8,7 @@ use uuid::Uuid;
|
||||
use crate::dto::{CreateNumberingRuleReq, GenerateNumberResp, NumberingRuleResp};
|
||||
use crate::entity::numbering_rule;
|
||||
use crate::error::{ConfigError, ConfigResult};
|
||||
use erp_core::error::check_version;
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::types::Pagination;
|
||||
|
||||
@@ -118,10 +119,11 @@ impl NumberingService {
|
||||
separator: req.separator.clone().unwrap_or_else(|| "-".to_string()),
|
||||
reset_cycle: req.reset_cycle.clone().unwrap_or_else(|| "never".to_string()),
|
||||
last_reset_date: Some(Utc::now().date_naive().to_string()),
|
||||
version: 1,
|
||||
})
|
||||
}
|
||||
|
||||
/// 更新编号规则的可编辑字段。
|
||||
/// 更新编号规则的可编辑字段。使用乐观锁校验版本。
|
||||
pub async fn update(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
@@ -136,6 +138,9 @@ impl NumberingService {
|
||||
.filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none())
|
||||
.ok_or_else(|| ConfigError::NotFound(format!("编号规则不存在: {id}")))?;
|
||||
|
||||
let next_version =
|
||||
check_version(req.version, model.version).map_err(|_| ConfigError::VersionMismatch)?;
|
||||
|
||||
let mut active: numbering_rule::ActiveModel = model.into();
|
||||
|
||||
if let Some(name) = &req.name {
|
||||
@@ -159,6 +164,7 @@ impl NumberingService {
|
||||
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
|
||||
let updated = active
|
||||
.update(db)
|
||||
@@ -168,11 +174,12 @@ impl NumberingService {
|
||||
Ok(Self::model_to_resp(&updated))
|
||||
}
|
||||
|
||||
/// 软删除编号规则。
|
||||
/// 软删除编号规则。使用乐观锁校验版本。
|
||||
pub async fn delete(
|
||||
id: Uuid,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Uuid,
|
||||
version: i32,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
event_bus: &EventBus,
|
||||
) -> ConfigResult<()> {
|
||||
@@ -183,10 +190,14 @@ impl NumberingService {
|
||||
.filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none())
|
||||
.ok_or_else(|| ConfigError::NotFound(format!("编号规则不存在: {id}")))?;
|
||||
|
||||
let next_version =
|
||||
check_version(version, model.version).map_err(|_| ConfigError::VersionMismatch)?;
|
||||
|
||||
let mut active: numbering_rule::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
active
|
||||
.update(db)
|
||||
.await
|
||||
@@ -374,6 +385,7 @@ impl NumberingService {
|
||||
separator: m.separator.clone(),
|
||||
reset_cycle: m.reset_cycle.clone(),
|
||||
last_reset_date: m.last_reset_date.map(|d| d.to_string()),
|
||||
version: m.version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use uuid::Uuid;
|
||||
use crate::dto::SettingResp;
|
||||
use crate::entity::setting;
|
||||
use crate::error::{ConfigError, ConfigResult};
|
||||
use erp_core::error::check_version;
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::types::Pagination;
|
||||
|
||||
@@ -89,11 +90,17 @@ impl SettingService {
|
||||
.map_err(|e| ConfigError::Validation(e.to_string()))?;
|
||||
|
||||
if let Some(model) = existing {
|
||||
// Update existing record
|
||||
// Update existing record — 乐观锁校验
|
||||
let next_version = match params.version {
|
||||
Some(v) => check_version(v, model.version).map_err(|_| ConfigError::VersionMismatch)?,
|
||||
None => model.version + 1,
|
||||
};
|
||||
|
||||
let mut active: setting::ActiveModel = model.into();
|
||||
active.setting_value = Set(params.value.clone());
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
|
||||
let updated = active
|
||||
.update(db)
|
||||
@@ -181,12 +188,14 @@ impl SettingService {
|
||||
}
|
||||
|
||||
/// Soft-delete a setting by setting the `deleted_at` timestamp.
|
||||
/// Performs optimistic locking via version check.
|
||||
pub async fn delete(
|
||||
key: &str,
|
||||
scope: &str,
|
||||
scope_id: &Option<Uuid>,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Uuid,
|
||||
version: i32,
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
) -> ConfigResult<()> {
|
||||
let model = setting::Entity::find()
|
||||
@@ -205,10 +214,14 @@ impl SettingService {
|
||||
))
|
||||
})?;
|
||||
|
||||
let next_version =
|
||||
check_version(version, model.version).map_err(|_| ConfigError::VersionMismatch)?;
|
||||
|
||||
let mut active: setting::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(Utc::now()));
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_version);
|
||||
active
|
||||
.update(db)
|
||||
.await
|
||||
@@ -283,6 +296,7 @@ impl SettingService {
|
||||
scope_id: model.scope_id,
|
||||
setting_key: model.setting_key.clone(),
|
||||
setting_value: model.setting_value.clone(),
|
||||
version: model.version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user