重构(会员契约): 会员结构体改用 ts-rs 生成类型

- shared-contracts 为 10 个会员结构体补 ts_rs::TS / ts(export),64 位整数字段补 ts(type = "number") 对齐前端 number
- 新增 10 个生成绑定;packages/shared 与 admin-web 改为 re-export 生成类型,仅 kind / status 等前端收窄联合用 Omit 组合保留
- 同步设计文档 §2.5 与决策日志
This commit is contained in:
2026-10-05 15:51:07 +08:00
parent 0930e402be
commit bc3e36ff33
15 changed files with 332 additions and 120 deletions
+7 -32
View File
@@ -2,6 +2,7 @@ import type {
ProfileMembershipCycleKind,
ProfileMembershipModelAccess,
ProfileMembershipPlan,
ProfileMembershipPlanResponse,
ProfileMembershipStatus,
} from '@genarrative/shared';
@@ -784,39 +785,13 @@ export interface ProfileRechargeProductConfigAdminListResponse {
entries: ProfileRechargeProductConfigAdminResponse[];
}
/** 会员档位目录行;后端 `profile_membership_plan` 表是价格与权益的唯一真相源。 */
export interface ProfileMembershipPlanAdminResponse {
plan: ProfileMembershipPlan;
title: string;
rank: number;
monthPriceCents: number;
yearPriceCents: number;
periodPoints: number;
modelAccess: ProfileMembershipModelAccess;
/** 并发上限;`128` 表示不设上限。 */
concurrentJobLimit: number;
/** 是否不设并发上限;由后端按哨兵值派生,前端不再自己比较 `128`。 */
unlimitedConcurrency: boolean;
enabled: boolean;
}
/** 会员档位目录行;直接复用 `@genarrative/shared` 的后端生成类型,避免手工漂移。 */
export type ProfileMembershipPlanAdminResponse = ProfileMembershipPlanResponse;
export interface ProfileMembershipPlanAdminListResponse {
entries: ProfileMembershipPlanAdminResponse[];
/** 并发哨兵阈值:`concurrentJobLimit >= 该值` 即不设上限;由后端下发,前端不硬编码。 */
unlimitedConcurrencyThreshold: number;
}
/** 后台按档位改价 / 改权益;`rank` 不在请求里,升级比较基准由代码内目录决定。 */
export interface AdminUpsertProfileMembershipPlanRequest {
plan: ProfileMembershipPlan;
title: string;
monthPriceCents: number;
yearPriceCents: number;
periodPoints: number;
modelAccess: ProfileMembershipModelAccess;
concurrentJobLimit: number;
enabled: boolean;
}
export type {
AdminUpsertProfileMembershipPlanRequest,
ProfileMembershipPlanAdminListResponse,
} from '@genarrative/shared';
export interface ProfileWalletConfigAdminResponse {
configId: string;
@@ -9757,3 +9757,10 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 决策:该 PR 尚未上线,不留兼容,直接改名为 `ProductIdUnparseable`,wire 串同步改为 `"product_id_unparseable"`;重新生成 SpacetimeDB 绑定。
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active/mapper/runtime_profile.rs,module_bindings/runtime_profile_membership_error_code_type.rs}`、`server-rs/crates/api-server/src/runtime_profile.rs`、设计文档 §5.8、本文件(含上文旧条目中的同名标识符同步更正)。
- 验证:`cargo test -p module-runtime --lib` 114 passed;`cargo test -p spacetime-client` 32 passed;`cargo test -p api-server runtime_profile::tests` 43 passed;`cargo test -p spacetime-module` 271 passed。
## 2026-10-03 会员结构体也走 ts-rs 生成类型(去前端手写漂移)
- 背景:会员 token 枚举早已 `ts(export)`,但承载它们的结构体(`ProfileMembershipPlanResponse`、`ProfileMembershipResponse`、`ProfileRechargeCenterResponse`、`ProfileRechargeProductResponse`、`ProfileRechargeOrderResponse`、`ProfileMembershipUpgradeQuoteResponse`、`ProfileMembershipPlanAdminListResponse`、`AdminUpsertProfileMembershipPlanRequest`、`ProfileDailyFreePointsResponse`、`ProfileMembershipUpgradeQuoteRequest`)仍是前端手写,字段与后端可能漂移。
- 决策:这 10 个结构体补齐 `ts_rs::TS` + `ts(export)`;64 位整数字段统一补 `ts(type = "number")`(ts-rs 默认 `bigint`,JSON 实际是 number)。`packages/shared/src/contracts/runtime.ts` 与 `apps/admin-web/src/api/adminApiTypes.ts` 改为 re-export 生成类型,仅前端需要收窄的 `kind` / `status` 联合用 `Omit` 组合保留;`ProfileRechargeCenterResponse` 的 `pointProducts` / `latestOrder` 同样收窄。
- 影响范围:`server-rs/crates/shared-contracts/src/runtime.rs`、`packages/shared/src/contracts/generated/*`(新增 10 个文件)、`packages/shared/src/contracts/runtime.ts`、`apps/admin-web/src/api/adminApiTypes.ts`、设计文档 §2.5。
- 验证:`npm run check:generated-bindings`(shared-contracts 23 个文件);根 `npm run typecheck`、`apps/admin-web` 与 AGC shell 主 `tsc` 通过;`cargo test -p shared-contracts --lib` 106 passed;`PlatformProfileRechargeModal` / `usePlatformProfileCenterController` vitest 14 passed。
@@ -160,6 +160,14 @@ pub struct ProfileMembershipPlan {
避免 AGC Tauri shell 等 Rust 客户端因后端新增一个 token 就整包解析失败(权益与下单金额仍以后端重算为准)。
这 5 个 token 枚举统一 `#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]`
导出到 `packages/shared/src/contracts/generated/`,前端只 re-export,不再手写字符串联合类型。
- **会员结构体也走生成类型(2026-10-03 补)**:`ProfileMembershipPlanResponse` / `ProfileMembershipResponse` /
`ProfileRechargeProductResponse` / `ProfileRechargeOrderResponse` / `ProfileRechargeCenterResponse` /
`ProfileMembershipUpgradeQuoteResponse` / `ProfileMembershipPlanAdminListResponse` /
`AdminUpsertProfileMembershipPlanRequest` / `ProfileDailyFreePointsResponse` /
`ProfileMembershipUpgradeQuoteRequest` 同样 `ts(export)`;64 位整数字段补 `ts(type = "number")`
对齐前端 `number`(ts-rs 默认 `bigint`)。`packages/shared/src/contracts/runtime.ts` 与
`apps/admin-web/src/api/adminApiTypes.ts` 改为 re-export 生成类型,只手写保留 `kind` / `status`
这类前端收窄联合(用 `Omit` 组合),`ProfileRechargeCenterResponse` 同理收窄内层商品 / 订单数组。
- **删除** `RuntimeProfileMembershipTier { Normal, Month, Season, Year, Starter, Basic, Pro, Ultimate }`
及其 `as_str()`(会员侧零存量,用户已授权破坏性变更)。
- `RuntimeProfileWalletLedgerSourceType` **末尾追加** `MembershipUpgradeGrant`
@@ -0,0 +1,20 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProfileMembershipModelAccessToken } from "./ProfileMembershipModelAccessToken";
import type { ProfileMembershipPlanToken } from "./ProfileMembershipPlanToken";
/**
* 后台按档位改会员价格与权益;`rank` 不在请求里,升级比较基准由代码内目录决定。
*/
export type AdminUpsertProfileMembershipPlanRequest = {
/**
* 档位 token,作为主键即档位身份。
*/
plan: ProfileMembershipPlanToken, title: string, monthPriceCents: number, yearPriceCents: number, periodPoints: number,
/**
* 模型权限 token:`basic` / `full`;本期只落字段、不做服务端拦截。
*/
modelAccess: ProfileMembershipModelAccessToken,
/**
* 并发上限;`128` 表示不设上限。
*/
concurrentJobLimit: number, enabled: boolean, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ProfileDailyFreePointsResponse = { dayKey: number, grantedPoints: number, remainingPoints: number, resetsAt: string, updatedAt: string, };
@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProfileMembershipPlanResponse } from "./ProfileMembershipPlanResponse";
export type ProfileMembershipPlanAdminListResponse = { entries: Array<ProfileMembershipPlanResponse>,
/**
* 并发哨兵阈值:`concurrent_job_limit >= 该值` 即不设上限;由后端下发,前端不硬编码。
*/
unlimitedConcurrencyThreshold: number, };
@@ -0,0 +1,25 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProfileMembershipModelAccessToken } from "./ProfileMembershipModelAccessToken";
import type { ProfileMembershipPlanToken } from "./ProfileMembershipPlanToken";
export type ProfileMembershipPlanResponse = {
/**
* 档位 token:`normal` / `starter` / `plus` / `pro` / `max`;未知取值按 `max` 兜底。
*/
plan: ProfileMembershipPlanToken, title: string, rank: number, monthPriceCents: number, yearPriceCents: number, periodPoints: number,
/**
* 模型权限 token:`basic` / `full`;未知取值按 `full` 兜底。
*/
modelAccess: ProfileMembershipModelAccessToken,
/**
* 并发上限;`128` 表示不设上限。
*/
concurrentJobLimit: number,
/**
* 是否为运营推荐档位;由后端目录决定,前端不再硬编码档位 token。
*/
recommended: boolean,
/**
* 是否不设并发上限;由后端按哨兵值派生,前端不再硬编码 `128`。
*/
unlimitedConcurrency: boolean, enabled: boolean, };
@@ -0,0 +1,26 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProfileMembershipCycleKindToken } from "./ProfileMembershipCycleKindToken";
import type { ProfileMembershipPlanToken } from "./ProfileMembershipPlanToken";
import type { ProfileMembershipStatusToken } from "./ProfileMembershipStatusToken";
export type ProfileMembershipResponse = {
/**
* 会员状态 token:`normal` / `active`;未知取值按 `active` 兜底。
*/
status: ProfileMembershipStatusToken,
/**
* 当前档位 token;非会员与已到期都是 `normal`;未知取值按 `max` 兜底。
*/
plan: ProfileMembershipPlanToken, startedAt: string | null, expiresAt: string | null, updatedAt: string | null, cycleStartedAt: string | null, cycleResetsAt: string | null, cycleGrantedPoints: number, cycleRemainingPoints: number,
/**
* 当前期序号(1-based)。
*/
cycleIndex: number,
/**
* 本次购买总期数(月付 1 / 年付 12)。
*/
cycleCount: number,
/**
* 周期 token:`monthly` / `yearly`;未知取值按 `yearly` 兜底。
*/
cycleKind: ProfileMembershipCycleKindToken, };
@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProfileMembershipPlanToken } from "./ProfileMembershipPlanToken";
export type ProfileMembershipUpgradeQuoteRequest = {
/**
* 目标档位 token:`starter` / `plus` / `pro` / `max`。
*/
targetPlan: ProfileMembershipPlanToken, };
@@ -0,0 +1,33 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProfileMembershipCycleKindToken } from "./ProfileMembershipCycleKindToken";
import type { ProfileMembershipPlanToken } from "./ProfileMembershipPlanToken";
/**
* 补差升级报价:付款前展示应付金额、补点与变更后的月度余额;下单时后端会用同一函数重算。
*/
export type ProfileMembershipUpgradeQuoteResponse = {
/**
* 当前档位 token;未知取值按 `max` 兜底。
*/
currentPlan: ProfileMembershipPlanToken,
/**
* 目标档位 token;未知取值按 `max` 兜底。
*/
targetPlan: ProfileMembershipPlanToken,
/**
* 周期 token:`monthly` / `yearly`;有效期内不支持月年互换,所以变更前后一致;
* 未知取值按 `yearly` 兜底。
*/
cycleKind: ProfileMembershipCycleKindToken, cycleIndex: number, cycleCount: number,
/**
* 应付金额(分);月付 = 月价差,年付 = 年价差 ÷ 12 × (后续完整月数 + 本期剩余比例),向上取整到分。
*/
amountCents: number,
/**
* 本期补点;年付按本期剩余比例向下取整。
*/
grantedPointsDelta: number,
/**
* 变更后月度余额 = 原剩余月度余额 + 补点(已用点不返还,永久泥点不动)。
*/
monthlyBalanceAfter: number, expiresAt: string | null, cycleResetsAt: string | null, remainingFullMonths: number, };
@@ -0,0 +1,13 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProfileDailyFreePointsResponse } from "./ProfileDailyFreePointsResponse";
import type { ProfileMembershipPlanResponse } from "./ProfileMembershipPlanResponse";
import type { ProfileMembershipResponse } from "./ProfileMembershipResponse";
import type { ProfileMudPointBalanceResponse } from "./ProfileMudPointBalanceResponse";
import type { ProfileRechargeOrderResponse } from "./ProfileRechargeOrderResponse";
import type { ProfileRechargeProductResponse } from "./ProfileRechargeProductResponse";
export type ProfileRechargeCenterResponse = { mudPointBalance: ProfileMudPointBalanceResponse, membership: ProfileMembershipResponse, pointProducts: Array<ProfileRechargeProductResponse>,
/**
* 会员档位目录(后台可改价),会员购买 / 升级的价格与权益都以它为准。
*/
membershipPlans: Array<ProfileMembershipPlanResponse>, latestOrder: ProfileRechargeOrderResponse | null, hasPointsRecharged: boolean, dailyFreePoints: ProfileDailyFreePointsResponse, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ProfileRechargeOrderResponse = { orderId: string, productId: string, productTitle: string, kind: string, amountCents: number, status: string, paymentChannel: string, paidAt: string | null, providerTransactionId: string | null, createdAt: string, pointsDelta: number, membershipExpiresAt: string | null, expiredAt: string | null, expirationCheckedAt: string | null, expirationProviderState: string | null, expirationLastError: string | null, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ProfileRechargeProductResponse = { productId: string, title: string, priceCents: number, kind: string, pointsAmount: number, bonusPoints: number, durationDays: number, badgeLabel: string, description: string, };
+39 -88
View File
@@ -1,18 +1,37 @@
import type { JsonObject } from './common';
import type { AdminUpsertProfileMembershipPlanRequest } from './generated/AdminUpsertProfileMembershipPlanRequest';
import type { ProfileDailyFreePointsResponse } from './generated/ProfileDailyFreePointsResponse';
import type { ProfileMembershipChangeKindToken } from './generated/ProfileMembershipChangeKindToken';
import type { ProfileMembershipCycleKindToken } from './generated/ProfileMembershipCycleKindToken';
import type { ProfileMembershipModelAccessToken } from './generated/ProfileMembershipModelAccessToken';
import type { ProfileMembershipPlanAdminListResponse } from './generated/ProfileMembershipPlanAdminListResponse';
import type { ProfileMembershipPlanResponse } from './generated/ProfileMembershipPlanResponse';
import type { ProfileMembershipPlanToken } from './generated/ProfileMembershipPlanToken';
import type { ProfileMembershipResponse } from './generated/ProfileMembershipResponse';
import type { ProfileMembershipStatusToken } from './generated/ProfileMembershipStatusToken';
import type { ProfileMembershipUpgradeQuoteRequest } from './generated/ProfileMembershipUpgradeQuoteRequest';
import type { ProfileMembershipUpgradeQuoteResponse } from './generated/ProfileMembershipUpgradeQuoteResponse';
import type { ProfileMudPointBalanceResponse as ProfileMudPointBalance } from './generated/ProfileMudPointBalanceResponse';
import type { ProfileRechargeCenterResponse as GeneratedProfileRechargeCenterResponse } from './generated/ProfileRechargeCenterResponse';
import type { ProfileRechargeOrderResponse } from './generated/ProfileRechargeOrderResponse';
import type { ProfileRechargeProductResponse } from './generated/ProfileRechargeProductResponse';
export type {
AdminUpsertProfileMembershipPlanRequest,
ProfileDailyFreePointsResponse,
ProfileMembershipChangeKindToken,
ProfileMembershipCycleKindToken,
ProfileMembershipModelAccessToken,
ProfileMembershipPlanAdminListResponse,
ProfileMembershipPlanResponse,
ProfileMembershipPlanToken,
ProfileMembershipResponse,
ProfileMembershipStatusToken,
ProfileMembershipUpgradeQuoteRequest,
ProfileMembershipUpgradeQuoteResponse,
ProfileMudPointBalance,
ProfileRechargeOrderResponse,
ProfileRechargeProductResponse,
};
export const SAVE_SNAPSHOT_VERSION = 2;
@@ -69,13 +88,7 @@ export type ProfileDashboardSummary = {
dailyFreePoints?: ProfileDailyFreePoints;
};
export type ProfileDailyFreePoints = {
dayKey: number;
grantedPoints: number;
remainingPoints: number;
resetsAt: string;
updatedAt: string;
};
export type ProfileDailyFreePoints = ProfileDailyFreePointsResponse;
export type ProfileWalletLedgerEntry = {
id: string;
@@ -148,101 +161,39 @@ export type ProfileRechargeOrderStatus =
| 'refunded'
| 'expired';
export type ProfileRechargeProduct = {
productId: string;
title: string;
priceCents: number;
/** 充点 / 会员商品;`kind` 是前端收窄的联合类型,其余字段以后端生成为准。 */
export type ProfileRechargeProduct = Omit<
ProfileRechargeProductResponse,
'kind'
> & {
kind: ProfileRechargeProductKind;
pointsAmount: number;
bonusPoints: number;
durationDays: number;
badgeLabel: string;
description: string;
};
/** 会员档位目录行;价格与权益的唯一真相源是后端 `profile_membership_plan` 表。 */
export type ProfileMembershipPlanRecord = {
plan: ProfileMembershipPlan;
title: string;
rank: number;
monthPriceCents: number;
yearPriceCents: number;
periodPoints: number;
modelAccess: ProfileMembershipModelAccess;
/** 并发上限;`128` 表示不设上限。 */
concurrentJobLimit: number;
/** 是否为运营推荐档位;由后端目录给出,前端不再硬编码档位 token。 */
recommended: boolean;
/** 是否不设并发上限;由后端按哨兵值派生,前端不再硬编码 `128`。 */
unlimitedConcurrency: boolean;
enabled: boolean;
};
export type ProfileMembershipPlanRecord = ProfileMembershipPlanResponse;
export type ProfileMembership = {
status: ProfileMembershipStatus;
/** 当前档位;非会员与已到期都是 `normal`。 */
plan: ProfileMembershipPlan;
startedAt: string | null;
expiresAt: string | null;
updatedAt: string | null;
cycleStartedAt: string | null;
cycleResetsAt: string | null;
cycleGrantedPoints: number;
cycleRemainingPoints: number;
/** 当前期序号(1-based)。 */
cycleIndex: number;
/** 本次购买总期数(月付 1 / 年付 12)。 */
cycleCount: number;
cycleKind: ProfileMembershipCycleKind;
};
export type ProfileMembership = ProfileMembershipResponse;
/** 补差升级报价;下单时后端会用同一套规则重算,前端展示的数字不是真相源。 */
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 ProfileMembershipUpgradeQuote =
ProfileMembershipUpgradeQuoteResponse;
export type ProfileRechargeOrder = {
orderId: string;
productId: string;
productTitle: string;
/** 充值订单;`kind` / `status` 是前端收窄的联合类型,其余字段以后端生成为准。 */
export type ProfileRechargeOrder = Omit<
ProfileRechargeOrderResponse,
'kind' | 'status'
> & {
kind: ProfileRechargeProductKind;
amountCents: number;
status: ProfileRechargeOrderStatus;
paymentChannel: string;
paidAt: string | null;
providerTransactionId: string | null;
createdAt: string;
pointsDelta: number;
membershipExpiresAt: string | null;
expiredAt?: string | null;
expirationCheckedAt?: string | null;
expirationProviderState?: string | null;
expirationLastError?: string | null;
};
export type ProfileRechargeCenterResponse = {
// TODO mirror backend: should not nullable
mudPointBalance?: ProfileMudPointBalance;
membership: ProfileMembership;
/** 充值中心;结构以后端生成为准,仅把商品 / 订单收窄成带联合类型的本地别名。 */
export type ProfileRechargeCenterResponse = Omit<
GeneratedProfileRechargeCenterResponse,
'pointProducts' | 'latestOrder'
> & {
pointProducts: ProfileRechargeProduct[];
/** 会员档位目录(后台可改价);会员购买 / 升级的价格与权益以它为准。 */
membershipPlans: ProfileMembershipPlanRecord[];
latestOrder: ProfileRechargeOrder | null;
hasPointsRecharged: boolean;
dailyFreePoints?: ProfileDailyFreePoints;
};
export type WechatMiniProgramPayParams = {
@@ -191,9 +191,23 @@ pub struct ProfileDashboardSummaryResponse {
#[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 ProfileDailyFreePointsResponse {
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub day_key: i64,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub granted_points: u64,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub remaining_points: u64,
pub resets_at: String,
pub updated_at: String,
@@ -467,12 +481,26 @@ where
#[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 ProfileRechargeProductResponse {
pub product_id: String,
pub title: String,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub price_cents: u64,
pub kind: String,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub points_amount: u64,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub bonus_points: u64,
pub duration_days: u32,
pub badge_label: String,
@@ -481,14 +509,28 @@ pub struct ProfileRechargeProductResponse {
#[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 ProfileMembershipPlanResponse {
/// 档位 token:`normal` / `starter` / `plus` / `pro` / `max`;未知取值按 `max` 兜底。
#[serde(deserialize_with = "deserialize_forward_compat_membership_plan")]
pub plan: ProfileMembershipPlanToken,
pub title: String,
pub rank: u8,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub month_price_cents: u64,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub year_price_cents: u64,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub period_points: u64,
/// 模型权限 token:`basic` / `full`;未知取值按 `full` 兜底。
#[serde(deserialize_with = "deserialize_forward_compat_model_access")]
@@ -504,6 +546,17 @@ pub struct ProfileMembershipPlanResponse {
#[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 ProfileMembershipResponse {
/// 会员状态 token:`normal` / `active`;未知取值按 `active` 兜底。
#[serde(deserialize_with = "deserialize_forward_compat_membership_status")]
@@ -516,7 +569,9 @@ pub struct ProfileMembershipResponse {
pub updated_at: Option<String>,
pub cycle_started_at: Option<String>,
pub cycle_resets_at: Option<String>,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub cycle_granted_points: u64,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub cycle_remaining_points: u64,
/// 当前期序号(1-based)。
pub cycle_index: u32,
@@ -529,17 +584,30 @@ pub struct ProfileMembershipResponse {
#[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 ProfileRechargeOrderResponse {
pub order_id: String,
pub product_id: String,
pub product_title: String,
pub kind: String,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub amount_cents: u64,
pub status: String,
pub payment_channel: String,
pub paid_at: Option<String>,
pub provider_transaction_id: Option<String>,
pub created_at: String,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub points_delta: i64,
pub membership_expires_at: Option<String>,
pub expired_at: Option<String>,
@@ -591,6 +659,17 @@ pub struct WechatNativePaymentResponse {
#[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 ProfileRechargeCenterResponse {
pub mud_point_balance: ProfileMudPointBalanceResponse,
pub membership: ProfileMembershipResponse,
@@ -908,6 +987,17 @@ pub struct AdminUpsertProfileRechargeProductRequest {
#[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 ProfileMembershipUpgradeQuoteRequest {
/// 目标档位 token:`starter` / `plus` / `pro` / `max`。
pub target_plan: ProfileMembershipPlanToken,
@@ -916,6 +1006,17 @@ pub struct ProfileMembershipUpgradeQuoteRequest {
/// 补差升级报价:付款前展示应付金额、补点与变更后的月度余额;下单时后端会用同一函数重算。
#[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 ProfileMembershipUpgradeQuoteResponse {
/// 当前档位 token;未知取值按 `max` 兜底。
#[serde(deserialize_with = "deserialize_forward_compat_membership_plan")]
@@ -930,10 +1031,13 @@ pub struct ProfileMembershipUpgradeQuoteResponse {
pub cycle_index: u32,
pub cycle_count: u32,
/// 应付金额(分);月付 = 月价差,年付 = 年价差 ÷ 12 × (后续完整月数 + 本期剩余比例),向上取整到分。
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub amount_cents: u64,
/// 本期补点;年付按本期剩余比例向下取整。
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub granted_points_delta: u64,
/// 变更后月度余额 = 原剩余月度余额 + 补点(已用点不返还,永久泥点不动)。
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub monthly_balance_after: u64,
pub expires_at: Option<String>,
pub cycle_resets_at: Option<String>,
@@ -942,6 +1046,17 @@ pub struct ProfileMembershipUpgradeQuoteResponse {
#[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 ProfileMembershipPlanAdminListResponse {
pub entries: Vec<ProfileMembershipPlanResponse>,
/// 并发哨兵阈值:`concurrent_job_limit >= 该值` 即不设上限;由后端下发,前端不硬编码。
@@ -951,12 +1066,26 @@ pub struct ProfileMembershipPlanAdminListResponse {
/// 后台按档位改会员价格与权益;`rank` 不在请求里,升级比较基准由代码内目录决定。
#[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 AdminUpsertProfileMembershipPlanRequest {
/// 档位 token,作为主键即档位身份。
pub plan: ProfileMembershipPlanToken,
pub title: String,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub month_price_cents: u64,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub year_price_cents: u64,
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
pub period_points: u64,
/// 模型权限 token:`basic` / `full`;本期只落字段、不做服务端拦截。
pub model_access: ProfileMembershipModelAccessToken,