新增功能: - 新增 ProjectDiscoveryTask 数据模型,支持异步项目探索任务管理 - 新增 /api/discovery/tasks CRUD API,支持任务创建、查询和状态更新 - 新增 /api/discovery/tasks/:id/complete API,完成探索并自动创建项目 - 新增 discovery-service 服务层,封装多级去重和标签处理逻辑 - 新增 discover-projects 命令,支持批量探索项目 - 新增 PROJECT_CONTENT_STANDARD.md 内容标准模板 代码质量修复: - 修复状态转换验证未使用问题,防止非法状态转换 - 修复 explorationData 类型安全问题,使用 z.record(z.unknown()) 替代 z.any() - 添加事务保护到 complete API,确保项目创建和任务状态更新的原子性 代码重构: - 将 webhook/projects 的去重逻辑迁移到 discovery-service - 统一探索任务相关的验证模式到 validations.ts Co-Authored-By: Claude <noreply@anthropic.com>
134 lines
3.5 KiB
TypeScript
134 lines
3.5 KiB
TypeScript
import { prisma } from '@/lib/prisma'
|
|
import type { ProjectInput } from '@/lib/validations'
|
|
import { generateSlug } from '@/lib/slug'
|
|
|
|
/**
|
|
* 多级去重策略:查找已存在的项目
|
|
*
|
|
* 优先级:
|
|
* 1. GitHub URL 完全匹配(最准确)
|
|
* 2. Website URL 完全匹配
|
|
* 3. slug 匹配(兜底)
|
|
*
|
|
* @param projectData - 项目数据
|
|
* @returns 已存在的项目,如果不存在则返回 null
|
|
*/
|
|
export async function findExistingProject(projectData: ProjectInput) {
|
|
// 优先级1: 通过 GitHub URL 匹配
|
|
const githubLink = projectData.links.find((link) => link.type === 'GITHUB')
|
|
if (githubLink) {
|
|
const existingByGithub = await prisma.externalLink.findFirst({
|
|
where: {
|
|
type: 'GITHUB',
|
|
url: githubLink.url,
|
|
},
|
|
include: {
|
|
project: {
|
|
include: {
|
|
links: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if (existingByGithub) {
|
|
console.warn(
|
|
`[Discovery] Found existing project by GitHub URL: ${githubLink.url}`
|
|
)
|
|
return existingByGithub.project
|
|
}
|
|
}
|
|
|
|
// 优先级2: 通过 Website URL 匹配
|
|
const websiteLink = projectData.links.find((link) => link.type === 'WEBSITE')
|
|
if (websiteLink) {
|
|
const existingByWebsite = await prisma.externalLink.findFirst({
|
|
where: {
|
|
type: 'WEBSITE',
|
|
url: websiteLink.url,
|
|
},
|
|
include: {
|
|
project: {
|
|
include: {
|
|
links: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
if (existingByWebsite) {
|
|
console.warn(
|
|
`[Discovery] Found existing project by Website URL: ${websiteLink.url}`
|
|
)
|
|
return existingByWebsite.project
|
|
}
|
|
}
|
|
|
|
// 优先级3: 通过 slug 匹配(兜底)
|
|
const slug = generateSlug(projectData.name, projectData.nameEn)
|
|
|
|
const existingBySlug = await prisma.project.findUnique({
|
|
where: { slug },
|
|
include: {
|
|
links: true,
|
|
},
|
|
})
|
|
|
|
if (existingBySlug) {
|
|
console.warn(`[Discovery] Found existing project by slug: ${slug}`)
|
|
return existingBySlug
|
|
}
|
|
|
|
console.warn(`[Discovery] No existing project found, will create new one`)
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* 批量创建或获取标签
|
|
*
|
|
* @param tags - 标签数组
|
|
* @returns 标签连接对象数组
|
|
*/
|
|
export async function upsertTags(tags: ProjectInput['tags']) {
|
|
// 优化:批量查询所有已存在的标签,避免 N+1 问题
|
|
const allTagNames = tags.map((t) => t.name)
|
|
const existingTags = await prisma.tag.findMany({
|
|
where: { name: { in: allTagNames } },
|
|
})
|
|
const existingTagNames = new Set(existingTags.map((t) => t.name))
|
|
|
|
// Upsert tags - 优化后只查询不存在的标签
|
|
return await Promise.all(
|
|
tags.map(async (tag) => {
|
|
const tagSlug = generateSlug(tag.name, tag.nameEn)
|
|
|
|
// 首先从批量查询结果中查找
|
|
if (existingTagNames.has(tag.name)) {
|
|
return existingTags.find((t) => t.name === tag.name)!
|
|
}
|
|
|
|
// 只有标签不存在时才尝试 upsert
|
|
try {
|
|
return await prisma.tag.upsert({
|
|
where: { slug: tagSlug },
|
|
update: {},
|
|
create: {
|
|
name: tag.name,
|
|
nameEn: tag.nameEn || null,
|
|
slug: tagSlug,
|
|
},
|
|
})
|
|
} catch (error) {
|
|
// 如果 slug 冲突,查找并使用已存在的标签
|
|
const existingBySlug = await prisma.tag.findUnique({
|
|
where: { slug: tagSlug },
|
|
})
|
|
if (existingBySlug) {
|
|
return existingBySlug
|
|
}
|
|
throw error
|
|
}
|
|
})
|
|
)
|
|
}
|