diff --git a/packages/shared/src/contracts/generated/ProfileMudPointBalanceResponse.ts b/packages/shared/src/contracts/generated/ProfileMudPointBalanceResponse.ts new file mode 100644 index 000000000..65cd3039a --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileMudPointBalanceResponse.ts @@ -0,0 +1,19 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProfileMudPointBalanceResponse = { + /** + * 每日免费泥点:北京业务日有效,跨日清零并重置。 + */ + dailyFreePoints: number; + dailyFreeResetPoints: number; + dailyFreeResetsAt: string; + /** + * 月度泥点:会员每期额度,到刷新时刻清零再发新额度。 + */ + monthlyPoints: number; + monthlyResetsAt: string | null; + /** + * 永久泥点:不过期,创作照常扣点。 + */ + permanentPoints: number; +}; diff --git a/packages/shared/src/contracts/runtime.ts b/packages/shared/src/contracts/runtime.ts index 2d30cc81d..e8a184688 100644 --- a/packages/shared/src/contracts/runtime.ts +++ b/packages/shared/src/contracts/runtime.ts @@ -1,4 +1,7 @@ import type { JsonObject } from './common'; +import type { ProfileMudPointBalanceResponse as ProfileMudPointBalance } from './generated/ProfileMudPointBalanceResponse'; + +export type { ProfileMudPointBalance }; export const SAVE_SNAPSHOT_VERSION = 2; export const DEFAULT_MUSIC_VOLUME = 0.42; @@ -118,15 +121,17 @@ export type ExternalApiKeyMutationResponse = { export type ProfileRechargeProductKind = 'points' | 'membership'; export type ProfileMembershipStatus = 'normal' | 'active'; -export type ProfileMembershipTier = +/** 会员档位:`normal` 是非会员占位档位,不可购买。 */ +export type ProfileMembershipPlan = | 'normal' - | 'month' - | 'season' - | 'year' | 'starter' - | 'basic' + | 'plus' | 'pro' - | 'ultimate'; + | 'max'; +/** 会员周期:有效期内不支持月年互换,到期后重新选择。 */ +export type ProfileMembershipCycleKind = 'monthly' | 'yearly'; +/** 模型权限:`basic` 仅基础模型,`full` 基础及高性能模型。 */ +export type ProfileMembershipModelAccess = 'basic' | 'full'; export type ProfileRechargeOrderStatus = | 'pending' | 'paid' @@ -145,28 +150,27 @@ export type ProfileRechargeProduct = { durationDays: number; badgeLabel: string; description: string; - tier: ProfileMembershipTier; - membershipPeriodPoints: number; - membershipPeriodDays: number; - membershipQueueLimit: number; - membershipDiscountBps: number; }; -export type ProfileMembershipBenefit = { - benefitName: string; - normalValue: string; - monthValue: string; - seasonValue: string; - yearValue: string; - starterValue: string; - basicValue: string; - proValue: string; - ultimateValue: string; +/** 会员档位目录行;价格与权益的唯一真相源是后端 `profile_membership_plan` 表。 */ +export type ProfileMembershipPlanRecord = { + plan: ProfileMembershipPlan; + title: string; + rank: number; + monthPriceCents: number; + yearPriceCents: number; + periodPoints: number; + modelAccess: ProfileMembershipModelAccess; + /** 并发上限;`128` 表示不设上限。 */ + concurrentJobLimit: number; + sortOrder: number; + enabled: boolean; }; export type ProfileMembership = { status: ProfileMembershipStatus; - tier: ProfileMembershipTier; + /** 当前档位;非会员与已到期都是 `normal`。 */ + plan: ProfileMembershipPlan; startedAt: string | null; expiresAt: string | null; updatedAt: string | null; @@ -174,17 +178,29 @@ export type ProfileMembership = { cycleResetsAt: string | null; cycleGrantedPoints: number; cycleRemainingPoints: number; - cyclePeriodDays: number; + /** 当前期序号(1-based)。 */ + cycleIndex: number; + /** 本次购买总期数(月付 1 / 年付 12)。 */ + cycleCount: number; + cycleKind: ProfileMembershipCycleKind; }; -export type ProfileMudPointBalance = { - totalPoints: number; - permanentPoints: number; - limitedPoints: number; - limitedExpiresAt: string | null; - dailyFreePoints: number; - dailyFreeResetPoints: number; - dailyFreeResetsAt: string; +/** 补差升级报价;下单时后端会用同一套规则重算,前端展示的数字不是真相源。 */ +export type ProfileMembershipUpgradeQuote = { + currentPlan: ProfileMembershipPlan; + targetPlan: ProfileMembershipPlan; + cycleKind: ProfileMembershipCycleKind; + cycleIndex: number; + cycleCount: number; + /** 应付金额(分)。 */ + amountCents: number; + /** 本期补点。 */ + grantedPointsDelta: number; + /** 变更后月度余额 = 原剩余月度余额 + 补点。 */ + monthlyBalanceAfter: number; + expiresAt: string | null; + cycleResetsAt: string | null; + remainingFullMonths: number; }; export type ProfileRechargeOrder = { @@ -207,13 +223,12 @@ export type ProfileRechargeOrder = { }; export type ProfileRechargeCenterResponse = { - walletBalance: number; // TODO mirror backend: should not nullable mudPointBalance?: ProfileMudPointBalance; membership: ProfileMembership; pointProducts: ProfileRechargeProduct[]; - membershipProducts: ProfileRechargeProduct[]; - benefits: ProfileMembershipBenefit[]; + /** 会员档位目录(后台可改价);会员购买 / 升级的价格与权益以它为准。 */ + membershipPlans: ProfileMembershipPlanRecord[]; latestOrder: ProfileRechargeOrder | null; hasPointsRecharged: boolean; dailyFreePoints?: ProfileDailyFreePoints; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 87fb5dc00..9505d6b0f 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -12,4 +12,5 @@ export * from './http'; export * from './llm/narrativeLanguage'; export * from './llm/parsers'; export * from './stores/createProfileWalletStore'; +export * from './utils/mudPoints'; export * from './utils/signedReadUrlCache'; diff --git a/packages/shared/src/utils/mudPoints.ts b/packages/shared/src/utils/mudPoints.ts new file mode 100644 index 000000000..d20282667 --- /dev/null +++ b/packages/shared/src/utils/mudPoints.ts @@ -0,0 +1,73 @@ +import type { ProfileMudPointBalance } from '../contracts/runtime'; + +/** + * 泥点余额的展示视图。 + * + * 后端只并列暴露三个池子(每日免费 / 月度 / 永久),**不**提供「可用总额」字段, + * 以「合计」与「可用」的口径散回后端或各自相加都会分叉。需要合计展示时统一走 + * {@link resolveProfileMudPointBalanceView}。 + */ +export type ProfileMudPointBalanceView = ProfileMudPointBalance & { + /** 每日免费 + 月度 + 永久;只用于展示,不代表「可用额度」。 */ + totalPoints: number; +}; + +export function resolveProfileMudPointBalanceView( + balance: ProfileMudPointBalance, +): ProfileMudPointBalanceView { + // 三个池子按 0 兜底:接口缺字段时宁可显示 0,也不能让合计变成 NaN。 + return { + ...balance, + dailyFreePoints: balance.dailyFreePoints ?? 0, + monthlyPoints: balance.monthlyPoints ?? 0, + permanentPoints: balance.permanentPoints ?? 0, + totalPoints: + (balance.dailyFreePoints ?? 0) + + (balance.monthlyPoints ?? 0) + + (balance.permanentPoints ?? 0), + }; +} + +/** + * 从任意余额形状推导展示用合计;三池缺省按 0 处理,整体为 `undefined` 时返回 `undefined`。 + */ +export function resolveProfileMudPointDisplayTotal( + balance: ProfileMudPointBalance | null | undefined, +): number | undefined { + if (!balance) { + return undefined; + } + return resolveProfileMudPointBalanceView(balance).totalPoints; +} + +/** + * 月度泥点的刷新时刻展示文案。 + * + * 账期锚点按北京时间定义(见 ADR「会员账期按开通日自然月」),所以展示也固定用北京时间, + * 不随设备时区漂移;无会员周期或时间非法时返回 `null`,由调用方给兜底说明。 + */ +export function formatMudPointResetAt( + value: string | null | undefined, +): string | null { + if (!value) { + return null; + } + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return null; + } + const parts = new Intl.DateTimeFormat('en-CA', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }).formatToParts(parsed); + const lookup = (type: Intl.DateTimeFormatPartTypes) => + parts.find((part) => part.type === type)?.value ?? '00'; + return `${lookup('year')}-${lookup('month')}-${lookup('day')} ${lookup('hour')}:${lookup( + 'minute', + )}`; +} diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index 6e21e9015..3d5dca1f6 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -201,15 +201,30 @@ pub struct ProfileDailyFreePointsResponse { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))] +#[cfg_attr( + feature = "ts-bindings", + ts( + export, + export_to = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../packages/shared/src/contracts/generated/" + ) + ) +)] pub struct ProfileMudPointBalanceResponse { /// 每日免费泥点:北京业务日有效,跨日清零并重置。 + #[cfg_attr(feature = "ts-bindings", ts(type = "number"))] pub daily_free_points: u64, + #[cfg_attr(feature = "ts-bindings", ts(type = "number"))] pub daily_free_reset_points: u64, pub daily_free_resets_at: String, /// 月度泥点:会员每期额度,到刷新时刻清零再发新额度。 + #[cfg_attr(feature = "ts-bindings", ts(type = "number"))] pub monthly_points: u64, pub monthly_resets_at: Option, /// 永久泥点:不过期,创作照常扣点。 + #[cfg_attr(feature = "ts-bindings", ts(type = "number"))] pub permanent_points: u64, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index 637214065..d19e12c93 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -665,7 +665,7 @@ pub mod runtime_profile_membership_snapshot_type; pub mod runtime_profile_membership_status_type; pub mod runtime_profile_membership_upgrade_quote_get_input_type; pub mod runtime_profile_membership_upgrade_quote_procedure_result_type; -pub mod runtime_profile_membership_upgrade_quote_record_type; +pub mod runtime_profile_membership_upgrade_quote_snapshot_type; pub mod runtime_profile_recharge_center_get_input_type; pub mod runtime_profile_recharge_center_procedure_result_type; pub mod runtime_profile_recharge_center_snapshot_type; @@ -1482,7 +1482,7 @@ pub use runtime_profile_membership_snapshot_type::RuntimeProfileMembershipSnapsh pub use runtime_profile_membership_status_type::RuntimeProfileMembershipStatus; pub use runtime_profile_membership_upgrade_quote_get_input_type::RuntimeProfileMembershipUpgradeQuoteGetInput; pub use runtime_profile_membership_upgrade_quote_procedure_result_type::RuntimeProfileMembershipUpgradeQuoteProcedureResult; -pub use runtime_profile_membership_upgrade_quote_record_type::RuntimeProfileMembershipUpgradeQuoteRecord; +pub use runtime_profile_membership_upgrade_quote_snapshot_type::RuntimeProfileMembershipUpgradeQuoteSnapshot; pub use runtime_profile_recharge_center_get_input_type::RuntimeProfileRechargeCenterGetInput; pub use runtime_profile_recharge_center_procedure_result_type::RuntimeProfileRechargeCenterProcedureResult; pub use runtime_profile_recharge_center_snapshot_type::RuntimeProfileRechargeCenterSnapshot; diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_procedure_result_type.rs index 92168b485..213059e5e 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_procedure_result_type.rs @@ -4,13 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; -use super::runtime_profile_membership_upgrade_quote_record_type::RuntimeProfileMembershipUpgradeQuoteRecord; +use super::runtime_profile_membership_upgrade_quote_snapshot_type::RuntimeProfileMembershipUpgradeQuoteSnapshot; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct RuntimeProfileMembershipUpgradeQuoteProcedureResult { pub ok: bool, - pub quote: Option, + pub quote: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_record_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_snapshot_type.rs similarity index 85% rename from server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_record_type.rs rename to server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_snapshot_type.rs index 504002690..1ffc80dc3 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_record_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_membership_upgrade_quote_snapshot_type.rs @@ -6,7 +6,7 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] -pub struct RuntimeProfileMembershipUpgradeQuoteRecord { +pub struct RuntimeProfileMembershipUpgradeQuoteSnapshot { pub current_plan: String, pub target_plan: String, pub cycle_kind: String, @@ -20,6 +20,6 @@ pub struct RuntimeProfileMembershipUpgradeQuoteRecord { pub remaining_full_months: u32, } -impl __sdk::InModule for RuntimeProfileMembershipUpgradeQuoteRecord { +impl __sdk::InModule for RuntimeProfileMembershipUpgradeQuoteSnapshot { type Module = super::RemoteModule; }