docs(comments): 补充中文注释
This commit is contained in:
parent
26b012c40c
commit
9921f8eebf
@ -1,18 +1,32 @@
|
||||
//! 评论审核状态批量查询。
|
||||
//!
|
||||
//! 用于前端轮询刚刚提交的评论是否已通过审核,支持传入多个 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![]);
|
||||
}
|
||||
@ -27,6 +41,7 @@ pub async fn check_pending_status(ids: Vec<i64>) -> Result<Vec<PendingStatusItem
|
||||
.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)))
|
||||
|
||||
@ -1,6 +1,18 @@
|
||||
//! 发表评论接口。
|
||||
//!
|
||||
//! 校验作者信息、父评论与目标文章,生成内容哈希防止重复提交,
|
||||
//! 新评论默认进入 pending 状态等待审核。
|
||||
//! Dioxus server function,注册在 `/api` 路径下。
|
||||
//! 仅在 `feature = "server"` 启用的服务端构建中写入数据库。
|
||||
|
||||
use crate::api::comments::types::*;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// 创建一条新评论。
|
||||
///
|
||||
/// 对作者昵称、邮箱、网址与内容进行基础校验;
|
||||
/// 若目标文章未发布或父评论未通过审核,则拒绝提交;
|
||||
/// 成功后将评论置为 pending,并清空相关缓存。
|
||||
#[server(CreateComment, "/api")]
|
||||
pub async fn create_comment(
|
||||
post_id: i32,
|
||||
@ -20,6 +32,7 @@ pub async fn create_comment(
|
||||
use crate::cache;
|
||||
use crate::db::pool::get_conn;
|
||||
|
||||
// 从 FullstackContext 获取客户端 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);
|
||||
@ -35,6 +48,7 @@ pub async fn create_comment(
|
||||
}
|
||||
}
|
||||
|
||||
// 依次校验昵称、邮箱、网址与评论内容。
|
||||
if let Err(e) = validate_comment_name(&author_name) {
|
||||
return Ok(CommentResponse {
|
||||
success: false,
|
||||
@ -80,6 +94,7 @@ pub async fn create_comment(
|
||||
|
||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||||
|
||||
// 确认目标文章存在且处于已发布状态。
|
||||
let post_row = client
|
||||
.query_opt(
|
||||
"SELECT status, deleted_at FROM posts WHERE id = $1",
|
||||
@ -115,6 +130,7 @@ pub async fn create_comment(
|
||||
}
|
||||
}
|
||||
|
||||
// 若存在父评论,校验其归属文章与审核状态,并计算当前评论的嵌套深度。
|
||||
let mut depth: i32 = 0;
|
||||
if let Some(pid) = parent_id {
|
||||
let parent_row = client
|
||||
@ -177,6 +193,7 @@ pub async fn create_comment(
|
||||
}
|
||||
}
|
||||
|
||||
// 基于文章、父评论、作者与内容计算哈希,防止短时间重复提交。
|
||||
let content_hash = compute_content_hash(post_id, parent_id, &author_name, &content_md);
|
||||
|
||||
let dup: Option<i64> = client
|
||||
@ -199,8 +216,10 @@ pub async fn create_comment(
|
||||
});
|
||||
}
|
||||
|
||||
// 将 Markdown 渲染为 HTML,并通过 sanitizer 过滤危险标签。
|
||||
let content_html = crate::api::comments::markdown::render_comment_markdown(&content_md);
|
||||
|
||||
// 获取客户端 IP 与 User-Agent,用于反垃圾与审计。
|
||||
let ip_address = if let Some(ctx) = dioxus::fullstack::FullstackContext::current() {
|
||||
let parts = ctx.parts_mut();
|
||||
Some(crate::api::rate_limit::get_client_ip(&parts.headers))
|
||||
@ -219,6 +238,7 @@ pub async fn create_comment(
|
||||
None
|
||||
};
|
||||
|
||||
// 插入评论,默认状态为 pending,等待管理员审核。
|
||||
let row = client
|
||||
.query_one(
|
||||
"INSERT INTO comments \
|
||||
@ -248,8 +268,10 @@ pub async fn create_comment(
|
||||
|
||||
let comment_id: i64 = row.get(0);
|
||||
|
||||
// 根据邮箱生成 Gravatar 头像链接。
|
||||
let avatar_url = crate::api::comments::helpers::gravatar_url(&author_email);
|
||||
|
||||
// 新评论可能影响文章评论列表与计数,清空相关缓存。
|
||||
cache::invalidate_comments_by_post(post_id).await;
|
||||
cache::invalidate_comment_count(post_id).await;
|
||||
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
//! 评论模块的辅助函数:数据转换、校验、哈希与头像生成。
|
||||
//!
|
||||
//! 大部分工具函数仅在 `feature = "server"` 启用的服务端构建中使用;
|
||||
//! 校验函数同时在前端构建中保留签名,避免编译器提示未使用。
|
||||
|
||||
#![allow(clippy::unused_unit, deprecated)]
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use crate::models::comment::{AdminComment, CommentStatus, PublicComment};
|
||||
|
||||
/// 计算字符串的 MD5 哈希,用于 Gravatar。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn md5_hash(input: &str) -> String {
|
||||
use md5::Digest;
|
||||
@ -10,12 +16,14 @@ pub fn md5_hash(input: &str) -> String {
|
||||
hex::encode(hash)
|
||||
}
|
||||
|
||||
/// 根据邮箱生成 Cravatar(Gravatar 国内镜像)头像 URL。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn gravatar_url(email: &str) -> String {
|
||||
let hash = md5_hash(&email.trim().to_lowercase());
|
||||
format!("https://cravatar.cn/avatar/{}?d=mp&s=80", hash)
|
||||
}
|
||||
|
||||
/// 将数据库行转换为前端展示的公开评论结构。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn row_to_public_comment(row: &tokio_postgres::Row) -> PublicComment {
|
||||
let email: String = row.get("author_email");
|
||||
@ -36,6 +44,7 @@ pub fn row_to_public_comment(row: &tokio_postgres::Row) -> PublicComment {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将数据库行转换为后台管理使用的评论结构。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn row_to_admin_comment(row: &tokio_postgres::Row) -> AdminComment {
|
||||
let status_str: String = row.get("status");
|
||||
@ -58,6 +67,7 @@ pub fn row_to_admin_comment(row: &tokio_postgres::Row) -> AdminComment {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 UTC 时间格式化为相对时间(刚刚 / N 分钟前 / N 小时前 / N 天前 / 日期)。
|
||||
pub fn format_relative_time(dt: chrono::DateTime<chrono::Utc>) -> String {
|
||||
let now = chrono::Utc::now();
|
||||
let diff = now.signed_duration_since(dt);
|
||||
@ -75,6 +85,7 @@ pub fn format_relative_time(dt: chrono::DateTime<chrono::Utc>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// 校验评论作者昵称:非空且不超过 50 字符。
|
||||
#[allow(dead_code)]
|
||||
pub fn validate_comment_name(name: &str) -> Result<(), String> {
|
||||
let trimmed = name.trim();
|
||||
@ -87,6 +98,7 @@ pub fn validate_comment_name(name: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 校验评论作者邮箱格式。
|
||||
#[allow(dead_code)]
|
||||
pub fn validate_comment_email(email: &str) -> Result<(), String> {
|
||||
let re = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
|
||||
@ -96,6 +108,7 @@ pub fn validate_comment_email(email: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 校验评论作者网址:为空时允许,非空时必须以 http:// 或 https:// 开头且不超过 200 字符。
|
||||
#[allow(dead_code)]
|
||||
pub fn validate_comment_url(url: &str) -> Result<(), String> {
|
||||
let trimmed = url.trim();
|
||||
@ -112,6 +125,7 @@ pub fn validate_comment_url(url: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 校验评论内容:非空且不超过 10000 字符。
|
||||
#[allow(dead_code)]
|
||||
pub fn validate_comment_content(content: &str) -> Result<(), String> {
|
||||
let trimmed = content.trim();
|
||||
@ -124,6 +138,7 @@ pub fn validate_comment_content(content: &str) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 计算评论内容哈希,用于检测短时间内的重复提交。
|
||||
pub fn compute_content_hash(
|
||||
post_id: i32,
|
||||
parent_id: Option<i64>,
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
//! 评论列表查询接口:后台管理用的待审核列表、全部评论列表与待审核计数。
|
||||
//!
|
||||
//! 所有接口均需管理员身份,Dioxus server function 注册在 `/api` 路径下。
|
||||
//! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。
|
||||
|
||||
use crate::api::comments::types::*;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// 获取待审核评论分页列表。
|
||||
///
|
||||
/// 每页 20 条,按创建时间倒序排列,并返回总数用于分页。
|
||||
#[server(GetPendingComments, "/api")]
|
||||
pub async fn get_pending_comments(page: i32) -> Result<PendingCommentsResponse, ServerFnError> {
|
||||
#[cfg(feature = "server")]
|
||||
@ -48,6 +56,9 @@ pub async fn get_pending_comments(page: i32) -> Result<PendingCommentsResponse,
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// 获取待审核评论总数。
|
||||
///
|
||||
/// 优先从缓存读取,未命中时查询数据库并写入缓存。
|
||||
#[server(GetPendingCount, "/api")]
|
||||
pub async fn get_pending_count() -> Result<PendingCountResponse, ServerFnError> {
|
||||
#[cfg(feature = "server")]
|
||||
@ -82,6 +93,9 @@ pub async fn get_pending_count() -> Result<PendingCountResponse, ServerFnError>
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// 获取全部评论分页列表。
|
||||
///
|
||||
/// 支持按状态筛选;未指定状态时返回所有未删除评论。
|
||||
#[server(GetAllComments, "/api")]
|
||||
pub async fn get_all_comments(
|
||||
page: i32,
|
||||
@ -102,6 +116,7 @@ pub async fn get_all_comments(
|
||||
|
||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||||
|
||||
// 根据是否传入状态参数,分别构造 SQL 与查询条件。
|
||||
let (total, rows) = match status.as_deref() {
|
||||
Some(s) if !s.is_empty() => {
|
||||
let total: i64 = client
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
//! 评论 Markdown 渲染与 HTML 清洗。
|
||||
//!
|
||||
//! 对评论内容做轻量 Markdown 解析,限制标签白名单并转义危险字符。
|
||||
//! 仅在 `feature = "server"` 启用的服务端构建中实际执行渲染。
|
||||
|
||||
#![allow(clippy::unused_unit, deprecated)]
|
||||
|
||||
/// 转义 HTML 特殊字符,用于无语言信息的代码块。
|
||||
#[cfg(feature = "server")]
|
||||
fn html_escape(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
@ -8,11 +14,19 @@ fn html_escape(s: &str) -> String {
|
||||
.replace('"', """)
|
||||
}
|
||||
|
||||
/// 清洗评论 HTML,移除危险标签与属性。
|
||||
///
|
||||
/// 实际委托给 `crate::api::sanitizer::clean_comment_html` 实现。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn clean_comment_html(input: &str) -> String {
|
||||
crate::api::sanitizer::clean_comment_html(input)
|
||||
}
|
||||
|
||||
/// 将评论 Markdown 渲染为安全的 HTML。
|
||||
///
|
||||
/// 支持表格与删除线;标题统一渲染为 `<strong>` 以避免层级混乱;
|
||||
/// 代码块若指定语言则调用服务端高亮,否则转义 HTML;
|
||||
/// 最终调用 `clean_comment_html` 过滤危险内容。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn render_comment_markdown(md: &str) -> String {
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, TagEnd};
|
||||
@ -25,9 +39,11 @@ pub fn render_comment_markdown(md: &str) -> String {
|
||||
let mut code_lang: Option<String> = None;
|
||||
let mut code_buffer = String::new();
|
||||
|
||||
// 逐事件处理 Markdown AST,转换标题并收集代码块内容。
|
||||
for event in parser {
|
||||
match event {
|
||||
Event::Start(Tag::Heading { .. }) => {
|
||||
// 评论中不保留标题层级,统一加粗。
|
||||
events.push(Event::Start(Tag::Strong));
|
||||
}
|
||||
Event::End(TagEnd::Heading(_)) => {
|
||||
@ -45,6 +61,7 @@ pub fn render_comment_markdown(md: &str) -> String {
|
||||
code_buffer.push_str(&text);
|
||||
}
|
||||
Event::End(TagEnd::CodeBlock) => {
|
||||
// 根据是否有语言信息决定高亮或转义。
|
||||
let html = if let Some(ref lang) = code_lang {
|
||||
let highlighted =
|
||||
crate::highlight::server::highlight_code(&code_buffer, Some(lang));
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
//! 评论模块:提供评论的 CRUD、Markdown 渲染、审核状态流转与分页查询。
|
||||
//!
|
||||
//! 所有 Dioxus server function 均注册在 `/api` 路径下,供前端与服务端调用。
|
||||
//! 仅在 `feature = "server"` 启用的服务端构建中执行数据库操作与缓存失效。
|
||||
|
||||
#![allow(clippy::unused_unit, deprecated)]
|
||||
|
||||
mod check;
|
||||
@ -9,16 +14,36 @@ mod read;
|
||||
mod types;
|
||||
mod update;
|
||||
|
||||
/// 查询一组评论的当前审核状态。
|
||||
pub use check::check_pending_status;
|
||||
/// 创建一条新评论。
|
||||
pub use create::create_comment;
|
||||
/// 获取全部评论分页列表。
|
||||
#[allow(unused_imports)]
|
||||
pub use list::{get_all_comments, get_pending_comments, get_pending_count};
|
||||
pub use list::get_all_comments;
|
||||
/// 获取待审核评论分页列表。
|
||||
#[allow(unused_imports)]
|
||||
pub use list::get_pending_comments;
|
||||
/// 获取待审核评论总数。
|
||||
#[allow(unused_imports)]
|
||||
pub use list::get_pending_count;
|
||||
/// 获取指定文章的已审核评论数量。
|
||||
#[allow(unused_imports)]
|
||||
pub use read::get_comment_count;
|
||||
/// 获取指定文章的已审核评论列表。
|
||||
pub use read::get_comments;
|
||||
/// 评论 API 的请求与响应数据结构。
|
||||
pub use types::*;
|
||||
pub use update::{approve_comment, batch_update_comment_status, spam_comment, trash_comment};
|
||||
/// 通过指定评论。
|
||||
pub use update::approve_comment;
|
||||
/// 批量更新评论状态。
|
||||
pub use update::batch_update_comment_status;
|
||||
/// 将指定评论标记为垃圾评论。
|
||||
pub use update::spam_comment;
|
||||
/// 将指定评论移入回收站。
|
||||
pub use update::trash_comment;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
/// 将评论 Markdown 渲染为安全的 HTML。
|
||||
#[allow(unused_imports)]
|
||||
pub use markdown::render_comment_markdown;
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
//! 前端评论读取接口:已审核评论列表与评论计数。
|
||||
//!
|
||||
//! 结果按文章 id 缓存,Dioxus server function 注册在 `/api` 路径下。
|
||||
//! 仅在 `feature = "server"` 启用的服务端构建中查询数据库。
|
||||
|
||||
use crate::api::comments::types::*;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// 获取指定文章的已审核评论列表。
|
||||
///
|
||||
/// 优先命中缓存;按 id 升序返回,便于前端构建嵌套树。
|
||||
#[server(GetComments, "/api")]
|
||||
pub async fn get_comments(post_id: i32) -> Result<CommentTreeResponse, ServerFnError> {
|
||||
#[cfg(feature = "server")]
|
||||
@ -42,6 +50,9 @@ pub async fn get_comments(post_id: i32) -> Result<CommentTreeResponse, ServerFnE
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// 获取指定文章的已审核评论数量。
|
||||
///
|
||||
/// 优先命中缓存;未命中时执行 COUNT 查询并写入缓存。
|
||||
#[server(GetCommentCount, "/api")]
|
||||
pub async fn get_comment_count(post_id: i32) -> Result<CommentCountResponse, ServerFnError> {
|
||||
#[cfg(feature = "server")]
|
||||
|
||||
@ -1,61 +1,94 @@
|
||||
//! 评论 API 的请求与响应数据结构。
|
||||
|
||||
use crate::models::comment::{AdminComment, PublicComment};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 创建评论请求体(客户端使用)。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CreateCommentRequest {
|
||||
/// 目标文章 id。
|
||||
pub post_id: i32,
|
||||
/// 父评论 id,顶层评论为 None。
|
||||
pub parent_id: Option<i64>,
|
||||
/// 评论者昵称。
|
||||
pub author_name: String,
|
||||
/// 评论者邮箱。
|
||||
pub author_email: String,
|
||||
/// 评论者个人网址。
|
||||
pub author_url: Option<String>,
|
||||
/// 评论 Markdown 原文。
|
||||
pub content_md: String,
|
||||
}
|
||||
|
||||
/// 创建/审核/删除评论的统一响应结构。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommentResponse {
|
||||
/// 操作是否成功。
|
||||
pub success: bool,
|
||||
/// 提示信息。
|
||||
pub message: String,
|
||||
/// 错误码,成功时为 None。
|
||||
pub error_code: Option<String>,
|
||||
/// 新评论 id。
|
||||
#[serde(default)]
|
||||
pub comment_id: Option<i64>,
|
||||
/// 评论者头像 URL。
|
||||
#[serde(default)]
|
||||
pub avatar_url: Option<String>,
|
||||
/// 评论嵌套深度。
|
||||
#[serde(default)]
|
||||
pub depth: Option<i32>,
|
||||
}
|
||||
|
||||
/// 评论树响应:包含文章下的全部已审核评论。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommentTreeResponse {
|
||||
/// 评论列表。
|
||||
pub comments: Vec<PublicComment>,
|
||||
/// 评论总数。
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
/// 评论计数响应。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommentCountResponse {
|
||||
/// 评论数量。
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
/// 待审核评论列表响应。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PendingCommentsResponse {
|
||||
/// 待审核评论列表。
|
||||
pub comments: Vec<AdminComment>,
|
||||
/// 总数。
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
/// 全部评论列表响应。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AllCommentsResponse {
|
||||
/// 评论列表。
|
||||
pub comments: Vec<AdminComment>,
|
||||
/// 总数。
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
/// 待审核评论计数响应。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PendingCountResponse {
|
||||
/// 待审核数量。
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
/// 批量更新状态响应。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BatchStatusResponse {
|
||||
/// 操作是否成功。
|
||||
pub success: bool,
|
||||
/// 实际更新的行数。
|
||||
pub updated_count: i64,
|
||||
/// 提示信息。
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
//! 评论审核状态更新接口:通过、垃圾、删除与批量更新。
|
||||
//!
|
||||
//! 所有接口均需管理员身份,Dioxus server function 注册在 `/api` 路径下。
|
||||
//! 状态变更后需要清空文章评论缓存、计数缓存与待审核计数缓存。
|
||||
//! 仅在 `feature = "server"` 启用的服务端构建中写入数据库。
|
||||
|
||||
use crate::api::comments::types::*;
|
||||
use dioxus::prelude::*;
|
||||
|
||||
/// 通过指定评论。
|
||||
///
|
||||
/// 同时递归将该评论的所有 pending 父评论一并通过,确保嵌套链可见。
|
||||
#[server(ApproveComment, "/api")]
|
||||
pub async fn approve_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
||||
#[cfg(feature = "server")]
|
||||
@ -36,6 +45,7 @@ pub async fn approve_comment(id: i64) -> Result<CommentResponse, ServerFnError>
|
||||
}
|
||||
};
|
||||
|
||||
// 直接通过目标评论并记录通过时间。
|
||||
client
|
||||
.execute(
|
||||
"UPDATE comments SET status = 'approved', approved_at = NOW() WHERE id = $1",
|
||||
@ -44,6 +54,7 @@ pub async fn approve_comment(id: i64) -> Result<CommentResponse, ServerFnError>
|
||||
.await
|
||||
.map_err(AppError::query)?;
|
||||
|
||||
// 递归向上查找所有 pending 父评论并同步通过,避免子评论可见但父评论被隐藏。
|
||||
client
|
||||
.execute(
|
||||
"WITH RECURSIVE ancestors AS ( \
|
||||
@ -75,6 +86,9 @@ pub async fn approve_comment(id: i64) -> Result<CommentResponse, ServerFnError>
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// 将指定评论标记为垃圾评论。
|
||||
///
|
||||
/// 若原状态为 approved,则需要清空该文章相关缓存。
|
||||
#[server(SpamComment, "/api")]
|
||||
pub async fn spam_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
||||
#[cfg(feature = "server")]
|
||||
@ -128,6 +142,9 @@ pub async fn spam_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// 将指定评论移入回收站(软删除)。
|
||||
///
|
||||
/// 软删除会设置 deleted_at 与状态为 trash,并清空相关缓存。
|
||||
#[server(TrashComment, "/api")]
|
||||
pub async fn trash_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
||||
#[cfg(feature = "server")]
|
||||
@ -178,6 +195,10 @@ pub async fn trash_comment(id: i64) -> Result<CommentResponse, ServerFnError> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
/// 批量更新评论状态。
|
||||
///
|
||||
/// 仅接受 approved / spam / trash 三种状态;trash 会软删除并设置 deleted_at,
|
||||
/// approved 会设置 approved_at。
|
||||
#[server(BatchUpdateCommentStatus, "/api")]
|
||||
pub async fn batch_update_comment_status(
|
||||
ids: Vec<i64>,
|
||||
@ -192,6 +213,7 @@ pub async fn batch_update_comment_status(
|
||||
|
||||
let _admin = get_current_admin_user().await?;
|
||||
|
||||
// 限制可批量操作的状态,防止非法状态写入数据库。
|
||||
if !matches!(status.as_str(), "approved" | "spam" | "trash") {
|
||||
return Ok(BatchStatusResponse {
|
||||
success: false,
|
||||
@ -202,6 +224,7 @@ pub async fn batch_update_comment_status(
|
||||
|
||||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||||
|
||||
// 收集受影响的文章 id,用于后续批量失效缓存。
|
||||
let post_ids: Vec<i32> = client
|
||||
.query(
|
||||
"SELECT DISTINCT post_id FROM comments WHERE id = ANY($1)",
|
||||
@ -213,6 +236,7 @@ pub async fn batch_update_comment_status(
|
||||
.map(|r| r.get("post_id"))
|
||||
.collect();
|
||||
|
||||
// 根据目标状态设置不同的附加字段:trash 软删除,approved 记录通过时间。
|
||||
let result = if status == "trash" {
|
||||
client
|
||||
.execute(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user