Files
hms/crates/erp-config/src/handler/dictionary_handler.rs
iven 5d6e1dc394 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.
2026-04-11 23:25:43 +08:00

274 lines
7.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use axum::Extension;
use axum::extract::{FromRef, Path, Query, State};
use axum::response::Json;
use validator::Validate;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, Pagination, TenantContext};
use uuid::Uuid;
use crate::config_state::ConfigState;
use crate::dto::{
CreateDictionaryItemReq, CreateDictionaryReq, DictionaryItemResp, DictionaryResp,
UpdateDictionaryItemReq, UpdateDictionaryReq,
};
use crate::service::dictionary_service::DictionaryService;
/// GET /api/v1/dictionaries
///
/// 分页查询当前租户下的字典列表。
/// 每个字典包含其关联的字典项。
/// 需要 `dictionary.list` 权限。
pub async fn list_dictionaries<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Query(pagination): Query<Pagination>,
) -> Result<Json<ApiResponse<PaginatedResponse<DictionaryResp>>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "dictionary.list")?;
let (dictionaries, total) =
DictionaryService::list(ctx.tenant_id, &pagination, &state.db).await?;
let page = pagination.page.unwrap_or(1);
let page_size = pagination.limit();
let total_pages = total.div_ceil(page_size);
Ok(Json(ApiResponse::ok(PaginatedResponse {
data: dictionaries,
total,
page,
page_size,
total_pages,
})))
}
/// POST /api/v1/dictionaries
///
/// 在当前租户下创建新字典。
/// 字典编码在租户内必须唯一。
/// 需要 `dictionary.create` 权限。
pub async fn create_dictionary<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<CreateDictionaryReq>,
) -> Result<Json<ApiResponse<DictionaryResp>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "dictionary.create")?;
req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
let dictionary = DictionaryService::create(
ctx.tenant_id,
ctx.user_id,
&req.name,
&req.code,
&req.description,
&state.db,
&state.event_bus,
)
.await?;
Ok(Json(ApiResponse::ok(dictionary)))
}
/// PUT /api/v1/dictionaries/:id
///
/// 更新字典的可编辑字段(名称、描述)。
/// 编码创建后不可更改。
/// 需要 `dictionary.update` 权限。
pub async fn update_dictionary<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(req): Json<UpdateDictionaryReq>,
) -> Result<Json<ApiResponse<DictionaryResp>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "dictionary.update")?;
let dictionary = DictionaryService::update(
id,
ctx.tenant_id,
ctx.user_id,
&req,
&state.db,
)
.await?;
Ok(Json(ApiResponse::ok(dictionary)))
}
/// 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>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "dictionary.delete")?;
DictionaryService::delete(
id,
ctx.tenant_id,
ctx.user_id,
req.version,
&state.db,
&state.event_bus,
)
.await?;
Ok(Json(ApiResponse {
success: true,
data: None,
message: Some("字典已删除".to_string()),
}))
}
/// GET /api/v1/dictionaries/items-by-code?code=xxx
///
/// 根据字典编码查询所有字典项。
/// 用于前端下拉框和枚举值查找。
/// 需要 `dictionary.list` 权限。
pub async fn list_items_by_code<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Query(query): Query<ItemsByCodeQuery>,
) -> Result<Json<ApiResponse<Vec<DictionaryItemResp>>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "dictionary.list")?;
let items =
DictionaryService::list_items_by_code(&query.code, ctx.tenant_id, &state.db).await?;
Ok(Json(ApiResponse::ok(items)))
}
/// POST /api/v1/dictionaries/:dict_id/items
///
/// 向指定字典添加新的字典项。
/// 字典项的 value 在同一字典内必须唯一。
/// 需要 `dictionary.create` 权限。
pub async fn create_item<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Path(dict_id): Path<Uuid>,
Json(req): Json<CreateDictionaryItemReq>,
) -> Result<Json<ApiResponse<DictionaryItemResp>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "dictionary.create")?;
req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
let item = DictionaryService::add_item(
dict_id,
ctx.tenant_id,
ctx.user_id,
&req,
&state.db,
)
.await?;
Ok(Json(ApiResponse::ok(item)))
}
/// PUT /api/v1/dictionaries/:dict_id/items/:item_id
///
/// 更新字典项的可编辑字段label、value、sort_order、color
/// 需要 `dictionary.update` 权限。
pub async fn update_item<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Path((dict_id, item_id)): Path<(Uuid, Uuid)>,
Json(req): Json<UpdateDictionaryItemReq>,
) -> Result<Json<ApiResponse<DictionaryItemResp>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "dictionary.update")?;
// 验证 item_id 属于 dict_id
let item = DictionaryService::update_item(
item_id,
ctx.tenant_id,
ctx.user_id,
&req,
&state.db,
)
.await?;
// 确保 item 属于指定的 dictionary
if item.dictionary_id != dict_id {
return Err(AppError::Validation(
"字典项不属于指定的字典".to_string(),
));
}
Ok(Json(ApiResponse::ok(item)))
}
/// 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>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "dictionary.delete")?;
DictionaryService::delete_item(item_id, ctx.tenant_id, ctx.user_id, req.version, &state.db)
.await?;
Ok(Json(ApiResponse {
success: true,
data: None,
message: Some("字典项已删除".to_string()),
}))
}
/// 按编码查询字典项的查询参数。
#[derive(Debug, serde::Deserialize)]
pub struct ItemsByCodeQuery {
pub code: String,
}
/// 删除操作的乐观锁版本号。
#[derive(Debug, serde::Deserialize)]
pub struct DeleteVersionReq {
pub version: i32,
}