Skip to main content

yggdrasil/models/
comment.rs

1//! 评论模型。
2//!
3//! 定义评论状态、服务端内部使用的 Comment 结构体,
4//! 以及面向前端展示的 PublicComment 与面向后台管理的 AdminComment。
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9/// 评论审核状态枚举,序列化时使用小写字符串。
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11#[serde(rename_all = "lowercase")]
12pub enum CommentStatus {
13    /// 待审核。
14    Pending,
15    /// 已通过。
16    Approved,
17    /// 垃圾评论。
18    Spam,
19    /// 已删除/回收站。
20    Trash,
21}
22
23impl CommentStatus {
24    /// 将数据库或 API 中的状态字符串解析为 CommentStatus,未知值默认回退到 Pending。
25    #[cfg(feature = "server")]
26    pub fn from_str(s: &str) -> Self {
27        match s {
28            "approved" => Self::Approved,
29            "spam" => Self::Spam,
30            "trash" => Self::Trash,
31            _ => Self::Pending,
32        }
33    }
34
35    /// 将 CommentStatus 序列化为小写字符串。
36    #[cfg(test)]
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            Self::Pending => "pending",
40            Self::Approved => "approved",
41            Self::Spam => "spam",
42            Self::Trash => "trash",
43        }
44    }
45}
46
47/// 面向前端展示的评论结构体,已脱敏。
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49pub struct PublicComment {
50    /// 评论主键。
51    pub id: i64,
52    /// 父评论主键,None 表示顶层评论。
53    pub parent_id: Option<i64>,
54    /// 嵌套深度。
55    pub depth: i32,
56    /// 评论者名称。
57    pub author_name: String,
58    /// 评论者个人主页 URL。
59    pub author_url: Option<String>,
60    /// 评论者头像 URL。
61    pub avatar_url: String,
62    /// 渲染后的 HTML 内容。
63    pub content_html: Option<String>,
64    /// 用于展示的人类可读创建时间。
65    pub created_at: String,
66    /// ISO 8601 格式的创建时间。
67    pub created_at_iso: String,
68}
69
70/// 面向后台管理的评论结构体,包含审核所需字段。
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72pub struct AdminComment {
73    /// 评论主键。
74    pub id: i64,
75    /// 所属文章主键。
76    pub post_id: i32,
77    /// 所属文章标题。
78    pub post_title: String,
79    /// 所属文章 slug。
80    pub post_slug: String,
81    /// 父评论主键。
82    pub parent_id: Option<i64>,
83    /// 嵌套深度。
84    pub depth: i32,
85    /// 评论者名称。
86    pub author_name: String,
87    /// 评论者邮箱。
88    pub author_email: String,
89    /// 评论者个人主页 URL。
90    pub author_url: Option<String>,
91    /// 评论者头像 URL。
92    pub avatar_url: String,
93    /// 原始 Markdown 内容。
94    pub content_md: String,
95    /// 当前审核状态。
96    pub status: CommentStatus,
97    /// 评论创建时间。
98    pub created_at: DateTime<Utc>,
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    #[cfg(feature = "server")]
107    fn comment_status_from_str() {
108        assert_eq!(CommentStatus::from_str("pending"), CommentStatus::Pending);
109        assert_eq!(CommentStatus::from_str("approved"), CommentStatus::Approved);
110        assert_eq!(CommentStatus::from_str("spam"), CommentStatus::Spam);
111        assert_eq!(CommentStatus::from_str("trash"), CommentStatus::Trash);
112    }
113
114    #[test]
115    #[cfg(feature = "server")]
116    fn comment_status_from_str_unknown_defaults_to_pending() {
117        assert_eq!(CommentStatus::from_str("unknown"), CommentStatus::Pending);
118        assert_eq!(CommentStatus::from_str(""), CommentStatus::Pending);
119    }
120
121    #[test]
122    fn comment_status_as_str() {
123        assert_eq!(CommentStatus::Pending.as_str(), "pending");
124        assert_eq!(CommentStatus::Approved.as_str(), "approved");
125        assert_eq!(CommentStatus::Spam.as_str(), "spam");
126        assert_eq!(CommentStatus::Trash.as_str(), "trash");
127    }
128
129    #[test]
130    fn comment_status_serde_roundtrip() {
131        let statuses = vec![
132            CommentStatus::Pending,
133            CommentStatus::Approved,
134            CommentStatus::Spam,
135            CommentStatus::Trash,
136        ];
137        for status in statuses {
138            let json = serde_json::to_string(&status).unwrap();
139            let expected = format!("\"{}\"", status.as_str());
140            assert_eq!(json, expected);
141            let deserialized: CommentStatus = serde_json::from_str(&json).unwrap();
142            assert_eq!(deserialized, status);
143        }
144    }
145
146    #[test]
147    fn comment_status_deserialize_from_lowercase() {
148        let pending: CommentStatus = serde_json::from_str("\"pending\"").unwrap();
149        assert_eq!(pending, CommentStatus::Pending);
150        let approved: CommentStatus = serde_json::from_str("\"approved\"").unwrap();
151        assert_eq!(approved, CommentStatus::Approved);
152    }
153}