- 更新项目章程,从模板更新为完整版本,包含 TypeScript 严格模式、组件优先架构和 TDD 原则 - 添加项目配置文件(.claude/settings.json、.gitignore、CLAUDE.md) - 添加完整的 specs 目录,包含需求、契约和文档 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
14 KiB
14 KiB
数据模型设计: Agent Park - AI项目导航网站
功能分支: 001-ai-project-navigator
创建时间: 2025-12-25
状态: 完成
概述
本文档定义了 Agent Park 项目的数据模型,包括实体定义、关系、验证规则和状态转换。数据模型使用 Prisma Schema 定义,存储在 PostgreSQL 数据库中。
实体关系图 (ERD)
┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
│ Project │<─────│ Tag │ │ ExternalLink │
│ │ N:M │ │ 1:N │ │
└─────────────┘ └─────────────┘ └──────────────────┘
│ 1 │ N
│ │
└────────────────────────────────────────────┘
1
实体定义
1. Project (AI项目)
代表一个AI工具、应用或研究项目。
字段
| 字段名 | 类型 | 约束 | 默认值 | 描述 |
|---|---|---|---|---|
id |
String | Primary Key | cuid() | 主键 |
name |
String | NOT NULL | - | 项目名称(中文) |
nameEn |
String? | Nullable | - | 项目名称(英文) |
slug |
String | UNIQUE, NOT NULL | - | URL 友好标识符 |
description |
String | NOT NULL, Min(10) | - | 项目简介(中文) |
descriptionEn |
String? | Nullable | - | 项目简介(英文) |
content |
Text? | Nullable | - | 详细介绍(中文) |
contentEn |
Text? | Nullable | - | 详细介绍(英文) |
status |
Enum | NOT NULL | ACTIVE | 项目状态 |
source |
String? | Nullable | - | 数据来源标识(如 n8n) |
createdAt |
DateTime | NOT NULL | now() | 收录时间 |
updatedAt |
DateTime | NOT NULL | now() | 更新时间 |
状态枚举
enum ProjectStatus {
ACTIVE # 活跃项目
ARCHIVED # 已归档项目
}
索引
idx_project_status_createdAt: (status, createdAt) - 用于按状态和时间筛选idx_project_slug: (slug) UNIQUE - 用于详情页路由
关系
tags: 与 Tag 的多对多关系links: 与 ExternalLink 的一对多关系
2. Tag (标签)
代表AI项目的特征标记,如"图像生成"、"代码助手"等。
字段
| 字段名 | 类型 | 约束 | 默认值 | 描述 |
|---|---|---|---|---|
id |
String | Primary Key | cuid() | 主键 |
name |
String | UNIQUE, NOT NULL | - | 标签名称(中文) |
nameEn |
String? | Nullable | - | 标签名称(英文) |
slug |
String | UNIQUE, NOT NULL | - | URL 友好标识符 |
createdAt |
DateTime | NOT NULL | now() | 创建时间 |
索引
idx_tag_slug: (slug) UNIQUE - 用于标签筛选页面
关系
projects: 与 Project 的多对多关系
3. ExternalLink (外部链接)
代表项目的外部来源链接,如官网、GitHub、HuggingFace、论文链接。
字段
| 字段名 | 类型 | 约束 | 默认值 | 描述 |
|---|---|---|---|---|
id |
String | Primary Key | cuid() | 主键 |
type |
Enum | NOT NULL | - | 链接类型 |
url |
String | NOT NULL | - | 链接地址 |
title |
String? | Nullable | - | 链接标题(可选) |
projectId |
String | Foreign Key | - | 关联的项目ID |
链接类型枚举
enum LinkType {
WEBSITE # 官方网站
GITHUB # GitHub 仓库
HUGGINGFACE # HuggingFace 模型/数据集
PAPER # 学术论文
}
索引
idx_link_projectId: (projectId) - 用于查询项目的外部链接idx_link_type: (type) - 用于按类型筛选链接
关系
project: 与 Project 的多对一关系(级联删除)
关系定义
Project ↔ Tag (多对多)
使用中间表 _ProjectTags 维护多对多关系。
model Project {
// ... 其他字段
tags Tag[]
}
model Tag {
// ... 其他字段
projects Project[]
}
级联规则: 删除项目时不删除标签(标签可能被其他项目使用)
Project ↔ ExternalLink (一对多)
一个项目可以有多个外部链接。
model Project {
// ... 其他字段
links ExternalLink[]
}
model ExternalLink {
id String @id @default(cuid())
type LinkType
url String
title String?
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
}
级联规则: 删除项目时级联删除所有关联的外部链接
Prisma Schema 完整定义
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
// ================================
// Enums
// ================================
enum ProjectStatus {
ACTIVE
ARCHIVED
}
enum LinkType {
WEBSITE
GITHUB
HUGGINGFACE
PAPER
}
// ================================
// Models
// ================================
model Project {
id String @id @default(cuid())
name String
nameEn String?
slug String @unique
description String
descriptionEn String?
content String? @db.Text
contentEn String? @db.Text
status ProjectStatus @default(ACTIVE)
source String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
tags Tag[]
links ExternalLink[]
// Indexes
@@index([status, createdAt], map: "idx_project_status_createdAt")
@@index([slug], map: "idx_project_slug")
@@map("projects")
}
model Tag {
id String @id @default(cuid())
name String @unique
nameEn String?
slug String @unique
createdAt DateTime @default(now())
// Relations
projects Project[]
// Indexes
@@index([slug], map: "idx_tag_slug")
@@map("tags")
}
model ExternalLink {
id String @id @default(cuid())
type LinkType
url String
title String?
projectId String
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
// Indexes
@@index([projectId], map: "idx_link_projectId")
@@index([type], map: "idx_link_type")
@@map("external_links")
}
Zod 验证 Schema
配合 Prisma 类型,使用 Zod 进行运行时数据验证。
// src/lib/validations.ts
import { z } from 'zod'
// ================================
// Enums
// ================================
export const ProjectStatusEnum = z.enum(['ACTIVE', 'ARCHIVED'])
export const LinkTypeEnum = z.enum(['WEBSITE', 'GITHUB', 'HUGGINGFACE', 'PAPER'])
// ================================
// Base Schemas
// ================================
export const ExternalLinkSchema = z.object({
type: LinkTypeEnum,
url: z.string().url('Invalid URL format'),
title: z.string().max(200).optional()
})
export const TagSchema = z.object({
name: z.string().min(1).max(50),
nameEn: z.string().max(50).optional()
})
// ================================
// Project Schemas
// ================================
export const ProjectBaseSchema = z.object({
name: z.string().min(1).max(200),
nameEn: z.string().max(200).optional(),
description: z.string().min(10).max(500),
descriptionEn: z.string().max(500).optional(),
content: z.string().max(10000).optional(),
contentEn: z.string().max(10000).optional(),
status: ProjectStatusEnum.default('ACTIVE'),
source: z.string().max(100).optional()
})
export const ProjectInputSchema = ProjectBaseSchema.extend({
tags: z.array(TagSchema).min(1, 'At least one tag is required').max(10),
links: z.array(ExternalLinkSchema).min(1, 'At least one link is required').max(10)
})
// ================================
// Webhook Schemas
// ================================
export const WebhookAuthSchema = z.object({
apiKey: z.string().min(32, 'Invalid API key format')
})
export const WebhookPayloadSchema = WebhookAuthSchema.extend({
projects: z.array(ProjectInputSchema).min(1).max(100)
})
// ================================
// Query Schemas
// ================================
export const ProjectQuerySchema = z.object({
search: z.string().max(100).optional(),
tags: z.array(z.string()).optional(),
status: ProjectStatusEnum.optional(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20)
})
// ================================
// Types
// ================================
export type ExternalLink = z.infer<typeof ExternalLinkSchema>
export type Tag = z.infer<typeof TagSchema>
export type ProjectInput = z.infer<typeof ProjectInputSchema>
export type WebhookPayload = z.infer<typeof WebhookPayloadSchema>
export type ProjectQuery = z.infer<typeof ProjectQuerySchema>
数据迁移策略
初始化迁移
# 创建初始迁移
npx prisma migrate dev --name init
# 生成 Prisma Client
npx prisma generate
# 推送 schema 到数据库(开发环境)
npx prisma db push
生产环境部署
# 应用待处理的迁移
npx prisma migrate deploy
# 重置数据库(仅开发环境,慎用!)
npx prisma migrate reset
迁移命名约定
init: 初始化 schemaadd_project_content: 添加项目内容字段add_external_link_title: 添加链接标题字段add_status_index: 添加状态索引
种子数据
假数据示例 (20-30个项目)
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
import { ProjectStatus, LinkType } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// 创建标签
const tagImageGen = await prisma.tag.upsert({
where: { slug: 'image-generation' },
update: {},
create: {
name: '图像生成',
nameEn: 'Image Generation',
slug: 'image-generation'
}
})
const tagCodeAssistant = await prisma.tag.upsert({
where: { slug: 'code-assistant' },
update: {},
create: {
name: '代码助手',
nameEn: 'Code Assistant',
slug: 'code-assistant'
}
})
const tagDataAnalysis = await prisma.tag.upsert({
where: { slug: 'data-analysis' },
update: {},
create: {
name: '数据分析',
nameEn: 'Data Analysis',
slug: 'data-analysis'
}
})
// 创建项目
await prisma.project.upsert({
where: { slug: 'claude' },
update: {},
create: {
name: 'Claude',
nameEn: 'Claude',
slug: 'claude',
description: 'Anthropic 开发的 AI 助手,擅长分析、写作和编程任务。',
descriptionEn: 'AI assistant by Anthropic, excels at analysis, writing, and coding tasks.',
content: 'Claude 是由 Anthropic 开发的下一代 AI 助手。它基于 Constitutional AI 方法训练,强调安全性、诚实性和有用性。Claude 擅长长文本分析、创意写作、编程辅助等多种任务。',
contentEn: 'Claude is a next-generation AI assistant developed by Anthropic. Trained using Constitutional AI methods, it emphasizes safety, honesty, and helpfulness. Claude excels at long-text analysis, creative writing, coding assistance, and more.',
status: ProjectStatus.ACTIVE,
source: 'manual',
tags: {
connect: [{ id: tagCodeAssistant.id }]
},
links: {
create: [
{
type: LinkType.WEBSITE,
url: 'https://www.anthropic.com/claude',
title: 'Official Website'
},
{
type: LinkType.GITHUB,
url: 'https://github.com/anthropics/anthropic-sdk-python',
title: 'Python SDK'
}
]
}
}
})
// 更多项目...
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
数据完整性约束
字段级约束
| 表 | 字段 | 约束 |
|---|---|---|
| projects | name | NOT NULL, Max(200) |
| projects | description | NOT NULL, Min(10), Max(500) |
| projects | slug | UNIQUE, URL-friendly |
| tags | name | UNIQUE, NOT NULL, Max(50) |
| external_links | url | NOT NULL, Valid URL |
业务规则
- 项目必填字段: name、description、至少一个 tag、至少一个 link
- 标签唯一性: 同名标签不能重复创建
- 链接类型限制: 每个项目每种类型的链接最多 5 个
- 项目状态: 新项目默认为 ACTIVE,仅可手动设置为 ARCHIVED
- 删除保护: 删除项目时级联删除链接,但保留标签
性能优化建议
- 索引优化: 为常用查询字段(status、slug、projectId)创建索引
- 查询优化: 使用 Prisma 的
select和include精确获取数据 - 分页查询: 使用
cursor或offset分页避免一次性加载大量数据 - 全文搜索: 如需高级搜索,可使用 PostgreSQL 的全文搜索功能
总结
数据模型设计遵循以下原则:
- 类型安全: Prisma + Zod 提供端到端类型安全
- 国际化支持: 核心字段提供中英双语版本
- 灵活性: 标签系统而非固定分类,适应 AI 领域快速变化
- 可扩展性: 清晰的实体关系,便于后续功能扩展
- 性能优先: 合理的索引设计,优化查询性能