From b0bae22ad092008f12bd1ec0f01134ec9587e23b Mon Sep 17 00:00:00 2001 From: zyh Date: Tue, 22 Oct 2024 07:50:19 +0000 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=B7=BB=E5=8A=A0=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E6=8B=AC=E8=AE=A2=E9=98=85=E8=AE=A1=E5=88=92=E5=92=8C=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E8=B4=AD=E4=B9=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/auth/SubscriptionDialog.tsx | 140 +++++++++++++-------- app/components/header/Header.tsx | 11 ++ app/routes/api.purchase-subscription.ts | 42 +++++++ app/routes/api.subscription-plans.ts | 12 ++ app/routes/api.user-subscription.ts | 24 ++++ 5 files changed, 176 insertions(+), 53 deletions(-) create mode 100644 app/routes/api.purchase-subscription.ts create mode 100644 app/routes/api.subscription-plans.ts create mode 100644 app/routes/api.user-subscription.ts diff --git a/app/components/auth/SubscriptionDialog.tsx b/app/components/auth/SubscriptionDialog.tsx index aa19673..c467c3d 100644 --- a/app/components/auth/SubscriptionDialog.tsx +++ b/app/components/auth/SubscriptionDialog.tsx @@ -1,6 +1,7 @@ import { Dialog, DialogTitle, DialogDescription, DialogRoot } from '~/components/ui/Dialog'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useAuth } from '~/hooks/useAuth'; +import { toast } from 'react-toastify'; interface SubscriptionDialogProps { isOpen: boolean; @@ -8,50 +9,78 @@ interface SubscriptionDialogProps { } interface SubscriptionPlan { + _id: number; name: string; tokens: number; price: number; description: string; - savePercentage?: number; + save_percentage?: number; } -const subscriptionPlans: SubscriptionPlan[] = [ - { - name: "专业版", - tokens: 10000000, - price: 20, - description: "适合业余爱好者和轻度用户进行探索性使用。" - }, - { - name: "专业版 50", - tokens: 26000000, - price: 50, - description: "为每周需要使用多八多几次的专业人士设计。", - savePercentage: 3 - }, - { - name: "专业版 100", - tokens: 55000000, - price: 100, - description: "适合希望提升日常工作流程的重度用户。", - savePercentage: 9 - }, - { - name: "专业版 200", - tokens: 120000000, - price: 200, - description: "最适合将多八多作为核心工具持续使用的超级用户。", - savePercentage: 17 - } -]; +interface UserSubscription { + plan: SubscriptionPlan; + tokensLeft: number; + nextReloadDate: string; +} export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) { const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly'); + const [subscriptionPlans, setSubscriptionPlans] = useState([]); + const [userSubscription, setUserSubscription] = useState(null); + const [isLoading, setIsLoading] = useState(true); const { user } = useAuth(); - if (!user) return null; + useEffect(() => { + if (isOpen && user) { + fetchSubscriptionData(); + } + }, [isOpen, user]); - const currentPlan = subscriptionPlans[1]; // 假设当前用户使用的是"专业版 50" + const fetchSubscriptionData = async () => { + setIsLoading(true); + try { + const [plansResponse, userSubResponse] = await Promise.all([ + fetch('/api/subscription-plans'), + fetch('/api/user-subscription') + ]); + const plans = await plansResponse.json(); + const userSub = await userSubResponse.json(); + setSubscriptionPlans(plans); + setUserSubscription(userSub); + } catch (error) { + console.error('Error fetching subscription data:', error); + toast.error('获取订阅信息失败,请稍后重试。'); + } finally { + setIsLoading(false); + } + }; + + const handlePurchase = async (planId: number) => { + try { + const response = await fetch('/api/purchase-subscription', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + planId, + billingCycle, + }), + }); + const result = await response.json(); + if (response.ok) { + toast.success('订阅购买成功!'); + fetchSubscriptionData(); // 刷新订阅信息 + } else { + toast.error(result.message || '购买失败,请稍后重试。'); + } + } catch (error) { + console.error('Error purchasing subscription:', error); + toast.error('购买过程中出现错误,请稍后重试。'); + } + }; + + if (!user || isLoading) return null; return ( @@ -65,23 +94,27 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)

-
-
-
- 300万 - 代币剩余。 - 2600万代币将在17天后添加。 -
-
- 需要更多代币? -
- - 升级您的计划或购买 - 代币充值包 - + {userSubscription && ( +
+
+
+ {userSubscription.tokensLeft.toLocaleString()} + 代币剩余。 + + {userSubscription.plan.tokens.toLocaleString()}代币将在{new Date(userSubscription.nextReloadDate).toLocaleDateString()}后添加。 + +
+
+ 需要更多代币? +
+ + 升级您的计划或购买 + 代币充值包 + +
-
+ )}
))} diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx index 8601076..9f7391a 100644 --- a/app/components/header/Header.tsx +++ b/app/components/header/Header.tsx @@ -9,12 +9,14 @@ import { LoginDialog } from '~/components/auth/LoginDialog'; import { RegisterDialog } from '~/components/auth/RegisterDialog'; import { useAuth } from '~/hooks/useAuth'; import { UserMenu } from './UserMenu'; +import { SubscriptionDialog } from '~/components/auth/SubscriptionDialog'; export function Header() { const chat = useStore(chatStore); const [isLoginOpen, setIsLoginOpen] = useState(false); const [isRegisterOpen, setIsRegisterOpen] = useState(false); const { isAuthenticated } = useAuth(); + const [isSubscriptionOpen, setIsSubscriptionOpen] = useState(false); return (
)} + {isAuthenticated && ( + + )}
setIsLoginOpen(false)} /> setIsRegisterOpen(false)} /> + setIsSubscriptionOpen(false)} /> ); } diff --git a/app/routes/api.purchase-subscription.ts b/app/routes/api.purchase-subscription.ts new file mode 100644 index 0000000..8dd1458 --- /dev/null +++ b/app/routes/api.purchase-subscription.ts @@ -0,0 +1,42 @@ +import { json } from '@remix-run/cloudflare'; +import { db } from '~/lib/db.server'; +import { requireUserId } from '~/lib/session.server'; + +export async function action({ request }) { + const userId = await requireUserId(request); + const { planId, billingCycle } = await request.json(); + + try { + // 开始数据库事务 + await db.transaction(async (trx) => { + // 获取订阅计划详情 + const plan = await trx('subscription_plans').where('_id', planId).first(); + if (!plan) { + throw new Error('Invalid subscription plan'); + } + + // 计算实际价格和代币数量 + const price = billingCycle === 'yearly' ? plan.price * 10 : plan.price; + const tokens = billingCycle === 'yearly' ? plan.tokens * 12 : plan.tokens; + + // 创建交易记录 + await trx('user_transactions').insert({ + user_id: userId, + type: 'subscription', + plan_id: planId, + amount: price, + tokens: tokens, + status: 'completed', // 假设支付已完成 + payment_method: 'credit_card', // 假设使用信用卡支付 + transaction_id: `sub_${Date.now()}`, // 生成一个简单的交易ID + }); + + // 这里可以添加更多逻辑,如更新用户的订阅状态等 + }); + + return json({ success: true, message: '订阅购买成功' }); + } catch (error) { + console.error('Error purchasing subscription:', error); + return json({ error: 'Failed to purchase subscription' }, { status: 500 }); + } +} diff --git a/app/routes/api.subscription-plans.ts b/app/routes/api.subscription-plans.ts new file mode 100644 index 0000000..96730e8 --- /dev/null +++ b/app/routes/api.subscription-plans.ts @@ -0,0 +1,12 @@ +import { json } from '@remix-run/cloudflare'; +import { db } from '~/lib/db.server'; + +export async function loader() { + try { + const plans = await db.select().from('subscription_plans').where('is_active', true); + return json(plans); + } catch (error) { + console.error('Error fetching subscription plans:', error); + return json({ error: 'Failed to fetch subscription plans' }, { status: 500 }); + } +} diff --git a/app/routes/api.user-subscription.ts b/app/routes/api.user-subscription.ts new file mode 100644 index 0000000..a4ccde8 --- /dev/null +++ b/app/routes/api.user-subscription.ts @@ -0,0 +1,24 @@ +import { json } from '@remix-run/cloudflare'; +import { db } from '~/lib/db.server'; +import { requireUserId } from '~/lib/session.server'; + +export async function loader({ request }) { + const userId = await requireUserId(request); + try { + const userSubscription = await db.select( + 'subscription_plans.*', + 'user_transactions.tokens as tokensLeft', + db.raw('DATE_ADD(user_transactions._create, INTERVAL 1 MONTH) as nextReloadDate') + ) + .from('user_transactions') + .join('subscription_plans', 'user_transactions.plan_id', 'subscription_plans._id') + .where('user_transactions.user_id', userId) + .orderBy('user_transactions._create', 'desc') + .first(); + + return json(userSubscription); + } catch (error) { + console.error('Error fetching user subscription:', error); + return json({ error: 'Failed to fetch user subscription' }, { status: 500 }); + } +}