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:
@@ -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;
|
||||
|
||||
+61
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+18
@@ -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;
|
||||
}
|
||||
+19
@@ -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;
|
||||
}
|
||||
+25
@@ -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,
|
||||
¤t,
|
||||
&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,
|
||||
|
||||
Reference in New Issue
Block a user