feat(会员): 新增补差升级报价接口、后台档位读写与订单权益快照

新增 POST /api/profile/membership/upgrade-quote:只读报价,下单时后端用同一纯函数重算

新增后台 /admin/api/profile/membership-plans 的读取与按档位改价改权益接口

订单与管理端契约补上会员权益变更快照(变更前后档位、周期、补点、有效期与价格拆分)

报价结果统一走「模块侧快照 + application 记录侧投影」两段式,与既有 DTO 约定一致

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-10-02 17:36:26 +08:00
parent 703a24b682
commit c772600d59
17 changed files with 583 additions and 8 deletions
+6
View File
@@ -2238,6 +2238,7 @@ fn admin_permission_requirement(_method: &Method, path: &str) -> AdminPermission
"/admin/api/profile/tasks" | "/admin/api/profile/tasks/disable" => AnyTab(&["tasks"]),
"/admin/api/profile/wallet-config" => AnyTab(&["profile-wallet"]),
"/admin/api/profile/recharge-products" => AnyTab(&["recharge-products"]),
"/admin/api/profile/membership-plans" => AnyTab(&["membership-plans"]),
path if path.starts_with("/admin/api/profile/recharge-") => AnyTab(&["recharge-orders"]),
"/admin/api/profile/wallet-restriction" => AnyTab(&["recharge-orders"]),
"/admin/api/profile/users/reconcile-consumption" => {
@@ -7444,6 +7445,11 @@ mod tests {
Method::GET,
"/admin/api/profile/recharge-products",
),
(
"membership-plans",
Method::GET,
"/admin/api/profile/membership-plans",
),
(
"recharge-orders",
Method::GET,
@@ -28,8 +28,9 @@ use crate::{
runtime_profile::{
admin_disable_profile_redeem_code, admin_disable_profile_task_config,
admin_get_profile_wallet_config, admin_list_profile_invite_codes,
admin_list_profile_recharge_products, admin_list_profile_redeem_codes,
admin_list_profile_task_configs, admin_upsert_profile_invite_code,
admin_list_profile_membership_plans, admin_list_profile_recharge_products,
admin_list_profile_redeem_codes, admin_list_profile_task_configs,
admin_upsert_profile_invite_code, admin_upsert_profile_membership_plan,
admin_upsert_profile_recharge_product, admin_upsert_profile_redeem_code,
admin_upsert_profile_task_config, admin_upsert_profile_wallet_config,
},
@@ -170,6 +171,10 @@ pub fn router(state: AppState) -> Router<AppState> {
"/admin/api/profile/recharge-products",
get(admin_list_profile_recharge_products).post(admin_upsert_profile_recharge_product),
),
(
"/admin/api/profile/membership-plans",
get(admin_list_profile_membership_plans).post(admin_upsert_profile_membership_plan),
),
(
"/admin/api/profile/recharge-orders",
get(admin_list_recharge_orders),
@@ -277,6 +282,7 @@ mod route_contract_tests {
("/admin/api/profile/tasks/disable", &["POST"]),
("/admin/api/profile/wallet-config", &["GET", "POST"]),
("/admin/api/profile/recharge-products", &["GET", "POST"]),
("/admin/api/profile/membership-plans", &["GET", "POST"]),
("/admin/api/profile/recharge-orders", &["GET"]),
("/admin/api/profile/recharge-refunds/preview", &["POST"]),
("/admin/api/profile/recharge-refunds/execute", &["POST"]),
@@ -13,8 +13,9 @@ use crate::{
runtime_profile::{
claim_profile_task_reward, confirm_wechat_profile_recharge_order,
create_profile_recharge_order, get_profile_analytics_metric, get_profile_dashboard,
get_profile_recharge_center, get_profile_referral_invite_center, get_profile_task_center,
get_profile_wallet_ledger, redeem_profile_referral_invite_code, redeem_profile_reward_code,
get_profile_membership_upgrade_quote, get_profile_recharge_center,
get_profile_referral_invite_center, get_profile_task_center, get_profile_wallet_ledger,
redeem_profile_referral_invite_code, redeem_profile_reward_code,
stream_wechat_profile_recharge_order_events, submit_profile_feedback,
},
state::AppState,
@@ -73,6 +74,13 @@ pub fn router(state: AppState) -> Router<AppState> {
require_bearer_auth,
)),
)
.route(
"/api/profile/membership/upgrade-quote",
post(get_profile_membership_upgrade_quote).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/profile/recharge/orders",
post(create_profile_recharge_order).route_layer(middleware::from_fn_with_state(
@@ -34,9 +34,10 @@ use shared_contracts::runtime::{
ANALYTICS_GRANULARITY_DAY, ANALYTICS_GRANULARITY_MONTH, ANALYTICS_GRANULARITY_QUARTER,
ANALYTICS_GRANULARITY_WEEK, ANALYTICS_GRANULARITY_YEAR, AdminDisableProfileRedeemCodeRequest,
AdminDisableProfileTaskConfigRequest, AdminUpsertProfileInviteCodeRequest,
AdminUpsertProfileRechargeProductRequest, AdminUpsertProfileRedeemCodeRequest,
AdminUpsertProfileTaskConfigRequest, AdminUpsertProfileWalletConfigRequest,
AnalyticsBucketMetricResponse, AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse,
AdminUpsertProfileMembershipPlanRequest, AdminUpsertProfileRechargeProductRequest,
AdminUpsertProfileRedeemCodeRequest, AdminUpsertProfileTaskConfigRequest,
AdminUpsertProfileWalletConfigRequest, AnalyticsBucketMetricResponse,
AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse,
ConfirmWechatProfileRechargeOrderResponse, CreateProfileRechargeOrderRequest,
CreateProfileRechargeOrderResponse, PROFILE_FEEDBACK_STATUS_OPEN,
PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP, PROFILE_RECHARGE_PRODUCT_KIND_POINTS,
@@ -61,7 +62,9 @@ use shared_contracts::runtime::{
ProfileDailyFreePointsResponse, ProfileDashboardSummaryResponse,
ProfileFeedbackEvidenceItemResponse, ProfileFeedbackSubmissionResponse,
ProfileInviteCodeAdminListResponse, ProfileInviteCodeAdminResponse,
ProfileMembershipPlanResponse, ProfileMembershipResponse, ProfileMudPointBalanceResponse,
ProfileMembershipPlanAdminListResponse, ProfileMembershipPlanResponse,
ProfileMembershipResponse, ProfileMembershipUpgradeQuoteRequest,
ProfileMembershipUpgradeQuoteResponse, ProfileMudPointBalanceResponse,
ProfileRechargeCenterResponse, ProfileRechargeOrderResponse,
ProfileRechargeProductConfigAdminListResponse, ProfileRechargeProductConfigAdminResponse,
ProfileRechargeProductResponse, ProfileRedeemCodeAdminListResponse,
@@ -240,6 +243,50 @@ pub async fn get_profile_recharge_center(
))
}
/// 补差升级报价:只读。入参是目标档位,周期沿用当前会员周期(有效期内不支持月年互换)。
pub async fn get_profile_membership_upgrade_quote(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
Json(payload): Json<ProfileMembershipUpgradeQuoteRequest>,
) -> Result<Json<Value>, Response> {
let user_id = authenticated.claims().user_id().to_string();
let target_plan =
parse_runtime_profile_membership_plan(&payload.target_plan).map_err(|error| {
runtime_profile_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
)
})?;
let quote = state
.spacetime_client()
.get_profile_membership_upgrade_quote(user_id, target_plan)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
ProfileMembershipUpgradeQuoteResponse {
current_plan: quote.current_plan,
target_plan: quote.target_plan,
cycle_kind: quote.cycle_kind,
cycle_index: quote.cycle_index,
cycle_count: quote.cycle_count,
amount_cents: quote.amount_cents,
granted_points_delta: quote.granted_points_delta,
monthly_balance_after: quote.monthly_balance_after,
expires_at: Some(quote.expires_at),
cycle_resets_at: Some(quote.cycle_resets_at),
remaining_full_months: quote.remaining_full_months,
},
))
}
pub async fn create_profile_recharge_order(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
@@ -1117,6 +1164,83 @@ pub async fn admin_upsert_profile_recharge_product(
))
}
pub async fn admin_list_profile_membership_plans(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(admin): Extension<AuthenticatedAdmin>,
) -> Result<Json<Value>, Response> {
let entries = state
.spacetime_client()
.admin_list_profile_membership_plans(admin.session().subject.clone())
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
ProfileMembershipPlanAdminListResponse {
entries: entries
.into_iter()
.map(build_profile_membership_plan_response)
.collect(),
},
))
}
/// 后台按档位改会员价格与权益。`rank` 不在请求里:升级比较基准由代码内目录决定。
pub async fn admin_upsert_profile_membership_plan(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Extension(admin): Extension<AuthenticatedAdmin>,
Json(payload): Json<AdminUpsertProfileMembershipPlanRequest>,
) -> Result<Json<Value>, Response> {
let plan = parse_runtime_profile_membership_plan(&payload.plan).map_err(|error| {
runtime_profile_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
)
})?;
let model_access = parse_runtime_profile_membership_model_access(&payload.model_access)
.map_err(|error| {
runtime_profile_error_response(
&request_context,
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
)
})?;
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
let record = state
.spacetime_client()
.admin_upsert_profile_membership_plan(
admin.session().subject.clone(),
plan,
payload.title,
payload.month_price_cents,
payload.year_price_cents,
payload.period_points,
model_access,
payload.concurrent_job_limit,
payload.enabled,
payload.sort_order.unwrap_or(10),
updated_at_micros as i64,
)
.await
.map_err(|error| {
runtime_profile_error_response(
&request_context,
map_runtime_profile_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
build_profile_membership_plan_response(record),
))
}
pub async fn admin_list_profile_redeem_codes(
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
@@ -2294,6 +2418,22 @@ fn parse_profile_recharge_product_kind(
}
}
/// 会员档位 token → 枚举。JSON 没有枚举,但解析必须严格:未知 token 一律 400。
fn parse_runtime_profile_membership_plan(
raw: &str,
) -> Result<module_runtime::RuntimeProfileMembershipPlan, String> {
module_runtime::RuntimeProfileMembershipPlan::parse(raw)
.ok_or_else(|| "会员档位无效".to_string())
}
/// 模型权限 token → 枚举;`basic` = 仅基础模型,`full` = 基础及高性能模型。
fn parse_runtime_profile_membership_model_access(
raw: &str,
) -> Result<module_runtime::RuntimeProfileMembershipModelAccess, String> {
module_runtime::RuntimeProfileMembershipModelAccess::parse(raw)
.ok_or_else(|| "模型权限无效".to_string())
}
fn parse_tracking_scope_kind(raw: &str) -> Result<RuntimeTrackingScopeKind, String> {
match raw.trim().to_ascii_lowercase().as_str() {
TRACKING_SCOPE_KIND_SITE => Ok(RuntimeTrackingScopeKind::Site),
@@ -1128,6 +1128,26 @@ pub fn build_runtime_profile_membership_record(
}
}
pub fn build_runtime_profile_membership_upgrade_quote_record(
snapshot: RuntimeProfileMembershipUpgradeQuoteSnapshot,
) -> RuntimeProfileMembershipUpgradeQuoteRecord {
RuntimeProfileMembershipUpgradeQuoteRecord {
current_plan: snapshot.current_plan,
target_plan: snapshot.target_plan,
cycle_kind: snapshot.cycle_kind,
cycle_index: snapshot.cycle_index,
cycle_count: snapshot.cycle_count,
amount_cents: snapshot.amount_cents,
granted_points_delta: snapshot.granted_points_delta,
monthly_balance_after: snapshot.monthly_balance_after,
expires_at: format_utc_micros(snapshot.expires_at_micros),
expires_at_micros: snapshot.expires_at_micros,
cycle_resets_at: format_utc_micros(snapshot.cycle_resets_at_micros),
cycle_resets_at_micros: snapshot.cycle_resets_at_micros,
remaining_full_months: snapshot.remaining_full_months,
}
}
pub fn build_runtime_profile_recharge_order_record(
snapshot: RuntimeProfileRechargeOrderSnapshot,
) -> RuntimeProfileRechargeOrderRecord {
@@ -499,6 +499,17 @@ pub fn build_runtime_profile_membership_plan_admin_upsert_input(
})
}
pub fn build_runtime_profile_membership_upgrade_quote_input(
user_id: String,
target_plan: RuntimeProfileMembershipPlan,
) -> Result<RuntimeProfileMembershipUpgradeQuoteGetInput, RuntimeProfileFieldError> {
let user_id = normalize_runtime_profile_user_id(user_id)?;
Ok(RuntimeProfileMembershipUpgradeQuoteGetInput {
user_id,
target_plan,
})
}
pub fn build_runtime_profile_recharge_order_paid_input(
order_id: String,
paid_at_micros: i64,
@@ -1846,6 +1846,56 @@ pub struct RuntimeProfileMembershipPlanAdminProcedureResult {
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileMembershipUpgradeQuoteGetInput {
pub user_id: String,
pub target_plan: RuntimeProfileMembershipPlan,
}
/// 补差升级报价快照(模块侧,时间戳为 micros)。报价与下单重算共用同一套规则。
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileMembershipUpgradeQuoteSnapshot {
pub current_plan: String,
pub target_plan: String,
pub cycle_kind: String,
pub cycle_index: u32,
pub cycle_count: u32,
pub amount_cents: u64,
pub granted_points_delta: u64,
pub monthly_balance_after: u64,
pub expires_at_micros: i64,
pub cycle_resets_at_micros: i64,
pub remaining_full_months: u32,
}
/// 补差升级报价的只读投影(记录侧,附 RFC3339 时间)。
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileMembershipUpgradeQuoteRecord {
pub current_plan: String,
pub target_plan: String,
pub cycle_kind: String,
pub cycle_index: u32,
pub cycle_count: u32,
pub amount_cents: u64,
pub granted_points_delta: u64,
pub monthly_balance_after: u64,
pub expires_at: String,
pub expires_at_micros: i64,
pub cycle_resets_at: String,
pub cycle_resets_at_micros: i64,
pub remaining_full_months: u32,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileMembershipUpgradeQuoteProcedureResult {
pub ok: bool,
pub quote: Option<RuntimeProfileMembershipUpgradeQuoteSnapshot>,
pub error_message: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileRechargeCenterGetInput {
@@ -665,6 +665,60 @@ pub struct AdminUpsertProfileRechargeProductRequest {
pub sort_order: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipUpgradeQuoteRequest {
/// 目标档位 token:`starter` / `plus` / `pro` / `max`。
pub target_plan: String,
}
/// 补差升级报价:付款前展示应付金额、补点与变更后的月度余额;下单时后端会用同一函数重算。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipUpgradeQuoteResponse {
pub current_plan: String,
pub target_plan: String,
/// 周期 token:`monthly` / `yearly`;有效期内不支持月年互换,所以变更前后一致。
pub cycle_kind: String,
pub cycle_index: u32,
pub cycle_count: u32,
/// 应付金额(分);月付 = 月价差,年付 = 年价差 ÷ 12 × (后续完整月数 + 本期剩余比例),向上取整到分。
pub amount_cents: u64,
/// 本期补点;年付按本期剩余比例向下取整。
pub granted_points_delta: u64,
/// 变更后月度余额 = 原剩余月度余额 + 补点(已用点不返还,永久泥点不动)。
pub monthly_balance_after: u64,
pub expires_at: Option<String>,
pub cycle_resets_at: Option<String>,
pub remaining_full_months: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipPlanAdminListResponse {
pub entries: Vec<ProfileMembershipPlanResponse>,
}
/// 后台按档位改会员价格与权益;`rank` 不在请求里,升级比较基准由代码内目录决定。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertProfileMembershipPlanRequest {
/// 档位 token,作为主键即档位身份。
pub plan: String,
pub title: String,
pub month_price_cents: u64,
pub year_price_cents: u64,
pub period_points: u64,
/// 模型权限 token:`basic` / `full`;本期只落字段、不做服务端拦截。
pub model_access: String,
/// 并发上限;`128` 表示不设上限。
pub concurrent_job_limit: u32,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub sort_order: Option<i32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminUpsertProfileWalletConfigRequest {
@@ -89,6 +89,8 @@ use module_runtime::{
build_runtime_profile_invite_code_admin_upsert_input, build_runtime_profile_invite_code_record,
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_upgrade_quote_record,
build_runtime_profile_recharge_center_get_input, build_runtime_profile_recharge_center_record,
build_runtime_profile_recharge_order_close_input,
build_runtime_profile_recharge_order_create_input,
@@ -2046,6 +2046,17 @@ pub(crate) fn map_runtime_profile_membership_order_change_snapshot(
}
}
impl From<module_runtime::RuntimeProfileMembershipUpgradeQuoteGetInput>
for RuntimeProfileMembershipUpgradeQuoteGetInput
{
fn from(input: module_runtime::RuntimeProfileMembershipUpgradeQuoteGetInput) -> Self {
Self {
user_id: input.user_id,
target_plan: map_runtime_profile_membership_plan(input.target_plan),
}
}
}
impl From<module_runtime::RuntimeProfileMembershipPlanAdminListInput>
for RuntimeProfileMembershipPlanAdminListInput
{
@@ -2076,6 +2087,34 @@ impl From<module_runtime::RuntimeProfileMembershipPlanAdminUpsertInput>
}
}
pub(crate) fn map_runtime_profile_membership_upgrade_quote_procedure_result(
result: RuntimeProfileMembershipUpgradeQuoteProcedureResult,
) -> Result<module_runtime::RuntimeProfileMembershipUpgradeQuoteRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::procedure_failed(result.error_message));
}
let quote = result
.quote
.ok_or_else(|| SpacetimeClientError::missing_snapshot("会员升级报价"))?;
Ok(build_runtime_profile_membership_upgrade_quote_record(
module_runtime::RuntimeProfileMembershipUpgradeQuoteSnapshot {
current_plan: quote.current_plan,
target_plan: quote.target_plan,
cycle_kind: quote.cycle_kind,
cycle_index: quote.cycle_index,
cycle_count: quote.cycle_count,
amount_cents: quote.amount_cents,
granted_points_delta: quote.granted_points_delta,
monthly_balance_after: quote.monthly_balance_after,
expires_at_micros: quote.expires_at_micros,
cycle_resets_at_micros: quote.cycle_resets_at_micros,
remaining_full_months: quote.remaining_full_months,
},
))
}
pub(crate) fn map_runtime_profile_membership_plan_admin_list_procedure_result(
result: RuntimeProfileMembershipPlanAdminListProcedureResult,
) -> Result<Vec<module_runtime::RuntimeProfileMembershipPlanRecord>, SpacetimeClientError> {
@@ -1695,6 +1695,39 @@ impl SpacetimeClient {
.await
}
/// 补差升级报价:只读,用来在付款前展示应付金额与补点;下单时后端会用同一函数重算。
pub async fn get_profile_membership_upgrade_quote(
&self,
user_id: String,
target_plan: module_runtime::RuntimeProfileMembershipPlan,
) -> Result<module_runtime::RuntimeProfileMembershipUpgradeQuoteRecord, SpacetimeClientError>
{
let procedure_input =
build_runtime_profile_membership_upgrade_quote_input(user_id, target_plan)
.map_err(SpacetimeClientError::validation_failed)?
.into();
self.call_after_connect(
"get_profile_membership_upgrade_quote",
move |connection, sender| {
connection
.procedures()
.get_profile_membership_upgrade_quote_then(
procedure_input,
move |_, result| {
let mapped = result
.map_err(SpacetimeClientError::from_sdk_error)
.and_then(
map_runtime_profile_membership_upgrade_quote_procedure_result,
);
send_once(&sender, mapped);
},
);
},
)
.await
}
/// 后台读取会员档位目录(价格与权益的唯一真相源)。
pub async fn admin_list_profile_membership_plans(
&self,
@@ -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_upgrade_quote_procedure;
pub mod get_profile_recharge_center_procedure;
pub mod get_profile_recharge_order_and_return_procedure;
pub mod get_profile_recharge_refund_and_return_procedure;
@@ -662,6 +663,9 @@ pub mod runtime_profile_membership_plan_record_type;
pub mod runtime_profile_membership_plan_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;
pub mod runtime_profile_membership_upgrade_quote_procedure_result_type;
pub mod runtime_profile_membership_upgrade_quote_record_type;
pub mod runtime_profile_recharge_center_get_input_type;
pub mod runtime_profile_recharge_center_procedure_result_type;
pub mod runtime_profile_recharge_center_snapshot_type;
@@ -1291,6 +1295,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_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;
pub use get_profile_recharge_refund_and_return_procedure::get_profile_recharge_refund_and_return;
@@ -1475,6 +1480,9 @@ pub use runtime_profile_membership_plan_record_type::RuntimeProfileMembershipPla
pub use runtime_profile_membership_plan_type::RuntimeProfileMembershipPlan;
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;
pub use runtime_profile_membership_upgrade_quote_procedure_result_type::RuntimeProfileMembershipUpgradeQuoteProcedureResult;
pub use runtime_profile_membership_upgrade_quote_record_type::RuntimeProfileMembershipUpgradeQuoteRecord;
pub use runtime_profile_recharge_center_get_input_type::RuntimeProfileRechargeCenterGetInput;
pub use runtime_profile_recharge_center_procedure_result_type::RuntimeProfileRechargeCenterProcedureResult;
pub use runtime_profile_recharge_center_snapshot_type::RuntimeProfileRechargeCenterSnapshot;
@@ -0,0 +1,61 @@
// 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_upgrade_quote_get_input_type::RuntimeProfileMembershipUpgradeQuoteGetInput;
use super::runtime_profile_membership_upgrade_quote_procedure_result_type::RuntimeProfileMembershipUpgradeQuoteProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct GetProfileMembershipUpgradeQuoteArgs {
pub input: RuntimeProfileMembershipUpgradeQuoteGetInput,
}
impl __sdk::InModule for GetProfileMembershipUpgradeQuoteArgs {
type Module = super::RemoteModule;
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the procedure `get_profile_membership_upgrade_quote`.
///
/// Implemented for [`super::RemoteProcedures`].
pub trait get_profile_membership_upgrade_quote {
fn get_profile_membership_upgrade_quote(
&self,
input: RuntimeProfileMembershipUpgradeQuoteGetInput,
) {
self.get_profile_membership_upgrade_quote_then(input, |_, _| {});
}
fn get_profile_membership_upgrade_quote_then(
&self,
input: RuntimeProfileMembershipUpgradeQuoteGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileMembershipUpgradeQuoteProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
impl get_profile_membership_upgrade_quote for super::RemoteProcedures {
fn get_profile_membership_upgrade_quote_then(
&self,
input: RuntimeProfileMembershipUpgradeQuoteGetInput,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<RuntimeProfileMembershipUpgradeQuoteProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp.invoke_procedure_with_callback::<_, RuntimeProfileMembershipUpgradeQuoteProcedureResult>(
"get_profile_membership_upgrade_quote",
GetProfileMembershipUpgradeQuoteArgs { input, },
__callback,
);
}
}
@@ -0,0 +1,18 @@
// 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_plan_type::RuntimeProfileMembershipPlan;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeProfileMembershipUpgradeQuoteGetInput {
pub user_id: String,
pub target_plan: RuntimeProfileMembershipPlan,
}
impl __sdk::InModule for RuntimeProfileMembershipUpgradeQuoteGetInput {
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_upgrade_quote_record_type::RuntimeProfileMembershipUpgradeQuoteRecord;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeProfileMembershipUpgradeQuoteProcedureResult {
pub ok: bool,
pub quote: Option<RuntimeProfileMembershipUpgradeQuoteRecord>,
pub error_message: Option<String>,
}
impl __sdk::InModule for RuntimeProfileMembershipUpgradeQuoteProcedureResult {
type Module = super::RemoteModule;
}
@@ -0,0 +1,25 @@
// 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 RuntimeProfileMembershipUpgradeQuoteRecord {
pub current_plan: String,
pub target_plan: String,
pub cycle_kind: String,
pub cycle_index: u32,
pub cycle_count: u32,
pub amount_cents: u64,
pub granted_points_delta: u64,
pub monthly_balance_after: u64,
pub expires_at_micros: i64,
pub cycle_resets_at_micros: i64,
pub remaining_full_months: u32,
}
impl __sdk::InModule for RuntimeProfileMembershipUpgradeQuoteRecord {
type Module = super::RemoteModule;
}
@@ -1192,6 +1192,26 @@ pub fn admin_upsert_profile_membership_plan(
}
}
// 补差升级报价:只读,下单时后端会用同一函数重算并落订单快照,前端展示的数字不是真相源。
#[spacetimedb::procedure]
pub fn get_profile_membership_upgrade_quote(
ctx: &mut ProcedureContext,
input: RuntimeProfileMembershipUpgradeQuoteGetInput,
) -> RuntimeProfileMembershipUpgradeQuoteProcedureResult {
match ctx.try_with_tx(|tx| build_profile_membership_upgrade_quote_record(tx, input.clone())) {
Ok(quote) => RuntimeProfileMembershipUpgradeQuoteProcedureResult {
ok: true,
quote: Some(quote),
error_message: None,
},
Err(message) => RuntimeProfileMembershipUpgradeQuoteProcedureResult {
ok: false,
quote: None,
error_message: Some(message),
},
}
}
// 新用户注册赠送由后端注册链路调用;流水 ID 固定,保证重试不重复发放。
#[spacetimedb::procedure]
pub fn grant_new_user_registration_wallet_reward(
@@ -8044,6 +8064,61 @@ fn list_profile_membership_plan_records(
Ok(membership_plan_records(ctx))
}
/// 补差升级报价。非会员、降档、月年互换、当前套餐重复购买都返回可读错误,不做静默兜底。
fn build_profile_membership_upgrade_quote_record(
ctx: &ReducerContext,
input: RuntimeProfileMembershipUpgradeQuoteGetInput,
) -> Result<RuntimeProfileMembershipUpgradeQuoteSnapshot, String> {
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
.profile_membership()
.user_id()
.find(&validated.user_id.to_string())
.filter(|row| active_membership_row_at(row, ctx.timestamp))
else {
return Err("当前不是会员,请直接选择套餐购买".to_string());
};
if row.plan == validated.target_plan {
return Err("当前套餐已生效,无需重复购买".to_string());
}
let target = membership_plan_snapshot(ctx, validated.target_plan, row.cycle_kind)
.ok_or_else(|| format!("会员目录缺少档位 {}", validated.target_plan.as_str()))?;
if !target.enabled {
return Err(format!(
"会员档位 {} 当前不可购买",
validated.target_plan.as_str()
));
}
let current = membership_plan_snapshot(ctx, row.plan, row.cycle_kind)
.ok_or_else(|| format!("会员目录缺少档位 {}", row.plan.as_str()))?;
let quote = quote_runtime_profile_membership_upgrade(membership_upgrade_quote_input(
&row,
&current,
&target,
ctx.timestamp,
))
.map_err(|rejection| format!("会员升级被拒绝: {}", rejection.as_str()))?;
Ok(RuntimeProfileMembershipUpgradeQuoteSnapshot {
current_plan: current.plan.as_str().to_string(),
target_plan: target.plan.as_str().to_string(),
cycle_kind: target.cycle_kind.as_str().to_string(),
cycle_index: quote.cycle_index,
cycle_count: target.cycle_count,
amount_cents: quote.amount_cents,
granted_points_delta: quote.granted_points_delta,
monthly_balance_after: quote.monthly_balance_after,
expires_at_micros: quote.expires_at_micros,
cycle_resets_at_micros: quote.cycle_resets_at_micros,
remaining_full_months: quote.remaining_full_months,
})
}
/// 后台改价 / 改权益。`rank` 与 `plan` 不可改:`plan` 是主键(即身份),`rank` 是升级比较基准。
fn upsert_profile_membership_plan_record(
ctx: &ReducerContext,