← 返回文章列表

错题会被纠正,但当时的作答证据不能被改写

TeachFlow 如何用 sourceRefId 与 latestEntryId 分离历史证据阅读和最新纠错动作,并把文字、图片、文件及下载失败纳入同一条可信证据链。

返回 TeachFlow 项目总览 · 交互图:IMMUTABLE · 证据 · 血缘

学生第一次把一道分数题答成 A,订正后做对;几天后,同一题的同一版本又答成 C。此时从第一次作业的深链进入错题本,界面应该展示什么?

直觉上,错题本总应显示“最新状态”。但如果“最新”同时支配页面上的所有内容,第一次作业留下的 A 就会被 C 替换。用户点开的明明是一次历史提交,看到的却是后来发生的答案。页面看似更“新”,证据却已经失真。

TeachFlow 对这个问题的处理不是给答案对象加更多状态,而是承认这里同时存在两种时间语义:

  • 证据时间:这条深链当时指向哪一次提交、哪份原作答、哪些附件;
  • 操作时间:现在应该订正哪一次错题、当前是否已掌握、标准答案是否可以解锁。

这两条时间轴可以落在同一道题上,却不一定落在同一次作答上。系统因此需要两个明确指针,而不是一份不断被覆盖的“当前答案”。

先定义不可破坏的合同

这个设计的核心合同可以压缩成一句话:

sourceRefId 决定“正在查看哪一次历史证据”,latestEntryId 决定“当前要对哪一次作答执行订正”。

正式错题 Case 以学生、题目 ID、题目版本为精确身份聚合多次 occurrence。题干文字相同,不代表它们是同一道可合并的题;题目 ID 不同或版本不同,都必须保持为不同 Case。聚合后,Case 保存 occurrence 列表,并单独投影出 latestEntryId

页面收到历史来源深链时,先用 sourceRefId 找到 focusedOccurrence。作答证据的读取目标是:

evidenceOccurrence = focusedOccurrence ?? latest

订正状态、订正按钮和答案解锁仍读取 latest。这不是一个 UI 条件分支,而是两个职责不同的领域指针:一个只读历史,一个驱动当前动作。

同一页面同时展示深链指定的历史作答 A,并把订正状态保持在最新一次待订正

上图来自当前版本的真实浏览器流程。测试夹具先形成三次同题同版本记录:历史来源的原作答为 A,较新的 occurrence 为 C,Case 当前状态为“最新一次待订正”。界面同时明确提示“正在查看深链指定的历史来源;下方订正状态与操作仍针对最新一次错题”。

如果两个区域都显示 A,当前纠错会落后;如果两个区域都显示 C,历史证据会被改写。只有双指针模型能让二者同时成立。

为什么不能只显示最新答案

“所有地方都显示最新一次”是最容易实现、也最危险的方案。它至少破坏三类事实。

第一,深链不再可信。作业、学习轨迹或教师复盘跳转到某次来源时,链接表达的是“回到当时发生的记录”,不是“打开这道题今天的摘要”。若目标页面忽略 sourceRefId,深链只剩导航意义,失去证据定位能力。

第二,纠错的因果关系会变模糊。用户无法判断一次订正到底修正了 A 还是 C,也无法解释为什么历史作业中的原答案和错题本中的“原作答”不同。

第三,多模态附件会错配。一次作答可能只有文字,另一次可能上传了整份作业照片或 PDF。只按最新答案替换文本,却继续沿用旧附件,或者把新附件挂到旧来源上,都会制造一个从未真实发生过的组合。

因此,历史证据不是“旧版当前值”,而是被来源标识定位的 occurrence。后续状态可以前进,过去发生过的内容不能跟着漂移。

数据血缘:读取历史,操作最新

历史作答证据与最新纠错目标的数据血缘图

这张图把系统分成五段:持久事实、错题投影、指针解析、学生界面、受保护资源。

左侧的作答发生记录携带题目身份、版本、sourceRefId 和证据引用。错题投影只负责把同题同版本 occurrence 汇总为一个 Case,并给出 latestEntryId。进入指针解析后,路径开始分叉:

  • 历史来源深链通过 sourceRefId 选择证据 occurrence,进入只读作答证据面板;
  • Case 通过 latestEntryId 选择当前动作 occurrence,进入订正状态与操作区域。

图中刻意不存在“最新纠错结果覆盖历史原作答”的数据流。文件下载也不是把附件复制到错题本,而是从证据引用发起受保护请求:成功返回文件,失败返回可见的失败状态。

