docs(models, db, cache): 补充中文注释

This commit is contained in:
xfy 2026-06-12 18:54:21 +08:00
parent a785683fc6
commit 2db652137d
7 changed files with 249 additions and 5 deletions

View File

@ -1,3 +1,9 @@
//! 基于 moka 的内存缓存层。
//!
//! 仅在启用 `server` feature 时编译,为文章列表、标签、单篇文章、统计信息
//! 以及评论相关数据提供按键缓存与失效能力。
//! 缓存使用 `std::sync::LazyLock` 全局实例,按不同业务数据设置独立的 TTL。
#[cfg(feature = "server")] #[cfg(feature = "server")]
use moka::future::Cache; use moka::future::Cache;
#[cfg(feature = "server")] #[cfg(feature = "server")]
@ -11,60 +17,90 @@ use crate::models::comment::PublicComment;
use crate::models::post::{Post, PostStats, Tag}; use crate::models::post::{Post, PostStats, Tag};
// ============================================================================ // ============================================================================
// Cache TTL Configuration // 缓存 TTL 配置
// ============================================================================ // ============================================================================
/// 文章列表缓存 TTL60 秒。
#[cfg(feature = "server")] #[cfg(feature = "server")]
const TTL_POST_LIST: Duration = Duration::from_secs(60); const TTL_POST_LIST: Duration = Duration::from_secs(60);
/// 标签列表缓存 TTL300 秒。
#[cfg(feature = "server")] #[cfg(feature = "server")]
const TTL_TAG_LIST: Duration = Duration::from_secs(300); const TTL_TAG_LIST: Duration = Duration::from_secs(300);
/// 单篇文章缓存 TTL600 秒。
#[cfg(feature = "server")] #[cfg(feature = "server")]
const TTL_SINGLE_POST: Duration = Duration::from_secs(600); const TTL_SINGLE_POST: Duration = Duration::from_secs(600);
/// 文章统计缓存 TTL60 秒。
#[cfg(feature = "server")] #[cfg(feature = "server")]
const TTL_POST_STATS: Duration = Duration::from_secs(60); const TTL_POST_STATS: Duration = Duration::from_secs(60);
/// 标签下文章列表缓存 TTL120 秒。
#[cfg(feature = "server")] #[cfg(feature = "server")]
const TTL_TAG_POSTS: Duration = Duration::from_secs(120); const TTL_TAG_POSTS: Duration = Duration::from_secs(120);
/// 评论列表缓存 TTL60 秒。
#[cfg(feature = "server")] #[cfg(feature = "server")]
const TTL_COMMENTS: Duration = Duration::from_secs(60); const TTL_COMMENTS: Duration = Duration::from_secs(60);
/// 评论数量缓存 TTL60 秒。
#[cfg(feature = "server")] #[cfg(feature = "server")]
const TTL_COMMENT_COUNT: Duration = Duration::from_secs(60); const TTL_COMMENT_COUNT: Duration = Duration::from_secs(60);
/// 待审核评论数量缓存 TTL10 秒,因管理后台需要较实时数据。
#[cfg(feature = "server")] #[cfg(feature = "server")]
const TTL_PENDING_COUNT: Duration = Duration::from_secs(10); const TTL_PENDING_COUNT: Duration = Duration::from_secs(10);
// ============================================================================ // ============================================================================
// Cache Key Types // 缓存 Key 类型
// ============================================================================ // ============================================================================
/// 统一的缓存键枚举,每个变体对应一类可缓存数据。
#[cfg(feature = "server")] #[cfg(feature = "server")]
#[derive(Debug, Clone, Hash, Eq, PartialEq)] #[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum CacheKey { pub enum CacheKey {
/// 已发布文章分页列表。
PublishedPosts { page: i32, per_page: i32 }, PublishedPosts { page: i32, per_page: i32 },
/// 已发布文章总数。
TotalPublishedPosts, TotalPublishedPosts,
/// 全部标签。
AllTags, AllTags,
/// 按 slug 查询的单篇文章。
PostBySlug(String), PostBySlug(String),
/// 按标签查询的文章列表。
PostsByTag(String), PostsByTag(String),
/// 文章统计信息。
PostStats, PostStats,
/// 某篇文章下的评论列表。
CommentsByPost { post_id: i32 }, CommentsByPost { post_id: i32 },
/// 某篇文章的评论数量。
CommentCount { post_id: i32 }, CommentCount { post_id: i32 },
/// 待审核评论总数。
PendingCommentCount, PendingCommentCount,
} }
// ============================================================================ // ============================================================================
// Cache Instances // 缓存实例
// ============================================================================ // ============================================================================
/// 文章列表缓存类型,值为(文章列表,总数)。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub type PostListCache = Cache<CacheKey, (Vec<Post>, i64)>; pub type PostListCache = Cache<CacheKey, (Vec<Post>, i64)>;
/// 标签列表缓存类型。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub type TagListCache = Cache<CacheKey, Vec<Tag>>; pub type TagListCache = Cache<CacheKey, Vec<Tag>>;
/// 单篇文章缓存类型。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub type SinglePostCache = Cache<CacheKey, Option<Post>>; pub type SinglePostCache = Cache<CacheKey, Option<Post>>;
/// 文章统计缓存类型。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub type PostStatsCache = Cache<CacheKey, PostStats>; pub type PostStatsCache = Cache<CacheKey, PostStats>;
/// 全局文章列表缓存实例,最大容量 100。
#[cfg(feature = "server")] #[cfg(feature = "server")]
static POST_LIST_CACHE: LazyLock<PostListCache> = LazyLock::new(|| { static POST_LIST_CACHE: LazyLock<PostListCache> = LazyLock::new(|| {
Cache::builder() Cache::builder()
@ -73,6 +109,7 @@ static POST_LIST_CACHE: LazyLock<PostListCache> = LazyLock::new(|| {
.build() .build()
}); });
/// 全局标签列表缓存实例,最大容量 50。
#[cfg(feature = "server")] #[cfg(feature = "server")]
static TAG_LIST_CACHE: LazyLock<TagListCache> = LazyLock::new(|| { static TAG_LIST_CACHE: LazyLock<TagListCache> = LazyLock::new(|| {
Cache::builder() Cache::builder()
@ -81,6 +118,7 @@ static TAG_LIST_CACHE: LazyLock<TagListCache> = LazyLock::new(|| {
.build() .build()
}); });
/// 全局单篇文章缓存实例,最大容量 200。
#[cfg(feature = "server")] #[cfg(feature = "server")]
static SINGLE_POST_CACHE: LazyLock<SinglePostCache> = LazyLock::new(|| { static SINGLE_POST_CACHE: LazyLock<SinglePostCache> = LazyLock::new(|| {
Cache::builder() Cache::builder()
@ -89,6 +127,7 @@ static SINGLE_POST_CACHE: LazyLock<SinglePostCache> = LazyLock::new(|| {
.build() .build()
}); });
/// 全局文章统计缓存实例,最大容量 10。
#[cfg(feature = "server")] #[cfg(feature = "server")]
static POST_STATS_CACHE: LazyLock<PostStatsCache> = LazyLock::new(|| { static POST_STATS_CACHE: LazyLock<PostStatsCache> = LazyLock::new(|| {
Cache::builder() Cache::builder()
@ -97,6 +136,7 @@ static POST_STATS_CACHE: LazyLock<PostStatsCache> = LazyLock::new(|| {
.build() .build()
}); });
/// 全局标签文章列表缓存实例,最大容量 100。
#[cfg(feature = "server")] #[cfg(feature = "server")]
static TAG_POSTS_CACHE: LazyLock<PostListCache> = LazyLock::new(|| { static TAG_POSTS_CACHE: LazyLock<PostListCache> = LazyLock::new(|| {
Cache::builder() Cache::builder()
@ -105,12 +145,15 @@ static TAG_POSTS_CACHE: LazyLock<PostListCache> = LazyLock::new(|| {
.build() .build()
}); });
/// 评论列表缓存类型。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub type CommentListCache = Cache<CacheKey, Vec<PublicComment>>; pub type CommentListCache = Cache<CacheKey, Vec<PublicComment>>;
/// 评论数量缓存类型。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub type CommentCountCache = Cache<CacheKey, i64>; pub type CommentCountCache = Cache<CacheKey, i64>;
/// 全局评论列表缓存实例,最大容量 200。
#[cfg(feature = "server")] #[cfg(feature = "server")]
static COMMENT_CACHE: LazyLock<CommentListCache> = LazyLock::new(|| { static COMMENT_CACHE: LazyLock<CommentListCache> = LazyLock::new(|| {
Cache::builder() Cache::builder()
@ -119,6 +162,7 @@ static COMMENT_CACHE: LazyLock<CommentListCache> = LazyLock::new(|| {
.build() .build()
}); });
/// 全局评论数量缓存实例,最大容量 200。
#[cfg(feature = "server")] #[cfg(feature = "server")]
static COMMENT_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| { static COMMENT_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| {
Cache::builder() Cache::builder()
@ -127,6 +171,7 @@ static COMMENT_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| {
.build() .build()
}); });
/// 全局待审核评论数量缓存实例,最大容量 10。
#[cfg(feature = "server")] #[cfg(feature = "server")]
static PENDING_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| { static PENDING_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| {
Cache::builder() Cache::builder()
@ -136,19 +181,22 @@ static PENDING_COUNT_CACHE: LazyLock<CommentCountCache> = LazyLock::new(|| {
}); });
// ============================================================================ // ============================================================================
// Public Cache API // 公共缓存 API
// ============================================================================ // ============================================================================
/// 读取文章分页列表缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn get_post_list(key: &CacheKey) -> Option<(Vec<Post>, i64)> { pub async fn get_post_list(key: &CacheKey) -> Option<(Vec<Post>, i64)> {
POST_LIST_CACHE.get(key).await POST_LIST_CACHE.get(key).await
} }
/// 写入文章分页列表缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn set_post_list(key: &CacheKey, posts: Vec<Post>, total: i64) { pub async fn set_post_list(key: &CacheKey, posts: Vec<Post>, total: i64) {
let _ = POST_LIST_CACHE.insert(key.clone(), (posts, total)).await; let _ = POST_LIST_CACHE.insert(key.clone(), (posts, total)).await;
} }
/// 读取已发布文章总数缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn get_total_published_posts() -> Option<i64> { pub async fn get_total_published_posts() -> Option<i64> {
POST_LIST_CACHE POST_LIST_CACHE
@ -157,6 +205,7 @@ pub async fn get_total_published_posts() -> Option<i64> {
.map(|(_, total)| total) .map(|(_, total)| total)
} }
/// 写入已发布文章总数缓存,文章列表部分置空以节省内存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn set_total_published_posts(total: i64) { pub async fn set_total_published_posts(total: i64) {
let _ = POST_LIST_CACHE let _ = POST_LIST_CACHE
@ -164,16 +213,19 @@ pub async fn set_total_published_posts(total: i64) {
.await; .await;
} }
/// 读取全部标签缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn get_tag_list() -> Option<Vec<Tag>> { pub async fn get_tag_list() -> Option<Vec<Tag>> {
TAG_LIST_CACHE.get(&CacheKey::AllTags).await TAG_LIST_CACHE.get(&CacheKey::AllTags).await
} }
/// 写入全部标签缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn set_tag_list(tags: Vec<Tag>) { pub async fn set_tag_list(tags: Vec<Tag>) {
let _ = TAG_LIST_CACHE.insert(CacheKey::AllTags, tags).await; let _ = TAG_LIST_CACHE.insert(CacheKey::AllTags, tags).await;
} }
/// 按 slug 读取单篇文章缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn get_post_by_slug(slug: &str) -> Option<Option<Post>> { pub async fn get_post_by_slug(slug: &str) -> Option<Option<Post>> {
SINGLE_POST_CACHE SINGLE_POST_CACHE
@ -181,6 +233,7 @@ pub async fn get_post_by_slug(slug: &str) -> Option<Option<Post>> {
.await .await
} }
/// 按 slug 写入单篇文章缓存None 表示文章不存在。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn set_post_by_slug(slug: &str, post: Option<Post>) { pub async fn set_post_by_slug(slug: &str, post: Option<Post>) {
let _ = SINGLE_POST_CACHE let _ = SINGLE_POST_CACHE
@ -188,6 +241,7 @@ pub async fn set_post_by_slug(slug: &str, post: Option<Post>) {
.await; .await;
} }
/// 按标签读取文章列表缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec<Post>, i64)> { pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec<Post>, i64)> {
TAG_POSTS_CACHE TAG_POSTS_CACHE
@ -195,6 +249,7 @@ pub async fn get_posts_by_tag(tag: &str) -> Option<(Vec<Post>, i64)> {
.await .await
} }
/// 按标签写入文章列表缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn set_posts_by_tag(tag: &str, posts: Vec<Post>, total: i64) { pub async fn set_posts_by_tag(tag: &str, posts: Vec<Post>, total: i64) {
let _ = TAG_POSTS_CACHE let _ = TAG_POSTS_CACHE
@ -202,30 +257,35 @@ pub async fn set_posts_by_tag(tag: &str, posts: Vec<Post>, total: i64) {
.await; .await;
} }
/// 读取文章统计缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn get_post_stats() -> Option<PostStats> { pub async fn get_post_stats() -> Option<PostStats> {
POST_STATS_CACHE.get(&CacheKey::PostStats).await POST_STATS_CACHE.get(&CacheKey::PostStats).await
} }
/// 写入文章统计缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn set_post_stats(stats: PostStats) { pub async fn set_post_stats(stats: PostStats) {
let _ = POST_STATS_CACHE.insert(CacheKey::PostStats, stats).await; let _ = POST_STATS_CACHE.insert(CacheKey::PostStats, stats).await;
} }
// ============================================================================ // ============================================================================
// Cache Invalidation // 缓存失效
// ============================================================================ // ============================================================================
/// 清空所有文章分页列表缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub fn invalidate_post_lists() { pub fn invalidate_post_lists() {
POST_LIST_CACHE.invalidate_all(); POST_LIST_CACHE.invalidate_all();
} }
/// 清空所有标签缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub fn invalidate_all_tags() { pub fn invalidate_all_tags() {
TAG_LIST_CACHE.invalidate_all(); TAG_LIST_CACHE.invalidate_all();
} }
/// 按 slug 失效单篇文章缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn invalidate_post_by_slug(slug: &str) { pub async fn invalidate_post_by_slug(slug: &str) {
SINGLE_POST_CACHE SINGLE_POST_CACHE
@ -233,6 +293,7 @@ pub async fn invalidate_post_by_slug(slug: &str) {
.await; .await;
} }
/// 按标签失效文章列表缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn invalidate_posts_by_tag(tag: &str) { pub async fn invalidate_posts_by_tag(tag: &str) {
TAG_POSTS_CACHE TAG_POSTS_CACHE
@ -240,11 +301,13 @@ pub async fn invalidate_posts_by_tag(tag: &str) {
.await; .await;
} }
/// 清空文章统计缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub fn invalidate_post_stats() { pub fn invalidate_post_stats() {
POST_STATS_CACHE.invalidate_all(); POST_STATS_CACHE.invalidate_all();
} }
/// 清空所有文章相关缓存(列表、标签、单篇、统计、标签文章)。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub fn invalidate_all_post_caches() { pub fn invalidate_all_post_caches() {
POST_LIST_CACHE.invalidate_all(); POST_LIST_CACHE.invalidate_all();
@ -254,6 +317,7 @@ pub fn invalidate_all_post_caches() {
TAG_POSTS_CACHE.invalidate_all(); TAG_POSTS_CACHE.invalidate_all();
} }
/// 按文章主键读取评论列表缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn get_comments_by_post(post_id: i32) -> Option<Vec<PublicComment>> { pub async fn get_comments_by_post(post_id: i32) -> Option<Vec<PublicComment>> {
COMMENT_CACHE COMMENT_CACHE
@ -261,6 +325,7 @@ pub async fn get_comments_by_post(post_id: i32) -> Option<Vec<PublicComment>> {
.await .await
} }
/// 按文章主键写入评论列表缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn set_comments_by_post(post_id: i32, comments: Vec<PublicComment>) { pub async fn set_comments_by_post(post_id: i32, comments: Vec<PublicComment>) {
let _ = COMMENT_CACHE let _ = COMMENT_CACHE
@ -268,6 +333,7 @@ pub async fn set_comments_by_post(post_id: i32, comments: Vec<PublicComment>) {
.await; .await;
} }
/// 按文章主键读取评论数量缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn get_comment_count(post_id: i32) -> Option<i64> { pub async fn get_comment_count(post_id: i32) -> Option<i64> {
COMMENT_COUNT_CACHE COMMENT_COUNT_CACHE
@ -275,6 +341,7 @@ pub async fn get_comment_count(post_id: i32) -> Option<i64> {
.await .await
} }
/// 按文章主键写入评论数量缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn set_comment_count(post_id: i32, count: i64) { pub async fn set_comment_count(post_id: i32, count: i64) {
let _ = COMMENT_COUNT_CACHE let _ = COMMENT_COUNT_CACHE
@ -282,6 +349,7 @@ pub async fn set_comment_count(post_id: i32, count: i64) {
.await; .await;
} }
/// 读取待审核评论总数缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn get_pending_count() -> Option<i64> { pub async fn get_pending_count() -> Option<i64> {
PENDING_COUNT_CACHE PENDING_COUNT_CACHE
@ -289,6 +357,7 @@ pub async fn get_pending_count() -> Option<i64> {
.await .await
} }
/// 写入待审核评论总数缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn set_pending_count(count: i64) { pub async fn set_pending_count(count: i64) {
let _ = PENDING_COUNT_CACHE let _ = PENDING_COUNT_CACHE
@ -296,6 +365,7 @@ pub async fn set_pending_count(count: i64) {
.await; .await;
} }
/// 按文章主键失效评论列表缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn invalidate_comments_by_post(post_id: i32) { pub async fn invalidate_comments_by_post(post_id: i32) {
COMMENT_CACHE COMMENT_CACHE
@ -303,6 +373,7 @@ pub async fn invalidate_comments_by_post(post_id: i32) {
.await; .await;
} }
/// 按文章主键失效评论数量缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn invalidate_comment_count(post_id: i32) { pub async fn invalidate_comment_count(post_id: i32) {
COMMENT_COUNT_CACHE COMMENT_COUNT_CACHE
@ -310,6 +381,7 @@ pub async fn invalidate_comment_count(post_id: i32) {
.await; .await;
} }
/// 失效待审核评论总数缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub async fn invalidate_pending_count() { pub async fn invalidate_pending_count() {
PENDING_COUNT_CACHE PENDING_COUNT_CACHE
@ -317,6 +389,7 @@ pub async fn invalidate_pending_count() {
.await; .await;
} }
/// 清空所有评论相关缓存。
#[cfg(feature = "server")] #[cfg(feature = "server")]
#[allow(dead_code)] #[allow(dead_code)]
pub async fn invalidate_all_comment_caches() { pub async fn invalidate_all_comment_caches() {

View File

@ -1,17 +1,41 @@
//! 数据库连接模块。
//!
//! 本模块根据 `server` feature 的启用情况提供两套实现:
//! - 启用 `server` 时,从 `pool` 子模块暴露真实的 PostgreSQL 连接池;
//! - 未启用 `server` 时(例如仅编译 WASM 前端),提供一个 `DummyPool` stub
//! 使代码在缺少数据库依赖的情况下仍能编译通过。
//!
//! 这种 stub 模式是 Dioxus fullstack 项目的常见做法:服务端函数体在 WASM 构建时会被剥离,
//! 但模块结构必须保持一致,因此需要一个占位实现来满足编译器的符号解析。
/// 真实的 PostgreSQL 连接池实现,仅在启用 server feature 时编译。
#[cfg(feature = "server")] #[cfg(feature = "server")]
pub mod pool; pub mod pool;
/// 占位连接池实现,仅在不启用 server feature 时编译。
///
/// `DummyPool` 是一个最小 stub它提供与真实连接池相同的公开接口形状
///(如 `get` 与 `get_conn`),但所有方法都直接返回错误。
/// 这样可以在不引入 deadpool-postgres、tokio-postgres 等依赖的情况下,
/// 让依赖 `db::pool::DB_POOL` 的代码通过前端编译。
/// **请勿删除此 stub**,否则非 server 构建将无法通过编译。
#[cfg(not(feature = "server"))] #[cfg(not(feature = "server"))]
#[allow(dead_code)] #[allow(dead_code)]
pub mod pool { pub mod pool {
/// 占位连接池,无实际数据库连接能力。
pub struct DummyPool; pub struct DummyPool;
impl DummyPool { impl DummyPool {
/// 占位方法,永远返回错误。
pub async fn get(&self) -> Result<(), ()> { pub async fn get(&self) -> Result<(), ()> {
Err(()) Err(())
} }
} }
/// 占位全局连接池实例。
pub static DB_POOL: DummyPool = DummyPool; pub static DB_POOL: DummyPool = DummyPool;
/// 占位函数,永远返回错误。
pub async fn get_conn() -> Result<(), ()> { pub async fn get_conn() -> Result<(), ()> {
Err(()) Err(())
} }

View File

@ -1,15 +1,25 @@
//! PostgreSQL 连接池实现。
//!
//! 仅在启用 `server` feature 时编译,使用 deadpool-postgres 管理连接池,
//! 并通过 `std::sync::LazyLock` 在首次访问时延迟初始化全局连接池。
//! `get_conn` 失败时按固定 2 秒间隔进行简单重试,以应对瞬时连接失败。
use std::sync::LazyLock; use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod}; use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod};
use tokio_postgres::NoTls; use tokio_postgres::NoTls;
/// 全局数据库连接池,基于 `DATABASE_URL` 环境变量延迟初始化。
///
/// 最大连接数可通过 `DB_POOL_SIZE` 环境变量调整,默认 20。
pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| { pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| {
let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set"); let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable not set");
let pg_cfg = db_url let pg_cfg = db_url
.parse::<tokio_postgres::Config>() .parse::<tokio_postgres::Config>()
.expect("Invalid DATABASE_URL format"); .expect("Invalid DATABASE_URL format");
// 使用 Fast 回收策略,避免每次归还连接时执行额外查询。
let mgr_cfg = ManagerConfig { let mgr_cfg = ManagerConfig {
recycling_method: RecyclingMethod::Fast, recycling_method: RecyclingMethod::Fast,
}; };
@ -26,9 +36,15 @@ pub static DB_POOL: LazyLock<Pool> = LazyLock::new(|| {
.expect("Failed to create database connection pool") .expect("Failed to create database connection pool")
}); });
/// 最大重试次数。
const MAX_RETRIES: u32 = 3; const MAX_RETRIES: u32 = 3;
/// 每次重试之间的固定等待时间。
const RETRY_DELAY: Duration = Duration::from_secs(2); const RETRY_DELAY: Duration = Duration::from_secs(2);
/// 从全局连接池获取一个数据库连接,失败时按 `MAX_RETRIES` 进行重试。
///
/// 若所有尝试均失败,则返回最后一次遇到的 PoolError。
pub async fn get_conn() -> Result<deadpool_postgres::Object, deadpool_postgres::PoolError> { pub async fn get_conn() -> Result<deadpool_postgres::Object, deadpool_postgres::PoolError> {
let mut last_err = None; let mut last_err = None;
for attempt in 0..=MAX_RETRIES { for attempt in 0..=MAX_RETRIES {

View File

@ -1,16 +1,27 @@
//! 评论模型。
//!
//! 定义评论状态、服务端内部使用的 Comment 结构体,
//! 以及面向前端展示的 PublicComment 与面向后台管理的 AdminComment。
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// 评论审核状态枚举,序列化时使用小写字符串。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum CommentStatus { pub enum CommentStatus {
/// 待审核。
Pending, Pending,
/// 已通过。
Approved, Approved,
/// 垃圾评论。
Spam, Spam,
/// 已删除/回收站。
Trash, Trash,
} }
impl CommentStatus { impl CommentStatus {
/// 将数据库或 API 中的状态字符串解析为 CommentStatus未知值默认回退到 Pending。
pub fn from_str(s: &str) -> Self { pub fn from_str(s: &str) -> Self {
match s { match s {
"approved" => Self::Approved, "approved" => Self::Approved,
@ -20,6 +31,7 @@ impl CommentStatus {
} }
} }
/// 将 CommentStatus 序列化为小写字符串。
#[allow(dead_code)] #[allow(dead_code)]
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
match self { match self {
@ -31,55 +43,99 @@ impl CommentStatus {
} }
} }
/// 服务端内部使用的完整评论结构体,仅在启用 server feature 时编译。
///
/// 包含作者邮箱、IP、User-Agent 等敏感或管理字段,不直接返回给前端。
#[cfg(feature = "server")] #[cfg(feature = "server")]
#[allow(dead_code)] #[allow(dead_code)]
pub struct Comment { pub struct Comment {
/// 评论主键。
pub id: i64, pub id: i64,
/// 所属文章主键。
pub post_id: i32, pub post_id: i32,
/// 父评论主键,用于实现嵌套回复。
pub parent_id: Option<i64>, pub parent_id: Option<i64>,
/// 嵌套深度0 表示顶层评论。
pub depth: i32, pub depth: i32,
/// 评论者名称。
pub author_name: String, pub author_name: String,
/// 评论者邮箱,用于 Gravatar 与后台联系。
pub author_email: String, pub author_email: String,
/// 评论者个人主页 URL。
pub author_url: Option<String>, pub author_url: Option<String>,
/// 原始 Markdown 内容。
pub content_md: String, pub content_md: String,
/// 渲染后的 HTML 内容。
pub content_html: Option<String>, pub content_html: Option<String>,
/// 内容哈希,用于检测重复或垃圾评论。
pub content_hash: Option<String>, pub content_hash: Option<String>,
/// 当前审核状态。
pub status: CommentStatus, pub status: CommentStatus,
/// 评论者 IP 地址。
pub ip_address: Option<String>, pub ip_address: Option<String>,
/// 评论者浏览器 User-Agent。
pub user_agent: Option<String>, pub user_agent: Option<String>,
/// 审核通过时间。
pub approved_at: Option<DateTime<Utc>>, pub approved_at: Option<DateTime<Utc>>,
/// 评论创建时间。
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
/// 评论最后更新时间。
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
/// 软删除时间。
pub deleted_at: Option<DateTime<Utc>>, pub deleted_at: Option<DateTime<Utc>>,
} }
/// 面向前端展示的评论结构体,已脱敏。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PublicComment { pub struct PublicComment {
/// 评论主键。
pub id: i64, pub id: i64,
/// 父评论主键None 表示顶层评论。
pub parent_id: Option<i64>, pub parent_id: Option<i64>,
/// 嵌套深度。
pub depth: i32, pub depth: i32,
/// 评论者名称。
pub author_name: String, pub author_name: String,
/// 评论者个人主页 URL。
pub author_url: Option<String>, pub author_url: Option<String>,
/// 评论者头像 URL。
pub avatar_url: String, pub avatar_url: String,
/// 渲染后的 HTML 内容。
pub content_html: Option<String>, pub content_html: Option<String>,
/// 用于展示的人类可读创建时间。
pub created_at: String, pub created_at: String,
/// ISO 8601 格式的创建时间。
pub created_at_iso: String, pub created_at_iso: String,
} }
/// 面向后台管理的评论结构体,包含审核所需字段。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AdminComment { pub struct AdminComment {
/// 评论主键。
pub id: i64, pub id: i64,
/// 所属文章主键。
pub post_id: i32, pub post_id: i32,
/// 所属文章标题。
pub post_title: String, pub post_title: String,
/// 所属文章 slug。
pub post_slug: String, pub post_slug: String,
/// 父评论主键。
pub parent_id: Option<i64>, pub parent_id: Option<i64>,
/// 嵌套深度。
pub depth: i32, pub depth: i32,
/// 评论者名称。
pub author_name: String, pub author_name: String,
/// 评论者邮箱。
pub author_email: String, pub author_email: String,
/// 评论者个人主页 URL。
pub author_url: Option<String>, pub author_url: Option<String>,
/// 评论者头像 URL。
pub avatar_url: String, pub avatar_url: String,
/// 原始 Markdown 内容。
pub content_md: String, pub content_md: String,
/// 当前审核状态。
pub status: CommentStatus, pub status: CommentStatus,
/// 评论创建时间。
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }

View File

@ -1,3 +1,11 @@
//! 数据模型模块。
//!
//! 定义博客系统使用的核心领域模型包括文章Post、用户User与评论Comment
//! 这些结构体通过 serde 在服务端与客户端之间共享序列化。
/// 评论模型及其状态枚举。
pub mod comment; pub mod comment;
/// 文章模型、文章状态、标签与统计信息。
pub mod post; pub mod post;
/// 用户模型、用户角色与可公开用户信息。
pub mod user; pub mod user;

View File

@ -1,13 +1,22 @@
//! 文章模型。
//!
//! 定义文章状态、文章结构体、标签、统计信息以及前后导航结构体。
//! Post 结构体在服务端渲染、客户端展示以及缓存层之间共享。
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// 文章发布状态枚举。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PostStatus { pub enum PostStatus {
/// 草稿,仅管理员可见。
Draft, Draft,
/// 已发布,面向读者公开。
Published, Published,
} }
impl PostStatus { impl PostStatus {
/// 将状态序列化为数据库或 API 使用的小写字符串。
#[allow(dead_code)] #[allow(dead_code)]
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
match self { match self {
@ -16,6 +25,7 @@ impl PostStatus {
} }
} }
/// 将字符串解析为 PostStatus无法识别时返回 None。
#[allow(dead_code)] #[allow(dead_code)]
pub fn from_str(s: &str) -> Option<Self> { pub fn from_str(s: &str) -> Option<Self> {
match s { match s {
@ -26,35 +36,56 @@ impl PostStatus {
} }
} }
/// 文章领域模型。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Post { pub struct Post {
/// 文章主键。
pub id: i32, pub id: i32,
/// 作者用户主键。
pub author_id: i32, pub author_id: i32,
/// 文章标题。
pub title: String, pub title: String,
/// URL slug用于生成文章链接。
pub slug: String, pub slug: String,
/// 摘要,可选。
pub summary: Option<String>, pub summary: Option<String>,
/// 原始 Markdown 内容。
pub content_md: String, pub content_md: String,
/// 渲染后的 HTML 内容,可选。
pub content_html: Option<String>, pub content_html: Option<String>,
/// 文章发布状态。
pub status: PostStatus, pub status: PostStatus,
/// 正式发布时间None 表示尚未发布。
pub published_at: Option<DateTime<Utc>>, pub published_at: Option<DateTime<Utc>>,
/// 创建时间。
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
/// 最后更新时间。
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
/// 关联标签列表。
pub tags: Vec<String>, pub tags: Vec<String>,
/// 封面图片 URL。
pub cover_image: Option<String>, pub cover_image: Option<String>,
/// 预计阅读时间(分钟)。
pub reading_time: u32, pub reading_time: u32,
/// 字数统计。
pub word_count: u32, pub word_count: u32,
/// 目录 HTML。
pub toc_html: Option<String>, pub toc_html: Option<String>,
/// 上一篇文章导航信息。
pub prev_post: Option<PostNav>, pub prev_post: Option<PostNav>,
/// 下一篇文章导航信息。
pub next_post: Option<PostNav>, pub next_post: Option<PostNav>,
} }
impl Post { impl Post {
/// 返回用于展示的文章日期:优先使用发布时间,否则回退到创建时间。
pub fn formatted_date(&self) -> String { pub fn formatted_date(&self) -> String {
self.published_at self.published_at
.map(|d| d.format("%Y-%m-%d").to_string()) .map(|d| d.format("%Y-%m-%d").to_string())
.unwrap_or_else(|| self.created_at.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 { pub fn status_label(&self) -> &'static str {
match self.status { match self.status {
PostStatus::Published => "已发布", PostStatus::Published => "已发布",
@ -62,6 +93,7 @@ impl Post {
} }
} }
/// 返回状态文本在 light/dark 模式下的 Tailwind 颜色类。
pub fn status_class(&self) -> &'static str { pub fn status_class(&self) -> &'static str {
match self.status { match self.status {
PostStatus::Published => "text-green-600 dark:text-green-400", 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 { pub fn status_badge_class(&self) -> &'static str {
match self.status { match self.status {
PostStatus::Published => { PostStatus::Published => {
@ -79,23 +112,34 @@ impl Post {
} }
} }
/// 前后文章导航结构体。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PostNav { pub struct PostNav {
/// 文章标题。
pub title: String, pub title: String,
/// 文章 slug。
pub slug: String, pub slug: String,
} }
/// 标签领域模型。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Tag { pub struct Tag {
/// 标签主键。
pub id: i32, pub id: i32,
/// 标签名称。
pub name: String, pub name: String,
/// 关联文章数量。
pub post_count: i64, pub post_count: i64,
} }
/// 文章统计信息。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PostStats { pub struct PostStats {
/// 文章总数。
pub total: i64, pub total: i64,
/// 草稿数量。
pub drafts: i64, pub drafts: i64,
/// 已发布数量。
pub published: i64, pub published: i64,
} }

View File

@ -1,13 +1,22 @@
//! 用户模型。
//!
//! 定义用户角色、内部用户结构体以及可暴露给前端的 PublicUser。
//! User 包含密码哈希等敏感字段PublicUser 用于在 API 中隐藏这些字段。
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// 用户角色枚举。
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum UserRole { pub enum UserRole {
/// 管理员,拥有全部后台权限。
Admin, Admin,
/// 被禁用的用户,无法登录或操作。
Blocked, Blocked,
} }
impl UserRole { impl UserRole {
/// 将数据库中的角色字符串解析为 UserRole无法识别时返回 None。
#[allow(dead_code)] #[allow(dead_code)]
pub fn from_str(s: &str) -> Option<Self> { pub fn from_str(s: &str) -> Option<Self> {
match s { match s {
@ -18,26 +27,40 @@ impl UserRole {
} }
} }
/// 内部使用的完整用户结构体,包含敏感字段。
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User { pub struct User {
/// 用户主键。
pub id: i32, pub id: i32,
/// 用户名,用于登录与展示。
pub username: String, pub username: String,
/// 邮箱地址。
pub email: String, pub email: String,
/// Argon2 密码哈希,不允许直接序列化返回给前端。
pub password_hash: String, pub password_hash: String,
/// 用户角色。
pub role: UserRole, pub role: UserRole,
/// 账户创建时间。
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }
/// 可公开的用户信息,从 User 转换而来,不含密码哈希。
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublicUser { pub struct PublicUser {
/// 用户主键。
pub id: i32, pub id: i32,
/// 用户名。
pub username: String, pub username: String,
/// 邮箱地址。
pub email: String, pub email: String,
/// 用户角色。
pub role: UserRole, pub role: UserRole,
/// 账户创建时间。
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }
impl From<User> for PublicUser { impl From<User> for PublicUser {
/// 将 User 转换为 PublicUser丢弃 password_hash 字段。
fn from(u: User) -> Self { fn from(u: User) -> Self {
PublicUser { PublicUser {
id: u.id, id: u.id,