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    // 返回值不是 Result(未知值回退/None),不能实现 std::str::FromStr。
22    #[allow(clippy::should_implement_trait)]
23    pub fn from_str(s: &str) -> Option<Self> {
24        match s {
25            "admin" => Some(UserRole::Admin),
26            "blocked" => Some(UserRole::Blocked),
27            _ => None,
28        }
29    }
30}
31
32/// 会话缓存使用的轻量用户结构体,不含密码哈希。
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct SessionUser {
35    /// 用户主键。
36    pub id: i32,
37    /// 用户名。
38    pub username: String,
39    /// 邮箱地址。
40    pub email: String,
41    /// 对外展示名称(为空时 UI 回退展示 username)。
42    pub display_name: Option<String>,
43    /// 头像 URL(/uploads/ 素材路径或 http(s) 外链)。
44    pub avatar_url: Option<String>,
45    /// 用户角色。
46    pub role: UserRole,
47    /// 账户创建时间。
48    #[cfg_attr(
49        target_arch = "wasm32",
50        serde(deserialize_with = "crate::models::rfc3339::deserialize")
51    )]
52    pub created_at: DateTime<Utc>,
53    /// 会话世代号,签发 session 时记录;与 users 表当前值不一致则 session 失效。
54    pub session_generation: i32,
55}
56
57/// 可公开的用户信息,从 User 转换而来,不含密码哈希。
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct PublicUser {
60    /// 用户主键。
61    pub id: i32,
62    /// 用户名。
63    pub username: String,
64    /// 邮箱地址。
65    pub email: String,
66    /// 对外展示名称(为空时 UI 回退展示 username)。
67    pub display_name: Option<String>,
68    /// 头像 URL(/uploads/ 素材路径或 http(s) 外链)。
69    pub avatar_url: Option<String>,
70    /// 用户角色。
71    pub role: UserRole,
72    /// 账户创建时间。
73    #[cfg_attr(
74        target_arch = "wasm32",
75        serde(deserialize_with = "crate::models::rfc3339::deserialize")
76    )]
77    pub created_at: DateTime<Utc>,
78}
79
80impl From<SessionUser> for PublicUser {
81    /// 将 SessionUser 转换为 PublicUser。
82    fn from(u: SessionUser) -> Self {
83        PublicUser {
84            id: u.id,
85            username: u.username,
86            email: u.email,
87            display_name: u.display_name,
88            avatar_url: u.avatar_url,
89            role: u.role,
90            created_at: u.created_at,
91        }
92    }
93}
94
95impl PublicUser {
96    /// 对外展示名称:优先 display_name,未设置时回退 username。
97    pub fn display_label(&self) -> &str {
98        self.display_name.as_deref().unwrap_or(&self.username)
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use chrono::{TimeZone, Utc};
106
107    fn sample_user() -> SessionUser {
108        SessionUser {
109            id: 1,
110            username: "admin".to_string(),
111            email: "admin@test.com".to_string(),
112            display_name: None,
113            avatar_url: None,
114            role: UserRole::Admin,
115            created_at: Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap(),
116            session_generation: 0,
117        }
118    }
119
120    #[test]
121    #[cfg(feature = "server")]
122    fn user_role_from_str() {
123        assert_eq!(UserRole::from_str("admin"), Some(UserRole::Admin));
124        assert_eq!(UserRole::from_str("blocked"), Some(UserRole::Blocked));
125        assert_eq!(UserRole::from_str("unknown"), None);
126        assert_eq!(UserRole::from_str(""), None);
127    }
128
129    #[test]
130    fn user_role_serde_roundtrip() {
131        let json = serde_json::to_string(&UserRole::Admin).unwrap();
132        assert_eq!(
133            serde_json::from_str::<UserRole>(&json).unwrap(),
134            UserRole::Admin
135        );
136    }
137
138    #[test]
139    fn session_user_to_public_user_conversion() {
140        // D2:User 结构体已删除(生产路径 auth.rs 直接从 row 构造 SessionUser),
141        // 仅保留 From<SessionUser> for PublicUser(auth.rs:431 在用)。
142        let session = sample_user();
143        let public: PublicUser = session.clone().into();
144        assert_eq!(public.id, session.id);
145        assert_eq!(public.username, session.username);
146        assert_eq!(public.email, session.email);
147        assert_eq!(public.role, session.role);
148        assert_eq!(public.created_at, session.created_at);
149    }
150
151    #[test]
152    fn public_user_excludes_session_generation() {
153        // PublicUser 不含 session_generation(敏感会话字段)。
154        let session = sample_user();
155        let public: PublicUser = session.into();
156        let json = serde_json::to_string(&public).unwrap();
157        assert!(!json.contains("session_generation"));
158    }
159
160    #[test]
161    fn display_label_falls_back_to_username() {
162        let session = sample_user();
163        let mut public: PublicUser = session.into();
164        // 未设置 display_name 时回退 username。
165        assert_eq!(public.display_label(), "admin");
166        // 设置后优先展示 display_name。
167        public.display_name = Some("站长".to_string());
168        assert_eq!(public.display_label(), "站长");
169    }
170}