yggdrasil/api/posts/
rebuild.rs1use dioxus::prelude::*;
9
10#[cfg(feature = "server")]
11use super::helpers::{get_current_admin_user, render_post_fields_minimal};
12#[cfg(feature = "server")]
13use crate::api::error::AppError;
14use crate::api::posts::{CreatePostResponse, RebuildResult};
15#[cfg(feature = "server")]
16use crate::db::pool::get_conn;
17
18#[cfg(feature = "server")]
20const REBUILD_BATCH_LIMIT: i64 = 500;
21#[cfg(feature = "server")]
23const MAX_DISPLAY_ERRORS: usize = 5;
24
25#[server(RebuildContentHtml, "/api")]
30pub async fn rebuild_content_html(rebuild_all: bool) -> Result<RebuildResult, ServerFnError> {
31 let _user = get_current_admin_user().await?;
32
33 #[cfg(feature = "server")]
34 {
35 let mut client = get_conn().await.map_err(AppError::db_conn)?;
36
37 let query = if rebuild_all {
42 format!(
43 "SELECT id, content_md FROM posts WHERE deleted_at IS NULL ORDER BY id LIMIT {REBUILD_BATCH_LIMIT} FOR UPDATE"
44 )
45 } else {
46 format!(
47 "SELECT id, content_md FROM posts WHERE deleted_at IS NULL AND content_html IS NULL ORDER BY id LIMIT {REBUILD_BATCH_LIMIT} FOR UPDATE"
48 )
49 };
50
51 let mut rebuilt: u64 = 0;
52 let mut failed: u64 = 0;
53 let mut errors: Vec<String> = Vec::new();
54
55 let tx = client.transaction().await.map_err(AppError::query)?;
58
59 let rows = tx.query(&query, &[]).await.map_err(AppError::query)?;
60
61 for row in &rows {
62 let id: i32 = row.get(0);
63 let content_md: String = row.get(1);
64
65 let (content_html, toc_html, word_count, reading_time) =
67 match render_post_fields_minimal(&content_md).await {
68 Ok(f) => f,
69 Err(_) => {
70 failed += 1;
71 if errors.len() < MAX_DISPLAY_ERRORS {
72 errors.push(format!("文章 #{id}: 渲染异常"));
73 }
74 continue;
75 }
76 };
77
78 match tx
79 .execute(
80 "UPDATE posts SET content_html = $1, toc_html = $2, word_count = $3, reading_time = $4 WHERE id = $5",
81 &[
82 &content_html,
83 &toc_html,
84 &word_count,
85 &reading_time,
86 &id,
87 ],
88 )
89 .await
90 {
91 Ok(_) => {
92 rebuilt += 1;
93 }
94 Err(e) => {
95 failed += 1;
98 if errors.len() < MAX_DISPLAY_ERRORS {
99 errors.push(format!("文章 #{id}: DB 写入失败(整批将回滚)"));
100 }
101 tracing::error!("rebuild UPDATE 失败,整批回滚: {:?}", e);
102 tx.rollback().await.ok();
103 return Ok(RebuildResult {
104 rebuilt: 0,
105 failed,
106 errors,
107 });
108 }
109 }
110 }
111
112 tx.commit().await.map_err(AppError::query)?;
113
114 if rebuilt > 0 {
117 crate::cache::invalidate_all_post_caches();
118 crate::cache::invalidate_search_results();
119 crate::ssr_cache::invalidate_ssr_all_public();
121 crate::ssr_cache::bump_global_generation();
122 }
123
124 Ok(RebuildResult {
125 rebuilt,
126 failed,
127 errors,
128 })
129 }
130
131 #[cfg(not(feature = "server"))]
132 {
133 Ok(RebuildResult {
134 rebuilt: 0,
135 failed: 0,
136 errors: vec![],
137 })
138 }
139}
140
141#[server(RebuildPostContentHtml, "/api")]
147pub async fn rebuild_post_content_html(post_id: i32) -> Result<CreatePostResponse, ServerFnError> {
148 let _user = get_current_admin_user().await?;
149
150 #[cfg(feature = "server")]
151 {
152 let mut client = get_conn().await.map_err(AppError::db_conn)?;
153 let tx = client.transaction().await.map_err(AppError::tx)?;
154
155 let row = tx
158 .query_opt(
159 "SELECT content_md, slug FROM posts WHERE id = $1 AND deleted_at IS NULL FOR UPDATE",
160 &[&post_id],
161 )
162 .await
163 .map_err(AppError::query)?;
164
165 let Some(row) = row else {
166 return Ok(CreatePostResponse::err("文章不存在".to_string()));
167 };
168
169 let content_md: String = row.get(0);
170 let slug: String = row.get(1);
171
172 let (content_html, toc_html, word_count, reading_time) =
174 render_post_fields_minimal(&content_md).await?;
175
176 tx.execute(
177 "UPDATE posts SET content_html = $1, toc_html = $2, word_count = $3, reading_time = $4 WHERE id = $5",
178 &[
179 &content_html,
180 &toc_html,
181 &word_count,
182 &reading_time,
183 &post_id,
184 ],
185 )
186 .await
187 .map_err(AppError::query)?;
188
189 tx.commit().await.map_err(AppError::tx)?;
190
191 crate::cache::invalidate_post_lists();
194 crate::cache::invalidate_search_results();
195 crate::cache::invalidate_post_by_slug(&slug).await;
196 crate::ssr_cache::invalidate_ssr_route(&format!("/post/{slug}"));
198 crate::ssr_cache::invalidate_ssr_all_public();
199 crate::ssr_cache::bump_global_generation();
200
201 Ok(CreatePostResponse::ok(
202 "重建成功".to_string(),
203 post_id,
204 slug,
205 ))
206 }
207
208 #[cfg(not(feature = "server"))]
209 {
210 Ok(CreatePostResponse::err("server only".to_string()))
211 }
212}