领域层:新增会员档位、周期、模型权限与变更类型枚举

- 新增 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 <junie@jetbrains.com>
This commit is contained in:
2026-10-02 14:06:20 +08:00
parent 482af92327
commit b827c93d39
2 changed files with 206 additions and 0 deletions
@@ -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<Self> {
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<Self> {
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<Self> {
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<Self> {
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 {
@@ -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);
}