第23次:面试题库功能

本文讲解题库系统的主干实现。它对应的不是单个文件,而是一条完整链路:InterviewQuizService.etsQuizBankPage.etsQuizPracticePage.ets,以及与错题本的联动。阅读时请把它当作一个完整的刷题系统来理解。


题库系统在项目里的真实定位

课程内测验强调“跟课学完顺手测一下”,而面试题库强调的是“体系化刷题”。因此它的设计比每日一题和普通课程测验都更完整:

  • 有模块分类
  • 有统计卡片
  • 有模块练习
  • 有错题本入口
  • 有练习总结页

先看整条链路:

QuizBankPage

InterviewQuizService.init

WrongAnswerService.init

加载所有题目与统计

显示模块列表与正确率

进入 QuizPracticePage

recordAnswer 更新统计

addWrongAnswer / markAsMastered

WrongAnswerBookPage


一、为什么题库首页要先显示统计再显示模块

QuizBankPage 的内容顺序非常有讲究:

  1. 顶部导航
  2. 我的统计卡片
  3. 错题本入口
  4. 模块题库列表

这个顺序不是为了好看,而是为了建立学习反馈感。用户进入题库首页时,先看到的是:

  • 我一共答了多少题
  • 我答对了多少题
  • 我的正确率是多少

然后再看到模块列表,才更容易知道下一步去哪里练。这个设计非常符合学习产品的逻辑,因为用户不是来“浏览信息”的,而是来“持续进步”的。


二、InterviewQuizService 的职责比你想象的大

当前题库服务做的事情非常完整:

  • 初始化全部题目
  • 读取整体统计数据
  • 记录每题答题结果
  • 维护模块级统计
  • 维护整体正确率
  • 记录每道题最后一次答题状态
  • 支持重置统计

这里最值得学习的是它不仅存“总答题数”,还存“每个模块的统计”。这意味着用户后续可以看到自己是 Hooks 模块弱,还是组件模块弱,而不是只知道一个笼统的全局正确率。

这类“全局统计 + 局部统计”并存的设计,在题库类应用中非常常见,也非常实用。


三、答题记录为什么不是简单累加

recordAnswer(questionId, answer, isCorrect) 是这套系统最关键的方法之一。它的设计并不是“每答一次就无脑加一”,而是更细致地处理了同一道题的重复作答:

  • 第一次作答:计入总答题数和正确/错误数。
  • 如果之前答错,后来答对:要修正统计。
  • 如果之前答过且结果没变:不重复累计。

这说明当前统计系统更接近“题目掌握情况”,而不是“机械刷题次数”。这种设计对学习产品更友好,因为它鼓励用户修正错误,而不是把统计做得越来越失真。


四、题库首页为什么还要初始化错题本服务

QuizBankPage 在加载时不仅调用 InterviewQuizService.init(),还调用 WrongAnswerService.init()。这是因为题库首页需要同时展示:

  • 我的答题统计
  • 错题本入口
  • 当前未掌握错题数量

也就是说,题库首页本质上已经是“练习中心”,而不是单纯的目录页。它既展示当前能力,也展示后续复习入口。

这一层入口在 QuizBankPage.ets 里对应的是下面这段真实加载逻辑:

private async loadData(): Promise<void> {
  this.isLoading = true;

  await InterviewQuizService.init();
  await WrongAnswerService.init();

  this.statistics = InterviewQuizService.getOverallStatistics();
  this.wrongAnswerCount = WrongAnswerService.getUnmasteredCount();

  const moduleList = InterviewQuizService.getAllModules();
  const tempModules: ModuleInfo[] = [];
  for (const m of moduleList) {
    const questions = InterviewQuizService.getQuestionsByModule(m.moduleId);
    const moduleInfo: ModuleInfo = {
      moduleId: m.moduleId,
      moduleName: m.moduleName,
      icon: m.icon,
      questionCount: questions.length,
      stats: InterviewQuizService.getModuleStatistics(m.moduleId)
    };
    tempModules.push(moduleInfo);
  }
  this.modules = tempModules;

  this.isLoading = false;
}
User WrongAnswerService InterviewQuizService QuizBankPage User WrongAnswerService InterviewQuizService QuizBankPage init() init() getOverallStatistics() getUnmasteredCount() 展示统计卡片、错题入口、模块列表

五、模块列表为什么要显示每个模块的正确率

当前 ModuleItem 中显示了:

  • 模块图标
  • 模块名称
  • 题目总数
  • 当前模块正确率

