feat: 金额上限校验、订阅详情展示优化、支付商品名称区分

- 硬编码 MAX_AMOUNT=99999999.99,所有金额输入(API+前端)统一校验上限
- 管理后台订阅列表改为卡片布局,Sub2API 分组信息嵌套只读展示(平台/倍率/限额/模型)
- 用户端套餐卡片和确认页展示平台、倍率、用量限制
- 订阅订单支付商品名改为 "Sub2API 订阅 {分组名}",余额充值保持原格式
This commit is contained in:
erio
2026-03-13 23:40:23 +08:00
parent ca03a501f2
commit 1bb11ee32b
8 changed files with 275 additions and 106 deletions

View File

@@ -15,6 +15,8 @@ import { getBizDayStartUTC } from '@/lib/time/biz-day';
import { buildOrderResultUrl, createOrderStatusAccessToken } from '@/lib/order/status-access';
const MAX_PENDING_ORDERS = 3;
/** Decimal(10,2) 允许的最大金额 */
export const MAX_AMOUNT = 99999999.99;
function message(locale: Locale, zh: string, en: string): string {
return pickLocaleText(locale, zh, en);
@@ -58,6 +60,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
// ── 订阅订单前置校验 ──
let subscriptionPlan: { id: string; groupId: number; price: Prisma.Decimal; validityDays: number; validityUnit: string; name: string } | null = null;
let subscriptionGroupName = '';
if (orderType === 'subscription') {
if (!input.planId) {
throw new OrderError('INVALID_INPUT', message(locale, '订阅订单必须指定套餐', 'Subscription order requires a plan'), 400);
@@ -75,6 +78,7 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
410,
);
}
subscriptionGroupName = group?.name || plan.name;
subscriptionPlan = plan;
// 订阅订单金额使用服务端套餐价格,不信任客户端
input.amount = Number(plan.price);
@@ -216,7 +220,9 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
orderId: order.id,
amount: payAmountNum,
paymentType: input.paymentType,
subject: `${env.PRODUCT_NAME} ${payAmountStr} CNY`,
subject: subscriptionPlan
? `Sub2API 订阅 ${subscriptionGroupName || subscriptionPlan.name}`
: `${env.PRODUCT_NAME} ${payAmountStr} CNY`,
notifyUrl,
returnUrl,
clientIp: input.clientIp,