Files
ai_soc_sw/docs/02_系统架构/数据模型.md
T
tupingr e3f4af9c0c docs(arch): 旧架构合并 — 30项决策落地,5份文档升级至v0.4.0
- 总体架构:新增打印/图像预处理/双飞轮/三环境部署
- 技术选型:调整决策理由(Coze沙盒自动化测试),新增Sharp+PDFKit
- 数据模型:新增code/role/question_type+print_tasks+audit_logs,ID+code并存
- 模块设计:新增Image/Print模块,推荐两阶段匹配(关键词粗筛→AI精排)
- PRD:目标用户扩展为学生+家长,新增PDF打印,年级聚焦小初,图像预处理流程
- ADR-010:题库抽象层Adapter Pattern

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:00:52 +08:00

17 KiB
Raw Blame History

数据模型

版本: 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 查询某用户下的完整子树(老师查看全班学生、机构查看所有老师)

-- 查询邀请人的一级下线
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 结构:

{
  "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 薄弱项)

示例数据:

{
  "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 结构:

[
  {
    "knowledge_point_id": 1021,
    "name": "二次函数顶点坐标",
    "error_count": 5,
    "weight": 0.85,
    "trend": "up"
  }
]

error_type_distribution JSONB 结构:

{
  "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_tasksPDF 导入任务)

自有题库 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 示例

// 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