say hello to pay system

This commit is contained in:
Zixin Zhou 2025-08-26 21:24:39 +08:00
parent eccb0a3e53
commit e57d9e6c6f
6 changed files with 793 additions and 2 deletions

View File

@ -19,6 +19,10 @@ export default function DashboardPage() {
const [isBackgroundRefreshing, setIsBackgroundRefreshing] = useState(false);
const [lastUpdateTime, setLastUpdateTime] = useState<Date | null>(null);
const [connectionStatus, setConnectionStatus] = useState<'connected' | 'disconnected' | 'checking'>('checking');
// 支付成功状态
const [showPaymentSuccess, setShowPaymentSuccess] = useState(false);
const [paymentData, setPaymentData] = useState<any>(null);
// 使用 ref 来存储最新的状态,避免定时器闭包问题
const stateRef = useRef({ isUsingMockData, dashboardData });
@ -31,6 +35,40 @@ export default function DashboardPage() {
// 检测支付成功
useEffect(() => {
const sessionId = searchParams.get('session_id');
const payment = searchParams.get('payment');
if (sessionId && payment === 'success') {
// 显示支付成功提示
setShowPaymentSuccess(true);
// 获取支付详情
fetchPaymentDetails(sessionId);
// 清除URL参数避免刷新页面时重复显示
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete('session_id');
newUrl.searchParams.delete('payment');
window.history.replaceState({}, '', newUrl.pathname);
}
}, [searchParams]);
// 获取支付详情
const fetchPaymentDetails = async (sessionId: string) => {
try {
const response = await fetch(`/api/payment/checkout-status/${sessionId}?user_id=test_user_123`);
const result = await response.json();
if (result.successful && result.data) {
setPaymentData(result.data);
}
} catch (error) {
console.error('获取支付详情失败:', error);
}
};
// 初始加载数据
const fetchInitialData = async () => {
try {
@ -370,6 +408,35 @@ export default function DashboardPage() {
return (
<div className="h-full bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex flex-col overflow-hidden">
{/* 支付成功提示 */}
{showPaymentSuccess && paymentData && (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 bg-green-50 border border-green-200 rounded-lg p-4 shadow-lg max-w-md">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-green-800"></h3>
<p className="text-sm text-green-700 mt-1">
: {paymentData.biz_order_no}
</p>
</div>
<div className="ml-auto pl-3">
<button
onClick={() => setShowPaymentSuccess(false)}
className="text-green-400 hover:text-green-600"
>
<svg className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
)}
{/* 后台刷新指示器 - 优化用户体验 */}
{isBackgroundRefreshing && (
<div className="fixed top-4 right-4 z-50 bg-blue-500/90 backdrop-blur-sm text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 max-w-xs">

View File

@ -0,0 +1,236 @@
'use client';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { CheckCircle, Loader2, XCircle } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
interface PaymentStatus {
payment_status: 'pending' | 'success' | 'fail';
biz_order_no: string;
pay_time?: string;
subscription?: {
plan_name: string;
plan_display_name: string;
status: string;
current_period_end?: string;
};
}
export default function PaymentSuccessPage() {
const searchParams = useSearchParams();
const sessionId = searchParams.get('session_id');
const [status, setStatus] = useState<'loading' | 'success' | 'failed' | 'timeout'>('loading');
const [paymentData, setPaymentData] = useState<PaymentStatus | null>(null);
const [attempts, setAttempts] = useState(0);
useEffect(() => {
if (!sessionId) {
setStatus('failed');
return;
}
const pollPaymentStatus = async () => {
const maxAttempts = 30; // 最多轮询30次
const interval = 2000; // 每2秒轮询一次
for (let i = 0; i < maxAttempts; i++) {
setAttempts(i + 1);
try {
// 使用新的Checkout Session状态查询
const { getCheckoutSessionStatus } = await import('@/lib/stripe');
const result = await getCheckoutSessionStatus(sessionId, 'test_user_123'); // 临时测试用户ID
if (result.successful && result.data) {
setPaymentData(result.data);
if (result.data.payment_status === 'success') {
setStatus('success');
return;
} else if (result.data.payment_status === 'fail') {
setStatus('failed');
return;
}
}
// 等待下次轮询
await new Promise(resolve => setTimeout(resolve, interval));
} catch (error) {
console.error('轮询Checkout Session状态失败:', error);
}
}
// 轮询超时
setStatus('timeout');
};
pollPaymentStatus();
}, [sessionId]);
const renderContent = () => {
switch (status) {
case 'loading':
return (
<Card className="max-w-md mx-auto">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<Loader2 className="w-16 h-16 text-blue-500 animate-spin" />
</div>
<CardTitle>...</CardTitle>
<CardDescription>
<br />
: {attempts}/30
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 text-center">
</p>
</CardContent>
</Card>
);
case 'success':
return (
<Card className="max-w-md mx-auto">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<CheckCircle className="w-16 h-16 text-green-500" />
</div>
<CardTitle className="text-green-600"></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{paymentData?.subscription && (
<div className="bg-green-50 p-4 rounded-lg">
<h3 className="font-semibold text-green-800">
{paymentData.subscription.plan_display_name}
</h3>
<p className="text-sm text-green-600">
: {paymentData.subscription.status}
</p>
{paymentData.subscription.current_period_end && (
<p className="text-sm text-green-600">
: {new Date(paymentData.subscription.current_period_end).toLocaleDateString()}
</p>
)}
</div>
)}
<div className="text-sm text-gray-600">
<p>: {paymentData?.biz_order_no}</p>
{paymentData?.pay_time && (
<p>: {new Date(paymentData.pay_time).toLocaleString()}</p>
)}
</div>
<div className="flex gap-2">
<Button
onClick={() => window.location.href = '/dashboard'}
className="flex-1"
>
</Button>
<Button
onClick={() => window.location.href = '/'}
variant="outline"
className="flex-1"
>
</Button>
</div>
</CardContent>
</Card>
);
case 'failed':
return (
<Card className="max-w-md mx-auto">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<XCircle className="w-16 h-16 text-red-500" />
</div>
<CardTitle className="text-red-600"></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-600 text-center">
</p>
<div className="flex gap-2">
<Button
onClick={() => window.location.href = '/pricing'}
className="flex-1"
>
</Button>
<Button
onClick={() => window.location.href = '/'}
variant="outline"
className="flex-1"
>
</Button>
</div>
</CardContent>
</Card>
);
case 'timeout':
return (
<Card className="max-w-md mx-auto">
<CardHeader className="text-center">
<div className="flex justify-center mb-4">
<Loader2 className="w-16 h-16 text-yellow-500" />
</div>
<CardTitle className="text-yellow-600"></CardTitle>
<CardDescription>
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-600 text-center">
</p>
<div className="flex gap-2">
<Button
onClick={() => window.location.reload()}
className="flex-1"
>
</Button>
<Button
onClick={() => window.location.href = '/dashboard'}
variant="outline"
className="flex-1"
>
</Button>
</div>
</CardContent>
</Card>
);
default:
return null;
}
};
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="w-full max-w-lg">
{renderContent()}
</div>
</div>
);
}

282
app/pricing/page.tsx Normal file
View File

@ -0,0 +1,282 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Check, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { fetchSubscriptionPlans, SubscriptionPlan } from '@/lib/stripe';
export default function PricingPage() {
const router = useRouter();
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
// 从后端获取订阅计划数据
useEffect(() => {
const loadPlans = async () => {
try {
const plansData = await fetchSubscriptionPlans();
setPlans(plansData);
} catch (err) {
console.error('加载订阅计划失败:', err);
}
};
loadPlans();
}, []);
// 转换后端数据为前端显示格式,保持原有的数据结构
const transformPlanForDisplay = (plan: SubscriptionPlan) => {
const monthlyPrice = plan.price_monthly / 100; // 后端存储的是分,转换为元
const yearlyPrice = plan.price_yearly / 100;
return {
name: plan.name,
displayName: plan.display_name,
price: {
monthly: monthlyPrice,
yearly: yearlyPrice
},
description: plan.description,
features: plan.features || [],
popular: plan.is_popular, // 使用后端返回的 is_popular 字段
buttonText: plan.is_free ? 'Start Free Trial' : 'Subscribe',
buttonVariant: plan.is_free ? 'outline' as const : 'default' as const
};
};
const handleSubscribe = async (planName: string) => {
if (planName === 'hobby') {
return;
}
try {
// 使用新的Checkout Session方案更简单
const { createCheckoutSession, redirectToCheckout } = await import('@/lib/stripe');
// 从localStorage获取当前用户信息
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
if (!User.id) {
throw new Error("无法获取用户ID请重新登录");
}
// 1. 创建Checkout Session
const result = await createCheckoutSession({
user_id: String(User.id),
plan_name: planName,
billing_cycle: billingCycle
});
if (!result.successful || !result.data) {
throw new Error("create checkout session failed");
}
// 2. 直接跳转到Stripe托管页面就这么简单
redirectToCheckout(result.data.checkout_url);
} catch (error) {
throw new Error("create checkout session failed, please try again later");
}
};
// 如果还没有加载到数据,显示加载状态但保持原有样式
if (plans.length === 0) {
return (
<div className="min-h-screen bg-black text-white">
<main className="container mx-auto px-6 py-16">
<div className="mb-8">
<Button
variant="ghost"
onClick={() => router.back()}
className="text-gray-400 hover:text-white"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>
<div className="text-center mb-16">
<h1 className="text-6xl font-bold mb-6">Pricing</h1>
<p className="text-xl text-gray-400 mb-8">Choose the plan that suits you best</p>
<div className="inline-flex items-center bg-gray-900/50 rounded-full p-1">
<button
onClick={() => setBillingCycle('yearly')}
className={`flex items-center justify-center px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
billingCycle === 'yearly'
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
: 'text-gray-300 hover:text-white'
}`}
>
Pay Yearly
<span className={`ml-2 text-sm ${
billingCycle === 'yearly'
? 'text-white/90'
: 'text-gray-400'
}`}>
Up to 20% off 🔥
</span>
</button>
<button
onClick={() => setBillingCycle('monthly')}
className={`px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
billingCycle === 'monthly'
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
: 'text-gray-300 hover:text-white'
}`}
>
Pay Monthly
</button>
</div>
</div>
<div className="mb-16">
<div className="text-center text-gray-400">
<p>...</p>
</div>
</div>
</main>
</div>
);
}
return (
<div className="min-h-screen bg-black text-white">
{/* Main Content */}
<main className="container mx-auto px-6 py-16">
{/* Back Button */}
<div className="mb-8">
<Button
variant="ghost"
onClick={() => router.back()}
className="text-gray-400 hover:text-white"
>
<ArrowLeft className="mr-2 h-4 w-4" />
Back
</Button>
</div>
<div className="text-center mb-16">
<h1 className="text-6xl font-bold mb-6">Pricing</h1>
<p className="text-xl text-gray-400 mb-8">Choose the plan that suits you best</p>
{/* Billing Toggle */}
<div className="inline-flex items-center bg-gray-900/50 rounded-full p-1">
<button
onClick={() => setBillingCycle('yearly')}
className={`flex items-center justify-center px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
billingCycle === 'yearly'
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
: 'text-gray-300 hover:text-white'
}`}
>
Pay Yearly
<span className={`ml-2 text-sm ${
billingCycle === 'yearly'
? 'text-white/90'
: 'text-gray-400'
}`}>
Up to 20% off 🔥
</span>
</button>
<button
onClick={() => setBillingCycle('monthly')}
className={`px-6 py-2.5 rounded-full transition-all duration-300 ease-out font-medium text-base ${
billingCycle === 'monthly'
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
: 'text-gray-300 hover:text-white'
}`}
>
Pay Monthly
</button>
</div>
</div>
{/* Plans Section */}
<div className="mb-16">
<div className="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto">
{plans.map((plan) => {
const displayPlan = transformPlanForDisplay(plan);
return (
<div
key={plan.id}
className={`flex flex-col h-full transition-all duration-300 ${
displayPlan.popular ? 'hover:scale-105 hover:shadow-2xl' : ''
}`}
style={{ minHeight: '540px' }}
>
{/* 预留标签空间,确保所有卡片组合高度一致 */}
<div className="h-10 flex items-center justify-center">
{displayPlan.popular && (
<div className="bg-gradient-to-r from-pink-500 to-purple-600 text-white py-2 text-center text-sm font-medium rounded-t-2xl w-full shadow-lg">
Most Popular
</div>
)}
</div>
<Card
className={`bg-gray-900/50 ${displayPlan.popular ? 'border-l border-r border-b border-gray-700/50 rounded-b-2xl rounded-t-none' : 'border border-gray-700/50 rounded-2xl'} overflow-hidden ${displayPlan.popular ? '' : 'transition-all duration-300 hover:scale-105 hover:shadow-2xl'} flex flex-col flex-grow ${
displayPlan.popular ? 'ring-2 ring-pink-500/50 shadow-pink-500/20' : ''
}`}
>
<CardHeader className="pt-8 pb-6 flex-shrink-0 text-center">
<CardTitle className="text-white text-2xl font-bold mb-4">{displayPlan.displayName}</CardTitle>
<div className="mb-4">
<div className="text-4xl font-bold text-white">
{displayPlan.price[billingCycle] === 0 ? (
'Free'
) : (
<>
<span className="text-5xl">${displayPlan.price[billingCycle]}</span>
<span className="text-lg text-gray-400 font-normal">
/{billingCycle === 'monthly' ? 'month' : 'year'}
</span>
</>
)}
</div>
</div>
<CardDescription className="text-gray-300 text-base px-4">
{displayPlan.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-8 pb-10 flex-grow flex flex-col justify-between">
<div className="space-y-5">
{displayPlan.features.map((feature, index) => (
<div key={index} className="flex items-start space-x-3">
<div className="w-5 h-5 bg-green-500/20 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
<Check className="w-3 h-3 text-green-400" />
</div>
<span className="text-gray-300 text-sm leading-relaxed">{feature}</span>
</div>
))}
</div>
<Button
onClick={() => handleSubscribe(displayPlan.name)}
variant={displayPlan.buttonVariant}
className={`w-full py-4 rounded-xl font-medium text-base transition-all duration-300 mt-6 ${
displayPlan.buttonText === 'Start Free Trial'
? 'border-2 border-gray-600 text-white hover:border-pink-500 hover:text-pink-400 hover:bg-gray-800/50'
: 'bg-gradient-to-r from-pink-500 to-purple-600 text-white hover:from-pink-600 hover:to-purple-700 shadow-lg hover:shadow-xl'
}`}
>
{displayPlan.buttonText}
</Button>
</CardContent>
</Card>
</div>
);
})}
</div>
</div>
</main>
</div>
);
}

View File

@ -73,6 +73,16 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
</div>
<div className="flex items-center space-x-4">
{/* Pricing Link */}
<Button
variant="ghost"
size="sm"
onClick={() => router.push('/pricing')}
className="text-gray-300 hover:text-white"
>
Pricing
</Button>
{/* Notifications */}
<Button variant="ghost" size="sm">
<Bell className="h-4 w-4" />

View File

@ -1,9 +1,9 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { Table, AlignHorizontalSpaceAround, Loader2, Clapperboard } from "lucide-react";
import { Table, AlignHorizontalSpaceAround, Loader2, Clapperboard, CreditCard } from "lucide-react";
import "./style/home-page2.css";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { VideoCarouselLayout } from '@/components/video-carousel-layout';
import { VideoGridLayout } from '@/components/video-grid-layout';
import { motion, AnimatePresence } from "framer-motion";
@ -24,6 +24,7 @@ import {
export function HomePage2() {
const router = useRouter();
const searchParams = useSearchParams();
const [activeTool, setActiveTool] = useState("stretch");
const [dropPosition, setDropPosition] = useState<"left" | "right">("left");
const [isCreating, setIsCreating] = useState(false);
@ -31,6 +32,10 @@ export function HomePage2() {
const [resources, setResources] = useState<Resource[]>([]);
const [isLoadingResources, setIsLoadingResources] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
// 支付成功状态
const [showPaymentSuccess, setShowPaymentSuccess] = useState(false);
const [paymentData, setPaymentData] = useState<any>(null);
// 将资源数据转换为视频格式
const videos = resources.map(resource => ({
@ -57,6 +62,40 @@ export function HomePage2() {
}
};
// 检测支付成功
useEffect(() => {
const sessionId = searchParams.get('session_id');
const payment = searchParams.get('payment');
if (sessionId && payment === 'success') {
// 显示支付成功提示
setShowPaymentSuccess(true);
// 获取支付详情
fetchPaymentDetails(sessionId);
// 清除URL参数避免刷新页面时重复显示
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete('session_id');
newUrl.searchParams.delete('payment');
window.history.replaceState({}, '', newUrl.pathname);
}
}, [searchParams]);
// 获取支付详情
const fetchPaymentDetails = async (sessionId: string) => {
try {
const response = await fetch(`/api/payment/checkout-status/${sessionId}?user_id=test_user_123`);
const result = await response.json();
if (result.successful && result.data) {
setPaymentData(result.data);
}
} catch (error) {
console.error('获取支付详情失败:', error);
}
};
// 组件挂载时获取资源
useEffect(() => {
fetchResources();
@ -119,6 +158,35 @@ export function HomePage2() {
return (
<div className="min-h-screen bg-[var(--background)]" ref={containerRef}>
{/* 支付成功提示 */}
{showPaymentSuccess && paymentData && (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 z-50 bg-green-50 border border-green-200 rounded-lg p-4 shadow-lg max-w-md">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-green-800"></h3>
<p className="text-sm text-green-700 mt-1">
: {paymentData.biz_order_no}
</p>
</div>
<div className="ml-auto pl-3">
<button
onClick={() => setShowPaymentSuccess(false)}
className="text-green-400 hover:text-green-600"
>
<svg className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
)}
<div className="flex relative" style={{height: '100vh'}}>
{/* 工具栏-列表形式切换 */}
<div className="absolute top-[8rem] z-[50] right-6 w-[128px] flex justify-end">
@ -188,6 +256,20 @@ export function HomePage2() {
/>
</div>
{/* Pricing 入口 */}
<div className="fixed bottom-[3rem] right-[3rem] z-50">
<LiquidButton className="w-[120px] h-[48px] text-sm">
<div className="flex items-center justify-center gap-2"
onClick={(e) => {
e.stopPropagation();
router.push('/pricing');
}}>
<CreditCard className="w-5 h-5 text-white" />
Pricing
</div>
</LiquidButton>
</div>
<div className="w-[128px] h-[128px] rounded-[50%] overflow-hidden fixed bottom-[3rem] left-[50%] -translate-x-1/2 z-50">
<LiquidButton className="w-[128px] h-[128px] text-lg">
<div className="flex items-center justify-center gap-2"

114
lib/stripe.ts Normal file
View File

@ -0,0 +1,114 @@
/**
* Stripe
*/
import { post, get } from '@/api/request';
import { ApiResponse } from '@/api/common';
export interface SubscriptionPlan {
id: number;
name: string;
display_name: string;
description: string;
price_monthly: number;
price_yearly: number;
features: string[];
is_free: boolean;
is_popular: boolean;
sort_order: number;
}
export interface PaymentStatusData {
payment_status: 'pending' | 'success' | 'fail';
biz_order_no: string;
pay_time?: string;
subscription?: {
plan_name: string;
plan_display_name: string;
status: string;
current_period_end?: string;
};
}
export type PaymentStatusResponse = ApiResponse<PaymentStatusData>;
export interface CreateCheckoutSessionRequest {
user_id: string;
plan_name: string;
billing_cycle: 'monthly' | 'yearly';
}
export interface CreateCheckoutSessionData {
checkout_url: string;
session_id: string;
biz_order_no: string;
amount: number;
currency: string;
}
export type CreateCheckoutSessionResponse = ApiResponse<CreateCheckoutSessionData>;
/**
*
* API获取所有活跃的订阅计划
*/
export async function fetchSubscriptionPlans(): Promise<SubscriptionPlan[]> {
try {
const response = await get<ApiResponse<SubscriptionPlan[]>>('/api/subscription/plans');
if (!response.successful || !response.data) {
throw new Error(response.message || '获取订阅计划失败');
}
// 后端已经过滤了活跃计划,直接按排序顺序排列
const sortedPlans = response.data.sort((a, b) => a.sort_order - b.sort_order);
return sortedPlans;
} catch (error) {
console.error('获取订阅计划失败:', error);
throw error;
}
}
/**
* Checkout Session
*
*
* 1. checkout_url
* 2. checkout_url
* 3. Stripe页面完成支付
* 4. success_url
*/
export async function createCheckoutSession(
request: CreateCheckoutSessionRequest
): Promise<CreateCheckoutSessionResponse> {
try {
return await post<CreateCheckoutSessionResponse>('/api/payment/checkoutDeepControl', request);
} catch (error) {
console.error('创建Checkout Session失败:', error);
throw error;
}
}
/**
* Checkout Session状态
*/
export async function getCheckoutSessionStatus(
sessionId: string,
userId: string
): Promise<PaymentStatusResponse> {
try {
return await get<PaymentStatusResponse>(`/api/payment/checkout-status/${sessionId}?user_id=${userId}`);
} catch (error) {
console.error('查询Checkout Session状态失败:', error);
throw error;
}
}
/**
* Checkout页面的工具函数
*/
export function redirectToCheckout(checkoutUrl: string) {
if (typeof window !== 'undefined') {
window.location.href = checkoutUrl;
}
}