feat: integrate Stripe payment with subscription plans
Add Stripe payment integration across the project: - Add @stripe/stripe-js dependency - Configure Stripe environment variables (secret key, publishable key, webhook secret) for dev, local, and production environments - Add Product/Price IDs for monthly, quarterly, and annual subscription tiers - Extend subscription plan data with voiceMinutesPerDay quotas (30/45/60 minutes per tier) - Update .gitignore to exclude implementation_plan.md
This commit is contained in:
@@ -4,6 +4,13 @@
|
||||
* Mock 阶段硬编码,后续可改为从 `ApiPath.paymentProducts` 拉取。
|
||||
*
|
||||
* 原始 Dart: lib/ui/subscription/...
|
||||
*
|
||||
* Stripe 接入新增字段:
|
||||
* - `stripeProductId` / `stripePriceId`:Stripe 后台创建 Product + Price 后拿到的 id
|
||||
* - `voiceMinutesPerDay`:该套餐每天给用户的语音分钟数(订阅后每天补充)
|
||||
*
|
||||
* 注:stripeProductId / stripePriceId 没有默认值 —— 为避免硬编码真实 id,由 env 动态注入(见 `src/lib/stripe/stripe-products.ts`)
|
||||
* 本常量表只是 UI 形状 + 业务字段的单一真相源
|
||||
*/
|
||||
export type SubscriptionPlanId = "monthly" | "quarterly" | "annual";
|
||||
|
||||
@@ -19,6 +26,8 @@ export interface SubscriptionPlan {
|
||||
perDay: string;
|
||||
/** 是否高亮(默认推荐:月付) */
|
||||
highlight: boolean;
|
||||
/** 该套餐每天补充的语音聊天分钟数(业务常量) */
|
||||
voiceMinutesPerDay: number;
|
||||
}
|
||||
|
||||
export const SUBSCRIPTION_PLANS: readonly SubscriptionPlan[] = [
|
||||
@@ -29,6 +38,7 @@ export const SUBSCRIPTION_PLANS: readonly SubscriptionPlan[] = [
|
||||
originalPrice: 24.9,
|
||||
perDay: "$0.66/day",
|
||||
highlight: true,
|
||||
voiceMinutesPerDay: 30, // 30 分钟/天
|
||||
},
|
||||
{
|
||||
id: "quarterly",
|
||||
@@ -37,6 +47,7 @@ export const SUBSCRIPTION_PLANS: readonly SubscriptionPlan[] = [
|
||||
originalPrice: 59.9,
|
||||
perDay: "$0.55/day",
|
||||
highlight: false,
|
||||
voiceMinutesPerDay: 45, // 45 分钟/天(比月付多,性价比)
|
||||
},
|
||||
{
|
||||
id: "annual",
|
||||
@@ -45,5 +56,6 @@ export const SUBSCRIPTION_PLANS: readonly SubscriptionPlan[] = [
|
||||
originalPrice: 199.9,
|
||||
perDay: "$0.47/day",
|
||||
highlight: false,
|
||||
voiceMinutesPerDay: 60, // 60 分钟/天(年度最多)
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -24,6 +24,14 @@ export class User {
|
||||
declare readonly avatarUrl: string;
|
||||
declare readonly loginProvider: string;
|
||||
declare readonly isGuest: boolean;
|
||||
/** 是否为 VIP 会员(Stripe webhook 更新) */
|
||||
declare readonly isVip: boolean;
|
||||
/** 语音聊天剩余分钟数(Stripe webhook 更新) */
|
||||
declare readonly voiceMinutesRemaining: number;
|
||||
/** Stripe Customer ID —— 首次 Checkout 完成后由 webhook 设置 */
|
||||
declare readonly stripeCustomerId: string | null;
|
||||
|
||||
|
||||
|
||||
private constructor(input: UserInput) {
|
||||
const data = UserSchema.parse(input);
|
||||
|
||||
@@ -17,12 +17,20 @@ export const UserSchema = z.object({
|
||||
personalityTraits: PersonalityTraitsSchema.default(PERSONALITY_TRAITS_DEFAULTS),
|
||||
preferredLanguage: z.string().default(""),
|
||||
createdAt: z.string().default(""),
|
||||
// 后端**新**游客返回 null("还没发过消息"),`.default("")` **不**兜 null —— 需 nullable
|
||||
// 后端新游客返回 null("还没发过消息"),`.default("")` 不兜 null —— 需 nullable
|
||||
lastMessageAt: z.string().nullable().default(null),
|
||||
avatarUrl: z.string().default(""),
|
||||
loginProvider: z.string().default("email"),
|
||||
isGuest: z.boolean().default(false),
|
||||
/** 是否为 VIP 会员(订阅中或未过期) —— Stripe webhook 更新 */
|
||||
isVip: z.boolean().default(false),
|
||||
/** 语音聊天剩余分钟数(订阅生效后每天补充;用完为 0 不变) */
|
||||
voiceMinutesRemaining: z.number().default(0),
|
||||
/** Stripe Customer ID —— 首次 Checkout 完成后由 webhook 设置,用于 Customer Portal 自助管理 */
|
||||
stripeCustomerId: z.string().nullable().default(null),
|
||||
});
|
||||
|
||||
|
||||
|
||||
export type UserInput = z.input<typeof UserSchema>;
|
||||
export type UserData = z.output<typeof UserSchema>;
|
||||
|
||||
Reference in New Issue
Block a user