From d2a059d57a9c9b8936c0ed19604a5220268c80cc Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 20 Apr 2026 03:28:03 +0000 Subject: [PATCH] =?UTF-8?q?=E8=B4=A6=E6=88=B7=E7=B3=BB=E7=BB=9F=E5=AE=8C?= =?UTF-8?q?=E5=96=84=EF=BC=8C=E4=BD=BF=E7=94=A8uuid+=E5=A4=9Aidentity?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...TIMEDB_SERVER_NODE_MIGRATION_2026-04-18.md | 32 +- packages/shared/src/contracts/runtime.ts | 6 +- .../src/modules/story/storyActionService.ts | 3 +- spacetimedb/src/auth.rs | 636 ++++++++++++++---- spacetimedb/src/common.rs | 33 +- spacetimedb/src/lib.rs | 11 +- spacetimedb/src/runtime.rs | 210 +++--- spacetimedb/src/types.rs | 56 +- .../game-shell/PlatformHomeView.tsx | 10 +- ...meSelectionFlow.agent.interaction.test.tsx | 6 +- .../game-shell/PreGameSelectionFlow.tsx | 10 +- src/services/authService.test.ts | 4 +- src/services/authService.ts | 2 +- src/services/platformBrowseHistory.ts | 10 +- src/services/storageService.test.ts | 8 +- src/services/storageService.ts | 10 +- src/spacetime/generated/index.ts | 16 +- ..._table.ts => my_account_sessions_table.ts} | 0 .../generated/my_auth_state_table.ts | 2 +- .../generated/my_browse_history_table.ts | 2 +- .../my_custom_world_profiles_table.ts | 4 +- .../my_profile_played_worlds_table.ts | 2 +- .../published_custom_world_gallery_table.ts | 4 +- .../published_custom_world_profiles_table.ts | 4 +- src/spacetime/generated/types.ts | 170 ++--- src/spacetime/mappers.ts | 12 +- 26 files changed, 832 insertions(+), 431 deletions(-) rename src/spacetime/generated/{my_user_sessions_table.ts => my_account_sessions_table.ts} (100%) diff --git a/docs/technical/SPACETIMEDB_SERVER_NODE_MIGRATION_2026-04-18.md b/docs/technical/SPACETIMEDB_SERVER_NODE_MIGRATION_2026-04-18.md index 39e8851b..19428610 100644 --- a/docs/technical/SPACETIMEDB_SERVER_NODE_MIGRATION_2026-04-18.md +++ b/docs/technical/SPACETIMEDB_SERVER_NODE_MIGRATION_2026-04-18.md @@ -36,6 +36,32 @@ - 有 JWT 时使用 SpacetimeDB 自带 `sender_auth().jwt()` 建档,`login_provider` 标记为 `jwt` - 用户主键按 `user_` 生成,避免再依赖原先 Node 自签 access token / refresh token 流程 +### 2.2 内部账户模型状态 + +当前 STDB 私有实现已经开始显式转向账户语义: + +- 私有 Rust struct 已经切到 `Account / AccountIdentity / AccountSession` +- `auth.rs / runtime.rs / lib.rs` 内部 helper 和生命周期接线也开始改用 `account` 语义命名 +- 公开 view / procedure 名称暂时保持不变,但当前 schema 字段已经切到 `account_id / owner_account_id` +- 前端 TypeScript bindings 已经重新生成并同步适配了 `account_id` 语义 + +### 2.1 当前统一账户策略 + +当前已经开始按“统一账户,多设备会话”方向调整: + +- 新建账户时,账户主键已经不再直接使用连接 identity,而是生成独立 `acct_*` 账户 id +- 当前设备在短信验证时,如果手机号已命中已有用户,不再直接报“手机号已绑定其他账号” +- 当前连接的 identity / session 会被归并到这个已有手机号用户 +- 当前游客账户下的快照、设置、自定义世界、游玩统计、浏览历史等运行时数据也会一起迁移到目标账户 +- 这样同一个手机号可以在多个设备上同时建立会话,并归到同一个用户主体下 + +当前限制: + +- 归并的是“当前连接身份”和“当前会话” +- 当前的账户数据迁移是规则式合并,不是全量业务语义级合并 +- 例如看板/游玩统计用了保守合并策略,自定义世界同名冲突按更新时间取新 +- 也就是说,统一账户主语义已经开始生效,但后续仍值得补一轮更细的并档策略 + ### 3. 短信验证门禁 当前行为已经按你的要求落地: @@ -62,7 +88,7 @@ - `client_app_config` - `my_auth_state` - `my_auth_audit_logs` -- `my_user_sessions` +- `my_account_sessions` - `my_auth_risk_blocks` - `my_snapshot` - `my_runtime_settings` @@ -85,7 +111,7 @@ 为了承接客户端账户弹窗,本轮补了: -- `my_user_sessions` +- `my_account_sessions` - 用于读取当前账号关联的会话列表 - `my_auth_risk_blocks` - 用于读取当前账号手机号/IP 对应的保护记录 @@ -153,7 +179,7 @@ spacetime sql genarrative-local "$(tr '\n' ' ' < scripts/spacetime/init_local_ap - `src/services/storageService.ts` - 已从 `/api/runtime/*` 的存档/设置/资料库接口切到 Spacetime。 - `src/services/authService.ts` - - 现在也会读取 `my_user_sessions` / `my_auth_risk_blocks`,并调用 `lift_my_risk_block`。 + - 现在也会读取 `my_account_sessions` / `my_auth_risk_blocks`,并调用 `lift_my_risk_block`。 - `src/components/auth/AuthGate.tsx` - 已改成默认游客建连,并监听 `verification_prompt_event` / `kick_event`。 - `src/components/auth/PhoneVerificationModal.tsx` diff --git a/packages/shared/src/contracts/runtime.ts b/packages/shared/src/contracts/runtime.ts index 284964e5..ae0c9e01 100644 --- a/packages/shared/src/contracts/runtime.ts +++ b/packages/shared/src/contracts/runtime.ts @@ -57,7 +57,7 @@ export type ProfileWalletLedgerResponse = { export type ProfilePlayedWorkSummary = { worldKey: string; - ownerUserId: string | null; + ownerAccountId: string | null; profileId: string | null; worldType: string | null; worldTitle: string; @@ -87,7 +87,7 @@ export type CustomWorldProfileRecord = JsonObject & { }; export type CustomWorldLibraryEntry = { - ownerUserId: string; + ownerAccountId: string; profileId: string; profile: TProfile; visibility: CustomWorldPublicationStatus; @@ -130,7 +130,7 @@ export type CustomWorldGalleryDetailResponse< }; export type PlatformBrowseHistoryEntry = { - ownerUserId: string; + ownerAccountId: string; profileId: string; worldName: string; subtitle: string; diff --git a/server-node/src/modules/story/storyActionService.ts b/server-node/src/modules/story/storyActionService.ts index 1dff3ff5..a8f7c488 100644 --- a/server-node/src/modules/story/storyActionService.ts +++ b/server-node/src/modules/story/storyActionService.ts @@ -14,7 +14,8 @@ import { } from '../ai/chatPromptBuilders.js'; import { generateNextStoryFromOrchestrator } from '../ai/storyOrchestrator.js'; import { resolveCombatAction } from '../combat/combatResolutionService.js'; -import { isSupportedInventoryStoryFunctionId,resolveInventoryStoryAction } from '../inventory/inventoryStoryActionService.js'; +import { isSupportedInventoryStoryFunctionId,resolveInventoryStoryAction } from '../inventory/inventory +.js'; import { ensureNpcInventorySessionState, isSupportedNpcInventoryStoryFunctionId, diff --git a/spacetimedb/src/auth.rs b/spacetimedb/src/auth.rs index 313e5040..cafa63a8 100644 --- a/spacetimedb/src/auth.rs +++ b/spacetimedb/src/auth.rs @@ -2,20 +2,69 @@ use spacetimedb::{procedure, view, ProcedureContext, ReducerContext, Table, TxCo use crate::common::{ guest_identity_id, ip_key, jwt_exp_ms, jwt_identity_id, mask_mainland_phone_number, + new_account_id, normalize_client_type, normalize_mainland_china_phone_number, normalize_optional_string, phone_identity_id, remaining_seconds, request_meta_ip, request_meta_user_agent, session_id_for_identity_hex, timestamp_ms, - user_id_for_identity_hex, validate_sms_verify_code, MAX_AUTH_AUDIT_LOGS, + validate_sms_verify_code, MAX_AUTH_AUDIT_LOGS, }; use crate::config::{default_app_config, ensure_app_config_row, load_app_config_read_only}; use crate::types::*; -pub(crate) struct UserProvisioning { - pub user: User, +pub(crate) struct AccountProvisioning { + pub account: Account, pub config: AppConfig, pub existed: bool, } +fn find_account_by_alias_id_view(ctx: &ViewContext, auth_identity_id: String) -> Option { + let auth_identity = ctx.db.account_identity().id().find(&auth_identity_id)?; + ctx.db.account().id().find(&auth_identity.account_id) +} + +fn find_account_by_alias_id_reducer( + ctx: &ReducerContext, + auth_identity_id: String, +) -> Option { + let auth_identity = ctx.db.account_identity().id().find(&auth_identity_id)?; + ctx.db.account().id().find(&auth_identity.account_id) +} + +fn find_account_by_alias_id_tx(tx: &TxContext, auth_identity_id: String) -> Option { + let auth_identity = tx.db.account_identity().id().find(&auth_identity_id)?; + tx.db.account().id().find(&auth_identity.account_id) +} + +fn resolve_account_by_identity_view( + ctx: &ViewContext, + identity: spacetimedb::Identity, +) -> Option { + let sender_hex = identity.to_hex().to_string(); + find_account_by_alias_id_view(ctx, jwt_identity_id(&sender_hex)) + .or_else(|| find_account_by_alias_id_view(ctx, guest_identity_id(&sender_hex))) + .or_else(|| ctx.db.account().identity().find(identity)) +} + +fn resolve_account_by_identity_reducer( + ctx: &ReducerContext, + identity: spacetimedb::Identity, +) -> Option { + let sender_hex = identity.to_hex().to_string(); + find_account_by_alias_id_reducer(ctx, jwt_identity_id(&sender_hex)) + .or_else(|| find_account_by_alias_id_reducer(ctx, guest_identity_id(&sender_hex))) + .or_else(|| ctx.db.account().identity().find(identity)) +} + +fn resolve_account_by_identity_tx( + tx: &TxContext, + identity: spacetimedb::Identity, +) -> Option { + let sender_hex = identity.to_hex().to_string(); + find_account_by_alias_id_tx(tx, jwt_identity_id(&sender_hex)) + .or_else(|| find_account_by_alias_id_tx(tx, guest_identity_id(&sender_hex))) + .or_else(|| tx.db.account().identity().find(identity)) +} + fn current_session_id_for_reducer(ctx: &ReducerContext) -> String { let session_key = ctx .connection_id() @@ -35,19 +84,20 @@ fn current_session_id_for_tx(tx: &TxContext) -> String { #[view(accessor = my_auth_state, public)] pub fn my_auth_state_view(ctx: &ViewContext) -> Option { let config = load_app_config_read_only().unwrap_or_else(|| default_app_config(0)); - find_user_by_identity_read_only(ctx.sender()).map(|user| to_auth_state_view(&config, &user)) + find_account_by_identity_read_only(ctx.sender()) + .map(|account| to_auth_state_view(&config, &account)) } #[view(accessor = my_auth_audit_logs, public)] pub fn my_auth_audit_logs_view(ctx: &ViewContext) -> Vec { - let Some(user) = find_user_by_identity_read_only(ctx.sender()) else { + let Some(account) = find_account_by_identity_read_only(ctx.sender()) else { return Vec::new(); }; let mut rows: Vec<_> = ctx .db .auth_audit_log() - .user_id() - .filter(&user.id) + .account_id() + .filter(&account.id) .map(|row| AuthAuditLogView { id: row.id, event_type: row.event_type, @@ -62,16 +112,16 @@ pub fn my_auth_audit_logs_view(ctx: &ViewContext) -> Vec { rows } -#[view(accessor = my_user_sessions, public)] -pub fn my_user_sessions_view(ctx: &ViewContext) -> Vec { - let Some(user) = find_user_by_identity_read_only(ctx.sender()) else { +#[view(accessor = my_account_sessions, public)] +pub fn my_account_sessions_view(ctx: &ViewContext) -> Vec { + let Some(account) = find_account_by_identity_read_only(ctx.sender()) else { return Vec::new(); }; let mut rows: Vec<_> = ctx .db - .user_session() - .user_id() - .filter(&user.id) + .account_session() + .account_id() + .filter(&account.id) .filter(|row| row.revoked_at_ms.is_none()) .map(|row| AuthSessionView { session_id: row.id.clone(), @@ -90,18 +140,18 @@ pub fn my_user_sessions_view(ctx: &ViewContext) -> Vec { #[view(accessor = my_auth_risk_blocks, public)] pub fn my_auth_risk_blocks_view(ctx: &ViewContext) -> Vec { - let Some(user) = find_user_by_identity_read_only(ctx.sender()) else { + let Some(account) = find_account_by_identity_read_only(ctx.sender()) else { return Vec::new(); }; let known_ips: std::collections::BTreeSet<_> = ctx .db - .user_session() - .user_id() - .filter(&user.id) + .account_session() + .account_id() + .filter(&account.id) .filter_map(|row| row.ip) .collect(); - let phone_key = user.phone_number.clone(); + let phone_key = account.phone_number.clone(); let mut rows: Vec<_> = ctx .db @@ -146,10 +196,10 @@ pub fn lift_my_risk_block( meta: RequestMeta, scope_type: RiskBlockScopeType, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "lift_my_risk_block") { - Ok(user) => { + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "lift_my_risk_block") { + Ok(account) => { let scope_key = match scope_type { - RiskBlockScopeType::Phone => match user.phone_number.clone() { + RiskBlockScopeType::Phone => match account.phone_number.clone() { Some(value) => value, None => { return MutationResult::error( @@ -163,7 +213,7 @@ pub fn lift_my_risk_block( session_id_for_identity_hex(&tx.sender().to_hex().to_string()); match tx .db - .user_session() + .account_session() .id() .find(¤t_session_id) .and_then(|row| row.ip) @@ -207,7 +257,7 @@ pub fn lift_my_risk_block( create_auth_audit_log( tx, - &user.id, + &account.id, match scope_type { RiskBlockScopeType::Phone => "risk_unblock_phone", RiskBlockScopeType::Ip => "risk_unblock_ip", @@ -228,20 +278,20 @@ pub fn revoke_user_session( meta: RequestMeta, session_id: String, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "revoke_user_session") { - Ok(user) => { + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "revoke_user_session") { + Ok(account) => { let normalized_session_id = normalize_optional_string(Some(&session_id)); let Some(normalized_session_id) = normalized_session_id else { return MutationResult::error("invalid_session_id", "sessionId 不能为空"); }; - let Some(existing) = tx.db.user_session().id().find(&normalized_session_id) else { + let Some(existing) = tx.db.account_session().id().find(&normalized_session_id) else { return MutationResult::error("session_not_found", "未找到目标会话"); }; - if existing.user_id != user.id { + if existing.account_id != account.id { return MutationResult::error("session_forbidden", "不能操作其他账号的会话"); } let now_ms = timestamp_ms(tx.timestamp); - tx.db.user_session().id().update(UserSession { + tx.db.account_session().id().update(AccountSession { revoked_at_ms: Some(now_ms), updated_at_ms: now_ms, ..existing @@ -254,7 +304,7 @@ pub fn revoke_user_session( ); create_auth_audit_log( tx, - &user.id, + &account.id, "revoke_session", &format!("已移除会话 {}", normalized_session_id), &meta, @@ -268,21 +318,21 @@ pub fn revoke_user_session( #[procedure] pub fn logout_all_user_sessions(ctx: &mut ProcedureContext, meta: RequestMeta) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "logout_all_user_sessions") { - Ok(user) => { + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "logout_all_user_sessions") { + Ok(account) => { let now_ms = timestamp_ms(tx.timestamp); let target_ids: Vec<_> = tx .db - .user_session() - .user_id() - .filter(&user.id) + .account_session() + .account_id() + .filter(&account.id) .filter(|row| row.revoked_at_ms.is_none()) .map(|row| row.id) .collect(); for session_id in &target_ids { - if let Some(existing) = tx.db.user_session().id().find(session_id) { - tx.db.user_session().id().update(UserSession { + if let Some(existing) = tx.db.account_session().id().find(session_id) { + tx.db.account_session().id().update(AccountSession { revoked_at_ms: Some(now_ms), updated_at_ms: now_ms, ..existing @@ -298,7 +348,7 @@ pub fn logout_all_user_sessions(ctx: &mut ProcedureContext, meta: RequestMeta) - create_auth_audit_log( tx, - &user.id, + &account.id, "logout_all", "已注销全部会话", &meta, @@ -318,7 +368,7 @@ pub fn send_sms_verification_code( scene: SmsAuthScene, ) -> SmsSendCodeResult { ctx.with_tx(|tx| { - let provisioned = provision_user_with_meta(tx, Some(&meta)); + let provisioned = provision_account_with_meta(tx, Some(&meta)); let config = provisioned.config; if !config.sms_auth_enabled { return SmsSendCodeResult::error("sms_disabled", "短信验证能力未启用"); @@ -418,9 +468,9 @@ pub fn verify_sms_code( code: String, ) -> SmsVerifyCodeResult { ctx.with_tx(|tx| { - let provisioned = provision_user_with_meta(tx, Some(&meta)); + let provisioned = provision_account_with_meta(tx, Some(&meta)); let config = provisioned.config; - let mut user = provisioned.user; + let mut account = provisioned.account; if !config.sms_auth_enabled { return SmsVerifyCodeResult::error("sms_disabled", "短信验证能力未启用"); @@ -525,7 +575,7 @@ pub fn verify_sms_code( create_auth_audit_log( tx, - &user.id, + &account.id, "sms_verify_failed", "短信验证码校验失败", &meta, @@ -534,9 +584,30 @@ pub fn verify_sms_code( return SmsVerifyCodeResult::error("sms_code_invalid", "验证码错误"); } - if let Some(existing_user) = find_user_by_phone_number(tx, &normalized_phone.e164) { - if existing_user.id != user.id { - return SmsVerifyCodeResult::error("phone_already_bound", "手机号已绑定其他账号"); + if let Some(existing_account) = find_account_by_phone_number(tx, &normalized_phone.e164) { + if existing_account.id != account.id { + merge_account_data(tx, &account, &existing_account); + let merged_account = Account { + login_provider: LoginProvider::Phone, + account_status: AccountStatus::Active, + phone_number: Some(normalized_phone.e164.clone()), + phone_verified_at_ms: Some(now_ms), + updated_at_ms: now_ms, + ..existing_account + }; + tx.db.account().id().update(merged_account.clone()); + ensure_sender_identity_tx(tx, &merged_account); + ensure_phone_identity(tx, &merged_account, &normalized_phone.e164); + upsert_user_session_tx(tx, &merged_account, Some(&meta)); + create_auth_audit_log( + tx, + &merged_account.id, + "sms_verified", + "当前设备已归并到已有手机号账户", + &meta, + Some(format!("{{\"mergedFromUserId\":\"{}\"}}", account.id)), + ); + return SmsVerifyCodeResult::ok("已登录到现有手机号账户"); } } @@ -548,22 +619,22 @@ pub fn verify_sms_code( SmsAuthAction::VerifyCode, true, ); - user.phone_number = Some(normalized_phone.e164.clone()); - user.phone_verified_at_ms = Some(now_ms); - user.account_status = AccountStatus::Active; - if user.login_provider == LoginProvider::Guest { - user.login_provider = LoginProvider::Phone; + account.phone_number = Some(normalized_phone.e164.clone()); + account.phone_verified_at_ms = Some(now_ms); + account.account_status = AccountStatus::Active; + if account.login_provider == LoginProvider::Guest { + account.login_provider = LoginProvider::Phone; } - user.updated_at_ms = now_ms; - tx.db.user().id().update(user.clone()); + account.updated_at_ms = now_ms; + tx.db.account().id().update(account.clone()); - ensure_phone_identity(tx, &user, &normalized_phone.e164); - create_auth_audit_log(tx, &user.id, "sms_verified", "短信验证已通过", &meta, None); + ensure_phone_identity(tx, &account, &normalized_phone.e164); + create_auth_audit_log(tx, &account.id, "sms_verified", "短信验证已通过", &meta, None); SmsVerifyCodeResult::ok("短信验证通过") }) } -pub fn provision_user(ctx: &ReducerContext) -> UserProvisioning { +pub fn provision_account(ctx: &ReducerContext) -> AccountProvisioning { let config = ctx .db .app_config() @@ -573,7 +644,7 @@ pub fn provision_user(ctx: &ReducerContext) -> UserProvisioning { let now_ms = timestamp_ms(ctx.timestamp); let sender = ctx.sender(); let sender_hex = sender.to_hex().to_string(); - if let Some(existing) = ctx.db.user().identity().find(sender) { + if let Some(existing) = resolve_account_by_identity_reducer(ctx, sender) { let updated = reconcile_user_row( existing, &config, @@ -581,18 +652,18 @@ pub fn provision_user(ctx: &ReducerContext) -> UserProvisioning { resolve_login_provider_from_auth(ctx.sender_auth().has_jwt()), build_guest_display_name(&config, &sender_hex), ); - ctx.db.user().id().update(updated.clone()); + ctx.db.account().id().update(updated.clone()); ensure_sender_identity(ctx, &updated); upsert_user_session(ctx, &updated, None); - return UserProvisioning { - user: updated, + return AccountProvisioning { + account: updated, config, existed: true, }; } - let row = User { - id: user_id_for_identity_hex(&sender_hex), + let row = Account { + id: new_account_id(now_ms, &sender_hex), identity: sender, username: None, password_hash: None, @@ -609,22 +680,25 @@ pub fn provision_user(ctx: &ReducerContext) -> UserProvisioning { created_at_ms: now_ms, updated_at_ms: now_ms, }; - let user = ctx.db.user().insert(row); + let user = ctx.db.account().insert(row); ensure_sender_identity(ctx, &user); upsert_user_session(ctx, &user, None); - UserProvisioning { - user, + AccountProvisioning { + account: user, config, existed: false, } } -pub fn provision_user_with_meta(tx: &TxContext, meta: Option<&RequestMeta>) -> UserProvisioning { +pub fn provision_account_with_meta( + tx: &TxContext, + meta: Option<&RequestMeta>, +) -> AccountProvisioning { let config = ensure_app_config_row(tx); let now_ms = timestamp_ms(tx.timestamp); let sender = tx.sender(); let sender_hex = sender.to_hex().to_string(); - if let Some(existing) = tx.db.user().identity().find(sender) { + if let Some(existing) = resolve_account_by_identity_tx(tx, sender) { let updated = reconcile_user_row( existing, &config, @@ -632,18 +706,18 @@ pub fn provision_user_with_meta(tx: &TxContext, meta: Option<&RequestMeta>) -> U resolve_login_provider_from_auth(tx.sender_auth().has_jwt()), build_guest_display_name(&config, &sender_hex), ); - tx.db.user().id().update(updated.clone()); + tx.db.account().id().update(updated.clone()); ensure_sender_identity_tx(tx, &updated); upsert_user_session_tx(tx, &updated, meta); - return UserProvisioning { - user: updated, + return AccountProvisioning { + account: updated, config, existed: true, }; } - let row = User { - id: user_id_for_identity_hex(&sender_hex), + let row = Account { + id: new_account_id(now_ms, &sender_hex), identity: sender, username: None, password_hash: None, @@ -660,24 +734,24 @@ pub fn provision_user_with_meta(tx: &TxContext, meta: Option<&RequestMeta>) -> U created_at_ms: now_ms, updated_at_ms: now_ms, }; - let user = tx.db.user().insert(row); + let user = tx.db.account().insert(row); ensure_sender_identity_tx(tx, &user); upsert_user_session_tx(tx, &user, meta); - UserProvisioning { - user, + AccountProvisioning { + account: user, config, existed: false, } } -pub fn guard_user_action( +pub fn guard_account_action( tx: &TxContext, meta: &RequestMeta, operation_name: &str, -) -> Result { - let provisioned = provision_user_with_meta(tx, Some(meta)); +) -> Result { + let provisioned = provision_account_with_meta(tx, Some(meta)); let current_session_id = current_session_id_for_tx(tx); - if let Some(current_session) = tx.db.user_session().id().find(¤t_session_id) { + if let Some(current_session) = tx.db.account_session().id().find(¤t_session_id) { if current_session.revoked_at_ms.is_some() { emit_session_revocation_event( tx, @@ -692,11 +766,11 @@ pub fn guard_user_action( )); } } - if provisioned.user.account_status == AccountStatus::Disabled { + if provisioned.account.account_status == AccountStatus::Disabled { emit_kick_event(tx, "account_disabled", "账号已被禁用"); create_auth_audit_log( tx, - &provisioned.user.id, + &provisioned.account.id, "account_disabled", &format!("调用 {} 时命中禁用账号", operation_name), meta, @@ -704,8 +778,8 @@ pub fn guard_user_action( ); return Err(MutationResult::kicked("account_disabled", "账号已被禁用")); } - if needs_sms_verification(&provisioned.config, &provisioned.user) { - emit_verification_prompt_tx(tx, &provisioned.user, "请先完成短信验证"); + if needs_sms_verification(&provisioned.config, &provisioned.account) { + emit_verification_prompt_tx(tx, &provisioned.account, "请先完成短信验证"); emit_kick_event( tx, "sms_verification_required", @@ -713,7 +787,7 @@ pub fn guard_user_action( ); create_auth_audit_log( tx, - &provisioned.user.id, + &provisioned.account.id, "sms_verification_required", &format!("调用 {} 时命中短信验证门禁", operation_name), meta, @@ -724,10 +798,10 @@ pub fn guard_user_action( &provisioned.config.kick_message_unverified, )); } - Ok(provisioned.user) + Ok(provisioned.account) } -pub fn emit_verification_prompt(ctx: &ReducerContext, user: &User, detail: &str) { +pub fn emit_verification_prompt(ctx: &ReducerContext, user: &Account, detail: &str) { ctx.db .verification_prompt_event() .insert(VerificationPromptEvent { @@ -742,17 +816,18 @@ pub fn emit_verification_prompt(ctx: &ReducerContext, user: &User, detail: &str) }); } -pub fn needs_sms_verification(config: &AppConfig, user: &User) -> bool { +pub fn needs_sms_verification(config: &AppConfig, user: &Account) -> bool { config.sms_verification_required && user.phone_verified_at_ms.is_none() } -pub fn find_user_by_identity_read_only(identity: spacetimedb::Identity) -> Option { - ViewContext::new(identity).db.user().identity().find(identity) +pub fn find_account_by_identity_read_only(identity: spacetimedb::Identity) -> Option { + let ctx = ViewContext::new(identity); + resolve_account_by_identity_view(&ctx, identity) } -fn to_auth_state_view(config: &AppConfig, user: &User) -> AuthStateView { +fn to_auth_state_view(config: &AppConfig, user: &Account) -> AuthStateView { AuthStateView { - user_id: user.id.clone(), + account_id: user.id.clone(), identity: user.identity, display_name: user.display_name.clone(), phone_number_masked: user @@ -781,12 +856,12 @@ fn build_guest_display_name(config: &AppConfig, sender_hex: &str) -> String { } fn reconcile_user_row( - mut user: User, + mut user: Account, config: &AppConfig, now_ms: u64, login_provider: LoginProvider, fallback_display_name: String, -) -> User { +) -> Account { if user.display_name.trim().is_empty() { user.display_name = fallback_display_name; } @@ -807,14 +882,14 @@ fn reconcile_user_row( user } -fn ensure_sender_identity(ctx: &ReducerContext, user: &User) { +fn ensure_sender_identity(ctx: &ReducerContext, user: &Account) { let now_ms = timestamp_ms(ctx.timestamp); let sender_hex = ctx.sender().to_hex().to_string(); if let Some(jwt) = ctx.sender_auth().jwt() { let identity_id = jwt_identity_id(&sender_hex); - let next_row = AuthIdentity { + let next_row = AccountIdentity { id: identity_id.clone(), - user_id: user.id.clone(), + account_id: user.id.clone(), provider: AuthIdentityProvider::Jwt, provider_uid: jwt.subject().to_string(), provider_union_id: None, @@ -824,7 +899,7 @@ fn ensure_sender_identity(ctx: &ReducerContext, user: &User) { meta_json: Some(jwt.raw_payload().to_string()), created_at_ms: ctx .db - .auth_identity() + .account_identity() .id() .find(&identity_id) .map(|row| row.created_at_ms) @@ -834,9 +909,9 @@ fn ensure_sender_identity(ctx: &ReducerContext, user: &User) { upsert_auth_identity_reducer(ctx, next_row); } else { let identity_id = guest_identity_id(&sender_hex); - let next_row = AuthIdentity { + let next_row = AccountIdentity { id: identity_id.clone(), - user_id: user.id.clone(), + account_id: user.id.clone(), provider: AuthIdentityProvider::Guest, provider_uid: sender_hex, provider_union_id: None, @@ -846,7 +921,7 @@ fn ensure_sender_identity(ctx: &ReducerContext, user: &User) { meta_json: None, created_at_ms: ctx .db - .auth_identity() + .account_identity() .id() .find(&identity_id) .map(|row| row.created_at_ms) @@ -857,14 +932,14 @@ fn ensure_sender_identity(ctx: &ReducerContext, user: &User) { } } -fn ensure_sender_identity_tx(tx: &TxContext, user: &User) { +fn ensure_sender_identity_tx(tx: &TxContext, user: &Account) { let now_ms = timestamp_ms(tx.timestamp); let sender_hex = tx.sender().to_hex().to_string(); if let Some(jwt) = tx.sender_auth().jwt() { let identity_id = jwt_identity_id(&sender_hex); - let next_row = AuthIdentity { + let next_row = AccountIdentity { id: identity_id.clone(), - user_id: user.id.clone(), + account_id: user.id.clone(), provider: AuthIdentityProvider::Jwt, provider_uid: jwt.subject().to_string(), provider_union_id: None, @@ -874,7 +949,7 @@ fn ensure_sender_identity_tx(tx: &TxContext, user: &User) { meta_json: Some(jwt.raw_payload().to_string()), created_at_ms: tx .db - .auth_identity() + .account_identity() .id() .find(&identity_id) .map(|row| row.created_at_ms) @@ -884,9 +959,9 @@ fn ensure_sender_identity_tx(tx: &TxContext, user: &User) { upsert_auth_identity_tx(tx, next_row); } else { let identity_id = guest_identity_id(&sender_hex); - let next_row = AuthIdentity { + let next_row = AccountIdentity { id: identity_id.clone(), - user_id: user.id.clone(), + account_id: user.id.clone(), provider: AuthIdentityProvider::Guest, provider_uid: sender_hex, provider_union_id: None, @@ -896,7 +971,7 @@ fn ensure_sender_identity_tx(tx: &TxContext, user: &User) { meta_json: None, created_at_ms: tx .db - .auth_identity() + .account_identity() .id() .find(&identity_id) .map(|row| row.created_at_ms) @@ -907,12 +982,12 @@ fn ensure_sender_identity_tx(tx: &TxContext, user: &User) { } } -fn ensure_phone_identity(tx: &TxContext, user: &User, phone_number: &str) { +fn ensure_phone_identity(tx: &TxContext, user: &Account, phone_number: &str) { let now_ms = timestamp_ms(tx.timestamp); let identity_id = phone_identity_id(phone_number); - let row = AuthIdentity { + let row = AccountIdentity { id: identity_id.clone(), - user_id: user.id.clone(), + account_id: user.id.clone(), provider: AuthIdentityProvider::Phone, provider_uid: phone_number.to_string(), provider_union_id: None, @@ -922,7 +997,7 @@ fn ensure_phone_identity(tx: &TxContext, user: &User, phone_number: &str) { meta_json: None, created_at_ms: tx .db - .auth_identity() + .account_identity() .id() .find(&identity_id) .map(|item| item.created_at_ms) @@ -932,30 +1007,30 @@ fn ensure_phone_identity(tx: &TxContext, user: &User, phone_number: &str) { upsert_auth_identity_tx(tx, row); } -fn upsert_auth_identity_reducer(ctx: &ReducerContext, row: AuthIdentity) { - if ctx.db.auth_identity().id().find(&row.id).is_some() { - ctx.db.auth_identity().id().update(row); +fn upsert_auth_identity_reducer(ctx: &ReducerContext, row: AccountIdentity) { + if ctx.db.account_identity().id().find(&row.id).is_some() { + ctx.db.account_identity().id().update(row); } else { - ctx.db.auth_identity().insert(row); + ctx.db.account_identity().insert(row); } } -fn upsert_auth_identity_tx(tx: &TxContext, row: AuthIdentity) { - if tx.db.auth_identity().id().find(&row.id).is_some() { - tx.db.auth_identity().id().update(row); +fn upsert_auth_identity_tx(tx: &TxContext, row: AccountIdentity) { + if tx.db.account_identity().id().find(&row.id).is_some() { + tx.db.account_identity().id().update(row); } else { - tx.db.auth_identity().insert(row); + tx.db.account_identity().insert(row); } } -fn upsert_user_session(ctx: &ReducerContext, user: &User, meta: Option<&RequestMeta>) { +fn upsert_user_session(ctx: &ReducerContext, user: &Account, meta: Option<&RequestMeta>) { let now_ms = timestamp_ms(ctx.timestamp); let sender_hex = ctx.sender().to_hex().to_string(); let session_id = current_session_id_for_reducer(ctx); - let existing = ctx.db.user_session().id().find(&session_id); - let row = UserSession { + let existing = ctx.db.account_session().id().find(&session_id); + let row = AccountSession { id: session_id.clone(), - user_id: user.id.clone(), + account_id: user.id.clone(), refresh_token_hash: sender_hex, client_type: normalize_client_type(meta.map(|value| value.client_type.as_str())), user_agent: meta.and_then(|value| normalize_optional_string(value.user_agent.as_deref())), @@ -967,20 +1042,20 @@ fn upsert_user_session(ctx: &ReducerContext, user: &User, meta: Option<&RequestM last_seen_at_ms: now_ms, }; if existing.is_some() { - ctx.db.user_session().id().update(row); + ctx.db.account_session().id().update(row); } else { - ctx.db.user_session().insert(row); + ctx.db.account_session().insert(row); } } -fn upsert_user_session_tx(tx: &TxContext, user: &User, meta: Option<&RequestMeta>) { +fn upsert_user_session_tx(tx: &TxContext, user: &Account, meta: Option<&RequestMeta>) { let now_ms = timestamp_ms(tx.timestamp); let sender_hex = tx.sender().to_hex().to_string(); let session_id = current_session_id_for_tx(tx); - let existing = tx.db.user_session().id().find(&session_id); - let row = UserSession { + let existing = tx.db.account_session().id().find(&session_id); + let row = AccountSession { id: session_id.clone(), - user_id: user.id.clone(), + account_id: user.id.clone(), refresh_token_hash: sender_hex, client_type: normalize_client_type(meta.map(|value| value.client_type.as_str())), user_agent: meta.and_then(|value| normalize_optional_string(value.user_agent.as_deref())), @@ -992,13 +1067,306 @@ fn upsert_user_session_tx(tx: &TxContext, user: &User, meta: Option<&RequestMeta last_seen_at_ms: now_ms, }; if existing.is_some() { - tx.db.user_session().id().update(row); + tx.db.account_session().id().update(row); } else { - tx.db.user_session().insert(row); + tx.db.account_session().insert(row); } } -fn emit_verification_prompt_tx(tx: &TxContext, user: &User, detail: &str) { +fn merge_account_data(tx: &TxContext, source_user: &Account, target_user: &Account) { + if source_user.id == target_user.id { + return; + } + + if let Some(source_snapshot) = tx.db.saved_snapshot_row().account_id().find(&source_user.id) { + let target_snapshot = tx.db.saved_snapshot_row().account_id().find(&target_user.id); + let should_use_source = target_snapshot + .as_ref() + .map(|row| source_snapshot.updated_at_ms >= row.updated_at_ms) + .unwrap_or(true); + if should_use_source { + let replacement = SaveSnapshot { + account_id: target_user.id.clone(), + ..source_snapshot.clone() + }; + if target_snapshot.is_some() { + tx.db.saved_snapshot_row().account_id().update(replacement); + } else { + tx.db.saved_snapshot_row().insert(replacement); + } + } + tx.db.saved_snapshot_row().account_id().delete(&source_user.id); + } + + if let Some(source_setting) = tx.db.runtime_setting().account_id().find(&source_user.id) { + let target_setting = tx.db.runtime_setting().account_id().find(&target_user.id); + let should_use_source = target_setting + .as_ref() + .map(|row| source_setting.updated_at_ms >= row.updated_at_ms) + .unwrap_or(true); + if should_use_source { + let replacement = RuntimeSetting { + account_id: target_user.id.clone(), + ..source_setting.clone() + }; + if target_setting.is_some() { + tx.db.runtime_setting().account_id().update(replacement); + } else { + tx.db.runtime_setting().insert(replacement); + } + } + tx.db.runtime_setting().account_id().delete(&source_user.id); + } + + let source_identities: Vec<_> = tx + .db + .account_identity() + .account_id() + .filter(&source_user.id) + .map(|row| row.id) + .collect(); + for identity_id in source_identities { + if let Some(existing) = tx.db.account_identity().id().find(&identity_id) { + tx.db.account_identity().id().update(AccountIdentity { + account_id: target_user.id.clone(), + ..existing + }); + } + } + + let source_sessions: Vec<_> = tx + .db + .account_session() + .account_id() + .filter(&source_user.id) + .map(|row| row.id) + .collect(); + for session_id in source_sessions { + if let Some(existing) = tx.db.account_session().id().find(&session_id) { + tx.db.account_session().id().update(AccountSession { + account_id: target_user.id.clone(), + ..existing + }); + } + } + + let source_logs: Vec<_> = tx + .db + .auth_audit_log() + .account_id() + .filter(&source_user.id) + .map(|row| row.id) + .collect(); + for log_id in source_logs { + if let Some(existing) = tx.db.auth_audit_log().id().find(&log_id) { + tx.db.auth_audit_log().id().update(AuthAuditLog { + account_id: target_user.id.clone(), + ..existing + }); + } + } + + let source_ledgers: Vec<_> = tx + .db + .profile_wallet_ledger() + .account_id() + .filter(&source_user.id) + .map(|row| row.id) + .collect(); + for ledger_id in source_ledgers { + if let Some(existing) = tx.db.profile_wallet_ledger().id().find(&ledger_id) { + tx.db.profile_wallet_ledger().id().update(ProfileWalletLedger { + account_id: target_user.id.clone(), + ..existing + }); + } + } + + if let Some(source_dashboard) = tx.db.profile_dashboard_state().account_id().find(&source_user.id) { + let merged_dashboard = if let Some(target_dashboard) = + tx.db.profile_dashboard_state().account_id().find(&target_user.id) + { + ProfileDashboardState { + account_id: target_user.id.clone(), + wallet_balance: source_dashboard.wallet_balance.max(target_dashboard.wallet_balance), + total_play_time_ms: source_dashboard + .total_play_time_ms + .saturating_add(target_dashboard.total_play_time_ms), + updated_at_ms: source_dashboard.updated_at_ms.max(target_dashboard.updated_at_ms), + } + } else { + ProfileDashboardState { + account_id: target_user.id.clone(), + ..source_dashboard.clone() + } + }; + if tx.db.profile_dashboard_state().account_id().find(&target_user.id).is_some() { + tx.db.profile_dashboard_state().account_id().update(merged_dashboard); + } else { + tx.db.profile_dashboard_state().insert(merged_dashboard); + } + tx.db.profile_dashboard_state().account_id().delete(&source_user.id); + } + + let source_played_worlds: Vec<_> = tx + .db + .profile_played_world() + .account_id() + .filter(&source_user.id) + .map(|row| row.id) + .collect(); + for played_world_id in source_played_worlds { + let Some(source_row) = tx.db.profile_played_world().id().find(&played_world_id) else { + continue; + }; + let target_id = crate::common::profile_played_world_key(&target_user.id, &source_row.world_key); + if let Some(target_row) = tx.db.profile_played_world().id().find(&target_id) { + let use_source_meta = source_row.last_played_at_ms >= target_row.last_played_at_ms; + tx.db.profile_played_world().id().update(ProfilePlayedWorld { + id: target_id.clone(), + account_id: target_user.id.clone(), + owner_account_id: if use_source_meta { + source_row.owner_account_id + } else { + target_row.owner_account_id + }, + profile_id: if use_source_meta { + source_row.profile_id + } else { + target_row.profile_id + }, + world_type: if use_source_meta { + source_row.world_type + } else { + target_row.world_type + }, + world_title: if use_source_meta { + source_row.world_title + } else { + target_row.world_title + }, + world_subtitle: if use_source_meta { + source_row.world_subtitle + } else { + target_row.world_subtitle + }, + first_played_at_ms: source_row.first_played_at_ms.min(target_row.first_played_at_ms), + last_played_at_ms: source_row.last_played_at_ms.max(target_row.last_played_at_ms), + last_observed_play_time_ms: source_row + .last_observed_play_time_ms + .max(target_row.last_observed_play_time_ms), + world_key: source_row.world_key, + }); + } else { + tx.db.profile_played_world().insert(ProfilePlayedWorld { + id: target_id, + account_id: target_user.id.clone(), + ..source_row + }); + } + tx.db.profile_played_world().id().delete(&played_world_id); + } + + let source_browse_rows: Vec<_> = tx + .db + .account_browse_history() + .account_id() + .filter(&source_user.id) + .map(|row| row.id) + .collect(); + for browse_id in source_browse_rows { + let Some(source_row) = tx.db.account_browse_history().id().find(&browse_id) else { + continue; + }; + let target_id = crate::common::browse_history_key( + &target_user.id, + &source_row.owner_account_id, + &source_row.profile_id, + ); + if let Some(target_row) = tx.db.account_browse_history().id().find(&target_id) { + if source_row.visited_at_ms >= target_row.visited_at_ms { + tx.db.account_browse_history().id().update(AccountBrowseHistory { + id: target_id.clone(), + account_id: target_user.id.clone(), + ..source_row + }); + } + } else { + tx.db.account_browse_history().insert(AccountBrowseHistory { + id: target_id, + account_id: target_user.id.clone(), + ..source_row + }); + } + tx.db.account_browse_history().id().delete(&browse_id); + } + + let source_profile_ids: Vec<_> = tx + .db + .custom_world_profile() + .account_id() + .filter(&source_user.id) + .map(|row| row.id) + .collect(); + for source_profile_id in source_profile_ids { + let Some(source_row) = tx.db.custom_world_profile().id().find(&source_profile_id) else { + continue; + }; + let target_id = + crate::common::custom_world_profile_key(&target_user.id, &source_row.profile_id); + if let Some(target_row) = tx.db.custom_world_profile().id().find(&target_id) { + if source_row.updated_at_ms >= target_row.updated_at_ms { + tx.db.custom_world_profile().id().update(CustomWorldProfile { + id: target_id.clone(), + account_id: target_user.id.clone(), + ..source_row + }); + } + } else { + tx.db.custom_world_profile().insert(CustomWorldProfile { + id: target_id, + account_id: target_user.id.clone(), + ..source_row + }); + } + tx.db.custom_world_profile().id().delete(&source_profile_id); + } + + let source_session_ids: Vec<_> = tx + .db + .custom_world_session() + .account_id() + .filter(&source_user.id) + .map(|row| row.id) + .collect(); + for source_session_id in source_session_ids { + let Some(source_row) = tx.db.custom_world_session().id().find(&source_session_id) else { + continue; + }; + let target_id = + crate::common::custom_world_session_key(&target_user.id, &source_row.session_id); + if let Some(target_row) = tx.db.custom_world_session().id().find(&target_id) { + if source_row.updated_at_ms >= target_row.updated_at_ms { + tx.db.custom_world_session().id().update(CustomWorldSession { + id: target_id.clone(), + account_id: target_user.id.clone(), + ..source_row + }); + } + } else { + tx.db.custom_world_session().insert(CustomWorldSession { + id: target_id, + account_id: target_user.id.clone(), + ..source_row + }); + } + tx.db.custom_world_session().id().delete(&source_session_id); + } + + tx.db.account().id().delete(&source_user.id); +} + +fn emit_verification_prompt_tx(tx: &TxContext, user: &Account, detail: &str) { tx.db .verification_prompt_event() .insert(VerificationPromptEvent { @@ -1040,7 +1408,7 @@ fn emit_session_revocation_event( fn create_auth_audit_log( tx: &TxContext, - user_id: &str, + account_id: &str, event_type: &str, detail: &str, meta: &RequestMeta, @@ -1048,7 +1416,7 @@ fn create_auth_audit_log( ) { tx.db.auth_audit_log().insert(AuthAuditLog { id: 0, - user_id: user_id.to_string(), + account_id: account_id.to_string(), event_type: event_type.to_string(), detail: detail.to_string(), ip: request_meta_ip(meta), @@ -1166,9 +1534,9 @@ fn create_or_refresh_risk_block( }); } -fn find_user_by_phone_number(tx: &TxContext, phone_number: &str) -> Option { +fn find_account_by_phone_number(tx: &TxContext, phone_number: &str) -> Option { tx.db - .user() + .account() .iter() .find(|row| row.phone_number.as_deref() == Some(phone_number)) } diff --git a/spacetimedb/src/common.rs b/spacetimedb/src/common.rs index 0c287ea6..79d6b5b5 100644 --- a/spacetimedb/src/common.rs +++ b/spacetimedb/src/common.rs @@ -57,8 +57,9 @@ pub fn normalize_saved_at_ms(saved_at_ms: u64, timestamp: Timestamp) -> u64 { } } -pub fn user_id_for_identity_hex(identity_hex: &str) -> String { - format!("user_{identity_hex}") +pub fn new_account_id(now_ms: u64, identity_hex: &str) -> String { + let suffix = identity_hex.get(..10).unwrap_or(identity_hex); + format!("acct_{now_ms:x}_{suffix}") } pub fn session_id_for_identity_hex(identity_hex: &str) -> String { @@ -77,24 +78,24 @@ pub fn phone_identity_id(phone_number: &str) -> String { format!("authi_phone_{}", phone_number.replace('+', "")) } -pub fn custom_world_profile_key(user_id: &str, profile_id: &str) -> String { - format!("{user_id}:{profile_id}") +pub fn custom_world_profile_key(account_id: &str, profile_id: &str) -> String { + format!("{account_id}:{profile_id}") } -pub fn custom_world_session_key(user_id: &str, session_id: &str) -> String { - format!("{user_id}:{session_id}") +pub fn custom_world_session_key(account_id: &str, session_id: &str) -> String { + format!("{account_id}:{session_id}") } -pub fn profile_wallet_ledger_id(user_id: &str, source_key: &str) -> String { - format!("ledger:{user_id}:{source_key}") +pub fn profile_wallet_ledger_id(account_id: &str, source_key: &str) -> String { + format!("ledger:{account_id}:{source_key}") } -pub fn profile_played_world_key(user_id: &str, world_key: &str) -> String { - format!("played:{user_id}:{world_key}") +pub fn profile_played_world_key(account_id: &str, world_key: &str) -> String { + format!("played:{account_id}:{world_key}") } -pub fn browse_history_key(user_id: &str, owner_user_id: &str, profile_id: &str) -> String { - format!("browse:{user_id}:{owner_user_id}:{profile_id}") +pub fn browse_history_key(account_id: &str, owner_account_id: &str, profile_id: &str) -> String { + format!("browse:{account_id}:{owner_account_id}:{profile_id}") } pub fn ip_key(ip: Option<&String>) -> String { @@ -254,7 +255,7 @@ pub fn dedupe_browse_history_entries( let mut deduped = Vec::new(); let mut seen_keys = std::collections::BTreeSet::new(); for entry in normalized_entries { - let key = format!("{}:{}", entry.owner_user_id, entry.profile_id); + let key = format!("{}:{}", entry.owner_account_id, entry.profile_id); if seen_keys.insert(key) { deduped.push(entry); } @@ -266,15 +267,15 @@ fn normalize_browse_history_entry( entry: PlatformBrowseHistoryWriteInput, now_ms: u64, ) -> Option { - let owner_user_id = normalize_required_string(&entry.owner_user_id); + let owner_account_id = normalize_required_string(&entry.owner_account_id); let profile_id = normalize_required_string(&entry.profile_id); let world_name = normalize_required_string(&entry.world_name); - if owner_user_id.is_empty() || profile_id.is_empty() || world_name.is_empty() { + if owner_account_id.is_empty() || profile_id.is_empty() || world_name.is_empty() { return None; } Some(PlatformBrowseHistoryWriteInput { - owner_user_id, + owner_account_id, profile_id, world_name, subtitle: normalize_required_string(&entry.subtitle), diff --git a/spacetimedb/src/lib.rs b/spacetimedb/src/lib.rs index e06abc82..b7ddfff4 100644 --- a/spacetimedb/src/lib.rs +++ b/spacetimedb/src/lib.rs @@ -15,10 +15,15 @@ pub fn init(ctx: &ReducerContext) { #[reducer(client_connected)] pub fn client_connected(ctx: &ReducerContext) { config::ensure_default_app_config(ctx); - let provisioned = auth::provision_user(ctx); - if provisioned.existed && auth::needs_sms_verification(&provisioned.config, &provisioned.user) + let provisioned = auth::provision_account(ctx); + if provisioned.existed + && auth::needs_sms_verification(&provisioned.config, &provisioned.account) { - auth::emit_verification_prompt(ctx, &provisioned.user, "账号尚未完成短信验证,请先验证手机号"); + auth::emit_verification_prompt( + ctx, + &provisioned.account, + "账号尚未完成短信验证,请先验证手机号", + ); } } diff --git a/spacetimedb/src/runtime.rs b/spacetimedb/src/runtime.rs index ca76efae..070d818b 100644 --- a/spacetimedb/src/runtime.rs +++ b/spacetimedb/src/runtime.rs @@ -3,7 +3,7 @@ use spacetimedb::{ procedure, view, AnonymousViewContext, ProcedureContext, Table, TxContext, ViewContext, }; -use crate::auth::{find_user_by_identity_read_only, guard_user_action}; +use crate::auth::{find_account_by_identity_read_only, guard_account_action}; use crate::common::{ browse_history_key, builtin_world_title, contains_any, custom_world_profile_key, custom_world_session_key, dedupe_browse_history_entries, is_valid_json, normalize_required_string, @@ -17,11 +17,11 @@ use crate::types::*; #[view(accessor = my_snapshot, public)] pub fn my_snapshot_view(ctx: &ViewContext) -> Option { - let user = find_user_by_identity_read_only(ctx.sender())?; + let account = find_account_by_identity_read_only(ctx.sender())?; ctx.db .saved_snapshot_row() - .user_id() - .find(&user.id) + .account_id() + .find(&account.id) .map(|snapshot| SnapshotView { version: snapshot.version, saved_at_ms: snapshot.saved_at_ms, @@ -33,13 +33,13 @@ pub fn my_snapshot_view(ctx: &ViewContext) -> Option { #[view(accessor = my_runtime_settings, public)] pub fn my_runtime_settings_view(ctx: &ViewContext) -> Option { - let user = find_user_by_identity_read_only(ctx.sender())?; + let account = find_account_by_identity_read_only(ctx.sender())?; let config = load_app_config_read_only().unwrap_or_else(|| default_app_config(0)); let music_volume = ctx .db .runtime_setting() - .user_id() - .find(&user.id) + .account_id() + .find(&account.id) .map(|row| row.music_volume) .unwrap_or(config.default_music_volume); Some(RuntimeSettingsView { music_volume }) @@ -47,13 +47,13 @@ pub fn my_runtime_settings_view(ctx: &ViewContext) -> Option Option { - let user = find_user_by_identity_read_only(ctx.sender())?; - let state = ctx.db.profile_dashboard_state().user_id().find(&user.id); + let account = find_account_by_identity_read_only(ctx.sender())?; + let state = ctx.db.profile_dashboard_state().account_id().find(&account.id); let played_world_count = ctx .db .profile_played_world() - .user_id() - .filter(&user.id) + .account_id() + .filter(&account.id) .count() as u32; Some(ProfileDashboardView { wallet_balance: state.as_ref().map(|row| row.wallet_balance).unwrap_or(0), @@ -65,14 +65,14 @@ pub fn my_profile_dashboard_view(ctx: &ViewContext) -> Option Vec { - let Some(user) = find_user_by_identity_read_only(ctx.sender()) else { + let Some(account) = find_account_by_identity_read_only(ctx.sender()) else { return Vec::new(); }; let mut rows: Vec<_> = ctx .db .profile_wallet_ledger() - .user_id() - .filter(&user.id) + .account_id() + .filter(&account.id) .map(to_profile_wallet_ledger_view) .collect(); sort_desc_by_key(&mut rows, |row| row.created_at_ms); @@ -82,14 +82,14 @@ pub fn my_profile_wallet_ledger_view(ctx: &ViewContext) -> Vec Vec { - let Some(user) = find_user_by_identity_read_only(ctx.sender()) else { + let Some(account) = find_account_by_identity_read_only(ctx.sender()) else { return Vec::new(); }; let mut rows: Vec<_> = ctx .db .profile_played_world() - .user_id() - .filter(&user.id) + .account_id() + .filter(&account.id) .map(to_profile_played_world_view) .collect(); sort_desc_by_key(&mut rows, |row| row.last_played_at_ms); @@ -98,14 +98,14 @@ pub fn my_profile_played_worlds_view(ctx: &ViewContext) -> Vec Vec { - let Some(user) = find_user_by_identity_read_only(ctx.sender()) else { + let Some(account) = find_account_by_identity_read_only(ctx.sender()) else { return Vec::new(); }; let mut rows: Vec<_> = ctx .db - .user_browse_history() - .user_id() - .filter(&user.id) + .account_browse_history() + .account_id() + .filter(&account.id) .map(to_platform_browse_history_view) .collect(); sort_desc_by_key(&mut rows, |row| row.visited_at_ms); @@ -114,14 +114,14 @@ pub fn my_browse_history_view(ctx: &ViewContext) -> Vec Vec { - let Some(user) = find_user_by_identity_read_only(ctx.sender()) else { + let Some(account) = find_account_by_identity_read_only(ctx.sender()) else { return Vec::new(); }; let mut rows: Vec<_> = ctx .db .custom_world_profile() - .user_id() - .filter(&user.id) + .account_id() + .filter(&account.id) .filter(|row| row.deleted_at_ms.is_none()) .map(to_custom_world_profile_view) .collect(); @@ -164,7 +164,7 @@ pub fn published_custom_world_profiles_view( .filter(&CustomWorldPublicationStatus::Published) .filter(|row| row.deleted_at_ms.is_none()) .map(|row| PublishedCustomWorldProfileView { - owner_user_id: row.user_id, + owner_account_id: row.account_id, profile_id: row.profile_id, payload_json: row.payload_json, visibility: row.visibility, @@ -193,14 +193,14 @@ pub fn published_custom_world_profiles_view( #[view(accessor = my_custom_world_sessions, public)] pub fn my_custom_world_sessions_view(ctx: &ViewContext) -> Vec { - let Some(user) = find_user_by_identity_read_only(ctx.sender()) else { + let Some(account) = find_account_by_identity_read_only(ctx.sender()) else { return Vec::new(); }; let mut rows: Vec<_> = ctx .db .custom_world_session() - .user_id() - .filter(&user.id) + .account_id() + .filter(&account.id) .map(to_custom_world_session_view) .collect(); sort_desc_by_key(&mut rows, |row| row.updated_at_ms); @@ -216,17 +216,17 @@ pub fn save_snapshot( bottom_tab: String, current_story_json: Option, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "save_snapshot") { - Ok(user) => save_snapshot_impl(tx, &user, saved_at_ms, game_state_json.clone(), bottom_tab.clone(), current_story_json.clone()), + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "save_snapshot") { + Ok(account) => save_snapshot_impl(tx, &account, saved_at_ms, game_state_json.clone(), bottom_tab.clone(), current_story_json.clone()), Err(result) => result, }) } #[procedure] pub fn delete_snapshot(ctx: &mut ProcedureContext, meta: RequestMeta) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "delete_snapshot") { - Ok(user) => { - tx.db.saved_snapshot_row().user_id().delete(&user.id); + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "delete_snapshot") { + Ok(account) => { + tx.db.saved_snapshot_row().account_id().delete(&account.id); MutationResult::ok("snapshot_deleted", "运行时快照已删除") } Err(result) => result, @@ -239,11 +239,11 @@ pub fn put_runtime_settings( meta: RequestMeta, music_volume: f32, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "put_runtime_settings") { - Ok(user) => { + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "put_runtime_settings") { + Ok(account) => { let config = crate::config::ensure_app_config_row(tx); let row = RuntimeSetting { - user_id: user.id, + account_id: account.id, music_volume: if music_volume.is_finite() { music_volume.clamp(0.0, 1.0) } else { @@ -264,15 +264,15 @@ pub fn upsert_platform_browse_history( meta: RequestMeta, entries: Vec, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "upsert_platform_browse_history") { - Ok(user) => { + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "upsert_platform_browse_history") { + Ok(account) => { let normalized_entries = dedupe_browse_history_entries(entries.clone(), timestamp_ms(tx.timestamp)); for entry in normalized_entries { - let row = UserBrowseHistory { - id: browse_history_key(&user.id, &entry.owner_user_id, &entry.profile_id), - user_id: user.id.clone(), - owner_user_id: entry.owner_user_id, + let row = AccountBrowseHistory { + id: browse_history_key(&account.id, &entry.owner_account_id, &entry.profile_id), + account_id: account.id.clone(), + owner_account_id: entry.owner_account_id, profile_id: entry.profile_id, world_name: entry.world_name, subtitle: entry.subtitle, @@ -295,17 +295,17 @@ pub fn clear_platform_browse_history( ctx: &mut ProcedureContext, meta: RequestMeta, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "clear_platform_browse_history") { - Ok(user) => { + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "clear_platform_browse_history") { + Ok(account) => { let keys: Vec<_> = tx .db - .user_browse_history() - .user_id() - .filter(&user.id) + .account_browse_history() + .account_id() + .filter(&account.id) .map(|row| row.id) .collect(); for key in keys { - tx.db.user_browse_history().id().delete(&key); + tx.db.account_browse_history().id().delete(&key); } MutationResult::ok("browse_history_cleared", "浏览历史已清空") } @@ -321,10 +321,10 @@ pub fn upsert_custom_world_profile( payload_json: String, author_display_name: String, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "upsert_custom_world_profile") { - Ok(user) => upsert_custom_world_profile_impl( + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "upsert_custom_world_profile") { + Ok(account) => upsert_custom_world_profile_impl( tx, - &user, + &account, profile_id.clone(), payload_json.clone(), author_display_name.clone(), @@ -339,10 +339,10 @@ pub fn delete_custom_world_profile( meta: RequestMeta, profile_id: String, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "delete_custom_world_profile") { - Ok(user) => { + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "delete_custom_world_profile") { + Ok(account) => { let normalized_profile_id = normalize_required_string(&profile_id); - let key = custom_world_profile_key(&user.id, &normalized_profile_id); + let key = custom_world_profile_key(&account.id, &normalized_profile_id); let Some(existing) = tx.db.custom_world_profile().id().find(&key) else { return MutationResult::error("custom_world_not_found", "未找到目标自定义世界档案"); }; @@ -369,8 +369,8 @@ pub fn upsert_custom_world_session( created_at_ms: u64, updated_at_ms: u64, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "upsert_custom_world_session") { - Ok(user) => { + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "upsert_custom_world_session") { + Ok(account) => { let normalized_session_id = normalize_required_string(&session_id); if normalized_session_id.is_empty() { return MutationResult::error("invalid_session_id", "sessionId 不能为空"); @@ -379,8 +379,8 @@ pub fn upsert_custom_world_session( return MutationResult::error("invalid_session_json", "会话数据 JSON 不合法"); } let row = CustomWorldSession { - id: custom_world_session_key(&user.id, &normalized_session_id), - user_id: user.id, + id: custom_world_session_key(&account.id, &normalized_session_id), + account_id: account.id, session_id: normalized_session_id, payload_json: payload_json.clone(), created_at_ms: if created_at_ms == 0 { @@ -408,10 +408,10 @@ pub fn publish_custom_world_profile( profile_id: String, author_display_name: String, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "publish_custom_world_profile") { - Ok(user) => publish_or_unpublish_custom_world_profile( + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "publish_custom_world_profile") { + Ok(account) => publish_or_unpublish_custom_world_profile( tx, - &user, + &account, profile_id.clone(), author_display_name.clone(), true, @@ -427,10 +427,10 @@ pub fn unpublish_custom_world_profile( profile_id: String, author_display_name: String, ) -> MutationResult { - ctx.with_tx(|tx| match guard_user_action(tx, &meta, "unpublish_custom_world_profile") { - Ok(user) => publish_or_unpublish_custom_world_profile( + ctx.with_tx(|tx| match guard_account_action(tx, &meta, "unpublish_custom_world_profile") { + Ok(account) => publish_or_unpublish_custom_world_profile( tx, - &user, + &account, profile_id.clone(), author_display_name.clone(), false, @@ -441,7 +441,7 @@ pub fn unpublish_custom_world_profile( fn save_snapshot_impl( tx: &TxContext, - user: &User, + account: &Account, saved_at_ms: u64, game_state_json: String, bottom_tab: String, @@ -454,7 +454,7 @@ fn save_snapshot_impl( let normalized_saved_at_ms = normalize_saved_at_ms(saved_at_ms, tx.timestamp); let snapshot = SaveSnapshot { - user_id: user.id.clone(), + account_id: account.id.clone(), version: SAVE_SNAPSHOT_VERSION, saved_at_ms: normalized_saved_at_ms, bottom_tab: normalized_bottom_tab, @@ -463,14 +463,14 @@ fn save_snapshot_impl( updated_at_ms: timestamp_ms(tx.timestamp), }; upsert_save_snapshot(tx, snapshot); - sync_profile_dashboard_from_snapshot(tx, user, normalized_saved_at_ms, &game_state_json); - sync_custom_world_profile_from_snapshot(tx, user, normalized_saved_at_ms, &game_state_json); + sync_profile_dashboard_from_snapshot(tx, account, normalized_saved_at_ms, &game_state_json); + sync_custom_world_profile_from_snapshot(tx, account, normalized_saved_at_ms, &game_state_json); MutationResult::ok("snapshot_saved", "运行时快照已保存") } fn upsert_custom_world_profile_impl( tx: &TxContext, - user: &User, + account: &Account, profile_id: String, payload_json: String, author_display_name: String, @@ -484,11 +484,11 @@ fn upsert_custom_world_profile_impl( None => return MutationResult::error("invalid_profile_json", "自定义世界配置 JSON 不合法"), }; let now_ms = timestamp_ms(tx.timestamp); - let key = custom_world_profile_key(&user.id, &normalized_profile_id); + let key = custom_world_profile_key(&account.id, &normalized_profile_id); let existing = tx.db.custom_world_profile().id().find(&key); let row = CustomWorldProfile { id: key, - user_id: user.id.clone(), + account_id: account.id.clone(), profile_id: normalized_profile_id, payload_json, visibility: existing @@ -497,7 +497,7 @@ fn upsert_custom_world_profile_impl( .unwrap_or(CustomWorldPublicationStatus::Draft), published_at_ms: existing.as_ref().and_then(|row| row.published_at_ms), updated_at_ms: now_ms, - author_display_name: resolve_author_display_name(&author_display_name, &user.display_name), + author_display_name: resolve_author_display_name(&author_display_name, &account.display_name), world_name: metadata.world_name, subtitle: metadata.subtitle, summary_text: metadata.summary_text, @@ -513,13 +513,13 @@ fn upsert_custom_world_profile_impl( fn publish_or_unpublish_custom_world_profile( tx: &TxContext, - user: &User, + account: &Account, profile_id: String, author_display_name: String, publish: bool, ) -> MutationResult { let normalized_profile_id = normalize_required_string(&profile_id); - let key = custom_world_profile_key(&user.id, &normalized_profile_id); + let key = custom_world_profile_key(&account.id, &normalized_profile_id); let Some(existing) = tx.db.custom_world_profile().id().find(&key) else { return MutationResult::error("custom_world_not_found", "未找到目标自定义世界档案"); }; @@ -536,7 +536,7 @@ fn publish_or_unpublish_custom_world_profile( }, published_at_ms: if publish { Some(now_ms) } else { None }, updated_at_ms: now_ms, - author_display_name: resolve_author_display_name(&author_display_name, &user.display_name), + author_display_name: resolve_author_display_name(&author_display_name, &account.display_name), world_name: metadata.world_name, subtitle: metadata.subtitle, summary_text: metadata.summary_text, @@ -555,26 +555,26 @@ fn publish_or_unpublish_custom_world_profile( } fn upsert_save_snapshot(tx: &TxContext, row: SaveSnapshot) { - if tx.db.saved_snapshot_row().user_id().find(&row.user_id).is_some() { - tx.db.saved_snapshot_row().user_id().update(row); + if tx.db.saved_snapshot_row().account_id().find(&row.account_id).is_some() { + tx.db.saved_snapshot_row().account_id().update(row); } else { tx.db.saved_snapshot_row().insert(row); } } fn upsert_runtime_setting(tx: &TxContext, row: RuntimeSetting) { - if tx.db.runtime_setting().user_id().find(&row.user_id).is_some() { - tx.db.runtime_setting().user_id().update(row); + if tx.db.runtime_setting().account_id().find(&row.account_id).is_some() { + tx.db.runtime_setting().account_id().update(row); } else { tx.db.runtime_setting().insert(row); } } -fn upsert_browse_history(tx: &TxContext, row: UserBrowseHistory) { - if tx.db.user_browse_history().id().find(&row.id).is_some() { - tx.db.user_browse_history().id().update(row); +fn upsert_browse_history(tx: &TxContext, row: AccountBrowseHistory) { + if tx.db.account_browse_history().id().find(&row.id).is_some() { + tx.db.account_browse_history().id().update(row); } else { - tx.db.user_browse_history().insert(row); + tx.db.account_browse_history().insert(row); } } @@ -595,8 +595,8 @@ fn upsert_custom_world_session_row(tx: &TxContext, row: CustomWorldSession) { } fn upsert_profile_dashboard_state(tx: &TxContext, row: ProfileDashboardState) { - if tx.db.profile_dashboard_state().user_id().find(&row.user_id).is_some() { - tx.db.profile_dashboard_state().user_id().update(row); + if tx.db.profile_dashboard_state().account_id().find(&row.account_id).is_some() { + tx.db.profile_dashboard_state().account_id().update(row); } else { tx.db.profile_dashboard_state().insert(row); } @@ -618,14 +618,14 @@ fn upsert_profile_wallet_ledger(tx: &TxContext, row: ProfileWalletLedger) { fn sync_profile_dashboard_from_snapshot( tx: &TxContext, - user: &User, + account: &Account, saved_at_ms: u64, game_state_json: &str, ) { let Some(game_state) = parse_json(game_state_json) else { return; }; - let current_state = tx.db.profile_dashboard_state().user_id().find(&user.id); + let current_state = tx.db.profile_dashboard_state().account_id().find(&account.id); let current_wallet_balance = current_state.as_ref().map(|row| row.wallet_balance).unwrap_or(0); let current_total_play_time_ms = current_state .as_ref() @@ -637,8 +637,8 @@ fn sync_profile_dashboard_from_snapshot( upsert_profile_wallet_ledger( tx, ProfileWalletLedger { - id: profile_wallet_ledger_id(&user.id, &source_key), - user_id: user.id.clone(), + id: profile_wallet_ledger_id(&account.id, &source_key), + account_id: account.id.clone(), amount_delta: next_wallet_balance - current_wallet_balance, balance_after: next_wallet_balance, source_type: "snapshot_sync".to_string(), @@ -650,7 +650,7 @@ fn sync_profile_dashboard_from_snapshot( let mut total_play_time_ms = current_total_play_time_ms; if let Some(world_meta) = resolve_snapshot_world_meta(&game_state) { - let current_world_key = profile_played_world_key(&user.id, &world_meta.world_key); + let current_world_key = profile_played_world_key(&account.id, &world_meta.world_key); let current_world = tx.db.profile_played_world().id().find(¤t_world_key); let observed_play_time_ms = read_nested_u64(&game_state, &["runtimeStats", "playTimeMs"]); let incremental_play_time_ms = observed_play_time_ms @@ -661,9 +661,9 @@ fn sync_profile_dashboard_from_snapshot( tx, ProfilePlayedWorld { id: current_world_key, - user_id: user.id.clone(), + account_id: account.id.clone(), world_key: world_meta.world_key, - owner_user_id: world_meta.owner_user_id, + owner_account_id: world_meta.owner_account_id, profile_id: world_meta.profile_id, world_type: world_meta.world_type, world_title: world_meta.world_title, @@ -686,7 +686,7 @@ fn sync_profile_dashboard_from_snapshot( upsert_profile_dashboard_state( tx, ProfileDashboardState { - user_id: user.id.clone(), + account_id: account.id.clone(), wallet_balance: next_wallet_balance, total_play_time_ms, updated_at_ms: saved_at_ms, @@ -696,7 +696,7 @@ fn sync_profile_dashboard_from_snapshot( fn sync_custom_world_profile_from_snapshot( tx: &TxContext, - user: &User, + account: &Account, saved_at_ms: u64, game_state_json: &str, ) { @@ -722,13 +722,13 @@ fn sync_custom_world_profile_from_snapshot( return; }; - let key = custom_world_profile_key(&user.id, &profile_id); + let key = custom_world_profile_key(&account.id, &profile_id); let existing = tx.db.custom_world_profile().id().find(&key); upsert_custom_world_profile_row( tx, CustomWorldProfile { id: key, - user_id: user.id.clone(), + account_id: account.id.clone(), profile_id, payload_json, visibility: existing @@ -737,7 +737,7 @@ fn sync_custom_world_profile_from_snapshot( .unwrap_or(CustomWorldPublicationStatus::Draft), published_at_ms: existing.as_ref().and_then(|row| row.published_at_ms), updated_at_ms: saved_at_ms, - author_display_name: user.display_name.clone(), + author_display_name: account.display_name.clone(), world_name: metadata.world_name, subtitle: metadata.subtitle, summary_text: metadata.summary_text, @@ -752,7 +752,7 @@ fn sync_custom_world_profile_from_snapshot( struct SnapshotWorldMeta { world_key: String, - owner_user_id: Option, + owner_account_id: Option, profile_id: Option, world_type: Option, world_title: String, @@ -772,7 +772,7 @@ fn resolve_snapshot_world_meta(game_state: &Value) -> Option } else { format!("custom:{profile_id}") }, - owner_user_id: None, + owner_account_id: None, profile_id: if profile_id.is_empty() { None } else { Some(profile_id) }, world_type: Some("CUSTOM".to_string()), world_title, @@ -797,7 +797,7 @@ fn resolve_snapshot_world_meta(game_state: &Value) -> Option .unwrap_or_default(); Some(SnapshotWorldMeta { world_key: format!("builtin:{world_type}"), - owner_user_id: None, + owner_account_id: None, profile_id: None, world_type: Some(world_type), world_title, @@ -962,7 +962,7 @@ fn to_profile_wallet_ledger_view(row: ProfileWalletLedger) -> ProfileWalletLedge fn to_profile_played_world_view(row: ProfilePlayedWorld) -> ProfilePlayedWorldView { ProfilePlayedWorldView { world_key: row.world_key, - owner_user_id: row.owner_user_id, + owner_account_id: row.owner_account_id, profile_id: row.profile_id, world_type: row.world_type, world_title: row.world_title, @@ -973,9 +973,9 @@ fn to_profile_played_world_view(row: ProfilePlayedWorld) -> ProfilePlayedWorldVi } } -fn to_platform_browse_history_view(row: UserBrowseHistory) -> PlatformBrowseHistoryView { +fn to_platform_browse_history_view(row: AccountBrowseHistory) -> PlatformBrowseHistoryView { PlatformBrowseHistoryView { - owner_user_id: row.owner_user_id, + owner_account_id: row.owner_account_id, profile_id: row.profile_id, world_name: row.world_name, subtitle: row.subtitle, @@ -989,7 +989,7 @@ fn to_platform_browse_history_view(row: UserBrowseHistory) -> PlatformBrowseHist fn to_custom_world_profile_view(row: CustomWorldProfile) -> CustomWorldProfileView { CustomWorldProfileView { - owner_user_id: row.user_id, + owner_account_id: row.account_id, profile_id: row.profile_id, payload_json: row.payload_json, visibility: row.visibility, @@ -1008,7 +1008,7 @@ fn to_custom_world_profile_view(row: CustomWorldProfile) -> CustomWorldProfileVi fn to_custom_world_gallery_card_view(row: CustomWorldProfile) -> CustomWorldGalleryCardView { CustomWorldGalleryCardView { - owner_user_id: row.user_id, + owner_account_id: row.account_id, profile_id: row.profile_id, visibility: row.visibility, published_at_ms: row.published_at_ms, diff --git a/spacetimedb/src/types.rs b/spacetimedb/src/types.rs index 73f87631..a5df0884 100644 --- a/spacetimedb/src/types.rs +++ b/spacetimedb/src/types.rs @@ -67,7 +67,7 @@ pub struct RequestMeta { #[derive(SpacetimeType, Clone, Debug)] pub struct PlatformBrowseHistoryWriteInput { - pub owner_user_id: String, + pub owner_account_id: String, pub profile_id: String, pub world_name: String, pub subtitle: String, @@ -199,7 +199,7 @@ pub struct ClientAppConfigView { #[derive(SpacetimeType, Clone, Debug)] pub struct AuthStateView { - pub user_id: String, + pub account_id: String, pub identity: Identity, pub display_name: String, pub phone_number_masked: Option, @@ -244,7 +244,7 @@ pub struct ProfileWalletLedgerView { #[derive(SpacetimeType, Clone, Debug)] pub struct ProfilePlayedWorldView { pub world_key: String, - pub owner_user_id: Option, + pub owner_account_id: Option, pub profile_id: Option, pub world_type: Option, pub world_title: String, @@ -256,7 +256,7 @@ pub struct ProfilePlayedWorldView { #[derive(SpacetimeType, Clone, Debug)] pub struct PlatformBrowseHistoryView { - pub owner_user_id: String, + pub owner_account_id: String, pub profile_id: String, pub world_name: String, pub subtitle: String, @@ -269,7 +269,7 @@ pub struct PlatformBrowseHistoryView { #[derive(SpacetimeType, Clone, Debug)] pub struct CustomWorldProfileView { - pub owner_user_id: String, + pub owner_account_id: String, pub profile_id: String, pub payload_json: String, pub visibility: CustomWorldPublicationStatus, @@ -287,7 +287,7 @@ pub struct CustomWorldProfileView { #[derive(SpacetimeType, Clone, Debug)] pub struct CustomWorldGalleryCardView { - pub owner_user_id: String, + pub owner_account_id: String, pub profile_id: String, pub visibility: CustomWorldPublicationStatus, pub published_at_ms: Option, @@ -304,7 +304,7 @@ pub struct CustomWorldGalleryCardView { #[derive(SpacetimeType, Clone, Debug)] pub struct PublishedCustomWorldProfileView { - pub owner_user_id: String, + pub owner_account_id: String, pub profile_id: String, pub payload_json: String, pub visibility: CustomWorldPublicationStatus, @@ -413,9 +413,9 @@ pub struct AppConfig { pub updated_at_ms: u64, } -#[table(accessor = user)] +#[table(accessor = account)] #[derive(Clone, Debug)] -pub struct User { +pub struct Account { #[primary_key] pub id: String, #[unique] @@ -432,13 +432,13 @@ pub struct User { pub updated_at_ms: u64, } -#[table(accessor = auth_identity)] +#[table(accessor = account_identity)] #[derive(Clone, Debug)] -pub struct AuthIdentity { +pub struct AccountIdentity { #[primary_key] pub id: String, #[index(btree)] - pub user_id: String, + pub account_id: String, pub provider: AuthIdentityProvider, pub provider_uid: String, pub provider_union_id: Option, @@ -450,13 +450,13 @@ pub struct AuthIdentity { pub updated_at_ms: u64, } -#[table(accessor = user_session)] +#[table(accessor = account_session)] #[derive(Clone, Debug)] -pub struct UserSession { +pub struct AccountSession { #[primary_key] pub id: String, #[index(btree)] - pub user_id: String, + pub account_id: String, pub refresh_token_hash: String, pub client_type: String, pub user_agent: Option, @@ -475,7 +475,7 @@ pub struct AuthAuditLog { #[auto_inc] pub id: u64, #[index(btree)] - pub user_id: String, + pub account_id: String, pub event_type: String, pub detail: String, pub ip: Option, @@ -524,7 +524,7 @@ pub struct AuthRiskBlock { #[derive(Clone, Debug)] pub struct SaveSnapshot { #[primary_key] - pub user_id: String, + pub account_id: String, pub version: u32, pub saved_at_ms: u64, pub bottom_tab: String, @@ -537,7 +537,7 @@ pub struct SaveSnapshot { #[derive(Clone, Debug)] pub struct RuntimeSetting { #[primary_key] - pub user_id: String, + pub account_id: String, pub music_volume: f32, pub updated_at_ms: u64, } @@ -548,7 +548,7 @@ pub struct CustomWorldProfile { #[primary_key] pub id: String, #[index(btree)] - pub user_id: String, + pub account_id: String, pub profile_id: String, #[index(btree)] pub visibility: CustomWorldPublicationStatus, @@ -572,7 +572,7 @@ pub struct CustomWorldSession { #[primary_key] pub id: String, #[index(btree)] - pub user_id: String, + pub account_id: String, pub session_id: String, pub payload_json: String, pub created_at_ms: u64, @@ -583,7 +583,7 @@ pub struct CustomWorldSession { #[derive(Clone, Debug)] pub struct ProfileDashboardState { #[primary_key] - pub user_id: String, + pub account_id: String, pub wallet_balance: i64, pub total_play_time_ms: u64, pub updated_at_ms: u64, @@ -595,7 +595,7 @@ pub struct ProfileWalletLedger { #[primary_key] pub id: String, #[index(btree)] - pub user_id: String, + pub account_id: String, pub amount_delta: i64, pub balance_after: i64, pub source_type: String, @@ -609,9 +609,9 @@ pub struct ProfilePlayedWorld { #[primary_key] pub id: String, #[index(btree)] - pub user_id: String, + pub account_id: String, pub world_key: String, - pub owner_user_id: Option, + pub owner_account_id: Option, pub profile_id: Option, pub world_type: Option, pub world_title: String, @@ -621,14 +621,14 @@ pub struct ProfilePlayedWorld { pub last_observed_play_time_ms: u64, } -#[table(accessor = user_browse_history)] +#[table(accessor = account_browse_history)] #[derive(Clone, Debug)] -pub struct UserBrowseHistory { +pub struct AccountBrowseHistory { #[primary_key] pub id: String, #[index(btree)] - pub user_id: String, - pub owner_user_id: String, + pub account_id: String, + pub owner_account_id: String, pub profile_id: String, pub world_name: String, pub subtitle: String, diff --git a/src/components/game-shell/PlatformHomeView.tsx b/src/components/game-shell/PlatformHomeView.tsx index 3b101189..43a7c4b9 100644 --- a/src/components/game-shell/PlatformHomeView.tsx +++ b/src/components/game-shell/PlatformHomeView.tsx @@ -509,7 +509,7 @@ export function PlatformHomeView({
{featuredShelf.map((entry: CustomWorldGalleryCard) => ( {latestEntries.map((entry: CustomWorldGalleryCard) => ( ) => ( {historyEntries.map((entry) => (