feat: integrate Stripe payment with bugfixes and active timeout cancellation
- Add Stripe payment provider with Checkout Session flow - Payment provider abstraction layer (EasyPay + Stripe unified interface) - Stripe webhook with proper raw body handling and signature verification - Frontend: Stripe button with URL validation, anti-duplicate click, noopener - Active timeout cancellation: query platform before expiring, recover paid orders - Singleton Stripe client, idempotency keys, Math.round for amounts - Handle async_payment events, return null for unknown webhook events - Set Checkout Session expires_at aligned with order timeout - Add cancelPayment to provider interface (Stripe: sessions.expire, EasyPay: no-op) - Enable stripe in frontend payment type list
This commit is contained in:
@@ -154,7 +154,9 @@ function OrdersContent() {
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950 text-slate-100' : 'bg-slate-50 text-slate-900'}`}>
|
||||
<div
|
||||
className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950 text-slate-100' : 'bg-slate-50 text-slate-900'}`}
|
||||
>
|
||||
正在切换到移动端订单 Tab...
|
||||
</div>
|
||||
);
|
||||
@@ -184,7 +186,9 @@ function OrdersContent() {
|
||||
onClick={loadOrders}
|
||||
className={[
|
||||
'rounded-lg border px-3 py-2 text-xs font-medium',
|
||||
isDark ? 'border-slate-600 text-slate-200 hover:bg-slate-800' : 'border-slate-300 text-slate-700 hover:bg-slate-100',
|
||||
isDark
|
||||
? 'border-slate-600 text-slate-200 hover:bg-slate-800'
|
||||
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
|
||||
].join(' ')}
|
||||
>
|
||||
刷新
|
||||
@@ -193,7 +197,9 @@ function OrdersContent() {
|
||||
href={payUrl}
|
||||
className={[
|
||||
'rounded-lg border px-3 py-2 text-xs font-medium',
|
||||
isDark ? 'border-slate-600 text-slate-200 hover:bg-slate-800' : 'border-slate-300 text-slate-700 hover:bg-slate-100',
|
||||
isDark
|
||||
? 'border-slate-600 text-slate-200 hover:bg-slate-800'
|
||||
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
|
||||
].join(' ')}
|
||||
>
|
||||
返回充值
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useState, useEffect, Suspense, useMemo } from 'react';
|
||||
import { useState, useEffect, Suspense } from 'react';
|
||||
import PaymentForm from '@/components/PaymentForm';
|
||||
import PaymentQRCode from '@/components/PaymentQRCode';
|
||||
import OrderStatus from '@/components/OrderStatus';
|
||||
@@ -13,9 +13,10 @@ interface OrderResult {
|
||||
orderId: string;
|
||||
amount: number;
|
||||
status: string;
|
||||
paymentType: 'alipay' | 'wxpay';
|
||||
paymentType: 'alipay' | 'wxpay' | 'stripe';
|
||||
payUrl?: string | null;
|
||||
qrCode?: string | null;
|
||||
checkoutUrl?: string | null;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ function PayContent() {
|
||||
const [activeMobileTab, setActiveMobileTab] = useState<'pay' | 'orders'>('pay');
|
||||
|
||||
const [config] = useState<AppConfig>({
|
||||
enabledPaymentTypes: ['alipay', 'wxpay'],
|
||||
enabledPaymentTypes: ['alipay', 'wxpay', 'stripe'],
|
||||
minAmount: 1,
|
||||
maxAmount: 10000,
|
||||
});
|
||||
@@ -185,6 +186,7 @@ function PayContent() {
|
||||
paymentType: data.paymentType || paymentType,
|
||||
payUrl: data.payUrl,
|
||||
qrCode: data.qrCode,
|
||||
checkoutUrl: data.checkoutUrl,
|
||||
expiresAt: data.expiresAt,
|
||||
});
|
||||
|
||||
@@ -385,6 +387,7 @@ function PayContent() {
|
||||
orderId={orderResult.orderId}
|
||||
payUrl={orderResult.payUrl}
|
||||
qrCode={orderResult.qrCode}
|
||||
checkoutUrl={orderResult.checkoutUrl}
|
||||
paymentType={orderResult.paymentType}
|
||||
amount={orderResult.amount}
|
||||
expiresAt={orderResult.expiresAt}
|
||||
|
||||
@@ -5,8 +5,9 @@ import { useEffect, useState, Suspense } from 'react';
|
||||
|
||||
function ResultContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const outTradeNo = searchParams.get('out_trade_no');
|
||||
const tradeStatus = searchParams.get('trade_status');
|
||||
// Support both ZPAY (out_trade_no) and Stripe (order_id) callback params
|
||||
const outTradeNo = searchParams.get('out_trade_no') || searchParams.get('order_id');
|
||||
const tradeStatus = searchParams.get('trade_status') || searchParams.get('status');
|
||||
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -62,9 +63,7 @@ function ResultContent() {
|
||||
{status === 'COMPLETED' ? '充值成功' : '充值处理中'}
|
||||
</h1>
|
||||
<p className="mt-2 text-gray-500">
|
||||
{status === 'COMPLETED'
|
||||
? '余额已成功到账!'
|
||||
: '支付成功,余额正在充值中...'}
|
||||
{status === 'COMPLETED' ? '余额已成功到账!' : '支付成功,余额正在充值中...'}
|
||||
</p>
|
||||
</>
|
||||
) : isPending ? (
|
||||
@@ -89,9 +88,7 @@ function ResultContent() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-xs text-gray-400">
|
||||
订单号: {outTradeNo || '未知'}
|
||||
</p>
|
||||
<p className="mt-4 text-xs text-gray-400">订单号: {outTradeNo || '未知'}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -99,11 +96,13 @@ function ResultContent() {
|
||||
|
||||
export default function PayResultPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</div>
|
||||
}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ResultContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user