From 703a24b682148c3e258446ed5da9dd5e8d272cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Fri, 2 Oct 2026 16:59:49 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84(=E8=AE=A1=E8=B4=B9):=20?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=E6=8E=A5=E5=8F=A3=E5=B9=B6=E5=88=97=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2=E4=B8=89=E6=B1=A0=E5=B9=B6=E5=8E=BB=E6=8E=89=E4=BC=9A?= =?UTF-8?q?=E5=91=98=20tier=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 余额响应改为每日免费 / 月度 / 永久三池并列,不再暴露总额与会员限时泥点 - 会员响应与充值中心以 plan 取代 tier,充值中心改为输出会员目录 - 微信虚拟支付按目录档位校验会员商品,不再依赖会员商品配置行 - 同步 shared-contracts 测试夹具到新的三池与 plan 口径 Co-authored-by: Junie --- .../crates/api-server/src/runtime_profile.rs | 309 +++++++----------- .../crates/shared-contracts/src/runtime.rs | 157 +++++---- 2 files changed, 200 insertions(+), 266 deletions(-) diff --git a/server-rs/crates/api-server/src/runtime_profile.rs b/server-rs/crates/api-server/src/runtime_profile.rs index d92ae2454..c656ec873 100644 --- a/server-rs/crates/api-server/src/runtime_profile.rs +++ b/server-rs/crates/api-server/src/runtime_profile.rs @@ -16,16 +16,15 @@ use module_runtime::{ PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE, RuntimeProfileCodeOperationRecord, RuntimeProfileDailyFreePointsRecord, RuntimeProfileFeedbackEvidenceRecord, RuntimeProfileFeedbackEvidenceSnapshot, RuntimeProfileFeedbackSubmissionRecord, - RuntimeProfileInviteCodeRecord, RuntimeProfileMembershipBenefitRecord, - RuntimeProfileMembershipTier, RuntimeProfileRechargeCenterRecord, - RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus, - RuntimeProfileRechargeProductConfigRecord, RuntimeProfileRechargeProductKind, - RuntimeProfileRechargeProductRecord, RuntimeProfileRedeemCodeMode, - RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord, - RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord, - RuntimeProfileTaskCycle, RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus, - RuntimeProfileWalletLedgerSourceType, RuntimeReferralInviteCenterRecord, - RuntimeTrackingScopeKind, + RuntimeProfileInviteCodeRecord, RuntimeProfileMembershipPlanRecord, + RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord, + RuntimeProfileRechargeOrderStatus, RuntimeProfileRechargeProductConfigRecord, + RuntimeProfileRechargeProductKind, RuntimeProfileRechargeProductRecord, + RuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord, + RuntimeProfileRewardCodeRedeemRecord, RuntimeProfileTaskCenterRecord, + RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord, RuntimeProfileTaskCycle, + RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus, RuntimeProfileWalletLedgerSourceType, + RuntimeReferralInviteCenterRecord, RuntimeTrackingScopeKind, }; use platform_wechat::pay::{WechatPayError, WechatPayNotifyOrder}; use serde::{Deserialize, Serialize}; @@ -40,9 +39,6 @@ use shared_contracts::runtime::{ AnalyticsBucketMetricResponse, AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse, ConfirmWechatProfileRechargeOrderResponse, CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse, PROFILE_FEEDBACK_STATUS_OPEN, - PROFILE_MEMBERSHIP_TIER_BASIC, PROFILE_MEMBERSHIP_TIER_MONTH, PROFILE_MEMBERSHIP_TIER_NORMAL, - PROFILE_MEMBERSHIP_TIER_PRO, PROFILE_MEMBERSHIP_TIER_SEASON, PROFILE_MEMBERSHIP_TIER_STARTER, - PROFILE_MEMBERSHIP_TIER_ULTIMATE, PROFILE_MEMBERSHIP_TIER_YEAR, PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP, PROFILE_RECHARGE_PRODUCT_KIND_POINTS, PROFILE_TASK_CYCLE_DAILY, PROFILE_TASK_STATUS_CLAIMABLE, PROFILE_TASK_STATUS_CLAIMED, PROFILE_TASK_STATUS_DISABLED, PROFILE_TASK_STATUS_INCOMPLETE, @@ -65,7 +61,7 @@ use shared_contracts::runtime::{ ProfileDailyFreePointsResponse, ProfileDashboardSummaryResponse, ProfileFeedbackEvidenceItemResponse, ProfileFeedbackSubmissionResponse, ProfileInviteCodeAdminListResponse, ProfileInviteCodeAdminResponse, - ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfileMudPointBalanceResponse, + ProfileMembershipPlanResponse, ProfileMembershipResponse, ProfileMudPointBalanceResponse, ProfileRechargeCenterResponse, ProfileRechargeOrderResponse, ProfileRechargeProductConfigAdminListResponse, ProfileRechargeProductConfigAdminResponse, ProfileRechargeProductResponse, ProfileRedeemCodeAdminListResponse, @@ -1089,12 +1085,6 @@ pub async fn admin_upsert_profile_recharge_product( AppError::from_status(StatusCode::BAD_REQUEST).with_message(error), ) })?; - let tier = parse_profile_membership_tier(&payload.tier).map_err(|error| { - runtime_profile_error_response( - &request_context, - AppError::from_status(StatusCode::BAD_REQUEST).with_message(error), - ) - })?; let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000; let record = state .spacetime_client() @@ -1109,11 +1099,6 @@ pub async fn admin_upsert_profile_recharge_product( payload.duration_days, payload.badge_label.unwrap_or_default(), payload.description.unwrap_or_default(), - tier, - payload.membership_period_points, - payload.membership_period_days, - payload.membership_queue_limit, - payload.membership_discount_bps, payload.enabled, payload.sort_order.unwrap_or(10), updated_at_micros as i64, @@ -1685,23 +1670,30 @@ fn build_wechat_virtual_pay_params( order: &RuntimeProfileRechargeOrderRecord, openid: &str, ) -> Result { - let product = match order.kind { + let buy_quantity = match order.kind { RuntimeProfileRechargeProductKind::Points => center .point_products .iter() .find(|item| item.product_id == order.product_id) + .map(|item| item.points_amount) .ok_or_else(|| { AppError::from_status(StatusCode::BAD_REQUEST) .with_message("当前充值商品不存在,请刷新后再试") })?, - RuntimeProfileRechargeProductKind::Membership => center - .membership_products - .iter() - .find(|item| item.product_id == order.product_id) - .ok_or_else(|| { - AppError::from_status(StatusCode::BAD_REQUEST) - .with_message("当前充值商品不存在,请刷新后再试") - })?, + RuntimeProfileRechargeProductKind::Membership => { + // 会员商品不在商品配置表里:确认档位仍在目录中即可,购买数量固定为 1。 + let (plan, _) = + module_runtime::parse_runtime_profile_membership_product_id(&order.product_id) + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST) + .with_message("当前会员套餐不存在,请刷新后再试") + })?; + if !center.membership_plans.iter().any(|item| item.plan == plan) { + return Err(AppError::from_status(StatusCode::BAD_REQUEST) + .with_message("当前会员套餐不存在,请刷新后再试")); + } + 1 + } }; let identity = state .wechat_auth_service() @@ -1729,10 +1721,6 @@ fn build_wechat_virtual_pay_params( RuntimeProfileRechargeProductKind::Points => "short_series_coin", RuntimeProfileRechargeProductKind::Membership => "short_series_goods", }; - let buy_quantity = match product.kind { - RuntimeProfileRechargeProductKind::Points => product.points_amount, - RuntimeProfileRechargeProductKind::Membership => 1, - }; let mut sign_data = serde_json::json!({ "offerId": offer_id, "buyQuantity": buy_quantity, @@ -1846,11 +1834,10 @@ fn build_profile_recharge_center_response( ) -> ProfileRechargeCenterResponse { let mud_point_balance = build_profile_mud_point_balance_response(&record); ProfileRechargeCenterResponse { - wallet_balance: record.wallet_balance, mud_point_balance, membership: ProfileMembershipResponse { status: record.membership.status.as_str().to_string(), - tier: record.membership.tier.as_str().to_string(), + plan: record.membership.plan.as_str().to_string(), started_at: record.membership.started_at, expires_at: record.membership.expires_at, updated_at: record.membership.updated_at, @@ -1858,22 +1845,19 @@ fn build_profile_recharge_center_response( cycle_resets_at: record.membership.cycle_resets_at, cycle_granted_points: record.membership.cycle_granted_points, cycle_remaining_points: record.membership.cycle_remaining_points, - cycle_period_days: record.membership.cycle_period_days, + cycle_index: record.membership.cycle_index, + cycle_count: record.membership.cycle_count, + cycle_kind: record.membership.cycle_kind.as_str().to_string(), }, point_products: record .point_products .into_iter() .map(build_profile_recharge_product_response) .collect(), - membership_products: record - .membership_products + membership_plans: record + .membership_plans .into_iter() - .map(build_profile_recharge_product_response) - .collect(), - benefits: record - .benefits - .into_iter() - .map(build_profile_membership_benefit_response) + .map(build_profile_membership_plan_response) .collect(), latest_order: record .latest_order @@ -1887,12 +1871,13 @@ fn build_profile_mud_point_balance_response( record: &RuntimeProfileRechargeCenterRecord, ) -> ProfileMudPointBalanceResponse { let daily_free_points = record.daily_free_points.remaining_points; - let limited_points = record.membership.cycle_remaining_points; + let monthly_points = record.membership.cycle_remaining_points; + // 永久泥点 = 单一总额 − 每日免费 − 月度;存储层仍是 `wallet_balance` 单一权威。 let permanent_points = record .wallet_balance .saturating_sub(daily_free_points) - .saturating_sub(limited_points); - let limited_expires_at = (limited_points > 0) + .saturating_sub(monthly_points); + let monthly_resets_at = (monthly_points > 0) .then(|| { record .membership @@ -1903,13 +1888,12 @@ fn build_profile_mud_point_balance_response( .flatten(); ProfileMudPointBalanceResponse { - total_points: record.wallet_balance, - permanent_points, - limited_points, - limited_expires_at, daily_free_points, daily_free_reset_points: record.daily_free_points.reset_points, daily_free_resets_at: record.daily_free_points.resets_at.clone(), + monthly_points, + monthly_resets_at, + permanent_points, } } @@ -1938,27 +1922,24 @@ fn build_profile_recharge_product_response( duration_days: record.duration_days, badge_label: record.badge_label, description: record.description, - tier: record.tier.as_str().to_string(), - membership_period_points: record.membership_period_points, - membership_period_days: record.membership_period_days, - membership_queue_limit: record.membership_queue_limit, - membership_discount_bps: record.membership_discount_bps, } } -fn build_profile_membership_benefit_response( - record: RuntimeProfileMembershipBenefitRecord, -) -> ProfileMembershipBenefitResponse { - ProfileMembershipBenefitResponse { - benefit_name: record.benefit_name, - normal_value: record.normal_value, - month_value: record.month_value, - season_value: record.season_value, - year_value: record.year_value, - starter_value: record.starter_value, - basic_value: record.basic_value, - pro_value: record.pro_value, - ultimate_value: record.ultimate_value, +/// 会员档位目录 → 对外响应;`concurrent_job_limit` 的 `128` 哨兵直接透出,由前端展示为“不设上限”。 +fn build_profile_membership_plan_response( + record: RuntimeProfileMembershipPlanRecord, +) -> ProfileMembershipPlanResponse { + ProfileMembershipPlanResponse { + plan: record.plan.as_str().to_string(), + title: record.title, + rank: record.rank, + month_price_cents: record.month_price_cents, + year_price_cents: record.year_price_cents, + period_points: record.period_points, + model_access: record.model_access.as_str().to_string(), + concurrent_job_limit: record.concurrent_job_limit, + sort_order: record.sort_order, + enabled: record.enabled, } } @@ -2211,11 +2192,6 @@ fn build_profile_recharge_product_config_admin_response( duration_days: record.duration_days, badge_label: record.badge_label, description: record.description, - tier: format_profile_membership_tier(record.tier).to_string(), - membership_period_points: record.membership_period_points, - membership_period_days: record.membership_period_days, - membership_queue_limit: record.membership_queue_limit, - membership_discount_bps: record.membership_discount_bps, enabled: record.enabled, sort_order: record.sort_order, created_by: record.created_by, @@ -2318,20 +2294,6 @@ fn parse_profile_recharge_product_kind( } } -fn parse_profile_membership_tier(raw: &str) -> Result { - match raw.trim().to_ascii_lowercase().as_str() { - PROFILE_MEMBERSHIP_TIER_NORMAL => Ok(RuntimeProfileMembershipTier::Normal), - PROFILE_MEMBERSHIP_TIER_MONTH => Ok(RuntimeProfileMembershipTier::Month), - PROFILE_MEMBERSHIP_TIER_SEASON => Ok(RuntimeProfileMembershipTier::Season), - PROFILE_MEMBERSHIP_TIER_YEAR => Ok(RuntimeProfileMembershipTier::Year), - PROFILE_MEMBERSHIP_TIER_STARTER => Ok(RuntimeProfileMembershipTier::Starter), - PROFILE_MEMBERSHIP_TIER_BASIC => Ok(RuntimeProfileMembershipTier::Basic), - PROFILE_MEMBERSHIP_TIER_PRO => Ok(RuntimeProfileMembershipTier::Pro), - PROFILE_MEMBERSHIP_TIER_ULTIMATE => Ok(RuntimeProfileMembershipTier::Ultimate), - _ => Err("会员档位无效".to_string()), - } -} - fn parse_tracking_scope_kind(raw: &str) -> Result { match raw.trim().to_ascii_lowercase().as_str() { TRACKING_SCOPE_KIND_SITE => Ok(RuntimeTrackingScopeKind::Site), @@ -2375,19 +2337,6 @@ fn format_profile_recharge_product_kind(kind: RuntimeProfileRechargeProductKind) } } -fn format_profile_membership_tier(tier: RuntimeProfileMembershipTier) -> &'static str { - match tier { - RuntimeProfileMembershipTier::Normal => PROFILE_MEMBERSHIP_TIER_NORMAL, - RuntimeProfileMembershipTier::Month => PROFILE_MEMBERSHIP_TIER_MONTH, - RuntimeProfileMembershipTier::Season => PROFILE_MEMBERSHIP_TIER_SEASON, - RuntimeProfileMembershipTier::Year => PROFILE_MEMBERSHIP_TIER_YEAR, - RuntimeProfileMembershipTier::Starter => PROFILE_MEMBERSHIP_TIER_STARTER, - RuntimeProfileMembershipTier::Basic => PROFILE_MEMBERSHIP_TIER_BASIC, - RuntimeProfileMembershipTier::Pro => PROFILE_MEMBERSHIP_TIER_PRO, - RuntimeProfileMembershipTier::Ultimate => PROFILE_MEMBERSHIP_TIER_ULTIMATE, - } -} - fn format_tracking_scope_kind(scope_kind: RuntimeTrackingScopeKind) -> &'static str { match scope_kind { RuntimeTrackingScopeKind::Site => TRACKING_SCOPE_KIND_SITE, @@ -2454,11 +2403,13 @@ mod tests { use module_auth::{ResolveWechatLoginInput, WechatIdentityProfile}; use module_runtime::{ PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL, - RuntimeProfileDailyFreePointsRecord, RuntimeProfileMembershipRecord, - RuntimeProfileMembershipStatus, RuntimeProfileMembershipTier, - RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord, - RuntimeProfileRechargeOrderStatus, RuntimeProfileRechargeProductKind, - RuntimeProfileRechargeProductRecord, RuntimeProfileWalletLedgerSourceType, + RuntimeProfileDailyFreePointsRecord, RuntimeProfileMembershipCycleKind, + RuntimeProfileMembershipModelAccess, RuntimeProfileMembershipPlan, + RuntimeProfileMembershipPlanRecord, RuntimeProfileMembershipRecord, + RuntimeProfileMembershipStatus, RuntimeProfileRechargeCenterRecord, + RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus, + RuntimeProfileRechargeProductKind, RuntimeProfileRechargeProductRecord, + RuntimeProfileWalletLedgerSourceType, }; use super::{ @@ -2566,7 +2517,7 @@ mod tests { membership: RuntimeProfileMembershipRecord { user_id: "user-1".to_string(), status: RuntimeProfileMembershipStatus::Active, - tier: RuntimeProfileMembershipTier::Starter, + plan: RuntimeProfileMembershipPlan::Starter, started_at: Some("2026-07-01T00:00:00Z".to_string()), started_at_micros: Some(1_783_036_800_000_000), expires_at: Some("2026-08-01T00:00:00Z".to_string()), @@ -2579,11 +2530,12 @@ mod tests { cycle_resets_at_micros: Some(1_784_073_600_000_000), cycle_granted_points: 80, cycle_remaining_points: 80, - cycle_period_days: 7, + cycle_index: 1, + cycle_count: 1, + cycle_kind: RuntimeProfileMembershipCycleKind::Monthly, }, point_products: vec![], - membership_products: vec![], - benefits: vec![], + membership_plans: vec![], latest_order: None, has_points_recharged: false, daily_free_points: daily_free_points_record(), @@ -2591,13 +2543,12 @@ mod tests { let balance = build_profile_mud_point_balance_response(&record); - assert_eq!(balance.total_points, 200); assert_eq!(balance.permanent_points, 100); - assert_eq!(balance.limited_points, 80); + assert_eq!(balance.monthly_points, 80); assert_eq!(balance.daily_free_points, 20); assert_eq!(balance.daily_free_reset_points, 35); assert_eq!( - balance.limited_expires_at.as_deref(), + balance.monthly_resets_at.as_deref(), Some("2026-07-15T00:00:00Z"), ); assert_eq!(balance.daily_free_resets_at, "2026-07-12T16:00:00Z"); @@ -3418,10 +3369,10 @@ mod tests { let order = RuntimeProfileRechargeOrderRecord { order_id: "memberorder01".to_string(), user_id: user_id.clone(), - product_id: "member_month".to_string(), - product_title: "月卡".to_string(), + product_id: "membership-starter-yearly".to_string(), + product_title: "Starter·年付".to_string(), kind: RuntimeProfileRechargeProductKind::Membership, - amount_cents: 2800, + amount_cents: 39000, status: RuntimeProfileRechargeOrderStatus::Pending, payment_channel: PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL .to_string(), @@ -3447,7 +3398,7 @@ mod tests { membership: RuntimeProfileMembershipRecord { user_id: user_id.clone(), status: RuntimeProfileMembershipStatus::Normal, - tier: RuntimeProfileMembershipTier::Normal, + plan: RuntimeProfileMembershipPlan::Normal, started_at: None, started_at_micros: None, expires_at: None, @@ -3460,26 +3411,24 @@ mod tests { cycle_resets_at_micros: None, cycle_granted_points: 0, cycle_remaining_points: 0, - cycle_period_days: 30, + cycle_index: 1, + cycle_count: 12, + cycle_kind: RuntimeProfileMembershipCycleKind::Yearly, }, point_products: vec![], - membership_products: vec![RuntimeProfileRechargeProductRecord { - product_id: "member_month".to_string(), - title: "月卡".to_string(), - price_cents: 2800, - kind: RuntimeProfileRechargeProductKind::Membership, - points_amount: 0, - bonus_points: 0, - duration_days: 30, - badge_label: String::new(), - description: "30天会员".to_string(), - tier: RuntimeProfileMembershipTier::Month, - membership_period_points: 0, - membership_period_days: 30, - membership_queue_limit: 0, - membership_discount_bps: 0, + membership_plans: vec![RuntimeProfileMembershipPlanRecord { + plan: RuntimeProfileMembershipPlan::Starter, + title: "Starter".to_string(), + rank: 1, + month_price_cents: 3900, + year_price_cents: 39000, + period_points: 400, + model_access: RuntimeProfileMembershipModelAccess::Basic, + concurrent_job_limit: 1, + sort_order: 1, + enabled: true, }], - benefits: vec![], + latest_order: None, has_points_recharged: false, daily_free_points: daily_free_points_record(), @@ -3500,8 +3449,8 @@ mod tests { assert_eq!(params.mode, "short_series_goods"); assert_eq!(sign_data["buyQuantity"], 1); assert_eq!(sign_data["offerId"], "offer-1"); - assert_eq!(sign_data["productId"], "member_month"); - assert_eq!(sign_data["goodsPrice"], 2800); + assert_eq!(sign_data["productId"], "membership-starter-yearly"); + assert_eq!(sign_data["goodsPrice"], 39000); assert_eq!(sign_data["outTradeNo"], "memberorder01"); assert_eq!(attach["paymentChannel"], "wechat_mp_virtual"); assert!(!params.pay_sig.is_empty()); @@ -3563,7 +3512,7 @@ mod tests { membership: RuntimeProfileMembershipRecord { user_id: user_id.clone(), status: RuntimeProfileMembershipStatus::Normal, - tier: RuntimeProfileMembershipTier::Normal, + plan: RuntimeProfileMembershipPlan::Normal, started_at: None, started_at_micros: None, expires_at: None, @@ -3576,7 +3525,9 @@ mod tests { cycle_resets_at_micros: None, cycle_granted_points: 0, cycle_remaining_points: 0, - cycle_period_days: 30, + cycle_index: 1, + cycle_count: 12, + cycle_kind: RuntimeProfileMembershipCycleKind::Yearly, }, point_products: vec![RuntimeProfileRechargeProductRecord { product_id: "points_60".to_string(), @@ -3588,14 +3539,8 @@ mod tests { duration_days: 0, badge_label: "首充双倍".to_string(), description: "60+60泥点".to_string(), - tier: RuntimeProfileMembershipTier::Normal, - membership_period_points: 0, - membership_period_days: 0, - membership_queue_limit: 0, - membership_discount_bps: 0, }], - membership_products: vec![], - benefits: vec![], + membership_plans: vec![], latest_order: None, has_points_recharged: true, daily_free_points: daily_free_points_record(), @@ -3623,7 +3568,7 @@ mod tests { } #[tokio::test] - async fn wechat_virtual_pay_params_accept_admin_membership_product_ids() { + async fn wechat_virtual_pay_params_accept_catalog_membership_product_ids() { let state = seed_authenticated_state_with_config(AppConfig { wechat_mini_program_virtual_payment_offer_id: Some("offer-1".to_string()), wechat_mini_program_virtual_payment_app_key: Some("app-key-1".to_string()), @@ -3646,12 +3591,12 @@ mod tests { .expect("wechat identity should seed"); let user_id = wechat_login.user.id.clone(); let order = RuntimeProfileRechargeOrderRecord { - order_id: "item01order01".to_string(), + order_id: "starterorder01".to_string(), user_id: user_id.clone(), - product_id: "item01".to_string(), - product_title: "测试道具".to_string(), + product_id: "membership-starter-monthly".to_string(), + product_title: "Starter·月付".to_string(), kind: RuntimeProfileRechargeProductKind::Membership, - amount_cents: 100, + amount_cents: 3900, status: RuntimeProfileRechargeOrderStatus::Pending, payment_channel: PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL .to_string(), @@ -3677,7 +3622,7 @@ mod tests { membership: RuntimeProfileMembershipRecord { user_id: user_id.clone(), status: RuntimeProfileMembershipStatus::Normal, - tier: RuntimeProfileMembershipTier::Normal, + plan: RuntimeProfileMembershipPlan::Normal, started_at: None, started_at_micros: None, expires_at: None, @@ -3690,26 +3635,24 @@ mod tests { cycle_resets_at_micros: None, cycle_granted_points: 0, cycle_remaining_points: 0, - cycle_period_days: 30, + cycle_index: 1, + cycle_count: 12, + cycle_kind: RuntimeProfileMembershipCycleKind::Yearly, }, point_products: vec![], - membership_products: vec![RuntimeProfileRechargeProductRecord { - product_id: "item01".to_string(), - title: "测试道具".to_string(), - price_cents: 100, - kind: RuntimeProfileRechargeProductKind::Membership, - points_amount: 0, - bonus_points: 0, - duration_days: 30, - badge_label: String::new(), - description: "30天会员".to_string(), - tier: RuntimeProfileMembershipTier::Month, - membership_period_points: 0, - membership_period_days: 30, - membership_queue_limit: 0, - membership_discount_bps: 0, + membership_plans: vec![RuntimeProfileMembershipPlanRecord { + plan: RuntimeProfileMembershipPlan::Starter, + title: "Starter".to_string(), + rank: 1, + month_price_cents: 3900, + year_price_cents: 39000, + period_points: 400, + model_access: RuntimeProfileMembershipModelAccess::Basic, + concurrent_job_limit: 1, + sort_order: 1, + enabled: true, }], - benefits: vec![], + latest_order: None, has_points_recharged: false, daily_free_points: daily_free_points_record(), @@ -3721,9 +3664,9 @@ mod tests { serde_json::from_str(¶ms.sign_data).expect("sign data should be valid json"); assert_eq!(params.mode, "short_series_goods"); - assert_eq!(sign_data["productId"], "item01"); - assert_eq!(sign_data["goodsPrice"], 100); - assert_eq!(sign_data["outTradeNo"], "item01order01"); + assert_eq!(sign_data["productId"], "membership-starter-monthly"); + assert_eq!(sign_data["goodsPrice"], 3900); + assert_eq!(sign_data["outTradeNo"], "starterorder01"); } #[test] @@ -3872,7 +3815,7 @@ mod tests { membership: RuntimeProfileMembershipRecord { user_id: user_id.clone(), status: RuntimeProfileMembershipStatus::Normal, - tier: RuntimeProfileMembershipTier::Normal, + plan: RuntimeProfileMembershipPlan::Normal, started_at: None, started_at_micros: None, expires_at: None, @@ -3885,7 +3828,9 @@ mod tests { cycle_resets_at_micros: None, cycle_granted_points: 0, cycle_remaining_points: 0, - cycle_period_days: 30, + cycle_index: 1, + cycle_count: 12, + cycle_kind: RuntimeProfileMembershipCycleKind::Yearly, }, point_products: vec![RuntimeProfileRechargeProductRecord { product_id: "points_60".to_string(), @@ -3897,14 +3842,8 @@ mod tests { duration_days: 0, badge_label: "首充双倍".to_string(), description: "60+60泥点".to_string(), - tier: RuntimeProfileMembershipTier::Normal, - membership_period_points: 0, - membership_period_days: 0, - membership_queue_limit: 0, - membership_discount_bps: 0, }], - membership_products: vec![], - benefits: vec![], + membership_plans: vec![], latest_order: None, has_points_recharged: false, daily_free_points: daily_free_points_record(), diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index ac339f8ac..e00836bdb 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -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, + /// 每日免费泥点:北京业务日有效,跨日清零并重置。 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, + /// 永久泥点:不过期,创作照常扣点。 + 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, pub expires_at: Option, pub updated_at: Option, @@ -282,7 +276,12 @@ pub struct ProfileMembershipResponse { pub cycle_resets_at: Option, 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, - pub membership_products: Vec, - pub benefits: Vec, + /// 会员档位目录(后台可改价),会员购买 / 升级的价格与权益都以它为准。 + pub membership_plans: Vec, pub latest_order: Option, 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, #[serde(default)] pub description: Option, - 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 {