数据结构与本地数据库 07:错题记录、事务与统计图

课程首页 · 全部知识课程 · 返回专题主页

本系列第 7/8 课,建议 45–60 分钟(估算)。先完成上一课。

下载本课完成工程 .aia

这课解决什么

一次记错要同时更新卡片次数并写入复习流水。事务保证两件事共同成功或共同失败;图表是业务数据的展示,不是固定画出来的装饰。横轴是卡片 ID,纵轴是记错次数。

先预测,再运行

次数加一成功,复习流水写失败,应该保留加一吗?

先记下自己的答案,运行起始工程与完成工程比较;答案见文末。

开始实战

以上一课完成工程为起点,先导入并运行,再另存为本课作业。

阶段工程各自保存数据;导入下一课不会自动带走手机上的旧数据。练习使用本课示例数据,迁移方法在数据课程第六课单独讲解。

新增或调整的组件

实例名 组件 本课配置
标签_标题 标签 Width=Fill parent;FontSize=24;TextColor=#183153;Text=知识卡片 · 第 7课
微数据库1 微数据库 Namespace=CardsB07
按钮_记错 按钮 Width=Fill parent;BackgroundColor=#2563EB;TextColor=#FFFFFF;FontSize=16;Text=本题记错一次
标签_统计 标签 Width=Fill parent;FontSize=16;TextColor=#183153;Text=错题统计
图表1 图表 Height=180;Width=Fill parent;Type=3
二维图表数据1 二维图表数据 Label=错题次数

逐步搭建

  1. 加入“本题记错一次”、图表与二维图表数据,确认先选卡片。
  2. 开始事务,更新 wrong,创建/写入 reviews;所有操作成功提交,否则回滚。
  3. 刷新统计遍历卡片,清除旧图点后添加新点,汇总累计次数。
  4. 同题两次记错显示 2;重启保留;在测试环境制造第二步失败,确认没有半条结果。

知识卡片与错题库第7课运行状态布局预览

图来自本课最终工程执行积木后的状态,为 HTML 近似预览;不是手机截图。

核心积木与数据流

下面是本课完成工程中的核心代码,包含所需全局变量和过程。可在块图中追踪数值,再回到工程对照完整事件;合并到已有工程时,同名事件合并处理。

global 卡片 = list(makedict(pair("id", 1), pair("question", "中国历史朝代顺序从哪里查?"), pair("answer", "按资料的年代字段排序"), pair("category", "历史"), pair("wrong", 0)), makedict(pair("id", 2), pair("question", "列表第一个索引是多少?"), pair("answer", "1"), pair("category", "编程"), pair("wrong", 0)))
global 显示 = []
global 选中ID = 0
global 下个ID = 3
global 关键词 = ""
global 倒序 = false
global 就绪 = false

procedure 刷新() {
卡片 = []
var 行们 = SQLite1.SelectSQL("SELECT id,question,answer,category,wrong FROM cards ORDER BY id", list())
var 已存编号 = SQLite1.SelectSQL("SELECT v FROM meta WHERE k=?",list("next_id"))
if length(已存编号) > 0 { 下个ID = max(下个ID, 已存编号[1] * 1) }
foreach 行 in 行们 {
listadd(卡片, makedict(pair("id", 行[1]), pair("question", 行[2]), pair("answer", 行[3]), pair("category", 行[4]), pair("wrong", 行[5])))
下个ID = max(下个ID, 行[1] + 1)
}
显示 = []
var 标题 = []
foreach 项 in 卡片 {
if 关键词 = "" or textContains(lower(join(dictget(项,"question",""), " ", dictget(项,"category",""))), lower(关键词)) {
listadd(显示, 项)
listadd(标题, join("#", dictget(项,"id",0), " · ", dictget(项,"category",""), " · ", dictget(项,"question","")))
}
}
if 倒序 { var 新显示 = [] var 新标题 = [] for i = length(显示) to 1 step -1 { listadd(新显示,显示[i]) listadd(新标题,标题[i]) } 显示 = 新显示 标题 = 新标题 }
列表显示框_卡片.Elements = 标题
选中ID = 0
标签_详情.Text = "点击卡片查看详情"
标签_状态.Text = join("显示 ", length(显示), " / ", length(卡片), " 张卡片")
二维图表数据1.Clear()
var 合计 = 0
foreach 项 in 卡片 {
var 次数 = dictget(项,"wrong",0)
if 次数 > 0 { 二维图表数据1.AddEntry(join(dictget(项,"id",0)), join(次数)) }
合计 = 合计 + 次数
}
标签_统计.Text = join("累计错题次数:", 合计)
}

