mirror of
https://github.com/touwaeriol/sub2apipay.git
synced 2026-10-29 18:14:40 +08:00
feat: StripeProvider 支持从实例配置读取密钥,无需环境变量
- StripeProvider 支持 instanceConfig(secretKey/publishableKey/webhookSecret) - ensureDBProviders 在环境变量缺失时从活跃实例配置注册 provider - createPaymentOrder 对 stripe 也注入实例配置 - user route 的 stripePublishableKey 从 provider 实例读取
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()(同步回退)
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user