perf(posts): remove content_md from list/search SQL, read stored word counts

This commit is contained in:
xfy 2026-06-17 16:06:27 +08:00
parent a6f08d5d3f
commit 1eedab8f21
5 changed files with 45 additions and 24 deletions

View File

@ -63,7 +63,7 @@ pub(super) async fn row_to_post_list(
/// 将数据库行转换为轻量列表项 DTO。
///
/// 不包含 `content_md`/`content_html`,但会临时读取 `content_md` 以计算字数与阅读时长
/// 不包含 `content_md`/`content_html`;字数与阅读时长直接读取已持久化的列
/// 同步函数,不依赖数据库连接。
#[cfg(feature = "server")]
pub(super) fn row_to_post_list_item(row: &tokio_postgres::Row) -> PostListItem {
@ -79,9 +79,8 @@ pub(super) fn row_to_post_list_item(row: &tokio_postgres::Row) -> PostListItem {
.filter(|t| !t.is_empty())
.collect();
// 临时读取 content_md 计算字数与阅读时长,不存入 DTO。
let content_md: String = row.get("content_md");
let word_count = count_words(&content_md);
let word_count: i32 = row.get("word_count");
let reading_time: i32 = row.get("reading_time");
PostListItem {
id,
@ -96,8 +95,8 @@ pub(super) fn row_to_post_list_item(row: &tokio_postgres::Row) -> PostListItem {
deleted_at: row.try_get("deleted_at").ok(),
tags,
cover_image: row.get("cover_image"),
reading_time: (word_count / 200).max(1),
word_count,
reading_time: reading_time.max(1) as u32,
word_count: word_count.max(0) as u32,
}
}
@ -150,6 +149,17 @@ pub(super) async fn row_to_post_full(
None
};
// 读取正文与已持久化的字数/阅读时长;若列尚未回填(旧数据为 0则现场计算。
let content_md: String = row.get("content_md");
let stored_word_count: i32 = row.get("word_count");
let stored_reading_time: i32 = row.get("reading_time");
let (word_count, reading_time) = if stored_word_count > 0 && stored_reading_time > 0 {
(stored_word_count as u32, stored_reading_time as u32)
} else {
let wc = count_words(&content_md);
(wc, (wc / 200).max(1))
};
let content_html: Option<String> = row.get("content_html");
let toc_html_row: Option<String> = row.get("toc_html");
@ -157,7 +167,6 @@ pub(super) async fn row_to_post_full(
let (content_html, toc_html) = if let Some(html) = content_html {
(html, toc_html_row)
} else {
let content_md: String = row.get("content_md");
let rendered = crate::api::markdown::render_markdown_enhanced(&content_md);
(
rendered.html,
@ -169,9 +178,6 @@ pub(super) async fn row_to_post_full(
)
};
let content_md: String = row.get("content_md");
let word_count = count_words(&content_md);
Post {
id,
author_id: row.get("author_id"),
@ -187,7 +193,7 @@ pub(super) async fn row_to_post_full(
deleted_at: row.try_get("deleted_at").ok(),
tags,
cover_image: row.get("cover_image"),
reading_time: (word_count / 200).max(1),
reading_time,
word_count,
toc_html,
prev_post,

View File

@ -82,8 +82,9 @@ pub async fn list_published_posts(
let rows = client
.query(
"SELECT
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
p.id, p.author_id, p.title, p.slug, p.summary, p.status,
p.published_at, p.created_at, p.updated_at, p.cover_image,
p.word_count, p.reading_time,
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags
FROM posts p
LEFT JOIN post_tags pt ON p.id = pt.post_id
@ -139,8 +140,9 @@ pub async fn list_posts(page: i32, per_page: i32) -> Result<PostListResponse, Se
let rows = client
.query(
"SELECT
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
p.id, p.author_id, p.title, p.slug, p.summary, p.status,
p.published_at, p.created_at, p.updated_at, p.cover_image,
p.word_count, p.reading_time,
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags
FROM posts p
LEFT JOIN post_tags pt ON p.id = pt.post_id
@ -198,8 +200,9 @@ pub async fn list_deleted_posts(
let rows = client
.query(
"SELECT
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image, p.deleted_at,
p.id, p.author_id, p.title, p.slug, p.summary, p.status,
p.published_at, p.created_at, p.updated_at, p.cover_image, p.deleted_at,
p.word_count, p.reading_time,
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags
FROM posts p
LEFT JOIN post_tags pt ON p.id = pt.post_id
@ -251,8 +254,9 @@ pub async fn get_posts_by_tag(tag_name: String) -> Result<PostListResponse, Serv
let rows = client
.query(
"SELECT
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
p.id, p.author_id, p.title, p.slug, p.summary, p.status,
p.published_at, p.created_at, p.updated_at, p.cover_image,
p.word_count, p.reading_time,
COALESCE(array_agg(t2.name) FILTER (WHERE t2.name IS NOT NULL), '{}') as tags
FROM posts p
JOIN post_tags pt ON p.id = pt.post_id

View File

@ -75,6 +75,7 @@ pub async fn get_post_by_slug(slug: String) -> Result<SinglePostResponse, Server
"SELECT
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md, p.content_html, p.toc_html,
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
p.word_count, p.reading_time,
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags,
prev.title as prev_title, prev.slug as prev_slug,
next.title as next_title, next.slug as next_slug

View File

@ -75,10 +75,19 @@ pub async fn rebuild_content_html(rebuild_all: bool) -> Result<RebuildResult, Se
Some(rendered.toc_html)
};
let word_count = crate::utils::text::count_words(&content_md);
let reading_time = (word_count / 200).max(1);
match client
.execute(
"UPDATE posts SET content_html = $1, toc_html = $2 WHERE id = $3",
&[&rendered.html, &toc_html, &id],
"UPDATE posts SET content_html = $1, toc_html = $2, word_count = $3, reading_time = $4 WHERE id = $5",
&[
&rendered.html,
&toc_html,
&(word_count as i32),
&(reading_time as i32),
&id,
],
)
.await
{

View File

@ -57,8 +57,9 @@ pub async fn search_posts(query: String) -> Result<PostListResponse, ServerFnErr
let rows = client
.query(
"SELECT
p.id, p.author_id, p.title, p.slug, p.summary, p.content_md,
p.status, p.published_at, p.created_at, p.updated_at, p.cover_image,
p.id, p.author_id, p.title, p.slug, p.summary, p.status,
p.published_at, p.created_at, p.updated_at, p.cover_image,
p.word_count, p.reading_time,
COALESCE(array_agg(t.name) FILTER (WHERE t.name IS NOT NULL), '{}') as tags,
word_similarity(p.search_text, $2) AS sml
FROM posts p