Files
sub2apipay/src/app/api/orders/route.ts
erio 33e4a811f3 fix: 提取 resolveEnabledPaymentTypes 共享函数,下单接口同步校验 + 恢复并发
- 将 resolveEnabledPaymentTypes 提取到 src/lib/payment/resolve-enabled-types.ts
- /api/orders 下单时也校验 ENABLED_PAYMENT_TYPES 配置,防止绕过前端直接调用
- /api/user 恢复 queryMethodLimits 与 getUser 并发执行,避免性能退化
2026-03-15 02:56:28 +08:00

91 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextRequest, NextResponse } from 'next/server';
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';
const createOrderSchema = z.object({
token: z.string().min(1),
amount: z.number().positive().max(99999999.99),
payment_type: z.string().min(1),
src_host: z.string().max(253).optional(),
src_url: z
.string()
.max(2048)
.refine((url) => {
try {
const protocol = new URL(url).protocol;
return protocol === 'http:' || protocol === 'https:';
} catch {
return false;
}
}, 'src_url must be a valid HTTP/HTTPS URL')
.optional(),
is_mobile: z.boolean().optional(),
order_type: z.enum(['balance', 'subscription']).optional(),
plan_id: z.string().optional(),
});
export async function POST(request: NextRequest) {
try {
const env = getEnv();
const body = await request.json();
const parsed = createOrderSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: '参数错误', details: parsed.error.flatten().fieldErrors }, { status: 400 });
}
const { token, amount, payment_type, src_host, src_url, is_mobile, order_type, plan_id } = parsed.data;
// 通过 token 解析用户身份
let userId: number;
try {
const user = await getCurrentUserByToken(token);
userId = user.id;
} catch {
return NextResponse.json({ error: '无效的 token请重新登录', code: 'INVALID_TOKEN' }, { status: 401 });
}
// 订阅订单跳过金额范围校验(价格由服务端套餐决定)
if (order_type !== 'subscription') {
if (amount < env.MIN_RECHARGE_AMOUNT || amount > env.MAX_RECHARGE_AMOUNT) {
return NextResponse.json(
{ error: `充值金额需在 ${env.MIN_RECHARGE_AMOUNT} - ${env.MAX_RECHARGE_AMOUNT} 之间` },
{ status: 400 },
);
}
}
// 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 });
}
const clientIp =
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || '127.0.0.1';
const result = await createOrder({
userId,
amount,
paymentType: payment_type,
clientIp,
isMobile: is_mobile,
srcHost: src_host,
srcUrl: src_url,
orderType: order_type,
planId: plan_id,
});
// 不向客户端暴露 userName / userBalance 等隐私字段
const { userName: _u, userBalance: _b, ...safeResult } = result;
return NextResponse.json(safeResult);
} catch (error) {
return handleApiError(error, '创建订单失败,请稍后重试');
}
}