Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33e4a811f3 | ||
|
|
0a94cecad8 | ||
|
|
b3730b567e | ||
|
|
9af7133d93 | ||
|
|
1f2d0499ed | ||
|
|
ae3aa2e0e4 |
114
src/__tests__/app/api/user-route.test.ts
Normal file
114
src/__tests__/app/api/user-route.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
const mockGetCurrentUserByToken = vi.fn();
|
||||
const mockGetUser = vi.fn();
|
||||
const mockGetSystemConfig = vi.fn();
|
||||
const mockQueryMethodLimits = vi.fn();
|
||||
const mockGetSupportedTypes = vi.fn();
|
||||
|
||||
vi.mock('@/lib/sub2api/client', () => ({
|
||||
getCurrentUserByToken: (...args: unknown[]) => mockGetCurrentUserByToken(...args),
|
||||
getUser: (...args: unknown[]) => mockGetUser(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/config', () => ({
|
||||
getEnv: () => ({
|
||||
MIN_RECHARGE_AMOUNT: 1,
|
||||
MAX_RECHARGE_AMOUNT: 1000,
|
||||
MAX_DAILY_RECHARGE_AMOUNT: 10000,
|
||||
PAY_HELP_IMAGE_URL: undefined,
|
||||
PAY_HELP_TEXT: undefined,
|
||||
STRIPE_PUBLISHABLE_KEY: 'pk_test',
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/order/limits', () => ({
|
||||
queryMethodLimits: (...args: unknown[]) => mockQueryMethodLimits(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/payment', () => ({
|
||||
initPaymentProviders: vi.fn(),
|
||||
paymentRegistry: {
|
||||
getSupportedTypes: (...args: unknown[]) => mockGetSupportedTypes(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/pay-utils', () => ({
|
||||
getPaymentDisplayInfo: (type: string) => ({
|
||||
channel: type === 'alipay_direct' ? 'alipay' : type,
|
||||
provider: type,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/locale', () => ({
|
||||
resolveLocale: () => 'zh',
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/system-config', () => ({
|
||||
getSystemConfig: (...args: unknown[]) => mockGetSystemConfig(...args),
|
||||
}));
|
||||
|
||||
import { GET } from '@/app/api/user/route';
|
||||
|
||||
function createRequest() {
|
||||
return new NextRequest('https://pay.example.com/api/user?user_id=1&token=test-token');
|
||||
}
|
||||
|
||||
describe('GET /api/user', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetCurrentUserByToken.mockResolvedValue({ id: 1 });
|
||||
mockGetUser.mockResolvedValue({ id: 1, status: 'active' });
|
||||
mockGetSupportedTypes.mockReturnValue(['alipay', 'wxpay', 'stripe']);
|
||||
mockQueryMethodLimits.mockResolvedValue({
|
||||
alipay: { maxDailyAmount: 1000 },
|
||||
wxpay: { maxDailyAmount: 1000 },
|
||||
stripe: { maxDailyAmount: 1000 },
|
||||
});
|
||||
mockGetSystemConfig.mockImplementation(async (key: string) => {
|
||||
if (key === 'ENABLED_PAYMENT_TYPES') return undefined;
|
||||
if (key === 'BALANCE_PAYMENT_DISABLED') return 'false';
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
it('filters enabled payment types by ENABLED_PAYMENT_TYPES config', async () => {
|
||||
mockGetSystemConfig.mockImplementation(async (key: string) => {
|
||||
if (key === 'ENABLED_PAYMENT_TYPES') return 'alipay,wxpay';
|
||||
if (key === 'BALANCE_PAYMENT_DISABLED') return 'false';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const response = await GET(createRequest());
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.config.enabledPaymentTypes).toEqual(['alipay', 'wxpay']);
|
||||
expect(mockQueryMethodLimits).toHaveBeenCalledWith(['alipay', 'wxpay']);
|
||||
});
|
||||
|
||||
it('falls back to supported payment types when ENABLED_PAYMENT_TYPES is empty', async () => {
|
||||
mockGetSystemConfig.mockImplementation(async (key: string) => {
|
||||
if (key === 'ENABLED_PAYMENT_TYPES') return ' ';
|
||||
if (key === 'BALANCE_PAYMENT_DISABLED') return 'false';
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const response = await GET(createRequest());
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.config.enabledPaymentTypes).toEqual(['alipay', 'wxpay', 'stripe']);
|
||||
expect(mockQueryMethodLimits).toHaveBeenCalledWith(['alipay', 'wxpay', 'stripe']);
|
||||
});
|
||||
|
||||
it('falls back to supported payment types when ENABLED_PAYMENT_TYPES is undefined', async () => {
|
||||
const response = await GET(createRequest());
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.config.enabledPaymentTypes).toEqual(['alipay', 'wxpay', 'stripe']);
|
||||
expect(mockQueryMethodLimits).toHaveBeenCalledWith(['alipay', 'wxpay', 'stripe']);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
||||
import { createOrder } from '@/lib/order/service';
|
||||
import { getEnv } from '@/lib/config';
|
||||
import { paymentRegistry } from '@/lib/payment';
|
||||
import { getEnabledPaymentTypes } from '@/lib/payment/resolve-enabled-types';
|
||||
import { getCurrentUserByToken } from '@/lib/sub2api/client';
|
||||
import { handleApiError } from '@/lib/utils/api';
|
||||
|
||||
@@ -59,8 +60,9 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate payment type is enabled
|
||||
if (!paymentRegistry.getSupportedTypes().includes(payment_type)) {
|
||||
// Validate payment type is enabled (registry + ENABLED_PAYMENT_TYPES config)
|
||||
const enabledTypes = await getEnabledPaymentTypes();
|
||||
if (!enabledTypes.includes(payment_type)) {
|
||||
return NextResponse.json({ error: `不支持的支付方式: ${payment_type}` }, { status: 400 });
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import { initPaymentProviders, paymentRegistry } from '@/lib/payment';
|
||||
import { getPaymentDisplayInfo } from '@/lib/pay-utils';
|
||||
import { resolveLocale } from '@/lib/locale';
|
||||
import { getSystemConfig } from '@/lib/system-config';
|
||||
import { resolveEnabledPaymentTypes } from '@/lib/payment/resolve-enabled-types';
|
||||
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const locale = resolveLocale(request.nextUrl.searchParams.get('lang'));
|
||||
@@ -40,13 +42,22 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const env = getEnv();
|
||||
initPaymentProviders();
|
||||
const enabledTypes = paymentRegistry.getSupportedTypes();
|
||||
const [user, methodLimits, balanceDisabledVal] = await Promise.all([
|
||||
getUser(userId),
|
||||
queryMethodLimits(enabledTypes),
|
||||
const supportedTypes = paymentRegistry.getSupportedTypes();
|
||||
|
||||
// getUser 与 config 查询并行;config 完成后立即启动 queryMethodLimits
|
||||
const configPromise = Promise.all([
|
||||
getSystemConfig('ENABLED_PAYMENT_TYPES'),
|
||||
getSystemConfig('BALANCE_PAYMENT_DISABLED'),
|
||||
]).then(async ([configuredPaymentTypesRaw, balanceDisabledVal]) => {
|
||||
const enabledTypes = resolveEnabledPaymentTypes(supportedTypes, configuredPaymentTypesRaw);
|
||||
const methodLimits = await queryMethodLimits(enabledTypes);
|
||||
return { enabledTypes, methodLimits, balanceDisabled: balanceDisabledVal === 'true' };
|
||||
});
|
||||
|
||||
const [user, { enabledTypes, methodLimits, balanceDisabled }] = await Promise.all([
|
||||
getUser(userId),
|
||||
configPromise,
|
||||
]);
|
||||
const balanceDisabled = balanceDisabledVal === 'true';
|
||||
|
||||
// 收集 sublabel 覆盖
|
||||
const sublabelOverrides: Record<string, string> = {};
|
||||
|
||||
30
src/lib/payment/resolve-enabled-types.ts
Normal file
30
src/lib/payment/resolve-enabled-types.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { getSystemConfig } from '@/lib/system-config';
|
||||
import { initPaymentProviders, paymentRegistry } from '@/lib/payment';
|
||||
|
||||
/**
|
||||
* 根据 ENABLED_PAYMENT_TYPES 配置过滤支持的支付类型。
|
||||
* configuredTypes 为 undefined 或空字符串时回退到全部支持类型。
|
||||
*/
|
||||
export function resolveEnabledPaymentTypes(supportedTypes: string[], configuredTypes: string | undefined): string[] {
|
||||
if (configuredTypes === undefined) return supportedTypes;
|
||||
|
||||
const configuredTypeSet = new Set(
|
||||
configuredTypes
|
||||
.split(',')
|
||||
.map((type) => type.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
if (configuredTypeSet.size === 0) return supportedTypes;
|
||||
|
||||
return supportedTypes.filter((type) => configuredTypeSet.has(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前启用的支付类型(结合 registry 支持类型 + 数据库 ENABLED_PAYMENT_TYPES 配置)。
|
||||
*/
|
||||
export async function getEnabledPaymentTypes(): Promise<string[]> {
|
||||
initPaymentProviders();
|
||||
const supportedTypes = paymentRegistry.getSupportedTypes();
|
||||
const configuredTypes = await getSystemConfig('ENABLED_PAYMENT_TYPES');
|
||||
return resolveEnabledPaymentTypes(supportedTypes, configuredTypes);
|
||||
}
|
||||
Reference in New Issue
Block a user