use crate::*; #[spacetimedb::table( accessor = user_browse_history, index(accessor = by_browse_history_user_id, btree(columns = [user_id])), index( accessor = by_browse_history_user_owner_profile, btree(columns = [user_id, owner_user_id, profile_id]) ) )] pub struct UserBrowseHistory { #[primary_key] pub(crate) browse_history_id: String, pub(crate) user_id: String, pub(crate) owner_user_id: String, pub(crate) profile_id: String, pub(crate) world_name: String, pub(crate) subtitle: String, pub(crate) summary_text: String, pub(crate) cover_image_src: Option, pub(crate) theme_mode: RuntimeBrowseHistoryThemeMode, pub(crate) author_display_name: String, pub(crate) visited_at: Timestamp, pub(crate) created_at: Timestamp, pub(crate) updated_at: Timestamp, } // procedure 面向 Axum 同步拉取浏览历史,继续沿用旧 Node 的 visitedAt 倒序输出语义。 #[spacetimedb::procedure] pub fn list_platform_browse_history( ctx: &mut ProcedureContext, input: RuntimeBrowseHistoryListInput, ) -> RuntimeBrowseHistoryProcedureResult { match ctx.try_with_tx(|tx| list_platform_browse_history_rows(tx, input.clone())) { Ok(entries) => RuntimeBrowseHistoryProcedureResult { ok: true, entries, error_message: None, }, Err(message) => RuntimeBrowseHistoryProcedureResult { ok: false, entries: Vec::new(), error_message: Some(message), }, } } // procedure 面向 Axum 承接 browse history 的单条/批量 POST,同步返回当前用户的完整列表。 #[spacetimedb::procedure] pub fn upsert_platform_browse_history_and_return( ctx: &mut ProcedureContext, input: RuntimeBrowseHistorySyncInput, ) -> RuntimeBrowseHistoryProcedureResult { match ctx.try_with_tx(|tx| upsert_platform_browse_history_rows(tx, input.clone())) { Ok(entries) => RuntimeBrowseHistoryProcedureResult { ok: true, entries, error_message: None, }, Err(message) => RuntimeBrowseHistoryProcedureResult { ok: false, entries: Vec::new(), error_message: Some(message), }, } } // procedure 面向 Axum 清空当前用户浏览历史,并直接返回空列表响应。 #[spacetimedb::procedure] pub fn clear_platform_browse_history_and_return( ctx: &mut ProcedureContext, input: RuntimeBrowseHistoryClearInput, ) -> RuntimeBrowseHistoryProcedureResult { match ctx.try_with_tx(|tx| clear_platform_browse_history_rows(tx, input.clone())) { Ok(entries) => RuntimeBrowseHistoryProcedureResult { ok: true, entries, error_message: None, }, Err(message) => RuntimeBrowseHistoryProcedureResult { ok: false, entries: Vec::new(), error_message: Some(message), }, } } fn list_platform_browse_history_rows( ctx: &ReducerContext, input: RuntimeBrowseHistoryListInput, ) -> Result, String> { let validated_input = build_runtime_browse_history_list_input(input.user_id) .map_err(|error| error.to_string())?; let mut entries = ctx .db .user_browse_history() .by_browse_history_user_id() .filter(&validated_input.user_id) .map(|row| build_runtime_browse_history_snapshot_from_row(&row)) .collect::>(); entries.sort_by(|left, right| { right .visited_at_micros .cmp(&left.visited_at_micros) .then_with(|| left.browse_history_id.cmp(&right.browse_history_id)) }); Ok(entries) } fn upsert_platform_browse_history_rows( ctx: &ReducerContext, input: RuntimeBrowseHistorySyncInput, ) -> Result, String> { let user_id = input.user_id.clone(); let prepared_entries = prepare_runtime_browse_history_entries(input).map_err(|error| error.to_string())?; for prepared in prepared_entries { let existing = ctx .db .user_browse_history() .browse_history_id() .find(&prepared.browse_history_id); let created_at = existing .as_ref() .map(|row| row.created_at) .unwrap_or_else(|| Timestamp::from_micros_since_unix_epoch(prepared.updated_at_micros)); if let Some(existing) = existing { ctx.db .user_browse_history() .browse_history_id() .delete(&existing.browse_history_id); } ctx.db.user_browse_history().insert(UserBrowseHistory { browse_history_id: prepared.browse_history_id, user_id: prepared.user_id, owner_user_id: prepared.owner_user_id, profile_id: prepared.profile_id, world_name: prepared.world_name, subtitle: prepared.subtitle, summary_text: prepared.summary_text, cover_image_src: prepared.cover_image_src, theme_mode: prepared.theme_mode, author_display_name: prepared.author_display_name, visited_at: Timestamp::from_micros_since_unix_epoch(prepared.visited_at_micros), created_at, updated_at: Timestamp::from_micros_since_unix_epoch(prepared.updated_at_micros), }); } list_platform_browse_history_rows(ctx, RuntimeBrowseHistoryListInput { user_id }) } fn clear_platform_browse_history_rows( ctx: &ReducerContext, input: RuntimeBrowseHistoryClearInput, ) -> Result, String> { let validated_input = build_runtime_browse_history_clear_input(input.user_id) .map_err(|error| error.to_string())?; let row_ids = ctx .db .user_browse_history() .by_browse_history_user_id() .filter(&validated_input.user_id) .map(|row| row.browse_history_id.clone()) .collect::>(); for row_id in row_ids { ctx.db .user_browse_history() .browse_history_id() .delete(&row_id); } Ok(Vec::new()) } fn build_runtime_browse_history_snapshot_from_row( row: &UserBrowseHistory, ) -> RuntimeBrowseHistorySnapshot { RuntimeBrowseHistorySnapshot { browse_history_id: row.browse_history_id.clone(), user_id: row.user_id.clone(), owner_user_id: row.owner_user_id.clone(), profile_id: row.profile_id.clone(), world_name: row.world_name.clone(), subtitle: row.subtitle.clone(), summary_text: row.summary_text.clone(), cover_image_src: row.cover_image_src.clone(), theme_mode: row.theme_mode, author_display_name: row.author_display_name.clone(), visited_at_micros: row.visited_at.to_micros_since_unix_epoch(), created_at_micros: row.created_at.to_micros_since_unix_epoch(), updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), } } #[allow(dead_code)] fn build_runtime_browse_history_row(snapshot: RuntimeBrowseHistorySnapshot) -> UserBrowseHistory { UserBrowseHistory { browse_history_id: snapshot.browse_history_id, user_id: snapshot.user_id, owner_user_id: snapshot.owner_user_id, profile_id: snapshot.profile_id, world_name: snapshot.world_name, subtitle: snapshot.subtitle, summary_text: snapshot.summary_text, cover_image_src: snapshot.cover_image_src, theme_mode: snapshot.theme_mode, author_display_name: snapshot.author_display_name, visited_at: Timestamp::from_micros_since_unix_epoch(snapshot.visited_at_micros), created_at: Timestamp::from_micros_since_unix_epoch(snapshot.created_at_micros), updated_at: Timestamp::from_micros_since_unix_epoch(snapshot.updated_at_micros), } }