← 完整项目实验室 · 第 3 篇

项目:培训练习与答题记录系统

冻结题目与评分版本,设计草稿、最终提交和结果权限,用独立练习系统检查重复请求与答案保护。

学完这一篇,你能做到

实现可复核的评分规则,并验证题库更新、并发提交、截止时间和结果权限。

培训练习系统让学习者完成一组题目,保存作答并获得可解释的评分。第一版不做监考,也不把 AI 生成的评价当作客观能力认证。它的工程目标是:同一份题目版本按同一规则评分,用户只能读取自己的练习记录,重试不会产生两份最终提交。

先做单选练习,再考虑开放题、提示和人工点评。开放题的质量评估与确定答案的计分需要不同规则,不能混在一个“正确率”里。

定义一次练习的生命周期

学习者创建练习尝试,保存草稿答案,最终提交,再读取结果。提交后是否允许重做应有明确政策,例如创建新尝试,而不是原地改写已计分答案。培训者负责题目和规则维护,但不能仅因知道记录 ID 就访问所有租户数据。

每个接口都要有失败约定:题目不存在、练习已经结束、输入字段不合法、无权限和重复提交分别如何返回。页面显示“已提交”前,应收到服务端确认;请求超时后先查询同一尝试的状态。

如果设置截止时间,服务器使用自己的时间和明确的时区规则判断。浏览器倒计时提供提示,不能授权延长提交窗口。离线草稿同步也要定义是否在截止后允许接收,避免各客户端自己决定。

冻结题目与评分规则

题目维护记录保存稳定 ID,具体题干、选项和答案属于题目版本。一次练习绑定一份版本清单与评分规则版本。题库更新不能改变已经开始的练习内容,历史结果也不能被新答案悄悄重新解释。

核心记录包括课程、题目版本、练习尝试、草稿、最终提交和结果。最终提交保存当时的答案与计分结果,关联练习版本。为同一尝试的最终提交配置唯一约束,以数据库参与保证“只有一份最终结果”。

若需要重新评分,创建明确的新评分记录,注明规则版本和原因;不要覆盖旧结果然后让用户误以为原来的分数就是这样。题目质量、训练收益和考试有效性仍需要另行评估,数据表完整不能证明它们。

服务端拥有答案与计分权

前端读取题干和必要选项,服务端保留答案键。提交时只发送允许的题目 ID 与选择值,服务端根据冻结版本计算结果。客户端发送的分数、正确标记和角色一律不作为权威数据。

校验器检查题目属于当前练习、选项值有效、数量和总大小受限。遗漏答案按预先规定记为未作答或拒绝,而不是让不同服务各自猜测。出现未知题目要拒绝,不能把它悄悄忽略后仍显示提交成功。

权限检查使用已验证身份与资源关系。查看结果同样需要授权,不能只在提交接口检查。培训者的批量报表应限制范围,导出与日志避免包含不必要的用户身份信息。

用 Express 组织提交路径

路由层处理身份、参数和响应,应用服务协调练习状态与提交,计分函数处理明确规则,存储层执行查询和事务。先把纯计分函数测清,再把数据库与 HTTP 边界接上。

最终提交应在可协调的事务中检查状态、写入唯一提交、写入结果并关闭尝试。两个并发请求不能仅靠内存中的“正在提交”标记排重。重复逻辑请求返回已有结果;同一请求键带不同答案时,应检测冲突,而不是任意接受后到的内容。

AI 提示可作为独立功能,在允许阶段解释知识点。它不能获取不应公开的答案,也不能改变最终计分。开放题点评应标明生成性质和审核要求,并与确定计分结果分开保存。

离线验证评分的核心约定

下面只有两道合成题,答对一题计一分,缺失答案计零分,未知题目拒绝。它验证的是这份教学规则,不是题库服务或完整提交事务。

key = {"Q-1": "B", "Q-2": "A"}
def grade(answers):
    if not set(answers) <= set(key):
        raise ValueError("unknown question")
    return sum(answers.get(q) == expected for q, expected in key.items())
assert grade({"Q-1": "B", "Q-2": "C"}) == 1
assert grade({}) == 0
try:
    grade({"Q-3": "A"})
    raise AssertionError("unknown question accepted")
except ValueError:
    pass
print(grade({"Q-1": "B", "Q-2": "A"}))

输出是 2。其中一题选错时结果是 1,空答案是 0。这里没有验证选择值是否属于某题选项,没有用户认证、截止时间或数据库。生产入口要先做这些检查,再调用对应计分规则;不能把这段函数包装为接口就宣称提交可靠。

让界面能解释状态

题目页显示未答、已选、草稿保存中和保存失败。保存失败时保留本地输入,让用户知道重试的是草稿保存还是最终提交。提交按钮在等待时限制重复点击,但后端仍承担并发与去重保证。

结果页展示规则、得分、未作答数量及允许公开的解释。若尚在处理,不用零分占位;若返回权限错误,不显示其他用户的缓存结果。路由切换或再次打开应用时,从服务端恢复当前尝试状态。

实施顺序是题目版本导入、练习创建、草稿、提交计分、结果读取,随后补齐界面。每一步先用合成数据验证输入、权限和状态,再增加批量练习与报表。

草稿也需要版本检查。两个浏览器同时编辑时,后到请求不能无条件覆盖较新的答案;响应返回当前草稿版本,并让冲突页面展示保存失败和可恢复内容。最终提交绑定明确的答案快照,关闭尝试后停止接受草稿修改。题目及选项的随机顺序应属于练习快照,否则刷新页面可能改变选项对应关系。若允许结果导出,导出接口重复检查练习归属和可公开字段,不直接复用培训者页面的全部数据。

练习:重复提交与题库更新

在练习开始后修改题库答案,确认该练习仍按绑定的旧版本评分。创建第二次练习,确认它使用明确选择的新版本。历史结果应能说明使用了哪份规则。

同时发送两次最终提交,并在响应前模拟超时。验收最终提交与结果只各保存一份,查询能够取回同一结果,无权限身份不能读取。再加入非法选项、未知题目、截止后提交和 AI 提示试图泄露答案的用例,分别记录预期反馈。

资料来源与延伸阅读

资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。