# 数据库设计文档 > **版本:** v1.0 > **创建日期:** 2025-10-10 > **数据库:** PostgreSQL 15+ > **ORM:** Prisma --- ## 📋 目录 1. [数据库概述](#数据库概述) 2. [ER图](#er图) 3. [表结构设计](#表结构设计) 4. [索引设计](#索引设计) 5. [数据约束](#数据约束) 6. [数据迁移策略](#数据迁移策略) --- ## 数据库概述 ### 设计原则 - ✅ 遵循第三范式(3NF) - ✅ 使用UUID作为主键 - ✅ 所有表包含created_at和updated_at时间戳 - ✅ 使用软删除(保留deleted_at字段,重要表) - ✅ 外键约束使用CASCADE删除 - ✅ 敏感字段加密存储(密码使用bcrypt) ### 命名规范 - 表名:复数形式,下划线分隔(如:`users`、`knowledge_bases`) - 字段名:下划线分隔(如:`created_at`、`user_id`) - 索引名:`idx_表名_字段名`(如:`idx_users_email`) - 外键名:`fk_表名_关联表名`(如:`fk_projects_users`) --- ## ER图 ``` ┌─────────────┐ │ Users │ └──────┬──────┘ │ │ 1:N ├─────────────────┐ │ │ ▼ ▼ ┌─────────────┐ ┌──────────────┐ │ Projects │ │KnowledgeBases│ └──────┬──────┘ └──────┬───────┘ │ │ │ 1:N │ 1:N │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │Conversations │ │ Documents │ └──────┬───────┘ └──────────────┘ │ │ 1:N │ ▼ ┌──────────────┐ │ Messages │ └──────────────┘ ``` --- ## 表结构设计 ### 1. users - 用户表 **用途:** 存储用户基本信息和认证信息 ```sql CREATE TABLE users ( id VARCHAR(50) PRIMARY KEY DEFAULT gen_random_uuid()::VARCHAR, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, -- bcrypt加密 name VARCHAR(100), avatar_url TEXT, -- 角色和状态 role VARCHAR(20) NOT NULL DEFAULT 'user', -- user, admin status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, inactive, suspended -- 配额(知识库限制) kb_quota INTEGER DEFAULT 3, kb_used INTEGER DEFAULT 0, -- 试用信息 trial_ends_at TIMESTAMP, is_trial BOOLEAN DEFAULT true, -- 时间戳 last_login_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), -- 索引 CONSTRAINT check_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$'), CONSTRAINT check_kb_quota CHECK (kb_used <= kb_quota) ); -- 索引 CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_status ON users(status); CREATE INDEX idx_users_created_at ON users(created_at); -- 注释 COMMENT ON TABLE users IS '用户表'; COMMENT ON COLUMN users.role IS '用户角色:user-普通用户, admin-管理员'; COMMENT ON COLUMN users.status IS '账户状态:active-激活, inactive-未激活, suspended-暂停'; ``` **字段说明:** | 字段 | 类型 | 说明 | 必填 | 默认值 | |------|------|------|------|--------| | id | VARCHAR(50) | 用户ID(UUID) | ✅ | 自动生成 | | email | VARCHAR(255) | 邮箱(登录名) | ✅ | - | | password | VARCHAR(255) | 密码(bcrypt) | ✅ | - | | name | VARCHAR(100) | 用户姓名 | ❌ | NULL | | role | VARCHAR(20) | 角色 | ✅ | 'user' | | status | VARCHAR(20) | 状态 | ✅ | 'active' | | kb_quota | INTEGER | 知识库配额 | ✅ | 3 | | kb_used | INTEGER | 已使用知识库数 | ✅ | 0 | --- ### 2. projects - 项目/课题表 **用途:** 存储用户创建的研究项目/课题 ```sql CREATE TABLE projects ( id VARCHAR(50) PRIMARY KEY DEFAULT gen_random_uuid()::VARCHAR, user_id VARCHAR(50) NOT NULL, -- 项目信息 name VARCHAR(200) NOT NULL, description TEXT NOT NULL, -- 项目背景信息(重要!用于上下文注入) -- 统计信息 conversation_count INTEGER DEFAULT 0, -- 时间戳 created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), -- 外键 CONSTRAINT fk_projects_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); -- 索引 CREATE INDEX idx_projects_user_id ON projects(user_id); CREATE INDEX idx_projects_created_at ON projects(created_at); -- 注释 COMMENT ON TABLE projects IS '项目/课题表'; COMMENT ON COLUMN projects.description IS '项目背景信息,会自动注入到对话上下文中'; ``` --- ### 3. conversations - 对话表 **用途:** 存储用户与智能体的对话会话 ```sql CREATE TABLE conversations ( id VARCHAR(50) PRIMARY KEY DEFAULT gen_random_uuid()::VARCHAR, user_id VARCHAR(50) NOT NULL, project_id VARCHAR(50), -- 可选,全局快速问答时为NULL agent_id VARCHAR(50) NOT NULL, -- 智能体ID(对应config/agents.yaml) -- 对话信息 title VARCHAR(200) NOT NULL, model_name VARCHAR(50) DEFAULT 'deepseek-v3', -- 统计信息 message_count INTEGER DEFAULT 0, total_tokens INTEGER DEFAULT 0, -- 时间戳 created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), -- 外键 CONSTRAINT fk_conversations_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_conversations_projects FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE ); -- 索引 CREATE INDEX idx_conversations_user_id ON conversations(user_id); CREATE INDEX idx_conversations_project_id ON conversations(project_id); CREATE INDEX idx_conversations_agent_id ON conversations(agent_id); CREATE INDEX idx_conversations_created_at ON conversations(created_at); -- 注释 COMMENT ON TABLE conversations IS '对话会话表'; COMMENT ON COLUMN conversations.project_id IS '项目ID,全局快速问答时为NULL'; COMMENT ON COLUMN conversations.agent_id IS '智能体ID,对应配置文件中的智能体'; ``` --- ### 4. messages - 消息表 **用途:** 存储对话中的每条消息 ```sql CREATE TABLE messages ( id VARCHAR(50) PRIMARY KEY DEFAULT gen_random_uuid()::VARCHAR, conversation_id VARCHAR(50) NOT NULL, -- 消息内容 role VARCHAR(20) NOT NULL, -- user, assistant content TEXT NOT NULL, -- 元数据(可选) metadata JSONB, -- 存储额外信息,如引用的知识库、模型参数等 -- 统计信息 tokens INTEGER, -- 是否固定到项目背景 is_pinned BOOLEAN DEFAULT false, -- 时间戳 created_at TIMESTAMP DEFAULT NOW(), -- 外键 CONSTRAINT fk_messages_conversations FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE, -- 约束 CONSTRAINT check_role CHECK (role IN ('user', 'assistant')) ); -- 索引 CREATE INDEX idx_messages_conversation_id ON messages(conversation_id); CREATE INDEX idx_messages_created_at ON messages(created_at); CREATE INDEX idx_messages_is_pinned ON messages(is_pinned); -- 注释 COMMENT ON TABLE messages IS '对话消息表'; COMMENT ON COLUMN messages.is_pinned IS '是否固定到项目背景,用于动态更新项目描述'; COMMENT ON COLUMN messages.metadata IS 'JSON格式,存储引用的知识库、使用的模型等'; ``` --- ### 5. knowledge_bases - 知识库表 **用途:** 存储用户创建的个人知识库 ```sql CREATE TABLE knowledge_bases ( id VARCHAR(50) PRIMARY KEY DEFAULT gen_random_uuid()::VARCHAR, user_id VARCHAR(50) NOT NULL, -- 知识库信息 name VARCHAR(100) NOT NULL, description TEXT, -- Dify集成 dify_dataset_id VARCHAR(100) NOT NULL, -- Dify中的知识库ID -- 统计信息 file_count INTEGER DEFAULT 0, total_size_bytes BIGINT DEFAULT 0, -- 时间戳 created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), -- 外键 CONSTRAINT fk_kb_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, -- 约束:每个用户最多3个知识库 CONSTRAINT check_kb_limit CHECK ( (SELECT COUNT(*) FROM knowledge_bases WHERE user_id = knowledge_bases.user_id) <= 3 ) ); -- 索引 CREATE INDEX idx_kb_user_id ON knowledge_bases(user_id); CREATE INDEX idx_kb_dify_dataset_id ON knowledge_bases(dify_dataset_id); -- 注释 COMMENT ON TABLE knowledge_bases IS '知识库表,每个用户最多3个'; COMMENT ON COLUMN knowledge_bases.dify_dataset_id IS 'Dify中对应的数据集ID'; ``` --- ### 6. documents - 文档表 **用途:** 存储知识库中上传的文档 ```sql CREATE TABLE documents ( id VARCHAR(50) PRIMARY KEY DEFAULT gen_random_uuid()::VARCHAR, kb_id VARCHAR(50) NOT NULL, user_id VARCHAR(50) NOT NULL, -- 文件信息 filename VARCHAR(255) NOT NULL, file_type VARCHAR(20) NOT NULL, -- pdf, docx file_size_bytes BIGINT NOT NULL, file_url TEXT NOT NULL, -- 对象存储URL -- Dify集成 dify_document_id VARCHAR(100) NOT NULL, -- Dify中的文档ID -- 处理状态 status VARCHAR(20) DEFAULT 'uploading', -- uploading, processing, completed, failed progress INTEGER DEFAULT 0, -- 0-100 error_message TEXT, -- 处理结果 segments_count INTEGER, -- 切分的段落数 tokens_count INTEGER, -- token数量 -- 时间戳 uploaded_at TIMESTAMP DEFAULT NOW(), processed_at TIMESTAMP, -- 外键 CONSTRAINT fk_documents_kb FOREIGN KEY (kb_id) REFERENCES knowledge_bases(id) ON DELETE CASCADE, CONSTRAINT fk_documents_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, -- 约束:每个知识库最多50个文档 CONSTRAINT check_doc_limit CHECK ( (SELECT COUNT(*) FROM documents WHERE kb_id = documents.kb_id) <= 50 ), -- 约束:状态和进度 CONSTRAINT check_status CHECK (status IN ('uploading', 'processing', 'completed', 'failed')), CONSTRAINT check_progress CHECK (progress >= 0 AND progress <= 100) ); -- 索引 CREATE INDEX idx_documents_kb_id ON documents(kb_id); CREATE INDEX idx_documents_user_id ON documents(user_id); CREATE INDEX idx_documents_status ON documents(status); CREATE INDEX idx_documents_dify_document_id ON documents(dify_document_id); -- 注释 COMMENT ON TABLE documents IS '文档表,每个知识库最多50个文档'; COMMENT ON COLUMN documents.status IS '处理状态:uploading-上传中, processing-处理中, completed-完成, failed-失败'; ``` --- ### 7. admin_logs - 管理员操作日志表(可选) **用途:** 记录管理员的敏感操作 ```sql CREATE TABLE admin_logs ( id SERIAL PRIMARY KEY, admin_id VARCHAR(50) NOT NULL, -- 操作信息 action VARCHAR(100) NOT NULL, -- 操作类型 resource_type VARCHAR(50), -- 资源类型:user, conversation, etc. resource_id VARCHAR(50), -- 资源ID -- 详细信息 details JSONB, ip_address VARCHAR(45), user_agent TEXT, -- 时间戳 created_at TIMESTAMP DEFAULT NOW(), -- 外键 CONSTRAINT fk_admin_logs_users FOREIGN KEY (admin_id) REFERENCES users(id) ON DELETE CASCADE ); -- 索引 CREATE INDEX idx_admin_logs_admin_id ON admin_logs(admin_id); CREATE INDEX idx_admin_logs_created_at ON admin_logs(created_at); CREATE INDEX idx_admin_logs_action ON admin_logs(action); -- 注释 COMMENT ON TABLE admin_logs IS '管理员操作日志表,用于审计'; ``` --- ## 索引设计 ### 主要索引 | 表名 | 索引字段 | 类型 | 用途 | |------|---------|------|------| | users | email | UNIQUE | 登录查询 | | users | status | INDEX | 按状态筛选用户 | | projects | user_id | INDEX | 查询用户的项目 | | conversations | user_id | INDEX | 查询用户的对话 | | conversations | project_id | INDEX | 查询项目的对话 | | conversations | agent_id | INDEX | 统计智能体使用情况 | | messages | conversation_id | INDEX | 查询对话消息 | | knowledge_bases | user_id | INDEX | 查询用户的知识库 | | documents | kb_id | INDEX | 查询知识库的文档 | | documents | status | INDEX | 筛选处理状态 | ### 复合索引(如需优化性能可添加) ```sql -- 按时间范围查询用户的对话 CREATE INDEX idx_conversations_user_created ON conversations(user_id, created_at DESC); -- 按项目查询特定智能体的对话 CREATE INDEX idx_conversations_project_agent ON conversations(project_id, agent_id); ``` --- ## 数据约束 ### 业务约束 1. **知识库数量限制** - 每个用户最多3个知识库 - 通过CHECK约束和应用层双重控制 2. **文档数量限制** - 每个知识库最多50个文档 - 通过CHECK约束和应用层双重控制 3. **邮箱格式验证** - 使用正则表达式验证邮箱格式 4. **密码安全** - 使用bcrypt加密,成本因子12 - 密码长度至少8位(应用层验证) ### 数据完整性 1. **级联删除** - 删除用户 → 级联删除其项目、对话、知识库 - 删除项目 → 级联删除其对话 - 删除知识库 → 级联删除其文档 2. **外键约束** - 所有外键都设置了ON DELETE CASCADE - 保证数据一致性 --- ## 数据迁移策略 ### 初始化迁移 ```bash # 创建初始迁移 npx prisma migrate dev --name init # 生成Prisma Client npx prisma generate ``` ### 迁移命名规范 ``` yyyymmdd_描述.sql 例如: 20251010_init.sql 20251015_add_admin_logs.sql 20251020_add_user_quotas.sql ``` ### 生产环境迁移流程 1. 在开发环境测试迁移 2. 备份生产数据库 3. 执行迁移脚本 4. 验证数据完整性 5. 回滚计划(如有问题) --- ## Prisma Schema ### 完整的schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(uuid()) email String @unique password String name String? avatarUrl String? @map("avatar_url") role String @default("user") status String @default("active") kbQuota Int @default(3) @map("kb_quota") kbUsed Int @default(0) @map("kb_used") trialEndsAt DateTime? @map("trial_ends_at") isTrial Boolean @default(true) @map("is_trial") lastLoginAt DateTime? @map("last_login_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") projects Project[] conversations Conversation[] knowledgeBases KnowledgeBase[] documents Document[] adminLogs AdminLog[] @@index([email]) @@index([status]) @@index([createdAt]) @@map("users") } model Project { id String @id @default(uuid()) userId String @map("user_id") name String description String @db.Text conversationCount Int @default(0) @map("conversation_count") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) conversations Conversation[] @@index([userId]) @@index([createdAt]) @@map("projects") } model Conversation { id String @id @default(uuid()) userId String @map("user_id") projectId String? @map("project_id") agentId String @map("agent_id") title String modelName String @default("deepseek-v3") @map("model_name") messageCount Int @default(0) @map("message_count") totalTokens Int @default(0) @map("total_tokens") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade) messages Message[] @@index([userId]) @@index([projectId]) @@index([agentId]) @@index([createdAt]) @@map("conversations") } model Message { id String @id @default(uuid()) conversationId String @map("conversation_id") role String content String @db.Text metadata Json? tokens Int? isPinned Boolean @default(false) @map("is_pinned") createdAt DateTime @default(now()) @map("created_at") conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade) @@index([conversationId]) @@index([createdAt]) @@index([isPinned]) @@map("messages") } model KnowledgeBase { id String @id @default(uuid()) userId String @map("user_id") name String description String? difyDatasetId String @map("dify_dataset_id") fileCount Int @default(0) @map("file_count") totalSizeBytes BigInt @default(0) @map("total_size_bytes") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) documents Document[] @@index([userId]) @@index([difyDatasetId]) @@map("knowledge_bases") } model Document { id String @id @default(uuid()) kbId String @map("kb_id") userId String @map("user_id") filename String fileType String @map("file_type") fileSizeBytes BigInt @map("file_size_bytes") fileUrl String @map("file_url") difyDocumentId String @map("dify_document_id") status String @default("uploading") progress Int @default(0) errorMessage String? @map("error_message") segmentsCount Int? @map("segments_count") tokensCount Int? @map("tokens_count") uploadedAt DateTime @default(now()) @map("uploaded_at") processedAt DateTime? @map("processed_at") knowledgeBase KnowledgeBase @relation(fields: [kbId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([kbId]) @@index([userId]) @@index([status]) @@index([difyDocumentId]) @@map("documents") } model AdminLog { id Int @id @default(autoincrement()) adminId String @map("admin_id") action String resourceType String? @map("resource_type") resourceId String? @map("resource_id") details Json? ipAddress String? @map("ip_address") userAgent String? @map("user_agent") createdAt DateTime @default(now()) @map("created_at") admin User @relation(fields: [adminId], references: [id], onDelete: Cascade) @@index([adminId]) @@index([createdAt]) @@index([action]) @@map("admin_logs") } ``` --- ## 数据字典 ### 枚举值定义 **用户角色 (user.role)** - `user` - 普通用户 - `admin` - 管理员 **账户状态 (user.status)** - `active` - 激活(正常使用) - `inactive` - 未激活(注册但未验证邮箱) - `suspended` - 暂停(违规或欠费) **消息角色 (message.role)** - `user` - 用户消息 - `assistant` - AI助手消息 **文档状态 (document.status)** - `uploading` - 上传中 - `processing` - Dify处理中 - `completed` - 处理完成 - `failed` - 处理失败 **文件类型 (document.file_type)** - `pdf` - PDF文档 - `docx` - Word文档 --- ## 数据安全 ### 敏感数据处理 1. **密码** - 使用bcrypt加密(成本因子12) - 不可逆加密,无法还原明文 2. **API Keys** - 不存储在数据库中 - 使用环境变量管理 3. **用户数据隔离** - 所有查询必须包含user_id过滤 - 防止越权访问 ### 备份策略 1. **自动备份** - 每日全量备份 - 保留最近7天 2. **手动备份** - 重大升级前手动备份 - 保留3个版本 --- ## 性能优化建议 ### 查询优化 1. **分页查询** - 使用 LIMIT + OFFSET - 或使用游标分页(基于ID) 2. **避免N+1查询** - 使用Prisma的include和select - 预加载关联数据 3. **合理使用索引** - 频繁查询的字段建立索引 - 定期检查索引使用情况 ### 数据归档 1. **历史数据归档** - 对话超过6个月自动归档 - 归档数据移至单独的表 2. **日志清理** - admin_logs保留3个月 - 定期清理过期日志 --- ## 版本变更记录 | 版本 | 日期 | 变更内容 | 作者 | |------|------|---------|------| | v1.0 | 2025-10-10 | 初始版本,定义所有核心表 | 开发团队 | --- **文档维护:** 数据库结构变更需同步更新本文档 **Review频率:** 每个里程碑结束后Review一次