Files
hms/crates/erp-config/src/dto.rs
iven 8a253a4910
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix: 低优先级收尾 — 图片上传/语言编辑/插件恢复/URL 编码
- P3-2: ArticleEditor 图片上传接入 /upload 端点 + 封面图上传按钮
- P4-3: recover_plugins 添加 tenant 日志 + 同 ID 去重保护
- P4-4: LanguageManager 编辑弹窗改为真实表单 (name 字段) + 后端 name 持久化
- P4-6: Settings API getSetting/updateSetting 添加 encodeURIComponent
2026-04-26 19:52:42 +08:00

671 lines
19 KiB
Rust

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>,
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<String>,
pub items: Vec<DictionaryItemResp>,
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<String>,
}
#[derive(Debug, Deserialize, ToSchema)]
pub struct UpdateDictionaryReq {
pub name: Option<String>,
pub description: Option<String>,
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<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>,
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<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>,
pub version: i32,
}
#[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>>,
pub version: i32,
}
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct BatchSaveMenusReq {
#[validate(length(min = 1, message = "菜单列表不能为空"), nested)]
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>>,
/// 乐观锁版本号。更新已有菜单时必填。
pub version: Option<i32>,
}
// --- 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,
pub version: i32,
}
#[derive(Debug, Deserialize, Validate, ToSchema)]
pub struct UpdateSettingReq {
pub setting_value: serde_json::Value,
/// 乐观锁版本号。更新已有设置时必填,创建新设置时忽略。
pub version: Option<i32>,
}
/// 内部参数结构体,用于减少 SettingService::set 的参数数量。
pub struct SetSettingParams {
pub key: String,
pub scope: String,
pub scope_id: Option<Uuid>,
pub value: serde_json::Value,
/// 乐观锁版本号。更新已有设置时用于校验。
pub version: Option<i32>,
}
// --- 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>,
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<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>,
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<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,
pub name: Option<String>,
}
#[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());
}
}