Skip to main content

yggdrasil/api/comments/
check.rs

1//! 评论审核状态批量查询。
2//!
3//! 用于前端轮询刚刚提交的评论是否已通过审核,支持传入多个 id。
4//! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。
5
6use dioxus::prelude::*;
7
8/// 单个评论的待审核状态结果。
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct PendingStatusItem {
11    /// 评论 id。
12    pub id: i64,
13    /// 当前状态:approved / pending / rejected / spam / trash / gone。
14    pub status: String,
15}
16
17/// 查询一组评论的当前审核状态。
18///
19/// 如果某 id 不存在,则返回状态 `"gone"`。
20/// Dioxus server function,注册在 `/api` 路径下。
21#[server(CheckPendingStatus, "/api")]
22pub async fn check_pending_status(ids: Vec<i64>) -> Result<Vec<PendingStatusItem>, ServerFnError> {
23    // 仅在服务端构建中执行 SQL 查询。
24    #[cfg(feature = "server")]
25    {
26        use crate::api::error::AppError;
27        use crate::db::pool::get_conn;
28
29        // 空列表直接返回空结果,避免无意义的数据库查询。
30        if ids.is_empty() {
31            return Ok(vec![]);
32        }
33        // 上限 100 条,防止超大 IN 列表拖慢查询或被滥用枚举评论状态。
34        if ids.len() > 100 {
35            return Ok(vec![]);
36        }
37
38        // 限流防高速遍历枚举评论状态(L3)。本接口供访客轮询自己刚提交的评论
39        // 审核状态,故不加 admin 鉴权;但 strict 限流(对 unknown IP 降级宽松桶)
40        // 足以阻止批量枚举。
41        if let Some(ctx) = dioxus::fullstack::FullstackContext::current() {
42            let parts = ctx.parts_mut();
43            let ip = crate::api::rate_limit::get_client_ip(&parts.headers);
44            if let Err(_msg) = crate::api::rate_limit::check_strict_limit(&ip) {
45                return Err(ServerFnError::new("请求过于频繁,请稍后再试"));
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}