Files
agent-park/specs/001-ai-project-navigator/data-model.md
T
mzaxdandClaude c5a8de8cf0 chore: 初始化 Agent Park v2 项目
- 更新项目章程,从模板更新为完整版本,包含 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>
2025-12-25 14:21:35 +08:00

14 KiB
Raw Blame History

数据模型设计: 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 的多对多关系

代表项目的外部来源链接,如官网、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[]
}

级联规则: 删除项目时不删除标签(标签可能被其他项目使用)

一个项目可以有多个外部链接。

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: 初始化 schema
  • add_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

业务规则

  1. 项目必填字段: name、description、至少一个 tag、至少一个 link
  2. 标签唯一性: 同名标签不能重复创建
  3. 链接类型限制: 每个项目每种类型的链接最多 5 个
  4. 项目状态: 新项目默认为 ACTIVE,仅可手动设置为 ARCHIVED
  5. 删除保护: 删除项目时级联删除链接,但保留标签

性能优化建议

  1. 索引优化: 为常用查询字段(status、slug、projectId)创建索引
  2. 查询优化: 使用 Prisma 的 selectinclude 精确获取数据
  3. 分页查询: 使用 cursoroffset 分页避免一次性加载大量数据
  4. 全文搜索: 如需高级搜索,可使用 PostgreSQL 的全文搜索功能

总结

数据模型设计遵循以下原则:

  • 类型安全: Prisma + Zod 提供端到端类型安全
  • 国际化支持: 核心字段提供中英双语版本
  • 灵活性: 标签系统而非固定分类,适应 AI 领域快速变化
  • 可扩展性: 清晰的实体关系,便于后续功能扩展
  • 性能优先: 合理的索引设计,优化查询性能