# 数据模型 > 版本: v0.4.0 | 作者: Arch AI | 基于 PRD v0.4.0 + 旧架构合并 --- ## 1. 实体关系图 (ER) ``` User ──< UserRelation (邀请树: inviter → invitee) │ User ──< ErrorItem >── Subject │ │ │ ├──< CorrectionLog (AI 值 vs 用户修正) │ └── KnowledgePoint (多对多) │ └──< AnalysisReport ErrorItem >──< KnowledgePoint (error_knowledge_points) PracticeRecommendation >──< KnowledgePoint └──< Question (题库题目, 多对多) Question ──< KnowledgePoint (question_knowledge_points) Question ── Subject ``` ## 2. 表定义 ### 2.1 users | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | UUID | PK, DEFAULT gen_random_uuid() | 用户 ID | | wx_openid | VARCHAR(128) | UNIQUE, NOT NULL | 微信 OpenID | | nickname | VARCHAR(64) | | 微信昵称 | | avatar_url | VARCHAR(512) | | 头像 URL | | grade | VARCHAR(16) | | 年级,如"初中二年级" | | role | VARCHAR(16) | NOT NULL, DEFAULT 'student' | 角色: student/parent/teacher/admin/super_admin (Phase 3 启用后台角色) | | invitation_code | VARCHAR(16) | UNIQUE | 个人邀请码(6 位字母数字,注册时生成) | | created_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | | updated_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | ### 2.2 user_relations 用户邀请树结构。记录邀请链,支持树状用户群。 | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | UUID | PK | | | inviter_id | UUID | FK → users.id, NOT NULL | 邀请人 | | invitee_id | UUID | FK → users.id, UNIQUE, NOT NULL | 被邀请人(一个用户只能被一个人邀请) | | relation_type | VARCHAR(16) | NOT NULL, DEFAULT 'student' | student/parent/colleague | | created_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | **索引**: `(inviter_id)` — 查询某用户邀请的所有人;`(invitee_id)` UNIQUE — 确保一对一邀请链 **树查询**: 通过递归 CTE 查询某用户下的完整子树(老师查看全班学生、机构查看所有老师) ```sql -- 查询邀请人的一级下线 SELECT * FROM user_relations WHERE inviter_id = $1; -- 递归查询完整子树(所有下级) WITH RECURSIVE tree AS ( SELECT invitee_id, inviter_id, 1 AS depth FROM user_relations WHERE inviter_id = $1 UNION ALL SELECT ur.invitee_id, ur.inviter_id, t.depth + 1 FROM user_relations ur JOIN tree t ON ur.inviter_id = t.invitee_id WHERE t.depth < 10 ) SELECT * FROM tree; ``` **典型结构**: ``` 机构负责人 (invitation_code: ABC123) ├── 老师A (受邀) │ ├── 学生1 (受邀) │ └── 学生2 (受邀) └── 老师B (受邀) ├── 学生3 (受邀) └── 学生4 (受邀) ``` ### 2.3 subjects | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | SERIAL | PK | 学科 ID | | name | VARCHAR(32) | UNIQUE, NOT NULL | 数学/英语/语文/... | | icon | VARCHAR(32) | | 图标标识 | | sort_order | INT | DEFAULT 0 | 排序 | **预置数据**: 数学、英语(首发)、语文、物理、化学、生物、地理、历史、政治(后续扩展) ### 2.4 knowledge_points | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | SERIAL | PK | 知识点 ID(内部关联用) | | code | VARCHAR(32) | UNIQUE | 业务编码,如 `G5-MATH-0201`(跨环境稳定,API 对外暴露) | | name | VARCHAR(128) | NOT NULL | 如"二次函数顶点式" | | subject_id | INT | FK → subjects.id, NOT NULL | 所属学科 | | parent_id | INT | FK → knowledge_points.id | 父级知识点(树形结构) | | level | SMALLINT | NOT NULL, DEFAULT 1 | 层级深度 | | sort_order | INT | DEFAULT 0 | 同级排序 | **索引**: `(subject_id, parent_id)`, `(name)` GIN trigram(模糊搜索) **示例数据(数学 + 英语双学科首发)**: **编码规则**: `{Grade}-{Subject}-{Category}{Detail}`,如 `G5-MATH-0201` = 五年级·数学·02 大类·01 知识点。ID 用于内部关联,code 跨环境稳定,API 对外暴露。 ``` 数学 (id=1) ├── 代数 (id=10, code=G5-MATH-0100, parent=NULL) │ ├── 一次函数 (id=101, code=G8-MATH-0101, parent=10) │ │ ├── 斜率与截距 (id=1011, code=G8-MATH-0101-1, parent=101) │ │ └── 一次函数应用 (id=1012, code=G8-MATH-0101-2, parent=101) │ └── 二次函数 (id=102, code=G9-MATH-0102, parent=10) │ ├── 顶点坐标 (id=1021, code=G9-MATH-0102-1, parent=102) │ └── 图像性质 (id=1022, code=G9-MATH-0102-2, parent=102) └── 几何 (id=20, code=G5-MATH-0200, parent=NULL) ├── 三角形 (id=201, code=G7-MATH-0201, parent=20) └── 圆 (id=202, code=G9-MATH-0202, parent=20) 英语 (id=2) ├── 语法 (id=200, code=G7-ENG-0100, parent=NULL) │ ├── 时态 (id=2001, code=G7-ENG-0101, parent=200) │ │ ├── 一般现在时 (id=20011, code=G7-ENG-0101-1, parent=2001) │ │ └── 现在完成时 (id=20012, code=G8-ENG-0101-2, parent=2001) │ ├── 从句 (id=2002, code=G8-ENG-0102, parent=200) │ │ ├── 定语从句 (id=20021, code=G9-ENG-0102-1, parent=2002) │ │ └── 状语从句 (id=20022, code=G8-ENG-0102-2, parent=2002) │ └── 被动语态 (id=2003, code=G8-ENG-0103, parent=200) ├── 词汇 (id=300, code=G7-ENG-0200, parent=NULL) │ ├── 词义辨析 (id=3001, code=G7-ENG-0201, parent=300) │ └── 固定搭配 (id=3002, code=G8-ENG-0202, parent=300) └── 阅读 (id=400, code=G7-ENG-0300, parent=NULL) ├── 主旨大意 (id=4001, code=G7-ENG-0301, parent=400) └── 细节理解 (id=4002, code=G7-ENG-0302, parent=400) ``` ### 2.6 error_items | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | UUID | PK | 错题 ID | | user_id | UUID | FK → users.id, NOT NULL | 所属用户 | | subject_id | INT | FK → subjects.id | 学科 | | image_url | VARCHAR(512) | NOT NULL | 原始图片 URL | | thumbnail_url | VARCHAR(512) | | 缩略图 URL | | question_text | TEXT | | AI 提取的题目文本 | | wrong_answer | TEXT | | 错误答案 | | correct_answer | TEXT | | 正确答案(可选) | | error_type | VARCHAR(32) | | 错误类型 | | difficulty | VARCHAR(8) | | 难度: basic/medium/advanced | | verification_status | VARCHAR(16) | NOT NULL, DEFAULT 'raw' | raw/reviewed/corrected/stale | | ai_confidence | JSONB | | AI 各字段置信度 | | note | TEXT | | 学生备注 | | created_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | | updated_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | **索引**: `(user_id, created_at DESC)`, `(user_id, subject_id)`, `(user_id, error_type)`, `(user_id, verification_status)` **verification_status 枚举**: - `raw` — AI 原始结果,用户尚未确认,不计入分析 - `reviewed` — 用户已确认(一键确认或查看后确认) - `corrected` — 用户修正了至少一个 AI 字段 - `stale` — 30 天未确认,系统标记,可恢复为 raw **ai_confidence JSONB 结构**: ```json { "question_text": 0.92, "subject_id": 0.88, "knowledge_points": { "1021": 0.95, "1022": 0.73 }, "error_type": 0.81, "correct_answer": 0.55 } ``` **error_type 枚举**: - `knowledge_gap` — 知识点欠缺 - `careless` — 粗心失误 - `misread` — 审题偏差 - `concept_confusion` — 概念混淆 ### 2.7 error_knowledge_points 错题与知识点的多对多关联表。 | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | SERIAL | PK | | | error_item_id | UUID | FK → error_items.id, NOT NULL | 错题 | | knowledge_point_id | INT | FK → knowledge_points.id, NOT NULL | 知识点 | | relevance | SMALLINT | DEFAULT 100 | 关联度 (0-100),主关联=100 | **唯一约束**: `(error_item_id, knowledge_point_id)` ### 2.8 correction_logs 用户修正 AI 识别结果的记录。P02 阶段用于微调自有模型。 | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | UUID | PK | | | error_item_id | UUID | FK → error_items.id, NOT NULL | 所属错题 | | field_name | VARCHAR(32) | NOT NULL | 修正的字段名 | | ai_value | JSONB | NOT NULL | AI 原始值 | | user_value | JSONB | NOT NULL | 用户修正值 | | ai_confidence | REAL | NOT NULL | 该字段 AI 置信度 | | corrected_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | 修正时间 | **索引**: `(error_item_id)`, `(field_name)`(P02 阶段按字段统计 AI 薄弱项) **示例数据**: ```json { "error_item_id": "uuid", "field_name": "knowledge_points", "ai_value": [1021], "user_value": [1022], "ai_confidence": 0.72, "corrected_at": "2026-05-26T10:30:00Z" } ``` ### 2.9 analysis_reports | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | UUID | PK | 报告 ID | | user_id | UUID | FK → users.id, NOT NULL | | | period_start | DATE | NOT NULL | 报告周期开始 | | period_end | DATE | NOT NULL | 报告周期结束 | | weak_points | JSONB | NOT NULL | 薄弱点数据 | | error_type_distribution | JSONB | NOT NULL | 错误类型分布 | | trend | VARCHAR(8) | | up/flat/down(与上周期对比) | | generated_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | **weak_points JSONB 结构**: ```json [ { "knowledge_point_id": 1021, "name": "二次函数顶点坐标", "error_count": 5, "weight": 0.85, "trend": "up" } ] ``` **error_type_distribution JSONB 结构**: ```json { "knowledge_gap": 12, "careless": 5, "misread": 3, "concept_confusion": 2 } ``` **注意**: AnalysisReport 仅统计 `verification_status != 'raw'` 的错题,确保分析基于用户确认过的数据。 ### 2.10 question_bank(题库抽象层) 支持多题库源的统一抽象。自有题库(PDF 录入)和第三方题库(作业帮 API)通过统一接口接入。 **2.10.1 questions(题库题目)** | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | UUID | PK | 题目 ID | | source | VARCHAR(16) | NOT NULL | 来源: self_built / zuoyebang / future_source | | external_id | VARCHAR(128) | | 外部题库的原始 ID(自建为空) | | subject_id | INT | FK → subjects.id, NOT NULL | 所属学科 | | question_type | VARCHAR(16) | NOT NULL, DEFAULT 'choice' | 题型: choice/fill/calculation/word_problem/geometry/composite | | question_text | TEXT | NOT NULL | 题目文本 | | options | JSONB | | 选项(如 ABCD) | | answer | TEXT | NOT NULL | 正确答案 | | analysis | TEXT | | 解析 | | difficulty | SMALLINT | DEFAULT 3 | 难度 1-5(1 基础 → 5 综合创新) | | cognitive_level | SMALLINT | | 认知层次 1-6(布鲁姆: 记忆/理解/应用/分析/评价/创造,预留) | | grade | VARCHAR(16) | | 适用年级 | | variation_params | JSONB | | 变式参数(数字替换、条件变换,预留,Phase 3 启用) | | status | VARCHAR(16) | DEFAULT 'active' | active/inactive | | created_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | **索引**: `(subject_id, knowledge_point_id)`, `(source)`, `(grade)` **2.10.2 question_knowledge_points** 题目与知识点的多对多关联(同 error_knowledge_points 模式)。 **2.10.3 pdf_import_tasks(PDF 导入任务)** 自有题库 PDF 导入的异步任务管理。 | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | UUID | PK | 任务 ID | | uploaded_by | UUID | FK → users.id, NOT NULL | 上传者 | | file_url | VARCHAR(512) | NOT NULL | PDF 文件 URL | | subject_id | INT | FK → subjects.id | 目标学科 | | status | VARCHAR(16) | NOT NULL, DEFAULT 'pending' | pending/parsing/ai_extracting/review/complete/failed | | parsed_count | INT | DEFAULT 0 | 解析出的题目数 | | imported_count | INT | DEFAULT 0 | 成功导入的题目数 | | error_log | JSONB | | 错误信息 | | created_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | **PDF 导入管线**: ``` PDF 上传 → OCR 解析 → AI 结构化提取(题目/选项/答案/知识点) → 人工审核(校验解析结果) → 入库(source=self_built) ``` ### 2.11 print_tasks(打印/PDF 输出任务)[P0] | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | UUID | PK | 任务 ID | | user_id | UUID | FK → users.id, NOT NULL | 创建者 | | error_item_ids | UUID[] | NOT NULL | 选中的错题 ID 列表 | | output_mode | VARCHAR(8) | NOT NULL, DEFAULT 'pdf' | pdf / image | | status | VARCHAR(16) | NOT NULL, DEFAULT 'pending' | pending/generating/complete/expired/failed | | file_url | VARCHAR(512) | | 生成的 PDF/图片下载链接 | | expires_at | TIMESTAMPTZ | | 下载链接过期时间(24h) | | created_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | **清晰度优先级**: 结构化内容(题库匹配)> 增强图片(经图像预处理)> 原始图片 ### 2.12 audit_logs(操作审计,预留 Phase 3) Phase 3 引入完整数据主权方案时建表。字段预留: id, user_id, action, resource_type, resource_id, detail (JSONB), ip_address, created_at。 ### 2.13 practice_recommendations (P1) | 列 | 类型 | 约束 | 说明 | |----|------|------|------| | id | UUID | PK | 推荐记录 ID | | user_id | UUID | FK → users.id, NOT NULL | | | knowledge_point_ids | INT[] | NOT NULL | 目标知识点 | | question_refs | JSONB | NOT NULL | 推荐题目引用 | | completed | BOOLEAN | DEFAULT false | 是否完成 | | score | SMALLINT | | 得分 | | generated_at | TIMESTAMPTZ | NOT NULL, DEFAULT NOW() | | ## 3. Drizzle Schema 示例 ```typescript // src/server/src/db/schema.ts import { pgTable, uuid, varchar, text, integer, smallint, date, jsonb, timestamp, boolean, uniqueIndex, index } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: uuid('id').defaultRandom().primaryKey(), wxOpenid: varchar('wx_openid', { length: 128 }).unique().notNull(), nickname: varchar('nickname', { length: 64 }), avatarUrl: varchar('avatar_url', { length: 512 }), grade: varchar('grade', { length: 16 }), role: varchar('role', { length: 16 }).default('student').notNull(), invitationCode: varchar('invitation_code', { length: 16 }).unique(), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), }); export const errorItems = pgTable('error_items', { id: uuid('id').defaultRandom().primaryKey(), userId: uuid('user_id').references(() => users.id).notNull(), subjectId: integer('subject_id').references(() => subjects.id), imageUrl: varchar('image_url', { length: 512 }).notNull(), thumbnailUrl: varchar('thumbnail_url', { length: 512 }), questionText: text('question_text'), wrongAnswer: text('wrong_answer'), correctAnswer: text('correct_answer'), errorType: varchar('error_type', { length: 32 }), difficulty: varchar('difficulty', { length: 8 }), verificationStatus: varchar('verification_status', { length: 16 }).default('raw').notNull(), aiConfidence: jsonb('ai_confidence'), note: text('note'), createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), }, (table) => ({ userIdCreatedIdx: index('idx_error_items_user_created').on(table.userId, table.createdAt.desc()), userIdSubjectIdx: index('idx_error_items_user_subject').on(table.userId, table.subjectId), userIdStatusIdx: index('idx_error_items_user_status').on(table.userId, table.verificationStatus), })); export const correctionLogs = pgTable('correction_logs', { id: uuid('id').defaultRandom().primaryKey(), errorItemId: uuid('error_item_id').references(() => errorItems.id).notNull(), fieldName: varchar('field_name', { length: 32 }).notNull(), aiValue: jsonb('ai_value').notNull(), userValue: jsonb('user_value').notNull(), aiConfidence: real('ai_confidence').notNull(), correctedAt: timestamp('corrected_at', { withTimezone: true }).defaultNow().notNull(), }, (table) => ({ errorItemIdIdx: index('idx_correction_logs_error_item').on(table.errorItemId), fieldNameIdx: index('idx_correction_logs_field').on(table.fieldName), })); ``` ## 4. 数据量预估 | 表 | MVP 年末预估 | 增长速度 | |----|-------------|----------| | users | 10K | 线性(含邀请裂变) | | user_relations | ~10K | 每用户 1 条邀请关系 | | error_items | 500K | 每用户日均 2-3 道 | | knowledge_points | ~5K (预置) | 版本更新追加 | | analysis_reports | 40K | 每用户每周 1 份 | | error_knowledge_points | 1M | 每错题 1-3 条关联 | | correction_logs | ~200K | 每错题平均修正 0.5-1 个字段 | | questions | 50K+ | 自有 PDF 导入 + 作业帮 API 同步 | | question_knowledge_points | 100K | 每题 1-3 条关联 | MVP 单表最大 500K 行,PostgreSQL 单实例完全可承载,无需分库分表。 --- *关联: 模块设计.md → 总体架构.md*