diff --git a/migrations/010_post_word_counts.sql b/migrations/010_post_word_counts.sql new file mode 100644 index 0000000..776f76b --- /dev/null +++ b/migrations/010_post_word_counts.sql @@ -0,0 +1,8 @@ +-- 为 posts 表添加字数与阅读时长列。 +-- +-- 旧数据默认 0,在管理员执行重建或编辑文章后自动回填。 +-- 0 也被 row_to_post_full 用作“未计算”标记,触发基于 content_md 的回退计算。 + +ALTER TABLE posts + ADD COLUMN IF NOT EXISTS word_count INTEGER NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS reading_time INTEGER NOT NULL DEFAULT 0; diff --git a/src/api/posts/create.rs b/src/api/posts/create.rs index d485f27..7e10a6c 100644 --- a/src/api/posts/create.rs +++ b/src/api/posts/create.rs @@ -91,6 +91,10 @@ pub async fn create_post( let post_status = PostStatus::from_str(&status).unwrap_or(PostStatus::Draft); let cover_image = cover_image.filter(|s| !s.trim().is_empty()); + // 计算字数与阅读时长,随文章一并持久化,供列表查询直接使用。 + let word_count = crate::utils::text::count_words(&content_md); + let reading_time = (word_count / 200).max(1); + // 发布状态的文章设置当前发布时间;草稿则为 None。 let published_at = if post_status == PostStatus::Published { Some(chrono::Utc::now()) @@ -106,8 +110,8 @@ pub async fn create_post( // 插入文章记录。 let row = tx .query_one( - "INSERT INTO posts (author_id, title, slug, summary, content_md, content_html, toc_html, status, published_at, cover_image) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + "INSERT INTO posts (author_id, title, slug, summary, content_md, content_html, toc_html, status, published_at, cover_image, word_count, reading_time) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id", &[ &user.id, @@ -120,6 +124,8 @@ pub async fn create_post( &post_status.as_str(), &published_at, &cover_image, + &(word_count as i32), + &(reading_time as i32), ], ) .await diff --git a/src/api/posts/update.rs b/src/api/posts/update.rs index ed2ebe4..f28069d 100644 --- a/src/api/posts/update.rs +++ b/src/api/posts/update.rs @@ -55,6 +55,10 @@ pub async fn update_post( let post_status = PostStatus::from_str(&status).unwrap_or(PostStatus::Draft); let cover_image = cover_image.filter(|s| !s.trim().is_empty()); + // 重新计算字数与阅读时长,保持与正文同步。 + let word_count = crate::utils::text::count_words(&content_md); + let reading_time = (word_count / 200).max(1); + let tx = client.transaction().await.map_err(AppError::tx)?; // 查询旧 slug,用于后续缓存失效。 @@ -150,8 +154,8 @@ pub async fn update_post( // 更新文章主表。 let updated = tx .execute( - "UPDATE posts SET title = $1, slug = $2, summary = $3, content_md = $4, content_html = $5, toc_html = $6, status = $7, published_at = $8, cover_image = $9, updated_at = NOW() - WHERE id = $10", + "UPDATE posts SET title = $1, slug = $2, summary = $3, content_md = $4, content_html = $5, toc_html = $6, status = $7, published_at = $8, cover_image = $9, word_count = $10, reading_time = $11, updated_at = NOW() + WHERE id = $12", &[ &title.trim(), &final_slug, @@ -162,6 +166,8 @@ pub async fn update_post( &post_status.as_str(), &published_at, &cover_image, + &(word_count as i32), + &(reading_time as i32), &post_id, ], )