diff --git a/src/api/auth.rs b/src/api/auth.rs index bd24926..b36c16c 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -293,6 +293,7 @@ pub async fn logout() -> Result { if let Some(t) = token { let token_hash = session::hash_token(&t); + crate::cache::invalidate_session_user(&token_hash).await; client .execute("DELETE FROM sessions WHERE token_hash = $1", &[&token_hash]) .await @@ -316,11 +317,17 @@ pub struct CurrentUserResponse { #[cfg(feature = "server")] /// 根据会话 token 查询对应用户(含密码哈希等完整信息)。 /// +/// 优先命中内存缓存,避免每次请求都执行 DB JOIN;未命中时回查数据库并回填缓存。 /// 仅服务端内部使用,不会暴露给前端。 pub async fn get_user_by_token(token: &str) -> Result, ServerFnError> { + let token_hash = session::hash_token(token); + + if let Some(user) = crate::cache::get_session_user(&token_hash).await { + return Ok(Some(user)); + } + let client = get_conn().await.map_err(AppError::db_conn)?; - let token_hash = session::hash_token(token); let row = client .query_opt( "SELECT u.id, u.username, u.email, u.password_hash, u.role, u.created_at @@ -348,6 +355,10 @@ pub async fn get_user_by_token(token: &str) -> Result, ServerFnErro None => None, }; + if let Some(ref u) = user { + crate::cache::set_session_user(&token_hash, u.clone()).await; + } + Ok(user) } diff --git a/src/cache.rs b/src/cache.rs index 44e98da..05bfe08 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -15,6 +15,8 @@ use std::time::Duration; use crate::models::comment::PublicComment; #[cfg(feature = "server")] use crate::models::post::{Post, PostListItem, PostStats, Tag}; +#[cfg(feature = "server")] +use crate::models::user::User; // ============================================================================ // 缓存 TTL 配置 @@ -48,6 +50,10 @@ const TTL_COMMENTS: Duration = Duration::from_secs(60); #[cfg(feature = "server")] const TTL_PENDING_COUNT: Duration = Duration::from_secs(10); +/// 会话用户缓存 TTL:300 秒(5 分钟),短于 DB 会话过期时间。 +#[cfg(feature = "server")] +const TTL_SESSION: Duration = Duration::from_secs(300); + // ============================================================================ // 缓存 Key 类型 // ============================================================================ @@ -161,6 +167,19 @@ static PENDING_COUNT_CACHE: LazyLock> = LazyLock::new(|| { .build() }); +/// 会话用户缓存类型。 +#[cfg(feature = "server")] +pub type SessionCache = Cache; + +/// 全局会话用户缓存实例,最大容量 1000,TTL 5 分钟。 +#[cfg(feature = "server")] +static SESSION_CACHE: LazyLock = LazyLock::new(|| { + Cache::builder() + .max_capacity(1000) + .time_to_live(TTL_SESSION) + .build() +}); + // ============================================================================ // 公共缓存 API // ============================================================================ @@ -345,6 +364,24 @@ pub async fn set_pending_count(count: i64) { .await; } +/// 读取会话用户缓存。 +#[cfg(feature = "server")] +pub async fn get_session_user(token_hash: &str) -> Option { + SESSION_CACHE.get(token_hash).await +} + +/// 写入会话用户缓存。 +#[cfg(feature = "server")] +pub async fn set_session_user(token_hash: &str, user: User) { + let _ = SESSION_CACHE.insert(token_hash.to_string(), user).await; +} + +/// 失效指定会话用户缓存。 +#[cfg(feature = "server")] +pub async fn invalidate_session_user(token_hash: &str) { + SESSION_CACHE.invalidate(token_hash).await; +} + /// 按文章主键失效评论列表缓存。 #[cfg(feature = "server")] pub async fn invalidate_comments_by_post(post_id: i32) { @@ -366,6 +403,7 @@ mod tests { use super::*; use crate::models::comment::PublicComment; use crate::models::post::PostStatus; + use crate::models::user::{User, UserRole}; use serial_test::serial; #[test] @@ -572,4 +610,32 @@ mod tests { assert!(get_pending_count().await.is_none()); } + #[tokio::test] + #[serial] + async fn session_cache_roundtrip() { + let user = User { + id: 42, + username: "cached_user".to_string(), + email: "cached@example.com".to_string(), + password_hash: "argon2_hash".to_string(), + role: UserRole::Admin, + created_at: chrono::Utc::now(), + }; + let token_hash = "sha256_token_hash"; + + set_session_user(token_hash, user.clone()).await; + let cached = get_session_user(token_hash).await; + + assert!(cached.is_some()); + let cached_user = cached.unwrap(); + assert_eq!(cached_user.id, user.id); + assert_eq!(cached_user.username, user.username); + assert_eq!(cached_user.email, user.email); + assert_eq!(cached_user.password_hash, user.password_hash); + assert_eq!(cached_user.role, user.role); + + invalidate_session_user(token_hash).await; + assert!(get_session_user(token_hash).await.is_none()); + } + }