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.
274 lines
7.4 KiB
Rust
274 lines
7.4 KiB
Rust
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,
|
||
}
|