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

@@ -6,7 +6,8 @@ import { getMethodDailyLimit } from './limits';
import { getMethodFeeRate, calculatePayAmount } from './fee';
import { initPaymentProviders, paymentRegistry } from '@/lib/payment';
import type { PaymentType, PaymentNotification } from '@/lib/payment';
import { getUser, createAndRedeem, subtractBalance, addBalance, getGroup, assignSubscription } from '@/lib/sub2api/client';
import { getUser, createAndRedeem, subtractBalance, addBalance, getGroup } from '@/lib/sub2api/client';
import { computeValidityDays, type ValidityUnit } from '@/lib/subscription-utils';
import { Prisma } from '@prisma/client';
import { deriveOrderState, isRefundStatus } from './status';
import { pickLocaleText, type Locale } from '@/lib/locale';
@@ -56,7 +57,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
const orderType = input.orderType ?? 'balance';
// ── 订阅订单前置校验 ──
let subscriptionPlan: { id: string; groupId: number; price: Prisma.Decimal; validityDays: number; name: string } | null = null;
let subscriptionPlan: { id: string; groupId: number; price: Prisma.Decimal; validityDays: number; validityUnit: string; name: string } | null = null;
if (orderType === 'subscription') {
if (!input.planId) {
throw new OrderError('INVALID_INPUT', message(locale, '订阅订单必须指定套餐', 'Subscription order requires a plan'), 400);
@@ -180,7 +181,9 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
orderType,
planId: subscriptionPlan?.id ?? null,
subscriptionGroupId: subscriptionPlan?.groupId ?? null,
subscriptionDays: subscriptionPlan?.validityDays ?? null,
subscriptionDays: subscriptionPlan
? computeValidityDays(subscriptionPlan.validityDays, subscriptionPlan.validityUnit as ValidityUnit)
: null,
},
});
@@ -598,12 +601,16 @@ export async function executeSubscriptionFulfillment(orderId: string): Promise<v
throw new Error(`Subscription group ${order.subscriptionGroupId} no longer exists or inactive`);
}
await assignSubscription(
await createAndRedeem(
order.rechargeCode,
Number(order.amount),
order.userId,
order.subscriptionGroupId,
order.subscriptionDays,
`sub2apipay subscription order:${orderId}`,
`sub2apipay:subscription:${order.rechargeCode}`,
{
type: 'subscription',
groupId: order.subscriptionGroupId,
validityDays: order.subscriptionDays,
},
);
await prisma.order.updateMany({