diff --git a/.env.example b/.env.example index fc6e7b3..1898c0e 100644 --- a/.env.example +++ b/.env.example @@ -23,7 +23,10 @@ MAX_SESSIONS_PER_USER=5 # Database connection pool size (default: 20) DB_POOL_SIZE=20 -# SSR page cache duration in seconds (default: 3600) +# SSR page cache duration in seconds (default: 3600). +# Generation-based invalidation is implemented in src/ssr_cache.rs and bumps on every post +# write. However, Dioxus 0.7 does not expose a public API to customize the incremental SSR +# cache key, so SSR_CACHE_SECS remains the effective fallback TTL until such API is available. SSR_CACHE_SECS=3600 # Image serving cache headers (hardcoded defaults) diff --git a/src/api/posts/create.rs b/src/api/posts/create.rs index d2e52b8..55641ec 100644 --- a/src/api/posts/create.rs +++ b/src/api/posts/create.rs @@ -149,6 +149,9 @@ pub async fn create_post( // 失效该文章涉及的所有标签下文章列表缓存。 crate::cache::invalidate_tag_posts_for(&tags_cleaned).await; + // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 + crate::ssr_cache::bump_global_generation(); + Ok(CreatePostResponse { success: true, message: "创建成功".to_string(), diff --git a/src/api/posts/delete.rs b/src/api/posts/delete.rs index 44e4019..4c6084d 100644 --- a/src/api/posts/delete.rs +++ b/src/api/posts/delete.rs @@ -84,6 +84,9 @@ pub async fn delete_post(post_id: i32) -> Result Result 0 { crate::cache::invalidate_all_post_caches(); crate::cache::invalidate_search_results(); + // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 + crate::ssr_cache::bump_global_generation(); } Ok(RebuildResult { diff --git a/src/api/posts/trash.rs b/src/api/posts/trash.rs index 2c33af6..3af8f45 100644 --- a/src/api/posts/trash.rs +++ b/src/api/posts/trash.rs @@ -95,6 +95,9 @@ pub async fn restore_post(post_id: i32) -> Result Result) -> Result>()).await; + + // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 + crate::ssr_cache::bump_global_generation(); } else { // 影响集过大时回退到全量失效,避免大量串行缓存操作。 crate::cache::invalidate_all_post_caches(); crate::cache::invalidate_search_results(); + // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 + crate::ssr_cache::bump_global_generation(); } Ok(CreatePostResponse { @@ -385,10 +396,15 @@ pub async fn batch_purge_posts(post_ids: Vec) -> Result Result { crate::cache::invalidate_post_by_slug(slug).await; } crate::cache::invalidate_tag_posts_for(&tags).await; + + // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 + crate::ssr_cache::bump_global_generation(); } else { // 影响集过大时回退到全量失效,避免大量串行缓存操作。 crate::cache::invalidate_all_post_caches(); crate::cache::invalidate_search_results(); + // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 + crate::ssr_cache::bump_global_generation(); } Ok(CreatePostResponse { diff --git a/src/api/posts/update.rs b/src/api/posts/update.rs index 9e6477d..f87a75e 100644 --- a/src/api/posts/update.rs +++ b/src/api/posts/update.rs @@ -218,6 +218,9 @@ pub async fn update_post( } } + // 递增 SSR 全局世代号(未来就绪基础设施;当前不会使 Dioxus 0.7 SSR 缓存失效)。 + crate::ssr_cache::bump_global_generation(); + Ok(CreatePostResponse { success: true, message: "更新成功".to_string(), diff --git a/src/main.rs b/src/main.rs index f387c60..dcee368 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,9 @@ mod hooks; mod models; mod pages; mod router; +// ssr_cache 仅在 server feature 启用时编译;保存 SSR 世代号失效状态。 +#[cfg(feature = "server")] +mod ssr_cache; mod tasks; mod theme; mod utils; @@ -78,7 +81,9 @@ fn main() { tasks::image_cache_cleanup::run_cleanup().await; }); - // 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖 + // 配置增量渲染缓存,默认缓存 3600 秒,可通过 SSR_CACHE_SECS 覆盖。 + // 注意:世代号失效机制已就位(见 src/ssr_cache.rs),但 Dioxus 0.7 未暴露 + // 自定义缓存键 API,因此 TTL 仍是当前有效的兜底策略。 let config = ServeConfig::builder().incremental( dioxus::server::IncrementalRendererConfig::default().invalidate_after( std::time::Duration::from_secs( @@ -90,6 +95,24 @@ fn main() { ), ); + // SSR 世代号中间件:把当前全局世代号注入请求扩展并附加到响应头。 + // 这是为 Dioxus 未来支持自定义 SSR 缓存键预留的钩子;目前主要提供可观测性。 + async fn ssr_generation_middleware( + req: axum::http::Request, + next: axum::middleware::Next, + ) -> axum::response::Response { + let generation = crate::ssr_cache::current_global_generation(); + let (mut parts, body) = req.into_parts(); + parts.extensions.insert(crate::ssr_cache::SsrGeneration(generation)); + let mut response = next.run(axum::http::Request::from_parts(parts, body)).await; + response.headers_mut().insert( + axum::http::header::HeaderName::from_static("x-ssr-generation"), + axum::http::HeaderValue::from_str(&generation.to_string()) + .unwrap_or_else(|_| axum::http::HeaderValue::from_static("0")), + ); + response + } + // 自定义 API 路由:图片上传(大文件,需要更长超时) let upload_route = axum::Router::new() .route( @@ -106,8 +129,9 @@ fn main() { let dioxus_app = axum::Router::new().serve_dioxus_application(config, router::AppRouter); - // 合并 Dioxus + 压缩/30s 超时中间件 + // 合并 Dioxus + 世代号/压缩/30s 超时中间件 let app_routes = dioxus_app + .layer(axum::middleware::from_fn(ssr_generation_middleware)) .layer(CompressionLayer::new()) .layer(TimeoutLayer::with_status_code( StatusCode::REQUEST_TIMEOUT, diff --git a/src/ssr_cache.rs b/src/ssr_cache.rs new file mode 100644 index 0000000..e56543c --- /dev/null +++ b/src/ssr_cache.rs @@ -0,0 +1,70 @@ +//! SSR 增量渲染缓存失效的未来就绪基础设施。 +//! +//! 本模块维护一个全局单调递增的世代号(generation)。文章写入成功后调用方会 +//! 使其递增,从而**标记** SSR 渲染结果已过期。然而: +//! +//! **Dioxus 0.7 的增量渲染器使用请求 URI 的 `path_and_query()` 作为内部缓存键, +//! 且没有暴露公开 API 供外部代码自定义缓存键或按路由失效已渲染页面。** +//! 因此,当前世代号并**不会**实际使 Dioxus 的 SSR 缓存失效;它只是为未来 API +//! 准备好状态,并在请求/响应中提供可观测性。 +//! +//! 在 Dioxus 提供以下任一能力之前,有效的 SSR 缓存失效手段仍是调低 +//! `SSR_CACHE_SECS` 这一兜底 TTL: +//! - 自定义增量渲染缓存键的回调;或 +//! - 从 server function 内部按路由失效缓存的公开 API。 +//! +//! 当前实现: +//! - `bump_global_generation()` / `current_global_generation()`:全局世代号。 +//! - `SsrGeneration`:注入到请求扩展中的类型;未来 Dioxus 支持读取扩展生成 +//! 缓存键时可直接使用。 +//! - `src/main.rs` 的中间件把当前世代号附加到 `X-SSR-Generation` 响应头 +//! (仅 GET 请求),便于调试与监控。 +//! +//! 仅在启用 `server` feature 时编译。 + +#![cfg(feature = "server")] + +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::LazyLock; + +/// 全局 SSR 世代号。 +/// +/// 任何文章写入操作都会使其递增,从而让所有基于该全局世代的 SSR 缓存键在未来 +/// Dioxus 支持自定义缓存键时失效。 +static GLOBAL_GENERATION: LazyLock = LazyLock::new(AtomicU64::default); + +/// 注入到请求扩展中的当前 SSR 世代号。 +/// +/// 这是为未来 Dioxus 支持自定义 SSR 缓存键预留的钩子。当前 Dioxus 0.7 的渲染器 +/// 不会读取此扩展。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SsrGeneration(pub u64); + +/// 原子递增并返回新的全局世代号。 +pub fn bump_global_generation() -> u64 { + GLOBAL_GENERATION.fetch_add(1, Ordering::SeqCst).wrapping_add(1) +} + +/// 返回当前全局世代号。 +pub fn current_global_generation() -> u64 { + GLOBAL_GENERATION.load(Ordering::SeqCst) +} + +#[cfg(test)] +mod tests { + use super::*; + use serial_test::serial; + + #[test] + #[serial] + fn global_generation_is_monotonic() { + let before = current_global_generation(); + let g1 = bump_global_generation(); + let g2 = bump_global_generation(); + let current = current_global_generation(); + + assert!(g1 > before || g1 == 1); + assert!(g2 > g1); + assert_eq!(current, g2); + } +}