feat(auth): 添加代币充值功能

This commit is contained in:
zyh 2024-10-22 09:23:57 +00:00
parent c40589c843
commit f070ec20fb
3 changed files with 205 additions and 2 deletions

View File

@ -7,6 +7,7 @@ import { LoginRegisterDialog } from './LoginRegisterDialog';
import type { SubscriptionPlan } from '~/types/subscription';
import pkg from 'lodash';
const {toString} = pkg;
import { TokenReloadModal } from './TokenReloadModal'; // 新增导入
interface SubscriptionDialogProps {
isOpen: boolean;
@ -48,6 +49,7 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
const { user, token, isAuthenticated, login } = useAuth();
const [paymentData, setPaymentData] = useState<PaymentResponse | null>(null);
const [isLoginRegisterOpen, setIsLoginRegisterOpen] = useState(false);
const [isTokenReloadModalOpen, setIsTokenReloadModalOpen] = useState(false);
useEffect(() => {
if (isOpen) {
@ -138,6 +140,15 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
toast.success('登录成功!');
}, [fetchUserSubscription]);
const handleTokenReloadClick = () => {
setIsTokenReloadModalOpen(true);
};
const handleTokenReloadSuccess = useCallback(() => {
fetchUserSubscription(); // 重新获取用户订阅信息
toast.success('代币充值成功!');
}, [fetchUserSubscription]);
if (isLoading) return null;
return (
@ -167,8 +178,17 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
<span className="text-bolt-elements-textSecondary"></span>
<br />
<span className="text-bolt-elements-textSecondary">
<a href="#" className="text-bolt-elements-item-contentAccent hover:underline"></a>
<a
href="#"
className="text-bolt-elements-item-contentAccent hover:underline"
onClick={(e) => {
e.preventDefault();
handleTokenReloadClick();
}}
>
</a>
</span>
</div>
</div>
@ -242,6 +262,11 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
onClose={() => setIsLoginRegisterOpen(false)}
onLoginSuccess={handleLoginSuccess}
/>
<TokenReloadModal
isOpen={isTokenReloadModalOpen}
onClose={() => setIsTokenReloadModalOpen(false)}
onReloadSuccess={handleTokenReloadSuccess}
/>
</>
);
}

View File

@ -0,0 +1,109 @@
import React, { useState, useEffect } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogRoot } from '~/components/ui/Dialog';
import { useAuth } from '~/hooks/useAuth';
import { toast } from 'react-toastify';
import type { PurchaseResponse } from '~/routes/api.purchase-token-reload';
interface TokenReloadModalProps {
isOpen: boolean;
onClose: () => void;
onReloadSuccess: () => void;
}
interface TokenReloadPack {
_id: string;
name: string;
tokens: number;
price: number;
description: string;
}
export function TokenReloadModal({ isOpen, onClose, onReloadSuccess }: TokenReloadModalProps) {
const [tokenReloadPacks, setTokenReloadPacks] = useState<TokenReloadPack[]>([]);
const [isLoading, setIsLoading] = useState(true);
const { token } = useAuth();
useEffect(() => {
if (isOpen) {
fetchTokenReloadPacks();
}
}, [isOpen]);
const fetchTokenReloadPacks = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/token-reload-packs');
if (!response.ok) {
throw new Error('获取代币充值包失败');
}
const data = await response.json() as TokenReloadPack[];
setTokenReloadPacks(data);
} catch (error) {
console.error('获取代币充值包时出错:', error);
toast.error('获取代币充值包失败,请稍后再试');
} finally {
setIsLoading(false);
}
};
const handlePurchase = async (packId: string) => {
if (!token) {
toast.error('登录状态异常,请重新登录');
return;
}
try {
const response = await fetch('/api/purchase-token-reload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ packId }),
});
const result = await response.json() as PurchaseResponse;
if (response.ok && result.success) {
onReloadSuccess();
onClose();
} else {
toast.error(result.error || '购买代币充值包失败,请稍后重试');
}
} catch (error) {
console.error('Error purchasing token reload pack:', error);
toast.error('购买代币充值包过程中出现错误,请稍后重试');
}
};
if (isLoading) return null;
return (
<DialogRoot open={isOpen}>
<Dialog onBackdrop={onClose} onClose={onClose} className="w-full max-w-2xl">
<DialogTitle></DialogTitle>
<DialogDescription>
<div className="space-y-4">
{tokenReloadPacks.map((pack) => (
<div key={pack._id} className="bg-bolt-elements-background-depth-2 p-4 rounded-lg">
<h3 className="text-bolt-elements-textPrimary font-bold text-lg">{pack.name}</h3>
<div className="text-bolt-elements-textSecondary mb-2">
{(pack.tokens / 1000000).toFixed(0)}M
</div>
<p className="text-bolt-elements-textTertiary text-sm mb-4">{pack.description}</p>
<div className="text-bolt-elements-textPrimary font-bold text-2xl mb-2">
¥{pack.price.toFixed(2)}
</div>
<button
onClick={() => handlePurchase(pack._id)}
className="w-full py-2 rounded-md bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text"
>
</button>
</div>
))}
</div>
</DialogDescription>
</Dialog>
</DialogRoot>
);
}

View File

@ -0,0 +1,69 @@
import { json } from '@remix-run/cloudflare';
import { db } from '~/utils/db.server';
import SDPay from '~/utils/SDPay.server';
import { requireAuth } from '~/middleware/auth.server';
export interface PurchaseResponse {
success: boolean;
paymentData?: {
// 根据实际返回的数据结构定义
no: string;
pay_type: string;
order_amount: string;
qr_img: string;
expires_in: string;
// ... 其他可能的字段
};
error?: string;
}
export async function action({ request }: { request: Request }) {
let userId;
try {
userId = await requireAuth(request);
} catch (error) {
return error as Response;
}
const { packId } = await request.json() as { packId: string };
try {
// 获取代币充值包详情
const pack = await db('token_reloads').where('_id', packId).first();
if (!pack) {
return json({ error: '无效的代币充值包' }, { status: 400 });
}
// 创建 SDPay 实例
const sdpay = new SDPay();
// 生成订单号
const orderNo = `reload_${Date.now()}_${userId}`;
// 获取支付数据
const paymentData = await sdpay.createPayment(
orderNo,
`${pack.name} 代币充值`,
'alipay', // 或其他支付方式
pack.price * 100, // 转换为分
userId.toString()
);
// 创建待处理的交易记录
await db('user_transactions').insert({
user_id: userId,
type: 'token_reload',
token_reload_id: packId,
amount: pack.price,
tokens: pack.tokens,
status: 'pending',
payment_method: 'alipay', // 或其他支付方式
transaction_id: orderNo,
});
return json({ success: true, paymentData });
} catch (error) {
console.error('初始化代币充值购买时出错:', error);
return json({ error: '初始化代币充值购买失败' }, { status: 500 });
}
}