Files
nj/crates/erp-config/src/handler/dictionary_handler.rs
iven c539e6fd83 feat: initialize Nuanji (Warm Notes) project
- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin)
- Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs)
- Integrated erp-diary into workspace and erp-server
- Added DiaryModule registration in main.rs
- Added DiaryState FromRef in state.rs
- Diary routes mounted (empty routes, ready for implementation)
- Product design spec v1.2 preserved in docs/
- Implementation plan preserved in plans/

Cargo check: OK
Cargo test: OK (78+ base tests passing)
2026-05-31 20:52:19 +08:00

361 lines
11 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;
#[utoipa::path(
get,
path = "/api/v1/dictionaries",
params(Pagination),
responses(
(status = 200, description = "成功", body = ApiResponse<PaginatedResponse<DictionaryResp>>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "字典管理"
)]
/// 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,
})))
}
#[utoipa::path(
post,
path = "/api/v1/dictionaries",
request_body = CreateDictionaryReq,
responses(
(status = 200, description = "成功", body = ApiResponse<DictionaryResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "字典管理"
)]
/// 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)))
}
#[utoipa::path(
put,
path = "/api/v1/dictionaries/{id}",
params(("id" = Uuid, Path, description = "字典ID")),
request_body = UpdateDictionaryReq,
responses(
(status = 200, description = "成功", body = ApiResponse<DictionaryResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "字典管理"
)]
/// 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)))
}
#[utoipa::path(
delete,
path = "/api/v1/dictionaries/{id}",
params(("id" = Uuid, Path, description = "字典ID")),
request_body = DeleteVersionReq,
responses(
(status = 200, description = "成功"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "字典管理"
)]
/// 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()),
}))
}
#[utoipa::path(
get,
path = "/api/v1/dictionaries/items-by-code",
params(("code" = String, Query, description = "字典编码")),
responses(
(status = 200, description = "成功", body = ApiResponse<Vec<DictionaryItemResp>>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "字典管理"
)]
/// 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)))
}
#[utoipa::path(
post,
path = "/api/v1/dictionaries/{dict_id}/items",
params(("dict_id" = Uuid, Path, description = "字典ID")),
request_body = CreateDictionaryItemReq,
responses(
(status = 200, description = "成功", body = ApiResponse<DictionaryItemResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "字典管理"
)]
/// 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)))
}
#[utoipa::path(
put,
path = "/api/v1/dictionaries/{dict_id}/items/{item_id}",
params(
("dict_id" = Uuid, Path, description = "字典ID"),
("item_id" = Uuid, Path, description = "字典项ID"),
),
request_body = UpdateDictionaryItemReq,
responses(
(status = 200, description = "成功", body = ApiResponse<DictionaryItemResp>),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "字典管理"
)]
/// 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)))
}
#[utoipa::path(
delete,
path = "/api/v1/dictionaries/{dict_id}/items/{item_id}",
params(
("dict_id" = Uuid, Path, description = "字典ID"),
("item_id" = Uuid, Path, description = "字典项ID"),
),
request_body = DeleteVersionReq,
responses(
(status = 200, description = "成功"),
(status = 401, description = "未授权"),
(status = 403, description = "权限不足"),
),
security(("bearer_auth" = [])),
tag = "字典管理"
)]
/// 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, utoipa::ToSchema)]
pub struct DeleteVersionReq {
pub version: i32,
}