238 lines
5.5 KiB
TypeScript
238 lines
5.5 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { isValidApiKey } from '@/lib/auth'
|
|
import { z } from 'zod'
|
|
|
|
/**
|
|
* 去重检查请求 Schema
|
|
*/
|
|
const CheckDuplicatesSchema = z.object({
|
|
apiKey: z.string(),
|
|
projects: z.array(
|
|
z.object({
|
|
githubUrl: z.string().url().optional(),
|
|
huggingfaceUrl: z.string().url().optional(),
|
|
websiteUrl: z.string().url().optional(),
|
|
slug: z.string().optional(),
|
|
})
|
|
),
|
|
})
|
|
|
|
/**
|
|
* 匹配类型
|
|
*/
|
|
type MatchType =
|
|
| 'GITHUB_URL'
|
|
| 'HUGGINGFACE_URL'
|
|
| 'WEBSITE_URL'
|
|
| 'SLUG'
|
|
| 'NONE'
|
|
|
|
/**
|
|
* 检查结果
|
|
*/
|
|
interface CheckResult {
|
|
githubUrl?: string
|
|
huggingfaceUrl?: string
|
|
websiteUrl?: string
|
|
slug?: string
|
|
exists: boolean
|
|
matchType: MatchType
|
|
projectId?: string
|
|
projectName?: string
|
|
}
|
|
|
|
/**
|
|
* 多级去重策略:检查项目是否已存在
|
|
*
|
|
* 优先级:
|
|
* 1. GitHub URL 完全匹配(最准确)
|
|
* 2. Hugging Face URL 完全匹配
|
|
* 3. Website URL 完全匹配
|
|
* 4. slug 匹配(兜底)
|
|
*
|
|
* @param project - 项目待检查信息
|
|
* @returns 检查结果
|
|
*/
|
|
async function checkProjectExists(project: {
|
|
githubUrl?: string
|
|
huggingfaceUrl?: string
|
|
websiteUrl?: string
|
|
slug?: string
|
|
}): Promise<CheckResult> {
|
|
// 优先级1: GitHub URL 匹配
|
|
if (project.githubUrl) {
|
|
const existingByGithub = await prisma.externalLink.findFirst({
|
|
where: {
|
|
type: 'GITHUB',
|
|
url: project.githubUrl,
|
|
},
|
|
include: {
|
|
project: true,
|
|
},
|
|
})
|
|
|
|
if (existingByGithub) {
|
|
return {
|
|
githubUrl: project.githubUrl,
|
|
exists: true,
|
|
matchType: 'GITHUB_URL',
|
|
projectId: existingByGithub.project.id,
|
|
projectName: existingByGithub.project.name,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 优先级2: Hugging Face URL 匹配
|
|
if (project.huggingfaceUrl) {
|
|
const existingByHuggingFace = await prisma.externalLink.findFirst({
|
|
where: {
|
|
type: 'HUGGINGFACE',
|
|
url: project.huggingfaceUrl,
|
|
},
|
|
include: {
|
|
project: true,
|
|
},
|
|
})
|
|
|
|
if (existingByHuggingFace) {
|
|
return {
|
|
huggingfaceUrl: project.huggingfaceUrl,
|
|
exists: true,
|
|
matchType: 'HUGGINGFACE_URL',
|
|
projectId: existingByHuggingFace.project.id,
|
|
projectName: existingByHuggingFace.project.name,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 优先级3: Website URL 匹配
|
|
if (project.websiteUrl) {
|
|
const existingByWebsite = await prisma.externalLink.findFirst({
|
|
where: {
|
|
type: 'WEBSITE',
|
|
url: project.websiteUrl,
|
|
},
|
|
include: {
|
|
project: true,
|
|
},
|
|
})
|
|
|
|
if (existingByWebsite) {
|
|
return {
|
|
websiteUrl: project.websiteUrl,
|
|
exists: true,
|
|
matchType: 'WEBSITE_URL',
|
|
projectId: existingByWebsite.project.id,
|
|
projectName: existingByWebsite.project.name,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 优先级4: Slug 匹配(兜底)
|
|
if (project.slug) {
|
|
const existingBySlug = await prisma.project.findUnique({
|
|
where: { slug: project.slug },
|
|
})
|
|
|
|
if (existingBySlug) {
|
|
return {
|
|
slug: project.slug,
|
|
exists: true,
|
|
matchType: 'SLUG',
|
|
projectId: existingBySlug.id,
|
|
projectName: existingBySlug.name,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 未找到匹配项
|
|
return {
|
|
githubUrl: project.githubUrl,
|
|
huggingfaceUrl: project.huggingfaceUrl,
|
|
websiteUrl: project.websiteUrl,
|
|
slug: project.slug,
|
|
exists: false,
|
|
matchType: 'NONE',
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
const startTime = Date.now()
|
|
|
|
try {
|
|
const body = await request.json()
|
|
|
|
// Validate payload
|
|
const validationResult = CheckDuplicatesSchema.safeParse(body)
|
|
|
|
if (!validationResult.success) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Validation error',
|
|
details: validationResult.error.errors.map((e) => e.message),
|
|
},
|
|
{ status: 400 }
|
|
)
|
|
}
|
|
|
|
const payload = validationResult.data
|
|
|
|
// Verify API Key
|
|
if (!isValidApiKey(payload.apiKey)) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Unauthorized',
|
|
details: ['Invalid or missing API Key'],
|
|
},
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
// 并行检查所有项目
|
|
const results = await Promise.all(
|
|
payload.projects.map((project) => checkProjectExists(project))
|
|
)
|
|
|
|
// 统计信息
|
|
const stats = {
|
|
total: results.length,
|
|
exists: results.filter((r) => r.exists).length,
|
|
new: results.filter((r) => !r.exists).length,
|
|
breakdown: {
|
|
githubUrl: results.filter((r) => r.matchType === 'GITHUB_URL').length,
|
|
huggingfaceUrl: results.filter(
|
|
(r) => r.matchType === 'HUGGINGFACE_URL'
|
|
).length,
|
|
websiteUrl: results.filter((r) => r.matchType === 'WEBSITE_URL')
|
|
.length,
|
|
slug: results.filter((r) => r.matchType === 'SLUG').length,
|
|
},
|
|
}
|
|
|
|
const duration = Date.now() - startTime
|
|
|
|
console.warn(
|
|
`[CheckDuplicates] Checked ${stats.total} projects in ${duration}ms: ${stats.exists} exist, ${stats.new} new`
|
|
)
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
results,
|
|
stats,
|
|
})
|
|
} catch (error) {
|
|
console.error('[CheckDuplicates] Error:', error)
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: 'Internal server error',
|
|
details: [error instanceof Error ? error.message : 'Unknown error'],
|
|
},
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|