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()}后添加。
+
+
+
+
需要更多代币?
+
+
+ 升级您的计划或购买
+ 代币充值包
+
+
-
+ )}
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 });
+ }
+}