在这里插入图片描述
在这里插入图片描述

实例:错题本(Mistake)|技术:复习轮次、掌握状态、错因分类

一、业务需求分析:错题本的数据形态

错题本是**「学习记忆」应用**——每道错题记录题目/答案/错因,并通过复习轮次推进掌握(类似间隔重复)。数据层核心是复习状态机(轮次 + 掌握度)与多维筛选(学科/知识点/错因)。

核心业务需求:

  1. 错题记录:学科、题目、正确答案、我的错误答案、知识点、错因;
  2. 复习机制:每次复习轮次 +1,复习 5 次自动标记掌握——CASE WHEN 条件更新
  3. 掌握状态:未掌握/已掌握切换;
  4. 复习队列:未掌握且轮次 < 5,按上次复习时间升序(最久没看的优先)——优先队列查询
  5. 统计:错因统计、学科统计、总览(总数/未掌握/已掌握/待复习)——多维度聚合

二、字段设计:学习驱动错题

错题表 mistake

字段名 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
subject TEXT NOT NULL 学科(数学/英语/物理…)
question TEXT NOT NULL 题目内容
answer TEXT DEFAULT ‘’ 正确答案
my_answer TEXT DEFAULT ‘’ 我的错误答案
knowledge TEXT DEFAULT ‘’ 知识点标签
reason TEXT DEFAULT ‘粗心大意’ 错因(计算失误/概念不清/粗心/思路错误)
rounds INTEGER NOT NULL DEFAULT 0 复习轮次 0~5
mastered INTEGER NOT NULL DEFAULT 0 0 未掌握 / 1 已掌握
created_time INTEGER NOT NULL 录入时间戳
review_time INTEGER NOT NULL DEFAULT 0 上次复习时间戳

设计要点拆解

1. rounds(复习轮次)是核心字段。0~5 的整数——每次复习 +1,达到 5 次自动掌握。「轮次 = 复习进度的量化」:轮次少 → 需多复习(列表排前),轮次 5 → 已掌握。这是简化版间隔重复(真实 Anki 有难度分级 + 间隔天数,Demo 用轮次线性推进)。

2. mastered(掌握度)二态。0 未掌握 / 1 已掌握——状态与轮次联动:轮次达到 5 自动置 1,也可手动切换(「标记掌握」/「取消掌握」)。**「轮次驱动 + 手动干预」**双通道。

3. reason(错因)文本分类。计算失误/概念不清/粗心大意/思路错误——错因统计(GROUP BY reason)让用户看到「我的错题集中在哪类错因」。错因是诊断维度(学习分析的核心)。

4. knowledge(知识点)标签。函数变换/绝对值不等式/被动语态…——知识点是复习的关联线索(同类知识点一起复习),LIKE 模糊搜索知识点(queryByKnowledge)。

5. my_answer vs answer 对照。错误答案与正确答案并存——「错题的价值 = 对照」:复习时先看自己错在哪、再看正确答案。

6. review_time(上次复习时间):驱动复习队列排序(最久没复习的优先)——ORDER BY review_time ASC 把「好几天没看的」排最前。**「时间越久越优先」**是间隔重复的核心排序。

三、建表 SQL 与索引

