From 166544fae619cac9eb71c43b5930f693dfaaf2b0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 13 May 2026 23:24:32 +0800 Subject: [PATCH] Persist auth store into formal tables --- .hermes/shared-memory/decision-log.md | 10 +- ...OT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md | 6 +- ...FORMAL_TABLE_RECOVERY_STAGE3_2026-04-24.md | 12 +- ...ACETIMEDB_TABLE_SPLIT_STAGE2_2026-04-24.md | 3 +- docs/technical/SPACETIMEDB_TABLE_CATALOG.md | 15 +- server-rs/crates/api-server/src/state.rs | 17 +- server-rs/crates/spacetime-client/src/auth.rs | 23 +++ .../auth_store_projection_meta_table.rs | 162 ++++++++++++++++++ .../auth_store_projection_meta_type.rs | 52 ++++++ ...port_auth_store_snapshot_json_procedure.rs | 59 +++++++ .../src/module_bindings/mod.rs | 32 +++- .../spacetime-module/src/auth/procedures.rs | 85 ++++++++- .../spacetime-module/src/auth/tables.rs | 7 + .../crates/spacetime-module/src/migration.rs | 1 + 14 files changed, 452 insertions(+), 32 deletions(-) create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/auth_store_projection_meta_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/auth_store_projection_meta_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/import_auth_store_snapshot_json_procedure.rs diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index b912bfb4..b0a911cb 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-05-13 认证运行期同步直接导入正式认证表 + +- 背景:`auth_store_snapshot` 是 Stage 1 整包快照过渡表,主键固定 `default`,会让所有用户状态集中在一条 `snapshot_json` 中;Stage 2/3 已有 `user_account/auth_identity/refresh_session` 正式认证表,继续刷新 `default` 容易让运行时真相和表拆分目标混在一起。 +- 决策:运行期认证变更继续由 `module-auth` 生成一致内存快照,但 `api-server` 改为调用 `import_auth_store_snapshot_json` 直接覆盖导入 `user_account/auth_identity/refresh_session`;`auth_store_projection_meta/default` 只记录正式认证表最近一次导入时间;`upsert_auth_store_snapshot` 与 `import_auth_store_snapshot` 仅保留为旧库迁移和兜底入口。 +- 影响范围:`spacetime-module` auth procedures/tables、`spacetime-client` auth facade/bindings、`api-server` 认证同步和启动恢复、SpacetimeDB 表目录与认证 Stage 3 文档。 +- 验证方式:执行 `npm run spacetime:generate -- --rust-only`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、认证相关定向测试和 `npm run check:encoding`。 +- 关联文档:`docs/technical/AUTH_SPACETIMEDB_FORMAL_TABLE_RECOVERY_STAGE3_2026-04-24.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。 + ## 2026-05-13 微信小程序支付以后端通知为唯一入账事实 - 背景:“我的”账户充值需要接入微信小程序支付,同时保留本地 / H5 mock 支付联调能力。 @@ -408,4 +416,4 @@ - 决策:埋点原始事实进入 `tracking_event`,聚合投影进入 `tracking_daily_stat`;个人任务配置/进度/领奖/钱包分别进入 `profile_task_config`、`profile_task_progress`、`profile_task_reward_claim`、`profile_wallet_ledger`;首版个人任务 scope 仅支持 `user`。 - 影响范围:用户侧任务中心、后台任务配置、运营查询、埋点查询、钱包流水。 - 验证方式:非 `user` scope 的个人任务配置应被 API 和领域构造层拒绝;任务查询与埋点查询分别放在 `docs/operations/` 和 `docs/tracking/`。 -- 关联文档:`PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`、`RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md`、`ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`。 +- 关联文档:`PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md`、`RUNTIME_PROFILE_TASK_SCOPE_2026-05-04.md`、`ANALYTICS_DATE_DIMENSION_IMPLEMENTATION_2026-05-04.md`。 \ No newline at end of file diff --git a/docs/technical/AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md b/docs/technical/AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md index 89d9e231..e461cde2 100644 --- a/docs/technical/AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md +++ b/docs/technical/AUTH_SNAPSHOT_AND_MATCH3D_LOCAL_DEV_FIX_2026-05-01.md @@ -40,12 +40,12 @@ HTTP status server error (503 Service Unavailable) ### 3.1 认证快照同步改为非阻断 -`AppState::sync_auth_store_snapshot_to_spacetime` 保持导出本地快照、写入 SpacetimeDB、导入正式表的顺序,但当远端写入或导入失败时只写 warn 日志并返回 `Ok(())`。 +`AppState::sync_auth_store_snapshot_to_spacetime` 保持先导出本地认证快照,但运行期会直接调用 `import_auth_store_snapshot_json` 覆盖导入 SpacetimeDB 正式认证表,不再刷新 `auth_store_snapshot/default`;当远端导入失败时只写 warn 日志并返回 `Ok(())`。 设计边界: 1. 当前认证请求的即时真相源是本地 `auth_store`。 -2. SpacetimeDB 认证快照用于跨进程恢复和正式表投影。 +2. SpacetimeDB 正式认证表用于跨进程恢复;`auth_store_snapshot/default` 只保留为历史迁移和兜底恢复记录。 3. 远端库挂起或网络异常只降级远端恢复能力,不回滚已经成功的登录、刷新、退出和资料更新。 ### 3.2 Vite 补齐创作接口代理 @@ -98,7 +98,7 @@ npm run dev:web 1. `GET http://127.0.0.1:3000/api/auth/login-options` 返回 `["phone","password"]`。 2. `GET http://127.0.0.1:3000/api/runtime/match3d/gallery` 返回 `{"items":[]}`,不再返回 SpacetimeDB 503。 3. 未登录请求 `POST http://127.0.0.1:3000/api/creation/match3d/sessions` 返回 `401`,说明同源请求已进入 Rust 鉴权层,不再被 Vite `404`。 -4. 隔离端口指向挂起的远端库并使用 mock 短信时,手机号验证码登录返回 `200` 和 token;日志只记录“认证快照写入 SpacetimeDB 失败,当前认证流程继续”。 +4. 隔离端口指向挂起的远端库并使用 mock 短信时,手机号验证码登录返回 `200` 和 token;日志只记录“认证快照导入 SpacetimeDB 正式表失败,当前认证流程继续”。 ## 6. 后续 diff --git a/docs/technical/AUTH_SPACETIMEDB_FORMAL_TABLE_RECOVERY_STAGE3_2026-04-24.md b/docs/technical/AUTH_SPACETIMEDB_FORMAL_TABLE_RECOVERY_STAGE3_2026-04-24.md index f41a7127..769a4164 100644 --- a/docs/technical/AUTH_SPACETIMEDB_FORMAL_TABLE_RECOVERY_STAGE3_2026-04-24.md +++ b/docs/technical/AUTH_SPACETIMEDB_FORMAL_TABLE_RECOVERY_STAGE3_2026-04-24.md @@ -6,8 +6,9 @@ 落地口径: - `user_account`、`auth_identity`、`refresh_session` 作为 SpacetimeDB 中的正式认证持久化表。 +- `auth_store_projection_meta` 只记录正式认证表最近一次由认证快照导入的时间,不保存用户快照内容。 - API 启动时优先从正式表导出兼容 `module-auth` 的认证快照,再恢复到内存认证服务。 -- 运行期认证变更仍先复用现有 `module-auth` 逻辑生成一致快照,随后同步快照并导入正式表,保证正式表与快照一致。 +- 运行期认证变更仍先复用现有 `module-auth` 逻辑生成一致快照,随后调用 `import_auth_store_snapshot_json` 直接覆盖导入正式表;不再继续刷新 `auth_store_snapshot/default`。 - 本阶段不重写登录、刷新、登出内部业务规则,避免在 JWT、refresh rotation、微信绑定合并等复杂语义中引入行为漂移。 ## 2. 非目标 @@ -21,7 +22,7 @@ ### 3.1 启动恢复 1. API 调用 `export_auth_store_snapshot_from_tables`。 -2. 若正式表已有用户、身份或会话数据,则返回兼容 `module-auth` 的 JSON 快照。 +2. 若正式表已有用户、身份或会话数据,则返回兼容 `module-auth` 的 JSON 快照,并带上 `auth_store_projection_meta/default.updated_at`。 3. API 用 `InMemoryAuthStore::from_snapshot_json` 恢复认证服务。 4. 若正式表为空或调用失败,则回退到 Stage 1 的 `auth_store_snapshot`。 5. 若 Stage 1 也不可用,则回退本地 JSON 热修复文件。 @@ -29,9 +30,10 @@ ### 3.2 运行期同步 1. 登录、刷新、登出等路径继续调用当前内存认证服务。 -2. 每次认证状态变更后调用 `upsert_auth_store_snapshot`。 -3. 快照写入成功后调用 `import_auth_store_snapshot`,覆盖导入正式表。 -4. 导入失败时返回错误,避免用户误以为状态已经持久化。 +2. 每次认证状态变更后导出当前内存认证快照 JSON。 +3. API 调用 `import_auth_store_snapshot_json`,在同一 SpacetimeDB transaction 中清空并重建 `user_account/auth_identity/refresh_session`,同时更新 `auth_store_projection_meta/default.updated_at`。 +4. `upsert_auth_store_snapshot` 和 `import_auth_store_snapshot` 保留为旧库迁移入口,只服务 `auth_store_snapshot/default` 到正式认证表的历史导入,不作为运行期同步路径。 +5. 远端导入失败只记录 warn 并继续当前认证响应,避免远端库挂起时回滚已经成功的登录、刷新、退出和资料更新。 ## 4. 数据重建规则 diff --git a/docs/technical/AUTH_SPACETIMEDB_TABLE_SPLIT_STAGE2_2026-04-24.md b/docs/technical/AUTH_SPACETIMEDB_TABLE_SPLIT_STAGE2_2026-04-24.md index fe958971..6d77c38d 100644 --- a/docs/technical/AUTH_SPACETIMEDB_TABLE_SPLIT_STAGE2_2026-04-24.md +++ b/docs/technical/AUTH_SPACETIMEDB_TABLE_SPLIT_STAGE2_2026-04-24.md @@ -23,7 +23,7 @@ Stage 1 已把 Rust 鉴权快照同步到 SpacetimeDB 的 `auth_store_snapshot` 1. `POST /api/auth/refresh` 改写 `refresh_session` 表。 2. 登录成功写 `user_account/auth_identity/refresh_session`。 3. `logout/logout-all/revoke-session` 改写细粒度表。 -4. `auth_store_snapshot` 退化为迁移备份。 +4. `auth_store_snapshot` 退化为迁移备份;运行期若仍复用内存认证快照,也应通过 `import_auth_store_snapshot_json` 直接导入正式认证表,不再刷新 `auth_store_snapshot/default`。 ## 3. 表设计落地口径 @@ -94,4 +94,3 @@ Stage 1 已把 Rust 鉴权快照同步到 SpacetimeDB 的 `auth_store_snapshot` 2. Rust bindings 已刷新。 3. `spacetime-client` 暴露导入 procedure facade。 4. `api-server/spacetime-client/module-auth` 定向检查通过。 - diff --git a/docs/technical/SPACETIMEDB_TABLE_CATALOG.md b/docs/technical/SPACETIMEDB_TABLE_CATALOG.md index bf472de7..301e1d54 100644 --- a/docs/technical/SPACETIMEDB_TABLE_CATALOG.md +++ b/docs/technical/SPACETIMEDB_TABLE_CATALOG.md @@ -23,7 +23,7 @@ spacetime sql "SELECT * FROM custom_world_gallery_entry" | 领域 | 表 | | --- | --- | | 运维迁移 | `database_migration_operator`, `database_migration_import_chunk` | -| 认证 | `auth_store_snapshot`, `user_account`, `auth_identity`, `refresh_session` | +| 认证 | `auth_store_snapshot`, `auth_store_projection_meta`, `user_account`, `auth_identity`, `refresh_session` | | 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `analytics_date_dimension`, `tracking_event`, `tracking_daily_stat`, `profile_task_config`, `profile_task_progress`, `profile_task_reward_claim`, `profile_redeem_code`, `profile_redeem_code_usage`, `profile_invite_code`, `profile_referral_relation`, `profile_played_world`, `profile_membership`, `profile_recharge_order`, `profile_feedback_submission`, `profile_save_archive` | | RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` | | 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` | @@ -60,7 +60,7 @@ SELECT * FROM database_migration_operator WHERE operator_identity = '' ### `auth_store_snapshot` -- 作用:保存旧内存认证仓储的整份 JSON 快照,用于迁移和恢复;后续正式表拆分后仍可作为导入/导出桥。 +- 作用:保存旧内存认证仓储的整份 JSON 快照,用于历史迁移和兜底恢复;运行期认证同步不再继续刷新 `snapshot_id = 'default'`,而是直接导入正式认证表。 - 结构:`snapshot_id PK: String`, `snapshot_json: String`, `updated_at: Timestamp`。 - 索引:主键 `snapshot_id`。 @@ -69,6 +69,17 @@ SELECT * FROM auth_store_snapshot; SELECT * FROM auth_store_snapshot WHERE snapshot_id = 'default'; ``` +### `auth_store_projection_meta` + +- 作用:记录正式认证表最近一次由认证快照导入的时间,避免启动恢复时旧 `auth_store_snapshot/default` 因带有时间戳而覆盖较新的正式认证表。 +- 结构:`meta_id PK: String`, `updated_at: Timestamp`。 +- 索引:主键 `meta_id`。 + +```sql +SELECT * FROM auth_store_projection_meta; +SELECT * FROM auth_store_projection_meta WHERE meta_id = 'default'; +``` + ### `user_account` - 作用:用户账号主表,保存用户名、公开百梦号、手机号掩码、登录方式、密码登录开关、token 版本和默认不前端展示的运营标签。 diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index ad730042..29350028 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -339,23 +339,14 @@ impl AppState { OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000, ) .map_err(|_| SpacetimeClientError::Runtime("认证快照更新时间超出 i64 范围".to_string()))?; - // 本地 auth_store 是当前认证请求的即时真相源;SpacetimeDB 快照用于跨进程恢复。 + // 本地 auth_store 是当前认证请求的即时真相源;SpacetimeDB 正式认证表用于跨进程恢复。 // 远端数据库挂起或网络异常时,只降级远端恢复能力,不能让已成功的登录/刷新/退出回滚为失败。 #[cfg(not(test))] if let Err(error) = self .spacetime_client - .upsert_auth_store_snapshot(snapshot_json, updated_at_micros) + .import_auth_store_snapshot_json(snapshot_json, updated_at_micros) .await { - warn!( - error = %error, - "认证快照写入 SpacetimeDB 失败,当前认证流程继续" - ); - return Ok(()); - } - // 写入快照后尝试拆入正式认证表;失败只影响远端表恢复,不阻断当前认证响应。 - #[cfg(not(test))] - if let Err(error) = self.spacetime_client.import_auth_store_snapshot().await { warn!( error = %error, "认证快照导入 SpacetimeDB 正式表失败,当前认证流程继续" @@ -859,8 +850,8 @@ fn select_auth_store_restore_candidate( fn auth_store_restore_source_priority(source: AuthStoreRestoreSource) -> u8 { match source { - AuthStoreRestoreSource::SpacetimeSnapshot => 3, - AuthStoreRestoreSource::SpacetimeTables => 2, + AuthStoreRestoreSource::SpacetimeTables => 3, + AuthStoreRestoreSource::SpacetimeSnapshot => 2, AuthStoreRestoreSource::LocalFile => 1, } } diff --git a/server-rs/crates/spacetime-client/src/auth.rs b/server-rs/crates/spacetime-client/src/auth.rs index 5b380948..438a2d69 100644 --- a/server-rs/crates/spacetime-client/src/auth.rs +++ b/server-rs/crates/spacetime-client/src/auth.rs @@ -57,6 +57,29 @@ impl SpacetimeClient { .await } + pub async fn import_auth_store_snapshot_json( + &self, + snapshot_json: String, + updated_at_micros: i64, + ) -> Result { + let procedure_input = AuthStoreSnapshotUpsertInput { + snapshot_json, + updated_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .import_auth_store_snapshot_json_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_auth_store_snapshot_import_procedure_result); + send_once(&sender, mapped); + }); + }) + .await + } + pub async fn import_auth_store_snapshot( &self, ) -> Result { diff --git a/server-rs/crates/spacetime-client/src/module_bindings/auth_store_projection_meta_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/auth_store_projection_meta_table.rs new file mode 100644 index 00000000..d0bbb6f4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/auth_store_projection_meta_table.rs @@ -0,0 +1,162 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::auth_store_projection_meta_type::AuthStoreProjectionMeta; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `auth_store_projection_meta`. +/// +/// Obtain a handle from the [`AuthStoreProjectionMetaTableAccess::auth_store_projection_meta`] method on [`super::RemoteTables`], +/// like `ctx.db.auth_store_projection_meta()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.auth_store_projection_meta().on_insert(...)`. +pub struct AuthStoreProjectionMetaTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `auth_store_projection_meta`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait AuthStoreProjectionMetaTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`AuthStoreProjectionMetaTableHandle`], which mediates access to the table `auth_store_projection_meta`. + fn auth_store_projection_meta(&self) -> AuthStoreProjectionMetaTableHandle<'_>; +} + +impl AuthStoreProjectionMetaTableAccess for super::RemoteTables { + fn auth_store_projection_meta(&self) -> AuthStoreProjectionMetaTableHandle<'_> { + AuthStoreProjectionMetaTableHandle { + imp: self + .imp + .get_table::("auth_store_projection_meta"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct AuthStoreProjectionMetaInsertCallbackId(__sdk::CallbackId); +pub struct AuthStoreProjectionMetaDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for AuthStoreProjectionMetaTableHandle<'ctx> { + type Row = AuthStoreProjectionMeta; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = AuthStoreProjectionMetaInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> AuthStoreProjectionMetaInsertCallbackId { + AuthStoreProjectionMetaInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: AuthStoreProjectionMetaInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = AuthStoreProjectionMetaDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> AuthStoreProjectionMetaDeleteCallbackId { + AuthStoreProjectionMetaDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: AuthStoreProjectionMetaDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct AuthStoreProjectionMetaUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for AuthStoreProjectionMetaTableHandle<'ctx> { + type UpdateCallbackId = AuthStoreProjectionMetaUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> AuthStoreProjectionMetaUpdateCallbackId { + AuthStoreProjectionMetaUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: AuthStoreProjectionMetaUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `meta_id` unique index on the table `auth_store_projection_meta`, +/// which allows point queries on the field of the same name +/// via the [`AuthStoreProjectionMetaMetaIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.auth_store_projection_meta().meta_id().find(...)`. +pub struct AuthStoreProjectionMetaMetaIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> AuthStoreProjectionMetaTableHandle<'ctx> { + /// Get a handle on the `meta_id` unique index on the table `auth_store_projection_meta`. + pub fn meta_id(&self) -> AuthStoreProjectionMetaMetaIdUnique<'ctx> { + AuthStoreProjectionMetaMetaIdUnique { + imp: self.imp.get_unique_constraint::("meta_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> AuthStoreProjectionMetaMetaIdUnique<'ctx> { + /// Find the subscribed row whose `meta_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("auth_store_projection_meta"); + _table.add_unique_constraint::("meta_id", |row| &row.meta_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `AuthStoreProjectionMeta`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait auth_store_projection_metaQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `AuthStoreProjectionMeta`. + fn auth_store_projection_meta(&self) -> __sdk::__query_builder::Table; +} + +impl auth_store_projection_metaQueryTableAccess for __sdk::QueryTableAccessor { + fn auth_store_projection_meta(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("auth_store_projection_meta") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/auth_store_projection_meta_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/auth_store_projection_meta_type.rs new file mode 100644 index 00000000..309dceff --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/auth_store_projection_meta_type.rs @@ -0,0 +1,52 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct AuthStoreProjectionMeta { + pub meta_id: String, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for AuthStoreProjectionMeta { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `AuthStoreProjectionMeta`. +/// +/// Provides typed access to columns for query building. +pub struct AuthStoreProjectionMetaCols { + pub meta_id: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for AuthStoreProjectionMeta { + type Cols = AuthStoreProjectionMetaCols; + fn cols(table_name: &'static str) -> Self::Cols { + AuthStoreProjectionMetaCols { + meta_id: __sdk::__query_builder::Col::new(table_name, "meta_id"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `AuthStoreProjectionMeta`. +/// +/// Provides typed access to indexed columns for query building. +pub struct AuthStoreProjectionMetaIxCols { + pub meta_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for AuthStoreProjectionMeta { + type IxCols = AuthStoreProjectionMetaIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + AuthStoreProjectionMetaIxCols { + meta_id: __sdk::__query_builder::IxCol::new(table_name, "meta_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for AuthStoreProjectionMeta {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/import_auth_store_snapshot_json_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/import_auth_store_snapshot_json_procedure.rs new file mode 100644 index 00000000..3cd6d71f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/import_auth_store_snapshot_json_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::auth_store_snapshot_import_procedure_result_type::AuthStoreSnapshotImportProcedureResult; +use super::auth_store_snapshot_upsert_input_type::AuthStoreSnapshotUpsertInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ImportAuthStoreSnapshotJsonArgs { + pub input: AuthStoreSnapshotUpsertInput, +} + +impl __sdk::InModule for ImportAuthStoreSnapshotJsonArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `import_auth_store_snapshot_json`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait import_auth_store_snapshot_json { + fn import_auth_store_snapshot_json(&self, input: AuthStoreSnapshotUpsertInput) { + self.import_auth_store_snapshot_json_then(input, |_, _| {}); + } + + fn import_auth_store_snapshot_json_then( + &self, + input: AuthStoreSnapshotUpsertInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl import_auth_store_snapshot_json for super::RemoteProcedures { + fn import_auth_store_snapshot_json_then( + &self, + input: AuthStoreSnapshotUpsertInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, AuthStoreSnapshotImportProcedureResult>( + "import_auth_store_snapshot_json", + ImportAuthStoreSnapshotJsonArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs index 13fa85bd..a4006654 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.1.0 (commit 6981f48b4bc1a71c8dd9bdfe5a2c343f6370243d). +// This was generated using spacetimedb cli version 2.2.0 (commit eb11e2f5c41dce6979715ad407996270d61329f6). #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; @@ -82,6 +82,8 @@ pub mod asset_object_upsert_snapshot_type; pub mod attach_ai_result_reference_and_return_procedure; pub mod auth_identity_table; pub mod auth_identity_type; +pub mod auth_store_projection_meta_table; +pub mod auth_store_projection_meta_type; pub mod auth_store_snapshot_import_procedure_result_type; pub mod auth_store_snapshot_import_record_type; pub mod auth_store_snapshot_procedure_result_type; @@ -338,6 +340,7 @@ pub mod grant_inventory_item_input_type; pub mod grant_new_user_registration_wallet_reward_procedure; pub mod grant_player_progression_experience_and_return_procedure; pub mod grant_player_progression_experience_reducer; +pub mod import_auth_store_snapshot_json_procedure; pub mod import_auth_store_snapshot_procedure; pub mod import_database_migration_from_chunks_procedure; pub mod import_database_migration_from_file_procedure; @@ -894,6 +897,8 @@ pub use asset_object_upsert_snapshot_type::AssetObjectUpsertSnapshot; pub use attach_ai_result_reference_and_return_procedure::attach_ai_result_reference_and_return; pub use auth_identity_table::*; pub use auth_identity_type::AuthIdentity; +pub use auth_store_projection_meta_table::*; +pub use auth_store_projection_meta_type::AuthStoreProjectionMeta; pub use auth_store_snapshot_import_procedure_result_type::AuthStoreSnapshotImportProcedureResult; pub use auth_store_snapshot_import_record_type::AuthStoreSnapshotImportRecord; pub use auth_store_snapshot_procedure_result_type::AuthStoreSnapshotProcedureResult; @@ -1150,6 +1155,7 @@ pub use grant_inventory_item_input_type::GrantInventoryItemInput; pub use grant_new_user_registration_wallet_reward_procedure::grant_new_user_registration_wallet_reward; pub use grant_player_progression_experience_and_return_procedure::grant_player_progression_experience_and_return; pub use grant_player_progression_experience_reducer::grant_player_progression_experience; +pub use import_auth_store_snapshot_json_procedure::import_auth_store_snapshot_json; pub use import_auth_store_snapshot_procedure::import_auth_store_snapshot; pub use import_database_migration_from_chunks_procedure::import_database_migration_from_chunks; pub use import_database_migration_from_file_procedure::import_database_migration_from_file; @@ -1912,6 +1918,7 @@ pub struct DbUpdate { asset_event: __sdk::TableUpdate, asset_object: __sdk::TableUpdate, auth_identity: __sdk::TableUpdate, + auth_store_projection_meta: __sdk::TableUpdate, auth_store_snapshot: __sdk::TableUpdate, battle_state: __sdk::TableUpdate, big_fish_agent_message: __sdk::TableUpdate, @@ -2020,6 +2027,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "auth_identity" => db_update .auth_identity .append(auth_identity_table::parse_table_update(table_update)?), + "auth_store_projection_meta" => db_update.auth_store_projection_meta.append( + auth_store_projection_meta_table::parse_table_update(table_update)?, + ), "auth_store_snapshot" => db_update .auth_store_snapshot .append(auth_store_snapshot_table::parse_table_update(table_update)?), @@ -2295,6 +2305,12 @@ impl __sdk::DbUpdate for DbUpdate { diff.auth_identity = cache .apply_diff_to_table::("auth_identity", &self.auth_identity) .with_updates_by_pk(|row| &row.identity_id); + diff.auth_store_projection_meta = cache + .apply_diff_to_table::( + "auth_store_projection_meta", + &self.auth_store_projection_meta, + ) + .with_updates_by_pk(|row| &row.meta_id); diff.auth_store_snapshot = cache .apply_diff_to_table::( "auth_store_snapshot", @@ -2695,6 +2711,9 @@ impl __sdk::DbUpdate for DbUpdate { "auth_identity" => db_update .auth_identity .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "auth_store_projection_meta" => db_update + .auth_store_projection_meta + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "auth_store_snapshot" => db_update .auth_store_snapshot .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -2948,6 +2967,9 @@ impl __sdk::DbUpdate for DbUpdate { "auth_identity" => db_update .auth_identity .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "auth_store_projection_meta" => db_update + .auth_store_projection_meta + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "auth_store_snapshot" => db_update .auth_store_snapshot .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3183,6 +3205,7 @@ pub struct AppliedDiff<'r> { asset_event: __sdk::TableAppliedDiff<'r, AssetEvent>, asset_object: __sdk::TableAppliedDiff<'r, AssetObject>, auth_identity: __sdk::TableAppliedDiff<'r, AuthIdentity>, + auth_store_projection_meta: __sdk::TableAppliedDiff<'r, AuthStoreProjectionMeta>, auth_store_snapshot: __sdk::TableAppliedDiff<'r, AuthStoreSnapshot>, battle_state: __sdk::TableAppliedDiff<'r, BattleState>, big_fish_agent_message: __sdk::TableAppliedDiff<'r, BigFishAgentMessage>, @@ -3309,6 +3332,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.auth_identity, event, ); + callbacks.invoke_table_row_callbacks::( + "auth_store_projection_meta", + &self.auth_store_projection_meta, + event, + ); callbacks.invoke_table_row_callbacks::( "auth_store_snapshot", &self.auth_store_snapshot, @@ -4317,6 +4345,7 @@ impl __sdk::SpacetimeModule for RemoteModule { asset_event_table::register_table(client_cache); asset_object_table::register_table(client_cache); auth_identity_table::register_table(client_cache); + auth_store_projection_meta_table::register_table(client_cache); auth_store_snapshot_table::register_table(client_cache); battle_state_table::register_table(client_cache); big_fish_agent_message_table::register_table(client_cache); @@ -4399,6 +4428,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "asset_event", "asset_object", "auth_identity", + "auth_store_projection_meta", "auth_store_snapshot", "battle_state", "big_fish_agent_message", diff --git a/server-rs/crates/spacetime-module/src/auth/procedures.rs b/server-rs/crates/spacetime-module/src/auth/procedures.rs index 3c9a37e6..7b9ee01d 100644 --- a/server-rs/crates/spacetime-module/src/auth/procedures.rs +++ b/server-rs/crates/spacetime-module/src/auth/procedures.rs @@ -7,12 +7,14 @@ use super::{ sanitize_identity_component, }, tables::{ - AuthIdentity, AuthStoreSnapshot, RefreshSession, UserAccount, auth_identity, - auth_store_snapshot, refresh_session, user_account, + AuthIdentity, AuthStoreProjectionMeta, AuthStoreSnapshot, RefreshSession, UserAccount, + auth_identity, auth_store_projection_meta, auth_store_snapshot, refresh_session, + user_account, }, }; const AUTH_STORE_SNAPSHOT_ID: &str = "default"; +const AUTH_STORE_PROJECTION_META_ID: &str = "default"; #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct AuthStoreSnapshotRecord { @@ -70,7 +72,7 @@ pub fn get_auth_store_snapshot(ctx: &mut ProcedureContext) -> AuthStoreSnapshotP } } -// Axum 每次鉴权仓储变更后覆盖写入整份快照,后续拆表阶段再替换为细粒度 reducer。 +// 历史迁移入口:覆盖写入整份快照,供旧库从 `auth_store_snapshot/default` 导入正式表。 #[spacetimedb::procedure] pub fn upsert_auth_store_snapshot( ctx: &mut ProcedureContext, @@ -90,6 +92,26 @@ pub fn upsert_auth_store_snapshot( } } +// Axum 运行期认证变更直接导入正式认证表,不再继续刷新 `auth_store_snapshot/default`。 +#[spacetimedb::procedure] +pub fn import_auth_store_snapshot_json( + ctx: &mut ProcedureContext, + input: AuthStoreSnapshotUpsertInput, +) -> AuthStoreSnapshotImportProcedureResult { + match ctx.try_with_tx(|tx| import_auth_store_snapshot_json_tx(tx, input.clone())) { + Ok(record) => AuthStoreSnapshotImportProcedureResult { + ok: true, + record: Some(record), + error_message: None, + }, + Err(message) => AuthStoreSnapshotImportProcedureResult { + ok: false, + record: None, + error_message: Some(message), + }, + } +} + #[spacetimedb::procedure] pub fn import_auth_store_snapshot( ctx: &mut ProcedureContext, @@ -191,10 +213,35 @@ fn import_auth_store_snapshot_tx( .snapshot_id() .find(&AUTH_STORE_SNAPSHOT_ID.to_string()) .ok_or_else(|| "认证快照不存在,无法导入正式表".to_string())?; - let parsed = serde_json::from_str::(&snapshot.snapshot_json) + + import_auth_store_snapshot_json_value_tx( + ctx, + &snapshot.snapshot_json, + snapshot.updated_at.to_micros_since_unix_epoch(), + ) +} + +fn import_auth_store_snapshot_json_tx( + ctx: &ReducerContext, + input: AuthStoreSnapshotUpsertInput, +) -> Result { + import_auth_store_snapshot_json_value_tx(ctx, &input.snapshot_json, input.updated_at_micros) +} + +fn import_auth_store_snapshot_json_value_tx( + ctx: &ReducerContext, + snapshot_json: &str, + updated_at_micros: i64, +) -> Result { + let snapshot_json = snapshot_json.trim(); + if snapshot_json.is_empty() { + return Err("认证快照 JSON 不能为空".to_string()); + } + let parsed = serde_json::from_str::(snapshot_json) .map_err(|error| format!("认证快照 JSON 解析失败:{error}"))?; clear_auth_target_tables(ctx); + upsert_auth_projection_meta(ctx, updated_at_micros); let mut imported_user_count = 0_u32; let mut imported_identity_count = 0_u32; @@ -293,6 +340,12 @@ fn export_auth_store_snapshot_from_tables_tx( updated_at_micros: None, }); } + let updated_at_micros = ctx + .db + .auth_store_projection_meta() + .meta_id() + .find(&AUTH_STORE_PROJECTION_META_ID.to_string()) + .map(|row| row.updated_at.to_micros_since_unix_epoch()); let mut phone_identity_by_user_id = std::collections::HashMap::new(); let mut phone_to_user_id = std::collections::HashMap::new(); @@ -407,7 +460,7 @@ fn export_auth_store_snapshot_from_tables_tx( Ok(AuthStoreSnapshotRecord { snapshot_json: Some(snapshot_json), - updated_at_micros: None, + updated_at_micros, }) } @@ -428,3 +481,25 @@ fn clear_auth_target_tables(ctx: &ReducerContext) { ctx.db.user_account().user_id().delete(&row.user_id); } } + +fn upsert_auth_projection_meta(ctx: &ReducerContext, updated_at_micros: i64) { + let meta_id = AUTH_STORE_PROJECTION_META_ID.to_string(); + if ctx + .db + .auth_store_projection_meta() + .meta_id() + .find(&meta_id) + .is_some() + { + ctx.db + .auth_store_projection_meta() + .meta_id() + .delete(&meta_id); + } + ctx.db + .auth_store_projection_meta() + .insert(AuthStoreProjectionMeta { + meta_id, + updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros), + }); +} diff --git a/server-rs/crates/spacetime-module/src/auth/tables.rs b/server-rs/crates/spacetime-module/src/auth/tables.rs index 9f12e29e..c2154cc5 100644 --- a/server-rs/crates/spacetime-module/src/auth/tables.rs +++ b/server-rs/crates/spacetime-module/src/auth/tables.rs @@ -8,6 +8,13 @@ pub struct AuthStoreSnapshot { pub(crate) updated_at: Timestamp, } +#[spacetimedb::table(accessor = auth_store_projection_meta)] +pub struct AuthStoreProjectionMeta { + #[primary_key] + pub(crate) meta_id: String, + pub(crate) updated_at: Timestamp, +} + #[spacetimedb::table( accessor = user_account, index(accessor = by_user_account_username, btree(columns = [username])), diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index 0447d739..70a95422 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -158,6 +158,7 @@ macro_rules! migration_tables { $macro_name! { $($arg,)* auth_store_snapshot, + auth_store_projection_meta, user_account, auth_identity, refresh_session,