2026-06-02 19:25:48 +08:00

175 lines
4.8 KiB
TypeScript

import { createHash } from 'crypto';
import { NextRequest, NextResponse } from 'next/server';
import { getPublicProjectDb } from '@/lib/project-auth';
import { verifyApiKey } from '@/lib/api-key';
import { LICENSE_CONFIGS } from '@/lib/types';
import { ensureDefaultModuleGrants } from '@/lib/module-grants';
import { z } from 'zod';
const generateSchema = z.object({
orderNo: z.string().min(1, '订单号不能为空'),
plan: z.enum(['STANDARD', 'PRO'], {
errorMap: () => ({ message: '许可证类型必须是 STANDARD 或 PRO' }),
}),
customerEmail: z.string().email('客户邮箱格式无效'),
customerName: z.string().optional(),
contactInfo: z.string().optional(),
validDays: z.number().int().min(1).max(3650).optional(),
timestamp: z.number().int(),
sign: z.string().min(1, '签名不能为空'),
});
const SIGN_EXPIRE_MS = 5 * 60 * 1000;
const PLAN_NAMES: Record<string, string> = {
STANDARD: '标准版',
PRO: '专业版',
};
function generateActivationCode(plan: 'STANDARD' | 'PRO'): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const typePrefix = plan === 'PRO' ? 'PRO' : 'STD';
const segments: string[] = [];
for (let i = 0; i < 4; i++) {
let segment = '';
for (let j = 0; j < 4; j++) {
segment += chars.charAt(Math.floor(Math.random() * chars.length));
}
segments.push(segment);
}
return `FLNXI-${typePrefix}-${segments.join('-')}`;
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ projectCode: string }> }
) {
try {
const { projectCode } = await params;
const projectDbResult = await getPublicProjectDb(projectCode);
if (!projectDbResult.success) {
return projectDbResult.response;
}
const apiKey = request.headers.get('X-API-Key');
const authResult = await verifyApiKey(apiKey, projectDbResult.prisma);
if (!authResult.valid) {
return NextResponse.json(
{ success: false, error: authResult.error || 'API Key 无效或已过期' },
{ status: 401 }
);
}
const body = await request.json().catch(() => null);
const parsed = generateSchema.safeParse(body);
if (!parsed.success) {
const firstError = parsed.error.errors[0]?.message || '参数验证失败';
return NextResponse.json(
{ success: false, error: firstError },
{ status: 400 }
);
}
const {
orderNo,
plan,
customerEmail,
customerName,
contactInfo,
validDays,
timestamp,
sign,
} = parsed.data;
if (Math.abs(Date.now() - timestamp) > SIGN_EXPIRE_MS) {
return NextResponse.json(
{ success: false, error: '请求已过期,请重新发起' },
{ status: 400 }
);
}
const expectedSign = createHash('md5')
.update(`${orderNo}${timestamp}${apiKey}`)
.digest('hex');
if (sign.toLowerCase() !== expectedSign.toLowerCase()) {
return NextResponse.json(
{ success: false, error: '签名验证失败' },
{ status: 401 }
);
}
const existingOrder = await projectDbResult.prisma.order.findUnique({
where: { orderNo },
});
if (existingOrder) {
return NextResponse.json(
{ success: false, error: '订单号已存在' },
{ status: 409 }
);
}
const activationCode = generateActivationCode(plan);
const config = LICENSE_CONFIGS[plan];
const expiresAt = validDays
? new Date(Date.now() + validDays * 24 * 60 * 60 * 1000)
: null;
await projectDbResult.prisma.$transaction(async (tx) => {
const license = await tx.license.create({
data: {
licenseKey: activationCode,
licenseType: plan,
customerName: customerName || null,
contactInfo: contactInfo || null,
maxUsers: config.limits.maxUsers,
maxMaterials: config.limits.maxMaterials,
features: config.features,
expiresAt,
},
});
await ensureDefaultModuleGrants(tx, {
id: license.id,
licenseType: license.licenseType,
expiresAt: license.expiresAt,
});
await tx.order.create({
data: {
orderNo,
plan,
customerEmail,
customerName: customerName || null,
licenseId: license.id,
validDays: validDays ?? undefined,
},
});
});
return NextResponse.json({
success: true,
data: {
activationCode,
plan,
planName: PLAN_NAMES[plan],
expiresAt: expiresAt ? expiresAt.toISOString() : null,
validDays: validDays || null,
isPermanent: !validDays,
},
});
} catch (error) {
console.error('[License] 项目激活码生成失败:', error);
return NextResponse.json(
{ success: false, error: '服务器内部错误' },
{ status: 500 }
);
}
}