diff --git a/server-rs/crates/module-runtime/src/membership/upgrade.rs b/server-rs/crates/module-runtime/src/membership/upgrade.rs index 051365bc5..64a9ae25a 100644 --- a/server-rs/crates/module-runtime/src/membership/upgrade.rs +++ b/server-rs/crates/module-runtime/src/membership/upgrade.rs @@ -182,15 +182,13 @@ pub fn quote_runtime_profile_membership_upgrade( input.now_micros, ); - // 目录价格 / 每期泥点必须严格随 rank 递增;倒挂时宁可报错,也不算出 0 元升级。 - let price_delta_cents = target - .price_cents - .checked_sub(current.price_cents) - .ok_or(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput)?; - let points_delta = target - .period_points - .checked_sub(current.period_points) - .ok_or(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput)?; + // 目录价格 / 每期泥点必须严格随 rank 递增;相等或倒挂都直接拒绝, + // 否则 checked_sub 会把「相等」算成 0 元升级 / 0 补点,违背目录不变量。 + if target.price_cents <= current.price_cents || target.period_points <= current.period_points { + return Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput); + } + let price_delta_cents = target.price_cents - current.price_cents; + let points_delta = target.period_points - current.period_points; let (amount_cents, granted_points_delta) = match target.cycle_kind { RuntimeProfileMembershipCycleKind::Monthly => (price_delta_cents, points_delta), @@ -494,6 +492,19 @@ mod tests { target.price_cents = current.price_cents + 1; target.period_points = current.period_points - 1; + assert_eq!( + quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput { + current: current.clone(), + target: target.clone(), + ..base_input() + }), + Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput) + ); + + // 中文注释:相等同样破坏「严格随 rank 递增」,必须拒绝;旧实现用 checked_sub + // 会把相等的价差 / 点差算成 0,从而产出 0 元升级 / 0 补点。 + target.price_cents = current.price_cents; + target.period_points = current.period_points; assert_eq!( quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput { current,