feat: StripeProvider 支持从实例配置读取密钥,无需环境变量

- StripeProvider 支持 instanceConfig(secretKey/publishableKey/webhookSecret)
- ensureDBProviders 在环境变量缺失时从活跃实例配置注册 provider
- createPaymentOrder 对 stripe 也注入实例配置
- user route 的 stripePublishableKey 从 provider 实例读取
This commit is contained in:
erio
2026-04-04 20:17:16 +08:00
parent 00634fd115
commit b14f52dd4a
4 changed files with 79 additions and 9 deletions
+10 -2
View File
@@ -143,8 +143,16 @@ export async function GET(request: NextRequest) {
methodLimits,
helpImageUrl: env.PAY_HELP_IMAGE_URL ?? null,
helpText: env.PAY_HELP_TEXT ?? null,
stripePublishableKey:
enabledTypes.includes('stripe') && env.STRIPE_PUBLISHABLE_KEY ? env.STRIPE_PUBLISHABLE_KEY : null,
stripePublishableKey: (() => {
if (!enabledTypes.includes('stripe')) return null;
// 优先从注册的 StripeProvider 实例读取
try {
const sp = paymentRegistry.getProvider('stripe' as import('@/lib/payment').PaymentType);
const pk = 'getPublishableKey' in sp ? (sp as { getPublishableKey(): string | undefined }).getPublishableKey() : undefined;
if (pk) return pk;
} catch { /* not registered */ }
return env.STRIPE_PUBLISHABLE_KEY || null;
})(),
balanceDisabled,
maxPendingOrders,
sublabelOverrides: Object.keys(sublabelOverrides).length > 0 ? sublabelOverrides : null,
+3
View File
@@ -381,6 +381,9 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
if (provider.providerKey === 'easypay') {
const { EasyPayProvider } = await import('@/lib/easy-pay/provider');
actualProvider = new EasyPayProvider(instanceResult.instanceId, instanceResult.config);
} else if (provider.providerKey === 'stripe') {
const { StripeProvider } = await import('@/lib/stripe/provider');
actualProvider = new StripeProvider(instanceResult.instanceId, instanceResult.config);
}
selectedInstanceId = instanceResult.instanceId;
} else {
+41
View File
@@ -6,6 +6,8 @@ import { AlipayProvider } from '@/lib/alipay/provider';
import { WxpayProvider } from '@/lib/wxpay/provider';
import { getEnv } from '@/lib/config';
import { getSystemConfig } from '@/lib/system-config';
import { prisma } from '@/lib/db';
import { decrypt } from '@/lib/crypto';
export { paymentRegistry } from './registry';
export type {
@@ -90,6 +92,7 @@ export function initPaymentProviders(): void {
/**
* 异步初始化:当数据库覆盖模式开启时,根据 ENABLED_PROVIDERS 补注册 provider。
* 对于有活跃实例且实例配置中包含密钥的 provider,即使没有环境变量也能注册。
* 在所有使用 paymentRegistry 的异步入口调用。
*/
export async function ensureDBProviders(): Promise<void> {
@@ -107,7 +110,45 @@ export async function ensureDBProviders(): Promise<void> {
.filter(Boolean);
const env = getEnv();
// 先用环境变量注册能注册的
registerFromList(dbProviders, env, false);
// 对于环境变量缺失但有活跃实例的 provider,从实例配置注册
for (const key of dbProviders) {
if (registeredKeys.has(key)) continue;
const instance = await prisma.paymentProviderInstance.findFirst({
where: { providerKey: key, enabled: true },
select: { id: true, config: true },
});
if (!instance) continue;
let config: Record<string, string>;
try {
config = JSON.parse(decrypt(instance.config));
} catch {
console.warn(`[payment] Failed to decrypt config for ${key} instance ${instance.id}, skipping`);
continue;
}
switch (key) {
case 'stripe':
if (config.secretKey) {
paymentRegistry.register(new StripeProvider(instance.id, config));
registeredKeys.add(key);
} else {
console.warn(`[payment] stripe instance ${instance.id} has no secretKey, skipping`);
}
break;
case 'easypay':
if (config.pid && config.pkey) {
paymentRegistry.register(new EasyPayProvider(instance.id, config));
registeredKeys.add(key);
}
break;
}
}
}
// 注入 lazy init:Registry 方法会自动调用 initPaymentProviders()(同步回退)
+25 -7
View File
@@ -13,23 +13,41 @@ import type {
} from '@/lib/payment/types';
export class StripeProvider implements PaymentProvider {
readonly name = 'stripe';
readonly name: string;
readonly providerKey = 'stripe';
readonly supportedTypes: PaymentType[] = ['stripe'];
readonly defaultLimits = {
stripe: { singleMax: 0, dailyMax: 0 }, // 0 = unlimited
};
readonly instanceId?: string;
private instanceConfig?: Record<string, string>;
private client: Stripe | null = null;
constructor(instanceId?: string, instanceConfig?: Record<string, string>) {
this.instanceId = instanceId;
this.instanceConfig = instanceConfig;
this.name = instanceId ? `stripe:${instanceId}` : 'stripe';
}
private getClient(): Stripe {
if (this.client) return this.client;
const env = getEnv();
if (!env.STRIPE_SECRET_KEY) throw new Error('STRIPE_SECRET_KEY not configured');
this.client = new Stripe(env.STRIPE_SECRET_KEY);
const secretKey = this.instanceConfig?.secretKey || getEnv().STRIPE_SECRET_KEY;
if (!secretKey) throw new Error('STRIPE_SECRET_KEY not configured');
this.client = new Stripe(secretKey);
return this.client;
}
/** 获取 publishable key(实例配置优先,回退到环境变量) */
getPublishableKey(): string | undefined {
return this.instanceConfig?.publishableKey || getEnv().STRIPE_PUBLISHABLE_KEY || undefined;
}
/** 获取 webhook secret(实例配置优先,回退到环境变量) */
private getWebhookSecret(): string | undefined {
return this.instanceConfig?.webhookSecret || getEnv().STRIPE_WEBHOOK_SECRET || undefined;
}
async createPayment(request: CreatePaymentRequest): Promise<CreatePaymentResponse> {
const stripe = this.getClient();
@@ -72,14 +90,14 @@ export class StripeProvider implements PaymentProvider {
headers: Record<string, string>,
): Promise<PaymentNotification | null> {
const stripe = this.getClient();
const env = getEnv();
if (!env.STRIPE_WEBHOOK_SECRET) throw new Error('STRIPE_WEBHOOK_SECRET not configured');
const webhookSecret = this.getWebhookSecret();
if (!webhookSecret) throw new Error('STRIPE_WEBHOOK_SECRET not configured');
const sig = headers['stripe-signature'] || '';
const event = stripe.webhooks.constructEvent(
typeof rawBody === 'string' ? Buffer.from(rawBody) : rawBody,
sig,
env.STRIPE_WEBHOOK_SECRET,
webhookSecret,
);
if (event.type === 'payment_intent.succeeded') {