这不是简单的信息堆砌,而是给用户非常明确的练习反馈:

  • 题量决定刷题成本
  • 正确率反映掌握程度

当用户看到某个模块题目不多但正确率很低时,就知道那里是弱项;如果某个模块正确率已经很高,可能就可以暂时放一放。这就是“数据驱动学习路径”的基本思路。


六、QuizPracticePage 为什么支持两种模式

这一页非常值得学,因为它没有被写成“只能练模块题”。

当前它支持两种进入方式:

  • 通过 moduleId 进入模块练习模式
  • 通过 isFromWrongBook 进入错题练习模式

这种设计很聪明,因为:

  • 页面 UI 基本一样
  • 差异主要体现在题目来源
  • 复用一个页面就能覆盖两种练习场景

也就是说,页面层的复用不是靠硬拷贝,而是靠“参数化驱动不同模式”。


七、练习页里的答题闭环是怎样形成的

QuizPracticePage 的答题流程非常完整:

  1. 加载题目并随机打乱顺序。
  2. 展示当前题目和选项。
  3. 用户选择答案。
  4. 提交后立即判断对错。
  5. 调用 InterviewQuizService.recordAnswer() 更新统计。
  6. 如果答错,调用 WrongAnswerService.addWrongAnswer()
  7. 如果来自错题本且答对,调用 WrongAnswerService.markAsMastered()
  8. 最后一题做完后显示总结页。

这说明题库系统真正形成了:

  • 练习
  • 统计
  • 错题
  • 复习

四段闭环,而不是只有“答题”这一个动作。

把它放回真实源码里看,会更容易理解这条链路是如何闭合的:

private async handleSubmit(): Promise<void> {
  const question = this.getCurrentQuestion();
  if (!question || this.selectedAnswer < 0) return;

  this.isCorrect = InterviewQuizService.validateAnswer(question.id, this.selectedAnswer);
  this.isSubmitted = true;

  this.answers.push(this.selectedAnswer);
  this.results.push(this.isCorrect);

  await InterviewQuizService.recordAnswer(question.id, this.selectedAnswer, this.isCorrect);

  if (!this.isCorrect) {
    await WrongAnswerService.addWrongAnswer(question, this.selectedAnswer);
  } else if (this.isFromWrongBook) {
    await WrongAnswerService.markAsMastered(question.id);
  }
}

注意这里除了更新统计,还把用户本次选择和结果顺手记录进了 answersresults,这样页面后续才能继续切题并生成总结页。


八、总结页为什么非常重要

练习页最后的 SummaryView() 展示了:

  • 总题数
  • 正确题数
  • 正确率
  • 用时

这个总结页的意义在于,它给用户一次完整练习画上了一个可感知的句号。没有总结页,用户只会觉得“题做完了”;有总结页,用户会觉得“我完成了一次训练”。

对于学习产品来说,这种仪式感其实很重要。它能帮助用户形成持续刷题的动力。


九、自己实操时最推荐的顺序

  1. 打开题库首页,先看统计卡片和模块列表。
  2. 进入任意模块做几道题。
  3. 故意答错一两道,观察结果卡片和解析。
  4. 完成全部题目后看总结页。
  5. 回到题库首页,观察统计是否更新。
  6. 进入错题本,再从错题本发起练习,看答对后是否会被标记为已掌握。

这一套走完后,你对题库链路的理解会非常扎实。


十、本篇常见坑

1. 只做题目列表,不做统计

没有统计的题库很难形成成长反馈。

2. 同一道题重复作答时直接无脑累加

这样会让统计越来越失真。

3. 错题本和练习页分家过度

当前项目的优点恰恰在于两者通过参数模式复用了同一个练习页。

4. 练习完成后不做总结页

会大大削弱一次练习的完成感。


本篇小结

面试题库功能并不是一个单独页面,而是一整套学习闭环:

  • QuizBankPage 负责入口和总览
  • InterviewQuizService 负责统计与题库规则
  • QuizPracticePage 负责实际练习
  • WrongAnswerService 负责错题沉淀与复习

如果你把这一套结构真正理解透了,后面看错题本功能就会非常自然,因为它本来就是题库系统的一部分。


课后练习

  1. 自己总结 QuizServiceInterviewQuizService 在业务目标上的区别。
  2. 画出“模块练习模式”和“错题练习模式”在 QuizPracticePage 中的分流图。
  3. 思考如果你要增加“只练未做过题目”模式,最适合把过滤逻辑写在服务层还是页面层,为什么。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