Files
AIclinicalresearch/docs/01-设计文档/数据库设计文档.md
AI Clinical Dev Team 239c7ea85e feat: Day 21-22 - knowledge base frontend completed, fix CORS and file upload issues
- Complete knowledge base list and detail pages
- Complete document upload component
- Fix CORS config (add PUT/DELETE method support)
- Fix file upload issues (disabled state and beforeUpload return value)
- Add detailed debug logs (cleaned up)
- Create Day 21-22 completion summary document
2025-10-11 15:40:12 +08:00

21 KiB
Raw Blame History

数据库设计文档

版本: v1.0
创建日期: 2025-10-10
数据库: PostgreSQL 15+
ORM Prisma


📋 目录

  1. 数据库概述
  2. ER图
  3. 表结构设计
  4. 索引设计
  5. 数据约束
  6. 数据迁移策略

数据库概述

设计原则

  • 遵循第三范式3NF
  • 使用UUID作为主键
  • 所有表包含created_at和updated_at时间戳
  • 使用软删除保留deleted_at字段重要表
  • 外键约束使用CASCADE删除
  • 敏感字段加密存储密码使用bcrypt

命名规范

  • 表名:复数形式,下划线分隔(如:usersknowledge_bases
  • 字段名:下划线分隔(如:created_atuser_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 - 用户表

用途: 存储用户基本信息和认证信息

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 - 项目/课题表

用途: 存储用户创建的研究项目/课题

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 - 对话表

用途: 存储用户与智能体的对话会话

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 - 消息表

用途: 存储对话中的每条消息

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 - 知识库表

用途: 存储用户创建的个人知识库

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 - 文档表

用途: 存储知识库中上传的文档

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 - 管理员操作日志表(可选)

用途: 记录管理员的敏感操作

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 筛选处理状态

复合索引(如需优化性能可添加)

-- 按时间范围查询用户的对话
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
    • 保证数据一致性

数据迁移策略

初始化迁移

# 创建初始迁移
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

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一次