Add public work read model and smooth puzzle transitions

This commit is contained in:
kdletters
2026-05-26 16:38:27 +08:00
parent 545f315cbc
commit aeee782fe0
47 changed files with 2679 additions and 79 deletions

View File

@@ -0,0 +1,171 @@
use super::*;
use crate::mapper::*;
impl SpacetimeClient {
pub async fn list_public_work_gallery_entries(
&self,
) -> Result<Vec<PublicWorkGalleryEntryRecord>, SpacetimeClientError> {
self.read_after_connect("list_public_work_gallery_entries", move |connection| {
let recent_play_counts = public_work_recent_play_counts_by_source(connection);
let mut entries = connection
.db()
.public_work_gallery_entry()
.iter()
.collect::<Vec<_>>();
sort_public_work_gallery_entries(&mut entries);
Ok(entries
.into_iter()
.map(|entry| {
let recent_play_count_7d = recent_play_counts
.get(&(entry.source_type.clone(), entry.profile_id.clone()))
.copied()
.unwrap_or(0);
map_public_work_gallery_entry(entry, recent_play_count_7d)
})
.collect())
})
.await
}
pub async fn list_public_work_detail_entries(
&self,
) -> Result<Vec<PublicWorkDetailEntryRecord>, SpacetimeClientError> {
self.read_after_connect("list_public_work_detail_entries", move |connection| {
let recent_play_counts = public_work_recent_play_counts_by_source(connection);
let mut entries = connection
.db()
.public_work_detail_entry()
.iter()
.collect::<Vec<_>>();
sort_public_work_detail_entries(&mut entries);
Ok(entries
.into_iter()
.map(|entry| {
let recent_play_count_7d = recent_play_counts
.get(&(entry.source_type.clone(), entry.profile_id.clone()))
.copied()
.unwrap_or(0);
map_public_work_gallery_entry_to_detail_entry(entry, recent_play_count_7d)
})
.collect())
})
.await
}
pub async fn get_public_work_detail_by_code(
&self,
public_work_code: String,
) -> Result<PublicWorkDetailEntryRecord, SpacetimeClientError> {
let public_work_code = public_work_code.trim().to_string();
if public_work_code.is_empty() {
return Err(SpacetimeClientError::validation_failed(
"publicWorkCode 不能为空",
));
}
self.read_after_connect("get_public_work_detail_by_code", move |connection| {
let recent_play_counts = public_work_recent_play_counts_by_source(connection);
let entry = connection
.db()
.public_work_detail_entry()
.iter()
.find(|entry| {
entry
.public_work_code
.eq_ignore_ascii_case(&public_work_code)
})
.ok_or_else(|| SpacetimeClientError::Procedure("公开作品不存在".to_string()))?;
let recent_play_count_7d = recent_play_counts
.get(&(entry.source_type.clone(), entry.profile_id.clone()))
.copied()
.unwrap_or(0);
Ok(map_public_work_gallery_entry_to_detail_entry(
entry,
recent_play_count_7d,
))
})
.await
}
pub async fn get_public_work_detail_by_source_profile(
&self,
source_type: String,
profile_id: String,
) -> Result<PublicWorkDetailEntryRecord, SpacetimeClientError> {
let source_type = source_type.trim().to_string();
let profile_id = profile_id.trim().to_string();
if source_type.is_empty() || profile_id.is_empty() {
return Err(SpacetimeClientError::validation_failed(
"sourceType 和 profileId 不能为空",
));
}
self.read_after_connect(
"get_public_work_detail_by_source_profile",
move |connection| {
let recent_play_counts = public_work_recent_play_counts_by_source(connection);
let entry = connection
.db()
.public_work_detail_entry()
.iter()
.find(|entry| {
entry.source_type == source_type && entry.profile_id == profile_id
})
.ok_or_else(|| SpacetimeClientError::Procedure("公开作品不存在".to_string()))?;
let recent_play_count_7d = recent_play_counts
.get(&(entry.source_type.clone(), entry.profile_id.clone()))
.copied()
.unwrap_or(0);
Ok(map_public_work_gallery_entry_to_detail_entry(
entry,
recent_play_count_7d,
))
},
)
.await
}
}
fn sort_public_work_gallery_entries(entries: &mut [PublicWorkGalleryEntry]) {
entries.sort_by(|left, right| {
right
.sort_time_micros
.cmp(&left.sort_time_micros)
.then_with(|| left.source_type.cmp(&right.source_type))
.then_with(|| left.profile_id.cmp(&right.profile_id))
});
}
fn sort_public_work_detail_entries(entries: &mut [PublicWorkDetailEntry]) {
entries.sort_by(|left, right| {
right
.sort_time_micros
.cmp(&left.sort_time_micros)
.then_with(|| left.source_type.cmp(&right.source_type))
.then_with(|| left.profile_id.cmp(&right.profile_id))
});
}
fn public_work_recent_play_counts_by_source(
connection: &DbConnection,
) -> HashMap<(String, String), u32> {
let current_day = current_public_work_day();
let first_day = current_day - (PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1);
let mut counts = HashMap::new();
for row in connection.db().public_work_play_daily_stat().iter() {
if row.played_day < first_day || row.played_day > current_day {
continue;
}
let entry = counts
.entry((row.source_type, row.profile_id))
.or_insert(0_u32);
*entry = entry.saturating_add(row.play_count);
}
counts
}