docs(models, db, cache): 补充中文注释
This commit is contained in:
parent
a785683fc6
commit
2db652137d
83
src/cache.rs
83
src/cache.rs
@ -1,3 +1,9 @@
|
||||
//! 基于 moka 的内存缓存层。
|
||||
//!
|
||||
//! 仅在启用 `server` feature 时编译,为文章列表、标签、单篇文章、统计信息
|
||||
//! 以及评论相关数据提供按键缓存与失效能力。
|
||||
//! 缓存使用 `std::sync::LazyLock` 全局实例,按不同业务数据设置独立的 TTL。
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use moka::future::Cache;
|
||||
#[cfg(feature = "server")]
|
||||
@ -11,60 +17,90 @@ use crate::models::comment::PublicComment;
|
||||
use crate::models::post::{Post, PostStats, Tag};
|
||||
|
||||
// ============================================================================
|
||||
// Cache TTL Configuration
|
||||
// 缓存 TTL 配置
|
||||
// ============================================================================
|
||||
|
||||
/// 文章列表缓存 TTL:60 秒。
|
||||
#[cfg(feature = "server")]
|
||||
const TTL_POST_LIST: Duration = Duration::from_secs(60);
|
||||
|
||||
/// 标签列表缓存 TTL:300 秒。
|
||||
#[cfg(feature = "server")]
|
||||
const TTL_TAG_LIST: Duration = Duration::from_secs(300);
|
||||
|
||||
/// 单篇文章缓存 TTL:600 秒。
|
||||
#[cfg(feature = "server")]
|
||||
const TTL_SINGLE_POST: Duration = Duration::from_secs(600);
|
||||
|
||||
/// 文章统计缓存 TTL:60 秒。
|
||||
#[cfg(feature = "server")]
|
||||
const TTL_POST_STATS: Duration = Duration::from_secs(60);
|
||||
|
||||
/// 标签下文章列表缓存 TTL:120 秒。
|
||||
#[cfg(feature = "server")]
|
||||
const TTL_TAG_POSTS: Duration = Duration::from_secs(120);
|
||||
|
||||
/// 评论列表缓存 TTL:60 秒。
|
||||
#[cfg(feature = "server")]
|
||||
const TTL_COMMENTS: Duration = Duration::from_secs(60);
|
||||
|
||||
/// 评论数量缓存 TTL:60 秒。
|
||||
#[cfg(feature = "server")]
|
||||
const TTL_COMMENT_COUNT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// 待审核评论数量缓存 TTL:10 秒,因管理后台需要较实时数据。
|
||||
#[cfg(feature = "server")]
|
||||
const TTL_PENDING_COUNT: Duration = Duration::from_secs(10);
|
||||
|
||||
// ============================================================================
|
||||
// Cache Key Types
|
||||
// 缓存 Key 类型
|
||||
// ============================================================================
|
||||
|
||||
/// 统一的缓存键枚举,每个变体对应一类可缓存数据。
|
||||
#[cfg(feature = "server")]
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
|
||||
pub enum CacheKey {
|
||||
/// 已发布文章分页列表。
|
||||
PublishedPosts { page: i32, per_page: i32 },
|
||||
/// 已发布文章总数。
|
||||
TotalPublishedPosts,
|
||||
/// 全部标签。
|
||||
AllTags,
|
||||
/// 按 slug 查询的单篇文章。
|
||||
PostBySlug(String),
|
||||
/// 按标签查询的文章列表。
|
||||
PostsByTag(String),
|
||||
/// 文章统计信息。
|
||||
PostStats,
|
||||
/// 某篇文章下的评论列表。
|
||||
CommentsByPost { post_id: i32 },
|
||||
/// 某篇文章的评论数量。
|
||||
CommentCount { post_id: i32 },
|
||||
/// 待审核评论总数。
|
||||
PendingCommentCount,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cache Instances
|
||||
// 缓存实例
|
||||
// ============================================================================
|
||||
|
||||
/// 文章列表缓存类型,值为(文章列表,总数)。
|
||||
#[cfg(feature = "server")]
|
||||
pub type PostListCache = Cache<CacheKey, (Vec<Post>, i64)>;
|
||||
|
||||
/// 标签列表缓存类型。
|
||||
#[cfg(feature = "server")]
|
||||
pub type TagListCache = Cache<CacheKey, Vec<Tag>>;
|
||||
|
||||
/// 单篇文章缓存类型。
|
||||
#[cfg(feature = "server")]
|
||||
pub type SinglePostCache = Cache<CacheKey, Option<Post>>;
|
||||
|
||||
/// 文章统计缓存类型。
|
||||
#[cfg(feature = "server")]
|
||||
pub type PostStatsCache = Cache<CacheKey, PostStats>;
|
||||
|
||||
/// 全局文章列表缓存实例,最大容量 100。
|
||||
#[cfg(feature = "server")]
|
||||
static POST_LIST_CACHE: LazyLock<PostListCache> = LazyLock::new(|| {
|
||||
Cache::builder()
|
||||
@ -73,6 +109,7 @@ static POST_LIST_CACHE: LazyLock<PostListCache> = LazyLock::new(|| {
|
||||
.build()
|
||||
});
|
||||
|
||||
/// 全局标签列表缓存实例,最大容量 50。
|
||||
#[cfg(feature = "server")]
|
||||
static TAG_LIST_CACHE: LazyLock<TagListCache> = LazyLock::new(|| {
|
||||
Cache::builder()
|
||||
@ -81,6 +118,7 @@ static TAG_LIST_CACHE: LazyLock<TagListCache> = LazyLock::new(|| {
|
||||
.build()
|
||||
});
|
||||
|
||||
/// 全局单篇文章缓存实例,最大容量 200。
|
||||
#[cfg(feature = "server")]
|
||||
static SINGLE_POST_CACHE: LazyLock<SinglePostCache> = LazyLock::new(|| {
|
||||
Cache::builder()
|
||||
@ -89,6 +127,7 @@ static SINGLE_POST_CACHE: LazyLock<SinglePostCache> = LazyLock::new(|| {
|
||||
.build()
|
||||
});
|
||||
|
||||
/// 全局文章统计缓存实例,最大容量 10。
|
||||
#[cfg(feature = "server")]
|
||||
static POST_STATS_CACHE: LazyLock<PostStatsCache> = LazyLock::new(|| {
|
||||
Cache::builder()
|
||||
@ -97,6 +136,7 @@ static POST_STATS_CACHE: LazyLock<PostStatsCache> = LazyLock::new(|| {
|
||||
.build()
|
||||
});
|
||||
|
||||
/// 全局标签文章列表缓存实例,最大容量 100。
|
||||
#[cfg(feature = "server")]
|
||||
static TAG_POSTS_CACHE: LazyLock<PostListCache> = LazyLock::new(|| {
|
||||
Cache::builder()
|
||||
@ -105,12 +145,15 @@ static TAG_POSTS_CACHE: LazyLock<PostListCache> = LazyLock::new(|| {
|
||||
.build()
|
||||
});
|
||||
|
||||
/// 评论列表缓存类型。
|
||||
#[cfg(feature = "server")]
|
||||
pub type CommentListCache = Cache<CacheKey, Vec<PublicComment>>;
|
||||
|
||||
/// 评论数量缓存类型。
|
||||
#[cfg(feature = "server")]
|
||||
pub type CommentCountCache = Cache<CacheKey, i64>;
|
||||
|
||||
/// 全局评论列表缓存实例,最大容量 200。
|
||||
#[cfg(feature = "server")]
|
||||
static COMMENT_CACHE: LazyLock<CommentListCache> = LazyLock::new(|| {
|
||||
Cache::builder()
|
||||
@ -119,6 +162,7 @@ static COMMENT_CACHE: LazyLock<CommentListCache> = LazyLock::new(|| {
|
||||
.build()
|
||||
});
|
||||
|
||||
/// 全局评论数量缓存实例,最大容量 200。
|
||||
#[cfg(feature = "server")]
|
||||
static COMMENT_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| {
|
||||
Cache::builder()
|
||||
@ -127,6 +171,7 @@ static COMMENT_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| {
|
||||
.build()
|
||||
});
|
||||
|
||||
/// 全局待审核评论数量缓存实例,最大容量 10。
|
||||
#[cfg(feature = "server")]
|
||||
static PENDING_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| {
|
||||
Cache::builder()
|
||||
@ -136,19 +181,22 @@ static PENDING_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| {
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Public Cache API
|
||||
// 公共缓存 API
|
||||
// ============================================================================
|
||||
|
||||
/// 读取文章分页列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_post_list(key: &CacheKey) -> Option<(Vec<Post>, i64)> {
|
||||
POST_LIST_CACHE.get(key).await
|
||||
}
|
||||
|
||||
/// 写入文章分页列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_post_list(key: &CacheKey, posts: Vec<Post>, total: i64) {
|
||||
let _ = POST_LIST_CACHE.insert(key.clone(), (posts, total)).await;
|
||||
}
|
||||
|
||||
/// 读取已发布文章总数缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_total_published_posts() -> Option<i64> {
|
||||
POST_LIST_CACHE
|
||||
@ -157,6 +205,7 @@ pub async fn get_total_published_posts() -> Option<i64> {
|
||||
.map(|(_, total)| total)
|
||||
}
|
||||
|
||||
/// 写入已发布文章总数缓存,文章列表部分置空以节省内存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_total_published_posts(total: i64) {
|
||||
let _ = POST_LIST_CACHE
|
||||
@ -164,16 +213,19 @@ pub async fn set_total_published_posts(total: i64) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 读取全部标签缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_tag_list() -> Option<Vec<Tag>> {
|
||||
TAG_LIST_CACHE.get(&CacheKey::AllTags).await
|
||||
}
|
||||
|
||||
/// 写入全部标签缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_tag_list(tags: Vec<Tag>) {
|
||||
let _ = TAG_LIST_CACHE.insert(CacheKey::AllTags, tags).await;
|
||||
}
|
||||
|
||||
/// 按 slug 读取单篇文章缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_post_by_slug(slug: &str) -> Option<Option<Post>> {
|
||||
SINGLE_POST_CACHE
|
||||
@ -181,6 +233,7 @@ pub async fn get_post_by_slug(slug: &str) -> Option<Option<Post>> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// 按 slug 写入单篇文章缓存,None 表示文章不存在。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_post_by_slug(slug: &str, post: Option<Post>) {
|
||||
let _ = SINGLE_POST_CACHE
|
||||
@ -188,6 +241,7 @@ pub async fn set_post_by_slug(slug: &str, post: Option<Post>) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 按标签读取文章列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec<Post>, i64)> {
|
||||
TAG_POSTS_CACHE
|
||||
@ -195,6 +249,7 @@ pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec<Post>, i64)> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// 按标签写入文章列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_posts_by_tag(tag: &str, posts: Vec<Post>, total: i64) {
|
||||
let _ = TAG_POSTS_CACHE
|
||||
@ -202,30 +257,35 @@ pub async fn set_posts_by_tag(tag: &str, posts: Vec<Post>, total: i64) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 读取文章统计缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_post_stats() -> Option<PostStats> {
|
||||
POST_STATS_CACHE.get(&CacheKey::PostStats).await
|
||||
}
|
||||
|
||||
/// 写入文章统计缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_post_stats(stats: PostStats) {
|
||||
let _ = POST_STATS_CACHE.insert(CacheKey::PostStats, stats).await;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Cache Invalidation
|
||||
// 缓存失效
|
||||
// ============================================================================
|
||||
|
||||
/// 清空所有文章分页列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn invalidate_post_lists() {
|
||||
POST_LIST_CACHE.invalidate_all();
|
||||
}
|
||||
|
||||
/// 清空所有标签缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn invalidate_all_tags() {
|
||||
TAG_LIST_CACHE.invalidate_all();
|
||||
}
|
||||
|
||||
/// 按 slug 失效单篇文章缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn invalidate_post_by_slug(slug: &str) {
|
||||
SINGLE_POST_CACHE
|
||||
@ -233,6 +293,7 @@ pub async fn invalidate_post_by_slug(slug: &str) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 按标签失效文章列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn invalidate_posts_by_tag(tag: &str) {
|
||||
TAG_POSTS_CACHE
|
||||
@ -240,11 +301,13 @@ pub async fn invalidate_posts_by_tag(tag: &str) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 清空文章统计缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn invalidate_post_stats() {
|
||||
POST_STATS_CACHE.invalidate_all();
|
||||
}
|
||||
|
||||
/// 清空所有文章相关缓存(列表、标签、单篇、统计、标签文章)。
|
||||
#[cfg(feature = "server")]
|
||||
pub fn invalidate_all_post_caches() {
|
||||
POST_LIST_CACHE.invalidate_all();
|
||||
@ -254,6 +317,7 @@ pub fn invalidate_all_post_caches() {
|
||||
TAG_POSTS_CACHE.invalidate_all();
|
||||
}
|
||||
|
||||
/// 按文章主键读取评论列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_comments_by_post(post_id: i32) -> Option<Vec<PublicComment>> {
|
||||
COMMENT_CACHE
|
||||
@ -261,6 +325,7 @@ pub async fn get_comments_by_post(post_id: i32) -> Option<Vec<PublicComment>> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// 按文章主键写入评论列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_comments_by_post(post_id: i32, comments: Vec<PublicComment>) {
|
||||
let _ = COMMENT_CACHE
|
||||
@ -268,6 +333,7 @@ pub async fn set_comments_by_post(post_id: i32, comments: Vec<PublicComment>) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 按文章主键读取评论数量缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_comment_count(post_id: i32) -> Option<i64> {
|
||||
COMMENT_COUNT_CACHE
|
||||
@ -275,6 +341,7 @@ pub async fn get_comment_count(post_id: i32) -> Option<i64> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// 按文章主键写入评论数量缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_comment_count(post_id: i32, count: i64) {
|
||||
let _ = COMMENT_COUNT_CACHE
|
||||
@ -282,6 +349,7 @@ pub async fn set_comment_count(post_id: i32, count: i64) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 读取待审核评论总数缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn get_pending_count() -> Option<i64> {
|
||||
PENDING_COUNT_CACHE
|
||||
@ -289,6 +357,7 @@ pub async fn get_pending_count() -> Option<i64> {
|
||||
.await
|
||||
}
|
||||
|
||||
/// 写入待审核评论总数缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn set_pending_count(count: i64) {
|
||||
let _ = PENDING_COUNT_CACHE
|
||||
@ -296,6 +365,7 @@ pub async fn set_pending_count(count: i64) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 按文章主键失效评论列表缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn invalidate_comments_by_post(post_id: i32) {
|
||||
COMMENT_CACHE
|
||||
@ -303,6 +373,7 @@ pub async fn invalidate_comments_by_post(post_id: i32) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 按文章主键失效评论数量缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn invalidate_comment_count(post_id: i32) {
|
||||
COMMENT_COUNT_CACHE
|
||||
@ -310,6 +381,7 @@ pub async fn invalidate_comment_count(post_id: i32) {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 失效待审核评论总数缓存。
|
||||
#[cfg(feature = "server")]
|
||||
pub async fn invalidate_pending_count() {
|
||||
PENDING_COUNT_CACHE
|
||||
@ -317,6 +389,7 @@ pub async fn invalidate_pending_count() {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// 清空所有评论相关缓存。
|
||||
#[cfg(feature = "server")]
|
||||
#[allow(dead_code)]
|
||||
pub async fn invalidate_all_comment_caches() {
|
||||
|
||||
@ -1,17 +1,41 @@
|
||||
//! 数据库连接模块。
|
||||
//!
|
||||
//! 本模块根据 `server` feature 的启用情况提供两套实现:
|
||||
//! - 启用 `server` 时,从 `pool` 子模块暴露真实的 PostgreSQL 连接池;
|
||||
//! - 未启用 `server` 时(例如仅编译 WASM 前端),提供一个 `DummyPool` stub,
|
||||
//! 使代码在缺少数据库依赖的情况下仍能编译通过。
|
||||
//!
|
||||
//! 这种 stub 模式是 Dioxus fullstack 项目的常见做法:服务端函数体在 WASM 构建时会被剥离,
|
||||
//! 但模块结构必须保持一致,因此需要一个占位实现来满足编译器的符号解析。
|
||||
|
||||
/// 真实的 PostgreSQL 连接池实现,仅在启用 server feature 时编译。
|
||||
#[cfg(feature = "server")]
|
||||
pub mod pool;
|
||||
|
||||
/// 占位连接池实现,仅在不启用 server feature 时编译。
|
||||
///
|
||||
/// `DummyPool` 是一个最小 stub:它提供与真实连接池相同的公开接口形状
|
||||
///(如 `get` 与 `get_conn`),但所有方法都直接返回错误。
|
||||
/// 这样可以在不引入 deadpool-postgres、tokio-postgres 等依赖的情况下,
|
||||
/// 让依赖 `db::pool::DB_POOL` 的代码通过前端编译。
|
||||
/// **请勿删除此 stub**,否则非 server 构建将无法通过编译。
|
||||
#[cfg(not(feature = "server"))]
|
||||
#[allow(dead_code)]
|
||||
pub mod pool {
|
||||
/// 占位连接池,无实际数据库连接能力。
|
||||
pub struct DummyPool;
|
||||
|
||||
impl DummyPool {
|
||||
/// 占位方法,永远返回错误。
|
||||
pub async fn get(&self) -> Result<(), ()> {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 占位全局连接池实例。
|
||||
pub static DB_POOL: DummyPool = DummyPool;
|
||||
|
||||
/// 占位函数,永远返回错误。
|
||||
pub async fn get_conn() -> Result<(), ()> {
|
||||
Err(())
|
||||
}
|
||||
|
||||
@ -1,15 +1,25 @@
|
||||
//! PostgreSQL 连接池实现。
|
||||
//!
|
||||
//! 仅在启用 `server` feature 时编译,使用 deadpool-postgres 管理连接池,
|
||||
//! 并通过 `std::sync::LazyLock` 在首次访问时延迟初始化全局连接池。
|
||||
//! `get_conn` 失败时按固定 2 秒间隔进行简单重试,以应对瞬时连接失败。
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod};
|
||||
use tokio_postgres::NoTls;
|
||||
|
||||
/// 全局数据库连接池,基于 `DATABASE_URL` 环境变量延迟初始化。
|
||||
///
|
||||
/// 最大连接数可通过 `DB_POOL_SIZE` 环境变量调整,默认 20。
|
||||
pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| {
|
||||
let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set");
|
||||
let pg_cfg = db_url
|
||||
.parse::<tokio_postgres::Config>()
|
||||
.expect("Invalid DATABASE_URL format");
|
||||
|
||||
// 使用 Fast 回收策略,避免每次归还连接时执行额外查询。
|
||||
let mgr_cfg = ManagerConfig {
|
||||
recycling_method: RecyclingMethod::Fast,
|
||||
};
|
||||
@ -26,9 +36,15 @@ pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| {
|
||||
.expect("Failed to create database connection pool")
|
||||
});
|
||||
|
||||
/// 最大重试次数。
|
||||
const MAX_RETRIES: u32 = 3;
|
||||
|
||||
/// 每次重试之间的固定等待时间。
|
||||
const RETRY_DELAY: Duration = Duration::from_secs(2);
|
||||
|
||||
/// 从全局连接池获取一个数据库连接,失败时按 `MAX_RETRIES` 进行重试。
|
||||
///
|
||||
/// 若所有尝试均失败,则返回最后一次遇到的 PoolError。
|
||||
pub async fn get_conn() -> Result<deadpool_postgres::Object, deadpool_postgres::PoolError> {
|
||||
let mut last_err = None;
|
||||
for attempt in 0..=MAX_RETRIES {
|
||||
|
||||
@ -1,16 +1,27 @@
|
||||
//! 评论模型。
|
||||
//!
|
||||
//! 定义评论状态、服务端内部使用的 Comment 结构体,
|
||||
//! 以及面向前端展示的 PublicComment 与面向后台管理的 AdminComment。
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 评论审核状态枚举,序列化时使用小写字符串。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CommentStatus {
|
||||
/// 待审核。
|
||||
Pending,
|
||||
/// 已通过。
|
||||
Approved,
|
||||
/// 垃圾评论。
|
||||
Spam,
|
||||
/// 已删除/回收站。
|
||||
Trash,
|
||||
}
|
||||
|
||||
impl CommentStatus {
|
||||
/// 将数据库或 API 中的状态字符串解析为 CommentStatus,未知值默认回退到 Pending。
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"approved" => Self::Approved,
|
||||
@ -20,6 +31,7 @@ impl CommentStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 CommentStatus 序列化为小写字符串。
|
||||
#[allow(dead_code)]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
@ -31,55 +43,99 @@ impl CommentStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// 服务端内部使用的完整评论结构体,仅在启用 server feature 时编译。
|
||||
///
|
||||
/// 包含作者邮箱、IP、User-Agent 等敏感或管理字段,不直接返回给前端。
|
||||
#[cfg(feature = "server")]
|
||||
#[allow(dead_code)]
|
||||
pub struct Comment {
|
||||
/// 评论主键。
|
||||
pub id: i64,
|
||||
/// 所属文章主键。
|
||||
pub post_id: i32,
|
||||
/// 父评论主键,用于实现嵌套回复。
|
||||
pub parent_id: Option<i64>,
|
||||
/// 嵌套深度,0 表示顶层评论。
|
||||
pub depth: i32,
|
||||
/// 评论者名称。
|
||||
pub author_name: String,
|
||||
/// 评论者邮箱,用于 Gravatar 与后台联系。
|
||||
pub author_email: String,
|
||||
/// 评论者个人主页 URL。
|
||||
pub author_url: Option<String>,
|
||||
/// 原始 Markdown 内容。
|
||||
pub content_md: String,
|
||||
/// 渲染后的 HTML 内容。
|
||||
pub content_html: Option<String>,
|
||||
/// 内容哈希,用于检测重复或垃圾评论。
|
||||
pub content_hash: Option<String>,
|
||||
/// 当前审核状态。
|
||||
pub status: CommentStatus,
|
||||
/// 评论者 IP 地址。
|
||||
pub ip_address: Option<String>,
|
||||
/// 评论者浏览器 User-Agent。
|
||||
pub user_agent: Option<String>,
|
||||
/// 审核通过时间。
|
||||
pub approved_at: Option<DateTime<Utc>>,
|
||||
/// 评论创建时间。
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 评论最后更新时间。
|
||||
pub updated_at: DateTime<Utc>,
|
||||
/// 软删除时间。
|
||||
pub deleted_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// 面向前端展示的评论结构体,已脱敏。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PublicComment {
|
||||
/// 评论主键。
|
||||
pub id: i64,
|
||||
/// 父评论主键,None 表示顶层评论。
|
||||
pub parent_id: Option<i64>,
|
||||
/// 嵌套深度。
|
||||
pub depth: i32,
|
||||
/// 评论者名称。
|
||||
pub author_name: String,
|
||||
/// 评论者个人主页 URL。
|
||||
pub author_url: Option<String>,
|
||||
/// 评论者头像 URL。
|
||||
pub avatar_url: String,
|
||||
/// 渲染后的 HTML 内容。
|
||||
pub content_html: Option<String>,
|
||||
/// 用于展示的人类可读创建时间。
|
||||
pub created_at: String,
|
||||
/// ISO 8601 格式的创建时间。
|
||||
pub created_at_iso: String,
|
||||
}
|
||||
|
||||
/// 面向后台管理的评论结构体,包含审核所需字段。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AdminComment {
|
||||
/// 评论主键。
|
||||
pub id: i64,
|
||||
/// 所属文章主键。
|
||||
pub post_id: i32,
|
||||
/// 所属文章标题。
|
||||
pub post_title: String,
|
||||
/// 所属文章 slug。
|
||||
pub post_slug: String,
|
||||
/// 父评论主键。
|
||||
pub parent_id: Option<i64>,
|
||||
/// 嵌套深度。
|
||||
pub depth: i32,
|
||||
/// 评论者名称。
|
||||
pub author_name: String,
|
||||
/// 评论者邮箱。
|
||||
pub author_email: String,
|
||||
/// 评论者个人主页 URL。
|
||||
pub author_url: Option<String>,
|
||||
/// 评论者头像 URL。
|
||||
pub avatar_url: String,
|
||||
/// 原始 Markdown 内容。
|
||||
pub content_md: String,
|
||||
/// 当前审核状态。
|
||||
pub status: CommentStatus,
|
||||
/// 评论创建时间。
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
//! 数据模型模块。
|
||||
//!
|
||||
//! 定义博客系统使用的核心领域模型,包括文章(Post)、用户(User)与评论(Comment)。
|
||||
//! 这些结构体通过 serde 在服务端与客户端之间共享序列化。
|
||||
|
||||
/// 评论模型及其状态枚举。
|
||||
pub mod comment;
|
||||
/// 文章模型、文章状态、标签与统计信息。
|
||||
pub mod post;
|
||||
/// 用户模型、用户角色与可公开用户信息。
|
||||
pub mod user;
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
//! 文章模型。
|
||||
//!
|
||||
//! 定义文章状态、文章结构体、标签、统计信息以及前后导航结构体。
|
||||
//! Post 结构体在服务端渲染、客户端展示以及缓存层之间共享。
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 文章发布状态枚举。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum PostStatus {
|
||||
/// 草稿,仅管理员可见。
|
||||
Draft,
|
||||
/// 已发布,面向读者公开。
|
||||
Published,
|
||||
}
|
||||
|
||||
impl PostStatus {
|
||||
/// 将状态序列化为数据库或 API 使用的小写字符串。
|
||||
#[allow(dead_code)]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
@ -16,6 +25,7 @@ impl PostStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将字符串解析为 PostStatus,无法识别时返回 None。
|
||||
#[allow(dead_code)]
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
@ -26,35 +36,56 @@ impl PostStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// 文章领域模型。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Post {
|
||||
/// 文章主键。
|
||||
pub id: i32,
|
||||
/// 作者用户主键。
|
||||
pub author_id: i32,
|
||||
/// 文章标题。
|
||||
pub title: String,
|
||||
/// URL slug,用于生成文章链接。
|
||||
pub slug: String,
|
||||
/// 摘要,可选。
|
||||
pub summary: Option<String>,
|
||||
/// 原始 Markdown 内容。
|
||||
pub content_md: String,
|
||||
/// 渲染后的 HTML 内容,可选。
|
||||
pub content_html: Option<String>,
|
||||
/// 文章发布状态。
|
||||
pub status: PostStatus,
|
||||
/// 正式发布时间,None 表示尚未发布。
|
||||
pub published_at: Option<DateTime<Utc>>,
|
||||
/// 创建时间。
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// 最后更新时间。
|
||||
pub updated_at: DateTime<Utc>,
|
||||
/// 关联标签列表。
|
||||
pub tags: Vec<String>,
|
||||
/// 封面图片 URL。
|
||||
pub cover_image: Option<String>,
|
||||
/// 预计阅读时间(分钟)。
|
||||
pub reading_time: u32,
|
||||
/// 字数统计。
|
||||
pub word_count: u32,
|
||||
/// 目录 HTML。
|
||||
pub toc_html: Option<String>,
|
||||
/// 上一篇文章导航信息。
|
||||
pub prev_post: Option<PostNav>,
|
||||
/// 下一篇文章导航信息。
|
||||
pub next_post: Option<PostNav>,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
/// 返回用于展示的文章日期:优先使用发布时间,否则回退到创建时间。
|
||||
pub fn formatted_date(&self) -> String {
|
||||
self.published_at
|
||||
.map(|d| d.format("%Y-%m-%d").to_string())
|
||||
.unwrap_or_else(|| self.created_at.format("%Y-%m-%d").to_string())
|
||||
}
|
||||
|
||||
/// 返回中文状态标签。
|
||||
pub fn status_label(&self) -> &'static str {
|
||||
match self.status {
|
||||
PostStatus::Published => "已发布",
|
||||
@ -62,6 +93,7 @@ impl Post {
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回状态文本在 light/dark 模式下的 Tailwind 颜色类。
|
||||
pub fn status_class(&self) -> &'static str {
|
||||
match self.status {
|
||||
PostStatus::Published => "text-green-600 dark:text-green-400",
|
||||
@ -69,6 +101,7 @@ impl Post {
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回状态徽章在 light/dark 模式下的 Tailwind 背景与颜色类。
|
||||
pub fn status_badge_class(&self) -> &'static str {
|
||||
match self.status {
|
||||
PostStatus::Published => {
|
||||
@ -79,23 +112,34 @@ impl Post {
|
||||
}
|
||||
}
|
||||
|
||||
/// 前后文章导航结构体。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PostNav {
|
||||
/// 文章标题。
|
||||
pub title: String,
|
||||
/// 文章 slug。
|
||||
pub slug: String,
|
||||
}
|
||||
|
||||
/// 标签领域模型。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Tag {
|
||||
/// 标签主键。
|
||||
pub id: i32,
|
||||
/// 标签名称。
|
||||
pub name: String,
|
||||
/// 关联文章数量。
|
||||
pub post_count: i64,
|
||||
}
|
||||
|
||||
/// 文章统计信息。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PostStats {
|
||||
/// 文章总数。
|
||||
pub total: i64,
|
||||
/// 草稿数量。
|
||||
pub drafts: i64,
|
||||
/// 已发布数量。
|
||||
pub published: i64,
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
//! 用户模型。
|
||||
//!
|
||||
//! 定义用户角色、内部用户结构体以及可暴露给前端的 PublicUser。
|
||||
//! User 包含密码哈希等敏感字段,PublicUser 用于在 API 中隐藏这些字段。
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 用户角色枚举。
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum UserRole {
|
||||
/// 管理员,拥有全部后台权限。
|
||||
Admin,
|
||||
/// 被禁用的用户,无法登录或操作。
|
||||
Blocked,
|
||||
}
|
||||
|
||||
impl UserRole {
|
||||
/// 将数据库中的角色字符串解析为 UserRole,无法识别时返回 None。
|
||||
#[allow(dead_code)]
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
@ -18,26 +27,40 @@ impl UserRole {
|
||||
}
|
||||
}
|
||||
|
||||
/// 内部使用的完整用户结构体,包含敏感字段。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
/// 用户主键。
|
||||
pub id: i32,
|
||||
/// 用户名,用于登录与展示。
|
||||
pub username: String,
|
||||
/// 邮箱地址。
|
||||
pub email: String,
|
||||
/// Argon2 密码哈希,不允许直接序列化返回给前端。
|
||||
pub password_hash: String,
|
||||
/// 用户角色。
|
||||
pub role: UserRole,
|
||||
/// 账户创建时间。
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 可公开的用户信息,从 User 转换而来,不含密码哈希。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PublicUser {
|
||||
/// 用户主键。
|
||||
pub id: i32,
|
||||
/// 用户名。
|
||||
pub username: String,
|
||||
/// 邮箱地址。
|
||||
pub email: String,
|
||||
/// 用户角色。
|
||||
pub role: UserRole,
|
||||
/// 账户创建时间。
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl From<User> for PublicUser {
|
||||
/// 将 User 转换为 PublicUser,丢弃 password_hash 字段。
|
||||
fn from(u: User) -> Self {
|
||||
PublicUser {
|
||||
id: u.id,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user