feat: Stripe 改用 PaymentIntent + Payment Element,iframe 嵌入支付宝弹窗支付

Stripe 集成重构:
- 从 Checkout Session 改为 PaymentIntent + Payment Element 模式
- 前端内联渲染 Stripe 支付表单,支持信用卡、支付宝等多种方式
- Webhook 事件改为 payment_intent.succeeded / payment_intent.payment_failed
- provider/test 同步更新

iframe 嵌入模式 (ui_mode=embedded):
- 支付宝等需跳转的方式改为弹出新窗口处理,避免 X-Frame-Options 冲破 iframe
- 信用卡等无跳转方式仍在 iframe 内联完成
- 弹窗使用 confirmAlipayPayment 直接跳转,无需二次操作
- result 页面检测弹窗模式,支付成功后自动关闭窗口

Bug 修复:
- 修复配置加载前支付方式闪烁(初始值改为空数组 + loading)
- 修复桌面端 PaymentForm 缺少 methodLimits prop
- 修复 stripeError 隐藏表单导致无法重试
- 快捷金额增加 1000/2000 选项,过滤低于 minAmount 的选项

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
miwei
2026-03-04 10:58:07 +08:00
parent 5be0616e78
commit 964a2aa6d9
14 changed files with 749 additions and 279 deletions

View File

@@ -31,7 +31,7 @@ export interface CreateOrderResult {
userBalance: number;
payUrl?: string | null;
qrCode?: string | null;
checkoutUrl?: string | null;
clientSecret?: string | null;
expiresAt: Date;
}
@@ -170,7 +170,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
userBalance: user.balance,
payUrl: paymentResult.payUrl,
qrCode: paymentResult.qrCode,
checkoutUrl: paymentResult.checkoutUrl,
clientSecret: paymentResult.clientSecret,
expiresAt,
};
} catch (error) {
@@ -181,6 +181,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
// 支付网关配置缺失或调用失败,转成友好错误
const msg = error instanceof Error ? error.message : String(error);
console.error(`Payment gateway error (${input.paymentType}):`, error);
if (msg.includes('environment variables') || msg.includes('not configured') || msg.includes('not found')) {
throw new OrderError('PAYMENT_GATEWAY_ERROR', `支付渠道(${input.paymentType})暂未配置,请联系管理员`, 503);
}