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)
|
.all(&state.db)
|
||||||
.await?;
|
.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)))
|
Ok(Json(Paginated::build(&ctx, results, count, page, size)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ async fn retrieve(
|
|||||||
.one(&state.db)
|
.one(&state.db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(ApiError::NotFound)?;
|
.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)
|
dtos.pop().map(Json).ok_or(ApiError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ async fn top(
|
|||||||
.all(&state.db)
|
.all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(
|
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)
|
.all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(
|
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)
|
.all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(
|
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_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?;
|
let user = build_user(&state, &ctx, me, Some(uid)).await?;
|
||||||
|
|
||||||
Ok(Json(DashboardOut {
|
Ok(Json(DashboardOut {
|
||||||
|
|||||||
+74
-60
@@ -189,12 +189,51 @@ pub async fn build_ratings(
|
|||||||
Ok(out)
|
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(
|
pub async fn build_flavors(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
ctx: &RequestCtx,
|
ctx: &RequestCtx,
|
||||||
flavors: Vec<flavor::Model>,
|
flavors: Vec<flavor::Model>,
|
||||||
viewer_id: Option<i32>,
|
viewer_id: Option<i32>,
|
||||||
followed_ids: &[i32],
|
followed_ids: &[i32],
|
||||||
|
include_replies: bool,
|
||||||
) -> ApiResult<Vec<FlavorOut>> {
|
) -> ApiResult<Vec<FlavorOut>> {
|
||||||
let media = &state.config.media_url;
|
let media = &state.config.media_url;
|
||||||
if flavors.is_empty() {
|
if flavors.is_empty() {
|
||||||
@@ -218,17 +257,21 @@ pub async fn build_flavors(
|
|||||||
.order_by_desc(rating::Column::CreatedAt)
|
.order_by_desc(rating::Column::CreatedAt)
|
||||||
.all(&state.db)
|
.all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
let rating_ids: Vec<i32> = ratings.iter().map(|r| r.id).collect();
|
|
||||||
|
|
||||||
// Replies (order: created_at asc)
|
// Replies (order: created_at asc) — only loaded for the detail view.
|
||||||
let replies = if rating_ids.is_empty() {
|
let replies = if include_replies {
|
||||||
Vec::new()
|
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 {
|
} else {
|
||||||
Reply::find()
|
Vec::new()
|
||||||
.filter(reply::Column::RatingId.is_in(rating_ids))
|
|
||||||
.order_by_asc(reply::Column::CreatedAt)
|
|
||||||
.all(&state.db)
|
|
||||||
.await?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Users referenced by ratings + replies
|
// Users referenced by ratings + replies
|
||||||
@@ -267,9 +310,13 @@ pub async fn build_flavors(
|
|||||||
|
|
||||||
// Flavor lookup for per-rating flavor fields
|
// Flavor lookup for per-rating flavor fields
|
||||||
let flavor_map: HashMap<i32, &flavor::Model> = flavors.iter().map(|f| (f.id, f)).collect();
|
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 ratings_by_flavor: HashMap<i32, Vec<RatingOut>> = HashMap::new();
|
||||||
|
let mut agg_by_flavor: HashMap<i32, RatingAgg> = HashMap::new();
|
||||||
for r in &ratings {
|
for r in &ratings {
|
||||||
let user = user_map.get(&r.user_id);
|
let user = user_map.get(&r.user_id);
|
||||||
let username = user.map(|u| u.username.clone()).unwrap_or_default();
|
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),
|
created_at: crate::datetime::drf_iso(&r.created_at),
|
||||||
replies: replies_by_rating.remove(&r.id).unwrap_or_default(),
|
replies: replies_by_rating.remove(&r.id).unwrap_or_default(),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Average + distribution per flavor, from loaded ratings
|
let a = agg_by_flavor.entry(r.flavor_id).or_default();
|
||||||
let followed_set: std::collections::HashSet<i32> = followed_ids.iter().copied().collect();
|
if (1..=10).contains(&r.score) {
|
||||||
let mut avg_by_flavor: HashMap<i32, Option<f64>> = HashMap::new();
|
*a.dist.get_mut(&r.score.to_string()).unwrap() += 1;
|
||||||
let mut followed_avg_by_flavor: HashMap<i32, Option<f64>> = HashMap::new();
|
}
|
||||||
let mut dist_by_flavor: HashMap<i32, BTreeMap<String, i64>> = HashMap::new();
|
a.sum += r.score as i64;
|
||||||
for fid in &flavor_ids {
|
a.n += 1;
|
||||||
let mut dist = empty_distribution();
|
if followed_set.contains(&r.user_id) {
|
||||||
let mut sum = 0i64;
|
a.followed_sum += r.score as i64;
|
||||||
let mut n = 0i64;
|
a.followed_n += 1;
|
||||||
let mut fsum = 0i64;
|
}
|
||||||
let mut fn_ = 0i64;
|
if Some(r.user_id) == viewer_id {
|
||||||
for r in ratings.iter().filter(|r| r.flavor_id == *fid) {
|
a.viewer_score = Some(r.score);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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());
|
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 (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 local_paths = json_string_list(&f.local_image_paths);
|
||||||
let image_urls = json_string_list(&f.image_urls);
|
let image_urls = json_string_list(&f.image_urls);
|
||||||
let user_rating = viewer_id.and_then(|vid| {
|
let agg = agg_by_flavor.remove(&f.id);
|
||||||
ratings
|
let user_rating = viewer_id.and(agg.as_ref().and_then(|a| a.viewer_score));
|
||||||
.iter()
|
|
||||||
.find(|r| r.flavor_id == f.id && r.user_id == vid)
|
|
||||||
.map(|r| r.score)
|
|
||||||
});
|
|
||||||
out.push(FlavorOut {
|
out.push(FlavorOut {
|
||||||
id: f.id,
|
id: f.id,
|
||||||
name: f.name.clone(),
|
name: f.name.clone(),
|
||||||
@@ -372,8 +388,8 @@ pub async fn build_flavors(
|
|||||||
category_name: cat_name,
|
category_name: cat_name,
|
||||||
category_slug: cat_slug,
|
category_slug: cat_slug,
|
||||||
description: f.description.clone(),
|
description: f.description.clone(),
|
||||||
average_rating: avg_by_flavor.get(&f.id).copied().flatten(),
|
average_rating: agg.as_ref().and_then(RatingAgg::average),
|
||||||
followed_average_rating: followed_avg_by_flavor.get(&f.id).copied().flatten(),
|
followed_average_rating: agg.as_ref().and_then(RatingAgg::followed_average),
|
||||||
user_rating,
|
user_rating,
|
||||||
ratings: ratings_by_flavor.remove(&f.id).unwrap_or_default(),
|
ratings: ratings_by_flavor.remove(&f.id).unwrap_or_default(),
|
||||||
image_url: flavor_primary_image(
|
image_url: flavor_primary_image(
|
||||||
@@ -394,9 +410,7 @@ pub async fn build_flavors(
|
|||||||
is_available: f.is_available,
|
is_available: f.is_available,
|
||||||
is_legacy: f.is_legacy,
|
is_legacy: f.is_legacy,
|
||||||
shop_url: f.shop_url.clone(),
|
shop_url: f.shop_url.clone(),
|
||||||
rating_distribution: dist_by_flavor
|
rating_distribution: agg.map(|a| a.dist).unwrap_or_else(empty_distribution),
|
||||||
.remove(&f.id)
|
|
||||||
.unwrap_or_else(empty_distribution),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
|
|||||||
Reference in new issue
Block a user