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, pub version: i32, } #[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, pub items: Vec, pub version: i32, } #[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, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateDictionaryReq { #[validate(length(min = 1, max = 100, message = "字典名称不能为空且不超过100字符"))] pub name: Option, pub description: Option, pub version: i32, } #[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, pub color: Option, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateDictionaryItemReq { #[validate(length(min = 1, max = 100, message = "标签不能为空且不超过100字符"))] pub label: Option, #[validate(length(min = 1, max = 100, message = "值不能为空且不超过100字符"))] pub value: Option, pub sort_order: Option, pub color: Option, pub version: i32, } // --- Menu DTOs --- #[derive(Debug, Serialize, ToSchema, Clone)] pub struct MenuResp { pub id: Uuid, #[serde(skip_serializing_if = "Option::is_none")] pub parent_id: Option, pub title: String, #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option, pub sort_order: i32, pub visible: bool, pub menu_type: String, #[serde(skip_serializing_if = "Option::is_none")] pub permission: Option, pub children: Vec, pub version: i32, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateMenuReq { pub parent_id: Option, #[validate(length(min = 1, max = 100, message = "菜单标题不能为空"))] pub title: String, pub path: Option, pub icon: Option, pub sort_order: Option, pub visible: Option, #[validate(length(min = 1, message = "菜单类型不能为空"))] pub menu_type: Option, pub permission: Option, pub role_ids: Option>, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateMenuReq { #[validate(length(min = 1, max = 100, message = "菜单标题不能为空且不超过100字符"))] pub title: Option, pub path: Option, pub icon: Option, pub sort_order: Option, pub visible: Option, pub permission: Option, pub role_ids: Option>, pub version: i32, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct BatchSaveMenusReq { #[validate(length(min = 1, message = "菜单列表不能为空"), nested)] pub menus: Vec, } #[derive(Debug, Serialize, Deserialize, Validate, ToSchema)] pub struct MenuItemReq { pub id: Option, pub parent_id: Option, #[validate(length(min = 1, max = 100, message = "菜单标题不能为空"))] pub title: String, pub path: Option, pub icon: Option, pub sort_order: Option, pub visible: Option, pub menu_type: Option, pub permission: Option, pub role_ids: Option>, /// 乐观锁版本号。更新已有菜单时必填。 pub version: Option, } // --- 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, pub setting_key: String, pub setting_value: serde_json::Value, pub version: i32, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateSettingReq { pub setting_value: serde_json::Value, /// 乐观锁版本号。更新已有设置时必填,创建新设置时忽略。 pub version: Option, } /// 内部参数结构体,用于减少 SettingService::set 的参数数量。 pub struct SetSettingParams { pub key: String, pub scope: String, pub scope_id: Option, pub value: serde_json::Value, /// 乐观锁版本号。更新已有设置时用于校验。 pub version: Option, } // --- 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, 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, pub version: i32, } #[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, pub date_format: Option, pub seq_length: Option, pub seq_start: Option, pub separator: Option, pub reset_cycle: Option, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct UpdateNumberingRuleReq { #[validate(length(min = 1, max = 100, message = "规则名称不能为空且不超过100字符"))] pub name: Option, pub prefix: Option, pub date_format: Option, pub seq_length: Option, pub separator: Option, pub reset_cycle: Option, pub version: i32, } #[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, #[serde(skip_serializing_if = "Option::is_none")] pub logo_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sidebar_style: Option, #[serde(skip_serializing_if = "Option::is_none")] pub brand_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub brand_slogan: Option, #[serde(skip_serializing_if = "Option::is_none")] pub brand_features: Option, #[serde(skip_serializing_if = "Option::is_none")] pub brand_copyright: Option, } /// 品牌信息公开响应(不含内部配置) #[derive(Debug, Serialize, Deserialize, ToSchema, Clone)] pub struct PublicBrandResp { pub brand_name: String, pub brand_slogan: String, pub brand_features: String, pub brand_copyright: 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, Validate, ToSchema)] pub struct UpdateLanguageReq { pub is_active: bool, #[validate(length(min = 1, max = 100, message = "语言名称不能为空且不超过100字符"))] pub name: Option, } #[cfg(test)] mod tests { use super::*; use validator::Validate; // ---- CreateDictionaryReq 验证 ---- #[test] fn create_dictionary_req_valid() { let req = CreateDictionaryReq { name: "状态字典".to_string(), code: "status".to_string(), description: Some("通用状态".to_string()), }; assert!(req.validate().is_ok()); } #[test] fn create_dictionary_req_empty_name_fails() { let req = CreateDictionaryReq { name: "".to_string(), code: "status".to_string(), description: None, }; assert!(req.validate().is_err()); } #[test] fn create_dictionary_req_empty_code_fails() { let req = CreateDictionaryReq { name: "状态字典".to_string(), code: "".to_string(), description: None, }; assert!(req.validate().is_err()); } #[test] fn create_dictionary_req_name_too_long_fails() { let req = CreateDictionaryReq { name: "x".repeat(101), code: "status".to_string(), description: None, }; assert!(req.validate().is_err()); } #[test] fn create_dictionary_req_code_too_long_fails() { let req = CreateDictionaryReq { name: "状态字典".to_string(), code: "x".repeat(51), description: None, }; assert!(req.validate().is_err()); } #[test] fn create_dictionary_req_max_boundary_ok() { let req = CreateDictionaryReq { name: "x".repeat(100), code: "x".repeat(50), description: None, }; assert!(req.validate().is_ok()); } // ---- CreateDictionaryItemReq 验证 ---- #[test] fn create_dictionary_item_req_valid() { let req = CreateDictionaryItemReq { label: "启用".to_string(), value: "active".to_string(), sort_order: Some(1), color: Some("#00FF00".to_string()), }; assert!(req.validate().is_ok()); } #[test] fn create_dictionary_item_req_empty_label_fails() { let req = CreateDictionaryItemReq { label: "".to_string(), value: "active".to_string(), sort_order: None, color: None, }; assert!(req.validate().is_err()); } #[test] fn create_dictionary_item_req_empty_value_fails() { let req = CreateDictionaryItemReq { label: "启用".to_string(), value: "".to_string(), sort_order: None, color: None, }; assert!(req.validate().is_err()); } #[test] fn create_dictionary_item_req_label_too_long_fails() { let req = CreateDictionaryItemReq { label: "x".repeat(101), value: "active".to_string(), sort_order: None, color: None, }; assert!(req.validate().is_err()); } #[test] fn create_dictionary_item_req_value_too_long_fails() { let req = CreateDictionaryItemReq { label: "启用".to_string(), value: "x".repeat(101), sort_order: None, color: None, }; assert!(req.validate().is_err()); } #[test] fn create_dictionary_item_req_min_boundary_ok() { let req = CreateDictionaryItemReq { label: "x".to_string(), value: "x".to_string(), sort_order: None, color: None, }; assert!(req.validate().is_ok()); } #[test] fn create_dictionary_item_req_max_boundary_ok() { let req = CreateDictionaryItemReq { label: "x".repeat(100), value: "x".repeat(100), sort_order: Some(99), color: Some("#FFFFFF".to_string()), }; assert!(req.validate().is_ok()); } // ---- CreateMenuReq 验证 ---- #[test] fn create_menu_req_valid() { let req = CreateMenuReq { parent_id: None, title: "系统设置".to_string(), path: Some("/settings".to_string()), icon: Some("SettingOutlined".to_string()), sort_order: Some(1), visible: Some(true), menu_type: Some("menu".to_string()), permission: None, role_ids: None, }; assert!(req.validate().is_ok()); } #[test] fn create_menu_req_empty_title_fails() { let req = CreateMenuReq { parent_id: None, title: "".to_string(), path: None, icon: None, sort_order: None, visible: None, menu_type: None, permission: None, role_ids: None, }; assert!(req.validate().is_err()); } #[test] fn create_menu_req_title_too_long_fails() { let req = CreateMenuReq { parent_id: None, title: "x".repeat(101), path: None, icon: None, sort_order: None, visible: None, menu_type: None, permission: None, role_ids: None, }; assert!(req.validate().is_err()); } #[test] fn create_menu_req_title_max_boundary_ok() { let req = CreateMenuReq { parent_id: None, title: "x".repeat(100), path: None, icon: None, sort_order: None, visible: None, menu_type: None, permission: None, role_ids: None, }; assert!(req.validate().is_ok()); } // ---- BatchSaveMenusReq 验证 ---- #[test] fn batch_save_menus_req_valid() { let req = BatchSaveMenusReq { menus: vec![MenuItemReq { id: None, parent_id: None, title: "首页".to_string(), path: Some("/home".to_string()), icon: None, sort_order: Some(0), visible: Some(true), menu_type: Some("menu".to_string()), permission: None, role_ids: None, version: None, }], }; assert!(req.validate().is_ok()); } #[test] fn batch_save_menus_req_empty_list_fails() { let req = BatchSaveMenusReq { menus: vec![] }; assert!(req.validate().is_err()); } #[test] fn batch_save_menus_req_item_empty_title_fails() { let req = BatchSaveMenusReq { menus: vec![MenuItemReq { id: None, parent_id: None, title: "".to_string(), path: None, icon: None, sort_order: None, visible: None, menu_type: None, permission: None, role_ids: None, version: None, }], }; assert!(req.validate().is_err()); } #[test] fn batch_save_menus_req_item_title_too_long_fails() { let req = BatchSaveMenusReq { menus: vec![MenuItemReq { id: None, parent_id: None, title: "x".repeat(101), path: None, icon: None, sort_order: None, visible: None, menu_type: None, permission: None, role_ids: None, version: None, }], }; assert!(req.validate().is_err()); } #[test] fn batch_save_menus_req_multiple_items_ok() { let req = BatchSaveMenusReq { menus: vec![ MenuItemReq { id: None, parent_id: None, title: "菜单A".to_string(), path: Some("/a".to_string()), icon: None, sort_order: Some(0), visible: Some(true), menu_type: Some("menu".to_string()), permission: None, role_ids: None, version: None, }, MenuItemReq { id: None, parent_id: None, title: "菜单B".to_string(), path: Some("/b".to_string()), icon: None, sort_order: Some(1), visible: Some(true), menu_type: Some("menu".to_string()), permission: None, role_ids: None, version: Some(1), }, ], }; assert!(req.validate().is_ok()); } // ---- CreateNumberingRuleReq 验证 ---- #[test] fn create_numbering_rule_req_valid() { let req = CreateNumberingRuleReq { name: "订单编号".to_string(), code: "ORDER".to_string(), prefix: Some("ORD".to_string()), date_format: Some("%Y%m%d".to_string()), seq_length: Some(4), seq_start: Some(1), separator: Some("-".to_string()), reset_cycle: Some("daily".to_string()), }; assert!(req.validate().is_ok()); } #[test] fn create_numbering_rule_req_empty_name_fails() { let req = CreateNumberingRuleReq { name: "".to_string(), code: "ORDER".to_string(), prefix: None, date_format: None, seq_length: None, seq_start: None, separator: None, reset_cycle: None, }; assert!(req.validate().is_err()); } #[test] fn create_numbering_rule_req_empty_code_fails() { let req = CreateNumberingRuleReq { name: "订单编号".to_string(), code: "".to_string(), prefix: None, date_format: None, seq_length: None, seq_start: None, separator: None, reset_cycle: None, }; assert!(req.validate().is_err()); } #[test] fn create_numbering_rule_req_name_too_long_fails() { let req = CreateNumberingRuleReq { name: "x".repeat(101), code: "ORDER".to_string(), prefix: None, date_format: None, seq_length: None, seq_start: None, separator: None, reset_cycle: None, }; assert!(req.validate().is_err()); } #[test] fn create_numbering_rule_req_code_too_long_fails() { let req = CreateNumberingRuleReq { name: "订单编号".to_string(), code: "x".repeat(51), prefix: None, date_format: None, seq_length: None, seq_start: None, separator: None, reset_cycle: None, }; assert!(req.validate().is_err()); } #[test] fn create_numbering_rule_req_max_boundary_ok() { let req = CreateNumberingRuleReq { name: "x".repeat(100), code: "x".repeat(50), prefix: None, date_format: None, seq_length: None, seq_start: None, separator: None, reset_cycle: None, }; assert!(req.validate().is_ok()); } // ---- UpdateSettingReq 验证 ---- #[test] fn update_setting_req_valid() { let req = UpdateSettingReq { setting_value: serde_json::json!({"key": "value"}), version: Some(1), }; assert!(req.validate().is_ok()); } #[test] fn update_setting_req_without_version_ok() { let req = UpdateSettingReq { setting_value: serde_json::json!("hello"), version: None, }; assert!(req.validate().is_ok()); } }