Feat/陶泥儿会员订阅方案 #588

Merged
k88936 merged 240 commits from feat/pricing-plan into master 2026-10-06 10:48:46 +08:00
4 changed files with 62 additions and 4 deletions
Showing only changes of commit f9b0cf786d - Show all commits
@@ -1521,6 +1521,7 @@ fn is_runtime_profile_membership_domain_error(message: &str) -> bool {
"当前不是会员,请直接选择套餐购买"
| "当前套餐已生效,无需重复购买"
| "当前会员有效期内不支持月付 / 年付转换,到期后可重新选择"
| "会员已到期,请按新购价格重新下单"
)
}
+3 -2
View File
@@ -23,8 +23,9 @@ pub use llm_billing::*;
pub use membership::{
MEMBERSHIP_UNLIMITED_CONCURRENCY, RuntimeProfileMembershipPlanRecord,
RuntimeProfileMembershipPlanSnapshot, RuntimeProfileMembershipUpgradeQuoteInput,
beijing_local_micros, build_runtime_profile_membership_plan_snapshot, is_unlimited_concurrency,
membership_cycle_window, membership_expires_at, parse_runtime_profile_membership_product_id,
beijing_local_micros, build_runtime_profile_membership_plan_snapshot,
check_expired_membership_upgrade_settlement, is_unlimited_concurrency, membership_cycle_window,
membership_expires_at, parse_runtime_profile_membership_product_id,
quote_runtime_profile_membership_upgrade, resolve_runtime_profile_membership_plan_row,
runtime_profile_membership_plan_catalog, runtime_profile_membership_plan_rank,
runtime_profile_membership_product_id,
@@ -16,6 +16,9 @@
//!
//! 入参要求 `cycle_started_at <= now < cycle_resets_at`:调用方必须先把会员行投射到当前账期,
//! 不接受已刷新的旧窗口——否则剩余比例与期号口径不一致,会静默多收一个完整月。
//!
//! 报价冻结后会员可能在支付前到期,此时补差价不再覆盖整期新购价,
//! 由 [`check_expired_membership_upgrade_settlement`] 在结算侧判定。
use serde::{Deserialize, Serialize};
@@ -74,6 +77,8 @@ pub enum RuntimeProfileMembershipUpgradeRejection {
TargetPlanNotPurchasable,
/// 报价入参的数值不变量被破坏(期号越界、账期窗口非法或 `now` 不在窗口内):拒绝,而不是静默夹取。
InvalidQuoteInput,
/// 补差价订单结算时会员已到期,且订单金额不够覆盖整期新购价:应重新下单购买。
MembershipExpiredBeforeSettlement,
}
impl RuntimeProfileMembershipUpgradeRejection {
@@ -84,6 +89,7 @@ impl RuntimeProfileMembershipUpgradeRejection {
Self::NotUpgrade => "not_upgrade",
Self::TargetPlanNotPurchasable => "target_plan_not_purchasable",
Self::InvalidQuoteInput => "invalid_quote_input",
Self::MembershipExpiredBeforeSettlement => "membership_expired_before_settlement",
}
}
}
@@ -210,6 +216,21 @@ pub fn quote_runtime_profile_membership_upgrade(
})
}
/// 补差价订单在「会员已到期」时是否仍可按新购结算。
///
/// 补差价只覆盖升级当期剩余权益的差额,前提是升级时会员仍在有效期内;一旦拖到到期后才支付,
/// 同一笔冻结金额就不再覆盖整期新购价。只有订单金额达到目标周期当前目录价才继续按新购结算,
/// 否则拒绝,避免用零头价换一整个周期。
pub fn check_expired_membership_upgrade_settlement(
order_amount_cents: u64,
target_price_cents: u64,
) -> Result<(), RuntimeProfileMembershipUpgradeRejection> {
if order_amount_cents < target_price_cents {
return Err(RuntimeProfileMembershipUpgradeRejection::MembershipExpiredBeforeSettlement);
}
Ok(())
}
/// 向上取整的整数除法(金额取整到分)。
///
/// 分母是 `MONTHS_PER_YEAR * REMAINING_RATIO_PPM` 这类编译期常量,恒不为 0;
@@ -551,4 +572,31 @@ mod tests {
assert_eq!(quote.amount_cents, 155_556);
assert_eq!(quote.granted_points_delta, 833);
}
#[test]
fn expired_settlement_needs_the_full_target_price() {
// 中文注释:年付 Pro 目录价 299_000 分;补差价 158_334 分覆盖不了整期新购价,必须拒绝,
// 否则用户可在到期前用补差价建单、到期后再支付,等于用零头价换一个完整周期。
let pro_yearly = snapshot(
RuntimeProfileMembershipPlan::Pro,
RuntimeProfileMembershipCycleKind::Yearly,
);
assert_eq!(pro_yearly.price_cents, 299_000);
assert_eq!(
check_expired_membership_upgrade_settlement(158_334, pro_yearly.price_cents),
Err(RuntimeProfileMembershipUpgradeRejection::MembershipExpiredBeforeSettlement)
);
// 中文注释:订单金额本身已覆盖整期新购价(改价后挡位降价的容错路径)时按新购结算。
assert_eq!(
check_expired_membership_upgrade_settlement(pro_yearly.price_cents, 299_000),
Ok(())
);
assert_eq!(
check_expired_membership_upgrade_settlement(
pro_yearly.price_cents + 1,
pro_yearly.price_cents
),
Ok(())
);
}
}
@@ -9832,7 +9832,9 @@ fn build_profile_membership_snapshot(
/// - 结算口径以订单建单时冻结的快照为准:档位、每期泥点、补点与价格都不再回查目录,
/// 因此下单后改价 / 改每期泥点 / 下架档位都不会影响已受理订单;历史 Pending 订单回退到按目录重算。
/// - 有效期内:只允许「同周期升更高档位」,按快照补点,刷新日与到期日不变。
/// - 无有效会员(从未开通或已到期):按快照重新起算,锚点就是本次支付时刻。
/// - 无有效会员(从未开通或已到期):按快照重新起算,锚点就是本次支付时刻;
/// 但补差价订单结算时会员已到期,必须先满足 [`check_expired_membership_upgrade_settlement`],
/// 否则拒绝——补差价只覆盖当期剩余权益的差额,不覆盖整期新购价。
///
/// 幂等锚点是**订单维度**的账本 id,重复支付回调天然幂等。
fn apply_profile_membership_purchase(
@@ -9872,7 +9874,13 @@ fn apply_profile_membership_purchase(
purchased_at,
);
}
// 中文注释:会员在支付前已到期,退化为按冻结档位重新起算,避免已付款订单卡死。
// 中文注释:会员在支付前已到期,冻结的补差价只覆盖当期剩余权益的差额,
// 不再覆盖整期新购价;只有订单金额达到目标周期当前目录价才按新购结算,
// 否则拒绝,避免用户用零头补差价换到一个完整周期。
let target = membership_plan_snapshot(ctx, settlement.plan, settlement.cycle_kind)
.ok_or_else(|| format!("会员目录缺少档位 {}", settlement.plan.as_str()))?;
check_expired_membership_upgrade_settlement(order.amount_cents, target.price_cents)
.map_err(|_| "会员已到期,请按新购价格重新下单".to_string())?;
}
apply_profile_membership_fresh_purchase(