Skip to main content

yggdrasil/api/comments/
update.rs

1//! 评论审核状态更新接口:通过、垃圾、删除与批量更新。
2//!
3//! 所有接口均需管理员身份,Dioxus server function 注册在 `/api` 路径下。
4//! 状态变更后需要清空文章评论缓存、计数缓存与待审核计数缓存。
5//! 仅在 `feature = "server"` 启用的服务端构建中写入数据库。
6
7use crate::api::comments::types::*;
8use dioxus::prelude::*;
9
10/// 通过指定评论。
11///
12/// 同时递归将该评论的所有 pending 父评论一并通过,确保嵌套链可见。
13#[server(ApproveComment, "/api")]
14pub async fn approve_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
15    #[cfg(feature = "server")]
16    {
17        use crate::api::auth::get_current_admin_user;
18        use crate::api::error::AppError;
19        use crate::cache;
20        use crate::db::pool::get_conn;
21
22        let _admin = get_current_admin_user().await?;
23
24        let client = get_conn().await.map_err(AppError::db_conn)?;
25
26        let row = client
27            .query_opt(
28                "SELECT post_id, status FROM comments WHERE id = $1 AND deleted_at IS NULL",
29                &[&id],
30            )
31            .await
32            .map_err(AppError::query)?;
33
34        let post_id: i32 = match row {
35            Some(r) => r.get("post_id"),
36            None => {
37                return Ok(CommentResponse::error(
38                    "not_found",
39                    "评论不存在".to_string(),
40                ));
41            }
42        };
43
44        // 直接通过目标评论并记录通过时间。
45        client
46            .execute(
47                "UPDATE comments SET status = 'approved', approved_at = NOW() WHERE id = $1",
48                &[&id],
49            )
50            .await
51            .map_err(AppError::query)?;
52
53        // 递归向上查找所有 pending 父评论并同步通过,避免子评论可见但父评论被隐藏。
54        client
55            .execute(
56                "WITH RECURSIVE ancestors AS ( \
57                     SELECT parent_id FROM comments WHERE id = $1 \
58                     UNION ALL \
59                     SELECT c.parent_id FROM comments c JOIN ancestors a ON c.id = a.parent_id WHERE a.parent_id IS NOT NULL \
60                 ) \
61                 UPDATE comments SET status = 'approved', approved_at = NOW() \
62                 WHERE id IN (SELECT parent_id FROM ancestors WHERE parent_id IS NOT NULL) AND status = 'pending'",
63                &[&id],
64            )
65            .await
66            .map_err(AppError::query)?;
67
68        cache::invalidate_comments_by_post(post_id).await;
69        cache::invalidate_pending_count().await;
70
71        Ok(CommentResponse::ok("已通过".to_string()))
72    }
73    #[cfg(not(feature = "server"))]
74    unreachable!()
75}
76
77/// 将指定评论标记为垃圾评论。
78///
79/// 若原状态为 approved,则需要清空该文章相关缓存。
80#[server(SpamComment, "/api")]
81pub async fn spam_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
82    #[cfg(feature = "server")]
83    {
84        use crate::api::auth::get_current_admin_user;
85        use crate::api::error::AppError;
86        use crate::cache;
87        use crate::db::pool::get_conn;
88
89        let _admin = get_current_admin_user().await?;
90
91        let client = get_conn().await.map_err(AppError::db_conn)?;
92
93        let row = client
94            .query_opt(
95                "SELECT post_id, status FROM comments WHERE id = $1 AND deleted_at IS NULL",
96                &[&id],
97            )
98            .await
99            .map_err(AppError::query)?;
100
101        if let Some(r) = row {
102            let post_id: i32 = r.get("post_id");
103            let old_status: String = r.get("status");
104
105            client
106                .execute(
107                    "UPDATE comments SET status = 'spam' WHERE id = $1 AND deleted_at IS NULL",
108                    &[&id],
109                )
110                .await
111                .map_err(AppError::query)?;
112
113            if old_status == "approved" {
114                cache::invalidate_comments_by_post(post_id).await;
115            }
116            cache::invalidate_pending_count().await;
117        }
118
119        Ok(CommentResponse::ok("已标记为垃圾".to_string()))
120    }
121    #[cfg(not(feature = "server"))]
122    unreachable!()
123}
124
125/// 将指定评论移入回收站(软删除)。
126///
127/// 软删除会设置 deleted_at 与状态为 trash,并清空相关缓存。
128#[server(TrashComment, "/api")]
129pub async fn trash_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
130    #[cfg(feature = "server")]
131    {
132        use crate::api::auth::get_current_admin_user;
133        use crate::api::error::AppError;
134        use crate::cache;
135        use crate::db::pool::get_conn;
136
137        let _admin = get_current_admin_user().await?;
138
139        let client = get_conn().await.map_err(AppError::db_conn)?;
140
141        let row = client
142            .query_opt(
143                "SELECT post_id FROM comments WHERE id = $1 AND deleted_at IS NULL",
144                &[&id],
145            )
146            .await
147            .map_err(AppError::query)?;
148
149        if let Some(r) = row {
150            let post_id: i32 = r.get("post_id");
151
152            client
153                .execute(
154                    "UPDATE comments SET status = 'trash', deleted_at = NOW() WHERE id = $1",
155                    &[&id],
156                )
157                .await
158                .map_err(AppError::query)?;
159
160            cache::invalidate_comments_by_post(post_id).await;
161            cache::invalidate_pending_count().await;
162        }
163
164        Ok(CommentResponse::ok("已删除".to_string()))
165    }
166    #[cfg(not(feature = "server"))]
167    unreachable!()
168}
169
170/// 批量更新评论状态。
171///
172/// 仅接受 approved / spam / trash 三种状态;trash 会软删除并设置 deleted_at,
173/// approved 会设置 approved_at。
174#[server(BatchUpdateCommentStatus, "/api")]
175pub async fn batch_update_comment_status(
176    ids: Vec<i64>,
177    status: String,
178) -> Result<BatchStatusResponse, ServerFnError> {
179    #[cfg(feature = "server")]
180    {
181        use crate::api::auth::get_current_admin_user;
182        use crate::api::error::AppError;
183        use crate::cache;
184        use crate::db::pool::get_conn;
185
186        let _admin = get_current_admin_user().await?;
187
188        // 限制可批量操作的状态,防止非法状态写入数据库。
189        if !matches!(status.as_str(), "approved" | "spam" | "trash") {
190            return Ok(BatchStatusResponse {
191                success: false,
192                updated_count: 0,
193                message: "无效的状态".to_string(),
194            });
195        }
196
197        let client = get_conn().await.map_err(AppError::db_conn)?;
198
199        // 收集受影响的文章 id,用于后续批量失效缓存。
200        let post_ids: Vec<i32> = client
201            .query(
202                "SELECT DISTINCT post_id FROM comments WHERE id = ANY($1)",
203                &[&ids],
204            )
205            .await
206            .map_err(AppError::query)?
207            .iter()
208            .map(|r| r.get("post_id"))
209            .collect();
210
211        // 根据目标状态设置不同的附加字段:trash 软删除,approved 记录通过时间。
212        let result = if status == "trash" {
213            client
214                .execute(
215                    "UPDATE comments SET status = $1, deleted_at = NOW() WHERE id = ANY($2)",
216                    &[&status, &ids],
217                )
218                .await
219                .map_err(AppError::query)?
220        } else if status == "approved" {
221            client
222                .execute(
223                    "UPDATE comments SET status = $1, approved_at = NOW() WHERE id = ANY($2)",
224                    &[&status, &ids],
225                )
226                .await
227                .map_err(AppError::query)?
228        } else {
229            client
230                .execute(
231                    "UPDATE comments SET status = $1 WHERE id = ANY($2)",
232                    &[&status, &ids],
233                )
234                .await
235                .map_err(AppError::query)?
236        };
237
238        cache::invalidate_pending_count().await;
239        for pid in post_ids {
240            cache::invalidate_comments_by_post(pid).await;
241        }
242
243        Ok(BatchStatusResponse {
244            success: true,
245            updated_count: result as i64,
246            message: format!("已更新 {} 条评论", result),
247        })
248    }
249    #[cfg(not(feature = "server"))]
250    unreachable!()
251}