会员目录响应下发推荐档位与不设并发上限标志

ProfileMembershipPlanResponse 新增 recommended 与 unlimitedConcurrency 两个派生布尔
module-runtime 新增 PROFILE_MEMBERSHIP_RECOMMENDED_PLAN 常量并导出,作为推荐档位唯一真相源
api-server 目录响应按该常量与 is_unlimited_concurrency 派生标志
前端 ProfileMembershipPlanRecord 契约补上两个字段
membershipCopy 删除硬编码的 'plus' 与 128,并发文案改读 unlimitedConcurrency
充值弹窗推荐标签改读 plan.recommended
更新充值弹窗测试夹具
This commit is contained in:
2026-10-03 16:18:14 +08:00
parent 3031f58875
commit 4c6af9f190
8 changed files with 55 additions and 35 deletions
@@ -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', () => ({
@@ -19,7 +19,6 @@ import {
resolveProfileMudPointDisplayTotal,
} from '../../utils/mudPoints';
import {
RECOMMENDED_MEMBERSHIP_PLAN,
resolveMembershipConcurrencyLabel,
resolveMembershipConcurrencyShortLabel,
resolveMembershipModelAccessLabel,
@@ -673,7 +672,7 @@ function PlatformMembershipPlanCard({
</div>
<ul className="grid gap-0.5 text-[10px] leading-5 text-[var(--platform-text-soft)]">
<li>{resolveMembershipModelAccessLabel(plan.modelAccess)}</li>
<li>{resolveMembershipConcurrencyLabel(plan.concurrentJobLimit)}</li>
<li>{resolveMembershipConcurrencyLabel(plan)}</li>
</ul>
<PlatformRechargeActionButton
ariaLabel={`${plan.title} ${cycleKind === 'yearly' ? '年付' : '月付'} ${actionLabel}`}
@@ -716,9 +715,7 @@ function PlatformMembershipComparisonTable({
},
{
label: '独立任务并发',
values: plans.map((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}
@@ -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} 个`;
}
+4
View File
@@ -163,6 +163,10 @@ export type ProfileMembershipPlanRecord = {
modelAccess: ProfileMembershipModelAccess;
/** 并发上限;`128` 表示不设上限。 */
concurrentJobLimit: number;
/** 是否为运营推荐档位;由后端目录给出,前端不再硬编码档位 token。 */
recommended: boolean;
/** 是否不设并发上限;由后端按哨兵值派生,前端不再硬编码 `128`。 */
unlimitedConcurrency: boolean;
enabled: boolean;
};
@@ -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,
}
}
+8 -8
View File
@@ -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,
};
@@ -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)]
@@ -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,