yggdrasil/api/comments/
update.rs1use crate::api::comments::types::*;
8use crate::api::error::ApiError;
9use leptos::prelude::*;
10
11#[server(ApproveComment, endpoint = "approve_comment", input = leptos::server_fn::codec::Json)]
15pub async fn approve_comment(id: i64) -> Result<CommentResponse, ApiError> {
16 #[cfg(feature = "server")]
17 {
18 use crate::api::auth::get_current_admin_user;
19
20 let _admin = get_current_admin_user().await?;
21 if !approve_comment_impl(id).await? {
22 return Ok(CommentResponse::error(
23 "not_found",
24 "评论不存在".to_string(),
25 ));
26 }
27
28 Ok(CommentResponse::ok("已通过".to_string()))
29 }
30 #[cfg(not(feature = "server"))]
31 unreachable!()
32}
33
34#[server(SpamComment, endpoint = "spam_comment", input = leptos::server_fn::codec::Json)]
38pub async fn spam_comment(id: i64) -> Result<CommentResponse, ApiError> {
39 #[cfg(feature = "server")]
40 {
41 use crate::api::auth::get_current_admin_user;
42
43 let _admin = get_current_admin_user().await?;
44 spam_comment_impl(id).await?;
45
46 Ok(CommentResponse::ok("已标记为垃圾".to_string()))
47 }
48 #[cfg(not(feature = "server"))]
49 unreachable!()
50}
51
52#[server(TrashComment, endpoint = "trash_comment", input = leptos::server_fn::codec::Json)]
56pub async fn trash_comment(id: i64) -> Result<CommentResponse, ApiError> {
57 #[cfg(feature = "server")]
58 {
59 use crate::api::auth::get_current_admin_user;
60
61 let _admin = get_current_admin_user().await?;
62 trash_comment_impl(id).await?;
63
64 Ok(CommentResponse::ok("已删除".to_string()))
65 }
66 #[cfg(not(feature = "server"))]
67 unreachable!()
68}
69
70#[server(BatchUpdateCommentStatus, endpoint = "batch_update_comment_status", input = leptos::server_fn::codec::Json)]
75pub async fn batch_update_comment_status(
76 ids: Vec<i64>,
77 status: String,
78) -> Result<BatchStatusResponse, ApiError> {
79 #[cfg(feature = "server")]
80 {
81 use crate::api::auth::get_current_admin_user;
82 use crate::api::error::AppError;
83 use crate::cache;
84 use crate::db::pool::get_conn;
85
86 let _admin = get_current_admin_user().await?;
87
88 if !matches!(status.as_str(), "approved" | "spam" | "trash") {
90 return Ok(BatchStatusResponse {
91 success: false,
92 updated_count: 0,
93 message: "无效的状态".to_string(),
94 });
95 }
96
97 let client = get_conn().await.map_err(AppError::db_conn)?;
98
99 let post_ids: Vec<i32> = client
101 .query(
102 "SELECT DISTINCT post_id FROM comments WHERE id = ANY($1)",
103 &[&ids],
104 )
105 .await
106 .map_err(AppError::query)?
107 .iter()
108 .map(|r| r.get("post_id"))
109 .collect();
110
111 let result = if status == "trash" {
113 client
114 .execute(
115 "UPDATE comments SET status = $1, deleted_at = NOW() WHERE id = ANY($2)",
116 &[&status, &ids],
117 )
118 .await
119 .map_err(AppError::query)?
120 } else if status == "approved" {
121 client
122 .execute(
123 "UPDATE comments SET status = $1, approved_at = NOW() WHERE id = ANY($2)",
124 &[&status, &ids],
125 )
126 .await
127 .map_err(AppError::query)?
128 } else {
129 client
130 .execute(
131 "UPDATE comments SET status = $1 WHERE id = ANY($2)",
132 &[&status, &ids],
133 )
134 .await
135 .map_err(AppError::query)?
136 };
137
138 cache::invalidate_pending_count().await;
139 for pid in post_ids {
140 cache::invalidate_comments_by_post(pid).await;
141 }
142
143 Ok(BatchStatusResponse {
144 success: true,
145 updated_count: result as i64,
146 message: format!("已更新 {} 条评论", result),
147 })
148 }
149 #[cfg(not(feature = "server"))]
150 unreachable!()
151}
152
153#[server(RestoreComments, endpoint = "restore_comments", input = leptos::server_fn::codec::Json)]
155pub async fn restore_comments(ids: Vec<i64>) -> Result<BatchStatusResponse, ApiError> {
156 #[cfg(feature = "server")]
157 {
158 use crate::api::auth::get_current_admin_user;
159 use crate::api::error::AppError;
160 use crate::cache;
161 use crate::db::pool::get_conn;
162
163 let _admin = get_current_admin_user().await?;
164 let client = get_conn().await.map_err(AppError::db_conn)?;
165 let rows = client
166 .query(
167 "UPDATE comments SET deleted_at = NULL, status = 'pending' \
168 WHERE id = ANY($1) AND deleted_at IS NOT NULL RETURNING post_id",
169 &[&ids],
170 )
171 .await
172 .map_err(AppError::query)?;
173 let mut post_ids: Vec<i32> = rows.iter().map(|r| r.get("post_id")).collect();
174 post_ids.sort_unstable();
175 post_ids.dedup();
176 cache::invalidate_pending_count().await;
177 for pid in post_ids {
178 cache::invalidate_comments_by_post(pid).await;
179 }
180 Ok(BatchStatusResponse {
181 success: true,
182 updated_count: rows.len() as i64,
183 message: format!("已恢复 {} 条评论,等待审核", rows.len()),
184 })
185 }
186 #[cfg(not(feature = "server"))]
187 {
188 let _ = ids;
189 unreachable!()
190 }
191}
192
193#[cfg(feature = "server")]
197pub(crate) async fn approve_comment_impl(id: i64) -> Result<bool, crate::api::error::AppError> {
198 use crate::api::error::AppError;
199 use crate::cache;
200 use crate::db::pool::get_conn;
201
202 let client = get_conn().await.map_err(AppError::db_conn)?;
203
204 let row = client
205 .query_opt(
206 "SELECT post_id FROM comments WHERE id = $1 AND deleted_at IS NULL",
207 &[&id],
208 )
209 .await
210 .map_err(AppError::query)?;
211
212 let post_id: i32 = match row {
213 Some(r) => r.get("post_id"),
214 None => return Ok(false),
215 };
216
217 client
219 .execute(
220 "UPDATE comments SET status = 'approved', approved_at = NOW() WHERE id = $1",
221 &[&id],
222 )
223 .await
224 .map_err(AppError::query)?;
225
226 client
228 .execute(
229 "WITH RECURSIVE ancestors AS ( \
230 SELECT parent_id FROM comments WHERE id = $1 \
231 UNION ALL \
232 SELECT c.parent_id FROM comments c JOIN ancestors a ON c.id = a.parent_id WHERE a.parent_id IS NOT NULL \
233 ) \
234 UPDATE comments SET status = 'approved', approved_at = NOW() \
235 WHERE id IN (SELECT parent_id FROM ancestors WHERE parent_id IS NOT NULL) AND status = 'pending'",
236 &[&id],
237 )
238 .await
239 .map_err(AppError::query)?;
240
241 cache::invalidate_comments_by_post(post_id).await;
242 cache::invalidate_pending_count().await;
243 Ok(true)
244}
245
246#[cfg(feature = "server")]
250pub(crate) async fn spam_comment_impl(id: i64) -> Result<bool, crate::api::error::AppError> {
251 use crate::api::error::AppError;
252 use crate::cache;
253 use crate::db::pool::get_conn;
254
255 let client = get_conn().await.map_err(AppError::db_conn)?;
256
257 let row = client
258 .query_opt(
259 "SELECT post_id, status FROM comments WHERE id = $1 AND deleted_at IS NULL",
260 &[&id],
261 )
262 .await
263 .map_err(AppError::query)?;
264
265 let Some(r) = row else {
266 return Ok(false);
267 };
268 let post_id: i32 = r.get("post_id");
269 let old_status: String = r.get("status");
270
271 client
272 .execute(
273 "UPDATE comments SET status = 'spam' WHERE id = $1 AND deleted_at IS NULL",
274 &[&id],
275 )
276 .await
277 .map_err(AppError::query)?;
278
279 if old_status == "approved" {
280 cache::invalidate_comments_by_post(post_id).await;
281 }
282 cache::invalidate_pending_count().await;
283 Ok(true)
284}
285
286#[cfg(feature = "server")]
290pub(crate) async fn trash_comment_impl(id: i64) -> Result<bool, crate::api::error::AppError> {
291 use crate::api::error::AppError;
292 use crate::cache;
293 use crate::db::pool::get_conn;
294
295 let client = get_conn().await.map_err(AppError::db_conn)?;
296
297 let row = client
298 .query_opt(
299 "SELECT post_id FROM comments WHERE id = $1 AND deleted_at IS NULL",
300 &[&id],
301 )
302 .await
303 .map_err(AppError::query)?;
304
305 let Some(r) = row else {
306 return Ok(false);
307 };
308 let post_id: i32 = r.get("post_id");
309
310 client
311 .execute(
312 "UPDATE comments SET status = 'trash', deleted_at = NOW() WHERE id = $1",
313 &[&id],
314 )
315 .await
316 .map_err(AppError::query)?;
317
318 cache::invalidate_comments_by_post(post_id).await;
319 cache::invalidate_pending_count().await;
320 Ok(true)
321}