diff --git a/server-rs/crates/module-runtime/src/membership/catalog.rs b/server-rs/crates/module-runtime/src/membership/catalog.rs index 1fee74750..e7d2860e3 100644 --- a/server-rs/crates/module-runtime/src/membership/catalog.rs +++ b/server-rs/crates/module-runtime/src/membership/catalog.rs @@ -8,6 +8,10 @@ //! 价格是**后端唯一真相**,前端展示价只用于渲染,下单时后端按同一份目录重算。 //! //! 本期模型权限只落字段、只用于展示,不做服务端拦截;并发上限只做判定,同样不拦截。 +//! +//! 目录不变量:按 `rank` 升序,月价、年价、每期泥点必须**严格递增**(`Normal` 为 0)。 +//! 升级补差按 `rank` 判定、按价差 / 点差计价,若高 `rank` 的行反而更便宜或更少点, +//! `saturating_sub` 会把补差静默算成 0;后台改价时不得破坏该不变量。 use serde::{Deserialize, Serialize}; @@ -298,6 +302,35 @@ mod tests { assert!(is_unlimited_concurrency(max.concurrent_job_limit)); } + #[test] + fn enabled_plans_increase_in_price_and_points_with_rank() { + // 中文注释:升级补差依赖「rank 越高、价格与每期泥点越大」这一不变量; + // 后台改价破坏它时,升级会静默算成 0,因此把种子目录的不变量固化成回归断言。 + let mut rows = runtime_profile_membership_plan_catalog(); + rows.sort_by_key(|row| row.rank); + for pair in rows.windows(2) { + let (lower, higher) = (&pair[0], &pair[1]); + assert!( + higher.month_price_cents > lower.month_price_cents, + "{} 的月价必须高于 {}", + higher.title, + lower.title + ); + assert!( + higher.year_price_cents > lower.year_price_cents, + "{} 的年价必须高于 {}", + higher.title, + lower.title + ); + assert!( + higher.period_points > lower.period_points, + "{} 的每期泥点必须高于 {}", + higher.title, + lower.title + ); + } + } + #[test] fn year_price_is_stored_independently_of_the_month_price() { // 中文注释:年价是独立可配置字段,这里断言的是种子数值本身,而不是月价 ×10 的推导关系。