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:
@@ -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,无法查询“我的订单”。
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user