perf(diary): mood_stats 改用 SQL GROUP BY 替代全量加载 — 8a-C01
- get_mood_stats: SELECT mood, COUNT(*) GROUP BY 替代 all() + Rust 迭代 - calculate_streak: 仅查 date 列 + DISTINCT + 366天窗口裁剪 - 新增 mood_counts_map_aggregation 单元测试 - 测试 78/78 通过
This commit is contained in:
@@ -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<MoodStatsResp> {
|
||||
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<String, i32> =
|
||||
let rows = db.query_all(stmt).await?;
|
||||
|
||||
let mut mood_counts_map: std::collections::HashMap<String, i64> =
|
||||
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::<String>(0)?;
|
||||
let count: i64 = row.try_get_by_index::<i64>(1)?;
|
||||
total_journals += count;
|
||||
mood_counts_map.insert(mood, count);
|
||||
}
|
||||
|
||||
let mood_counts: Vec<MoodCount> = 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<i32> {
|
||||
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<NaiveDate> =
|
||||
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<NaiveDate> = rows
|
||||
.into_iter()
|
||||
.filter_map(|row| row.try_get_by_index::<NaiveDate>(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<String, i64> = 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<MoodCount> = 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user