Skip to main content

yggdrasil/models/
public_note.rs

1//! 笔记阅读页传输模型。编辑器、MCP 与知识库继续使用完整 [`crate::models::note::Note`],
2//! 公开笔记详情不再向匿名读者发送编辑 Markdown 与知识库内部版本号。
3//!
4//! 对应 docs/wasm-optimization-plan.md P4.1;字段以公开详情页(`NoteDocument`)
5//! 实际渲染所需为准,参照 [`crate::models::public_post::PublicPost`] 的模式。
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use super::note::{Note, NoteKind};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct PublicNote {
14    pub id: i32,
15    pub slug: String,
16    /// 公开展示的版本号(已发布修订版本的版本号)。详情页 meta 直接显示
17    /// 「版本 N」,因此不能像其他内部版本号一样省略。
18    pub version: i32,
19    pub kind: NoteKind,
20    pub title: String,
21    pub summary: String,
22    pub content_html: String,
23    pub toc_html: String,
24    pub tags: Vec<String>,
25    #[cfg_attr(
26        target_arch = "wasm32",
27        serde(deserialize_with = "crate::models::rfc3339::deserialize")
28    )]
29    pub created_at: DateTime<Utc>,
30    /// 已发布修订版本的创建时间(列表与详情的「更新于」语义)。
31    #[cfg_attr(
32        target_arch = "wasm32",
33        serde(deserialize_with = "crate::models::rfc3339::deserialize")
34    )]
35    pub updated_at: DateTime<Utc>,
36    /// 目录的一级小节数(服务端从 `toc_html` 解析),详情 meta 显示「N 节」。
37    #[serde(default)]
38    pub sections: u32,
39}
40
41impl From<Note> for PublicNote {
42    fn from(note: Note) -> Self {
43        Self {
44            id: note.id,
45            slug: note.slug,
46            version: note.version,
47            kind: note.kind,
48            title: note.title,
49            summary: note.summary,
50            content_html: note.content_html,
51            toc_html: note.toc_html,
52            tags: note.tags,
53            created_at: note.created_at,
54            updated_at: note.updated_at,
55            sections: note.sections,
56        }
57    }
58}
59
60impl PublicNote {
61    /// 与 [`Note::display_title`] 相同的展示标题规则:空标题回退摘要前 48 字,
62    /// 再回退类型标签。后台预览把完整 Note 转换为 PublicNote 后复用同一渲染路径。
63    pub fn display_title(&self) -> String {
64        super::note::display_title(self.kind, &self.title, &self.summary)
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::models::note::NoteKind;
72
73    fn sample_note() -> Note {
74        let now = Utc::now();
75        Note {
76            id: 7,
77            slug: "garden".into(),
78            version: 3,
79            kind: NoteKind::Topic,
80            title: "生长中的想法".into(),
81            content_md: "仅编辑需要的原文".into(),
82            content_html: "<p>正文</p>".into(),
83            toc_html: "<nav>目录</nav>".into(),
84            summary: "摘要".into(),
85            tags: vec!["rust".into()],
86            created_at: now,
87            updated_at: now,
88            published_at: Some(now),
89            published_version: Some(3),
90            knowledge_version: Some(2),
91            deleted_at: None,
92            notebook_ids: vec![1, 2],
93            sections: 0,
94        }
95    }
96
97    #[test]
98    fn public_note_omits_markdown_and_internal_fields() {
99        let payload = serde_json::to_value(PublicNote::from(sample_note())).unwrap();
100        assert!(
101            payload.get("content_md").is_none(),
102            "公开 DTO 不得携带编辑 Markdown"
103        );
104        assert!(
105            payload.get("published_version").is_none(),
106            "工作稿版本不外发"
107        );
108        assert!(
109            payload.get("knowledge_version").is_none(),
110            "知识库版本不外发"
111        );
112        assert!(payload.get("deleted_at").is_none());
113        assert!(payload.get("notebook_ids").is_none(), "笔记本归属不外发");
114        assert_eq!(payload["version"], 3);
115        assert_eq!(payload["content_html"], "<p>正文</p>");
116    }
117
118    #[test]
119    fn display_title_matches_note_rules() {
120        let mut public = PublicNote::from(sample_note());
121        assert_eq!(public.display_title(), "生长中的想法");
122        public.title = String::new();
123        assert_eq!(public.display_title(), "摘要");
124        public.summary = String::new();
125        assert_eq!(public.display_title(), "主题笔记");
126    }
127}