This commit is contained in:
220
server-rs/crates/spacetime-module/src/runtime/browse_history.rs
Normal file
220
server-rs/crates/spacetime-module/src/runtime/browse_history.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
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<String>,
|
||||
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<Vec<RuntimeBrowseHistorySnapshot>, 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()
|
||||
.iter()
|
||||
.filter(|row| row.user_id == validated_input.user_id)
|
||||
.map(|row| build_runtime_browse_history_snapshot_from_row(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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<Vec<RuntimeBrowseHistorySnapshot>, 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<Vec<RuntimeBrowseHistorySnapshot>, 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()
|
||||
.iter()
|
||||
.filter(|row| row.user_id == validated_input.user_id)
|
||||
.map(|row| row.browse_history_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
9
server-rs/crates/spacetime-module/src/runtime/mod.rs
Normal file
9
server-rs/crates/spacetime-module/src/runtime/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod browse_history;
|
||||
mod profile;
|
||||
mod settings;
|
||||
mod snapshots;
|
||||
|
||||
pub use browse_history::*;
|
||||
pub use profile::*;
|
||||
pub use settings::*;
|
||||
pub use snapshots::*;
|
||||
1475
server-rs/crates/spacetime-module/src/runtime/profile.rs
Normal file
1475
server-rs/crates/spacetime-module/src/runtime/profile.rs
Normal file
File diff suppressed because it is too large
Load Diff
141
server-rs/crates/spacetime-module/src/runtime/settings.rs
Normal file
141
server-rs/crates/spacetime-module/src/runtime/settings.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use crate::*;
|
||||
|
||||
#[spacetimedb::table(accessor = runtime_setting)]
|
||||
pub struct RuntimeSetting {
|
||||
#[primary_key]
|
||||
pub(crate) user_id: String,
|
||||
pub(crate) music_volume: f32,
|
||||
pub(crate) platform_theme: RuntimePlatformTheme,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
// procedure 面向 Axum 同步读取设置;若没有持久化记录则返回默认值快照,但不产生额外写入。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_runtime_setting_or_default(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeSettingGetInput,
|
||||
) -> RuntimeSettingProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_runtime_setting_snapshot(tx, input.clone())) {
|
||||
Ok(record) => RuntimeSettingProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeSettingProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// procedure 面向 Axum 同步写入设置,并返回最终归一化后的持久化结果。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn upsert_runtime_setting_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeSettingUpsertInput,
|
||||
) -> RuntimeSettingProcedureResult {
|
||||
match ctx.try_with_tx(|tx| upsert_runtime_setting(tx, input.clone())) {
|
||||
Ok(record) => RuntimeSettingProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeSettingProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_runtime_setting_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeSettingGetInput,
|
||||
) -> Result<RuntimeSettingSnapshot, String> {
|
||||
let validated_input =
|
||||
build_runtime_setting_get_input(input.user_id).map_err(|error| error.to_string())?;
|
||||
|
||||
if let Some(existing) = ctx
|
||||
.db
|
||||
.runtime_setting()
|
||||
.user_id()
|
||||
.find(&validated_input.user_id)
|
||||
{
|
||||
return Ok(RuntimeSettingSnapshot {
|
||||
user_id: existing.user_id,
|
||||
music_volume: existing.music_volume,
|
||||
platform_theme: existing.platform_theme,
|
||||
created_at_micros: existing.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: existing.updated_at.to_micros_since_unix_epoch(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(RuntimeSettingSnapshot {
|
||||
user_id: validated_input.user_id,
|
||||
music_volume: DEFAULT_MUSIC_VOLUME,
|
||||
platform_theme: DEFAULT_PLATFORM_THEME,
|
||||
created_at_micros: 0,
|
||||
updated_at_micros: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn upsert_runtime_setting(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeSettingUpsertInput,
|
||||
) -> Result<RuntimeSettingSnapshot, String> {
|
||||
let validated_input = build_runtime_setting_upsert_input(
|
||||
input.user_id,
|
||||
input.music_volume,
|
||||
input.platform_theme,
|
||||
input.updated_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
|
||||
|
||||
let snapshot = match ctx
|
||||
.db
|
||||
.runtime_setting()
|
||||
.user_id()
|
||||
.find(&validated_input.user_id)
|
||||
{
|
||||
Some(existing) => {
|
||||
ctx.db.runtime_setting().user_id().delete(&existing.user_id);
|
||||
ctx.db.runtime_setting().insert(RuntimeSetting {
|
||||
user_id: existing.user_id.clone(),
|
||||
music_volume: validated_input.music_volume,
|
||||
platform_theme: validated_input.platform_theme,
|
||||
created_at: existing.created_at,
|
||||
updated_at,
|
||||
});
|
||||
|
||||
RuntimeSettingSnapshot {
|
||||
user_id: existing.user_id,
|
||||
music_volume: validated_input.music_volume,
|
||||
platform_theme: validated_input.platform_theme,
|
||||
created_at_micros: existing.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: validated_input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ctx.db.runtime_setting().insert(RuntimeSetting {
|
||||
user_id: validated_input.user_id.clone(),
|
||||
music_volume: validated_input.music_volume,
|
||||
platform_theme: validated_input.platform_theme,
|
||||
created_at: updated_at,
|
||||
updated_at,
|
||||
});
|
||||
|
||||
RuntimeSettingSnapshot {
|
||||
user_id: validated_input.user_id,
|
||||
music_volume: validated_input.music_volume,
|
||||
platform_theme: validated_input.platform_theme,
|
||||
created_at_micros: validated_input.updated_at_micros,
|
||||
updated_at_micros: validated_input.updated_at_micros,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(snapshot)
|
||||
}
|
||||
216
server-rs/crates/spacetime-module/src/runtime/snapshots.rs
Normal file
216
server-rs/crates/spacetime-module/src/runtime/snapshots.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use crate::*;
|
||||
|
||||
#[spacetimedb::table(accessor = runtime_snapshot)]
|
||||
pub struct RuntimeSnapshotRow {
|
||||
#[primary_key]
|
||||
pub(crate) user_id: String,
|
||||
pub(crate) version: u32,
|
||||
pub(crate) saved_at: Timestamp,
|
||||
pub(crate) bottom_tab: String,
|
||||
pub(crate) game_state_json: String,
|
||||
pub(crate) current_story_json: Option<String>,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
// 当前快照读取保持旧 Node 语义:无快照时返回 ok=true + record=None,而不是默认空对象。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_runtime_snapshot(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeSnapshotGetInput,
|
||||
) -> RuntimeSnapshotProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_runtime_snapshot_record(tx, input.clone())) {
|
||||
Ok(record) => RuntimeSnapshotProcedureResult {
|
||||
ok: true,
|
||||
record,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeSnapshotProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// PUT snapshot 主链会同步刷新 dashboard / wallet / played_world / save_archive 四类 projection。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn upsert_runtime_snapshot_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeSnapshotUpsertInput,
|
||||
) -> RuntimeSnapshotProcedureResult {
|
||||
match ctx.try_with_tx(|tx| upsert_runtime_snapshot_record(tx, input.clone())) {
|
||||
Ok(record) => RuntimeSnapshotProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeSnapshotProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 删除当前快照只影响 runtime_snapshot 主表,不联动清理 profile projection。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn delete_runtime_snapshot_and_return(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeSnapshotDeleteInput,
|
||||
) -> RuntimeSnapshotProcedureResult {
|
||||
match ctx.try_with_tx(|tx| delete_runtime_snapshot_record(tx, input.clone())) {
|
||||
Ok(record) => RuntimeSnapshotProcedureResult {
|
||||
ok: true,
|
||||
record,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeSnapshotProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_runtime_snapshot_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeSnapshotGetInput,
|
||||
) -> Result<Option<RuntimeSnapshot>, String> {
|
||||
let validated_input =
|
||||
build_runtime_snapshot_get_input(input.user_id).map_err(|error| error.to_string())?;
|
||||
|
||||
Ok(ctx
|
||||
.db
|
||||
.runtime_snapshot()
|
||||
.user_id()
|
||||
.find(&validated_input.user_id)
|
||||
.map(|row| build_runtime_snapshot_from_row(&row)))
|
||||
}
|
||||
|
||||
pub(crate) fn upsert_runtime_snapshot_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeSnapshotUpsertInput,
|
||||
) -> Result<RuntimeSnapshot, String> {
|
||||
let current_story_value = parse_optional_json_str(input.current_story_json.as_deref())?;
|
||||
let game_state = parse_json_str(&input.game_state_json)?;
|
||||
let prepared = build_runtime_snapshot_upsert_input(
|
||||
input.user_id,
|
||||
input.saved_at_micros,
|
||||
input.bottom_tab,
|
||||
game_state.clone(),
|
||||
current_story_value.clone(),
|
||||
input.updated_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(prepared.updated_at_micros);
|
||||
let saved_at = Timestamp::from_micros_since_unix_epoch(prepared.saved_at_micros);
|
||||
|
||||
let snapshot = match ctx.db.runtime_snapshot().user_id().find(&prepared.user_id) {
|
||||
Some(existing) => {
|
||||
ctx.db
|
||||
.runtime_snapshot()
|
||||
.user_id()
|
||||
.delete(&existing.user_id);
|
||||
ctx.db.runtime_snapshot().insert(RuntimeSnapshotRow {
|
||||
user_id: existing.user_id.clone(),
|
||||
version: SAVE_SNAPSHOT_VERSION,
|
||||
saved_at,
|
||||
bottom_tab: prepared.bottom_tab.clone(),
|
||||
game_state_json: prepared.game_state_json.clone(),
|
||||
current_story_json: prepared.current_story_json.clone(),
|
||||
created_at: existing.created_at,
|
||||
updated_at,
|
||||
});
|
||||
|
||||
RuntimeSnapshot {
|
||||
user_id: existing.user_id,
|
||||
version: SAVE_SNAPSHOT_VERSION,
|
||||
saved_at_micros: prepared.saved_at_micros,
|
||||
bottom_tab: prepared.bottom_tab,
|
||||
game_state_json: prepared.game_state_json,
|
||||
current_story_json: prepared.current_story_json,
|
||||
created_at_micros: existing.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: prepared.updated_at_micros,
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ctx.db.runtime_snapshot().insert(RuntimeSnapshotRow {
|
||||
user_id: prepared.user_id.clone(),
|
||||
version: SAVE_SNAPSHOT_VERSION,
|
||||
saved_at,
|
||||
bottom_tab: prepared.bottom_tab.clone(),
|
||||
game_state_json: prepared.game_state_json.clone(),
|
||||
current_story_json: prepared.current_story_json.clone(),
|
||||
created_at: updated_at,
|
||||
updated_at,
|
||||
});
|
||||
|
||||
RuntimeSnapshot {
|
||||
user_id: prepared.user_id,
|
||||
version: SAVE_SNAPSHOT_VERSION,
|
||||
saved_at_micros: prepared.saved_at_micros,
|
||||
bottom_tab: prepared.bottom_tab,
|
||||
game_state_json: prepared.game_state_json,
|
||||
current_story_json: prepared.current_story_json,
|
||||
created_at_micros: prepared.updated_at_micros,
|
||||
updated_at_micros: prepared.updated_at_micros,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sync_profile_projections_from_snapshot(ctx, &snapshot)?;
|
||||
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
pub(crate) fn delete_runtime_snapshot_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeSnapshotDeleteInput,
|
||||
) -> Result<Option<RuntimeSnapshot>, String> {
|
||||
let validated_input =
|
||||
build_runtime_snapshot_delete_input(input.user_id).map_err(|error| error.to_string())?;
|
||||
|
||||
let existing = ctx
|
||||
.db
|
||||
.runtime_snapshot()
|
||||
.user_id()
|
||||
.find(&validated_input.user_id);
|
||||
if let Some(existing) = existing {
|
||||
let snapshot = build_runtime_snapshot_from_row(&existing);
|
||||
ctx.db
|
||||
.runtime_snapshot()
|
||||
.user_id()
|
||||
.delete(&existing.user_id);
|
||||
return Ok(Some(snapshot));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn build_runtime_snapshot_from_row(row: &RuntimeSnapshotRow) -> RuntimeSnapshot {
|
||||
RuntimeSnapshot {
|
||||
user_id: row.user_id.clone(),
|
||||
version: row.version,
|
||||
saved_at_micros: row.saved_at.to_micros_since_unix_epoch(),
|
||||
bottom_tab: row.bottom_tab.clone(),
|
||||
game_state_json: row.game_state_json.clone(),
|
||||
current_story_json: row.current_story_json.clone(),
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_json_str(raw: &str) -> Result<JsonValue, String> {
|
||||
serde_json::from_str::<JsonValue>(raw)
|
||||
.map_err(|error| format!("game_state_json 解析失败: {error}"))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_optional_json_str(raw: Option<&str>) -> Result<Option<JsonValue>, String> {
|
||||
match raw.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
Some(value) => serde_json::from_str::<JsonValue>(value)
|
||||
.map(Some)
|
||||
.map_err(|error| format!("current_story_json 解析失败: {error}")),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user