该接口供访客轮询自己刚提交评论的审核状态,故不加 admin 鉴权(会破坏合法 轮询);改为加 strict 限流(对 unknown IP 降级宽松桶),阻止批量枚举评论 状态(L3)。复用 check_strict_limit,与其它敏感接口一致。
77 lines
2.7 KiB
Rust
77 lines
2.7 KiB
Rust
//! 评论审核状态批量查询。
|
||
//!
|
||
//! 用于前端轮询刚刚提交的评论是否已通过审核,支持传入多个 id。
|
||
//! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。
|
||
|
||
use dioxus::prelude::*;
|
||
|
||
/// 单个评论的待审核状态结果。
|
||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||
pub struct PendingStatusItem {
|
||
/// 评论 id。
|
||
pub id: i64,
|
||
/// 当前状态:approved / pending / rejected / spam / trash / gone。
|
||
pub status: String,
|
||
}
|
||
|
||
/// 查询一组评论的当前审核状态。
|
||
///
|
||
/// 如果某 id 不存在,则返回状态 `"gone"`。
|
||
/// Dioxus server function,注册在 `/api` 路径下。
|
||
#[server(CheckPendingStatus, "/api")]
|
||
pub async fn check_pending_status(ids: Vec<i64>) -> Result<Vec<PendingStatusItem>, ServerFnError> {
|
||
// 仅在服务端构建中执行 SQL 查询。
|
||
#[cfg(feature = "server")]
|
||
{
|
||
use crate::api::error::AppError;
|
||
use crate::db::pool::get_conn;
|
||
|
||
// 空列表直接返回空结果,避免无意义的数据库查询。
|
||
if ids.is_empty() {
|
||
return Ok(vec![]);
|
||
}
|
||
|
||
// 限流防高速遍历枚举评论状态(L3)。本接口供访客轮询自己刚提交的评论
|
||
// 审核状态,故不加 admin 鉴权;但 strict 限流(对 unknown IP 降级宽松桶)
|
||
// 足以阻止批量枚举。
|
||
if let Some(ctx) = dioxus::fullstack::FullstackContext::current() {
|
||
let parts = ctx.parts_mut();
|
||
let ip = crate::api::rate_limit::get_client_ip(&parts.headers);
|
||
if let Err(_msg) = crate::api::rate_limit::check_strict_limit(&ip) {
|
||
return Err(ServerFnError::new("请求过于频繁,请稍后再试"));
|
||
}
|
||
}
|
||
|
||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||
|
||
let rows = client
|
||
.query(
|
||
"SELECT id, status FROM comments WHERE id = ANY($1)",
|
||
&[&ids],
|
||
)
|
||
.await
|
||
.map_err(AppError::query)?;
|
||
|
||
// 将查询结果收集为 HashMap,便于后续按传入顺序补齐缺失项。
|
||
let found: std::collections::HashMap<i64, String> = rows
|
||
.iter()
|
||
.map(|r| (r.get::<_, i64>(0), r.get::<_, String>(1)))
|
||
.collect();
|
||
|
||
let result: Vec<PendingStatusItem> = ids
|
||
.into_iter()
|
||
.map(|id| {
|
||
let status = found
|
||
.get(&id)
|
||
.cloned()
|
||
.unwrap_or_else(|| "gone".to_string());
|
||
PendingStatusItem { id, status }
|
||
})
|
||
.collect();
|
||
|
||
Ok(result)
|
||
}
|
||
#[cfg(not(feature = "server"))]
|
||
unreachable!()
|
||
}
|