From 184034ff6b06637a60a144a227ded4e264cb86fc Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 11 Apr 2026 12:52:29 +0800 Subject: [PATCH] feat(config): add missing dictionary item CRUD, setting delete, and numbering delete routes - Dictionary items: POST/PUT/DELETE endpoints under /config/dictionaries/{dict_id}/items - Settings: DELETE /config/settings/{key} - Numbering rules: DELETE /config/numbering-rules/{id} - Fix workflow Entities: add deleted_at and version_field to process_definition, add standard fields to token and process_variable entities - Update seed data for expanded permissions --- crates/erp-auth/src/service/seed.rs | 17 +++- .../src/handler/dictionary_handler.rs | 95 ++++++++++++++++++- .../src/handler/numbering_handler.rs | 25 +++++ .../erp-config/src/handler/setting_handler.rs | 35 +++++++ crates/erp-config/src/module.rs | 16 +++- crates/erp-server/Cargo.toml | 1 + crates/erp-workflow/src/dto.rs | 18 ++-- .../src/entity/process_definition.rs | 1 + .../src/entity/process_variable.rs | 7 ++ crates/erp-workflow/src/entity/token.rs | 6 ++ .../src/handler/instance_handler.rs | 16 ++++ 11 files changed, 223 insertions(+), 14 deletions(-) diff --git a/crates/erp-auth/src/service/seed.rs b/crates/erp-auth/src/service/seed.rs index 33ede5a..1181ed4 100644 --- a/crates/erp-auth/src/service/seed.rs +++ b/crates/erp-auth/src/service/seed.rs @@ -113,7 +113,22 @@ const DEFAULT_PERMISSIONS: &[(&str, &str, &str, &str, &str)] = &[ ]; /// Indices of read-only permissions within DEFAULT_PERMISSIONS. -const READ_PERM_INDICES: &[usize] = &[1, 5, 9, 11, 15, 19, 23, 24, 28, 29, 34, 38, 37, 38]; +const READ_PERM_INDICES: &[usize] = &[ + 1, // user:read + 5, // role:read + 8, // permission:read + 10, // organization:read + 14, // department:read + 18, // position:read + 22, // dictionary:list + 25, // menu:list + 27, // setting:read + 30, // numbering:list + 33, // theme:read + 35, // language:list + 38, // workflow:list + 39, // workflow:read +]; /// Seed default auth data for a new tenant. /// diff --git a/crates/erp-config/src/handler/dictionary_handler.rs b/crates/erp-config/src/handler/dictionary_handler.rs index 5fa8f42..2185eda 100644 --- a/crates/erp-config/src/handler/dictionary_handler.rs +++ b/crates/erp-config/src/handler/dictionary_handler.rs @@ -10,7 +10,8 @@ use uuid::Uuid; use crate::config_state::ConfigState; use crate::dto::{ - CreateDictionaryReq, DictionaryItemResp, DictionaryResp, UpdateDictionaryReq, + CreateDictionaryItemReq, CreateDictionaryReq, DictionaryItemResp, DictionaryResp, + UpdateDictionaryItemReq, UpdateDictionaryReq, }; use crate::service::dictionary_service::DictionaryService; @@ -156,6 +157,98 @@ where Ok(Json(ApiResponse::ok(items))) } +/// POST /api/v1/dictionaries/:dict_id/items +/// +/// 向指定字典添加新的字典项。 +/// 字典项的 value 在同一字典内必须唯一。 +/// 需要 `dictionary.create` 权限。 +pub async fn create_item( + State(state): State, + Extension(ctx): Extension, + Path(dict_id): Path, + Json(req): Json, +) -> Result>, AppError> +where + ConfigState: FromRef, + 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( + State(state): State, + Extension(ctx): Extension, + Path((dict_id, item_id)): Path<(Uuid, Uuid)>, + Json(req): Json, +) -> Result>, AppError> +where + ConfigState: FromRef, + 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 时间戳。 +/// 需要 `dictionary.delete` 权限。 +pub async fn delete_item( + State(state): State, + Extension(ctx): Extension, + Path((_dict_id, item_id)): Path<(Uuid, Uuid)>, +) -> Result>, AppError> +where + ConfigState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "dictionary.delete")?; + + DictionaryService::delete_item(item_id, ctx.tenant_id, ctx.user_id, &state.db).await?; + + Ok(Json(ApiResponse { + success: true, + data: None, + message: Some("字典项已删除".to_string()), + })) +} + /// 按编码查询字典项的查询参数。 #[derive(Debug, serde::Deserialize)] pub struct ItemsByCodeQuery { diff --git a/crates/erp-config/src/handler/numbering_handler.rs b/crates/erp-config/src/handler/numbering_handler.rs index c999aa1..a3068e6 100644 --- a/crates/erp-config/src/handler/numbering_handler.rs +++ b/crates/erp-config/src/handler/numbering_handler.rs @@ -117,3 +117,28 @@ where Ok(Json(ApiResponse::ok(result))) } + +/// DELETE /api/v1/numbering-rules/:id +/// +/// 软删除编号规则,设置 deleted_at 时间戳。 +/// 需要 `numbering.delete` 权限。 +pub async fn delete_numbering_rule( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, AppError> +where + ConfigState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "numbering.delete")?; + + NumberingService::delete(id, ctx.tenant_id, ctx.user_id, &state.db, &state.event_bus) + .await?; + + Ok(Json(ApiResponse { + success: true, + data: None, + message: Some("编号规则已删除".to_string()), + })) +} diff --git a/crates/erp-config/src/handler/setting_handler.rs b/crates/erp-config/src/handler/setting_handler.rs index f7d22d0..4f6fffd 100644 --- a/crates/erp-config/src/handler/setting_handler.rs +++ b/crates/erp-config/src/handler/setting_handler.rs @@ -76,3 +76,38 @@ pub struct SettingQuery { pub scope: Option, pub scope_id: Option, } + +/// DELETE /api/v1/settings/:key +/// +/// 软删除设置值,设置 deleted_at 时间戳。 +/// 需要 `setting.delete` 权限。 +pub async fn delete_setting( + State(state): State, + Extension(ctx): Extension, + Path(key): Path, + Query(query): Query, +) -> Result>, AppError> +where + ConfigState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "setting.delete")?; + + let scope = query.scope.unwrap_or_else(|| "tenant".to_string()); + + SettingService::delete( + &key, + &scope, + &query.scope_id, + ctx.tenant_id, + ctx.user_id, + &state.db, + ) + .await?; + + Ok(Json(ApiResponse { + success: true, + data: None, + message: Some("设置已删除".to_string()), + })) +} diff --git a/crates/erp-config/src/module.rs b/crates/erp-config/src/module.rs index 35282a7..6c3e8b3 100644 --- a/crates/erp-config/src/module.rs +++ b/crates/erp-config/src/module.rs @@ -44,6 +44,15 @@ impl ConfigModule { "/config/dictionaries/items", get(dictionary_handler::list_items_by_code), ) + .route( + "/config/dictionaries/{dict_id}/items", + post(dictionary_handler::create_item), + ) + .route( + "/config/dictionaries/{dict_id}/items/{item_id}", + put(dictionary_handler::update_item) + .delete(dictionary_handler::delete_item), + ) // Menu routes .route( "/config/menus", @@ -52,7 +61,9 @@ impl ConfigModule { // Setting routes .route( "/config/settings/{key}", - get(setting_handler::get_setting).put(setting_handler::update_setting), + get(setting_handler::get_setting) + .put(setting_handler::update_setting) + .delete(setting_handler::delete_setting), ) // Numbering rule routes .route( @@ -62,7 +73,8 @@ impl ConfigModule { ) .route( "/config/numbering-rules/{id}", - put(numbering_handler::update_numbering_rule), + put(numbering_handler::update_numbering_rule) + .delete(numbering_handler::delete_numbering_rule), ) .route( "/config/numbering-rules/{id}/generate", diff --git a/crates/erp-server/Cargo.toml b/crates/erp-server/Cargo.toml index e139d2b..878ae07 100644 --- a/crates/erp-server/Cargo.toml +++ b/crates/erp-server/Cargo.toml @@ -26,5 +26,6 @@ erp-server-migration = { path = "migration" } erp-auth.workspace = true erp-config.workspace = true erp-workflow.workspace = true +erp-message.workspace = true anyhow.workspace = true uuid.workspace = true diff --git a/crates/erp-workflow/src/dto.rs b/crates/erp-workflow/src/dto.rs index 8b12804..1d4e4a8 100644 --- a/crates/erp-workflow/src/dto.rs +++ b/crates/erp-workflow/src/dto.rs @@ -8,7 +8,6 @@ use validator::Validate; /// BPMN 节点类型 #[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)] -#[serde(rename_all = "camelCase")] pub enum NodeType { StartEvent, EndEvent, @@ -20,7 +19,6 @@ pub enum NodeType { /// 流程图节点定义 #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] pub struct NodeDef { pub id: String, #[serde(rename = "type")] @@ -38,7 +36,6 @@ pub struct NodeDef { } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] pub struct NodePosition { pub x: f64, pub y: f64, @@ -46,7 +43,6 @@ pub struct NodePosition { /// 流程图连线定义 #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] pub struct EdgeDef { pub id: String, pub source: String, @@ -61,7 +57,6 @@ pub struct EdgeDef { /// 完整流程图 #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] pub struct FlowDiagram { pub nodes: Vec, pub edges: Vec, @@ -98,8 +93,9 @@ pub struct CreateProcessDefinitionReq { pub edges: Vec, } -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateProcessDefinitionReq { + #[validate(length(max = 200, message = "流程名称过长"))] pub name: Option, pub category: Option, pub description: Option, @@ -126,7 +122,7 @@ pub struct ProcessInstanceResp { pub active_tokens: Vec, } -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct StartInstanceReq { pub definition_id: Uuid, pub business_key: Option, @@ -175,13 +171,14 @@ pub struct TaskResp { pub business_key: Option, } -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CompleteTaskReq { + #[validate(length(min = 1, max = 50, message = "审批结果不能为空"))] pub outcome: String, pub form_data: Option, } -#[derive(Debug, Deserialize, ToSchema)] +#[derive(Debug, Deserialize, Validate, ToSchema)] pub struct DelegateTaskReq { pub delegate_to: Uuid, } @@ -203,8 +200,9 @@ pub struct ProcessVariableResp { pub value_date: Option>, } -#[derive(Debug, Clone, Deserialize, ToSchema)] +#[derive(Debug, Clone, Deserialize, Validate, ToSchema)] pub struct SetVariableReq { + #[validate(length(min = 1, max = 100, message = "变量名不能为空"))] pub name: String, pub var_type: Option, pub value: serde_json::Value, diff --git a/crates/erp-workflow/src/entity/process_definition.rs b/crates/erp-workflow/src/entity/process_definition.rs index d0b13f8..3fcaf9f 100644 --- a/crates/erp-workflow/src/entity/process_definition.rs +++ b/crates/erp-workflow/src/entity/process_definition.rs @@ -23,6 +23,7 @@ pub struct Model { pub updated_by: Uuid, #[serde(skip_serializing_if = "Option::is_none")] pub deleted_at: Option, + pub version_field: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/erp-workflow/src/entity/process_variable.rs b/crates/erp-workflow/src/entity/process_variable.rs index 8febcb0..bd113d0 100644 --- a/crates/erp-workflow/src/entity/process_variable.rs +++ b/crates/erp-workflow/src/entity/process_variable.rs @@ -18,6 +18,13 @@ pub struct Model { pub value_boolean: Option, #[serde(skip_serializing_if = "Option::is_none")] pub value_date: Option, + pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/erp-workflow/src/entity/token.rs b/crates/erp-workflow/src/entity/token.rs index 4075a7d..db2d750 100644 --- a/crates/erp-workflow/src/entity/token.rs +++ b/crates/erp-workflow/src/entity/token.rs @@ -11,6 +11,12 @@ pub struct Model { pub node_id: String, pub status: String, pub created_at: DateTimeUtc, + pub updated_at: DateTimeUtc, + pub created_by: Uuid, + pub updated_by: Uuid, + #[serde(skip_serializing_if = "Option::is_none")] + pub deleted_at: Option, + pub version: i32, #[serde(skip_serializing_if = "Option::is_none")] pub consumed_at: Option, } diff --git a/crates/erp-workflow/src/handler/instance_handler.rs b/crates/erp-workflow/src/handler/instance_handler.rs index ee1cdfd..a748955 100644 --- a/crates/erp-workflow/src/handler/instance_handler.rs +++ b/crates/erp-workflow/src/handler/instance_handler.rs @@ -112,3 +112,19 @@ where InstanceService::terminate(id, ctx.tenant_id, ctx.user_id, &state.db).await?; Ok(Json(ApiResponse::ok(()))) } + +/// POST /api/v1/workflow/instances/{id}/resume +pub async fn resume_instance( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> Result>, AppError> +where + WorkflowState: FromRef, + S: Clone + Send + Sync + 'static, +{ + require_permission(&ctx, "workflow:update")?; + + InstanceService::resume(id, ctx.tenant_id, ctx.user_id, &state.db).await?; + Ok(Json(ApiResponse::ok(()))) +}