旧系统 vs V6 新设计 —— 题库层面差异对比
本文档对比「池馆」旧 API 系统与 V6 题库分类设计方案,在题库数据结构层面的主要差异。 基于旧系统源码 src/database/schema.ts、src/models/question.model.ts、src/models/course-task.model.ts 与 V6 设计文档 题库分类设计方案_v6.md 及种子数据 seed.py 的分析。
一、架构层级差异(最根本)
| 维度 | 旧系统 | V6 新设计 |
|---|
| 层级结构 | 三层扁平:CourseTask(题组)→ Question(题目)→ QuestionMeta(元数据/答案) | 四层分离:Material ⇄ 多对多 ⇄ QuestionBlock → Question → Point |
| 材料独立性 | ❌ 没有独立的「材料」实体。材料(音频/文本/视频)作为字段(audioUrl、videoUrl、transcript、mainTitle、subTitle)散落在 CourseTask 和 Question 上,无法跨题组复用 | ✅ Material 独立建表,多对多关系,同一材料可被多个 QuestionBlock 引用 |
| 题组与题目的关系 | CourseTask 与 Question 是 1:N(直接外键),但 CourseTask 本身混杂了课程内容(paragraphs、audioMarks、notes、transcript)和题库两种职责 | QuestionBlock 与 Question 是 1:N,职责纯粹——只管「教学和发布的最小单元」 |
| 计分粒度 | QuestionMeta.answers 是扁平数组 QuestionSingleAnswer[],每项只有 key/value/defaultValue——答案和判分混在一起 | Point 是独立实体,有 locator(定位)、answer、hint、match_rule、scoring(批改方式)五个维度,判分规则从答案中解耦 |
二、题型/作答形式体系
| 维度 | 旧系统 | V6 新设计 |
|---|
| 分类枚举 | QuestionType 枚举有 13 种(含注释掉的 6 种),实际启用 13 个值,如 SINGLE_CHOICE、GAP_FILLING_WITH_CHOICES、ORAL、COMPOSITION、TRANSLATION、FREE_FORM、STRESS、MARK、FREE_TALK、CONNECT、CLOZE 等 | question_type 固定词表 8 值:单选、多选、正误判断、匹配、排序、选词填空、单行填空、开放作答 |
| 口语/写作/翻译 | 分成三个独立的 QuestionType:ORAL(口语跟读)、COMPOSITION(作文)、TRANSLATION(翻译),且 FREE_FORM 和 FREE_TALK 也是独立类型 | 合并为单一 question_type = 开放作答,通过 input_modality(文本多行/语音/图片)区分录入方式 |
| 题型 vs 作答形式 | 混在一起——QuestionType 同时表达了"题型"(如完形填空)和"作答形式"(如单选)两个概念 | 分离:question_type(作答形式,8 值,挂题目)+ block_type(题型,开放词表,挂题组) |
| 主观/客观标记 | 用 isSubjective: boolean 一个布尔值粗暴区分 | 去掉了 isSubjective,由计分点的 scoring 字段(机器/人工/AI/混合)精确表达批改方式 |
三、分类/标注体系
| 维度 | 旧系统 | V6 新设计 |
|---|
| 能力维度 | ❌ 无。没有"听力/阅读/写作/翻译/口语/语言知识运用"的能力标注 | ✅ primary_ability(主能力,单选)+ secondary_abilities(次能力,多选),挂在题组上 |
| 标签体系 | Tag 表只有 name 字段,通过 CourseTagLink 关联到 CourseTask——单一维度,无法区分公共/私人标签 | 双轨制:public_tag(封闭键 exam/topic,全体可见)+ tags(个人标签,仅本人可见) |
| 考试对照 | ❌ 无。Exam 是独立的考试实体(组卷/考试管理),和题库没有分类关系 | ✅ public_tag.exam 键直接关联考试/卷种(CET-4、TEM-8、考研一 等),可做筛选 |
| 材料体裁 | ❌ 无。CourseTask.type 只区分 passage/exercise,无法表达"对话/篇章/新闻/讲座·独白/图表"等体裁 | ✅ Material.genre 固定词表 11 种体裁 |
| 材料模态 | 散落在多个字段:audioUrl、videoUrl、transcript、captionUrl | ✅ Material.modality(JSON 数组:文本/音频/视频/图片),多选 |
四、来源与审核
| 维度 | 旧系统 | V6 新设计 |
|---|
| 来源 | CourseTaskSource 三值枚举:original(原创)、official(官方真题)、ai(AI 生成);QuestionSource 两值枚举:original/official——两套词表不统一,且无 AI 选项在 Question 层 | 统一二值:AI生成 / 真人编制(挂在 Material 和 Question 两层),题组层不存来源而是自动推导为 三态(真人编制/AI生成/人机混合) |
| 审核状态 | ❌ 无审核机制。只有 isPublished(发布)和 isPublic(公共)两个布尔开关 | ✅ review_status 三态(待审核/已通过/已驳回),且材料有独立的审核闸门 |
| 可见性 | isPublished(boolean)+ isPublic(boolean)+ universityIds(逗号分隔字符串)——三个字段混搭,无统一模型 | ✅ visibility(private/public/custom)+ visibility_scale(JSON 对象,机构+用户 ID 列表),且非 private 须已通过审核 |
五、挖空/填空机制
| 维度 | 旧系统 | V6 新设计 |
|---|
| 完形填空 | GAP_FILLING_WITH_CHOICES 类型,body 结构为 {text: string, options?: string[]},空位用 ${_} 占位符标记——空位定位和答案解析都在运行时处理 | blanks 常驻键(含 answer_word + occurrence),材料存填入答案后的完整文本,空位由题目层规格定位——前端渲染灵活 |
| 填空标记 | ${_} 占位符——语义不明确,无编号 | {{n}} 内嵌标记——与 Point.blank[n] 一一对应,编号为题组内顺序号 |
| 词库/选项 | options?: string[]——简单字符串数组,无标签(label) | bank: [{label, word}] 和 options: [{label, text}]——带标签的结构化选项 |
六、数据存储/技术栈
| 维度 | 旧系统 | V6 新设计 |
|---|
| 数据库 | MySQL(Drizzle ORM) | SQLite(demo 阶段,设计上无关具体 DB) |
| 题干/答案存储 | body: object(宽泛 JSON)+ answer: QuestionAnswerType({value: QuestionAnswerValueType})——类型系统靠 TypeScript 枚举/联合类型,运行时是弱类型 | content: TEXT(JSON,结构随 question_type 宽松承载)+ Point.answer(字符串或列表)+ Point.hint + Point.match_rule——判分细节结构化独立存储 |
| 媒体资源 | 内嵌字段:audioUrl、videoUrl、transcript、audioMarks、vocabularyMarks、captionUrl 直接挂在实体上 | 媒体文件不入库,一律走 Material.resources_url 外链或附件 |
七、入库校验
| 维度 | 旧系统 | V6 新设计 |
|---|
| 校验机制 | ❌ 无入库校验。数据结构弱约束(body: object,运行时才解析) | ✅ 10 条入库时自动检查(题型与作答形式相容、翻译方向越界、blanks 定位、可见性闸门等),违例直接拒绝 |
| 数据一致性 | 靠代码运行时校验(checkers 目录下有 single/multiple/filling/default 四种 checker) | 靠入库前一次性校验(Python validate_block),确保数据结构层面自洽 |
八、旧系统有而 V6 去掉的
| 旧系统概念 | V6 处理 |
|---|
CourseTask.paragraphs(段落)、audioMarks、vocabularyMarks | 归入 Material 或 Question 的 content 结构,不再作为题组字段 |
Question.mainTitle / subTitle | 题组的 instruction 和 Material 的 content 承载,不再单独列标题字段 |
Question.analysis(解析) | V6 方案本轮未定义解析字段(备忘中提到后续处理) |
STRESS(重音标注)、MARK(单词高亮)、CONNECT(连线题) | 这些是池馆的特色题型,V6 的 29 种题型词表未收录——需要走扩展评审 |
QuestionOverview / TaskOverview(统计) | V6 方案有统计口径说明(第十二节),但本轮未落地表结构 |
九、V6 有而旧系统没有的
| V6 新概念 | 说明 |
|---|
| Material 层 | 完全独立的材料实体,支持多对多复用、独立审核 |
| Point 层 | 计分点独立建模,支持 hint/match_rule/scoring |
| 能力维度 | L/R/W/T/S/K 六维度,主+次能力体系 |
| public_tag + tags 双轨 | 系统级标签与个人标签完全隔离 |
| source 推导 | 题组来源由材料+题目 source 自动推导为三态 |
| review_status 闸门 | 未过审 → 只能 private,从结构上杜绝污染 |
| input_modality | 开放作答的录入方式正交维度(文本多行/语音/图片) |
| translation_direction | 翻译题的翻译方向,仅翻译类题组使用 |
| source_text | 保留原题样貌,结构化数据与教学现场可互相对照 |
总结
旧系统本质上是一个课程内容管理 + 简单题库的混合体(CourseTask 身兼课程段落和题目载体两职),题型分类靠单一 QuestionType 枚举硬编码。
V6 则是一套专业的外语题库分类体系,通过四层分离、双轨标签、能力维度、来源推导、审核闸门、入库校验等机制,解决了旧系统的录入重、词表打架、发布口径对不上三大核心问题。