feat(config): add system configuration module (Phase 3)
Implement the complete erp-config crate with: - Data dictionaries (CRUD + items management) - Dynamic menus (tree structure with role filtering) - System settings (hierarchical: platform > tenant > org > user) - Numbering rules (concurrency-safe via PostgreSQL advisory_lock) - Theme and language configuration (via settings store) - 6 database migrations (dictionaries, menus, settings, numbering_rules) - Frontend Settings page with 5 tabs (dictionary, menu, numbering, settings, theme) Refactor: move RBAC functions (require_permission) from erp-auth to erp-core to avoid cross-module dependencies. Add 20 new seed permissions for config module operations.
This commit is contained in:
218
crates/erp-config/src/dto.rs
Normal file
218
crates/erp-config/src/dto.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
use validator::Validate;
|
||||
|
||||
// --- Dictionary DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct DictionaryItemResp {
|
||||
pub id: Uuid,
|
||||
pub dictionary_id: Uuid,
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
pub sort_order: i32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct DictionaryResp {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub code: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
pub items: Vec<DictionaryItemResp>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateDictionaryReq {
|
||||
#[validate(length(min = 1, max = 100, message = "字典名称不能为空"))]
|
||||
pub name: String,
|
||||
#[validate(length(min = 1, max = 50, message = "字典编码不能为空"))]
|
||||
pub code: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateDictionaryReq {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateDictionaryItemReq {
|
||||
#[validate(length(min = 1, max = 100, message = "标签不能为空"))]
|
||||
pub label: String,
|
||||
#[validate(length(min = 1, max = 100, message = "值不能为空"))]
|
||||
pub value: String,
|
||||
pub sort_order: Option<i32>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateDictionaryItemReq {
|
||||
pub label: Option<String>,
|
||||
pub value: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
// --- Menu DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema, Clone)]
|
||||
pub struct MenuResp {
|
||||
pub id: Uuid,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_id: Option<Uuid>,
|
||||
pub title: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<String>,
|
||||
pub sort_order: i32,
|
||||
pub visible: bool,
|
||||
pub menu_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub permission: Option<String>,
|
||||
pub children: Vec<MenuResp>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateMenuReq {
|
||||
pub parent_id: Option<Uuid>,
|
||||
#[validate(length(min = 1, max = 100, message = "菜单标题不能为空"))]
|
||||
pub title: String,
|
||||
pub path: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
pub visible: Option<bool>,
|
||||
#[validate(length(min = 1, message = "菜单类型不能为空"))]
|
||||
pub menu_type: Option<String>,
|
||||
pub permission: Option<String>,
|
||||
pub role_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateMenuReq {
|
||||
pub title: Option<String>,
|
||||
pub path: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
pub visible: Option<bool>,
|
||||
pub permission: Option<String>,
|
||||
pub role_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct BatchSaveMenusReq {
|
||||
#[validate(length(min = 1, message = "菜单列表不能为空"))]
|
||||
pub menus: Vec<MenuItemReq>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct MenuItemReq {
|
||||
pub id: Option<Uuid>,
|
||||
pub parent_id: Option<Uuid>,
|
||||
#[validate(length(min = 1, max = 100, message = "菜单标题不能为空"))]
|
||||
pub title: String,
|
||||
pub path: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub sort_order: Option<i32>,
|
||||
pub visible: Option<bool>,
|
||||
pub menu_type: Option<String>,
|
||||
pub permission: Option<String>,
|
||||
pub role_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
// --- Setting DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct SettingResp {
|
||||
pub id: Uuid,
|
||||
pub scope: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub scope_id: Option<Uuid>,
|
||||
pub setting_key: String,
|
||||
pub setting_value: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateSettingReq {
|
||||
pub setting_value: serde_json::Value,
|
||||
}
|
||||
|
||||
// --- Numbering Rule DTOs ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct NumberingRuleResp {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub code: String,
|
||||
pub prefix: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub date_format: Option<String>,
|
||||
pub seq_length: i32,
|
||||
pub seq_start: i32,
|
||||
pub seq_current: i64,
|
||||
pub separator: String,
|
||||
pub reset_cycle: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_reset_date: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateNumberingRuleReq {
|
||||
#[validate(length(min = 1, max = 100, message = "规则名称不能为空"))]
|
||||
pub name: String,
|
||||
#[validate(length(min = 1, max = 50, message = "规则编码不能为空"))]
|
||||
pub code: String,
|
||||
pub prefix: Option<String>,
|
||||
pub date_format: Option<String>,
|
||||
pub seq_length: Option<i32>,
|
||||
pub seq_start: Option<i32>,
|
||||
pub separator: Option<String>,
|
||||
pub reset_cycle: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateNumberingRuleReq {
|
||||
pub name: Option<String>,
|
||||
pub prefix: Option<String>,
|
||||
pub date_format: Option<String>,
|
||||
pub seq_length: Option<i32>,
|
||||
pub separator: Option<String>,
|
||||
pub reset_cycle: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct GenerateNumberResp {
|
||||
pub number: String,
|
||||
}
|
||||
|
||||
// --- Theme DTOs (stored via settings) ---
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)]
|
||||
pub struct ThemeResp {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub primary_color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logo_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sidebar_style: Option<String>,
|
||||
}
|
||||
|
||||
// --- Language DTOs (stored via settings) ---
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct LanguageResp {
|
||||
pub code: String,
|
||||
pub name: String,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateLanguageReq {
|
||||
pub is_active: bool,
|
||||
}
|
||||
Reference in New Issue
Block a user