diff --git a/src/cache.rs b/src/cache.rs index 62ee5d1..77cd385 100644 --- a/src/cache.rs +++ b/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, i64)>; +/// 标签列表缓存类型。 #[cfg(feature = "server")] pub type TagListCache = Cache>; +/// 单篇文章缓存类型。 #[cfg(feature = "server")] pub type SinglePostCache = Cache>; +/// 文章统计缓存类型。 #[cfg(feature = "server")] pub type PostStatsCache = Cache; +/// 全局文章列表缓存实例,最大容量 100。 #[cfg(feature = "server")] static POST_LIST_CACHE: LazyLock = LazyLock::new(|| { Cache::builder() @@ -73,6 +109,7 @@ static POST_LIST_CACHE: LazyLock = LazyLock::new(|| { .build() }); +/// 全局标签列表缓存实例,最大容量 50。 #[cfg(feature = "server")] static TAG_LIST_CACHE: LazyLock = LazyLock::new(|| { Cache::builder() @@ -81,6 +118,7 @@ static TAG_LIST_CACHE: LazyLock = LazyLock::new(|| { .build() }); +/// 全局单篇文章缓存实例,最大容量 200。 #[cfg(feature = "server")] static SINGLE_POST_CACHE: LazyLock = LazyLock::new(|| { Cache::builder() @@ -89,6 +127,7 @@ static SINGLE_POST_CACHE: LazyLock = LazyLock::new(|| { .build() }); +/// 全局文章统计缓存实例,最大容量 10。 #[cfg(feature = "server")] static POST_STATS_CACHE: LazyLock = LazyLock::new(|| { Cache::builder() @@ -97,6 +136,7 @@ static POST_STATS_CACHE: LazyLock = LazyLock::new(|| { .build() }); +/// 全局标签文章列表缓存实例,最大容量 100。 #[cfg(feature = "server")] static TAG_POSTS_CACHE: LazyLock = LazyLock::new(|| { Cache::builder() @@ -105,12 +145,15 @@ static TAG_POSTS_CACHE: LazyLock = LazyLock::new(|| { .build() }); +/// 评论列表缓存类型。 #[cfg(feature = "server")] pub type CommentListCache = Cache>; +/// 评论数量缓存类型。 #[cfg(feature = "server")] pub type CommentCountCache = Cache; +/// 全局评论列表缓存实例,最大容量 200。 #[cfg(feature = "server")] static COMMENT_CACHE: LazyLock = LazyLock::new(|| { Cache::builder() @@ -119,6 +162,7 @@ static COMMENT_CACHE: LazyLock = LazyLock::new(|| { .build() }); +/// 全局评论数量缓存实例,最大容量 200。 #[cfg(feature = "server")] static COMMENT_COUNT_CACHE: LazyLock = LazyLock::new(|| { Cache::builder() @@ -127,6 +171,7 @@ static COMMENT_COUNT_CACHE: LazyLock = LazyLock::new(|| { .build() }); +/// 全局待审核评论数量缓存实例,最大容量 10。 #[cfg(feature = "server")] static PENDING_COUNT_CACHE: LazyLock = LazyLock::new(|| { Cache::builder() @@ -136,19 +181,22 @@ static PENDING_COUNT_CACHE: LazyLock = LazyLock::new(|| { }); // ============================================================================ -// Public Cache API +// 公共缓存 API // ============================================================================ +/// 读取文章分页列表缓存。 #[cfg(feature = "server")] pub async fn get_post_list(key: &CacheKey) -> Option<(Vec, i64)> { POST_LIST_CACHE.get(key).await } +/// 写入文章分页列表缓存。 #[cfg(feature = "server")] pub async fn set_post_list(key: &CacheKey, posts: Vec, total: i64) { let _ = POST_LIST_CACHE.insert(key.clone(), (posts, total)).await; } +/// 读取已发布文章总数缓存。 #[cfg(feature = "server")] pub async fn get_total_published_posts() -> Option { POST_LIST_CACHE @@ -157,6 +205,7 @@ pub async fn get_total_published_posts() -> Option { .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> { TAG_LIST_CACHE.get(&CacheKey::AllTags).await } +/// 写入全部标签缓存。 #[cfg(feature = "server")] pub async fn set_tag_list(tags: Vec) { let _ = TAG_LIST_CACHE.insert(CacheKey::AllTags, tags).await; } +/// 按 slug 读取单篇文章缓存。 #[cfg(feature = "server")] pub async fn get_post_by_slug(slug: &str) -> Option> { SINGLE_POST_CACHE @@ -181,6 +233,7 @@ pub async fn get_post_by_slug(slug: &str) -> Option> { .await } +/// 按 slug 写入单篇文章缓存,None 表示文章不存在。 #[cfg(feature = "server")] pub async fn set_post_by_slug(slug: &str, post: Option) { let _ = SINGLE_POST_CACHE @@ -188,6 +241,7 @@ pub async fn set_post_by_slug(slug: &str, post: Option) { .await; } +/// 按标签读取文章列表缓存。 #[cfg(feature = "server")] pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec, i64)> { TAG_POSTS_CACHE @@ -195,6 +249,7 @@ pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec, i64)> { .await } +/// 按标签写入文章列表缓存。 #[cfg(feature = "server")] pub async fn set_posts_by_tag(tag: &str, posts: Vec, total: i64) { let _ = TAG_POSTS_CACHE @@ -202,30 +257,35 @@ pub async fn set_posts_by_tag(tag: &str, posts: Vec, total: i64) { .await; } +/// 读取文章统计缓存。 #[cfg(feature = "server")] pub async fn get_post_stats() -> Option { 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> { COMMENT_CACHE @@ -261,6 +325,7 @@ pub async fn get_comments_by_post(post_id: i32) -> Option> { .await } +/// 按文章主键写入评论列表缓存。 #[cfg(feature = "server")] pub async fn set_comments_by_post(post_id: i32, comments: Vec) { let _ = COMMENT_CACHE @@ -268,6 +333,7 @@ pub async fn set_comments_by_post(post_id: i32, comments: Vec) { .await; } +/// 按文章主键读取评论数量缓存。 #[cfg(feature = "server")] pub async fn get_comment_count(post_id: i32) -> Option { COMMENT_COUNT_CACHE @@ -275,6 +341,7 @@ pub async fn get_comment_count(post_id: i32) -> Option { .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 { PENDING_COUNT_CACHE @@ -289,6 +357,7 @@ pub async fn get_pending_count() -> Option { .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() { diff --git a/src/db/mod.rs b/src/db/mod.rs index 08c09bd..fe935ab 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -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(()) } diff --git a/src/db/pool.rs b/src/db/pool.rs index 5dd1026..11bab36 100644 --- a/src/db/pool.rs +++ b/src/db/pool.rs @@ -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 = LazyLock::new(|| { let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set"); let pg_cfg = db_url .parse::() .expect("Invalid DATABASE_URL format"); + // 使用 Fast 回收策略,避免每次归还连接时执行额外查询。 let mgr_cfg = ManagerConfig { recycling_method: RecyclingMethod::Fast, }; @@ -26,9 +36,15 @@ pub static DB_POOL: LazyLock = 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 { let mut last_err = None; for attempt in 0..=MAX_RETRIES { diff --git a/src/models/comment.rs b/src/models/comment.rs index 8cd54d6..4cb1c66 100644 --- a/src/models/comment.rs +++ b/src/models/comment.rs @@ -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, + /// 嵌套深度,0 表示顶层评论。 pub depth: i32, + /// 评论者名称。 pub author_name: String, + /// 评论者邮箱,用于 Gravatar 与后台联系。 pub author_email: String, + /// 评论者个人主页 URL。 pub author_url: Option, + /// 原始 Markdown 内容。 pub content_md: String, + /// 渲染后的 HTML 内容。 pub content_html: Option, + /// 内容哈希,用于检测重复或垃圾评论。 pub content_hash: Option, + /// 当前审核状态。 pub status: CommentStatus, + /// 评论者 IP 地址。 pub ip_address: Option, + /// 评论者浏览器 User-Agent。 pub user_agent: Option, + /// 审核通过时间。 pub approved_at: Option>, + /// 评论创建时间。 pub created_at: DateTime, + /// 评论最后更新时间。 pub updated_at: DateTime, + /// 软删除时间。 pub deleted_at: Option>, } +/// 面向前端展示的评论结构体,已脱敏。 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PublicComment { + /// 评论主键。 pub id: i64, + /// 父评论主键,None 表示顶层评论。 pub parent_id: Option, + /// 嵌套深度。 pub depth: i32, + /// 评论者名称。 pub author_name: String, + /// 评论者个人主页 URL。 pub author_url: Option, + /// 评论者头像 URL。 pub avatar_url: String, + /// 渲染后的 HTML 内容。 pub content_html: Option, + /// 用于展示的人类可读创建时间。 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, + /// 嵌套深度。 pub depth: i32, + /// 评论者名称。 pub author_name: String, + /// 评论者邮箱。 pub author_email: String, + /// 评论者个人主页 URL。 pub author_url: Option, + /// 评论者头像 URL。 pub avatar_url: String, + /// 原始 Markdown 内容。 pub content_md: String, + /// 当前审核状态。 pub status: CommentStatus, + /// 评论创建时间。 pub created_at: DateTime, } diff --git a/src/models/mod.rs b/src/models/mod.rs index 2a5d7a6..4d54b91 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,11 @@ +//! 数据模型模块。 +//! +//! 定义博客系统使用的核心领域模型,包括文章(Post)、用户(User)与评论(Comment)。 +//! 这些结构体通过 serde 在服务端与客户端之间共享序列化。 + +/// 评论模型及其状态枚举。 pub mod comment; +/// 文章模型、文章状态、标签与统计信息。 pub mod post; +/// 用户模型、用户角色与可公开用户信息。 pub mod user; diff --git a/src/models/post.rs b/src/models/post.rs index 3d3b4f5..42b0302 100644 --- a/src/models/post.rs +++ b/src/models/post.rs @@ -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 { 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, + /// 原始 Markdown 内容。 pub content_md: String, + /// 渲染后的 HTML 内容,可选。 pub content_html: Option, + /// 文章发布状态。 pub status: PostStatus, + /// 正式发布时间,None 表示尚未发布。 pub published_at: Option>, + /// 创建时间。 pub created_at: DateTime, + /// 最后更新时间。 pub updated_at: DateTime, + /// 关联标签列表。 pub tags: Vec, + /// 封面图片 URL。 pub cover_image: Option, + /// 预计阅读时间(分钟)。 pub reading_time: u32, + /// 字数统计。 pub word_count: u32, + /// 目录 HTML。 pub toc_html: Option, + /// 上一篇文章导航信息。 pub prev_post: Option, + /// 下一篇文章导航信息。 pub next_post: Option, } 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, } diff --git a/src/models/user.rs b/src/models/user.rs index 288698f..29fdf27 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -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 { 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, } +/// 可公开的用户信息,从 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, } impl From for PublicUser { + /// 将 User 转换为 PublicUser,丢弃 password_hash 字段。 fn from(u: User) -> Self { PublicUser { id: u.id,