Skip to main content

yggdrasil/models/
user.rs

1//! 用户模型。
2//!
3//! 定义用户角色、内部用户结构体以及可暴露给前端的 PublicUser。
4//! User 包含密码哈希等敏感字段,PublicUser 用于在 API 中隐藏这些字段。
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9/// 用户角色枚举。
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub enum UserRole {
12    /// 管理员,拥有全部后台权限。
13    Admin,
14    /// 被禁用的用户,无法登录或操作。
15    Blocked,
16}
17
18impl UserRole {
19    /// 将数据库中的角色字符串解析为 UserRole,无法识别时返回 None。
20    #[cfg(feature = "server")]
21    pub fn from_str(s: &str) -> Option<Self> {
22        match s {
23            "admin" => Some(UserRole::Admin),
24            "blocked" => Some(UserRole::Blocked),
25            _ => None,
26        }
27    }
28}
29
30/// 会话缓存使用的轻量用户结构体,不含密码哈希。
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct SessionUser {
33    /// 用户主键。
34    pub id: i32,
35    /// 用户名。
36    pub username: String,
37    /// 邮箱地址。
38    pub email: String,
39    /// 对外展示名称(为空时 UI 回退展示 username)。
40    pub display_name: Option<String>,
41    /// 头像 URL(/uploads/ 素材路径或 http(s) 外链)。
42    pub avatar_url: Option<String>,
43    /// 用户角色。
44    pub role: UserRole,
45    /// 账户创建时间。
46    pub created_at: DateTime<Utc>,
47    /// 会话世代号,签发 session 时记录;与 users 表当前值不一致则 session 失效。
48    pub session_generation: i32,
49}
50
51/// 可公开的用户信息,从 User 转换而来,不含密码哈希。
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct PublicUser {
54    /// 用户主键。
55    pub id: i32,
56    /// 用户名。
57    pub username: String,
58    /// 邮箱地址。
59    pub email: String,
60    /// 对外展示名称(为空时 UI 回退展示 username)。
61    pub display_name: Option<String>,
62    /// 头像 URL(/uploads/ 素材路径或 http(s) 外链)。
63    pub avatar_url: Option<String>,
64    /// 用户角色。
65    pub role: UserRole,
66    /// 账户创建时间。
67    pub created_at: DateTime<Utc>,
68}
69
70impl From<SessionUser> for PublicUser {
71    /// 将 SessionUser 转换为 PublicUser。
72    fn from(u: SessionUser) -> Self {
73        PublicUser {
74            id: u.id,
75            username: u.username,
76            email: u.email,
77            display_name: u.display_name,
78            avatar_url: u.avatar_url,
79            role: u.role,
80            created_at: u.created_at,
81        }
82    }
83}
84
85impl PublicUser {
86    /// 对外展示名称:优先 display_name,未设置时回退 username。
87    pub fn display_label(&self) -> &str {
88        self.display_name.as_deref().unwrap_or(&self.username)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use chrono::{TimeZone, Utc};
96
97    fn sample_user() -> SessionUser {
98        SessionUser {
99            id: 1,
100            username: "admin".to_string(),
101            email: "admin@test.com".to_string(),
102            display_name: None,
103            avatar_url: None,
104            role: UserRole::Admin,
105            created_at: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
106            session_generation: 0,
107        }
108    }
109
110    #[test]
111    #[cfg(feature = "server")]
112    fn user_role_from_str() {
113        assert_eq!(UserRole::from_str("admin"), Some(UserRole::Admin));
114        assert_eq!(UserRole::from_str("blocked"), Some(UserRole::Blocked));
115        assert_eq!(UserRole::from_str("unknown"), None);
116        assert_eq!(UserRole::from_str(""), None);
117    }
118
119    #[test]
120    fn user_role_serde_roundtrip() {
121        let json = serde_json::to_string(&UserRole::Admin).unwrap();
122        assert_eq!(
123            serde_json::from_str::<UserRole>(&json).unwrap(),
124            UserRole::Admin
125        );
126    }
127
128    #[test]
129    fn session_user_to_public_user_conversion() {
130        // D2:User 结构体已删除(生产路径 auth.rs 直接从 row 构造 SessionUser),
131        // 仅保留 From<SessionUser> for PublicUser(auth.rs:431 在用)。
132        let session = sample_user();
133        let public: PublicUser = session.clone().into();
134        assert_eq!(public.id, session.id);
135        assert_eq!(public.username, session.username);
136        assert_eq!(public.email, session.email);
137        assert_eq!(public.role, session.role);
138        assert_eq!(public.created_at, session.created_at);
139    }
140
141    #[test]
142    fn public_user_excludes_session_generation() {
143        // PublicUser 不含 session_generation(敏感会话字段)。
144        let session = sample_user();
145        let public: PublicUser = session.into();
146        let json = serde_json::to_string(&public).unwrap();
147        assert!(!json.contains("session_generation"));
148    }
149
150    #[test]
151    fn display_label_falls_back_to_username() {
152        let session = sample_user();
153        let mut public: PublicUser = session.into();
154        // 未设置 display_name 时回退 username。
155        assert_eq!(public.display_label(), "admin");
156        // 设置后优先展示 display_name。
157        public.display_name = Some("站长".to_string());
158        assert_eq!(public.display_label(), "站长");
159    }
160}