AI数据库表关系与字段说明
DATABASE REFERENCE生成时间 2026/10/03 18:00·支持目录跳转与全文搜索

英语学习平台:数据库表关系与字段说明#

数据库:PostgreSQL
ORM:Prisma
模型来源:server/prisma/schema.prisma
行号基准:Git 提交 82883ee1097876d3159ffb5b50fae0ecd93542ec
生成日期:2026-10-03

1. 先理解四类存储#

本项目不只有 PostgreSQL。面试时要先把四类存储的职责分清:

存储 是否是业务事实来源 保存内容
PostgreSQL 是 用户、课程、订单、学习记录、知识库元数据、Agent 会话/运行/Action、学习计划、测验、LangGraph checkpoint。
MinIO 否 用户上传的原始 PDF/MD/TXT、头像、课程静态文件。
Chroma 否,可重建 私人文档分片、Embedding 向量、页码和用户/文档过滤元数据。
Redis 否,短期协调 Celery 队列、文档锁、限流计数、HMAC nonce、Worker 心跳、删除 tombstone。

本文以 PostgreSQL 为主,并在 8.2 节单独列出 Chroma 向量库的 Collection、记录字段、metadata 字段以及它们和 PostgreSQL 的逻辑关系。知识库正文和向量不在 KnowledgeDocument 表中;原文在 MinIO,分片与向量在 Chroma。

2. Prisma 类型速查#

Prisma 类型/标记 PostgreSQL 含义
String 通常是 TEXT;本项目主键多为 cuid() 字符串。
String? 可空字符串。
Int 32 位整数。
Float 双精度浮点数。
Decimal 精确小数,适合金额/成本。
Boolean 布尔值。
DateTime 时间戳。
Json PostgreSQL JSONB。
String[] PostgreSQL 字符串数组。
@id 主键。
@unique 唯一约束。
@default(...) 默认值。
@updatedAt Prisma 更新记录时自动刷新。
@@index([...]) 普通索引。
@@unique([...]) 组合唯一约束。
onDelete: Cascade 删除父记录时,数据库自动删除子记录。
onDelete: SetNull 删除父记录时保留子记录,但把外键设为 NULL。
onDelete: Restrict 有子记录引用时禁止删除父记录。

Prisma 模型中的 xxx[] 和关系对象字段只是 ORM 关系导航字段,不会额外生成数据库列。真正落库的是 userId、conversationId 等外键字段。

3. 全局关系概览#

系统共 18 个 Prisma 业务模型,可以分为五组:

领域 表
用户与词汇 User、WordBook、WordBookRecord
课程与支付 Course、CourseRecord、PaymentRecord
前端埋点 Visitor、PageView、TrackEvent、PerformanceEntry、ErrorEntry
Agent 与知识库 KnowledgeDocument、AiConversation、AiMessage、AiRun、AgentAction
学习计划与测验 StudyPlan、StudyTask、QuizSession、QuizAttempt

3.1 用户、词汇、课程关系#

