From 92917f57c9e54938fc9d059b3328944ad31427ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 3 Oct 2026 11:15:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E6=8A=A5=E4=BB=B7=E9=81=87?= =?UTF-8?q?=E5=88=B0=E7=9B=AE=E5=BD=95=E5=80=92=E6=8C=82=E6=97=B6=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit quote_runtime_profile_membership_upgrade 用 checked_sub 替换 saturating_sub,价格或每期泥点倒挂时返回 InvalidQuoteInput 新增 inverted_catalog_price_and_points_are_rejected 覆盖原本会算成 0 元升级的场景 Co-authored-by: Junie --- .../module-runtime/src/membership/upgrade.rs | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/server-rs/crates/module-runtime/src/membership/upgrade.rs b/server-rs/crates/module-runtime/src/membership/upgrade.rs index f74a5a65b..db3474798 100644 --- a/server-rs/crates/module-runtime/src/membership/upgrade.rs +++ b/server-rs/crates/module-runtime/src/membership/upgrade.rs @@ -158,8 +158,15 @@ pub fn quote_runtime_profile_membership_upgrade( input.now_micros, ); - let price_delta_cents = target.price_cents.saturating_sub(current.price_cents); - let points_delta = target.period_points.saturating_sub(current.period_points); + // 目录价格 / 每期泥点必须严格随 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)?; let (amount_cents, granted_points_delta) = match target.cycle_kind { RuntimeProfileMembershipCycleKind::Monthly => (price_delta_cents, points_delta), @@ -401,6 +408,41 @@ mod tests { ); } + #[test] + fn inverted_catalog_price_and_points_are_rejected() { + // 中文注释:后台把更高档位定得更便宜 / 给更少每期泥点时,报价必须报错, + // 而不是被 saturating_sub 静默算成 0 元升级。 + let current = snapshot( + RuntimeProfileMembershipPlan::Pro, + RuntimeProfileMembershipCycleKind::Yearly, + ); + let mut target = snapshot( + RuntimeProfileMembershipPlan::Max, + RuntimeProfileMembershipCycleKind::Yearly, + ); + + target.price_cents = current.price_cents - 1; + assert_eq!( + quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput { + current: current.clone(), + target: target.clone(), + ..base_input() + }), + Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput) + ); + + target.price_cents = current.price_cents + 1; + target.period_points = current.period_points - 1; + assert_eq!( + quote_runtime_profile_membership_upgrade(RuntimeProfileMembershipUpgradeQuoteInput { + current, + target, + ..base_input() + }), + Err(RuntimeProfileMembershipUpgradeRejection::InvalidQuoteInput) + ); + } + #[test] fn cycle_kind_change_is_rejected() { assert_eq!(