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

30
src/lib/payment/index.ts Normal file
View File

@@ -0,0 +1,30 @@
import { paymentRegistry } from './registry';
import { EasyPayProvider } from '@/lib/easy-pay/provider';
import { StripeProvider } from '@/lib/stripe/provider';
import { getEnv } from '@/lib/config';
export { paymentRegistry } from './registry';
export type {
PaymentType,
PaymentProvider,
CreatePaymentRequest,
CreatePaymentResponse,
QueryOrderResponse,
PaymentNotification,
RefundRequest,
RefundResponse,
} from './types';
let initialized = false;
export function initPaymentProviders(): void {
if (initialized) return;
paymentRegistry.register(new EasyPayProvider());
const env = getEnv();
if (env.STRIPE_SECRET_KEY) {
paymentRegistry.register(new StripeProvider());
}
initialized = true;
}

View File

@@ -0,0 +1,29 @@
import type { PaymentProvider, PaymentType } from './types';
export class PaymentProviderRegistry {
private providers = new Map<PaymentType, PaymentProvider>();
register(provider: PaymentProvider): void {
for (const type of provider.supportedTypes) {
this.providers.set(type, provider);
}
}
getProvider(type: PaymentType): PaymentProvider {
const provider = this.providers.get(type);
if (!provider) {
throw new Error(`No payment provider registered for type: ${type}`);
}
return provider;
}
hasProvider(type: PaymentType): boolean {
return this.providers.has(type);
}
getSupportedTypes(): PaymentType[] {
return Array.from(this.providers.keys());
}
}
export const paymentRegistry = new PaymentProviderRegistry();

66
src/lib/payment/types.ts Normal file
View File

@@ -0,0 +1,66 @@
/** Unified payment method types across all providers */
export type PaymentType = 'alipay' | 'wxpay' | 'stripe';
/** Request to create a payment with any provider */
export interface CreatePaymentRequest {
orderId: string;
amount: number; // in CNY (yuan)
paymentType: PaymentType;
subject: string; // product description
notifyUrl?: string;
returnUrl?: string;
clientIp?: string;
}
/** Response from creating a payment */
export interface CreatePaymentResponse {
tradeNo: string; // third-party transaction ID
payUrl?: string; // H5 payment URL (alipay/wxpay)
qrCode?: string; // QR code content
checkoutUrl?: string; // Stripe Checkout URL
}
/** Response from querying an order's payment status */
export interface QueryOrderResponse {
tradeNo: string;
status: 'pending' | 'paid' | 'failed' | 'refunded';
amount: number;
paidAt?: Date;
}
/** Parsed payment notification from webhook/notify callback */
export interface PaymentNotification {
tradeNo: string;
orderId: string;
amount: number;
status: 'success' | 'failed';
rawData: unknown;
}
/** Request to refund a payment */
export interface RefundRequest {
tradeNo: string;
orderId: string;
amount: number;
reason?: string;
}
/** Response from a refund request */
export interface RefundResponse {
refundId: string;
status: 'success' | 'pending' | 'failed';
}
/** Common interface that all payment providers must implement */
export interface PaymentProvider {
readonly name: string;
readonly supportedTypes: PaymentType[];
createPayment(request: CreatePaymentRequest): Promise<CreatePaymentResponse>;
queryOrder(tradeNo: string): Promise<QueryOrderResponse>;
/** Returns null for unrecognized/irrelevant webhook events (caller should return 200). */
verifyNotification(rawBody: string | Buffer, headers: Record<string, string>): Promise<PaymentNotification | null>;
refund(request: RefundRequest): Promise<RefundResponse>;
/** Cancel/expire a pending payment on the platform. Optional — not all providers support it. */
cancelPayment?(tradeNo: string): Promise<void>;
}