diff --git a/packages/shared/src/components/PlatformProfileRechargeModal/index.test.tsx b/packages/shared/src/components/PlatformProfileRechargeModal/index.test.tsx index 3855a4fcd..58875c494 100644 --- a/packages/shared/src/components/PlatformProfileRechargeModal/index.test.tsx +++ b/packages/shared/src/components/PlatformProfileRechargeModal/index.test.tsx @@ -96,6 +96,8 @@ const STARTER_PLAN = { periodPoints: 400, modelAccess: 'basic' as const, concurrentJobLimit: 1, + recommended: false, + unlimitedConcurrency: false, enabled: true, }; @@ -109,6 +111,7 @@ const PLUS_PLAN = { periodPoints: 1_150, modelAccess: 'full' as const, concurrentJobLimit: 3, + recommended: true, }; const MAX_PLAN = { @@ -121,6 +124,7 @@ const MAX_PLAN = { periodPoints: 8_650, modelAccess: 'full' as const, concurrentJobLimit: 128, + unlimitedConcurrency: true, }; vi.mock('qrcode', () => ({ diff --git a/packages/shared/src/components/PlatformProfileRechargeModal/index.tsx b/packages/shared/src/components/PlatformProfileRechargeModal/index.tsx index 07989fff9..2b02f5c57 100644 --- a/packages/shared/src/components/PlatformProfileRechargeModal/index.tsx +++ b/packages/shared/src/components/PlatformProfileRechargeModal/index.tsx @@ -19,7 +19,6 @@ import { resolveProfileMudPointDisplayTotal, } from '../../utils/mudPoints'; import { - RECOMMENDED_MEMBERSHIP_PLAN, resolveMembershipConcurrencyLabel, resolveMembershipConcurrencyShortLabel, resolveMembershipModelAccessLabel, @@ -673,7 +672,7 @@ function PlatformMembershipPlanCard({ - resolveMembershipConcurrencyShortLabel(plan.concurrentJobLimit), - ), + values: plans.map((plan) => resolveMembershipConcurrencyShortLabel(plan)), }, ]; @@ -1380,7 +1377,7 @@ export function PlatformProfileRechargeModal({ plan={plan} cycleKind={cycleKind} isCurrentPlan={plan.plan === membership?.plan} - recommended={plan.plan === RECOMMENDED_MEMBERSHIP_PLAN} + recommended={plan.recommended} actionLabel={action.label} disabled={action.disabled} submitting={planSubmitting} diff --git a/packages/shared/src/components/PlatformProfileRechargeModal/membershipCopy.ts b/packages/shared/src/components/PlatformProfileRechargeModal/membershipCopy.ts index 87bb554ab..ff8132278 100644 --- a/packages/shared/src/components/PlatformProfileRechargeModal/membershipCopy.ts +++ b/packages/shared/src/components/PlatformProfileRechargeModal/membershipCopy.ts @@ -1,18 +1,16 @@ import type { ProfileMembershipModelAccess, ProfileMembershipPlan, + ProfileMembershipPlanRecord, } from '../../contracts/runtime'; /** * 会员侧的纯展示文案。 * - * 价格、每期泥点、模型权限、并发上限、标题的唯一真相源是后端 `profile_membership_plan` 目录, - * 这里只放后端没有、也不该由后端承担的市场表达(数量估算、推荐标签、省费口径)。 + * 价格、每期泥点、模型权限、并发上限、标题、推荐档位与「是否不设并发上限」的唯一真相源 + * 都是后端 `profile_membership_plan` 目录响应;这里只放后端没有、也不该由后端承担的 + * 市场表达(数量估算、省费口径)。 */ -export const RECOMMENDED_MEMBERSHIP_PLAN: ProfileMembershipPlan = 'plus'; - -/** 并发上限哨兵值:与后端 `MEMBERSHIP_UNLIMITED_CONCURRENCY` 对齐,`>= 128` 视为不设上限。 */ -export const UNLIMITED_CONCURRENT_JOB_LIMIT = 128; type MembershipPlanCopy = { /** 每月预计可完成的数量估算。 */ @@ -66,24 +64,26 @@ export function resolveMembershipModelAccessLabel( return modelAccess === 'full' ? '基础及高性能模型' : '基础模型'; } -/** 套餐卡里的并发文案。 */ +/** 套餐卡里的并发文案;是否不设上限由后端 `unlimitedConcurrency` 标志决定。 */ export function resolveMembershipConcurrencyLabel( - concurrentJobLimit: number, + plan: Pick< + ProfileMembershipPlanRecord, + 'concurrentJobLimit' | 'unlimitedConcurrency' + >, ): string { - return concurrentJobLimit >= UNLIMITED_CONCURRENT_JOB_LIMIT + return plan.unlimitedConcurrency ? '独立任务并发不设套餐上限' - : `同时运行 ${concurrentJobLimit} 个独立任务`; + : `同时运行 ${plan.concurrentJobLimit} 个独立任务`; } /** 对比表里的并发短文案。 */ export function resolveMembershipConcurrencyShortLabel( - concurrentJobLimit: number, + plan: Pick< + ProfileMembershipPlanRecord, + 'concurrentJobLimit' | 'unlimitedConcurrency' + >, ): string { - return isUnlimitedConcurrency(concurrentJobLimit) + return plan.unlimitedConcurrency ? '不设套餐上限' - : `${concurrentJobLimit} 个`; -} - -export function isUnlimitedConcurrency(concurrentJobLimit: number): boolean { - return concurrentJobLimit >= UNLIMITED_CONCURRENT_JOB_LIMIT; + : `${plan.concurrentJobLimit} 个`; } diff --git a/packages/shared/src/contracts/runtime.ts b/packages/shared/src/contracts/runtime.ts index 4abb20f2b..3638f075a 100644 --- a/packages/shared/src/contracts/runtime.ts +++ b/packages/shared/src/contracts/runtime.ts @@ -163,6 +163,10 @@ export type ProfileMembershipPlanRecord = { modelAccess: ProfileMembershipModelAccess; /** 并发上限;`128` 表示不设上限。 */ concurrentJobLimit: number; + /** 是否为运营推荐档位;由后端目录给出,前端不再硬编码档位 token。 */ + recommended: boolean; + /** 是否不设并发上限;由后端按哨兵值派生,前端不再硬编码 `128`。 */ + unlimitedConcurrency: boolean; enabled: boolean; }; diff --git a/server-rs/crates/api-server/src/runtime_profile.rs b/server-rs/crates/api-server/src/runtime_profile.rs index 672c5da4f..3627ab371 100644 --- a/server-rs/crates/api-server/src/runtime_profile.rs +++ b/server-rs/crates/api-server/src/runtime_profile.rs @@ -9,8 +9,9 @@ use axum::{ }; use hmac::{Hmac, Mac}; use module_runtime::{ - AnalyticsGranularity, PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, - PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI, + AnalyticsGranularity, PROFILE_MEMBERSHIP_RECOMMENDED_PLAN, + PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5, + PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE, RuntimeProfileCodeOperationRecord, @@ -24,7 +25,7 @@ use module_runtime::{ RuntimeProfileRewardCodeRedeemRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord, RuntimeProfileTaskCycle, RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus, RuntimeProfileWalletLedgerSourceType, - RuntimeReferralInviteCenterRecord, RuntimeTrackingScopeKind, + RuntimeReferralInviteCenterRecord, RuntimeTrackingScopeKind, is_unlimited_concurrency, }; use platform_wechat::pay::{WechatPayError, WechatPayNotifyOrder}; use serde::{Deserialize, Serialize}; @@ -2061,7 +2062,8 @@ fn build_profile_recharge_product_response( } } -/// 会员档位目录 → 对外响应;`concurrent_job_limit` 的 `128` 哨兵直接透出,由前端展示为“不设上限”。 +/// 会员档位目录 → 对外响应。`recommended` 与 `unlimited_concurrency` 由后端派生, +/// 前端只消费标志,不再各自硬编码推荐档位或并发哨兵 `128`。 fn build_profile_membership_plan_response( record: RuntimeProfileMembershipPlanRecord, ) -> ProfileMembershipPlanResponse { @@ -2074,6 +2076,8 @@ fn build_profile_membership_plan_response( period_points: record.period_points, model_access: record.model_access.as_str().to_string(), concurrent_job_limit: record.concurrent_job_limit, + recommended: record.plan == PROFILE_MEMBERSHIP_RECOMMENDED_PLAN, + unlimited_concurrency: is_unlimited_concurrency(record.concurrent_job_limit), enabled: record.enabled, } } diff --git a/server-rs/crates/module-runtime/src/lib.rs b/server-rs/crates/module-runtime/src/lib.rs index 801644791..b3e48fb39 100644 --- a/server-rs/crates/module-runtime/src/lib.rs +++ b/server-rs/crates/module-runtime/src/lib.rs @@ -21,14 +21,14 @@ pub use llm_billing::*; // 通配会把公历基元和目录内部件都变成公开 API,模块继续增长后容易命名冲突; // 这里只重导出真正对外的会员契约符号,其余留在 `membership::` / `civil_calendar::` 命名空间下。 pub use membership::{ - MEMBERSHIP_UNLIMITED_CONCURRENCY, RuntimeProfileMembershipPlanRecord, - RuntimeProfileMembershipPlanSnapshot, RuntimeProfileMembershipUpgradeQuoteInput, - 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, + MEMBERSHIP_UNLIMITED_CONCURRENCY, PROFILE_MEMBERSHIP_RECOMMENDED_PLAN, + RuntimeProfileMembershipPlanRecord, RuntimeProfileMembershipPlanSnapshot, + RuntimeProfileMembershipUpgradeQuoteInput, 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, validate_runtime_profile_membership_plan_row_against_catalog, }; diff --git a/server-rs/crates/module-runtime/src/membership/catalog.rs b/server-rs/crates/module-runtime/src/membership/catalog.rs index f6c1962fd..b907dc49e 100644 --- a/server-rs/crates/module-runtime/src/membership/catalog.rs +++ b/server-rs/crates/module-runtime/src/membership/catalog.rs @@ -24,6 +24,11 @@ use crate::{ RuntimeProfileMembershipModelAccess, RuntimeProfileMembershipPlan, }; +/// 运营推荐档位。推荐是后端单一真相源:目录响应按此给出 `recommended` 标志, +/// 前端只消费标志,不再各自硬编码 `'plus'`。 +pub const PROFILE_MEMBERSHIP_RECOMMENDED_PLAN: RuntimeProfileMembershipPlan = + RuntimeProfileMembershipPlan::Plus; + /// 目录一行:一个档位的可配置价格与权益。 #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index 55c074d1a..7bf133256 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -274,6 +274,10 @@ pub struct ProfileMembershipPlanResponse { pub model_access: String, /// 并发上限;`128` 表示不设上限。 pub concurrent_job_limit: u32, + /// 是否为运营推荐档位;由后端目录决定,前端不再硬编码档位 token。 + pub recommended: bool, + /// 是否不设并发上限;由后端按哨兵值派生,前端不再硬编码 `128`。 + pub unlimited_concurrency: bool, pub enabled: bool, } @@ -1699,6 +1703,8 @@ mod tests { period_points: 1_150, model_access: "full".to_string(), concurrent_job_limit: 3, + recommended: true, + unlimited_concurrency: false, enabled: true, }], latest_order: None,