procedure 保存() {
var 保存编号 = SQLite1.Execute("INSERT OR REPLACE INTO meta(k,v) VALUES(?,?)",list("next_id",下个ID))
if 保存编号 { 刷新() } else { 标签_状态.Text = "编号保存失败,请重试" }
}

when 按钮_记错.Click() {
if 选中ID > 0 and 就绪 {
SQLite1.BeginTransaction()
var 更新 = SQLite1.Execute("UPDATE cards SET wrong=wrong+1 WHERE id=?",list(选中ID))
var 建表 = SQLite1.Execute("CREATE TABLE IF NOT EXISTS reviews(id INTEGER PRIMARY KEY AUTOINCREMENT, card_id INTEGER NOT NULL, result TEXT NOT NULL)",list())
var 写入 = SQLite1.Execute("INSERT INTO reviews(card_id,result) VALUES(?,?)",list(选中ID,"wrong"))
if 更新 and 建表 and 写入 { SQLite1.CommitTransaction() 保存() }
else { SQLite1.RollbackTransaction() 标签_状态.Text = "记录失败,已回滚" }
} else { 标签_状态.Text = "先选择卡片" }
}

中途检查与排错

本课以下控制与数据流已执行最终归档中的真实积木检查。请在伴侣或安装的 APK 中按相同场景操作,记录实际结果;自动检查通过不代表手机已实测。

  1. 按索引读取详情。
  2. 空题不保存。
  3. 含引号逗号换行的记录保存。
  4. 重启完整恢复。
  5. 筛选后按稳定ID修改。
  6. 倒序保持记录映射。
  7. 取消删除保留。
  8. 确认删除正确对象。
  9. 实际读取嵌套asset JSON、重复导入去重。
  10. 错误顶层、缺字段、坏JSON与资源不存在拒绝。
  11. 已删除ID不复用。
  12. SQLite就绪、绑定参数和迁移只执行一次。
  13. 错题事务成功、第二步失败回滚。

排查时先确定出错的是输入、状态、数据读取还是显示;用标签暂时显示关键值,每次只改变一个条件。

当前课仅实现表中对应功能,导出和备份在第八课加入。内置 SQLite 无需扩展。

两个理解题与独立改造

  1. 次数加一成功,复习流水写失败,应该保留加一吗?
  2. 画出图表就能证明记录写入成功了吗?

独立改造: 增加答对流水,但不减少历史错题次数;另算本次练习正确率。

先完成自己的工程,再对照答案。检查成功路径,也检查空数据、重复操作和取消;写出你改动的规则。

查看预测与练习参考答案

预测: 不应该,两步属于同一个业务动作,回滚后次数仍是原值。

追踪: 不能。图表是显示;应核对数据库记录、重启结果和事务失败路径。

改造思路: 记录 result=correct 或 wrong,每次练习另有 session ID。正确率是本次答对/本次总数,不能用全局历史错题直接算。

完成与下一步

保存自己的工程并导出 .aia;给文件命名以区分本课作业和参考源码。请用自己的话解释本课知识,并展示一次失败输入的处理。

上一课 · 下一课:导出、备份与独立完成卡片库

文档反馈