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({
- {resolveMembershipModelAccessLabel(plan.modelAccess)}
- - {resolveMembershipConcurrencyLabel(plan.concurrentJobLimit)}
+ - {resolveMembershipConcurrencyLabel(plan)}
- 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,