use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; use validator::Validate; use erp_core::sanitize::{sanitize_option, sanitize_string}; // --- Auth DTOs --- #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct LoginReq { #[validate(length(min = 1, message = "用户名不能为空"))] pub username: String, #[validate(length(min = 1, message = "密码不能为空"))] pub password: String, } #[derive(Debug, Serialize, ToSchema)] pub struct LoginResp { pub access_token: String, pub refresh_token: String, pub expires_in: u64, pub user: UserResp, } #[derive(Debug, Deserialize, ToSchema)] pub struct RefreshReq { pub refresh_token: String, } // --- Wechat DTOs --- #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct WechatLoginReq { #[validate(length(min = 1, message = "code 不能为空"))] pub code: String, } #[derive(Debug, Serialize, ToSchema)] pub struct WechatLoginResp { pub bound: bool, pub openid: String, pub token: Option, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct WechatBindPhoneReq { #[validate(length(min = 1, message = "openid 不能为空"))] pub openid: String, #[validate(length(min = 1, message = "encrypted_data 不能为空"))] pub encrypted_data: String, #[validate(length(min = 1, message = "iv 不能为空"))] pub iv: String, } /// 修改密码请求 #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct ChangePasswordReq { #[validate(length(min = 1, message = "当前密码不能为空"))] pub current_password: String, #[validate(length(min = 6, max = 128, message = "新密码长度需在6-128之间"))] pub new_password: String, } // --- User DTOs --- #[derive(Debug, Serialize, ToSchema)] pub struct UserResp { pub id: Uuid, pub username: String, pub email: Option, pub phone: Option, pub display_name: Option, pub avatar_url: Option, pub status: String, pub roles: Vec, pub version: i32, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateUserReq { #[validate(length(min = 1, max = 50))] pub username: String, #[validate(length(min = 6, max = 128))] pub password: String, #[validate(email)] pub email: Option, #[validate(length(max = 20))] pub phone: Option, #[validate(length(max = 100))] pub display_name: Option, } impl CreateUserReq { /// 清理所有用户输入字段中的 HTML 标签,防止存储型 XSS。 pub fn sanitize(&mut self) { self.username = sanitize_string(&self.username); self.email = sanitize_option(self.email.take()); self.phone = sanitize_option(self.phone.take()); self.display_name = sanitize_option(self.display_name.take()); } } #[derive(Debug, Deserialize, ToSchema)] pub struct UpdateUserReq { pub email: Option, pub phone: Option, pub display_name: Option, pub status: Option, pub version: i32, } impl UpdateUserReq { /// 清理所有用户输入字段中的 HTML 标签,防止存储型 XSS。 pub fn sanitize(&mut self) { self.email = sanitize_option(self.email.take()); self.phone = sanitize_option(self.phone.take()); self.display_name = sanitize_option(self.display_name.take()); } } // --- Role DTOs --- #[derive(Debug, Clone, Serialize, ToSchema)] pub struct RoleResp { pub id: Uuid, pub name: String, pub code: String, pub description: Option, pub is_system: bool, pub version: i32, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateRoleReq { #[validate(length(min = 1, max = 50))] pub name: String, #[validate(length(min = 1, max = 50))] pub code: String, pub description: Option, } #[derive(Debug, Deserialize, ToSchema)] pub struct UpdateRoleReq { pub name: Option, pub description: Option, pub version: i32, } #[derive(Debug, Deserialize, ToSchema)] pub struct AssignRolesReq { pub role_ids: Vec, } // --- Permission DTOs --- #[derive(Debug, Serialize, ToSchema)] pub struct PermissionResp { pub id: Uuid, pub code: String, pub name: String, pub resource: String, pub action: String, pub description: Option, } #[derive(Debug, Deserialize, ToSchema)] pub struct AssignPermissionsReq { pub permission_ids: Vec, } // --- Organization DTOs --- #[derive(Debug, Serialize, ToSchema)] pub struct OrganizationResp { pub id: Uuid, pub name: String, pub code: Option, pub parent_id: Option, pub path: Option, pub level: i32, pub sort_order: i32, pub children: Vec, pub version: i32, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateOrganizationReq { #[validate(length(min = 1))] pub name: String, pub code: Option, pub parent_id: Option, pub sort_order: Option, } #[derive(Debug, Deserialize, ToSchema)] pub struct UpdateOrganizationReq { pub name: Option, pub code: Option, pub sort_order: Option, pub version: i32, } // --- Department DTOs --- #[derive(Debug, Serialize, ToSchema)] pub struct DepartmentResp { pub id: Uuid, pub org_id: Uuid, pub name: String, pub code: Option, pub parent_id: Option, pub manager_id: Option, pub path: Option, pub sort_order: i32, pub children: Vec, pub version: i32, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreateDepartmentReq { #[validate(length(min = 1))] pub name: String, pub code: Option, pub parent_id: Option, pub manager_id: Option, pub sort_order: Option, } #[derive(Debug, Deserialize, ToSchema)] pub struct UpdateDepartmentReq { pub name: Option, pub code: Option, pub manager_id: Option, pub sort_order: Option, pub version: i32, } // --- Position DTOs --- #[derive(Debug, Serialize, ToSchema)] pub struct PositionResp { pub id: Uuid, pub dept_id: Uuid, pub name: String, pub code: Option, pub level: i32, pub sort_order: i32, pub version: i32, } #[derive(Debug, Deserialize, Validate, ToSchema)] pub struct CreatePositionReq { #[validate(length(min = 1))] pub name: String, pub code: Option, pub level: Option, pub sort_order: Option, } #[derive(Debug, Deserialize, ToSchema)] pub struct UpdatePositionReq { pub name: Option, pub code: Option, pub level: Option, pub sort_order: Option, pub version: i32, } #[cfg(test)] mod tests { use super::*; use validator::Validate; #[test] fn login_req_valid() { let req = LoginReq { username: "admin".to_string(), password: "password123".to_string(), }; assert!(req.validate().is_ok()); } #[test] fn login_req_empty_username_fails() { let req = LoginReq { username: "".to_string(), password: "password123".to_string(), }; let result = req.validate(); assert!(result.is_err()); } #[test] fn change_password_req_valid() { let req = ChangePasswordReq { current_password: "oldPassword123".to_string(), new_password: "newPassword456".to_string(), }; assert!(req.validate().is_ok()); } #[test] fn change_password_req_empty_current_fails() { let req = ChangePasswordReq { current_password: "".to_string(), new_password: "newPassword456".to_string(), }; assert!(req.validate().is_err()); } #[test] fn change_password_req_short_new_fails() { let req = ChangePasswordReq { current_password: "oldPassword123".to_string(), new_password: "12345".to_string(), // min 6 }; assert!(req.validate().is_err()); } #[test] fn change_password_req_long_new_fails() { let req = ChangePasswordReq { current_password: "oldPassword123".to_string(), new_password: "a".repeat(129), // max 128 }; assert!(req.validate().is_err()); } #[test] fn login_req_empty_password_fails() { let req = LoginReq { username: "admin".to_string(), password: "".to_string(), }; assert!(req.validate().is_err()); } #[test] fn create_user_req_valid() { let req = CreateUserReq { username: "alice".to_string(), password: "secret123".to_string(), email: Some("alice@example.com".to_string()), phone: None, display_name: Some("Alice".to_string()), }; assert!(req.validate().is_ok()); } #[test] fn create_user_req_short_password_fails() { let req = CreateUserReq { username: "bob".to_string(), password: "12345".to_string(), // min 6 email: None, phone: None, display_name: None, }; assert!(req.validate().is_err()); } #[test] fn create_user_req_empty_username_fails() { let req = CreateUserReq { username: "".to_string(), password: "secret123".to_string(), email: None, phone: None, display_name: None, }; assert!(req.validate().is_err()); } #[test] fn create_user_req_invalid_email_fails() { let req = CreateUserReq { username: "charlie".to_string(), password: "secret123".to_string(), email: Some("not-an-email".to_string()), phone: None, display_name: None, }; assert!(req.validate().is_err()); } #[test] fn create_user_req_long_username_fails() { let req = CreateUserReq { username: "a".repeat(51), // max 50 password: "secret123".to_string(), email: None, phone: None, display_name: None, }; assert!(req.validate().is_err()); } #[test] fn create_role_req_valid() { let req = CreateRoleReq { name: "管理员".to_string(), code: "admin".to_string(), description: Some("系统管理员".to_string()), }; assert!(req.validate().is_ok()); } #[test] fn create_role_req_empty_name_fails() { let req = CreateRoleReq { name: "".to_string(), code: "admin".to_string(), description: None, }; assert!(req.validate().is_err()); } #[test] fn create_role_req_empty_code_fails() { let req = CreateRoleReq { name: "管理员".to_string(), code: "".to_string(), description: None, }; assert!(req.validate().is_err()); } #[test] fn create_org_req_valid() { let req = CreateOrganizationReq { name: "总部".to_string(), code: Some("HQ".to_string()), parent_id: None, sort_order: Some(0), }; assert!(req.validate().is_ok()); } #[test] fn create_org_req_empty_name_fails() { let req = CreateOrganizationReq { name: "".to_string(), code: None, parent_id: None, sort_order: None, }; assert!(req.validate().is_err()); } #[test] fn create_dept_req_valid() { let req = CreateDepartmentReq { name: "技术部".to_string(), code: Some("TECH".to_string()), parent_id: None, manager_id: None, sort_order: Some(1), }; assert!(req.validate().is_ok()); } #[test] fn create_position_req_valid() { let req = CreatePositionReq { name: "高级工程师".to_string(), code: Some("SENIOR".to_string()), level: Some(3), sort_order: None, }; assert!(req.validate().is_ok()); } #[test] fn create_position_req_empty_name_fails() { let req = CreatePositionReq { name: "".to_string(), code: None, level: None, sort_order: None, }; assert!(req.validate().is_err()); } }