Skip to main content

yggdrasil/models/
public_post.rs

1//! 阅读页传输模型。编辑器和 MCP 继续使用完整 Post,避免向每位读者重复发送 Markdown。
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use super::post::{Post, PostNav, PostStatus};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub struct PublicPost {
10    pub id: i32,
11    pub author_id: i32,
12    pub title: String,
13    pub slug: String,
14    pub summary: Option<String>,
15    pub content_html: Option<String>,
16    #[cfg_attr(
17        target_arch = "wasm32",
18        serde(
19            default,
20            deserialize_with = "crate::models::rfc3339::deserialize_option"
21        )
22    )]
23    pub published_at: Option<DateTime<Utc>>,
24    #[cfg_attr(
25        target_arch = "wasm32",
26        serde(deserialize_with = "crate::models::rfc3339::deserialize")
27    )]
28    pub created_at: DateTime<Utc>,
29    #[cfg_attr(
30        target_arch = "wasm32",
31        serde(deserialize_with = "crate::models::rfc3339::deserialize")
32    )]
33    pub updated_at: DateTime<Utc>,
34    pub tags: Vec<String>,
35    pub cover_image: Option<String>,
36    pub reading_time: u32,
37    pub word_count: u32,
38    pub toc_html: Option<String>,
39    pub prev_post: Option<PostNav>,
40    pub next_post: Option<PostNav>,
41}
42
43impl From<Post> for PublicPost {
44    fn from(post: Post) -> Self {
45        Self {
46            id: post.id,
47            author_id: post.author_id,
48            title: post.title,
49            slug: post.slug,
50            summary: post.summary,
51            content_html: post.content_html,
52            published_at: post.published_at,
53            created_at: post.created_at,
54            updated_at: post.updated_at,
55            tags: post.tags,
56            cover_image: post.cover_image,
57            reading_time: post.reading_time,
58            word_count: post.word_count,
59            toc_html: post.toc_html,
60            prev_post: post.prev_post,
61            next_post: post.next_post,
62        }
63    }
64}
65
66impl PublicPost {
67    /// 复用文章展示组件;此值只用于展示,不能作为编辑器的数据源。
68    pub fn into_display_post(self) -> Post {
69        Post {
70            id: self.id,
71            author_id: self.author_id,
72            title: self.title,
73            slug: self.slug,
74            summary: self.summary,
75            content_md: String::new(),
76            content_html: self.content_html,
77            status: PostStatus::Published,
78            published_at: self.published_at,
79            created_at: self.created_at,
80            updated_at: self.updated_at,
81            deleted_at: None,
82            tags: self.tags,
83            cover_image: self.cover_image,
84            reading_time: self.reading_time,
85            word_count: self.word_count,
86            toc_html: self.toc_html,
87            prev_post: self.prev_post,
88            next_post: self.next_post,
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn reading_payload_omits_markdown_and_preserves_rendered_content() {
99        let now = Utc::now();
100        let full = Post {
101            id: 42,
102            author_id: 7,
103            title: "文章".into(),
104            slug: "post".into(),
105            summary: Some("摘要".into()),
106            content_md: "仅编辑需要的原文".into(),
107            content_html: Some("<p>正文</p>".into()),
108            status: PostStatus::Published,
109            published_at: Some(now),
110            created_at: now,
111            updated_at: now,
112            deleted_at: None,
113            tags: vec!["Rust".into()],
114            cover_image: None,
115            reading_time: 2,
116            word_count: 300,
117            toc_html: Some("<nav>目录</nav>".into()),
118            prev_post: Some(PostNav {
119                title: "上一篇".into(),
120                slug: "prev".into(),
121            }),
122            next_post: None,
123        };
124        let public = PublicPost::from(full.clone());
125        let payload = serde_json::to_value(&public).unwrap();
126        assert!(payload.get("content_md").is_none());
127        assert_eq!(
128            serde_json::to_value(&full).unwrap()["content_md"],
129            full.content_md
130        );
131        let display = serde_json::from_value::<PublicPost>(payload)
132            .unwrap()
133            .into_display_post();
134        assert_eq!(display.content_html, full.content_html);
135        assert_eq!(display.toc_html, full.toc_html);
136        assert_eq!(display.prev_post, full.prev_post);
137        assert_eq!(display.author_id, full.author_id);
138        assert_eq!(display.updated_at, full.updated_at);
139    }
140}