From 4c6af9f190e500868d9746c6bbbf65ce060c1154 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 16:18:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=9A=E5=91=98=E7=9B=AE=E5=BD=95=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E4=B8=8B=E5=8F=91=E6=8E=A8=E8=8D=90=E6=A1=A3=E4=BD=8D?= =?UTF-8?q?=E4=B8=8E=E4=B8=8D=E8=AE=BE=E5=B9=B6=E5=8F=91=E4=B8=8A=E9=99=90?= =?UTF-8?q?=E6=A0=87=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProfileMembershipPlanResponse 新增 recommended 与 unlimitedConcurrency 两个派生布尔 module-runtime 新增 PROFILE_MEMBERSHIP_RECOMMENDED_PLAN 常量并导出,作为推荐档位唯一真相源 api-server 目录响应按该常量与 is_unlimited_concurrency 派生标志 前端 ProfileMembershipPlanRecord 契约补上两个字段 membershipCopy 删除硬编码的 'plus' 与 128,并发文案改读 unlimitedConcurrency 充值弹窗推荐标签改读 plan.recommended 更新充值弹窗测试夹具 --- .../index.test.tsx | 4 +++ .../PlatformProfileRechargeModal/index.tsx | 9 ++--- .../membershipCopy.ts | 34 +++++++++---------- packages/shared/src/contracts/runtime.ts | 4 +++ .../crates/api-server/src/runtime_profile.rs | 12 ++++--- server-rs/crates/module-runtime/src/lib.rs | 16 ++++----- .../module-runtime/src/membership/catalog.rs | 5 +++ .../crates/shared-contracts/src/runtime.rs | 6 ++++ 8 files changed, 55 insertions(+), 35 deletions(-) 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,