feat: 订阅管理增强、商品名称配置、余额充值开关

- R1: 用户订阅搜索改为模糊关键词(邮箱/用户名/备注/APIKey)
- R2: "分组状态"列名改为"Sub2API 状态"
- R3: 订阅套餐可配置支付商品名称(productName)
- R4: 订阅订单校验 subscription_type 必须为 subscription
- R5: 渠道管理配置余额充值商品名前缀/后缀
- R6: 渠道管理可关闭余额充值,前端隐藏入口,API 拒绝
- R7: 所有入口关闭时显示"入口被管理员关闭"提示
- fix: easy-pay client 测试 mock 方式修复(vi.fn + 参数快照)
This commit is contained in:
erio
2026-03-14 00:43:00 +08:00
parent 1bb11ee32b
commit 6c61c3f877
16 changed files with 873 additions and 32 deletions

View File

@@ -48,6 +48,9 @@ export async function PUT(request: NextRequest) {
'DAILY_RECHARGE_LIMIT',
'ORDER_TIMEOUT_MINUTES',
'IFRAME_ALLOW_ORIGINS',
'PRODUCT_NAME_PREFIX',
'PRODUCT_NAME_SUFFIX',
'BALANCE_PAYMENT_DISABLED',
]);
// 校验每条配置

View File

@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from 'next/server';
import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth';
import { searchUsers } from '@/lib/sub2api/client';
export async function GET(request: NextRequest) {
if (!(await verifyAdminToken(request))) return unauthorizedResponse(request);
const keyword = request.nextUrl.searchParams.get('keyword')?.trim();
if (!keyword) {
return NextResponse.json({ users: [] });
}
try {
const users = await searchUsers(keyword);
return NextResponse.json({ users });
} catch (error) {
console.error('Failed to search users:', error instanceof Error ? error.message : String(error));
return NextResponse.json({ error: '搜索用户失败' }, { status: 500 });
}
}

View File

@@ -48,6 +48,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
data.validityUnit = body.validity_unit;
}
if (body.features !== undefined) data.features = body.features ? JSON.stringify(body.features) : null;
if (body.product_name !== undefined) data.productName = body.product_name?.trim() || null;
if (body.for_sale !== undefined) data.forSale = body.for_sale;
if (body.sort_order !== undefined) data.sortOrder = body.sort_order;
@@ -69,6 +70,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
features: plan.features ? JSON.parse(plan.features) : [],
sortOrder: plan.sortOrder,
enabled: plan.forSale,
productName: plan.productName ?? null,
createdAt: plan.createdAt,
updatedAt: plan.updatedAt,
});

View File

@@ -44,6 +44,7 @@ export async function GET(request: NextRequest) {
groupWeeklyLimit: group?.weekly_limit_usd ?? null,
groupMonthlyLimit: group?.monthly_limit_usd ?? null,
groupModelScopes: group?.supported_model_scopes ?? null,
productName: plan.productName ?? null,
createdAt: plan.createdAt,
updatedAt: plan.updatedAt,
};
@@ -62,7 +63,7 @@ export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { group_id, name, description, price, original_price, validity_days, validity_unit, features, for_sale, sort_order } = body;
const { group_id, name, description, price, original_price, validity_days, validity_unit, features, for_sale, sort_order, product_name } = body;
if (!group_id || !name || price === undefined) {
return NextResponse.json({ error: '缺少必填字段: group_id, name, price' }, { status: 400 });
@@ -103,6 +104,7 @@ export async function POST(request: NextRequest) {
validityDays: validity_days ?? 30,
validityUnit: ['day', 'week', 'month'].includes(validity_unit) ? validity_unit : 'day',
features: features ? JSON.stringify(features) : null,
productName: product_name?.trim() || null,
forSale: for_sale ?? false,
sortOrder: sort_order ?? 0,
},
@@ -122,6 +124,7 @@ export async function POST(request: NextRequest) {
features: plan.features ? JSON.parse(plan.features) : [],
sortOrder: plan.sortOrder,
enabled: plan.forSale,
productName: plan.productName ?? null,
createdAt: plan.createdAt,
updatedAt: plan.updatedAt,
},