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 f36cad305..a4891d3d3 100644 --- a/server-rs/crates/spacetime-module/src/runtime/active/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/active/profile.rs @@ -8168,9 +8168,9 @@ fn list_profile_membership_plan_records( /// 补差升级报价。非会员、降档、月年互换、当前套餐重复购买都返回可读错误,不做静默兜底。 /// -/// 注意:本函数会先刷新该用户的会员 / 钱包账期(写库),因此「预览报价」并非纯只读操作。 -/// 这是有意保留的现状——保证预览、建单、结算三处读到同一口径的账期;副作用仅限与刷新一致, -/// 不额外引入其它写入。 +/// 全程只读:不刷新账期 / 钱包(不写库、不发账本),改用 [`project_profile_membership_cycle_at`] +/// 在内存里把会员行投射到当前时刻再报价,因此与建单、结算读到的账期口径一致, +/// 也不会让「预览报价」提前消耗用户的懒刷新。 fn build_profile_membership_upgrade_quote_record( ctx: &ReducerContext, input: RuntimeProfileMembershipUpgradeQuoteGetInput, @@ -8178,8 +8178,6 @@ fn build_profile_membership_upgrade_quote_record( let validated = build_runtime_profile_membership_upgrade_quote_input(input.user_id, input.target_plan) .map_err(|error| error.to_string())?; - // 有意保留的写操作:先把账期推进到当前时刻再报价;改成只读投影需先确认契约与前端预期。 - refresh_profile_wallet_expiring_points(ctx, &validated.user_id, ctx.timestamp); let Some(row) = ctx .db @@ -8190,6 +8188,8 @@ fn build_profile_membership_upgrade_quote_record( else { return Err("当前不是会员,请直接选择套餐购买".to_string()); }; + // 只读投射账期到当前时刻:不落库、不发账本,避免预览就改会员 / 钱包状态。 + let row = project_profile_membership_cycle_at(ctx, row, ctx.timestamp); if row.plan == validated.target_plan { return Err("当前套餐已生效,无需重复购买".to_string()); }