Files
AIclinicalresearch/docs/01-设计文档/数据库设计文档.md
2025-10-10 15:14:54 +08:00

771 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据库设计文档
> **版本:** 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) | 用户IDUUID | ✅ | 自动生成 |
| 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一次