diff --git a/src/api/posts/rebuild.rs b/src/api/posts/rebuild.rs index 46e041e..cb29142 100644 --- a/src/api/posts/rebuild.rs +++ b/src/api/posts/rebuild.rs @@ -4,10 +4,14 @@ use dioxus::prelude::*; use super::helpers::get_current_admin_user; #[cfg(feature = "server")] use crate::api::error::AppError; +use crate::api::posts::RebuildResult; use crate::db::pool::get_conn; +const REBUILD_BATCH_LIMIT: i64 = 500; +const MAX_DISPLAY_ERRORS: usize = 5; + #[server(RebuildContentHtml, "/api")] -pub async fn rebuild_content_html(rebuild_all: bool) -> Result { +pub async fn rebuild_content_html(rebuild_all: bool) -> Result { let _user = get_current_admin_user().await?; #[cfg(feature = "server")] @@ -15,47 +19,78 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result = Vec::new(); for row in &rows { let id: i32 = row.get(0); let content_md: String = row.get(1); - let rendered = crate::api::markdown::render_markdown_enhanced(&content_md); + let rendered = match std::panic::catch_unwind(|| { + crate::api::markdown::render_markdown_enhanced(&content_md) + }) { + Ok(r) => r, + Err(_) => { + failed += 1; + if errors.len() < MAX_DISPLAY_ERRORS { + errors.push(format!("文章 #{id}: 渲染异常")); + } + continue; + } + }; + let toc_html = if rendered.toc_html.is_empty() { None:: } else { Some(rendered.toc_html) }; - client + match client .execute( - "UPDATE posts SET content_html = $1, toc_html = $2, updated_at = NOW() WHERE id = $3", + "UPDATE posts SET content_html = $1, toc_html = $2 WHERE id = $3", &[&rendered.html, &toc_html, &id], ) .await - .map_err(AppError::tx)?; - - count += 1; + { + Ok(_) => rebuilt += 1, + Err(_) => { + failed += 1; + if errors.len() < MAX_DISPLAY_ERRORS { + errors.push(format!("文章 #{id}: DB 写入失败")); + } + } + } } - if count > 0 { - crate::cache::invalidate_post_lists(); - crate::cache::invalidate_post_stats(); + if rebuilt > 0 || failed > 0 { + crate::cache::invalidate_all_post_caches(); } - Ok(count) + Ok(RebuildResult { + rebuilt, + failed, + errors, + }) } #[cfg(not(feature = "server"))] { - Ok(0) + Ok(RebuildResult { + rebuilt: 0, + failed: 0, + errors: vec![], + }) } } diff --git a/src/api/posts/types.rs b/src/api/posts/types.rs index 94d6aaa..247bb91 100644 --- a/src/api/posts/types.rs +++ b/src/api/posts/types.rs @@ -40,3 +40,10 @@ pub struct PostStatsResponse { pub struct SinglePostResponse { pub post: Option, } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RebuildResult { + pub rebuilt: u64, + pub failed: u64, + pub errors: Vec, +} diff --git a/src/pages/admin/posts.rs b/src/pages/admin/posts.rs index 4254415..050c5ad 100644 --- a/src/pages/admin/posts.rs +++ b/src/pages/admin/posts.rs @@ -5,7 +5,7 @@ use dioxus::router::components::Link; use crate::api::posts::list_posts; #[cfg(target_arch = "wasm32")] use crate::api::posts::PostListResponse; -use crate::api::posts::{delete_post, rebuild_content_html, CreatePostResponse}; +use crate::api::posts::{delete_post, rebuild_content_html, CreatePostResponse, RebuildResult}; use crate::components::skeletons::delayed_skeleton::DelayedSkeleton; use crate::components::skeletons::posts_skeleton::PostsSkeleton; use crate::models::post::Post; @@ -74,8 +74,19 @@ pub fn PostsPage(page: i32) -> Element { rebuild_result.set(None); spawn(async move { match rebuild_content_html(false).await { - Ok(count) => { - rebuild_result.set(Some(format!("已重建 {count} 篇文章"))); + Ok(RebuildResult { rebuilt, failed, errors }) => { + if failed > 0 { + let mut msg = format!( + "已重建 {rebuilt} 篇,失败 {failed} 篇" + ); + if let Some(first) = errors.first() { + msg.push_str(&format!("\n{first}")); + } + rebuild_result.set(Some(msg)); + } else { + rebuild_result + .set(Some(format!("已重建 {rebuilt} 篇文章"))); + } } Err(e) => { rebuild_result.set(Some(format!("失败: {e}")));