后台用户详情改走只读会员快照

新增只读会员快照 procedure get_profile_membership_snapshot_and_return,不刷新钱包 / 账期、不写库
module-runtime 新增 RuntimeProfileMembershipGetInput 与 RuntimeProfileMembershipSnapshotProcedureResult 及入参构造器
spacetime-client 增加 get_profile_membership_snapshot facade、From 转换与结果映射,并同步生成绑定
build_profile_membership_snapshot 拆成写库与纯读两条路径,共用 from_row 投影
admin_get_user_detail 改用只读会员快照,打开详情不再改用户钱包与会员账期
This commit is contained in:
2026-10-03 16:42:48 +08:00
parent 6f0d773dde
commit 43f0c51120
11 changed files with 232 additions and 7 deletions
@@ -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),
@@ -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<RuntimeProfileMembershipGetInput, RuntimeProfileFieldError> {
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<RuntimeProfileRechargeOrderGetInput, RuntimeProfileFieldError> {
@@ -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<RuntimeProfileMembershipSnapshot>,
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeOrderSnapshot {
@@ -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,
@@ -18,6 +18,14 @@ impl From<module_runtime::RuntimeProfileRechargeCenterGetInput>
}
}
impl From<module_runtime::RuntimeProfileMembershipGetInput> 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<RuntimeProfileMembershipRecord, SpacetimeClientError> {
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<
@@ -394,6 +394,34 @@ impl SpacetimeClient {
.await
}
/// 只读会员快照:不刷新钱包 / 账期,供后台详情等只读入口使用。
pub async fn get_profile_membership_snapshot(
&self,
user_id: String,
) -> Result<RuntimeProfileMembershipRecord, SpacetimeClientError> {
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,
@@ -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;
@@ -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<RuntimeProfileMembershipSnapshotProcedureResult, __sdk::InternalError>,
) + 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<RuntimeProfileMembershipSnapshotProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp
.invoke_procedure_with_callback::<_, RuntimeProfileMembershipSnapshotProcedureResult>(
"get_profile_membership_snapshot_and_return",
GetProfileMembershipSnapshotAndReturnArgs { input },
__callback,
);
}
}
@@ -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;
}
@@ -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<RuntimeProfileMembershipSnapshot>,
pub error_message: Option<String>,
}
impl __sdk::InModule for RuntimeProfileMembershipSnapshotProcedureResult {
type Module = super::RemoteModule;
}
@@ -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<ProfileMembership>,
now_micros: i64,
) -> RuntimeProfileMembershipSnapshot {
match membership {
Some(row) if row.expires_at.to_micros_since_unix_epoch() > now_micros => {
RuntimeProfileMembershipSnapshot {