Skip to main content

yggdrasil/api/posts/
types.rs

1//! 文章 API 的请求与响应数据结构。
2
3#[cfg(feature = "server")]
4use crate::api::error::ApiError;
5use crate::models::post::{Post, PostListItem, PostStats, PostStatus, Tag};
6
7/// 创建/更新/删除文章的统一响应结构。
8#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
9pub struct CreatePostResponse {
10    /// 操作是否成功。
11    pub success: bool,
12    /// 提示信息。
13    pub message: String,
14    /// 新文章 id,失败时为 None。
15    pub post_id: Option<i32>,
16    /// 最终 slug,失败时为 None。
17    pub slug: Option<String>,
18}
19
20/// 后台文章列表的筛选条件。各项为空表示不按该项筛选。
21#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
22pub struct AdminPostQuery {
23    /// 标题子串(不区分大小写)。
24    pub search: Option<String>,
25    /// 发布状态。
26    pub status: Option<PostStatus>,
27    /// 标签名(精确匹配)。
28    pub tag: Option<String>,
29}
30
31impl AdminPostQuery {
32    /// 只按标题搜索。
33    pub fn search(text: impl Into<String>) -> Self {
34        Self {
35            search: Some(text.into()),
36            ..Self::default()
37        }
38    }
39}
40
41/// 批量操作:转为草稿或移到回收站。
42#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
43pub enum BatchPostAction {
44    /// 发布(草稿预览的「发布」按钮);沿用首次发布时间。
45    Publish,
46    Unpublish,
47    Trash,
48}
49
50/// 批量操作结果:成功篇数与失败篇数。
51#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52pub struct BatchPostResult {
53    pub done: usize,
54    pub failed: usize,
55}
56
57/// 这些构造器只在 server function body 内调用,WASM 端因 cfg gate 剥离了调用点,
58/// 故对非 server 构建允许 dead_code。
59#[cfg_attr(not(feature = "server"), allow(dead_code))]
60impl CreatePostResponse {
61    /// 构造失败响应(post_id / slug 均为 None)。
62    pub fn err(message: impl Into<String>) -> Self {
63        Self {
64            success: false,
65            message: message.into(),
66            post_id: None,
67            slug: None,
68        }
69    }
70
71    /// 构造成功响应,携带新文章 id 与 slug。
72    pub fn ok(message: impl Into<String>, post_id: i32, slug: impl Into<String>) -> Self {
73        Self {
74            success: true,
75            message: message.into(),
76            post_id: Some(post_id),
77            slug: Some(slug.into()),
78        }
79    }
80
81    /// 构造成功响应(无关联 id/slug,用于批量操作)。
82    pub fn ok_msg(message: impl Into<String>) -> Self {
83        Self {
84            success: true,
85            message: message.into(),
86            post_id: None,
87            slug: None,
88        }
89    }
90}
91
92/// 文章列表响应。
93#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
94pub struct PostListResponse {
95    /// 文章列表(轻量 DTO,不含正文)。
96    pub posts: Vec<PostListItem>,
97    /// 符合查询条件的总数。
98    pub total: i64,
99}
100
101/// 标签列表响应。
102#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
103pub struct TagListResponse {
104    /// 标签列表。
105    pub tags: Vec<Tag>,
106}
107
108/// 搜索面板(⌘K)里的一条文章建议:只含标题、摘要、发布时间与标签。
109// 浏览器端只经反序列化构造,静态分析看不到。
110#[cfg_attr(not(feature = "server"), allow(dead_code))]
111#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
112pub struct PostSuggestion {
113    /// URL slug。
114    pub slug: String,
115    /// 文章标题。
116    pub title: String,
117    /// 摘要,可选。
118    pub summary: Option<String>,
119    /// 发布时间。
120    #[cfg_attr(
121        target_arch = "wasm32",
122        serde(
123            default,
124            deserialize_with = "crate::models::rfc3339::deserialize_option"
125        )
126    )]
127    pub published_at: Option<chrono::DateTime<chrono::Utc>>,
128    /// 标签名(按名称排序)。
129    pub tags: Vec<String>,
130}
131
132/// 文章建议响应。
133#[cfg_attr(not(feature = "server"), allow(dead_code))]
134#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
135pub struct PostSuggestionResponse {
136    /// 最多 8 条。
137    pub posts: Vec<PostSuggestion>,
138}
139
140/// 文章建议的条数上限。
141#[cfg(feature = "server")]
142pub const SUGGESTION_LIMIT: i64 = 8;
143
144/// 首页「最新一篇」的目录预览:一级小节总数与前几个小节标题。
145#[cfg_attr(not(feature = "server"), allow(dead_code))]
146#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
147pub struct PostOutline {
148    /// 对应文章的 slug;前台据此确认与列表首篇一致。
149    pub slug: String,
150    /// 一级小节总数。
151    pub total: u32,
152    /// 前 `OUTLINE_PREVIEW_LIMIT` 个小节标题(纯文本)。
153    pub headings: Vec<String>,
154}
155
156/// 目录预览列出的小节数。
157#[cfg(feature = "server")]
158pub const OUTLINE_PREVIEW_LIMIT: usize = 6;
159
160/// 文章统计响应。
161#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
162pub struct PostStatsResponse {
163    /// 文章统计信息。
164    pub stats: PostStats,
165}
166
167/// 单篇文章详情响应。
168#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
169pub struct SinglePostResponse {
170    /// 文章详情,不存在时为 None。
171    pub post: Option<Post>,
172}
173
174/// 阅读页详情不携带原始 Markdown;完整响应保留给编辑、预览和旧客户端。
175#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
176pub struct PublicPostResponse {
177    pub post: Option<crate::models::public_post::PublicPost>,
178}
179
180/// Markdown 重建结果响应。
181#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
182pub struct RebuildResult {
183    /// 成功重建的文章数量。
184    pub rebuilt: u64,
185    /// 重建失败的文章数量。
186    pub failed: u64,
187    /// 失败信息摘要(最多 5 条)。
188    pub errors: Vec<String>,
189    /// 本批已满时为最后处理的 id,前端以此续传;None 表示已处理完。
190    pub next_after_id: Option<i32>,
191}
192
193/// 浏览器协议适配:业务拒绝保留 success=false,内部错误使用统一脱敏转换。
194#[cfg(feature = "server")]
195pub(super) fn write_response(
196    result: Result<super::service::WriteOutcome, super::service::WriteError>,
197    message: &str,
198) -> Result<CreatePostResponse, ApiError> {
199    use super::service::WriteError;
200    match result {
201        Ok(value) => Ok(CreatePostResponse::ok(message, value.post_id, value.slug)),
202        Err(WriteError::Invalid(message)) => Ok(CreatePostResponse::err(message)),
203        Err(WriteError::Conflict(_)) => Ok(CreatePostResponse::err(
204            "文章已被修改,请重新读取后合并更改",
205        )),
206        Err(WriteError::Failed(error)) => Err(error.into()),
207    }
208}