From 71d4126e94500951ed024865f67186206120778a Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 18 Jun 2026 13:32:55 +0800 Subject: [PATCH] fix(posts): wrap rebuild_content_html in single transaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 整批 UPDATE 纳入事务,任一写入失败整批回滚,避免产生「部分文章已重建」的 中间态(M5)。渲染失败(spawn_blocking panic)仍跳过该条不进入事务操作。 失败时返回 rebuilt:0 明确告知整批回滚。 --- src/api/posts/rebuild.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/api/posts/rebuild.rs b/src/api/posts/rebuild.rs index 32fa801..5ba7b45 100644 --- a/src/api/posts/rebuild.rs +++ b/src/api/posts/rebuild.rs @@ -32,7 +32,7 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result Result = Vec::new(); + // 整批 UPDATE 纳入单事务:中途断连或写入失败整批回滚,避免产生 + // 「部分文章已重建」的中间态(M5)。 + let tx = client.transaction().await.map_err(AppError::query)?; + for row in &rows { let id: i32 = row.get(0); let content_md: String = row.get(1); @@ -82,7 +86,7 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result Result { rebuilt += 1; } - Err(_) => { + Err(e) => { + // 事务内任一写入失败会使事务进入 abort 状态,后续写入都会失败; + // 此时整批回滚,保证不产生中间态。 failed += 1; if errors.len() < MAX_DISPLAY_ERRORS { - errors.push(format!("文章 #{id}: DB 写入失败")); + errors.push(format!("文章 #{id}: DB 写入失败(整批将回滚)")); } + tracing::error!("rebuild UPDATE 失败,整批回滚: {:?}", e); + tx.rollback().await.ok(); + return Ok(RebuildResult { + rebuilt: 0, + failed, + errors, + }); } } } + tx.commit().await.map_err(AppError::query)?; + // 重建会修改 word_count / reading_time 等列表项字段,批量影响列表、标签云、 // 标签文章及单篇缓存;这里使用全量失效作为务实的回退策略。 if rebuilt > 0 {