erDiagram
    User ||--o{ WordBookRecord : learns
    WordBook ||--o{ WordBookRecord : recorded_by
    User ||--o{ PaymentRecord : pays
    User ||--o{ CourseRecord : owns
    Course ||--o{ CourseRecord : purchased_as
    PaymentRecord ||--o{ CourseRecord : grants
  • User 与 WordBook 是多对多,通过 WordBookRecord 保存用户对每个单词的掌握状态。
  • User 与 Course 是多对多,通过 CourseRecord 保存购买关系。
  • 一笔 PaymentRecord 可以关联一个或多个 CourseRecord;CourseRecord.paymentRecordId 允许为空,便于先创建关系或兼容非支付来源。

3.2 埋点关系#

erDiagram
    User o|--o{ Visitor : may_bind
    Visitor ||--o{ PageView : has
    Visitor ||--o{ TrackEvent : has
    Visitor ||--o{ PerformanceEntry : has
    Visitor ||--o{ ErrorEntry : has
  • Visitor 用 anonymousId 标识浏览器访客,登录后可选地绑定 User。
  • PV、行为、性能、错误都挂在 Visitor 下,因此未登录访问也能统计。

3.3 Agent、知识库和学习关系#

erDiagram
    User ||--o{ KnowledgeDocument : uploads
    User ||--o{ AiConversation : owns
    User ||--o{ AiRun : starts
    User ||--o{ AgentAction : confirms
    User ||--o{ StudyPlan : has
    User ||--o{ StudyTask : has
    User ||--o{ QuizSession : takes
    User ||--o{ QuizAttempt : submits

    AiConversation ||--o{ AiMessage : contains
    AiConversation ||--o{ AiRun : executes
    AiConversation ||--o{ AgentAction : requests
    AiConversation o|--o{ QuizSession : generates
    AiConversation o|--o{ QuizAttempt : records

    AiRun o|--o{ AiMessage : produces
    AiRun o|--o{ AgentAction : creates

    StudyPlan ||--o{ StudyTask : contains
    QuizSession ||--o| QuizAttempt : submitted_as

关键设计:

  • AiConversation 是长期会话;AiRun 是一次用户消息触发的一次执行。
  • AiMessage 属于会话,也可以属于某次 Run;Action 回执等消息可能不绑定原 Run。
  • AgentAction 把“模型建议执行”与“真实业务写入”隔开。
  • StudyPlan 是计划头,StudyTask 是按日期展开的具体任务。
  • QuizSession 保存尚未提交的服务端题目快照;QuizAttempt 保存最终答案与评分,两者是一对零或一。
  • KnowledgeDocument 与 Chroma 没有数据库外键,但通过 KnowledgeDocument.id = metadata.document_id、User.id = metadata.owner_user_id 形成跨存储的一对多逻辑关系。

4. 状态枚举#

枚举定义位于 server/prisma/schema.prisma:17-98。

4.1 TradeStatus#

值 含义
NOT_PAY 本地订单已创建但尚未支付。
WAIT_BUYER_PAY 支付宝等待买家付款。
TRADE_CLOSED 交易关闭。
TRADE_SUCCESS 支付成功,通常仍处于可退款期。
TRADE_FINISHED 交易完成,不再允许退款。

4.2 KnowledgeDocumentStatus#

UPLOADED → QUEUED → PROCESSING → READY
                         └──────→ FAILED
READY / FAILED / 其他状态 → DELETING → 软删除
值 含义
UPLOADED PostgreSQL 和 MinIO 已接收文件,尚未成功入队。
QUEUED FastAPI 已返回 Celery jobId,等待 Worker。
PROCESSING Worker 正在解析、分片、向量化或写 Chroma。
READY 向量完整写入并通过数量校验,可以检索。
FAILED 入库失败,可由用户或恢复任务重试。
DELETING 正在清理 Chroma、MinIO 和业务记录。

4.3 Agent 相关枚举#

枚举 值 含义
AiConversationStatus ACTIVE、ARCHIVED 活跃会话或已归档会话。
AiConversationScene learning_coach、knowledge_tutor、writing_coach 学习教练、私人资料辅导、写作教练。
AiMessageRole USER、ASSISTANT、SYSTEM、TOOL 消息角色。
AiRunStatus RUNNING、COMPLETED、FAILED、CANCELLED、TIMED_OUT 一次 Agent 执行的生命周期。
AgentActionType CREATE_STUDY_PLAN、COMPLETE_STUDY_TASK 当前支持的高影响写操作。
AgentActionStatus PENDING、CONFIRMED、REJECTED、EXPIRED、EXECUTED、FAILED 待确认、已确认、拒绝、过期、已执行、执行失败。

典型 Action 状态流:

PENDING → CONFIRMED → EXECUTED
   ├────→ REJECTED
   └────→ EXPIRED
CONFIRMED → FAILED(达到重试上限或永久失败)

4.4 学习相关枚举#

枚举 值 含义
StudyPlanStatus PENDING_CONFIRMATION、ACTIVE、COMPLETED、CANCELLED 计划等待确认、执行中、已完成、已取消。
StudyTaskType VOCABULARY、READING、WRITING、QUIZ、REVIEW 词汇、阅读、写作、测验、复习任务。
StudyTaskStatus PENDING、COMPLETED、SKIPPED 待执行、已完成、已跳过。
QuizMode DUE_REVIEW、WEAK_WORDS 从到期复习词或薄弱词生成测验。

5. 用户与词汇表#

5.1 User - 用户主表#

源码:server/prisma/schema.prisma:100。

字段 类型/约束 含义
id String,PK,cuid() 用户唯一 ID,也是其他业务表最主要的租户隔离键。
name String 用户昵称/姓名。
email String?,unique 可选邮箱;非空时全局唯一。PostgreSQL 唯一索引允许多个 NULL。
phone String,unique 登录手机号,全局唯一。
address String? 用户地址。
password String 登录密码字段。当前代码以明文保存/比较,必须改为 Argon2/bcrypt 哈希。
avatar String? 头像对象路径/访问路径。
bio String? 个性签名。
isTimingTask Boolean,默认 false 是否启用定时学习提醒/任务。
timingTaskTime String,默认 00:00:00 定时任务时间;当前是字符串而非数据库 time 类型。
wordNumber Int,默认 0 已掌握词数量的冗余计数,用于快速展示。
dayNumber Int,默认 0 学习/打卡天数的冗余计数。
createdAt DateTime,默认 now 注册时间。
updatedAt DateTime,自动更新 用户记录最后修改时间。
lastLoginAt DateTime? 最近登录时间。

关系数组 wordBookRecords、paymentRecords、courseRecords、visitors、knowledgeDocuments、aiConversations、aiRuns、agentActions、studyPlans、studyTasks、quizAttempts、quizSessions 都是 Prisma 导航字段,不是物理列。

关联表#

关联表 关系 关联字段 删除 User 时
WordBookRecord 一对多;一个用户有多条单词掌握记录 WordBookRecord.userId → User.id Cascade
PaymentRecord 一对多;一个用户有多笔支付订单 PaymentRecord.userId → User.id Cascade
CourseRecord 一对多;一个用户有多条课程购买关系 CourseRecord.userId → User.id Cascade
Visitor 一对多且外键可空;匿名访客登录后可绑定用户 Visitor.userId → User.id Cascade
KnowledgeDocument 一对多;一个用户有多份私人文档 KnowledgeDocument.userId → User.id Cascade
AiConversation 一对多;一个用户有多个 Agent 会话 AiConversation.userId → User.id Cascade
AiRun 一对多;冗余保存发起用户,便于限流和恢复 AiRun.userId → User.id Cascade
AgentAction 一对多;只有所属用户可确认操作 AgentAction.userId → User.id Cascade
StudyPlan 一对多;一个用户有多份历史计划 StudyPlan.userId → User.id Cascade
StudyTask 一对多;便于不联表直接查询用户任务 StudyTask.userId → User.id Cascade
QuizSession 一对多;用户生成的待提交测验 QuizSession.userId → User.id Cascade
QuizAttempt 一对多;用户已经提交的测验结果 QuizAttempt.userId → User.id Cascade

User 还会通过 WordBookRecord 间接关联 WordBook,通过 CourseRecord 间接关联 Course 和 PaymentRecord。这些属于中间表关系,不是 User 表上的直接外键。

5.2 WordBook - 单词主数据表#

源码:server/prisma/schema.prisma:156。

字段 类型/约束 含义
id String,PK,cuid() 单词记录 ID。
word String 单词文本。
phonetic String? 音标。
definition String? 英文释义。
translation String? 中文翻译。
pos String? 词性。
collins String? 柯林斯等级/标记。
oxford String? 牛津词典相关标记。
tag String? 业务标签。
bnc String? BNC 语料词频/排名。
frq String? 通用词频字段;当前是字符串,排序时要注意字典序与数值序差异。
exchange String? 词形变化、同义/交换信息。
gk Boolean? 是否属于高考词库。
zk Boolean? 是否属于中考词库。
gre Boolean? 是否属于 GRE 词库。
toefl Boolean? 是否属于 TOEFL 词库。
ielts Boolean? 是否属于 IELTS 词库。
cet6 Boolean? 是否属于 CET-6 词库。
cet4 Boolean? 是否属于 CET-4 词库。
ky Boolean? 是否属于考研词库。
createdAt DateTime 创建时间。
updatedAt DateTime 修改时间。

索引:word、tag、word + tag。这些索引服务单词搜索与标签筛选。考试标签使用多个布尔列,适合固定标签集;若标签不断增长,可改为标签表和多对多关系。

关联表#

关联表 关系 关联字段 删除 WordBook 时
WordBookRecord 一对多;一个单词可被多个用户学习 WordBookRecord.wordId → WordBook.id Cascade

WordBook 通过 WordBookRecord 与 User 构成多对多关系。QuizAttempt.wordIds 也会保存单词 ID 数组用于测验统计,但它不是数据库外键,只是逻辑关联。

5.3 WordBookRecord - 用户单词掌握关系表#

源码:server/prisma/schema.prisma:131。

这是 User 与 WordBook 的多对多中间表,但不仅保存关系,还保存间隔复习状态。

字段 类型/约束 含义
id String,PK 用户单词记录 ID。
userId String,FK 所属用户。
wordId String,FK 对应单词。
isMaster Boolean,默认 false 当前是否达到“已掌握”业务阈值。
masteryScore Float,默认 0 0-100 的掌握度分数;Migration 有 CHECK 约束。
correctCount Int,默认 0 历史答对次数。
wrongCount Int,默认 0 历史答错次数。
reviewCount Int,默认 0 总复习次数。
lastAnswerCorrect Boolean? 最近一次回答是否正确;与历史累计计数分开,便于立即判定薄弱词。
lastReviewedAt DateTime? 最近复习时间。
nextReviewAt DateTime? 下一次建议复习时间。
intervalDays Int,默认 1 当前复习间隔天数,数据库限制 1-365。
easeFactor Float,默认 2.5 间隔复习难度/增长系数,数据库限制 1.3-3.0。
createdAt DateTime 第一次创建记录时间。
updatedAt DateTime 最后更新掌握状态时间。

约束与索引:

  • unique(userId, wordId):一个用户对一个单词最多一条记录。
  • index(userId, nextReviewAt):查询“当前用户到期复习词”。
  • index(userId, masteryScore):查询“当前用户薄弱词”。
  • 删除用户或单词时,该关系记录级联删除。

关联表#

关联表 关系 关联字段 上游删除策略
User 多对一;每条掌握记录属于一个用户 WordBookRecord.userId → User.id 删除 User 时 Cascade
WordBook 多对一;每条掌握记录对应一个单词 WordBookRecord.wordId → WordBook.id 删除 WordBook 时 Cascade

QuizAttempt 提交后会按 wordIds 更新这张表,但二者没有外键;这是业务流程关联,不是数据库结构关联。

6. 课程与支付表#

6.1 Course - 课程主表#

源码:server/prisma/schema.prisma:219。

字段 类型/约束 含义
id String,PK 课程 ID。
name String 课程名称。
value String 课程业务值/标识,用于关联词库或前端选择。
description String? 课程描述。
teacher String 教师名称。
url String 课程封面或资源 URL。
price Decimal 课程价格,使用精确小数。
createdAt DateTime 创建时间。
updatedAt DateTime 修改时间。

关联表#

关联表 关系 关联字段 删除 Course 时
CourseRecord 一对多;课程可被多个用户购买 CourseRecord.courseId → Course.id Cascade

Course 通过 CourseRecord 间接关联 User 和 PaymentRecord,自身没有直接指向用户或订单的外键。

6.2 PaymentRecord - 支付订单表#

源码:server/prisma/schema.prisma:187。

字段 类型/约束 含义
id String,PK 内部支付记录 ID。
userId String,FK 下单用户。
tradeNo String?,index 支付宝交易号,支付完成后由第三方返回。
outTradeNo String,unique 商户侧订单号,用于回调幂等和对账。
amount Decimal 实际订单金额。
subject String 支付标题。
body String 支付说明。
tradeStatus TradeStatus,默认 NOT_PAY 订单状态。
sendPayTime DateTime? 支付成功/发送支付的业务时间。
createdAt DateTime 下单时间。
updatedAt DateTime 状态最后更新时间。

关联表#

关联表 关系 关联字段 删除 PaymentRecord 时
User 多对一;每笔支付属于一个用户 PaymentRecord.userId → User.id 删除 User 时 PaymentRecord Cascade
CourseRecord 一对多;订单可关联课程购买记录,外键可空 CourseRecord.paymentRecordId → PaymentRecord.id CourseRecord Cascade

支付订单与 Course 没有直接外键,而是通过 CourseRecord 关联,因此一笔订单对应哪些课程要查询购买关系表。

6.3 CourseRecord - 用户课程购买关系表#

源码:server/prisma/schema.prisma:204。

字段 类型/约束 含义
id String,PK 购买关系 ID。
userId String,FK 课程所属用户。
courseId String,FK 被购买课程。
isPurchased Boolean,默认 false 是否已完成购买授权。
paymentRecordId String?,FK 对应支付订单;可空。
createdAt DateTime 关系创建时间。
updatedAt DateTime 购买状态修改时间。

约束:unique(userId, courseId) 防止同一用户重复生成同一课程关系。删除用户、课程或关联支付记录时会级联删除购买关系。

关联表#

关联表 关系 关联字段 上游删除策略
User 多对一;购买关系属于一个用户 CourseRecord.userId → User.id 删除 User 时 Cascade
Course 多对一;购买关系对应一个课程 CourseRecord.courseId → Course.id 删除 Course 时 Cascade
PaymentRecord 多对一且可空;可记录授权来自哪笔订单 CourseRecord.paymentRecordId → PaymentRecord.id 删除 PaymentRecord 时 Cascade

7. 前端监控与埋点表#

7.1 Visitor - 访客表#

源码:server/prisma/schema.prisma:233。

字段 类型/约束 含义
id String,PK 访客 ID。
anonymousId String,unique 浏览器/设备匿名标识,登录前也可统计。
userId String?,FK 登录后可绑定用户;未登录时为空。
browser String? 浏览器信息。
os String? 操作系统信息。
device String? 设备类型。
createdAt DateTime 首次记录时间。
updatedAt DateTime 最近更新访客信息时间。

索引:userId 和 anonymousId。anonymousId 已是 unique,额外普通索引通常是冗余的,可在优化时评估删除。

关联表#

关联表 关系 关联字段 删除 Visitor 时
User 多对一且可空;未登录访客没有用户 Visitor.userId → User.id 删除 User 时 Visitor Cascade
PageView 一对多;访客的页面访问记录 PageView.visitorId → Visitor.id Cascade
TrackEvent 一对多;访客的自定义行为记录 TrackEvent.visitorId → Visitor.id Cascade
PerformanceEntry 一对多;访客上报的性能指标 PerformanceEntry.visitorId → Visitor.id Cascade
ErrorEntry 一对多;访客上报的前端错误 ErrorEntry.visitorId → Visitor.id Cascade

7.2 PageView - 页面访问表#

源码:server/prisma/schema.prisma:253。

字段 类型/约束 含义
id String,PK PV 记录 ID。
visitorId String,FK 访问者。
url String 完整页面 URL。
referrer String? 来源页面。
path String 路由路径,便于按页面聚合。
createdAt DateTime 访问发生时间。
updatedAt DateTime 记录更新时间。

索引:visitorId + createdAt 查某访客访问轨迹;path + createdAt 查某页面时序流量。

关联表#

关联表 关系 关联字段 删除策略
Visitor 多对一;每条 PV 属于一个访客 PageView.visitorId → Visitor.id 删除 Visitor 时 Cascade

通过 Visitor.userId 可间接关联 User;PageView 自身没有 userId 外键,因此匿名访问也能保存。

7.3 TrackEvent - 自定义行为事件表#

源码:server/prisma/schema.prisma:268。

字段 类型/约束 含义
id String,PK 事件 ID。
visitorId String,FK 触发事件的访客。
event String 事件名,如按钮点击、搜索、购买入口。
payload Json? 事件扩展参数。
url String? 事件发生页面。
createdAt DateTime 发生时间。
updatedAt DateTime 更新时间。

索引:visitorId + createdAt、event + createdAt。

关联表#

关联表 关系 关联字段 删除策略
Visitor 多对一;每条行为事件属于一个访客 TrackEvent.visitorId → Visitor.id 删除 Visitor 时 Cascade

通过 Visitor 可间接找到 User,但 payload 中即使包含业务 ID 也只是事件数据,不受数据库外键约束。

7.4 PerformanceEntry - Web 性能表#

源码:server/prisma/schema.prisma:283。

字段 类型/约束 含义
id String,PK 性能记录 ID。
visitorId String,FK 对应访客。
fp Float? First Paint,首次绘制。
fcp Float? First Contentful Paint,首次内容绘制。
lcp Float? Largest Contentful Paint,最大内容绘制。
inp Float? Interaction to Next Paint,交互响应指标。
cls Float? Cumulative Layout Shift,累计布局偏移。
createdAt DateTime 采集时间。
updatedAt DateTime 更新时间。

当前为每个指标分别建索引,并额外建五指标加时间的组合索引。组合索引列很多,实际是否命中要根据查询和 EXPLAIN ANALYZE 验证。

关联表#

关联表 关系 关联字段 删除策略
Visitor 多对一;每条性能数据属于一个访客 PerformanceEntry.visitorId → Visitor.id 删除 Visitor 时 Cascade

通过 Visitor.userId 可间接关联登录用户;未登录访客仍可产生性能记录。

7.5 ErrorEntry - 前端错误表#

源码:server/prisma/schema.prisma:303。

字段 类型/约束 含义
id String,PK 错误记录 ID。
visitorId String,FK 发生错误的访客。
error String 错误类别,如 js、promise。
message String? 错误消息。
stack String? 堆栈。需注意隐私与敏感信息脱敏。
url String? 错误页面。
createdAt DateTime 错误时间。
updatedAt DateTime 更新时间。

删除 Visitor 会级联删除四类埋点子表;删除绑定用户也会级联删除该用户关联的 Visitor 及其明细。

关联表#

关联表 关系 关联字段 删除策略
Visitor 多对一;每条错误属于一个访客 ErrorEntry.visitorId → Visitor.id 删除 Visitor 时 Cascade

通过 Visitor 可间接关联 User。url、message 和 stack 只是错误上下文字段,不会与其他业务表建立外键。

8. 知识库表#

8.1 KnowledgeDocument - 私人文档元数据表#

源码:server/prisma/schema.prisma:318。

这张表只保存“业务事实和索引状态”,不保存 PDF 文件本体,也不保存向量。

字段 类型/约束 含义
id String,PK 文档 ID,也是跨 NestJS、FastAPI、Celery、Chroma 的关联 ID。
userId String,FK 文档所有者,是租户隔离关键字段。
title String 用户可见标题。
fileName String 原始文件名。
mimeType String 经服务端验证的 MIME。
size Int,CHECK > 0 原始文件字节数。
objectKey String,unique MinIO 私有对象 Key,一般包含 userId/documentId。
status KnowledgeDocumentStatus 文档状态机。
jobId String? Celery 任务 ID。
ingestionTraceId String? 当前入库链路 Trace ID,用于拒绝旧任务的晚到回调。
chunkCount Int,默认 0 已完整写入 Chroma 的分片数,数据库限制 0-20000。
embeddingProvider String? 入库使用的向量服务类型。
embeddingModel String? 入库使用的 Embedding 模型。
indexVersion String? 分片/向量索引版本。
contentHash String 原文件 SHA-256,用于完整性校验、幂等和去重。
activeContentKey String?,unique 活跃文档去重键,值为 userId:contentHash;删除时设 NULL,允许以后重新上传。
errorCode String? 机器可读失败码。
errorMessage String? 脱敏后的用户可见/运维错误信息。
lastIntegrityCheckAt DateTime? 最近一次 PostgreSQL/MinIO/Chroma 一致性核对时间。
createdAt DateTime 上传记录创建时间。
updatedAt DateTime 状态更新时间。
deletedAt DateTime? 软删除时间。非空表示业务上已删除。

索引:

  • userId + status + createdAt:用户文档列表和 READY 白名单。
  • userId + contentHash:用户范围内容查重。
  • status + updatedAt:恢复任务扫描卡住的 QUEUED/PROCESSING/DELETING。
  • objectKey 唯一:防止两个业务记录指向同一 MinIO 对象。
  • activeContentKey 唯一:数据库层防并发重复上传。

关联表与外部存储#

关联对象 关系 关联字段 删除策略/说明
User 多对一;每份文档属于一个用户 KnowledgeDocument.userId → User.id 删除 User 时 Cascade
MinIO 对象 一对一逻辑关联;保存原始文件 objectKey 无数据库外键,由文档删除流程清理
Chroma chunks 一对多逻辑关联;保存文档分片与向量 documentId = KnowledgeDocument.id,同时带 userId/indexVersion 无数据库外键,由索引/删除/对账流程维护

这张表与 AiConversation 没有直接外键。每次 Agent 请求携带并校验 documentIds,形成“会话本轮使用哪些文档”的临时业务关联。

8.2 Chroma 向量库:Collection、记录与全部字段#

源码位置:

  • Collection 命名:server-py/app/config.py:233-250 的 Settings.chroma_collection_name。
  • Collection 创建与元数据校验:server-py/app/vectorstore/chroma_store.py:58-108 的 validate_collection()、_collection()、_existing_collection()。
  • 正式分片写入:server-py/app/vectorstore/chroma_store.py:169-233 的 ChromaStore.upsert_document()。
  • 私有检索:server-py/app/vectorstore/chroma_store.py:432-508 的 ChromaStore.search_private()。

先回答“向量库有哪些表”#

Chroma 对业务代码暴露的概念是 Collection(集合),不是 PostgreSQL 那种由本项目定义的关系表。不要把 Chroma 自己底层的内部系统表当成本项目业务表;内部表结构由 Chroma 1.5.9 管理,升级版本可能改变,本项目也没有直接查询它们。

本项目可见的向量存储对象如下:

Chroma 对象 数量/命名 保存内容 是否长期保留
当前知识库 Collection 当前配置对应 1 个;名称动态生成 所有用户的正式知识分片、Embedding、过滤 metadata,以及短暂的探针记录 是;是当前检索目标
历史受管 Collection 可能有 0 个或多个;名称使用相同前缀 旧 Embedding 模型或旧 indexVersion 产生的历史向量 迁移/重建期间可能保留;代码会跨 Collection 清理旧文档向量
knowledge_chunk 逻辑记录 每个文档有多条 正式文档分片和对应向量 是,直到文档删除或重新索引
stage1_probe 逻辑记录 健康探测时临时 1 条 用来验证写入、查询、删除和向量维度 否,探针结束后删除

这里的 knowledge_chunk 和 stage1_probe 是同一个 Collection 中由 metadata.kind 区分的两种记录,不是两个物理表,也不是两个 Collection。

Collection 名称是怎么生成的#

命名公式:

{CHROMA_COLLECTION_PREFIX}_{embedding_provider}_{model_slug}_{model_sha256前12位}_{RAG_INDEX_VERSION}

例如开发环境默认配置大致生成:

english_knowledge_dev_ollama_mxbai_embed_large_latest_c55f6eb80700_v1
名称部分 来源 用途
CHROMA_COLLECTION_PREFIX 开发默认 english_knowledge_dev;生产默认 english_knowledge_prod 区分环境,并让代码能识别哪些 Collection 属于本项目。
embedding_provider 如 ollama、openai_compatible 不同 Provider 的向量不能混用。
model_slug Embedding 模型名清洗后的可读部分 方便运维人员看出使用的模型。
model_sha256前12位 原始模型名的稳定摘要 防止两个模型名清洗后碰巧得到相同 slug。
RAG_INDEX_VERSION 默认 v1 分片规则、metadata 结构或索引方案升级时创建新版本。

这套设计意味着:不是每个用户一个 Collection,也不是每个文档一个 Collection。当前模型和索引版本下,所有用户文档共用一个 Collection,租户隔离依赖每条记录的 metadata 和服务端白名单。

Collection 自身的 metadata 字段#

创建 Collection 时,ChromaStore._collection() 会写入并在每次使用时核对以下 metadata。只要实际值和当前配置不一致就直接报错,不会拿错误维度或错误模型继续检索。

字段 类型 含义 为什么需要
embedding_provider String 生成向量的 Provider。 防止 Ollama 和 OpenAI-compatible 等不同来源混用。
embedding_model String 生成向量的模型名。 查询向量必须与文档向量使用相同模型。
embedding_dimension Int 每个向量应有多少维。 写入和查询前校验维度,防止错模型得到无意义相似度。
index_version String 当前 RAG 索引版本。 分片算法或 metadata 升级后隔离新旧索引。
hnsw:space String,固定 cosine HNSW 使用余弦距离。 与代码里的 score = 1 - distance 保持一致。

每条 Chroma 记录的顶层字段#

Chroma 的每条记录不是一行 Prisma Model,而是由下面四组数据一起组成:

顶层字段 类型 正式知识分片中的值 含义
id / ids[] String {document_id}:{chunk_index}:{content_hash前12位} 分片唯一 ID。内容或分片位置变化后 ID 会变化;相同输入重试会得到同一 ID,便于 upsert 幂等。
embedding / embeddings[] Float[] Embedding Provider 对该分片正文生成的向量 用于余弦相似度检索;长度必须等于 Collection 的 embedding_dimension。
document / documents[] String DocumentChunk.text 真正交给模型作为资料上下文的分片正文。
metadata / metadatas[] 标量键值对象 见下一张表 保存权限、文档归属、页码、哈希、版本等过滤和审计字段。

distance 不是持久化字段。它是 collection.query() 查询时由 Chroma 计算并返回的距离;项目把它换算为 score = 1 - distance,再应用最低分阈值。score 也不会写回向量库。

knowledge_chunk 正式分片的全部 metadata 字段#

metadata 字段 类型 来源 含义/用途
kind String,固定 knowledge_chunk 写入时固定 区分正式知识分片与健康探针;查询、统计、删除都会带此条件。
document_id String KnowledgeDocument.id 关联 PostgreSQL 文档;一份文档对应多条分片记录。
owner_user_id String KnowledgeDocument.userId / 当前用户 租户隔离字段;检索、统计和删除必须与当前用户一致。
visibility String,固定 PRIVATE 写入时固定 明确私人数据属性;不是 PRIVATE 的记录不会进入私人检索结果。
file_name String KnowledgeDocument.fileName 原文件名快照,返回引用时展示。
title String KnowledgeDocument.title 文档标题快照,返回引用时展示。
page Int 解析器得到的 DocumentChunk.page PDF 页码;没有页码时保存 -1,返回前转换为 null。
chunk_index Int,从 0 开始 DocumentChunk.index 分片在整份文档中的顺序,用于完整性检查。
document_chunk_count Int len(chunks) 该文档本次入库应有的总分片数;每条分片都保存一份,用于核对是否缺片。
content_hash String,SHA-256 DocumentChunk.content_hash 当前分片正文哈希;用于精确去重和构造确定性分片 ID。
document_content_hash String,SHA-256 KnowledgeDocument.contentHash 整份原文件哈希;用于确认 Chroma 分片对应的确实是当前 MinIO/PostgreSQL 版本。
ingestion_trace_id String KnowledgeDocument.ingestionTraceId / 本次任务 trace 区分不同入库批次;迟到的旧 Worker 结果不能冒充当前索引。
index_version String RAG_INDEX_VERSION 记录级索引版本快照;完整性检查要求与当前配置一致。
embedding_model String 实际 Embedding probe 的模型名 记录该批向量由什么模型生成,便于审计和排障。

注意:embedding_provider 和 embedding_dimension 保存在 Collection metadata,不在每条 knowledge_chunk metadata 中重复保存。

stage1_probe 健康探针记录字段#

ChromaStore.probe() 会临时写一条记录验证 Chroma 真的能写、能查、能删。它和正式分片共用顶层字段,但 metadata 只有三项:

字段 值/类型 含义
顶层 id 调用方传入的 probe_id 本次探针 ID。
顶层 embedding Float[] 对探针文字生成的真实向量。
顶层 document String 探针文字。
metadata.kind 固定 stage1_probe 让读探针只查询探针,不混入正式知识结果。
metadata.embedding_model String 本次探针使用的模型。
metadata.index_version String 本次探针使用的索引版本。

探针完成 delete 后还会再次 get,确认 ID 已不存在。它不是用户文档,不关联 KnowledgeDocument。

与 PostgreSQL、用户和文件的逻辑关系#

左侧对象/字段 关系 Chroma 字段 数据库外键 一致性维护方式
KnowledgeDocument.id 一对多:一份文档拆成多个 chunk metadata.document_id 无 确定性 ID、入库数量校验、删除/恢复任务。
User.id 一对多:一个用户拥有多份文档的多条 chunk metadata.owner_user_id 无 Business 先构造 READY 文档白名单,Chroma 再按 owner 过滤并在返回后复核。
KnowledgeDocument.contentHash 一对多快照 metadata.document_content_hash 无 Worker 下载 MinIO 后重算 SHA-256,完整性检查再次比对。
KnowledgeDocument.ingestionTraceId 一对多:本次入库批次 metadata.ingestion_trace_id 无 READY 前要求所有分片 trace 与当前任务一致,拒绝旧任务残留。
KnowledgeDocument.chunkCount 应等于实际记录数 metadata.document_chunk_count + 实际 ids 数量 无 写入后核对 ID 集合;后台对 READY 文档再次 count。
KnowledgeDocument.indexVersion 应等于当前索引版本 Collection metadata.index_version + record metadata.index_version 无 创建、读取、READY 回调和后台对账多层校验。
KnowledgeDocument.fileName/title 一对多展示快照 metadata.file_name / title 无 写入时复制,用于引用结果展示;PostgreSQL 仍是业务事实来源。

私有检索实际使用哪些字段#

ChromaStore.search_private() 不会只按向量相似度裸搜,而是先使用以下 metadata 过滤:

kind = knowledge_chunk
AND owner_user_id = 当前登录用户
AND visibility = PRIVATE
AND document_id IN 服务端校验后的 READY 文档白名单

Chroma 返回后,代码还会逐条再次检查 owner_user_id、visibility、kind 和 document_id,然后按 content_hash 去重、按文本相似度去近重复、按 min_score 丢掉低分结果。这样即使向量库过滤出现异常,也不会直接把其他用户或未选择文档的内容交给模型。

写入、重建和删除时怎么保证一致性#

操作 使用的方法 做法
写入/重试 ChromaStore.upsert_document() 使用确定性 chunk ID 批量 upsert;结束后重新 get,要求实际 ID 数量和集合完全一致。
确认完整入库 completed_ingestion_count() 核对 owner、PRIVATE、整文档哈希、trace、版本、总分片数以及 chunk_index = 0..N-1。
当前 Collection 删除 delete_document() 删除前先核对 owner;where 同时包含 kind、document_id、owner、PRIVATE;删除后再查 remaining。
跨版本删除 delete_document_everywhere() 遍历所有同项目前缀的历史 Collection,防止旧版本向量残留。
重建后清旧版本 delete_document_other_collections() 保留当前 Collection,清理其他受管 Collection 中同一文档的向量。
READY 对账 Business reconcileReadyDocuments() + RAG count_document() 周期性核对 MinIO 原文件 SHA-256、PostgreSQL chunkCount 与 Chroma 实际数量。

最重要的面试表述:PostgreSQL 的 KnowledgeDocument 是业务事实来源;Chroma 的向量、正文分片和 metadata 都是可根据 MinIO 原文件重新生成的派生数据,所以二者采用“逻辑关联 + 状态机 + 对账/恢复”,而不是跨数据库外键或分布式事务。

9. Agent 会话表#

9.1 AiConversation - Agent 会话表#

源码:server/prisma/schema.prisma:348。

字段 类型/约束 含义
id String,PK 会话 ID,同时作为 LangGraph thread_id。
userId String,FK 会话所有者。
title String 会话标题,初始可为“新对话”,首条消息后自动更新。
scene AiConversationScene 决定系统 Prompt 和可用工具范围。
status AiConversationStatus ACTIVE 或 ARCHIVED。
lastMessageAt DateTime? 最近消息时间,用于会话排序。
checkpointDeletedAt DateTime? 归档后 LangGraph checkpoint 成功清理的时间;NULL 表示仍需恢复任务重试。
createdAt DateTime 会话创建时间。
updatedAt DateTime 会话更新时间。

索引:userId + status + lastMessageAt 用于用户会话列表;status + checkpointDeletedAt 用于扫描已归档但 checkpoint 未清理的会话。

关联表#

关联表 关系 关联字段 删除 AiConversation 时
User 多对一;会话属于一个用户 AiConversation.userId → User.id 删除 User 时 Conversation Cascade
AiMessage 一对多;会话中的消息历史 AiMessage.conversationId → AiConversation.id Cascade
AiRun 一对多;会话中的多次 Agent 执行 AiRun.conversationId → AiConversation.id Cascade
AgentAction 一对多;会话产生的待确认操作 AgentAction.conversationId → AiConversation.id Cascade
QuizSession 一对多且子表外键可空 QuizSession.conversationId → AiConversation.id SetNull
QuizAttempt 一对多且子表外键可空 QuizAttempt.conversationId → AiConversation.id SetNull
langgraph.* 逻辑一对多;图执行检查点 thread_id = AiConversation.id 无外键,归档后由 CheckpointService 清理

9.2 AiMessage - 会话消息表#

源码:server/prisma/schema.prisma:369。

字段 类型/约束 含义
id String,PK 消息 ID。
conversationId String,FK 所属会话。
runId String?,FK 产生该消息的 Run;Action 回执等消息可不绑定 Run。
role AiMessageRole USER、ASSISTANT、SYSTEM 或 TOOL。
content String 用户/助手/工具消息正文。
citations Json? 最终回答实际使用的可信引用数组。
toolCalls Json? 本次回答的工具审计摘要。
createdAt DateTime 消息时间。

关系策略:删会话时消息 Cascade;删 Run 时只把 runId SetNull,消息仍保留在会话历史中。

索引:conversationId + createdAt 用于按时间加载历史;runId 用于追踪一次执行产生的消息。

关联表#

关联表 关系 关联字段 删除策略
AiConversation 多对一;消息属于一个会话 AiMessage.conversationId → AiConversation.id 删除 Conversation 时 Cascade
AiRun 多对一且可空;消息可由某次 Run 产生 AiMessage.runId → AiRun.id 删除 Run 时 SetNull,消息保留
AgentAction 逻辑关联;执行/拒绝回执消息记录 Action ID AiMessage.toolCalls.actionId = AgentAction.id 无数据库外键;靠回执幂等逻辑维护

9.3 AiRun - 单次 Agent 执行表#

源码:server/prisma/schema.prisma:385。

一条用户消息通常对应一个 AiRun。Conversation 是长期容器,Run 是一次执行审计记录。

字段 类型/约束 含义
id String,PK Run ID。
conversationId String,FK 所属会话。
userId String,FK 发起用户;虽然可从 Conversation 推导,但冗余后方便限额和恢复扫描。
traceId String,index 跨前端、NestJS、FastAPI、Worker 的链路追踪 ID。
provider String Chat 模型 Provider。
model String 实际模型名。
status AiRunStatus 运行状态。
startedAt DateTime 开始时间。
completedAt DateTime? 结束时间。
durationMs Int?,CHECK >= 0 总耗时毫秒。
inputTokens Int?,CHECK >= 0 Provider 实际返回的输入 Token。
outputTokens Int?,CHECK >= 0 输出 Token。
totalTokens Int?,CHECK >= 0 总 Token。
cost Decimal?,CHECK >= 0 根据真实 Token 和配置单价计算的成本;未知时为 NULL。
toolStepCount Int,默认 0,CHECK >= 0 本轮完成/失败的工具步骤数。
errorCode String? 机器可读错误码。
errorMessage String? 脱敏错误说明。
createdAt DateTime 记录创建时间。
updatedAt DateTime 状态更新时间。

索引:

  • conversationId + createdAt:查看会话执行历史。
  • userId + status + createdAt:限制用户并发 Run、查用户失败记录。
  • status + startedAt:恢复长时间 RUNNING 的 stale Run。
  • traceId:运维链路查询。

关联表#

关联表 关系 关联字段 删除 AiRun 时
AiConversation 多对一;Run 属于一个会话 AiRun.conversationId → AiConversation.id 删除 Conversation 时 Run Cascade
User 多对一;冗余保存 Run 发起用户 AiRun.userId → User.id 删除 User 时 Run Cascade
AiMessage 一对多;Run 产生的消息,外键可空 AiMessage.runId → AiRun.id AiMessage.runId SetNull
AgentAction 一对多;Run 产生的待确认操作,外键可空 AgentAction.runId → AiRun.id AgentAction.runId SetNull

traceId 还会逻辑关联 FastAPI、Celery 和回调日志,但它不是这些系统之间的数据库外键。

9.4 AgentAction - 高影响操作确认表#

源码:server/prisma/schema.prisma:416。

字段 类型/约束 含义
id String,PK Action ID。
conversationId String,FK Action 所属会话。
runId String?,FK 创建 Action 的 Run;Run 删除时保留 Action 并置 NULL。
userId String,FK 只有该用户能确认或拒绝。
type AgentActionType 创建计划或完成任务。
payload Json 待执行参数和关联业务 ID。
summary String 给用户展示的确认摘要。
status AgentActionStatus Action 生命周期。
idempotencyKey String,unique 防止模型重复调用、用户重复确认和服务重试造成重复写入。
expiresAt DateTime PENDING 确认截止时间。
confirmedAt DateTime? 用户确认时间。
executedAt DateTime? 业务写入真正完成时间。
compensatedAt DateTime? 对关联计划等资源完成补偿/取消的时间。
executionAttempts Int,默认 0 执行尝试次数,数据库要求非负。
lastAttemptAt DateTime? 最近一次抢占执行租约的时间;恢复任务用它判断是否可重试。
result Json? 执行成功后的结构化结果。
errorCode String? 失败码。
errorMessage String? 脱敏失败说明。
createdAt DateTime 创建时间。
updatedAt DateTime 状态更新时间。

索引分别服务:用户待确认列表、会话 Action 历史、已确认但未执行恢复、终态 Action 补偿扫描。

关联表#

关联表 关系 关联字段 删除策略/说明
User 多对一;Action 属于一个用户 AgentAction.userId → User.id 删除 User 时 Cascade
AiConversation 多对一;Action 属于一个会话 AgentAction.conversationId → AiConversation.id 删除 Conversation 时 Cascade
AiRun 多对一且可空;记录由哪次 Run 创建 AgentAction.runId → AiRun.id 删除 Run 时 SetNull
StudyPlan 逻辑关联;创建计划 Action 保存计划 ID AgentAction.payload.planId = StudyPlan.id 无外键;拒绝/过期/失败时做补偿取消
StudyTask 逻辑关联;完成任务 Action 保存任务 ID AgentAction.payload.taskId = StudyTask.id 无外键;执行时重新校验所有权与状态
AiMessage 逻辑关联;Action 结果回执 AiMessage.toolCalls.actionId = AgentAction.id 无外键;按 actionId 防止重复回执

10. 学习计划与任务表#

10.1 StudyPlan - 学习计划表#

源码:server/prisma/schema.prisma:447。

字段 类型/约束 含义
id String,PK 计划 ID。
userId String,FK 计划所属用户。
goal String 用户学习目标。
startDate DateTime 计划开始日期。
endDate DateTime 计划结束日期,CHECK 保证不早于开始日期。
status StudyPlanStatus 等待确认、活动、完成或取消。
plan Json Agent/服务端生成的完整计划快照和展示结构。
confirmedAt DateTime? 用户确认并激活时间。
completedAt DateTime? 全部任务完成时间。
createdAt DateTime 创建时间。
updatedAt DateTime 修改时间。

普通索引 userId + status + createdAt 支持用户计划列表。Migration 还建立了部分唯一索引 StudyPlan_one_active_per_user_idx:只对 status = ACTIVE 生效,保证一个用户同时最多一个活动计划。这个部分索引无法完整表示在 Prisma Schema 中,因此看 Schema 时还必须看 Migration。

关联表#

关联表 关系 关联字段 删除 StudyPlan 时
User 多对一;计划属于一个用户 StudyPlan.userId → User.id 删除 User 时 Plan Cascade
StudyTask 一对多;计划激活后生成具体任务 StudyTask.planId → StudyPlan.id Cascade
AgentAction 逻辑关联;待确认 Action 指向计划 AgentAction.payload.planId = StudyPlan.id 无外键;Action 补偿流程负责取消计划

10.2 StudyTask - 计划任务表#

源码:server/prisma/schema.prisma:465。

字段 类型/约束 含义
id String,PK 任务 ID。
planId String,FK 所属学习计划。
userId String,FK 所属用户;虽然可从 Plan 推导,但便于按用户/日期直接查任务。
scheduledFor DateTime 计划执行日期。
type StudyTaskType 任务类别。
title String 任务标题。
description String? 任务说明。
payload Json? 与任务类型相关的结构化参数,如词汇列表、测验配置。
order Int,默认 0,CHECK >= 0 同一天任务排序。
status StudyTaskStatus 待完成、已完成或跳过。
completedAt DateTime? 完成时间。
createdAt DateTime 创建时间。
updatedAt DateTime 修改时间。

约束:unique(planId, scheduledFor, order) 防止同一计划同一天出现重复顺序;index(userId, status, scheduledFor) 支持查询某用户待办和到期任务。删除 Plan 会级联删除 Task。

关联表#

关联表 关系 关联字段 删除策略/说明
StudyPlan 多对一;任务属于一个计划 StudyTask.planId → StudyPlan.id 删除 Plan 时 Cascade
User 多对一;冗余保存任务所属用户 StudyTask.userId → User.id 删除 User 时 Cascade
AgentAction 逻辑关联;完成任务 Action 指向任务 AgentAction.payload.taskId = StudyTask.id 无外键;执行时按 userId 和状态复核

11. 测验表#

11.1 QuizSession - 未提交测验会话#

源码:server/prisma/schema.prisma:509。

字段 类型/约束 含义
id String,PK 测验会话 ID。
userId String,FK 测验所属用户。
conversationId String?,FK 若由 Agent 生成,记录来源会话;普通 Study 页面生成时可为空。
mode QuizMode,默认 WEAK_WORDS 按薄弱词或到期复习词出题。
questions Json 服务端完整题目快照,包含评分所需信息;返回前端时会移除答案。
expiresAt DateTime 会话失效时间。
submittedAt DateTime? 成功提交时间;非空后不可再次提交。
createdAt DateTime 生成时间。

索引:userId + expiresAt、userId + mode + expiresAt 查某模式未过期测验;conversationId 查 Agent 会话产生的测验。

关联表#

关联表 关系 关联字段 删除策略
User 多对一;测验会话属于一个用户 QuizSession.userId → User.id 删除 User 时 Cascade
AiConversation 多对一且可空;记录由哪个 Agent 会话生成 QuizSession.conversationId → AiConversation.id 删除 Conversation 时 SetNull
QuizAttempt 一对一;一个 Session 最多生成一个 Attempt QuizAttempt.sessionId → QuizSession.id,且 sessionId unique 已有 Attempt 时删除 Session 被 Restrict

11.2 QuizAttempt - 已提交测验结果#

源码:server/prisma/schema.prisma:486。

字段 类型/约束 含义
id String,PK 测验结果 ID。
userId String,FK 答题用户。
sessionId String,unique,FK 对应 QuizSession;唯一约束保证一个 Session 只能生成一个 Attempt。
conversationId String?,FK 来源 Agent 会话;删除会话时 SetNull,测验历史仍保留。
mode QuizMode 生成测验时的模式快照。
questions Json 提交时的题目快照,防止以后题库变化影响历史解释。
answers Json 用户答案。
score Float,CHECK 0-100 总分。
feedback Json? 逐题正确性、正确答案和反馈。
wordIds String[] 本次涉及的单词 ID 列表,便于统计与掌握度更新。
idempotencyKey String,unique 防止重复提交生成两份结果。
submittedAt DateTime 实际提交时间。
createdAt DateTime 结果记录创建时间。

删除策略:QuizAttempt.sessionId 对 Session 使用 Restrict,已有答题结果时不能删除原 Session;对 Conversation 使用 SetNull;对 User 使用 Cascade。

索引:userId + submittedAt、userId + mode + submittedAt 查近期测验;conversationId 查会话关联测验。

关联表#

关联表 关系 关联字段 删除策略/说明
User 多对一;答题结果属于一个用户 QuizAttempt.userId → User.id 删除 User 时 Cascade
QuizSession 一对一;Attempt 必须来自一个 Session QuizAttempt.sessionId → QuizSession.id 删除 Session 时 Restrict
AiConversation 多对一且可空;记录 Agent 来源会话 QuizAttempt.conversationId → AiConversation.id 删除 Conversation 时 SetNull
WordBook 逻辑多对多;保存本次涉及的单词 ID QuizAttempt.wordIds[] 包含 WordBook.id 不是外键,不自动级联
WordBookRecord 业务流程关联;提交后更新掌握度和复习数据 通过 userId + wordIds[] 定位 不是外键,由提交事务更新

12. 外键与删除策略汇总#

父表 子表 外键 删除父表时
User WordBookRecord userId Cascade
WordBook WordBookRecord wordId Cascade
User PaymentRecord userId Cascade
User CourseRecord userId Cascade
Course CourseRecord courseId Cascade
PaymentRecord CourseRecord paymentRecordId Cascade
User Visitor userId Cascade
Visitor 四类埋点明细 visitorId Cascade
User KnowledgeDocument userId Cascade
User AiConversation userId Cascade
AiConversation AiMessage conversationId Cascade
AiConversation AiRun conversationId Cascade
AiRun AiMessage runId SetNull
AiConversation AgentAction conversationId Cascade
AiRun AgentAction runId SetNull
User StudyPlan userId Cascade
StudyPlan StudyTask planId Cascade
QuizSession QuizAttempt sessionId Restrict
AiConversation QuizSession/QuizAttempt conversationId SetNull

理解原则:

  • 强归属数据随父记录删除,使用 Cascade。
  • 有独立审计价值的数据在上游执行记录删除后仍保留,使用 SetNull。
  • 已提交测验必须保留题目来源,Session → Attempt 使用 Restrict。
  • Chroma metadata.document_id / owner_user_id 与 PostgreSQL 只是逻辑关联,没有外键和 Cascade;删除必须由应用显式清理,并由恢复任务核对。

13. 数据库级 CHECK 与特殊索引#

这些约束定义在 Migration,Prisma Schema 不能完整表达,面试时这是一个加分点。

来源:server/prisma/migrations/20260722190000_agent_rag_learning/migration.sql:192-248。

对象 约束
WordBookRecord.masteryScore 0-100。
correctCount/wrongCount/reviewCount 不得小于 0。
intervalDays 1-365。
easeFactor 1.3-3.0。
KnowledgeDocument.size 必须大于 0。
KnowledgeDocument.chunkCount 0-20000。
AiRun.durationMs NULL 或非负。
AiRun Token 三字段 NULL 或非负。
AiRun.toolStepCount 非负。
AiRun.cost NULL 或非负。
AgentAction.executionAttempts 非负。
StudyPlan endDate >= startDate。
StudyTask.order 非负。
QuizAttempt.score 0-100。
StudyPlan_one_active_per_user_idx WHERE status = 'ACTIVE' 的部分唯一索引,一个用户最多一个活动计划。

服务端校验改善用户体验,数据库约束负责最后一道一致性防线。二者应同时存在。

14. LangGraph 独立 Schema#

Prisma Migration server/prisma/migrations/20260722122000_add_langgraph_schema/migration.sql:1 只创建 langgraph schema。Agent 启动时,server/apps/ai/src/llm/llm.config.ts:31 的 createCheckpoint() 调用 PostgresSaver.setup() 自动创建以下表:

表 作用 关键字段 关联表/对象
langgraph.checkpoint_migrations LangGraph 自己的表结构版本。 v。 不关联业务表,只供 PostgresSaver 判断内部 Migration 版本。
langgraph.checkpoints 每个 thread 的 checkpoint 主记录。 thread_id、checkpoint_ns、checkpoint_id、parent_checkpoint_id、checkpoint JSONB、metadata JSONB。 thread_id 逻辑关联 AiConversation.id;parent_checkpoint_id 逻辑关联同一 thread/namespace 的上一条 checkpoints.checkpoint_id,但没有业务外键。
langgraph.checkpoint_blobs 各 Channel 各版本的序列化状态 Blob。 thread_id、checkpoint_ns、channel、version、type、blob BYTEA。 通过 thread_id + checkpoint_ns 与 checkpoints 属于同一执行线程;具体版本由 checkpoint JSON 中的 channel version 引用,无 Prisma 关系。
langgraph.checkpoint_writes 图节点执行期间的中间写入。 thread_id、checkpoint_ns、checkpoint_id、task_id、idx、channel、type、blob。 thread_id + checkpoint_ns + checkpoint_id 逻辑关联 checkpoints;thread_id 同时逻辑关联 AiConversation.id,无 Prisma 外键。

项目把 AiConversation.id 作为 LangGraph thread_id。业务会话归档后,CheckpointService.deleteThread() 删除对应 checkpoint、blob 和 pending writes;checkpointDeletedAt 用来记录是否清理成功。

这四张表由第三方库管理,不应加入 Prisma Model,也不应手工修改字段。

15. 三条核心数据流#

15.1 用户完成测验#

QuizSession(题目快照)
  → QuizAttempt(答案、分数、反馈,sessionId 唯一)
  → WordBookRecord(正误计数、掌握度、间隔、下次复习)
  → User.wordNumber(已掌握词冗余计数)

事务和 Advisory Lock 保证重复提交不会重复累计,入口在 study-domain.service.ts:292。

15.2 Agent 创建学习计划#

AiConversation
  → AiRun
  → StudyPlan(PENDING_CONFIRMATION) + StudyTask[]
  → AgentAction(PENDING,payload 内含 planId)
  → 用户确认
  → AgentAction(EXECUTED) + StudyPlan(ACTIVE)
  → AiMessage(Action 执行回执)

AgentAction.idempotencyKey、事务、Advisory Lock 和部分唯一索引共同防止计划重复激活。

15.3 文档上传并入库#

KnowledgeDocument(UPLOADED)
  → MinIO 原始对象
  → KnowledgeDocument(QUEUED, jobId, ingestionTraceId)
  → Worker PROCESSING 回调
  → Chroma 当前 Collection
      └─ ids + embeddings + documents + metadatas
         └─ kind/document_id/owner_user_id/visibility/page/hash/trace/version...
  → Worker READY 回调
  → KnowledgeDocument(READY, chunkCount, model, indexVersion)

PostgreSQL 决定文档归属和是否可检索;MinIO 与 Chroma 不通过数据库外键连接,而是共同使用 documentId/userId/contentHash 做跨存储关联。

16. 常用查询为什么能命中索引#

业务查询 对应索引
查询用户到期复习词 WordBookRecord(userId, nextReviewAt)
查询用户薄弱词 WordBookRecord(userId, masteryScore)
查询用户 READY 文档 KnowledgeDocument(userId, status, createdAt)
扫描卡住的索引任务 KnowledgeDocument(status, updatedAt)
加载会话历史 AiMessage(conversationId, createdAt)
限制用户并发 Run AiRun(userId, status, createdAt)
扫描 stale Run AiRun(status, startedAt)
查询待确认 Action AgentAction(userId, status, expiresAt)
恢复已确认 Action AgentAction(status, confirmedAt)
查某日未完成任务 StudyTask(userId, status, scheduledFor)
查近期某模式测验 QuizAttempt(userId, mode, submittedAt)
查页面时序流量 PageView(path, createdAt)

组合索引遵循左前缀原则。例如 (userId, status, createdAt) 可以高效支持 userId 或 userId + status 开头的条件,但通常不能单独高效支持只查 createdAt。

17. 面试时容易被追问的设计点#

  1. 为什么 AiRun、AgentAction、StudyTask 都冗余保存 userId?答题重点:租户过滤、限额、恢复扫描和索引效率,同时必须用事务保证与父对象用户一致。
  2. 为什么 StudyPlan.plan 已有 JSON,还需要 StudyTask 表?答题重点:JSON 是完整快照/展示结构,Task 是可查询、可更新、可约束的执行实体。
  3. 为什么 QuizSession.questions 和 QuizAttempt.questions 都保存?答题重点:Session 用于服务端防篡改评分,Attempt 用于不可变历史审计。
  4. 为什么 AiMessage.runId 与 AgentAction.runId 是可空并使用 SetNull?答题重点:会话历史和 Action 审计生命周期长于某个 Run。
  5. 为什么不用 PostgreSQL 存向量?当前选择是 Chroma 职责隔离;但 pgvector 可以减少组件数,需要按规模、检索能力、运维成本权衡。
  6. 为什么软删除 KnowledgeDocument?跨 PostgreSQL/MinIO/Chroma 删除无法用单库事务,需要 DELETING + deletedAt + 恢复任务 实现最终一致性。
  7. 为什么不是“每个用户一个 Chroma Collection”?共用按模型/版本生成的 Collection 可以减少 Collection 管理成本;租户隔离靠服务端 READY 白名单和 owner_user_id + visibility + document_id 多层过滤,但必须做好返回后二次校验。
  8. Chroma 记录为什么同时存 content_hash 和 document_content_hash?前者标识单个分片并去重,后者确认整份文档版本,解决的问题不同。
  9. activeContentKey 为什么比“先查再插”更可靠?唯一约束能挡住并发请求的竞态。
  10. Prisma Schema 为什么不是数据库真相的全部?CHECK、部分索引、LangGraph 自动表都只在 Migration/第三方 setup 中体现;Chroma Collection schema 更不在 Prisma 中。
  11. 哪些字段是冗余数据?User.wordNumber、AiRun.userId、StudyTask.userId、模型/索引快照、Chroma 的 title/fileName 等;冗余换查询效率,但要设计一致性修复。
  12. 当前最严重的数据库安全问题是什么?User.password 尚未哈希。

18. 快速记忆总结#

  • User 是租户根。
  • WordBookRecord 是“用户 × 单词”的学习状态。
  • CourseRecord 是“用户 × 课程”的购买状态。
  • Visitor 是埋点根,四张明细表记录 PV、行为、性能和错误。
  • KnowledgeDocument 是私人文档的业务元数据与状态机,不存文件和向量。
  • Chroma 没有本项目定义的关系表;当前模型/版本对应一个 Collection,正式记录由 ids + embeddings + documents + metadatas 组成,knowledge_chunk 与 stage1_probe 用 metadata.kind 区分。
  • Chroma 通过 metadata.document_id 逻辑关联 KnowledgeDocument.id,通过 owner_user_id 逻辑关联 User.id;没有数据库外键。
  • AiConversation 是长期对话,AiRun 是单次执行,AiMessage 是消息历史。
  • AgentAction 是模型建议与真实写操作之间的安全闸门。
  • StudyPlan 是计划头,StudyTask 是可执行任务。
  • QuizSession 是待提交题目,QuizAttempt 是不可重复的最终答题结果。
  • langgraph.* 四张表保存 Agent checkpoint,由 LangGraph 库管理。
文档目录