feat: 套餐有效期支持日/周/月单位,订阅履约改用兑换码流程,UI层次感优化

- Prisma: SubscriptionPlan 新增 validityUnit 字段 (day/week/month)
- 新增 subscription-utils.ts 计算实际天数及格式化显示
- Sub2API client createAndRedeem 支持 subscription 类型 (group_id, validity_days)
- 订阅履约从 assignSubscription 改为 createAndRedeem,在 Sub2API 留痕
- 订单创建动态计算天数(月单位按自然月差值)
- 管理后台表单支持有效期数值+单位下拉
- 前端 ChannelCard 渠道卡片视觉层次优化(模型标签渐变、倍率突出、闪电图标)
- 按量付费 banner 改为渐变背景+底部倍率说明标签
- 帮助/客服信息区块添加到充值、订阅、支付全流程页面
- 移除系统配置独立页面入口,subscriptions API 返回用户信息
This commit is contained in:
erio
2026-03-13 21:19:22 +08:00
parent 9096271307
commit 687336cfd8
16 changed files with 672 additions and 1027 deletions

View File

@@ -34,6 +34,9 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
if (body.price !== undefined) data.price = body.price;
if (body.original_price !== undefined) data.originalPrice = body.original_price;
if (body.validity_days !== undefined) data.validityDays = body.validity_days;
if (body.validity_unit !== undefined && ['day', 'week', 'month'].includes(body.validity_unit)) {
data.validityUnit = body.validity_unit;
}
if (body.features !== undefined) data.features = body.features;
if (body.for_sale !== undefined) data.forSale = body.for_sale;
if (body.sort_order !== undefined) data.sortOrder = body.sort_order;

View File

@@ -42,7 +42,7 @@ export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { group_id, name, description, price, original_price, validity_days, features, for_sale, sort_order } = body;
const { group_id, name, description, price, original_price, validity_days, validity_unit, features, for_sale, sort_order } = body;
if (!group_id || !name || price === undefined) {
return NextResponse.json({ error: '缺少必填字段: group_id, name, price' }, { status: 400 });
@@ -68,6 +68,7 @@ export async function POST(request: NextRequest) {
price,
originalPrice: original_price ?? null,
validityDays: validity_days ?? 30,
validityUnit: ['day', 'week', 'month'].includes(validity_unit) ? validity_unit : 'day',
features: features ?? null,
forSale: for_sale ?? false,
sortOrder: sort_order ?? 0,

View File

@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { verifyAdminToken, unauthorizedResponse } from '@/lib/admin-auth';
import { getUserSubscriptions } from '@/lib/sub2api/client';
import { getUserSubscriptions, getUser } from '@/lib/sub2api/client';
export async function GET(request: NextRequest) {
if (!(await verifyAdminToken(request))) return unauthorizedResponse(request);
@@ -18,7 +18,10 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: '无效的 user_id' }, { status: 400 });
}
const subscriptions = await getUserSubscriptions(parsedUserId);
const [subscriptions, user] = await Promise.all([
getUserSubscriptions(parsedUserId),
getUser(parsedUserId).catch(() => null),
]);
// 如果提供了 group_id 筛选,过滤结果
const groupId = searchParams.get('group_id');
@@ -26,7 +29,10 @@ export async function GET(request: NextRequest) {
? subscriptions.filter((s) => s.group_id === Number(groupId))
: subscriptions;
return NextResponse.json({ subscriptions: filtered });
return NextResponse.json({
subscriptions: filtered,
user: user ? { id: user.id, username: user.username, email: user.email } : null,
});
} catch (error) {
console.error('Failed to query subscriptions:', error);
return NextResponse.json({ error: '查询订阅信息失败' }, { status: 500 });