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

原文: https://yishto.com/learn/project-labs/lab-training-assessments/

作者: [Yishto](https://yishto.com/about/)

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

首次发布: 2026-11-10
内容更新: 2026-11-10

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

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

## 定义一次练习的生命周期

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

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

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

## 冻结题目与评分规则

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

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

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

## 服务端拥有答案与计分权

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

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

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

## 用 Express 组织提交路径

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

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

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

## 离线验证评分的核心约定

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

```python
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 提示试图泄露答案的用例，分别记录预期反馈。

## 资料来源与延伸阅读

- [Express routing](https://expressjs.com/en/guide/routing.html)
- [SQLite transaction documentation](https://www.sqlite.org/lang_transaction.html)
- [API security risks](https://owasp.org/API-Security/editions/2023/en/0x00-header/)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [大模型评测：用任务集比较质量、延迟与成本](https://yishto.com/learn/models/model-evaluation/)
- [让数据库约束参与业务正确性](https://yishto.com/learn/service-data/database-indexes-and-transactions/)
