枚举化(会员):快照与报价/档位响应改用 typed enum

- shared-contracts 新增 ProfileMembershipPlan/CycleKind/ModelAccess/ChangeKind Token 枚举
- RuntimeProfileMembershipOrderChangeSnapshot 与 UpgradeQuoteRecord 档位/周期改用模块枚举
- AdminMembershipOrderChangePayload、ProfileMembership/Plan/UpgradeQuote Response 改用 token 枚举
- module-runtime 提供模块枚举与 wire token 的双向 From 映射
- api-server / spacetime-client / spacetime-module 同步去掉 as_str().to_string() 拼装
- 重生成 SpacetimeDB 绑定:订单变更快照字段改为枚举
This commit is contained in:
2026-10-04 11:43:57 +08:00
parent 1744262b0b
commit c660ca41fe
9 changed files with 256 additions and 74 deletions
@@ -995,11 +995,11 @@ fn map_membership_order_change(
change: RuntimeProfileMembershipOrderChangeSnapshot,
) -> AdminMembershipOrderChangePayload {
AdminMembershipOrderChangePayload {
change_kind: change.change_kind,
plan_before: change.plan_before,
plan_after: change.plan_after,
cycle_kind_before: change.cycle_kind_before,
cycle_kind_after: change.cycle_kind_after,
change_kind: change.change_kind.into(),
plan_before: change.plan_before.map(Into::into),
plan_after: change.plan_after.map(Into::into),
cycle_kind_before: change.cycle_kind_before.map(Into::into),
cycle_kind_after: change.cycle_kind_after.map(Into::into),
period_points_before: change.period_points_before,
period_points_after: change.period_points_after,
granted_points_delta: change.granted_points_delta,
@@ -252,13 +252,7 @@ pub async fn get_profile_membership_upgrade_quote(
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 target_plan = payload.target_plan.into();
let quote = state
.spacetime_client()
.get_profile_membership_upgrade_quote(user_id, target_plan)
@@ -273,9 +267,9 @@ pub async fn get_profile_membership_upgrade_quote(
Ok(json_success_body(
Some(&request_context),
ProfileMembershipUpgradeQuoteResponse {
current_plan: quote.current_plan,
target_plan: quote.target_plan,
cycle_kind: quote.cycle_kind,
current_plan: quote.current_plan.into(),
target_plan: quote.target_plan.into(),
cycle_kind: quote.cycle_kind.into(),
cycle_index: quote.cycle_index,
cycle_count: quote.cycle_count,
amount_cents: quote.amount_cents,
@@ -1981,7 +1975,7 @@ fn build_profile_recharge_center_response(
mud_point_balance,
membership: ProfileMembershipResponse {
status: record.membership.status.as_str().to_string(),
plan: record.membership.plan.as_str().to_string(),
plan: record.membership.plan.into(),
started_at: record.membership.started_at,
expires_at: record.membership.expires_at,
updated_at: record.membership.updated_at,
@@ -1991,7 +1985,7 @@ fn build_profile_recharge_center_response(
cycle_remaining_points: record.membership.cycle_remaining_points,
cycle_index: record.membership.cycle_index,
cycle_count: record.membership.cycle_count,
cycle_kind: record.membership.cycle_kind.as_str().to_string(),
cycle_kind: record.membership.cycle_kind.into(),
},
point_products: record
.point_products
@@ -2075,13 +2069,13 @@ fn build_profile_membership_plan_response(
record: RuntimeProfileMembershipPlanRecord,
) -> ProfileMembershipPlanResponse {
ProfileMembershipPlanResponse {
plan: record.plan.as_str().to_string(),
plan: record.plan.into(),
title: record.title,
rank: record.rank,
month_price_cents: record.month_price_cents,
year_price_cents: record.year_price_cents,
period_points: record.period_points,
model_access: record.model_access.as_str().to_string(),
model_access: record.model_access.into(),
concurrent_job_limit: record.concurrent_job_limit,
recommended: record.plan == PROFILE_MEMBERSHIP_RECOMMENDED_PLAN,
unlimited_concurrency: is_unlimited_concurrency(record.concurrent_job_limit),
@@ -1132,9 +1132,9 @@ pub fn build_runtime_profile_membership_upgrade_quote_record(
snapshot: RuntimeProfileMembershipUpgradeQuoteSnapshot,
) -> RuntimeProfileMembershipUpgradeQuoteRecord {
RuntimeProfileMembershipUpgradeQuoteRecord {
current_plan: snapshot.current_plan.as_str().to_string(),
target_plan: snapshot.target_plan.as_str().to_string(),
cycle_kind: snapshot.cycle_kind.as_str().to_string(),
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,
+98 -8
View File
@@ -1377,6 +1377,96 @@ impl RuntimeProfileMembershipChangeKind {
}
}
impl From<RuntimeProfileMembershipPlan> for shared_contracts::runtime::ProfileMembershipPlanToken {
fn from(value: RuntimeProfileMembershipPlan) -> Self {
match value {
RuntimeProfileMembershipPlan::Normal => Self::Normal,
RuntimeProfileMembershipPlan::Starter => Self::Starter,
RuntimeProfileMembershipPlan::Plus => Self::Plus,
RuntimeProfileMembershipPlan::Pro => Self::Pro,
RuntimeProfileMembershipPlan::Max => Self::Max,
}
}
}
impl From<shared_contracts::runtime::ProfileMembershipPlanToken> for RuntimeProfileMembershipPlan {
fn from(value: shared_contracts::runtime::ProfileMembershipPlanToken) -> Self {
match value {
shared_contracts::runtime::ProfileMembershipPlanToken::Normal => Self::Normal,
shared_contracts::runtime::ProfileMembershipPlanToken::Starter => Self::Starter,
shared_contracts::runtime::ProfileMembershipPlanToken::Plus => Self::Plus,
shared_contracts::runtime::ProfileMembershipPlanToken::Pro => Self::Pro,
shared_contracts::runtime::ProfileMembershipPlanToken::Max => Self::Max,
}
}
}
impl From<RuntimeProfileMembershipCycleKind>
for shared_contracts::runtime::ProfileMembershipCycleKindToken
{
fn from(value: RuntimeProfileMembershipCycleKind) -> Self {
match value {
RuntimeProfileMembershipCycleKind::Monthly => Self::Monthly,
RuntimeProfileMembershipCycleKind::Yearly => Self::Yearly,
}
}
}
impl From<shared_contracts::runtime::ProfileMembershipCycleKindToken>
for RuntimeProfileMembershipCycleKind
{
fn from(value: shared_contracts::runtime::ProfileMembershipCycleKindToken) -> Self {
match value {
shared_contracts::runtime::ProfileMembershipCycleKindToken::Monthly => Self::Monthly,
shared_contracts::runtime::ProfileMembershipCycleKindToken::Yearly => Self::Yearly,
}
}
}
impl From<RuntimeProfileMembershipModelAccess>
for shared_contracts::runtime::ProfileMembershipModelAccessToken
{
fn from(value: RuntimeProfileMembershipModelAccess) -> Self {
match value {
RuntimeProfileMembershipModelAccess::Basic => Self::Basic,
RuntimeProfileMembershipModelAccess::Full => Self::Full,
}
}
}
impl From<shared_contracts::runtime::ProfileMembershipModelAccessToken>
for RuntimeProfileMembershipModelAccess
{
fn from(value: shared_contracts::runtime::ProfileMembershipModelAccessToken) -> Self {
match value {
shared_contracts::runtime::ProfileMembershipModelAccessToken::Basic => Self::Basic,
shared_contracts::runtime::ProfileMembershipModelAccessToken::Full => Self::Full,
}
}
}
impl From<RuntimeProfileMembershipChangeKind>
for shared_contracts::runtime::ProfileMembershipChangeKindToken
{
fn from(value: RuntimeProfileMembershipChangeKind) -> Self {
match value {
RuntimeProfileMembershipChangeKind::Purchase => Self::Purchase,
RuntimeProfileMembershipChangeKind::Upgrade => Self::Upgrade,
}
}
}
impl From<shared_contracts::runtime::ProfileMembershipChangeKindToken>
for RuntimeProfileMembershipChangeKind
{
fn from(value: shared_contracts::runtime::ProfileMembershipChangeKindToken) -> Self {
match value {
shared_contracts::runtime::ProfileMembershipChangeKindToken::Purchase => Self::Purchase,
shared_contracts::runtime::ProfileMembershipChangeKindToken::Upgrade => Self::Upgrade,
}
}
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RuntimeProfileRechargeOrderStatus {
@@ -1743,11 +1833,11 @@ pub struct RuntimeProfileRechargeOrderAdminEntrySnapshot {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileMembershipOrderChangeSnapshot {
/// `purchase` 或 `upgrade`。
pub change_kind: String,
pub plan_before: Option<String>,
pub plan_after: Option<String>,
pub cycle_kind_before: Option<String>,
pub cycle_kind_after: Option<String>,
pub change_kind: RuntimeProfileMembershipChangeKind,
pub plan_before: Option<RuntimeProfileMembershipPlan>,
pub plan_after: Option<RuntimeProfileMembershipPlan>,
pub cycle_kind_before: Option<RuntimeProfileMembershipCycleKind>,
pub cycle_kind_after: Option<RuntimeProfileMembershipCycleKind>,
pub period_points_before: Option<u64>,
pub period_points_after: Option<u64>,
pub granted_points_delta: u64,
@@ -1908,9 +1998,9 @@ pub struct RuntimeProfileMembershipUpgradeQuoteSnapshot {
/// 补差升级报价的只读投影(记录侧,附 RFC3339 时间)。
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuntimeProfileMembershipUpgradeQuoteRecord {
pub current_plan: String,
pub target_plan: String,
pub cycle_kind: String,
pub current_plan: RuntimeProfileMembershipPlan,
pub target_plan: RuntimeProfileMembershipPlan,
pub cycle_kind: RuntimeProfileMembershipCycleKind,
pub cycle_index: u32,
pub cycle_count: u32,
pub amount_cents: u64,
@@ -3,6 +3,10 @@ use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::runtime::{
ProfileMembershipChangeKindToken, ProfileMembershipCycleKindToken, ProfileMembershipPlanToken,
};
#[cfg(any())]
use crate::creation_entry_config::{
CreationEntryEventBannerResponse, PublicWorkInteractionConfigResponse,
@@ -1243,11 +1247,11 @@ pub struct AdminRechargeRefundHoldPayload {
#[serde(rename_all = "camelCase")]
pub struct AdminMembershipOrderChangePayload {
/// `purchase` 或 `upgrade`。
pub change_kind: String,
pub plan_before: Option<String>,
pub plan_after: Option<String>,
pub cycle_kind_before: Option<String>,
pub cycle_kind_after: Option<String>,
pub change_kind: ProfileMembershipChangeKindToken,
pub plan_before: Option<ProfileMembershipPlanToken>,
pub plan_after: Option<ProfileMembershipPlanToken>,
pub cycle_kind_before: Option<ProfileMembershipCycleKindToken>,
pub cycle_kind_after: Option<ProfileMembershipCycleKindToken>,
pub period_points_before: Option<u64>,
pub period_points_after: Option<u64>,
pub granted_points_delta: u64,
@@ -246,6 +246,83 @@ pub struct ProfileWalletLedgerResponse {
pub entries: Vec<ProfileWalletLedgerEntryResponse>,
}
/// 会员档位 token;与前端 `ProfileMembershipPlan` 联合类型一一对应。
///
/// 用枚举而非 `String` 收口 wire 值:未知 token 在反序列化阶段即被拒绝,后台也不能再
/// 写入任意档位字符串。
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProfileMembershipPlanToken {
Normal,
Starter,
Plus,
Pro,
Max,
}
impl ProfileMembershipPlanToken {
pub fn as_str(&self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Starter => "starter",
Self::Plus => "plus",
Self::Pro => "pro",
Self::Max => "max",
}
}
}
/// 会员购买周期 token;与前端 `ProfileMembershipCycleKind` 一一对应。
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProfileMembershipCycleKindToken {
Monthly,
Yearly,
}
impl ProfileMembershipCycleKindToken {
pub fn as_str(&self) -> &'static str {
match self {
Self::Monthly => "monthly",
Self::Yearly => "yearly",
}
}
}
/// 模型权限 token;与前端 `ProfileMembershipModelAccess` 一一对应。
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProfileMembershipModelAccessToken {
Basic,
Full,
}
impl ProfileMembershipModelAccessToken {
pub fn as_str(&self) -> &'static str {
match self {
Self::Basic => "basic",
Self::Full => "full",
}
}
}
/// 会员订单权益变更类型 token;区分“新购”与“补差升级”。
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProfileMembershipChangeKindToken {
Purchase,
Upgrade,
}
impl ProfileMembershipChangeKindToken {
pub fn as_str(&self) -> &'static str {
match self {
Self::Purchase => "purchase",
Self::Upgrade => "upgrade",
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileRechargeProductResponse {
@@ -264,14 +341,14 @@ pub struct ProfileRechargeProductResponse {
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipPlanResponse {
/// 档位 token:`normal` / `starter` / `plus` / `pro` / `max`。
pub plan: String,
pub plan: ProfileMembershipPlanToken,
pub title: String,
pub rank: u8,
pub month_price_cents: u64,
pub year_price_cents: u64,
pub period_points: u64,
/// 模型权限 token:`basic` / `full`。
pub model_access: String,
pub model_access: ProfileMembershipModelAccessToken,
/// 并发上限;`128` 表示不设上限。
pub concurrent_job_limit: u32,
/// 是否为运营推荐档位;由后端目录决定,前端不再硬编码档位 token。
@@ -286,7 +363,7 @@ pub struct ProfileMembershipPlanResponse {
pub struct ProfileMembershipResponse {
pub status: String,
/// 当前档位 token;非会员与已到期都是 `normal`。
pub plan: String,
pub plan: ProfileMembershipPlanToken,
pub started_at: Option<String>,
pub expires_at: Option<String>,
pub updated_at: Option<String>,
@@ -299,7 +376,7 @@ pub struct ProfileMembershipResponse {
/// 本次购买总期数(月付 1 / 年付 12)。
pub cycle_count: u32,
/// 周期 token:`monthly` / `yearly`。
pub cycle_kind: String,
pub cycle_kind: ProfileMembershipCycleKindToken,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -687,17 +764,17 @@ pub struct AdminUpsertProfileRechargeProductRequest {
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipUpgradeQuoteRequest {
/// 目标档位 token:`starter` / `plus` / `pro` / `max`。
pub target_plan: String,
pub target_plan: ProfileMembershipPlanToken,
}
/// 补差升级报价:付款前展示应付金额、补点与变更后的月度余额;下单时后端会用同一函数重算。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipUpgradeQuoteResponse {
pub current_plan: String,
pub target_plan: String,
pub current_plan: ProfileMembershipPlanToken,
pub target_plan: ProfileMembershipPlanToken,
/// 周期 token:`monthly` / `yearly`;有效期内不支持月年互换,所以变更前后一致。
pub cycle_kind: String,
pub cycle_kind: ProfileMembershipCycleKindToken,
pub cycle_index: u32,
pub cycle_count: u32,
/// 应付金额(分);月付 = 月价差,年付 = 年价差 ÷ 12 × (后续完整月数 + 本期剩余比例),向上取整到分。
@@ -1673,7 +1750,7 @@ mod tests {
},
membership: ProfileMembershipResponse {
status: "active".to_string(),
plan: "plus".to_string(),
plan: ProfileMembershipPlanToken::Plus,
started_at: Some("2026-04-25T10:00:00Z".to_string()),
expires_at: Some("2026-05-25T10:00:00Z".to_string()),
updated_at: Some("2026-04-25T10:00:00Z".to_string()),
@@ -1683,7 +1760,7 @@ mod tests {
cycle_remaining_points: 320,
cycle_index: 1,
cycle_count: 1,
cycle_kind: "monthly".to_string(),
cycle_kind: ProfileMembershipCycleKindToken::Monthly,
},
point_products: vec![ProfileRechargeProductResponse {
product_id: "points_60".to_string(),
@@ -1697,13 +1774,13 @@ mod tests {
description: "60泥点".to_string(),
}],
membership_plans: vec![ProfileMembershipPlanResponse {
plan: "plus".to_string(),
plan: ProfileMembershipPlanToken::Plus,
title: "Plus".to_string(),
rank: 2,
month_price_cents: 9_900,
year_price_cents: 99_000,
period_points: 1_150,
model_access: "full".to_string(),
model_access: ProfileMembershipModelAccessToken::Full,
concurrent_job_limit: 3,
recommended: true,
unlimited_concurrency: false,
@@ -1783,7 +1860,7 @@ mod tests {
mud_point_balance: empty_mud_point_balance(),
membership: ProfileMembershipResponse {
status: "normal".to_string(),
plan: "normal".to_string(),
plan: ProfileMembershipPlanToken::Normal,
started_at: None,
expires_at: None,
updated_at: None,
@@ -1793,7 +1870,7 @@ mod tests {
cycle_remaining_points: 0,
cycle_index: 1,
cycle_count: 1,
cycle_kind: "monthly".to_string(),
cycle_kind: ProfileMembershipCycleKindToken::Monthly,
},
point_products: vec![],
membership_plans: vec![],
@@ -1859,7 +1936,7 @@ mod tests {
mud_point_balance: empty_mud_point_balance(),
membership: ProfileMembershipResponse {
status: "normal".to_string(),
plan: "normal".to_string(),
plan: ProfileMembershipPlanToken::Normal,
started_at: None,
expires_at: None,
updated_at: None,
@@ -1869,7 +1946,7 @@ mod tests {
cycle_remaining_points: 0,
cycle_index: 1,
cycle_count: 1,
cycle_kind: "monthly".to_string(),
cycle_kind: ProfileMembershipCycleKindToken::Monthly,
},
point_products: vec![],
membership_plans: vec![],
@@ -2019,6 +2019,19 @@ pub(crate) fn map_runtime_profile_membership_cycle_kind_back(
}
}
pub(crate) fn map_runtime_profile_membership_change_kind_back(
value: crate::module_bindings::RuntimeProfileMembershipChangeKind,
) -> module_runtime::RuntimeProfileMembershipChangeKind {
match value {
crate::module_bindings::RuntimeProfileMembershipChangeKind::Purchase => {
module_runtime::RuntimeProfileMembershipChangeKind::Purchase
}
crate::module_bindings::RuntimeProfileMembershipChangeKind::Upgrade => {
module_runtime::RuntimeProfileMembershipChangeKind::Upgrade
}
}
}
pub(crate) fn map_runtime_profile_membership_model_access(
value: module_runtime::RuntimeProfileMembershipModelAccess,
) -> crate::module_bindings::RuntimeProfileMembershipModelAccess {
@@ -2049,11 +2062,19 @@ pub(crate) fn map_runtime_profile_membership_order_change_snapshot(
snapshot: RuntimeProfileMembershipOrderChangeSnapshot,
) -> module_runtime::RuntimeProfileMembershipOrderChangeSnapshot {
module_runtime::RuntimeProfileMembershipOrderChangeSnapshot {
change_kind: snapshot.change_kind,
plan_before: snapshot.plan_before,
plan_after: snapshot.plan_after,
cycle_kind_before: snapshot.cycle_kind_before,
cycle_kind_after: snapshot.cycle_kind_after,
change_kind: map_runtime_profile_membership_change_kind_back(snapshot.change_kind),
plan_before: snapshot
.plan_before
.map(map_runtime_profile_membership_plan_back),
plan_after: snapshot
.plan_after
.map(map_runtime_profile_membership_plan_back),
cycle_kind_before: snapshot
.cycle_kind_before
.map(map_runtime_profile_membership_cycle_kind_back),
cycle_kind_after: snapshot
.cycle_kind_after
.map(map_runtime_profile_membership_cycle_kind_back),
period_points_before: snapshot.period_points_before,
period_points_after: snapshot.period_points_after,
granted_points_delta: snapshot.granted_points_delta,
@@ -4,14 +4,18 @@
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::runtime_profile_membership_change_kind_type::RuntimeProfileMembershipChangeKind;
use super::runtime_profile_membership_cycle_kind_type::RuntimeProfileMembershipCycleKind;
use super::runtime_profile_membership_plan_type::RuntimeProfileMembershipPlan;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
pub struct RuntimeProfileMembershipOrderChangeSnapshot {
pub change_kind: String,
pub plan_before: Option<String>,
pub plan_after: Option<String>,
pub cycle_kind_before: Option<String>,
pub cycle_kind_after: Option<String>,
pub change_kind: RuntimeProfileMembershipChangeKind,
pub plan_before: Option<RuntimeProfileMembershipPlan>,
pub plan_after: Option<RuntimeProfileMembershipPlan>,
pub cycle_kind_before: Option<RuntimeProfileMembershipCycleKind>,
pub cycle_kind_after: Option<RuntimeProfileMembershipCycleKind>,
pub period_points_before: Option<u64>,
pub period_points_after: Option<u64>,
pub granted_points_delta: u64,
@@ -5207,19 +5207,11 @@ fn build_profile_membership_change_snapshot_from_order_row(
) -> Option<RuntimeProfileMembershipOrderChangeSnapshot> {
let change_kind = row.membership_change_kind?;
Some(RuntimeProfileMembershipOrderChangeSnapshot {
change_kind: change_kind.as_str().to_string(),
plan_before: row
.membership_plan_before
.map(|plan| plan.as_str().to_string()),
plan_after: row
.membership_plan_after
.map(|plan| plan.as_str().to_string()),
cycle_kind_before: row
.membership_cycle_kind_before
.map(|kind| kind.as_str().to_string()),
cycle_kind_after: row
.membership_cycle_kind_after
.map(|kind| kind.as_str().to_string()),
change_kind,
plan_before: row.membership_plan_before,
plan_after: row.membership_plan_after,
cycle_kind_before: row.membership_cycle_kind_before,
cycle_kind_after: row.membership_cycle_kind_after,
period_points_before: row.membership_period_points_before,
period_points_after: row.membership_period_points_after,
granted_points_delta: row.membership_granted_points_delta,