Merge remote-tracking branch 'origin/master' into codex/ddd
# Conflicts: # docs/technical/README.md # docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md # docs/technical/SPACETIMEDB_TABLE_CATALOG.md # scripts/generate-spacetime-bindings.mjs # server-rs/crates/api-server/src/app.rs # server-rs/crates/api-server/src/assets.rs # server-rs/crates/api-server/src/big_fish.rs # server-rs/crates/api-server/src/custom_world_ai.rs # server-rs/crates/api-server/src/llm.rs # server-rs/crates/api-server/src/main.rs # server-rs/crates/api-server/src/puzzle.rs # server-rs/crates/api-server/src/runtime_profile.rs # server-rs/crates/api-server/src/runtime_story/compat/ai.rs # server-rs/crates/api-server/src/runtime_story/compat/npc_actions.rs # server-rs/crates/api-server/src/runtime_story/compat/presentation.rs # server-rs/crates/api-server/src/runtime_story/compat/tests.rs # server-rs/crates/api-server/src/state.rs # server-rs/crates/module-auth/src/lib.rs # server-rs/crates/module-big-fish/src/lib.rs # server-rs/crates/module-custom-world/src/lib.rs # server-rs/crates/module-puzzle/src/lib.rs # server-rs/crates/module-runtime/src/lib.rs # server-rs/crates/spacetime-client/src/big_fish.rs # server-rs/crates/spacetime-client/src/lib.rs # server-rs/crates/spacetime-client/src/mapper.rs # server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_redeem_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_redeem_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/append_ai_text_chunk_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/attach_ai_result_reference_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/authorize_database_migration_operator_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_type.rs # server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/cancel_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/clear_platform_browse_history_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/compile_custom_world_published_profile_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/compile_puzzle_agent_draft_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/complete_ai_stage_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/complete_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/confirm_asset_object_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/consume_profile_wallet_points_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/continue_story_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_battle_state_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_big_fish_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_custom_world_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_profile_recharge_order_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/create_puzzle_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_big_fish_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_puzzle_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/delete_runtime_snapshot_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/drag_puzzle_piece_or_group_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/execute_custom_world_agent_action_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/export_auth_store_snapshot_from_tables_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/export_database_migration_to_file_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/fail_ai_task_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/finalize_big_fish_agent_message_turn_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/finalize_custom_world_agent_message_turn_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/finalize_puzzle_agent_message_turn_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/generate_big_fish_asset_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_auth_store_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_battle_state_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_chapter_progression_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_card_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_operation_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_by_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_library_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_player_progression_or_default_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_dashboard_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_play_stats_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_recharge_center_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_profile_referral_invite_center_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_agent_session_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_run_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_runtime_inventory_state_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_runtime_setting_or_default_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_runtime_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/get_story_session_state_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/grant_player_progression_experience_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/import_auth_store_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_from_file_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_incremental_from_file_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_asset_history_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_big_fish_works_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_gallery_entries_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_profiles_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_works_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_platform_browse_history_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_profile_save_archives_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_profile_wallet_ledger_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_gallery_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_works_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/mod.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_big_fish_game_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_world_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/publish_puzzle_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/record_big_fish_play_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_referral_invite_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_reward_code_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/refund_profile_wallet_points_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_combat_action_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_battle_interaction_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_interaction_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_social_action_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resolve_treasure_interaction_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/resume_profile_save_archive_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/revoke_database_migration_operator_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/save_puzzle_generated_images_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/select_puzzle_cover_image_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/start_puzzle_run_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_message_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_custom_world_agent_message_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_agent_message_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_leaderboard_entry_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/swap_puzzle_pieces_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/unpublish_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/update_puzzle_work_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_auth_store_snapshot_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_chapter_progression_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_agent_operation_progress_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_profile_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_npc_state_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_platform_browse_history_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_setting_and_return_procedure.rs # server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_snapshot_and_return_procedure.rs # server-rs/crates/spacetime-module/src/auth/procedures.rs # server-rs/crates/spacetime-module/src/custom_world/mod.rs # server-rs/crates/spacetime-module/src/lib.rs # server-rs/crates/spacetime-module/src/migration.rs # server-rs/crates/spacetime-module/src/puzzle.rs # server-rs/crates/spacetime-module/src/runtime/profile.rs # src/components/platform-entry/PlatformEntryFlowShellImpl.tsx # src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx # src/services/aiService.ts # src/services/puzzle-runtime/puzzleRuntimeClient.ts
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
use crate::*;
|
||||
|
||||
const PUBLIC_WORK_PLAY_DAY_MICROS: i64 = 86_400_000_000;
|
||||
const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7;
|
||||
const PROFILE_REFERRAL_INVITED_USERS_LIMIT: usize = 20;
|
||||
const PROFILE_NEW_USER_REGISTRATION_LEDGER_PREFIX: &str = "new-user-registration";
|
||||
|
||||
#[spacetimedb::table(accessor = profile_dashboard_state)]
|
||||
pub struct ProfileDashboardState {
|
||||
#[primary_key]
|
||||
@@ -67,6 +72,7 @@ pub struct ProfileInviteCode {
|
||||
pub(crate) user_id: String,
|
||||
#[unique]
|
||||
pub(crate) invite_code: String,
|
||||
pub(crate) metadata_json: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
@@ -116,6 +122,42 @@ pub struct ProfilePlayedWorld {
|
||||
pub(crate) last_observed_play_time_ms: u64,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = public_work_play_daily_stat,
|
||||
index(
|
||||
accessor = by_public_work_play_daily_stat_work_day,
|
||||
btree(columns = [source_type, profile_id, played_day])
|
||||
)
|
||||
)]
|
||||
pub struct PublicWorkPlayDailyStat {
|
||||
#[primary_key]
|
||||
pub(crate) stat_id: String,
|
||||
// 中文注释:source_type 区分 custom-world / puzzle / big-fish,避免不同玩法 profile_id 撞桶。
|
||||
pub(crate) source_type: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
// 中文注释:UTC 自 Unix 纪元起的自然日桶,用于快速聚合近 7 日新增游玩次数。
|
||||
pub(crate) played_day: i64,
|
||||
pub(crate) play_count: u32,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = public_work_like,
|
||||
index(accessor = by_public_work_like_work, btree(columns = [source_type, profile_id])),
|
||||
index(accessor = by_public_work_like_user, btree(columns = [user_id]))
|
||||
)]
|
||||
pub struct PublicWorkLike {
|
||||
#[primary_key]
|
||||
pub(crate) like_id: String,
|
||||
// 中文注释:source_type 与 play 统计保持同一套作品类型命名,确保跨玩法 profile_id 不会互相冲突。
|
||||
pub(crate) source_type: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
pub(crate) user_id: String,
|
||||
pub(crate) liked_at: Timestamp,
|
||||
}
|
||||
|
||||
pub(crate) struct ProfilePlayedWorkUpsertInput {
|
||||
pub(crate) user_id: String,
|
||||
pub(crate) world_key: String,
|
||||
@@ -127,6 +169,37 @@ pub(crate) struct ProfilePlayedWorkUpsertInput {
|
||||
pub(crate) played_at_micros: i64,
|
||||
}
|
||||
|
||||
pub(crate) struct PublicWorkPlayRecordInput {
|
||||
pub(crate) source_type: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
pub(crate) played_at_micros: i64,
|
||||
}
|
||||
|
||||
pub(crate) struct PublicWorkLikeRecordInput {
|
||||
pub(crate) source_type: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
pub(crate) user_id: String,
|
||||
pub(crate) liked_at_micros: i64,
|
||||
}
|
||||
|
||||
pub(crate) struct ProfileSaveArchiveUpsertInput {
|
||||
pub(crate) user_id: String,
|
||||
pub(crate) world_key: String,
|
||||
pub(crate) owner_user_id: Option<String>,
|
||||
pub(crate) profile_id: Option<String>,
|
||||
pub(crate) world_type: Option<String>,
|
||||
pub(crate) world_name: String,
|
||||
pub(crate) subtitle: String,
|
||||
pub(crate) summary_text: String,
|
||||
pub(crate) cover_image_src: Option<String>,
|
||||
pub(crate) bottom_tab: String,
|
||||
pub(crate) game_state_json: String,
|
||||
pub(crate) current_story_json: Option<String>,
|
||||
pub(crate) saved_at_micros: i64,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(accessor = profile_membership)]
|
||||
pub struct ProfileMembership {
|
||||
#[primary_key]
|
||||
@@ -282,6 +355,26 @@ pub fn list_profile_wallet_ledger(
|
||||
}
|
||||
}
|
||||
|
||||
// 新用户注册赠送由后端注册链路调用;流水 ID 固定,保证重试不重复发放。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn grant_new_user_registration_wallet_reward(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeProfileDashboardGetInput,
|
||||
) -> RuntimeProfileWalletAdjustmentProcedureResult {
|
||||
match ctx.try_with_tx(|tx| grant_new_user_registration_wallet_reward_tx(tx, input.clone())) {
|
||||
Ok(record) => RuntimeProfileWalletAdjustmentProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileWalletAdjustmentProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 资产生成由 Axum 调用外部模型,钱包扣费必须先在 SpacetimeDB 内原子落账。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn consume_profile_wallet_points_and_return(
|
||||
@@ -420,7 +513,7 @@ pub fn get_profile_referral_invite_center(
|
||||
}
|
||||
}
|
||||
|
||||
// 填码绑定、每日邀请者奖励上限和双方叙世币发放都在同一事务内完成。
|
||||
// 填码绑定、每日邀请者奖励上限和双方光点发放都在同一事务内完成。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn redeem_profile_referral_invite_code(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -498,6 +591,25 @@ pub fn admin_disable_profile_redeem_code(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn admin_upsert_profile_invite_code(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeProfileInviteCodeAdminUpsertInput,
|
||||
) -> RuntimeProfileInviteCodeAdminProcedureResult {
|
||||
match ctx.try_with_tx(|tx| admin_upsert_profile_invite_code_record(tx, input.clone())) {
|
||||
Ok(record) => RuntimeProfileInviteCodeAdminProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileInviteCodeAdminProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn list_profile_save_archive_rows(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileSaveArchiveListInput,
|
||||
@@ -705,6 +817,172 @@ pub(crate) fn add_profile_observed_play_time(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn upsert_profile_save_archive(
|
||||
ctx: &ReducerContext,
|
||||
input: ProfileSaveArchiveUpsertInput,
|
||||
) -> Result<(), String> {
|
||||
let user_id = input.user_id.trim();
|
||||
let world_key = input.world_key.trim();
|
||||
if user_id.is_empty() || world_key.is_empty() {
|
||||
return Err("profile_save_archive 参数不能为空".to_string());
|
||||
}
|
||||
|
||||
let saved_at = Timestamp::from_micros_since_unix_epoch(input.saved_at_micros);
|
||||
let archive_id = format!("{user_id}:{world_key}");
|
||||
let existing = ctx.db.profile_save_archive().archive_id().find(&archive_id);
|
||||
let created_at = existing
|
||||
.as_ref()
|
||||
.map(|row| row.created_at)
|
||||
.unwrap_or(saved_at);
|
||||
|
||||
if let Some(existing) = existing {
|
||||
ctx.db
|
||||
.profile_save_archive()
|
||||
.archive_id()
|
||||
.delete(&existing.archive_id);
|
||||
}
|
||||
|
||||
ctx.db.profile_save_archive().insert(ProfileSaveArchive {
|
||||
archive_id,
|
||||
user_id: user_id.to_string(),
|
||||
world_key: world_key.to_string(),
|
||||
owner_user_id: input.owner_user_id,
|
||||
profile_id: input.profile_id,
|
||||
world_type: input.world_type,
|
||||
world_name: input.world_name,
|
||||
subtitle: input.subtitle,
|
||||
summary_text: input.summary_text,
|
||||
cover_image_src: input.cover_image_src,
|
||||
saved_at,
|
||||
bottom_tab: input.bottom_tab,
|
||||
game_state_json: input.game_state_json,
|
||||
current_story_json: input.current_story_json,
|
||||
created_at,
|
||||
updated_at: saved_at,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn record_public_work_play(
|
||||
ctx: &ReducerContext,
|
||||
input: PublicWorkPlayRecordInput,
|
||||
) -> Result<(), String> {
|
||||
let source_type = input.source_type.trim();
|
||||
let owner_user_id = input.owner_user_id.trim();
|
||||
let profile_id = input.profile_id.trim();
|
||||
if source_type.is_empty() || owner_user_id.is_empty() || profile_id.is_empty() {
|
||||
return Err("public_work_play_daily_stat 参数不能为空".to_string());
|
||||
}
|
||||
|
||||
let played_day = public_work_play_day_from_micros(input.played_at_micros);
|
||||
let stat_id = build_public_work_play_daily_stat_id(source_type, profile_id, played_day);
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros);
|
||||
let next_count = ctx
|
||||
.db
|
||||
.public_work_play_daily_stat()
|
||||
.stat_id()
|
||||
.find(&stat_id)
|
||||
.map(|existing| {
|
||||
ctx.db
|
||||
.public_work_play_daily_stat()
|
||||
.stat_id()
|
||||
.delete(&existing.stat_id);
|
||||
existing.play_count.saturating_add(1)
|
||||
})
|
||||
.unwrap_or(1);
|
||||
|
||||
ctx.db
|
||||
.public_work_play_daily_stat()
|
||||
.insert(PublicWorkPlayDailyStat {
|
||||
stat_id,
|
||||
source_type: source_type.to_string(),
|
||||
owner_user_id: owner_user_id.to_string(),
|
||||
profile_id: profile_id.to_string(),
|
||||
played_day,
|
||||
play_count: next_count,
|
||||
updated_at,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn record_public_work_like(
|
||||
ctx: &ReducerContext,
|
||||
input: PublicWorkLikeRecordInput,
|
||||
) -> Result<bool, String> {
|
||||
let source_type = input.source_type.trim();
|
||||
let owner_user_id = input.owner_user_id.trim();
|
||||
let profile_id = input.profile_id.trim();
|
||||
let user_id = input.user_id.trim();
|
||||
if source_type.is_empty()
|
||||
|| owner_user_id.is_empty()
|
||||
|| profile_id.is_empty()
|
||||
|| user_id.is_empty()
|
||||
{
|
||||
return Err("public_work_like 参数不能为空".to_string());
|
||||
}
|
||||
|
||||
let like_id = build_public_work_like_id(source_type, profile_id, user_id);
|
||||
if ctx.db.public_work_like().like_id().find(&like_id).is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
ctx.db.public_work_like().insert(PublicWorkLike {
|
||||
like_id,
|
||||
source_type: source_type.to_string(),
|
||||
owner_user_id: owner_user_id.to_string(),
|
||||
profile_id: profile_id.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
liked_at: Timestamp::from_micros_since_unix_epoch(input.liked_at_micros),
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn count_recent_public_work_plays(
|
||||
ctx: &ReducerContext,
|
||||
source_type: &str,
|
||||
profile_id: &str,
|
||||
now_micros: i64,
|
||||
) -> u32 {
|
||||
let source_type = source_type.trim();
|
||||
let profile_id = profile_id.trim();
|
||||
if source_type.is_empty() || profile_id.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let current_day = public_work_play_day_from_micros(now_micros);
|
||||
let first_day = current_day.saturating_sub(PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1);
|
||||
|
||||
ctx.db
|
||||
.public_work_play_daily_stat()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.source_type == source_type
|
||||
&& row.profile_id == profile_id
|
||||
&& row.played_day >= first_day
|
||||
&& row.played_day <= current_day
|
||||
})
|
||||
.fold(0u32, |total, row| total.saturating_add(row.play_count))
|
||||
}
|
||||
|
||||
fn public_work_play_day_from_micros(value: i64) -> i64 {
|
||||
value.div_euclid(PUBLIC_WORK_PLAY_DAY_MICROS)
|
||||
}
|
||||
|
||||
fn build_public_work_play_daily_stat_id(
|
||||
source_type: &str,
|
||||
profile_id: &str,
|
||||
played_day: i64,
|
||||
) -> String {
|
||||
format!("{source_type}:{profile_id}:{played_day}")
|
||||
}
|
||||
|
||||
fn build_public_work_like_id(source_type: &str, profile_id: &str, user_id: &str) -> String {
|
||||
format!("{source_type}:{profile_id}:{user_id}")
|
||||
}
|
||||
|
||||
fn ensure_profile_dashboard_state(ctx: &ReducerContext, user_id: &str, updated_at: Timestamp) {
|
||||
if ctx
|
||||
.db
|
||||
@@ -785,12 +1063,20 @@ fn sync_profile_dashboard_from_snapshot(
|
||||
.as_ref()
|
||||
.map(|row| row.total_play_time_ms)
|
||||
.unwrap_or(0);
|
||||
let next_wallet_balance = module_runtime::read_runtime_json_non_negative_u64(
|
||||
game_state.and_then(|state| state.get("playerCurrency")),
|
||||
);
|
||||
let has_business_wallet_ledger = has_profile_business_wallet_ledger(ctx, &snapshot.user_id);
|
||||
let synced_wallet_balance = if has_business_wallet_ledger {
|
||||
None
|
||||
} else {
|
||||
game_state
|
||||
.and_then(|state| state.get("playerCurrency"))
|
||||
.map(|value| module_runtime::read_runtime_json_non_negative_u64(Some(value)))
|
||||
};
|
||||
let next_wallet_balance = synced_wallet_balance.unwrap_or(previous_wallet_balance);
|
||||
let mut next_total_play_time_ms = previous_total_play_time_ms;
|
||||
|
||||
if next_wallet_balance != previous_wallet_balance {
|
||||
if let Some(next_wallet_balance) = synced_wallet_balance
|
||||
&& next_wallet_balance != previous_wallet_balance
|
||||
{
|
||||
ctx.db.profile_wallet_ledger().insert(ProfileWalletLedger {
|
||||
wallet_ledger_id: build_runtime_profile_snapshot_wallet_ledger_id(
|
||||
&snapshot.user_id,
|
||||
@@ -965,6 +1251,174 @@ pub(crate) fn build_profile_save_archive_snapshot_from_row(
|
||||
}
|
||||
}
|
||||
|
||||
fn read_string_from_json(value: Option<&JsonValue>) -> Option<String> {
|
||||
value
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
fn resolve_profile_world_snapshot_meta(
|
||||
game_state: Option<&serde_json::Map<String, JsonValue>>,
|
||||
) -> Option<RuntimeProfileWorldSnapshotMeta> {
|
||||
let game_state = game_state?;
|
||||
let custom_world_profile = game_state
|
||||
.get("customWorldProfile")
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
if let Some(custom_world_profile) = custom_world_profile {
|
||||
let profile_id = read_string_from_json(custom_world_profile.get("id"));
|
||||
let world_title = read_string_from_json(custom_world_profile.get("name"))
|
||||
.or_else(|| read_string_from_json(custom_world_profile.get("title")));
|
||||
if profile_id.is_some() || world_title.is_some() {
|
||||
let world_title = world_title.unwrap_or_else(|| "自定义世界".to_string());
|
||||
return Some(RuntimeProfileWorldSnapshotMeta {
|
||||
world_key: profile_id
|
||||
.as_ref()
|
||||
.map(|profile_id| format!("custom:{profile_id}"))
|
||||
.unwrap_or_else(|| format!("custom:{world_title}")),
|
||||
owner_user_id: None,
|
||||
profile_id,
|
||||
world_type: Some("CUSTOM".to_string()),
|
||||
world_title,
|
||||
world_subtitle: read_string_from_json(custom_world_profile.get("summary"))
|
||||
.or_else(|| read_string_from_json(custom_world_profile.get("settingText")))
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let world_type = read_string_from_json(game_state.get("worldType"))?;
|
||||
let current_scene_preset = game_state
|
||||
.get("currentScenePreset")
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
Some(RuntimeProfileWorldSnapshotMeta {
|
||||
world_key: format!("builtin:{world_type}"),
|
||||
owner_user_id: None,
|
||||
profile_id: None,
|
||||
world_type: Some(world_type.clone()),
|
||||
world_title: current_scene_preset
|
||||
.and_then(|preset| read_string_from_json(preset.get("name")))
|
||||
.unwrap_or_else(|| build_builtin_world_title(&world_type)),
|
||||
world_subtitle: current_scene_preset
|
||||
.and_then(|preset| {
|
||||
read_string_from_json(preset.get("summary"))
|
||||
.or_else(|| read_string_from_json(preset.get("description")))
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_profile_save_archive_meta(
|
||||
game_state: &JsonValue,
|
||||
current_story_json: Option<&str>,
|
||||
) -> Option<RuntimeProfileSaveArchiveMeta> {
|
||||
if is_non_persistent_runtime_snapshot(game_state) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let game_state_object = game_state.as_object();
|
||||
let world_meta = resolve_profile_world_snapshot_meta(game_state_object)?;
|
||||
let story_engine_memory = game_state_object
|
||||
.and_then(|state| state.get("storyEngineMemory"))
|
||||
.and_then(JsonValue::as_object);
|
||||
let continue_game_digest = story_engine_memory
|
||||
.and_then(|memory| read_string_from_json(memory.get("continueGameDigest")));
|
||||
let current_story_text = parse_optional_json_str(current_story_json)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|story| story.as_object().cloned())
|
||||
.and_then(|story| read_string_from_json(story.get("text")));
|
||||
let custom_world_profile = game_state_object
|
||||
.and_then(|state| state.get("customWorldProfile"))
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
if let Some(custom_world_profile) = custom_world_profile {
|
||||
let world_name = read_string_from_json(custom_world_profile.get("name"))
|
||||
.or_else(|| read_string_from_json(custom_world_profile.get("title")))
|
||||
.unwrap_or_else(|| world_meta.world_title.clone());
|
||||
let subtitle = read_string_from_json(custom_world_profile.get("summary"))
|
||||
.or_else(|| read_string_from_json(custom_world_profile.get("settingText")))
|
||||
.unwrap_or_else(|| world_meta.world_subtitle.clone());
|
||||
let summary_text = continue_game_digest
|
||||
.or(current_story_text)
|
||||
.or_else(|| {
|
||||
if subtitle.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(subtitle.clone())
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT.to_string());
|
||||
|
||||
return Some(RuntimeProfileSaveArchiveMeta {
|
||||
world_key: world_meta.world_key,
|
||||
owner_user_id: world_meta.owner_user_id,
|
||||
profile_id: world_meta.profile_id,
|
||||
world_type: world_meta.world_type,
|
||||
world_name,
|
||||
subtitle,
|
||||
summary_text,
|
||||
cover_image_src: read_string_from_json(custom_world_profile.get("coverImageSrc")),
|
||||
});
|
||||
}
|
||||
|
||||
let summary_text = continue_game_digest
|
||||
.or(current_story_text)
|
||||
.or_else(|| {
|
||||
if world_meta.world_subtitle.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(world_meta.world_subtitle.clone())
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT.to_string());
|
||||
let current_scene_preset = game_state_object
|
||||
.and_then(|state| state.get("currentScenePreset"))
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
Some(RuntimeProfileSaveArchiveMeta {
|
||||
world_key: world_meta.world_key,
|
||||
owner_user_id: world_meta.owner_user_id,
|
||||
profile_id: world_meta.profile_id,
|
||||
world_type: world_meta.world_type,
|
||||
world_name: world_meta.world_title,
|
||||
subtitle: world_meta.world_subtitle.clone(),
|
||||
summary_text,
|
||||
cover_image_src: current_scene_preset
|
||||
.and_then(|preset| read_string_from_json(preset.get("imageSrc"))),
|
||||
})
|
||||
}
|
||||
|
||||
fn is_non_persistent_runtime_snapshot(game_state: &JsonValue) -> bool {
|
||||
let Some(game_state) = game_state.as_object() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if game_state
|
||||
.get("runtimePersistenceDisabled")
|
||||
.and_then(JsonValue::as_bool)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
matches!(
|
||||
read_string_from_json(game_state.get("runtimeMode")).as_deref(),
|
||||
Some("preview") | Some("test")
|
||||
)
|
||||
}
|
||||
|
||||
fn build_builtin_world_title(world_type: &str) -> String {
|
||||
match world_type {
|
||||
"WUXIA" => "武侠世界".to_string(),
|
||||
"XIANXIA" => "仙侠世界".to_string(),
|
||||
_ => "叙事世界".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_profile_dashboard_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileDashboardGetInput,
|
||||
@@ -1215,10 +1669,14 @@ fn redeem_profile_referral_invite_code_record(
|
||||
),
|
||||
bound_at,
|
||||
)?;
|
||||
let today_inviter_reward_count =
|
||||
count_today_profile_referral_inviter_rewards(ctx, &inviter_code.user_id, bound_at);
|
||||
let inviter_reward_granted =
|
||||
should_grant_runtime_profile_inviter_reward(today_inviter_reward_count);
|
||||
let is_admin_invite_code = is_admin_profile_invite_code_user_id(&inviter_code.user_id);
|
||||
let today_inviter_reward_count = if is_admin_invite_code {
|
||||
0
|
||||
} else {
|
||||
count_today_profile_referral_inviter_rewards(ctx, &inviter_code.user_id, bound_at)
|
||||
};
|
||||
let inviter_reward_granted = !is_admin_invite_code
|
||||
&& module_runtime::should_grant_runtime_profile_inviter_reward(today_inviter_reward_count);
|
||||
let inviter_balance_after = if inviter_reward_granted {
|
||||
apply_profile_wallet_delta(
|
||||
ctx,
|
||||
@@ -1410,6 +1868,56 @@ fn admin_disable_profile_redeem_code_record(
|
||||
Ok(build_profile_redeem_code_snapshot_from_row(&inserted))
|
||||
}
|
||||
|
||||
fn admin_upsert_profile_invite_code_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileInviteCodeAdminUpsertInput,
|
||||
) -> Result<RuntimeProfileInviteCodeSnapshot, String> {
|
||||
let validated_input = build_runtime_profile_invite_code_admin_upsert_input(
|
||||
input.admin_user_id,
|
||||
input.invite_code,
|
||||
input.metadata_json,
|
||||
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 user_id = build_admin_profile_invite_code_user_id(
|
||||
&validated_input.admin_user_id,
|
||||
&validated_input.invite_code,
|
||||
);
|
||||
|
||||
if let Some(existing) = ctx
|
||||
.db
|
||||
.profile_invite_code()
|
||||
.invite_code()
|
||||
.find(&validated_input.invite_code)
|
||||
{
|
||||
if existing.user_id != user_id {
|
||||
return Err("邀请码已被其他用户占用".to_string());
|
||||
}
|
||||
ctx.db
|
||||
.profile_invite_code()
|
||||
.user_id()
|
||||
.delete(&existing.user_id);
|
||||
let inserted = ctx.db.profile_invite_code().insert(ProfileInviteCode {
|
||||
user_id,
|
||||
invite_code: validated_input.invite_code,
|
||||
metadata_json: validated_input.metadata_json,
|
||||
created_at: existing.created_at,
|
||||
updated_at,
|
||||
});
|
||||
return Ok(build_profile_invite_code_snapshot_from_row(&inserted));
|
||||
}
|
||||
|
||||
let inserted = ctx.db.profile_invite_code().insert(ProfileInviteCode {
|
||||
user_id,
|
||||
invite_code: validated_input.invite_code,
|
||||
metadata_json: validated_input.metadata_json,
|
||||
created_at: updated_at,
|
||||
updated_at,
|
||||
});
|
||||
Ok(build_profile_invite_code_snapshot_from_row(&inserted))
|
||||
}
|
||||
|
||||
fn build_profile_referral_invite_center_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
@@ -1445,6 +1953,7 @@ fn build_profile_referral_invite_center_snapshot(
|
||||
today_inviter_reward_remaining: PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT
|
||||
.saturating_sub(today_inviter_reward_count),
|
||||
reward_points: PROFILE_REFERRAL_REWARD_POINTS,
|
||||
invited_users: list_profile_referral_invited_users(ctx, user_id),
|
||||
has_redeemed_code: bound_relation.is_some(),
|
||||
bound_inviter_user_id: bound_relation
|
||||
.as_ref()
|
||||
@@ -1456,6 +1965,50 @@ fn build_profile_referral_invite_center_snapshot(
|
||||
}
|
||||
}
|
||||
|
||||
fn list_profile_referral_invited_users(
|
||||
ctx: &ReducerContext,
|
||||
inviter_user_id: &str,
|
||||
) -> Vec<RuntimeReferralInvitedUserSnapshot> {
|
||||
// 中文注释:邀请面板只展示最近成功邀请用户,完整统计仍由计数字段承担。
|
||||
let inviter_user_id = inviter_user_id.to_string();
|
||||
let mut relations = ctx
|
||||
.db
|
||||
.profile_referral_relation()
|
||||
.by_profile_referral_inviter_user_id()
|
||||
.filter(&inviter_user_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
relations.sort_by(|left, right| {
|
||||
right
|
||||
.bound_at
|
||||
.to_micros_since_unix_epoch()
|
||||
.cmp(&left.bound_at.to_micros_since_unix_epoch())
|
||||
});
|
||||
|
||||
relations
|
||||
.into_iter()
|
||||
.take(PROFILE_REFERRAL_INVITED_USERS_LIMIT)
|
||||
.map(|relation| {
|
||||
let account = ctx
|
||||
.db
|
||||
.user_account()
|
||||
.user_id()
|
||||
.find(&relation.invitee_user_id);
|
||||
RuntimeReferralInvitedUserSnapshot {
|
||||
user_id: relation.invitee_user_id,
|
||||
display_name: account
|
||||
.as_ref()
|
||||
.map(|user| user.display_name.trim())
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or("玩家")
|
||||
.to_string(),
|
||||
avatar_url: account.and_then(|user| user.avatar_url),
|
||||
bound_at_micros: relation.bound_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInviteCode {
|
||||
if let Some(row) = ctx
|
||||
.db
|
||||
@@ -1482,6 +2035,7 @@ fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInv
|
||||
ctx.db.profile_invite_code().insert(ProfileInviteCode {
|
||||
user_id: user_id.to_string(),
|
||||
invite_code,
|
||||
metadata_json: PROFILE_INVITE_CODE_METADATA_DEFAULT_JSON.to_string(),
|
||||
created_at: ctx.timestamp,
|
||||
updated_at: ctx.timestamp,
|
||||
})
|
||||
@@ -1504,6 +2058,14 @@ fn count_today_profile_referral_inviter_rewards(
|
||||
.count() as u32
|
||||
}
|
||||
|
||||
fn is_admin_profile_invite_code_user_id(user_id: &str) -> bool {
|
||||
user_id.starts_with("admin:")
|
||||
}
|
||||
|
||||
fn build_admin_profile_invite_code_user_id(admin_user_id: &str, invite_code: &str) -> String {
|
||||
format!("admin:{}:{}", admin_user_id, invite_code)
|
||||
}
|
||||
|
||||
fn profile_wallet_balance(ctx: &ReducerContext, user_id: &str) -> u64 {
|
||||
ctx.db
|
||||
.profile_dashboard_state()
|
||||
@@ -1513,6 +2075,42 @@ fn profile_wallet_balance(ctx: &ReducerContext, user_id: &str) -> u64 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn build_new_user_registration_wallet_ledger_id(user_id: &str) -> String {
|
||||
format!("{PROFILE_NEW_USER_REGISTRATION_LEDGER_PREFIX}:{user_id}")
|
||||
}
|
||||
|
||||
fn grant_new_user_registration_wallet_reward_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileDashboardGetInput,
|
||||
) -> Result<RuntimeProfileDashboardSnapshot, String> {
|
||||
let validated_input = build_runtime_profile_dashboard_get_input(input.user_id)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let ledger_id = build_new_user_registration_wallet_ledger_id(&validated_input.user_id);
|
||||
if ctx
|
||||
.db
|
||||
.profile_wallet_ledger()
|
||||
.wallet_ledger_id()
|
||||
.find(&ledger_id)
|
||||
.is_none()
|
||||
{
|
||||
apply_profile_wallet_delta(
|
||||
ctx,
|
||||
&validated_input.user_id,
|
||||
PROFILE_NEW_USER_INITIAL_WALLET_POINTS,
|
||||
RuntimeProfileWalletLedgerSourceType::NewUserRegistrationReward,
|
||||
&ledger_id,
|
||||
ctx.timestamp,
|
||||
)?;
|
||||
}
|
||||
|
||||
get_profile_dashboard_snapshot(
|
||||
ctx,
|
||||
RuntimeProfileDashboardGetInput {
|
||||
user_id: validated_input.user_id,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn build_profile_recharge_center_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
@@ -1645,6 +2243,24 @@ fn apply_profile_wallet_delta(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn grant_profile_wallet_points(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
amount_delta: u64,
|
||||
source_type: RuntimeProfileWalletLedgerSourceType,
|
||||
ledger_id: &str,
|
||||
created_at: Timestamp,
|
||||
) -> Result<u64, String> {
|
||||
apply_profile_wallet_delta(
|
||||
ctx,
|
||||
user_id,
|
||||
amount_delta,
|
||||
source_type,
|
||||
ledger_id,
|
||||
created_at,
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_profile_wallet_adjustment(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileWalletAdjustmentInput,
|
||||
@@ -1762,6 +2378,13 @@ fn has_profile_points_recharged(ctx: &ReducerContext, user_id: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn has_profile_business_wallet_ledger(ctx: &ReducerContext, user_id: &str) -> bool {
|
||||
ctx.db.profile_wallet_ledger().iter().any(|row| {
|
||||
row.user_id == user_id
|
||||
&& row.source_type != RuntimeProfileWalletLedgerSourceType::SnapshotSync
|
||||
})
|
||||
}
|
||||
|
||||
fn latest_profile_recharge_order(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
@@ -1837,6 +2460,18 @@ fn build_profile_redeem_code_snapshot_from_row(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_invite_code_snapshot_from_row(
|
||||
row: &ProfileInviteCode,
|
||||
) -> RuntimeProfileInviteCodeSnapshot {
|
||||
RuntimeProfileInviteCodeSnapshot {
|
||||
user_id: row.user_id.clone(),
|
||||
invite_code: row.invite_code.clone(),
|
||||
metadata_json: row.metadata_json.clone(),
|
||||
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_wallet_ledger_snapshot_from_row(
|
||||
row: &ProfileWalletLedger,
|
||||
) -> RuntimeProfileWalletLedgerEntrySnapshot {
|
||||
|
||||
Reference in New Issue
Block a user