diff --git a/server-rs/crates/api-server/src/admin_recharge.rs b/server-rs/crates/api-server/src/admin_recharge.rs index 88b30e8f2..1d54a189f 100644 --- a/server-rs/crates/api-server/src/admin_recharge.rs +++ b/server-rs/crates/api-server/src/admin_recharge.rs @@ -162,10 +162,9 @@ pub async fn admin_get_user_detail( fetch_admin_user_cumulative_recharged_cents(&state, &user.id).await; let membership_record = state .spacetime_client() - .get_profile_recharge_center(user.id.clone()) + .get_profile_membership_snapshot(user.id.clone()) .await - .map_err(|error| spacetime_error_response(&request_context, error))? - .membership; + .map_err(|error| spacetime_error_response(&request_context, error))?; Ok(json_success_body( Some(&request_context), diff --git a/server-rs/crates/module-runtime/src/commands.rs b/server-rs/crates/module-runtime/src/commands.rs index 702bc8398..31e93298f 100644 --- a/server-rs/crates/module-runtime/src/commands.rs +++ b/server-rs/crates/module-runtime/src/commands.rs @@ -345,6 +345,13 @@ pub fn build_runtime_profile_recharge_center_get_input( Ok(RuntimeProfileRechargeCenterGetInput { user_id }) } +pub fn build_runtime_profile_membership_get_input( + user_id: String, +) -> Result { + let user_id = normalize_runtime_profile_user_id(user_id)?; + Ok(RuntimeProfileMembershipGetInput { user_id }) +} + pub fn build_runtime_profile_recharge_order_get_input( order_id: String, ) -> Result { diff --git a/server-rs/crates/module-runtime/src/domain.rs b/server-rs/crates/module-runtime/src/domain.rs index 3fd46f4ac..29e195f25 100644 --- a/server-rs/crates/module-runtime/src/domain.rs +++ b/server-rs/crates/module-runtime/src/domain.rs @@ -1532,6 +1532,22 @@ pub struct RuntimeProfileMembershipSnapshot { pub cycle_kind: RuntimeProfileMembershipCycleKind, } +/// 只读会员快照的入参;后台详情等只读入口使用。 +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileMembershipGetInput { + pub user_id: String, +} + +/// 只读会员快照 procedure 的结果:成功时 `record` 为会员投影,失败时给出 `error_message`。 +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileMembershipSnapshotProcedureResult { + pub ok: bool, + pub record: Option, + pub error_message: Option, +} + #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct RuntimeProfileRechargeOrderSnapshot { diff --git a/server-rs/crates/spacetime-client/src/active.rs b/server-rs/crates/spacetime-client/src/active.rs index 61f50ea68..83526c14b 100644 --- a/server-rs/crates/spacetime-client/src/active.rs +++ b/server-rs/crates/spacetime-client/src/active.rs @@ -72,8 +72,9 @@ use module_runtime::{ AnalyticsMetricQueryResponse as DomainAnalyticsMetricQueryResponse, RuntimeProfileDashboardRecord, RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeAdminListRecord, RuntimeProfileInviteCodeRecord, - RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord, - RuntimeProfileRechargeProductConfigRecord, RuntimeProfileRedeemCodeAdminListRecord, + RuntimeProfileMembershipRecord, RuntimeProfileRechargeCenterRecord, + RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeProductConfigRecord, + RuntimeProfileRedeemCodeAdminListRecord, RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord, @@ -87,9 +88,10 @@ use module_runtime::{ build_runtime_profile_feedback_submission_record, build_runtime_profile_invite_code_admin_list_input, build_runtime_profile_invite_code_admin_upsert_input, build_runtime_profile_invite_code_record, + build_runtime_profile_membership_get_input, build_runtime_profile_membership_plan_admin_list_input, build_runtime_profile_membership_plan_admin_upsert_input, - build_runtime_profile_membership_upgrade_quote_input, + build_runtime_profile_membership_record, build_runtime_profile_membership_upgrade_quote_input, build_runtime_profile_membership_upgrade_quote_record, build_runtime_profile_recharge_center_get_input, build_runtime_profile_recharge_center_record, build_runtime_profile_recharge_order_close_input, diff --git a/server-rs/crates/spacetime-client/src/active/mapper/runtime_profile.rs b/server-rs/crates/spacetime-client/src/active/mapper/runtime_profile.rs index c65d6ab68..591dd1209 100644 --- a/server-rs/crates/spacetime-client/src/active/mapper/runtime_profile.rs +++ b/server-rs/crates/spacetime-client/src/active/mapper/runtime_profile.rs @@ -18,6 +18,14 @@ impl From } } +impl From for RuntimeProfileMembershipGetInput { + fn from(input: module_runtime::RuntimeProfileMembershipGetInput) -> Self { + Self { + user_id: input.user_id, + } + } +} + pub(crate) fn map_runtime_profile_wallet_ledger_source_type_back( value: crate::module_bindings::RuntimeProfileWalletLedgerSourceType, ) -> module_runtime::RuntimeProfileWalletLedgerSourceType { @@ -740,6 +748,20 @@ pub(crate) fn map_runtime_profile_recharge_center_procedure_result( )) } +pub(crate) fn map_runtime_profile_membership_snapshot_procedure_result( + result: RuntimeProfileMembershipSnapshotProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + let snapshot = result + .record + .ok_or_else(|| SpacetimeClientError::missing_snapshot("profile membership 快照"))?; + Ok(build_runtime_profile_membership_record( + map_runtime_profile_membership_snapshot(snapshot), + )) +} + pub(crate) fn map_runtime_profile_recharge_order_procedure_result( result: RuntimeProfileRechargeCenterProcedureResult, ) -> Result< diff --git a/server-rs/crates/spacetime-client/src/active/runtime.rs b/server-rs/crates/spacetime-client/src/active/runtime.rs index 7c5c4bb34..99e3f4bf6 100644 --- a/server-rs/crates/spacetime-client/src/active/runtime.rs +++ b/server-rs/crates/spacetime-client/src/active/runtime.rs @@ -394,6 +394,34 @@ impl SpacetimeClient { .await } + /// 只读会员快照:不刷新钱包 / 账期,供后台详情等只读入口使用。 + pub async fn get_profile_membership_snapshot( + &self, + user_id: String, + ) -> Result { + let procedure_input = build_runtime_profile_membership_get_input(user_id) + .map_err(SpacetimeClientError::validation_failed)? + .into(); + + self.call_after_connect( + "get_profile_membership_snapshot_and_return", + move |connection, sender| { + connection + .procedures() + .get_profile_membership_snapshot_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_runtime_profile_membership_snapshot_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + pub async fn create_profile_recharge_order( &self, user_id: String, diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index d19e12c93..2bfd173fe 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -478,6 +478,7 @@ pub mod get_game_distribution_version_and_return_procedure; pub mod get_llm_router_account_and_return_procedure; pub mod get_owner_game_distribution_version_and_return_procedure; pub mod get_profile_dashboard_procedure; +pub mod get_profile_membership_snapshot_and_return_procedure; pub mod get_profile_membership_upgrade_quote_procedure; pub mod get_profile_recharge_center_procedure; pub mod get_profile_recharge_order_and_return_procedure; @@ -653,6 +654,7 @@ pub mod runtime_profile_invite_code_admin_upsert_input_type; pub mod runtime_profile_invite_code_snapshot_type; pub mod runtime_profile_membership_change_kind_type; pub mod runtime_profile_membership_cycle_kind_type; +pub mod runtime_profile_membership_get_input_type; pub mod runtime_profile_membership_model_access_type; pub mod runtime_profile_membership_order_change_snapshot_type; pub mod runtime_profile_membership_plan_admin_list_input_type; @@ -661,6 +663,7 @@ pub mod runtime_profile_membership_plan_admin_procedure_result_type; pub mod runtime_profile_membership_plan_admin_upsert_input_type; pub mod runtime_profile_membership_plan_record_type; pub mod runtime_profile_membership_plan_type; +pub mod runtime_profile_membership_snapshot_procedure_result_type; pub mod runtime_profile_membership_snapshot_type; pub mod runtime_profile_membership_status_type; pub mod runtime_profile_membership_upgrade_quote_get_input_type; @@ -1295,6 +1298,7 @@ pub use get_game_distribution_version_and_return_procedure::get_game_distributio pub use get_llm_router_account_and_return_procedure::get_llm_router_account_and_return; pub use get_owner_game_distribution_version_and_return_procedure::get_owner_game_distribution_version_and_return; pub use get_profile_dashboard_procedure::get_profile_dashboard; +pub use get_profile_membership_snapshot_and_return_procedure::get_profile_membership_snapshot_and_return; pub use get_profile_membership_upgrade_quote_procedure::get_profile_membership_upgrade_quote; pub use get_profile_recharge_center_procedure::get_profile_recharge_center; pub use get_profile_recharge_order_and_return_procedure::get_profile_recharge_order_and_return; @@ -1470,6 +1474,7 @@ pub use runtime_profile_invite_code_admin_upsert_input_type::RuntimeProfileInvit pub use runtime_profile_invite_code_snapshot_type::RuntimeProfileInviteCodeSnapshot; pub use runtime_profile_membership_change_kind_type::RuntimeProfileMembershipChangeKind; pub use runtime_profile_membership_cycle_kind_type::RuntimeProfileMembershipCycleKind; +pub use runtime_profile_membership_get_input_type::RuntimeProfileMembershipGetInput; pub use runtime_profile_membership_model_access_type::RuntimeProfileMembershipModelAccess; pub use runtime_profile_membership_order_change_snapshot_type::RuntimeProfileMembershipOrderChangeSnapshot; pub use runtime_profile_membership_plan_admin_list_input_type::RuntimeProfileMembershipPlanAdminListInput; @@ -1478,6 +1483,7 @@ pub use runtime_profile_membership_plan_admin_procedure_result_type::RuntimeProf pub use runtime_profile_membership_plan_admin_upsert_input_type::RuntimeProfileMembershipPlanAdminUpsertInput; pub use runtime_profile_membership_plan_record_type::RuntimeProfileMembershipPlanRecord; pub use runtime_profile_membership_plan_type::RuntimeProfileMembershipPlan; +pub use runtime_profile_membership_snapshot_procedure_result_type::RuntimeProfileMembershipSnapshotProcedureResult; pub use runtime_profile_membership_snapshot_type::RuntimeProfileMembershipSnapshot; pub use runtime_profile_membership_status_type::RuntimeProfileMembershipStatus; pub use runtime_profile_membership_upgrade_quote_get_input_type::RuntimeProfileMembershipUpgradeQuoteGetInput; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_profile_membership_snapshot_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_profile_membership_snapshot_and_return_procedure.rs new file mode 100644 index 000000000..0ea44fe05 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_profile_membership_snapshot_and_return_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::runtime_profile_membership_get_input_type::RuntimeProfileMembershipGetInput; +use super::runtime_profile_membership_snapshot_procedure_result_type::RuntimeProfileMembershipSnapshotProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetProfileMembershipSnapshotAndReturnArgs { + pub input: RuntimeProfileMembershipGetInput, +} + +impl __sdk::InModule for GetProfileMembershipSnapshotAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_profile_membership_snapshot_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_profile_membership_snapshot_and_return { + fn get_profile_membership_snapshot_and_return(&self, input: RuntimeProfileMembershipGetInput) { + self.get_profile_membership_snapshot_and_return_then(input, |_, _| {}); + } + + fn get_profile_membership_snapshot_and_return_then( + &self, + input: RuntimeProfileMembershipGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_profile_membership_snapshot_and_return for super::RemoteProcedures { + fn get_profile_membership_snapshot_and_return_then( + &self, + input: RuntimeProfileMembershipGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, RuntimeProfileMembershipSnapshotProcedureResult>( + "get_profile_membership_snapshot_and_return", + GetProfileMembershipSnapshotAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_get_input_type.rs new file mode 100644 index 000000000..3e1dbd0a8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_get_input_type.rs @@ -0,0 +1,15 @@ +// 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 RuntimeProfileMembershipGetInput { + pub user_id: String, +} + +impl __sdk::InModule for RuntimeProfileMembershipGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_snapshot_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_snapshot_procedure_result_type.rs new file mode 100644 index 000000000..6c9abd06a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_snapshot_procedure_result_type.rs @@ -0,0 +1,19 @@ +// 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::runtime_profile_membership_snapshot_type::RuntimeProfileMembershipSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeProfileMembershipSnapshotProcedureResult { + pub ok: bool, + pub record: Option, + pub error_message: Option, +} + +impl __sdk::InModule for RuntimeProfileMembershipSnapshotProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-module/src/runtime/active/profile.rs b/server-rs/crates/spacetime-module/src/runtime/active/profile.rs index 48d64193c..869fc7af6 100644 --- a/server-rs/crates/spacetime-module/src/runtime/active/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/active/profile.rs @@ -1619,6 +1619,37 @@ pub fn get_profile_recharge_center( } } +/// 只读会员快照:不刷新钱包 / 账期、不写库,供后台详情等只读入口使用。 +#[spacetimedb::procedure] +pub fn get_profile_membership_snapshot_and_return( + ctx: &mut ProcedureContext, + input: RuntimeProfileMembershipGetInput, +) -> RuntimeProfileMembershipSnapshotProcedureResult { + let caller = ctx.sender(); + match ctx.try_with_tx(|tx| { + crate::editor_project_storage::require_editor_generation_runtime_service_identity( + tx, caller, + )?; + let validated = build_runtime_profile_membership_get_input(input.user_id.clone()) + .map_err(|error| error.to_string())?; + Ok(build_profile_membership_snapshot_readonly( + tx, + &validated.user_id, + )) + }) { + Ok(record) => RuntimeProfileMembershipSnapshotProcedureResult { + ok: true, + record: Some(record), + error_message: None, + }, + Err(message) => RuntimeProfileMembershipSnapshotProcedureResult { + ok: false, + record: None, + error_message: Some(message), + }, + } +} + #[spacetimedb::procedure] pub fn get_profile_recharge_order_and_return( ctx: &mut ProcedureContext, @@ -9764,17 +9795,38 @@ fn refresh_profile_membership_cycle(ctx: &ReducerContext, user_id: &str, now: Ti ); } +/// 会写库的会员快照:先刷新钱包 / 账期(可能换期、补发每日免费额度),再按库内投影读取。 fn build_profile_membership_snapshot( ctx: &ReducerContext, user_id: &str, ) -> RuntimeProfileMembershipSnapshot { refresh_profile_wallet_expiring_points(ctx, user_id, ctx.timestamp); + build_profile_membership_snapshot_readonly(ctx, user_id) +} + +/// 纯读会员快照:用只读账期投影替代写库刷新,不落任何表,供后台详情等只读入口使用。 +/// +/// 与写库刷新后的对外语义一致:未过期按当前账期投影,已过期按非会员返回;区别只是不触发换期与补发。 +fn build_profile_membership_snapshot_readonly( + ctx: &ReducerContext, + user_id: &str, +) -> RuntimeProfileMembershipSnapshot { let now_micros = ctx.timestamp.to_micros_since_unix_epoch(); let membership = ctx .db .profile_membership() .user_id() - .find(&user_id.to_string()); + .find(&user_id.to_string()) + .map(|row| project_profile_membership_cycle_at(ctx, row, ctx.timestamp)); + build_profile_membership_snapshot_from_row(user_id, membership, now_micros) +} + +/// 把会员行(`None` 表示从未开通)映射为对外快照,是写库与纯读两条路径共用的投影。 +fn build_profile_membership_snapshot_from_row( + user_id: &str, + membership: Option, + now_micros: i64, +) -> RuntimeProfileMembershipSnapshot { match membership { Some(row) if row.expires_at.to_micros_since_unix_epoch() > now_micros => { RuntimeProfileMembershipSnapshot {