打开可交互的数据血缘图

不复制附件,保留来源语义

作答证据并不等于一段答案字符串。当前实现把证据拆成三类:文字、图片和普通文件,并在界面中组合呈现。没有任何证据时,页面显示“无可展示证据”,而不是根据题干或后来的答案补造一份看似完整的历史。

图片还有额外的来源边界。正式作业上传的图片可能是整份作业,而不是该题精确裁片。界面必须把这件事说出来,不能仅因为图片出现在某道错题详情中,就把它描述为“本题裁片”。原题裁片是题目来源证据,整份作业照片是学生提交证据,两者的来源和粒度不同。

文件也不复制进一个新的错题本附件目录。错题记录保留原附件引用,浏览器下载时通过带身份的请求获取 Blob。这样做保留了原来源、权限和失败语义;代价是页面必须认真处理加载中、失败和重试,而不能用普通静态链接假装附件永远可用。

文字、图片和文件证据同时出现,文件下载失败后显示明确的重试状态

上图同样来自真实浏览器流程。作答证据中同时存在文字、图片和 PDF。测试把文件请求临时置为 503,界面没有吞掉错误,也没有改变历史内容,而是把按钮转为“重试”,并提示检查权限或稍后再试。

三个被放弃的方案

方案一:Case 里只保留一份可变答案

它让列表和详情读取最简单,却无法回答“这是哪一次作答”。一旦出现重复错题、历史深链或多模态附件,覆盖就会破坏来源一致性。

方案二:把历史证据复制进错题本

复制看似能冻结页面,但会产生第二份权限、生命周期和删除策略。原作业附件撤销权限或过期后,副本是否仍可访问?两份文件哈希不同怎么办?TeachFlow 选择保留受保护引用,并让下载失败成为可观察状态。

方案三:历史深链同时接管订正动作

这会让用户在查看旧来源时意外订正旧 occurrence,而 Case 最新一次仍然待处理。界面上的历史阅读上下文不能悄悄改变当前操作目标。因此订正按钮始终绑定 latest,旁边还明确说明“默认针对最近一次作答,历史来源不会被覆盖”。

验证这项设计,不能只看一个截图

当前证据分为三层。

第一层是代码合同。错题投影按学生、题目 ID 和版本聚合,数据库查询单独计算 latest_entry_id;前端再分别解析 focusedOccurrencelatest。这是 code-confirmed

第二层是真实浏览器行为。隔离数据库中的演示记录完成了以下断言:历史深链展示 A,不展示更新的 C;最新状态仍为待订正;历史来源在 occurrence 列表中保持高亮;文字、图片和文件可以同时呈现;文件请求失败后显示重试。两张截图就是这条执行链路的可见结果,因此这些目标结论为 verified

第三层是本机合成规模检查。一次当前版本运行覆盖 50、200、1000 个正式错题 Case。1000 Case 场景下,API P95 为 253 ms,首张卡片 527 ms,DOM 节点 745,首屏图片请求 0;对应脚本预算均通过。这里的数字只能说明本机、合成数据、单次运行没有越过既定门槛,不能外推为学校部署容量或生产 SLA。

完整前端回归在上述目标断言之后继续执行,最终停在与本文无关的知识图谱入口数量断言。因此这里不声称“全套前端回归通过”;能确认的是本文涉及的历史证据、多模态展示、失败重试和规模门槛。

这套设计没有证明什么

这套本地演示和合成数据证明的是证据合同与用户流程,不是教学效果。它没有证明学生因此学得更好,也没有证明某所学校的真实并发、身份系统、对象存储、保留策略、灾备或长期数据增长已经完成生产验证。

如果未来接入真实学校环境,还需要至少补齐:正式 IAM 与附件授权审计、保留与删除策略、对象存储故障演练、跨版本迁移后的 sourceRefId 可解析性,以及在代表性数据分布下的容量测试。

结论

错题本的“当前”不应该吞掉作答的“当时”。

TeachFlow 没有把这个问题处理成更多 UI 文案,而是把两个时间语义放进数据合同:sourceRefId 固定历史证据,latestEntryId 驱动最新纠错。这样,错题可以继续被纠正,Case 可以重新打开,标准答案可以重新锁定;但用户当时提交过什么,仍然能被准确地看到、解释和追溯。

← 返回文章列表