CREATE TABLE IF NOT EXISTS mistake (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  subject TEXT NOT NULL,
  question TEXT NOT NULL,
  answer TEXT DEFAULT '',
  my_answer TEXT DEFAULT '',
  knowledge TEXT DEFAULT '',
  reason TEXT DEFAULT '粗心大意',
  rounds INTEGER NOT NULL DEFAULT 0,
  mastered INTEGER NOT NULL DEFAULT 0,
  created_time INTEGER NOT NULL,
  review_time INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_mistake_subject ON mistake (subject);
CREATE INDEX IF NOT EXISTS idx_mistake_knowledge ON mistake (knowledge);

idx_mistake_subject:学科筛选(WHERE subject = '数学')走索引。idx_mistake_knowledge:知识点 LIKE 搜索的前缀索引(knowledge LIKE '函数%' 可走,%函数% 不行——教学索引示例)。

四、MistakeDao 封装:复习机制是亮点

数据层核心 MistakeDao,本实例最独特的是reviewOnce(复习一次的条件更新)

/** 复习一次:轮次 +1,刷新复习时间;轮次达到 5 自动标记已掌握 */
static async reviewOnce(context: common.Context, id: number): Promise<void> {
  const store = await MistakeDao.getStore(context);
  const now = Date.now();
  await store.executeSql(
    `UPDATE ${MistakeDao.TABLE}
     SET rounds = rounds + 1,
         review_time = ${now},
         mastered = CASE WHEN rounds + 1 >= 5 THEN 1 ELSE mastered END
     WHERE id = ${id}`
  );
}
UPDATE mistake
SET rounds = rounds + 1,
    review_time = 1720869600000,
    mastered = CASE WHEN rounds + 1 >= 5 THEN 1 ELSE mastered END
WHERE id = 3;

逐行拆解

1. 自增更新 rounds = rounds + 1:复习轮次在原值上加 1——SQL 的自增更新(不需要先查再算,一条 UPDATE 完成)。

2. CASE WHEN 条件赋值 mastered = CASE WHEN rounds + 1 >= 5 THEN 1 ELSE mastered END:复习后轮次达到 5 → 自动标记掌握(1),否则保持原值——「根据新轮次条件决定新状态」的 CASE 表达式。这是本实例最亮眼的 SQL 技巧:在 UPDATE 里做条件分支(比先查再改少一次往返)。

3. 为什么用 executeSql 而不用 update(values, predicates)? RdbPredicates 的 update 只能设固定值({ rounds: ? } 不知道当前值);rounds = rounds + 1CASE WHENSQL 表达式(引用旧值)——「需要引用旧值的更新必须用 SQL 表达式」,executeSql 才能写完整 UPDATE。

4. 单条复习按 idWHERE id = ${id}——精确单条。

markMastered(手动掌握切换)

static async markMastered(context: common.Context, id: number, mastered: number): Promise<number> {
  const store = await MistakeDao.getStore(context);
  const values: relationalStore.ValuesBucket = { mastered: mastered, review_time: Date.now() };
  const predicates = new relationalStore.RdbPredicates(MistakeDao.TABLE);
  predicates.equalTo('id', id);
  return await store.update(values, predicates);
}

手动设置固定值{ mastered: mastered, review_time: Date.now() }——**「设固定值用 update(values, predicates),自增/条件用 executeSql」**的 API 选择准则。

queryReviewQueue(复习队列)

static async queryReviewQueue(context: common.Context): Promise<Mistake[]> {
  const store = await MistakeDao.getStore(context);
  const predicates = new relationalStore.RdbPredicates(MistakeDao.TABLE);
  predicates.equalTo('mastered', 0).lessThan('rounds', 5).orderByAsc('review_time');
  const result = await store.query(predicates);
  return MistakeDao.collect(result);
}

「待复习」的数学定义mastered=0(未掌握)AND rounds < 5(还没复习够)——双条件交集排序 review_time ASC:最久没复习的排最前(间隔重复的优先级)。**「条件筛选 + 时间升序」**是待办队列的通用模式(最旧优先)。

五、统计三件套

overview(总览,一条 SQL 四个统计)

static async overview(context: common.Context): Promise<MistakeOverview> {
  const result = await store.querySql(
    `SELECT COUNT(*) AS total,
            SUM(CASE WHEN mastered=0 THEN 1 ELSE 0 END) AS pending,
            SUM(CASE WHEN mastered=1 THEN 1 ELSE 0 END) AS mastered,
            SUM(CASE WHEN mastered=0 AND rounds<5 THEN 1 ELSE 0 END) AS to_review
     FROM ${MistakeDao.TABLE}`
  );
  // 解析四列
}

SUM(CASE WHEN … THEN 1 ELSE 0 END) 条件计数:一行 SQL 算出四个统计(总数/未掌握/已掌握/待复习)——**「CASE WHEN 配合 SUM 做条件计数」**是本实例的第二个 SQL 亮点(不用四次 COUNT + WHERE)。一次聚合多统计的极致写法。

reasonStats / subjectStats(错因/学科分组统计)

static async reasonStats(context: common.Context): Promise<ReasonStat[]> {
  // SELECT reason, COUNT(*) AS cnt FROM mistake GROUP BY reason ORDER BY cnt DESC
}
static async subjectStats(context: common.Context): Promise<SubjectStat[]> {
  // SELECT subject, COUNT(*) AS cnt FROM mistake GROUP BY subject ORDER BY cnt DESC
}

GROUP BY + COUNT + DESC——与实例 16/18 同构(按维度分组计数)。错因统计的意义计算失误 4 / 概念不清 4 / 思路错误 2——用户一眼看到「我错题集中在哪」——学习诊断

六、技术要点对照表

技术点 实现方式 生产价值
轮次自增 rounds = rounds + 1 复习计数
条件赋值 CASE WHEN 自动掌握 无往返状态流转
复习队列 mastered=0 + rounds<5 + review ASC 最久优先
条件计数 SUM(CASE WHEN) 四统计一 SQL 总览一次拿
分组统计 GROUP BY reason/subject 诊断维度
知识点搜索 LIKE %kw% 关联复习

七、文章小结

错题本的数据层核心是**「复习轮次 + 条件更新」:rounds 轮次量化复习进度(0~5)、reviewOnce 用 SQL 表达式(rounds + 1 + CASE WHEN)一条 UPDATE 完成「轮次自增 + 时间刷新 + 满 5 自动掌握」、queryReviewQueue 用「未掌握 + 轮次未满 + 最久优先」定义复习队列、overview 用 SUM(CASE WHEN) 一次 SQL 算出四个总览统计。「SQL 表达式更新」与「条件计数」**是两个可复用的高级技巧。这是「学习复习类应用」的通用数据模型。

下一篇(19-2)展示横线笔记本风 UI——错因统计条 + 学科筛选 + 横线纸题目卡。

八、字段联动深入:复习状态机的三种流转

rounds 与 mastered 的联动状态机

复习状态由 rounds(0~5)与 mastered(0/1)共同决定,共三种合法组合:

状态 rounds mastered 含义 复习队列
待复习 0~4 0 轮次未满且未掌握 ✅ 进队列
已掌握(自动) 5 1 复习满 5 次自动标记 ❌ 出队列
已掌握(手动) 0~4 1 手动「标记掌握」 ❌ 出队列

关键点:「已掌握」的判定是 mastered 而非 rounds——所以 queryReviewQueue 只查 mastered=0 AND rounds<5,手动掌握项不会被误拉进队列;而 reviewOnce 的 CASE WHEN rounds+1>=5 THEN 1 保证自动通道在轮次满 5 时兜底。自动 + 手动两条通道汇合到同一个状态位,是这套状态机的设计精髓:

// 状态机判定:复习队列只看 mastered,自动掌握看 rounds
const inQueue = item.mastered === 0 && item.rounds < 5;
const autoMastered = item.rounds >= 5;      // reviewOnce 里由 CASE WHEN 保证
const manualMastered = item.mastered === 1; // markMastered 手动置位

reason 错因的诊断维度

reason 不只是文本标签,它是学习诊断的切面GROUP BY reason 让错因分布可视化——「计算失误多 → 刷题练计算;概念不清多 → 回去看书」。四种错因对应四种补救动作:

错因 诊断含义 复习建议
计算失误 会做但算错 限时练习 + 验算习惯
概念不清 知识点没懂 回课本重学该知识点
粗心大意 审题/抄写失误 慢读题 + 关键词圈画
思路错误 方法不对 看解析对比思路

knowledge 知识点标签

knowledge 是跨错题的关联线索——queryByKnowledge 用 LIKE 把同知识点的错题捞出来一起复习(函数变换的几道题连刷,形成记忆组块)。

九、my_answer / answer 对照的字段意义

错题本区别于普通题库的关键:错题的价值在「错」。my_answer 存现场错误答案,answer 存正确答案,两者对照产生三个用途:

  1. 复习时先遮答案:看题目 → 默写 → 对 answer——「错误记忆被重新激活」正是遗忘曲线上的最佳复习点;
  2. 统计错因的依据:对比 my_answer 与 answer 的偏差类型(算错/概念错/思路错),人工归类到 reason;
  3. UI 对照展示:横线纸卡上「我的答案(红)/ 正确答案(绿)」并排,一眼看出差距:
// 列表卡片对照展示:红我的答案 / 绿正确答案
Text('我的答案:' + item.my_answer).fontColor('#E5484D');
Text('正确答案:' + item.answer).fontColor('#30A46C');

十、复习队列思路预览

queryReviewQueue 的核心是**「最久未复习优先」**——ORDER BY review_time ASC 让 3 天没看的排在 1 天前看过的前面。为什么时间升序就是间隔重复?因为 review_time 越小 = 距上次复习越久 = 记忆越模糊,最该先复习。真正的 Anki 按「到期日」排,Demo 用 review_time 做线性简化。19-3 将深入队列查询与条件计数的 SQL 细节。

十一、FAQ

Q1:为什么 rounds 到 5 自动掌握,还要手动 mastered 字段?
自动通道只管「复习满 5 次」,但用户可能想提前「标记掌握」(这道题其实早会了),或复习时发现忘了再「取消掌握」。mastered 是用户可干预的最终状态,rounds 是机器自动的进度计数——两个字段各司其职。

Q2:手动标记掌握后,rounds 还继续涨吗?
会。markMastered 只改 mastered 不动 rounds;之后 reviewOnce 仍会 rounds+1,但 mastered 已是 1,队列不再拉取。这是「状态与进度解耦」的刻意设计。

Q3:为什么 my_answer 用 DEFAULT ‘’ 而不是 NOT NULL?
录入错题时可能只记题目忘了填错误答案,DEFAULT '' 保证 INSERT 不报错,UI 端对空串显示「未记录」兜底——**「可空即默认值」**比 NOT NULL 更适合用户录入场景。

Q4:knowledge LIKE ‘%函数%’ 能走索引吗?
不能。idx_mistake_knowledge 是普通 B-tree 索引,%kw% 前导通配无法命中,只有 函数% 前缀才能走索引。Demo 数据量小无所谓,生产可考虑 FTS5 全文检索。

Q5:rounds 满 5 后这道题会怎样?
mastered 置 1 后自动退出复习队列(mastered=0 AND rounds<5 双条件),列表进入「已掌握」tab。想重刷时手动取消掌握即可重新入队——队列的进出完全由状态机控制

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