From b827c93d39fccbf2b1df090ac7bb1d02bf10737a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 2 Oct 2026 14:06:20 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=86=E5=9F=9F=E5=B1=82=EF=BC=9A=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=BC=9A=E5=91=98=E6=A1=A3=E4=BD=8D=E3=80=81=E5=91=A8?= =?UTF-8?q?=E6=9C=9F=E3=80=81=E6=A8=A1=E5=9E=8B=E6=9D=83=E9=99=90=E4=B8=8E?= =?UTF-8?q?=E5=8F=98=E6=9B=B4=E7=B1=BB=E5=9E=8B=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 RuntimeProfileMembershipPlan { Normal, Starter, Plus, Pro, Max } 作为档位权威身份,取代旧 RuntimeProfileMembershipTier - 新增 RuntimeProfileMembershipCycleKind:月付 1 期、年付 12 期(每期仍为一个自然月) - 新增 RuntimeProfileMembershipModelAccess { Basic, Full } 与 RuntimeProfileMembershipChangeKind { Purchase, Upgrade } - 四个枚举各带 as_str 与 parse,未知对外 token 返回 None,由调用方显式拒绝 - 新增集成测试 tests/membership_plan_enum.rs:token 往返、Normal 不可购买、期数映射、未知 token 拒绝 Co-authored-by: Junie --- server-rs/crates/module-runtime/src/domain.rs | 125 ++++++++++++++++++ .../tests/membership_plan_enum.rs | 81 ++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 server-rs/crates/module-runtime/tests/membership_plan_enum.rs diff --git a/server-rs/crates/module-runtime/src/domain.rs b/server-rs/crates/module-runtime/src/domain.rs index 44148900c..9d60e5cef 100644 --- a/server-rs/crates/module-runtime/src/domain.rs +++ b/server-rs/crates/module-runtime/src/domain.rs @@ -1255,6 +1255,131 @@ impl RuntimeProfileMembershipTier { } } +/// 会员档位身份。取代旧的 `RuntimeProfileMembershipTier`,作为唯一权威档位标识。 +/// +/// 变体顺序即排序基准(`Normal < Starter < Plus < Pro < Max`),升级比较不再依赖旧枚举别名。 +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum RuntimeProfileMembershipPlan { + Normal, + Starter, + Plus, + Pro, + Max, +} + +impl RuntimeProfileMembershipPlan { + pub fn as_str(&self) -> &'static str { + match self { + Self::Normal => "normal", + Self::Starter => "starter", + Self::Plus => "plus", + Self::Pro => "pro", + Self::Max => "max", + } + } + + /// 严格解析对外 wire token;未知 token 返回 `None`,调用方必须显式拒绝,不接受自由字符串。 + pub fn parse(value: &str) -> Option { + match value { + "normal" => Some(Self::Normal), + "starter" => Some(Self::Starter), + "plus" => Some(Self::Plus), + "pro" => Some(Self::Pro), + "max" => Some(Self::Max), + _ => None, + } + } + + /// `Normal` 不是可售档位,其余档位可在充值中心购买。 + pub fn is_purchasable(&self) -> bool { + !matches!(self, Self::Normal) + } +} + +/// 会员购买周期类型。它只决定本次购买的总期数与到期日,单期窗口一律按自然月推进。 +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum RuntimeProfileMembershipCycleKind { + Monthly, + Yearly, +} + +impl RuntimeProfileMembershipCycleKind { + pub fn as_str(&self) -> &'static str { + match self { + Self::Monthly => "monthly", + Self::Yearly => "yearly", + } + } + + pub fn parse(value: &str) -> Option { + match value { + "monthly" => Some(Self::Monthly), + "yearly" => Some(Self::Yearly), + _ => None, + } + } + + /// 本次购买的总期数:月付 1 期、年付 12 期(每期仍是一个自然月)。 + pub fn cycle_count(&self) -> u32 { + match self { + Self::Monthly => 1, + Self::Yearly => 12, + } + } +} + +/// 档位允许选择的模型范围。本期只落字段、只用于展示,服务端不做拦截。 +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum RuntimeProfileMembershipModelAccess { + Basic, + Full, +} + +impl RuntimeProfileMembershipModelAccess { + pub fn as_str(&self) -> &'static str { + match self { + Self::Basic => "basic", + Self::Full => "full", + } + } + + pub fn parse(value: &str) -> Option { + match value { + "basic" => Some(Self::Basic), + "full" => Some(Self::Full), + _ => None, + } + } +} + +/// 会员订单产生的权益变更类型,用于订单快照区分「新购」与「补差升级」。 +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum RuntimeProfileMembershipChangeKind { + Purchase, + Upgrade, +} + +impl RuntimeProfileMembershipChangeKind { + pub fn as_str(&self) -> &'static str { + match self { + Self::Purchase => "purchase", + Self::Upgrade => "upgrade", + } + } + + pub fn parse(value: &str) -> Option { + match value { + "purchase" => Some(Self::Purchase), + "upgrade" => Some(Self::Upgrade), + _ => None, + } + } +} + #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RuntimeProfileRechargeOrderStatus { diff --git a/server-rs/crates/module-runtime/tests/membership_plan_enum.rs b/server-rs/crates/module-runtime/tests/membership_plan_enum.rs new file mode 100644 index 000000000..fb3cdfda1 --- /dev/null +++ b/server-rs/crates/module-runtime/tests/membership_plan_enum.rs @@ -0,0 +1,81 @@ +use module_runtime::{ + RuntimeProfileMembershipChangeKind, RuntimeProfileMembershipCycleKind, + RuntimeProfileMembershipModelAccess, RuntimeProfileMembershipPlan, +}; + +#[test] +fn membership_plan_tokens_round_trip_strictly() { + // 中文注释:对外 wire token 必须与枚举一一对应,未知 token 一律解析失败。 + let cases = [ + (RuntimeProfileMembershipPlan::Normal, "normal"), + (RuntimeProfileMembershipPlan::Starter, "starter"), + (RuntimeProfileMembershipPlan::Plus, "plus"), + (RuntimeProfileMembershipPlan::Pro, "pro"), + (RuntimeProfileMembershipPlan::Max, "max"), + ]; + for (plan, token) in cases { + assert_eq!(plan.as_str(), token); + assert_eq!(RuntimeProfileMembershipPlan::parse(token), Some(plan)); + } + + assert_eq!(RuntimeProfileMembershipPlan::parse("basic"), None); + assert_eq!(RuntimeProfileMembershipPlan::parse("ultimate"), None); + assert_eq!(RuntimeProfileMembershipPlan::parse("PLUS"), None); + assert_eq!(RuntimeProfileMembershipPlan::parse(""), None); +} + +#[test] +fn membership_plan_normal_is_not_purchasable() { + // 中文注释:Normal 是非会员占位档位,不允许被购买。 + assert!(!RuntimeProfileMembershipPlan::Normal.is_purchasable()); + for plan in [ + RuntimeProfileMembershipPlan::Starter, + RuntimeProfileMembershipPlan::Plus, + RuntimeProfileMembershipPlan::Pro, + RuntimeProfileMembershipPlan::Max, + ] { + assert!(plan.is_purchasable()); + } +} + +#[test] +fn membership_cycle_kind_maps_to_period_count() { + // 中文注释:年付 12 期、月付 1 期,每期仍是一个自然月。 + assert_eq!(RuntimeProfileMembershipCycleKind::Monthly.cycle_count(), 1); + assert_eq!(RuntimeProfileMembershipCycleKind::Yearly.cycle_count(), 12); + assert_eq!( + RuntimeProfileMembershipCycleKind::Monthly.as_str(), + "monthly" + ); + assert_eq!(RuntimeProfileMembershipCycleKind::Yearly.as_str(), "yearly"); + assert_eq!( + RuntimeProfileMembershipCycleKind::parse("yearly"), + Some(RuntimeProfileMembershipCycleKind::Yearly) + ); + assert_eq!(RuntimeProfileMembershipCycleKind::parse("weekly"), None); +} + +#[test] +fn membership_model_access_and_change_kind_tokens_round_trip() { + assert_eq!(RuntimeProfileMembershipModelAccess::Basic.as_str(), "basic"); + assert_eq!(RuntimeProfileMembershipModelAccess::Full.as_str(), "full"); + assert_eq!( + RuntimeProfileMembershipModelAccess::parse("full"), + Some(RuntimeProfileMembershipModelAccess::Full) + ); + assert_eq!(RuntimeProfileMembershipModelAccess::parse("premium"), None); + + assert_eq!( + RuntimeProfileMembershipChangeKind::Purchase.as_str(), + "purchase" + ); + assert_eq!( + RuntimeProfileMembershipChangeKind::Upgrade.as_str(), + "upgrade" + ); + assert_eq!( + RuntimeProfileMembershipChangeKind::parse("upgrade"), + Some(RuntimeProfileMembershipChangeKind::Upgrade) + ); + assert_eq!(RuntimeProfileMembershipChangeKind::parse("downgrade"), None); +}