feat: 更新支付modal和订阅对话框的逻辑和接口

This commit is contained in:
zyh 2024-10-22 10:30:17 +00:00
parent 68cccbb822
commit cd3a79e954
5 changed files with 61 additions and 20 deletions

View File

@ -24,28 +24,42 @@ interface PaymentResponse {
return_url: string; return_url: string;
} }
interface PaymentStatusResponse {
status: string;
error?: string;
}
export function PaymentModal({ isOpen, onClose, paymentData, onPaymentSuccess }: PaymentModalProps) { export function PaymentModal({ isOpen, onClose, paymentData, onPaymentSuccess }: PaymentModalProps) {
const [timeLeft, setTimeLeft] = useState(parseInt(paymentData.expires_in)); const [timeLeft, setTimeLeft] = useState(parseInt(paymentData.expires_in));
const checkPaymentStatus = useCallback(async () => { const checkPaymentStatus = useCallback(async () => {
try { try {
const response = await fetch(`/api/check-payment-status?orderNo=${paymentData.no}`); const response = await fetch(`/api/check-payment-status?orderNo=${paymentData.no}`);
const data = await response.json(); if (!response.ok) {
if (response.status === 404) {
console.error('Order not found');
return;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json() as PaymentStatusResponse;
if (data.status === 'completed') { if (data.status === 'completed') {
clearInterval(timer);
onPaymentSuccess(); onPaymentSuccess();
onClose(); onClose();
toast.success('支付成功!'); toast.success('支付成功!');
} }
} catch (error) { } catch (error) {
console.error('Error checking payment status:', error); console.error('Error checking payment status:', error);
toast.error('检查支付状态时出错,请稍后再试');
} }
}, [paymentData.no, onPaymentSuccess, onClose]); }, [paymentData.no, onPaymentSuccess, onClose]);
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
const timer = setInterval(() => { let timer: NodeJS.Timeout;
const checkAndUpdateStatus = () => {
setTimeLeft((prevTime) => { setTimeLeft((prevTime) => {
if (prevTime <= 1) { if (prevTime <= 1) {
clearInterval(timer); clearInterval(timer);
@ -57,7 +71,9 @@ export function PaymentModal({ isOpen, onClose, paymentData, onPaymentSuccess }:
}); });
checkPaymentStatus(); checkPaymentStatus();
}, 3000); // 每3秒检查一次支付状态 };
timer = setInterval(checkAndUpdateStatus, 3000); // 每3秒检查一次支付状态
return () => clearInterval(timer); return () => clearInterval(timer);
}, [isOpen, onClose, checkPaymentStatus]); }, [isOpen, onClose, checkPaymentStatus]);

View File

@ -15,10 +15,19 @@ interface SubscriptionDialogProps {
} }
interface UserSubscription { interface UserSubscription {
plan: SubscriptionPlan; tokenBalance: number;
tokensLeft: number; subscription: {
nextReloadDate: string; plan: {
} _id: string;
name: string;
tokens: number;
price: number;
description: string;
save_percentage?: number;
};
expirationDate: string;
} | null;
}
interface PaymentResponse { interface PaymentResponse {
status: string; status: string;
@ -164,14 +173,14 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
</p> </p>
</div> </div>
{isAuthenticated && userSubscription && ( {isAuthenticated && userSubscription && userSubscription.subscription && (
<div className="bg-bolt-elements-background-depth-2 p-4 rounded-lg"> <div className="bg-bolt-elements-background-depth-2 p-4 rounded-lg">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<span className="text-bolt-elements-textPrimary font-bold">{toString(userSubscription.tokensLeft)}</span> <span className="text-bolt-elements-textPrimary font-bold">{toString(userSubscription.tokenBalance)}</span>
<span className="text-bolt-elements-textSecondary"> </span> <span className="text-bolt-elements-textSecondary"> </span>
<span className="text-bolt-elements-textSecondary"> <span className="text-bolt-elements-textSecondary">
{userSubscription.plan.tokens.toLocaleString()}{new Date(userSubscription.nextReloadDate).toLocaleDateString()} {userSubscription.subscription.plan.tokens.toLocaleString()}{new Date(userSubscription.subscription.expirationDate).toLocaleDateString()}
</span> </span>
</div> </div>
<div className="text-right"> <div className="text-right">
@ -220,7 +229,7 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{subscriptionPlans.map((plan) => ( {subscriptionPlans.map((plan) => (
<div key={plan._id} className={`bg-bolt-elements-background-depth-2 p-4 rounded-lg ${isAuthenticated && plan._id === userSubscription?.plan._id ? 'border-2 border-bolt-elements-item-contentAccent' : ''}`}> <div key={plan._id} className={`bg-bolt-elements-background-depth-2 p-4 rounded-lg ${isAuthenticated && userSubscription && userSubscription.subscription && plan._id === userSubscription.subscription.plan._id ? 'border-2 border-bolt-elements-item-contentAccent' : ''}`}>
<h3 className="text-bolt-elements-textPrimary font-bold text-lg">{plan.name}</h3> <h3 className="text-bolt-elements-textPrimary font-bold text-lg">{plan.name}</h3>
<div className="text-bolt-elements-textSecondary mb-2"> <div className="text-bolt-elements-textSecondary mb-2">
{(plan.tokens / 1000000).toFixed(0)}M {(plan.tokens / 1000000).toFixed(0)}M
@ -235,12 +244,12 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
<button <button
onClick={() => handlePurchase(plan._id)} onClick={() => handlePurchase(plan._id)}
className={`w-full py-2 rounded-md ${ className={`w-full py-2 rounded-md ${
isAuthenticated && plan._id === userSubscription?.plan._id isAuthenticated && userSubscription && userSubscription.subscription && plan._id === userSubscription.subscription.plan._id
? 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text' ? 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
: 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text' : 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
}`} }`}
> >
{isAuthenticated && plan._id === userSubscription?.plan._id ? '管理当前计划' : `升级到${plan.name}`} {isAuthenticated && userSubscription && userSubscription.subscription && plan._id === userSubscription.subscription.plan._id ? '管理当前计划' : `升级到${plan.name}`}
</button> </button>
</div> </div>
))} ))}

View File

@ -1,26 +1,35 @@
import { json } from '@remix-run/cloudflare'; import { json } from '@remix-run/cloudflare';
import { db } from '~/utils/db.server'; import { db } from '~/utils/db.server';
import { requireAuth } from '~/middleware/auth.server';
export async function loader({ request }: { request: Request }) { export async function loader({ request }: { request: Request }) {
let userId;
try {
userId = await requireAuth(request);
} catch (error) {
return error as Response;
}
const url = new URL(request.url); const url = new URL(request.url);
const orderNo = url.searchParams.get('orderNo'); const orderNo = url.searchParams.get('orderNo');
if (!orderNo) { if (!orderNo) {
return json({ error: 'Order number is required' }, { status: 400 }); return json({ error: '订单号不能为空' }, { status: 400 });
} }
try { try {
const transaction = await db('user_transactions') const transaction = await db('user_transactions')
.where('transaction_id', orderNo) .where('transaction_id', orderNo)
.where('user_id', userId)
.first(); .first();
if (!transaction) { if (!transaction) {
return json({ error: 'Transaction not found' }, { status: 404 }); return json({ error: '订单不存在' }, { status: 404 });
} }
return json({ status: transaction.status }); return json({ status: transaction.status });
} catch (error) { } catch (error) {
console.error('Error checking payment status:', error); console.error('Error checking payment status:', error);
return json({ error: 'Failed to check payment status' }, { status: 500 }); return json({ error: '检查支付状态失败' }, { status: 500 });
} }
} }

View File

@ -35,7 +35,7 @@ export async function action({ request }: { request: Request }) {
orderNo, orderNo,
`${plan.name} 订阅 (${billingCycle === 'yearly' ? '年付' : '月付'})`, `${plan.name} 订阅 (${billingCycle === 'yearly' ? '年付' : '月付'})`,
'alipay', // 或其他支付方式 'alipay', // 或其他支付方式
price * 100, // 转换为分 price, // 不用转换为分
userId.toString() userId.toString()
); );

View File

@ -30,9 +30,16 @@ export async function loader({ request }: { request: Request }) {
return json({ return json({
tokenBalance: user.token_balance, tokenBalance: user.token_balance,
subscription: subscription subscription: subscription && subscriptionPlan
? { ? {
planName: subscriptionPlan.name, plan: {
_id: subscriptionPlan._id,
name: subscriptionPlan.name,
tokens: subscriptionPlan.tokens,
price: subscriptionPlan.price,
description: subscriptionPlan.description,
save_percentage: subscriptionPlan.save_percentage,
},
expirationDate: subscription.expiration_date, expirationDate: subscription.expiration_date,
} }
: null, : null,