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:
erio
2026-03-01 17:58:08 +08:00
parent 2f45044073
commit d9ab65ecf2
59 changed files with 1571 additions and 432 deletions

View File

@@ -2,7 +2,13 @@
import { useMemo, useState } from 'react';
import OrderFilterBar from '@/components/OrderFilterBar';
import { formatStatus, formatCreatedAt, getStatusBadgeClass, type MyOrder, type OrderStatusFilter } from '@/lib/pay-utils';
import {
formatStatus,
formatCreatedAt,
getStatusBadgeClass,
type MyOrder,
type OrderStatusFilter,
} from '@/lib/pay-utils';
interface MobileOrderListProps {
isDark: boolean;
@@ -22,13 +28,17 @@ export default function MobileOrderList({ isDark, hasToken, orders, onRefresh }:
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className={['text-base font-semibold', isDark ? 'text-slate-100' : 'text-slate-900'].join(' ')}></h3>
<h3 className={['text-base font-semibold', isDark ? 'text-slate-100' : 'text-slate-900'].join(' ')}>
</h3>
<button
type="button"
onClick={onRefresh}
className={[
'rounded-lg border px-2.5 py-1 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(' ')}
>
@@ -38,11 +48,21 @@ export default function MobileOrderList({ isDark, hasToken, orders, onRefresh }:
<OrderFilterBar isDark={isDark} activeFilter={activeFilter} onChange={setActiveFilter} />
{!hasToken ? (
<div className={['rounded-xl border border-dashed px-4 py-8 text-center text-sm', isDark ? 'border-amber-500/40 text-amber-200' : 'border-amber-300 text-amber-700'].join(' ')}>
token"我的订单"
<div
className={[
'rounded-xl border border-dashed px-4 py-8 text-center text-sm',
isDark ? 'border-amber-500/40 text-amber-200' : 'border-amber-300 text-amber-700',
].join(' ')}
>
token&ldquo;&rdquo;
</div>
) : filteredOrders.length === 0 ? (
<div className={['rounded-xl border border-dashed px-4 py-8 text-center text-sm', isDark ? 'border-slate-600 text-slate-400' : 'border-slate-300 text-slate-500'].join(' ')}>
<div
className={[
'rounded-xl border border-dashed px-4 py-8 text-center text-sm',
isDark ? 'border-slate-600 text-slate-400' : 'border-slate-300 text-slate-500',
].join(' ')}
>
</div>
) : (
@@ -50,11 +70,16 @@ export default function MobileOrderList({ isDark, hasToken, orders, onRefresh }:
{filteredOrders.map((order) => (
<div
key={order.id}
className={['rounded-xl border px-3 py-3', isDark ? 'border-slate-700 bg-slate-900/70' : 'border-slate-200 bg-white'].join(' ')}
className={[
'rounded-xl border px-3 py-3',
isDark ? 'border-slate-700 bg-slate-900/70' : 'border-slate-200 bg-white',
].join(' ')}
>
<div className="flex items-center justify-between">
<span className="text-2xl font-semibold">¥{order.amount.toFixed(2)}</span>
<span className={['rounded-full px-2 py-0.5 text-xs', getStatusBadgeClass(order.status, isDark)].join(' ')}>
<span
className={['rounded-full px-2 py-0.5 text-xs', getStatusBadgeClass(order.status, isDark)].join(' ')}
>
{formatStatus(order.status)}
</span>
</div>