mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
fixed
This commit is contained in:
1 parent
1e0ccf53a4
commit
befe773aa7
3 files changed
+81
-66
No files matched your search
@@ -99,7 +99,7 @@ async fn list(
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
|
||||
let results = build_flavors(&state, &ctx, models, viewer, &[]).await?;
|
||||
let results = build_flavors(&state, &ctx, models, viewer, &[], false).await?;
|
||||
Ok(Json(Paginated::build(&ctx, results, count, page, size)))
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ async fn retrieve(
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
let mut dtos = build_flavors(&state, &ctx, vec![model], viewer, &[]).await?;
|
||||
let mut dtos = build_flavors(&state, &ctx, vec![model], viewer, &[], true).await?;
|
||||
dtos.pop().map(Json).ok_or(ApiError::NotFound)
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ async fn top(
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
Ok(Json(
|
||||
build_flavors(&state, &ctx, models, viewer, &[]).await?,
|
||||
build_flavors(&state, &ctx, models, viewer, &[], false).await?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ async fn newest(
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
Ok(Json(
|
||||
build_flavors(&state, &ctx, models, viewer, &[]).await?,
|
||||
build_flavors(&state, &ctx, models, viewer, &[], false).await?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ async fn followed_top(
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
Ok(Json(
|
||||
build_flavors(&state, &ctx, models, Some(uid), &[]).await?,
|
||||
build_flavors(&state, &ctx, models, Some(uid), &[], false).await?,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -1363,7 +1363,8 @@ async fn dashboard(
|
||||
});
|
||||
|
||||
let missing_count = all_flavors.len() as i64;
|
||||
let missing_flavors = build_flavors(&state, &ctx, all_flavors, Some(uid), &followed).await?;
|
||||
let missing_flavors =
|
||||
build_flavors(&state, &ctx, all_flavors, Some(uid), &followed, false).await?;
|
||||
let user = build_user(&state, &ctx, me, Some(uid)).await?;
|
||||
|
||||
Ok(Json(DashboardOut {
|
||||
|
||||
+74
-60
@@ -189,12 +189,51 @@ pub async fn build_ratings(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Per-flavor rating rollup accumulated in a single pass over the loaded ratings,
|
||||
/// replacing the previous O(flavors × ratings) re-scan.
|
||||
struct RatingAgg {
|
||||
sum: i64,
|
||||
n: i64,
|
||||
followed_sum: i64,
|
||||
followed_n: i64,
|
||||
dist: BTreeMap<String, i64>,
|
||||
/// The viewer's own score for this flavor, if they rated it.
|
||||
viewer_score: Option<i32>,
|
||||
}
|
||||
|
||||
impl Default for RatingAgg {
|
||||
fn default() -> Self {
|
||||
RatingAgg {
|
||||
sum: 0,
|
||||
n: 0,
|
||||
followed_sum: 0,
|
||||
followed_n: 0,
|
||||
dist: empty_distribution(),
|
||||
viewer_score: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RatingAgg {
|
||||
fn average(&self) -> Option<f64> {
|
||||
(self.n > 0).then(|| self.sum as f64 / self.n as f64)
|
||||
}
|
||||
fn followed_average(&self) -> Option<f64> {
|
||||
(self.followed_n > 0).then(|| self.followed_sum as f64 / self.followed_n as f64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build `FlavorOut`s. `include_replies` controls whether each nested rating
|
||||
/// carries its replies: the flavor *detail* view needs them, but card/list
|
||||
/// callers (list/top/newest/followed_top) don't — skipping the reply query +
|
||||
/// reply-author lookups avoids shipping data the list UI never reads.
|
||||
pub async fn build_flavors(
|
||||
state: &AppState,
|
||||
ctx: &RequestCtx,
|
||||
flavors: Vec<flavor::Model>,
|
||||
viewer_id: Option<i32>,
|
||||
followed_ids: &[i32],
|
||||
include_replies: bool,
|
||||
) -> ApiResult<Vec<FlavorOut>> {
|
||||
let media = &state.config.media_url;
|
||||
if flavors.is_empty() {
|
||||
@@ -218,17 +257,21 @@ pub async fn build_flavors(
|
||||
.order_by_desc(rating::Column::CreatedAt)
|
||||
.all(&state.db)
|
||||
.await?;
|
||||
let rating_ids: Vec<i32> = ratings.iter().map(|r| r.id).collect();
|
||||
|
||||
// Replies (order: created_at asc)
|
||||
let replies = if rating_ids.is_empty() {
|
||||
Vec::new()
|
||||
// Replies (order: created_at asc) — only loaded for the detail view.
|
||||
let replies = if include_replies {
|
||||
let rating_ids: Vec<i32> = ratings.iter().map(|r| r.id).collect();
|
||||
if rating_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
Reply::find()
|
||||
.filter(reply::Column::RatingId.is_in(rating_ids))
|
||||
.order_by_asc(reply::Column::CreatedAt)
|
||||
.all(&state.db)
|
||||
.await?
|
||||
}
|
||||
} else {
|
||||
Reply::find()
|
||||
.filter(reply::Column::RatingId.is_in(rating_ids))
|
||||
.order_by_asc(reply::Column::CreatedAt)
|
||||
.all(&state.db)
|
||||
.await?
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Users referenced by ratings + replies
|
||||
@@ -267,9 +310,13 @@ pub async fn build_flavors(
|
||||
|
||||
// Flavor lookup for per-rating flavor fields
|
||||
let flavor_map: HashMap<i32, &flavor::Model> = flavors.iter().map(|f| (f.id, f)).collect();
|
||||
let followed_set: std::collections::HashSet<i32> = followed_ids.iter().copied().collect();
|
||||
|
||||
// Ratings grouped by flavor id
|
||||
// Single pass over ratings: build the nested RatingOut grouped by flavor AND
|
||||
// accumulate average / followed-average / distribution / the viewer's own
|
||||
// score per flavor.
|
||||
let mut ratings_by_flavor: HashMap<i32, Vec<RatingOut>> = HashMap::new();
|
||||
let mut agg_by_flavor: HashMap<i32, RatingAgg> = HashMap::new();
|
||||
for r in &ratings {
|
||||
let user = user_map.get(&r.user_id);
|
||||
let username = user.map(|u| u.username.clone()).unwrap_or_default();
|
||||
@@ -311,47 +358,20 @@ pub async fn build_flavors(
|
||||
created_at: crate::datetime::drf_iso(&r.created_at),
|
||||
replies: replies_by_rating.remove(&r.id).unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
|
||||
// Average + distribution per flavor, from loaded ratings
|
||||
let followed_set: std::collections::HashSet<i32> = followed_ids.iter().copied().collect();
|
||||
let mut avg_by_flavor: HashMap<i32, Option<f64>> = HashMap::new();
|
||||
let mut followed_avg_by_flavor: HashMap<i32, Option<f64>> = HashMap::new();
|
||||
let mut dist_by_flavor: HashMap<i32, BTreeMap<String, i64>> = HashMap::new();
|
||||
for fid in &flavor_ids {
|
||||
let mut dist = empty_distribution();
|
||||
let mut sum = 0i64;
|
||||
let mut n = 0i64;
|
||||
let mut fsum = 0i64;
|
||||
let mut fn_ = 0i64;
|
||||
for r in ratings.iter().filter(|r| r.flavor_id == *fid) {
|
||||
if (1..=10).contains(&r.score) {
|
||||
*dist.get_mut(&r.score.to_string()).unwrap() += 1;
|
||||
}
|
||||
sum += r.score as i64;
|
||||
n += 1;
|
||||
if followed_set.contains(&r.user_id) {
|
||||
fsum += r.score as i64;
|
||||
fn_ += 1;
|
||||
}
|
||||
let a = agg_by_flavor.entry(r.flavor_id).or_default();
|
||||
if (1..=10).contains(&r.score) {
|
||||
*a.dist.get_mut(&r.score.to_string()).unwrap() += 1;
|
||||
}
|
||||
a.sum += r.score as i64;
|
||||
a.n += 1;
|
||||
if followed_set.contains(&r.user_id) {
|
||||
a.followed_sum += r.score as i64;
|
||||
a.followed_n += 1;
|
||||
}
|
||||
if Some(r.user_id) == viewer_id {
|
||||
a.viewer_score = Some(r.score);
|
||||
}
|
||||
avg_by_flavor.insert(
|
||||
*fid,
|
||||
if n == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(sum as f64 / n as f64)
|
||||
},
|
||||
);
|
||||
followed_avg_by_flavor.insert(
|
||||
*fid,
|
||||
if fn_ == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(fsum as f64 / fn_ as f64)
|
||||
},
|
||||
);
|
||||
dist_by_flavor.insert(*fid, dist);
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(flavors.len());
|
||||
@@ -359,12 +379,8 @@ pub async fn build_flavors(
|
||||
let (cat_name, cat_slug) = cat_map.get(&f.category_id).cloned().unwrap_or_default();
|
||||
let local_paths = json_string_list(&f.local_image_paths);
|
||||
let image_urls = json_string_list(&f.image_urls);
|
||||
let user_rating = viewer_id.and_then(|vid| {
|
||||
ratings
|
||||
.iter()
|
||||
.find(|r| r.flavor_id == f.id && r.user_id == vid)
|
||||
.map(|r| r.score)
|
||||
});
|
||||
let agg = agg_by_flavor.remove(&f.id);
|
||||
let user_rating = viewer_id.and(agg.as_ref().and_then(|a| a.viewer_score));
|
||||
out.push(FlavorOut {
|
||||
id: f.id,
|
||||
name: f.name.clone(),
|
||||
@@ -372,8 +388,8 @@ pub async fn build_flavors(
|
||||
category_name: cat_name,
|
||||
category_slug: cat_slug,
|
||||
description: f.description.clone(),
|
||||
average_rating: avg_by_flavor.get(&f.id).copied().flatten(),
|
||||
followed_average_rating: followed_avg_by_flavor.get(&f.id).copied().flatten(),
|
||||
average_rating: agg.as_ref().and_then(RatingAgg::average),
|
||||
followed_average_rating: agg.as_ref().and_then(RatingAgg::followed_average),
|
||||
user_rating,
|
||||
ratings: ratings_by_flavor.remove(&f.id).unwrap_or_default(),
|
||||
image_url: flavor_primary_image(
|
||||
@@ -394,9 +410,7 @@ pub async fn build_flavors(
|
||||
is_available: f.is_available,
|
||||
is_legacy: f.is_legacy,
|
||||
shop_url: f.shop_url.clone(),
|
||||
rating_distribution: dist_by_flavor
|
||||
.remove(&f.id)
|
||||
.unwrap_or_else(empty_distribution),
|
||||
rating_distribution: agg.map(|a| a.dist).unwrap_or_else(empty_distribution),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
|
||||
Reference in new issue
Block a user