diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index f888d5913..aa532dc60 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -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; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 13dc664d9..46ba3d5af 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -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。 diff --git a/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md b/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md index cc74ca675..3d1abaf46 100644 --- a/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md +++ b/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md @@ -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` diff --git a/packages/shared/src/contracts/generated/AdminUpsertProfileMembershipPlanRequest.ts b/packages/shared/src/contracts/generated/AdminUpsertProfileMembershipPlanRequest.ts new file mode 100644 index 000000000..e1cbc53ab --- /dev/null +++ b/packages/shared/src/contracts/generated/AdminUpsertProfileMembershipPlanRequest.ts @@ -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, }; diff --git a/packages/shared/src/contracts/generated/ProfileDailyFreePointsResponse.ts b/packages/shared/src/contracts/generated/ProfileDailyFreePointsResponse.ts new file mode 100644 index 000000000..398f8425b --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileDailyFreePointsResponse.ts @@ -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, }; diff --git a/packages/shared/src/contracts/generated/ProfileMembershipPlanAdminListResponse.ts b/packages/shared/src/contracts/generated/ProfileMembershipPlanAdminListResponse.ts new file mode 100644 index 000000000..8749193c5 --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileMembershipPlanAdminListResponse.ts @@ -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, +/** + * 并发哨兵阈值:`concurrent_job_limit >= 该值` 即不设上限;由后端下发,前端不硬编码。 + */ +unlimitedConcurrencyThreshold: number, }; diff --git a/packages/shared/src/contracts/generated/ProfileMembershipPlanResponse.ts b/packages/shared/src/contracts/generated/ProfileMembershipPlanResponse.ts new file mode 100644 index 000000000..adca547ab --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileMembershipPlanResponse.ts @@ -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, }; diff --git a/packages/shared/src/contracts/generated/ProfileMembershipResponse.ts b/packages/shared/src/contracts/generated/ProfileMembershipResponse.ts new file mode 100644 index 000000000..fa5fd8db4 --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileMembershipResponse.ts @@ -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, }; diff --git a/packages/shared/src/contracts/generated/ProfileMembershipUpgradeQuoteRequest.ts b/packages/shared/src/contracts/generated/ProfileMembershipUpgradeQuoteRequest.ts new file mode 100644 index 000000000..785ffdd1f --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileMembershipUpgradeQuoteRequest.ts @@ -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, }; diff --git a/packages/shared/src/contracts/generated/ProfileMembershipUpgradeQuoteResponse.ts b/packages/shared/src/contracts/generated/ProfileMembershipUpgradeQuoteResponse.ts new file mode 100644 index 000000000..9301658ab --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileMembershipUpgradeQuoteResponse.ts @@ -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, }; diff --git a/packages/shared/src/contracts/generated/ProfileRechargeCenterResponse.ts b/packages/shared/src/contracts/generated/ProfileRechargeCenterResponse.ts new file mode 100644 index 000000000..4eb1a0a85 --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileRechargeCenterResponse.ts @@ -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, +/** + * 会员档位目录(后台可改价),会员购买 / 升级的价格与权益都以它为准。 + */ +membershipPlans: Array, latestOrder: ProfileRechargeOrderResponse | null, hasPointsRecharged: boolean, dailyFreePoints: ProfileDailyFreePointsResponse, }; diff --git a/packages/shared/src/contracts/generated/ProfileRechargeOrderResponse.ts b/packages/shared/src/contracts/generated/ProfileRechargeOrderResponse.ts new file mode 100644 index 000000000..7178fb1de --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileRechargeOrderResponse.ts @@ -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, }; diff --git a/packages/shared/src/contracts/generated/ProfileRechargeProductResponse.ts b/packages/shared/src/contracts/generated/ProfileRechargeProductResponse.ts new file mode 100644 index 000000000..84f522479 --- /dev/null +++ b/packages/shared/src/contracts/generated/ProfileRechargeProductResponse.ts @@ -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, }; diff --git a/packages/shared/src/contracts/runtime.ts b/packages/shared/src/contracts/runtime.ts index d8bafe63e..e00ad4071 100644 --- a/packages/shared/src/contracts/runtime.ts +++ b/packages/shared/src/contracts/runtime.ts @@ -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 = { diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index 927446144..03a7bc41b 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -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, pub cycle_started_at: Option, pub cycle_resets_at: Option, + #[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, pub provider_transaction_id: Option, pub created_at: String, + #[cfg_attr(feature = "ts-bindings", ts(type = "number"))] pub points_delta: i64, pub membership_expires_at: Option, pub expired_at: Option, @@ -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, pub cycle_resets_at: Option, @@ -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, /// 并发哨兵阈值:`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,