From 0c9ada242a081c57838eb52571e9d33e6995e486 Mon Sep 17 00:00:00 2001 From: iven Date: Wed, 3 Jun 2026 15:37:09 +0800 Subject: [PATCH] =?UTF-8?q?perf(diary):=20mood=5Fstats=20=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20SQL=20GROUP=20BY=20=E6=9B=BF=E4=BB=A3=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E5=8A=A0=E8=BD=BD=20=E2=80=94=208a-C01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_mood_stats: SELECT mood, COUNT(*) GROUP BY 替代 all() + Rust 迭代 - calculate_streak: 仅查 date 列 + DISTINCT + 366天窗口裁剪 - 新增 mood_counts_map_aggregation 单元测试 - 测试 78/78 通过 --- .../src/service/mood_stats_service.rs | 146 ++++++++++++------ 1 file changed, 98 insertions(+), 48 deletions(-) diff --git a/crates/erp-diary/src/service/mood_stats_service.rs b/crates/erp-diary/src/service/mood_stats_service.rs index 06dacbf..20f7491 100644 --- a/crates/erp-diary/src/service/mood_stats_service.rs +++ b/crates/erp-diary/src/service/mood_stats_service.rs @@ -1,12 +1,15 @@ // 心情统计服务 — 心情趋势与连续天数 +// +// 性能优化 (8a-C01): +// - mood_counts: SQL GROUP BY 替代全量加载 + Rust 迭代 +// - streak: 仅查 date 列 + DISTINCT + 时间窗口裁剪,避免加载所有日记字段 use chrono::{Duration, NaiveDate, Utc}; -use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; +use sea_orm::{ConnectionTrait, DatabaseConnection}; use serde::Deserialize; use uuid::Uuid; use crate::dto::{Mood, MoodCount, MoodStatsResp}; -use crate::entity::journal_entry; use crate::error::DiaryResult; /// 统计查询范围 @@ -37,8 +40,8 @@ pub struct MoodStatsService; impl MoodStatsService { /// 获取心情统计 /// - /// 统计指定时间范围内各心情出现次数、连续写日记天数、 - /// 最常用心情等数据。 + /// 使用 SQL GROUP BY 聚合,避免全量加载日记到内存。 + /// 性能: O(mood_types) 而非 O(total_journals)。 pub async fn get_mood_stats( tenant_id: Uuid, user_id: Uuid, @@ -47,24 +50,34 @@ impl MoodStatsService { ) -> DiaryResult { let since_date = (Utc::now() - Duration::days(period.days())).date_naive(); - // 查询时间范围内的日记 - let journals = journal_entry::Entity::find() - .filter(journal_entry::Column::TenantId.eq(tenant_id)) - .filter(journal_entry::Column::AuthorId.eq(user_id)) - .filter(journal_entry::Column::Date.gte(since_date)) - .filter(journal_entry::Column::DeletedAt.is_null()) - .all(db) - .await?; + // SQL GROUP BY — 一次查询获取所有心情计数(替代全量加载) + let sql = r#" + SELECT mood, COUNT(*) AS count + FROM journal_entry + WHERE tenant_id = $1 + AND author_id = $2 + AND date >= $3 + AND deleted_at IS NULL + GROUP BY mood + "#; - let total_journals = journals.len() as i32; + let stmt = sea_orm::Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + sql, + [tenant_id.into(), user_id.into(), since_date.into()], + ); - // 计算各心情出现次数 - let mut mood_counts_map: std::collections::HashMap = + let rows = db.query_all(stmt).await?; + + let mut mood_counts_map: std::collections::HashMap = std::collections::HashMap::new(); - for journal in &journals { - *mood_counts_map - .entry(journal.mood.clone()) - .or_insert(0) += 1; + let mut total_journals: i64 = 0; + + for row in rows { + let mood: String = row.try_get_by_index::(0)?; + let count: i64 = row.try_get_by_index::(1)?; + total_journals += count; + mood_counts_map.insert(mood, count); } let mood_counts: Vec = mood_counts_map @@ -77,53 +90,64 @@ impl MoodStatsService { }; MoodCount { mood: parse_mood(mood), - count, + count: count as i32, percentage, } }) .collect(); - // 查找最常用心情 let dominant_mood = mood_counts .iter() .max_by_key(|mc| mc.count) .map(|mc| mc.mood.clone()); - // 计算连续写日记天数 let streak_days = Self::calculate_streak(tenant_id, user_id, db).await?; Ok(MoodStatsResp { mood_counts, streak_days, - total_journals, + total_journals: total_journals as i32, dominant_mood, }) } /// 计算连续写日记天数 /// - /// 从今天开始往前数,连续有日记记录的天数。 + /// 只查询 date 列 + DISTINCT,按日期降序,限定 366 天窗口。 + /// 避免全量加载所有日记字段到内存。 async fn calculate_streak( tenant_id: Uuid, user_id: Uuid, db: &DatabaseConnection, ) -> DiaryResult { - let journals = journal_entry::Entity::find() - .filter(journal_entry::Column::TenantId.eq(tenant_id)) - .filter(journal_entry::Column::AuthorId.eq(user_id)) - .filter(journal_entry::Column::DeletedAt.is_null()) - .all(db) - .await?; + let cutoff = (Utc::now() - Duration::days(366)).date_naive(); - // 收集所有有日记的日期 - let mut dates: std::collections::HashSet = - journals.into_iter().map(|j| j.date).collect(); + let sql = r#" + SELECT DISTINCT date + FROM journal_entry + WHERE tenant_id = $1 + AND author_id = $2 + AND date >= $3 + AND deleted_at IS NULL + ORDER BY date DESC + "#; + + let stmt = sea_orm::Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + sql, + [tenant_id.into(), user_id.into(), cutoff.into()], + ); + + let rows = db.query_all(stmt).await?; + + let dates: std::collections::HashSet = rows + .into_iter() + .filter_map(|row| row.try_get_by_index::(0).ok()) + .collect(); let mut streak = 0i32; let mut check_date = Utc::now().date_naive(); - - // 从今天开始往前检查 - while dates.remove(&check_date) { + while dates.contains(&check_date) { streak += 1; check_date -= Duration::days(1); } @@ -206,8 +230,7 @@ mod tests { let mut streak = 0i32; let mut check_date = today; - let mut mutable_dates = dates.clone(); - while mutable_dates.remove(&check_date) { + while dates.contains(&check_date) { streak += 1; check_date -= Duration::days(1); } @@ -223,8 +246,7 @@ mod tests { let mut streak = 0i32; let mut check_date = today; - let mut mutable_dates = dates.clone(); - while mutable_dates.remove(&check_date) { + while dates.contains(&check_date) { streak += 1; check_date -= Duration::days(1); } @@ -240,8 +262,7 @@ mod tests { let mut streak = 0i32; let mut check_date = today; - let mut mutable_dates = dates.clone(); - while mutable_dates.remove(&check_date) { + while dates.contains(&check_date) { streak += 1; check_date -= Duration::days(1); } @@ -258,8 +279,7 @@ mod tests { let mut streak = 0i32; let mut check_date = today; - let mut mutable_dates = dates.clone(); - while mutable_dates.remove(&check_date) { + while dates.contains(&check_date) { streak += 1; check_date -= Duration::days(1); } @@ -271,9 +291,9 @@ mod tests { #[test] fn mood_counts_percentage_calculation() { // 模拟聚合逻辑:3 happy + 2 calm = 5 total - let total = 5i32; - let happy_count = 3i32; - let calm_count = 2i32; + let total = 5i64; + let happy_count = 3i64; + let calm_count = 2i64; let happy_pct = (happy_count as f64 / total as f64) * 100.0; let calm_pct = (calm_count as f64 / total as f64) * 100.0; @@ -285,8 +305,38 @@ mod tests { #[test] fn mood_counts_empty_total_zero_percentage() { // 无日记时,百分比为 0 - let total = 0i32; + let total = 0i64; let percentage = if total > 0 { 100.0 } else { 0.0 }; assert_eq!(percentage, 0.0); } + + #[test] + fn mood_counts_map_aggregation() { + // 模拟 SQL GROUP BY 结果在 Rust 中构建 MoodCount 列表 + let mut map: std::collections::HashMap = std::collections::HashMap::new(); + *map.entry("happy".to_string()).or_insert(0) += 3; + *map.entry("calm".to_string()).or_insert(0) += 2; + *map.entry("happy".to_string()).or_insert(0) += 1; + + let total: i64 = map.values().sum(); + assert_eq!(total, 6); + assert_eq!(*map.get("happy").unwrap(), 4); + assert_eq!(*map.get("calm").unwrap(), 2); + + let counts: Vec = map + .iter() + .map(|(mood, &count)| { + let percentage = (count as f64 / total as f64) * 100.0; + MoodCount { + mood: parse_mood(mood), + count: count as i32, + percentage, + } + }) + .collect(); + + assert_eq!(counts.len(), 2); + let happy_mc = counts.iter().find(|c| matches!(c.mood, Mood::Happy)).unwrap(); + assert!((happy_mc.percentage - 66.6667).abs() < 0.01); + } }