- erp-auth/config/workflow/message/plugin/health: 44 处 DTO 校验缺失修复 - erp-plugin/data_dto: utoipa derive 宏 import 修复 - erp-server/main: tracing 宏类型推断修复 - web AuthButton: AiAnalysisCard/VitalSignsTab Button 包裹在 children 内 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
507 lines
13 KiB
Rust
507 lines
13 KiB
Rust
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, max = 128, message = "密码长度需在1-128之间"))]
|
|
pub password: String,
|
|
/// 客户端类型: "miniprogram" 允许患者角色登录
|
|
#[serde(default)]
|
|
pub client_type: Option<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<LoginResp>,
|
|
}
|
|
|
|
#[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,
|
|
}
|
|
|
|
/// 管理员重置用户密码请求
|
|
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
|
pub struct ResetPasswordReq {
|
|
#[validate(length(min = 6, max = 128, message = "新密码长度需在6-128之间"))]
|
|
pub new_password: String,
|
|
#[validate(range(min = 0))]
|
|
pub version: i32,
|
|
}
|
|
|
|
// --- User DTOs ---
|
|
|
|
#[derive(Debug, Serialize, ToSchema)]
|
|
pub struct UserResp {
|
|
pub id: Uuid,
|
|
pub username: String,
|
|
pub email: Option<String>,
|
|
pub phone: Option<String>,
|
|
pub display_name: Option<String>,
|
|
pub avatar_url: Option<String>,
|
|
pub status: String,
|
|
pub roles: Vec<RoleResp>,
|
|
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<String>,
|
|
#[validate(length(max = 20))]
|
|
pub phone: Option<String>,
|
|
#[validate(length(max = 100))]
|
|
pub display_name: Option<String>,
|
|
}
|
|
|
|
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, Validate, ToSchema)]
|
|
pub struct UpdateUserReq {
|
|
#[validate(email)]
|
|
pub email: Option<String>,
|
|
#[validate(length(max = 20))]
|
|
pub phone: Option<String>,
|
|
#[validate(length(max = 100))]
|
|
pub display_name: Option<String>,
|
|
#[validate(length(min = 1, max = 20))]
|
|
pub status: Option<String>,
|
|
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<String>,
|
|
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<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
|
pub struct UpdateRoleReq {
|
|
#[validate(length(min = 1, max = 50))]
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub version: i32,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
|
pub struct AssignRolesReq {
|
|
#[validate(length(min = 1, message = "至少需要分配一个角色"))]
|
|
pub role_ids: Vec<Uuid>,
|
|
}
|
|
|
|
// --- 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<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
|
pub struct AssignPermissionsReq {
|
|
#[validate(length(min = 1, message = "至少需要分配一个权限"))]
|
|
pub permission_ids: Vec<Uuid>,
|
|
}
|
|
|
|
// --- Organization DTOs ---
|
|
|
|
#[derive(Debug, Serialize, ToSchema)]
|
|
pub struct OrganizationResp {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
pub code: Option<String>,
|
|
pub parent_id: Option<Uuid>,
|
|
pub path: Option<String>,
|
|
pub level: i32,
|
|
pub sort_order: i32,
|
|
pub children: Vec<OrganizationResp>,
|
|
pub version: i32,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
|
pub struct CreateOrganizationReq {
|
|
#[validate(length(min = 1))]
|
|
pub name: String,
|
|
pub code: Option<String>,
|
|
pub parent_id: Option<Uuid>,
|
|
pub sort_order: Option<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, ToSchema)]
|
|
pub struct UpdateOrganizationReq {
|
|
pub name: Option<String>,
|
|
pub code: Option<String>,
|
|
pub sort_order: Option<i32>,
|
|
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<String>,
|
|
pub parent_id: Option<Uuid>,
|
|
pub manager_id: Option<Uuid>,
|
|
pub path: Option<String>,
|
|
pub sort_order: i32,
|
|
pub children: Vec<DepartmentResp>,
|
|
pub version: i32,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
|
pub struct CreateDepartmentReq {
|
|
#[validate(length(min = 1))]
|
|
pub name: String,
|
|
pub code: Option<String>,
|
|
pub parent_id: Option<Uuid>,
|
|
pub manager_id: Option<Uuid>,
|
|
pub sort_order: Option<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, ToSchema)]
|
|
pub struct UpdateDepartmentReq {
|
|
pub name: Option<String>,
|
|
pub code: Option<String>,
|
|
pub manager_id: Option<Uuid>,
|
|
pub sort_order: Option<i32>,
|
|
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<String>,
|
|
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<String>,
|
|
pub level: Option<i32>,
|
|
pub sort_order: Option<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, ToSchema)]
|
|
pub struct UpdatePositionReq {
|
|
pub name: Option<String>,
|
|
pub code: Option<String>,
|
|
pub level: Option<i32>,
|
|
pub sort_order: Option<i32>,
|
|
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(),
|
|
client_type: None,
|
|
};
|
|
assert!(req.validate().is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn login_req_empty_username_fails() {
|
|
let req = LoginReq {
|
|
username: "".to_string(),
|
|
password: "password123".to_string(),
|
|
client_type: None,
|
|
};
|
|
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(),
|
|
client_type: None,
|
|
};
|
|
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());
|
|
}
|
|
}
|