重构(计费): 余额接口并列暴露三池并去掉会员 tier 字段

- 余额响应改为每日免费 / 月度 / 永久三池并列,不再暴露总额与会员限时泥点
- 会员响应与充值中心以 plan 取代 tier,充值中心改为输出会员目录
- 微信虚拟支付按目录档位校验会员商品,不再依赖会员商品配置行
- 同步 shared-contracts 测试夹具到新的三池与 plan 口径

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-10-02 16:59:49 +08:00
parent e98564e13c
commit 703a24b682
2 changed files with 200 additions and 266 deletions
File diff suppressed because it is too large Load Diff
@@ -35,14 +35,6 @@ pub const PROFILE_TASK_STATUS_CLAIMED: &str = "claimed";
pub const PROFILE_TASK_STATUS_DISABLED: &str = "disabled";
pub const PROFILE_RECHARGE_PRODUCT_KIND_POINTS: &str = "points";
pub const PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP: &str = "membership";
pub const PROFILE_MEMBERSHIP_TIER_NORMAL: &str = "normal";
pub const PROFILE_MEMBERSHIP_TIER_MONTH: &str = "month";
pub const PROFILE_MEMBERSHIP_TIER_SEASON: &str = "season";
pub const PROFILE_MEMBERSHIP_TIER_YEAR: &str = "year";
pub const PROFILE_MEMBERSHIP_TIER_STARTER: &str = "starter";
pub const PROFILE_MEMBERSHIP_TIER_BASIC: &str = "basic";
pub const PROFILE_MEMBERSHIP_TIER_PRO: &str = "pro";
pub const PROFILE_MEMBERSHIP_TIER_ULTIMATE: &str = "ultimate";
pub const PROFILE_FEEDBACK_STATUS_OPEN: &str = "open";
pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
@@ -210,13 +202,15 @@ pub struct ProfileDailyFreePointsResponse {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMudPointBalanceResponse {
pub total_points: u64,
pub permanent_points: u64,
pub limited_points: u64,
pub limited_expires_at: Option<String>,
/// 每日免费泥点:北京业务日有效,跨日清零并重置。
pub daily_free_points: u64,
pub daily_free_reset_points: u64,
pub daily_free_resets_at: String,
/// 月度泥点:会员每期额度,到刷新时刻清零再发新额度。
pub monthly_points: u64,
pub monthly_resets_at: Option<String>,
/// 永久泥点:不过期,创作照常扣点。
pub permanent_points: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -249,32 +243,32 @@ pub struct ProfileRechargeProductResponse {
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: String,
pub membership_period_points: u64,
pub membership_period_days: u32,
pub membership_queue_limit: u32,
pub membership_discount_bps: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipBenefitResponse {
pub benefit_name: String,
pub normal_value: String,
pub month_value: String,
pub season_value: String,
pub year_value: String,
pub starter_value: String,
pub basic_value: String,
pub pro_value: String,
pub ultimate_value: String,
pub struct ProfileMembershipPlanResponse {
/// 档位 token:`normal` / `starter` / `plus` / `pro` / `max`。
pub plan: String,
pub title: String,
pub rank: u8,
pub month_price_cents: u64,
pub year_price_cents: u64,
pub period_points: u64,
/// 模型权限 token:`basic` / `full`。
pub model_access: String,
/// 并发上限;`128` 表示不设上限。
pub concurrent_job_limit: u32,
pub sort_order: i32,
pub enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipResponse {
pub status: String,
pub tier: String,
/// 当前档位 token;非会员与已到期都是 `normal`。
pub plan: String,
pub started_at: Option<String>,
pub expires_at: Option<String>,
pub updated_at: Option<String>,
@@ -282,7 +276,12 @@ pub struct ProfileMembershipResponse {
pub cycle_resets_at: Option<String>,
pub cycle_granted_points: u64,
pub cycle_remaining_points: u64,
pub cycle_period_days: u32,
/// 当前期序号(1-based)。
pub cycle_index: u32,
/// 本次购买总期数(月付 1 / 年付 12)。
pub cycle_count: u32,
/// 周期 token:`monthly` / `yearly`。
pub cycle_kind: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -350,12 +349,11 @@ pub struct WechatNativePaymentResponse {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileRechargeCenterResponse {
pub wallet_balance: u64,
pub mud_point_balance: ProfileMudPointBalanceResponse,
pub membership: ProfileMembershipResponse,
pub point_products: Vec<ProfileRechargeProductResponse>,
pub membership_products: Vec<ProfileRechargeProductResponse>,
pub benefits: Vec<ProfileMembershipBenefitResponse>,
/// 会员档位目录(后台可改价),会员购买 / 升级的价格与权益都以它为准。
pub membership_plans: Vec<ProfileMembershipPlanResponse>,
pub latest_order: Option<ProfileRechargeOrderResponse>,
pub has_points_recharged: bool,
pub daily_free_points: ProfileDailyFreePointsResponse,
@@ -577,11 +575,6 @@ pub struct ProfileRechargeProductConfigAdminResponse {
pub duration_days: u32,
pub badge_label: String,
pub description: String,
pub tier: String,
pub membership_period_points: u64,
pub membership_period_days: u32,
pub membership_queue_limit: u32,
pub membership_discount_bps: u32,
pub enabled: bool,
pub sort_order: i32,
pub created_by: String,
@@ -666,15 +659,6 @@ pub struct AdminUpsertProfileRechargeProductRequest {
pub badge_label: Option<String>,
#[serde(default)]
pub description: Option<String>,
pub tier: String,
#[serde(default)]
pub membership_period_points: u64,
#[serde(default)]
pub membership_period_days: u32,
#[serde(default)]
pub membership_queue_limit: u32,
#[serde(default)]
pub membership_discount_bps: u32,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
@@ -1325,13 +1309,12 @@ mod tests {
fn empty_mud_point_balance() -> ProfileMudPointBalanceResponse {
ProfileMudPointBalanceResponse {
total_points: 0,
permanent_points: 0,
limited_points: 0,
limited_expires_at: None,
daily_free_points: 0,
daily_free_reset_points: 20,
daily_free_resets_at: "2026-07-12T16:00:00Z".to_string(),
monthly_points: 0,
monthly_resets_at: None,
permanent_points: 0,
}
}
@@ -1608,27 +1591,27 @@ mod tests {
#[test]
fn profile_recharge_center_response_uses_camel_case_fields() {
let payload = serde_json::to_value(ProfileRechargeCenterResponse {
wallet_balance: 29,
mud_point_balance: ProfileMudPointBalanceResponse {
total_points: 29,
permanent_points: 4,
limited_points: 5,
limited_expires_at: Some("2026-05-25T10:00:00Z".to_string()),
daily_free_points: 20,
daily_free_reset_points: 20,
daily_free_resets_at: "2026-07-12T16:00:00Z".to_string(),
monthly_points: 5,
monthly_resets_at: Some("2026-05-25T10:00:00Z".to_string()),
permanent_points: 4,
},
membership: ProfileMembershipResponse {
status: "active".to_string(),
tier: "month".to_string(),
plan: "plus".to_string(),
started_at: Some("2026-04-25T10:00:00Z".to_string()),
expires_at: Some("2026-05-25T10:00:00Z".to_string()),
updated_at: Some("2026-04-25T10:00:00Z".to_string()),
cycle_started_at: Some("2026-04-25T10:00:00Z".to_string()),
cycle_resets_at: Some("2026-05-25T10:00:00Z".to_string()),
cycle_granted_points: 800,
cycle_granted_points: 1_150,
cycle_remaining_points: 320,
cycle_period_days: 30,
cycle_index: 1,
cycle_count: 1,
cycle_kind: "monthly".to_string(),
},
point_products: vec![ProfileRechargeProductResponse {
product_id: "points_60".to_string(),
@@ -1638,16 +1621,21 @@ mod tests {
points_amount: 60,
bonus_points: 60,
duration_days: 0,
badge_label: "首充双倍".to_string(),
description: "首充送60泥点".to_string(),
tier: "normal".to_string(),
membership_period_points: 0,
membership_period_days: 0,
membership_queue_limit: 0,
membership_discount_bps: 0,
badge_label: String::new(),
description: "60泥点".to_string(),
}],
membership_plans: vec![ProfileMembershipPlanResponse {
plan: "plus".to_string(),
title: "Plus".to_string(),
rank: 2,
month_price_cents: 9_900,
year_price_cents: 99_000,
period_points: 1_150,
model_access: "full".to_string(),
concurrent_job_limit: 3,
sort_order: 2,
enabled: true,
}],
membership_products: vec![],
benefits: vec![],
latest_order: None,
has_points_recharged: false,
daily_free_points: ProfileDailyFreePointsResponse {
@@ -1660,21 +1648,28 @@ mod tests {
})
.expect("payload should serialize");
assert_eq!(payload["walletBalance"], json!(29));
assert_eq!(payload["mudPointBalance"]["permanentPoints"], json!(4));
assert_eq!(payload["mudPointBalance"]["limitedPoints"], json!(5));
assert_eq!(payload["mudPointBalance"]["monthlyPoints"], json!(5));
assert_eq!(payload["mudPointBalance"]["dailyFreePoints"], json!(20));
assert_eq!(
payload["mudPointBalance"]["monthlyResetsAt"],
json!("2026-05-25T10:00:00Z")
);
assert_eq!(payload["membership"]["plan"], json!("plus"));
assert_eq!(
payload["membership"]["expiresAt"],
json!("2026-05-25T10:00:00Z")
);
assert_eq!(payload["membership"]["cycleRemainingPoints"], json!(320));
assert_eq!(payload["membership"]["cycleKind"], json!("monthly"));
assert_eq!(payload["pointProducts"][0]["productId"], json!("points_60"));
assert_eq!(payload["pointProducts"][0]["title"], json!("60泥点"));
assert_eq!(payload["pointProducts"][0]["priceCents"], json!(600));
assert_eq!(payload["pointProducts"][0]["description"], json!("60泥点"));
assert_eq!(payload["membershipPlans"][0]["plan"], json!("plus"));
assert_eq!(
payload["pointProducts"][0]["description"],
json!("首充送60泥点")
payload["membershipPlans"][0]["concurrentJobLimit"],
json!(3)
);
assert_eq!(payload["hasPointsRecharged"], json!(false));
assert_eq!(payload["dailyFreePoints"]["grantedPoints"], json!(20));
@@ -1712,11 +1707,10 @@ mod tests {
expiration_last_error: None,
};
let center = ProfileRechargeCenterResponse {
wallet_balance: 0,
mud_point_balance: empty_mud_point_balance(),
membership: ProfileMembershipResponse {
status: "normal".to_string(),
tier: "normal".to_string(),
plan: "normal".to_string(),
started_at: None,
expires_at: None,
updated_at: None,
@@ -1724,11 +1718,12 @@ mod tests {
cycle_resets_at: None,
cycle_granted_points: 0,
cycle_remaining_points: 0,
cycle_period_days: 30,
cycle_index: 1,
cycle_count: 1,
cycle_kind: "monthly".to_string(),
},
point_products: vec![],
membership_products: vec![],
benefits: vec![],
membership_plans: vec![],
latest_order: None,
has_points_recharged: false,
daily_free_points: ProfileDailyFreePointsResponse {
@@ -1788,11 +1783,10 @@ mod tests {
expiration_last_error: None,
};
let center = ProfileRechargeCenterResponse {
wallet_balance: 0,
mud_point_balance: empty_mud_point_balance(),
membership: ProfileMembershipResponse {
status: "normal".to_string(),
tier: "normal".to_string(),
plan: "normal".to_string(),
started_at: None,
expires_at: None,
updated_at: None,
@@ -1800,11 +1794,12 @@ mod tests {
cycle_resets_at: None,
cycle_granted_points: 0,
cycle_remaining_points: 0,
cycle_period_days: 30,
cycle_index: 1,
cycle_count: 1,
cycle_kind: "monthly".to_string(),
},
point_products: vec![],
membership_products: vec![],
benefits: vec![],
membership_plans: vec![],
latest_order: None,
has_points_recharged: false,
daily_free_points: ProfileDailyFreePointsResponse {