Skip to main content

yggdrasil/api/comments/
check.rs

1//! 评论审核状态批量查询。
2//!
3//! 用于前端轮询刚刚提交的评论是否已通过审核,支持传入多个 id。
4//! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。
5
6use crate::api::error::ApiError;
7use leptos::prelude::*;
8
9/// 单个评论的待审核状态结果。
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
11pub struct PendingStatusItem {
12    /// 评论 id。
13    pub id: i64,
14    /// 当前状态:approved / pending / rejected / spam / trash / gone。
15    pub status: String,
16}
17
18/// 查询一组评论的当前审核状态。
19///
20/// 如果某 id 不存在,则返回状态 `"gone"`。
21/// Leptos server function,注册在 `/api` 路径下。
22#[server(CheckPendingStatus, endpoint = "check_pending_status", input = leptos::server_fn::codec::Json)]
23pub async fn check_pending_status(ids: Vec<i64>) -> Result<Vec<PendingStatusItem>, ApiError> {
24    // 仅在服务端构建中执行 SQL 查询。
25    #[cfg(feature = "server")]
26    {
27        use crate::api::error::AppError;
28        use crate::db::pool::get_conn;
29
30        // 空列表直接返回空结果,避免无意义的数据库查询。
31        if ids.is_empty() {
32            return Ok(vec![]);
33        }
34        // 上限 100 条,防止超大 IN 列表拖慢查询或被滥用枚举评论状态。
35        if ids.len() > 100 {
36            return Ok(vec![]);
37        }
38
39        // 限流防高速遍历枚举评论状态(L3)。本接口供访客轮询自己刚提交的评论
40        // 审核状态,故不加 admin 鉴权;但 strict 限流(对 unknown IP 降级宽松桶)
41        // 足以阻止批量枚举。
42        if let Some(headers) = crate::request_ctx::request_headers() {
43            let ip = crate::api::rate_limit::get_client_ip(&headers).await;
44            if let Err(_msg) = crate::api::rate_limit::check_strict_limit(&ip) {
45                return Err(ApiError::BadRequest("请求过于频繁,请稍后再试".into()));
46            }
47        }
48
49        let client = get_conn().await.map_err(AppError::db_conn)?;
50
51        let rows = client
52            .query(
53                "SELECT id, status FROM comments WHERE id = ANY($1)",
54                &[&ids],
55            )
56            .await
57            .map_err(AppError::query)?;
58
59        // 将查询结果收集为 HashMap,便于后续按传入顺序补齐缺失项。
60        let found: std::collections::HashMap<i64, String> = rows
61            .iter()
62            .map(|r| (r.get::<_, i64>(0), r.get::<_, String>(1)))
63            .collect();
64
65        let result: Vec<PendingStatusItem> = ids
66            .into_iter()
67            .map(|id| {
68                let status = found
69                    .get(&id)
70                    .cloned()
71                    .unwrap_or_else(|| "gone".to_string());
72                PendingStatusItem { id, status }
73            })
74            .collect();
75
76        Ok(result)
77    }
78    #[cfg(not(feature = "server"))]
79    unreachable!()
80}