Files
ny-exam-platform/docs/guide/scores.md
T
nxxy335 a3be81b261 fix(security): 修复上传名单文件可被 URL 直连访问(nginx 下可 getshell)
严重漏洞:roster_import 使用 $_FILES 的原始扩展名拼 roster_YYYYMMDD_HHMMSS.<ext>
落在 Web 根目录内的 data/uploads/,既无扩展名白名单(可上传 .php),文件名又
可按秒枚举;而 bootstrap 只写了 data/.htaccess,nginx 不读 .htaccess,
生产环境(nginx + PHP-FPM)下可直接远程代码执行。

修复(api.php roster_import 四层防护):
- 扩展名白名单:仅 xlsx / xlsm / csv / txt
- 随机文件名:bin2hex(random_bytes(16)),杜绝枚举
- 解析完成后 @unlink 立即删除,Web 目录下不留存任何上传文件
- 20MB 大小限制 + UPLOAD_ERR 检查

加固(bootstrap.php):data 及 cache/uploads/snapshots 每个目录都写
.htaccess(Deny from all)+ index.html 占位;README 与文档补充 nginx
location ^~ /data/ { deny all; return 404; } 规则。

测试:新增 e2e tools/test-upload-guard.js(11 断言,覆盖 .php/.phtml 被拒、
无残留、合法 csv 导入成功且即删、无可枚举文件名),并入 tests/run-all.sh。
全套 90 项通过。

docs: 新增 VitePress 文档站(18 页)
- 使用指南 12 页:简介/部署/快速上手/系统设置/名单管理/统计口径/试卷设置/
  快照趋势异常/导出打印/数据看板/随机抽取/常见问题
- 安全 2 页:安全总览(含本次漏洞复盘)与部署加固清单(P0-P2)
- 开发 4 页:架构与目录/接口一览/SQLite 存储/测试
- 配图迁移至 docs/public/images/;.gitignore 忽略文档构建产物
2026-09-10 22:42:13 +08:00

86 lines
3.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 成绩明细与统计口径
这一页说明**每个数字到底是怎么算出来的**,以及表格里的每一列是什么含义。
## 统计卡片
| 卡片 | 计算方式 |
| --- | --- |
| 名单总人数 | 名单条目数;被当前试卷设为**免考**的人不计入 |
| 已提交 | 匹配到名单、且提交了答卷的人数(去重后的代表成绩行数) |
| 缺考 | `max(0, 名单总人数 − 已提交)` |
| 提交率 | `已提交 ÷ 名单总人数 × 100%` |
| 平均分 | 所有**有成绩**的答卷成绩平均值;无成绩者不计入分母 |
| 最高分 / 最低分 | 同上,取自所有有成绩的答卷 |
| 合格率 | `成绩 ≥ 满分 × 合格率阈值` 的人数 ÷ **已提交人数**。阈值全局默认 60%,可在 [试卷设置](./project-config.md) 针对单卷覆盖 |
| 平均用时 | 所有**有时长**的答卷时长平均值(秒),格式化为「X 分 Y 秒」 |
::: tip 卡片标题会跟着配置变
合格率卡片的标题会实时显示当前阈值,例如「合格率 (≥70%满分)」,避免你误以为还是 60%。
:::
## 班级 / 组别联动
选择顶部 **班级** 下拉后:
- 全部统计卡片只统计该班级;
- 成绩明细只显示该班级;
- 缺考名单只显示该班级,标题右侧计数同步变化;
- 切换班级是**本地即时重算**,不会额外请求服务器。
再选 **组别** 可做二次切片。
## 成绩明细列
| 列 | 说明 |
| --- | --- |
| 学号 / 姓名 / 班级 / 组别 | 来自名单;未匹配到的答卷不会进入本表 |
| 成绩 | 卷王返回的 `examScore`;该问卷不是考试模式时为 `-` |
| 考试次数 | 该学生**全部提交次数**,与「去重」开关无关 |
| 提交时间 | **最近一次**提交时间(重考低分也会让这行按最新时间排序到最上面) |
| 考试时长 | 最近一次答卷的时长;后台手动录入等没有起止时间的答卷为 `-` |
点击任意一行可查看该学生的**每一次提交**(时间、成绩、时长),最高分有标记。
## 排序与搜索
- 点击表头即可排序,支持学号 / 姓名 / 班级 / 组别 / 成绩 / 考试次数 / 提交时间 / 时长;
- 默认排序键是**最近提交时间**,所以刚交卷的人一定在最上面;
- 搜索框支持学号 / 姓名模糊匹配,200 ms 防抖;
- 成绩明细**无分页**,采用虚拟滚动,几千行也不卡;页面切到后台时自动暂停刷新。
## 新数据高亮
每次刷新时,与上一次相比新出现的提交(按学号判断)整行显示绿色底纹,
**30 秒后自动清除**;也可以点 **清除高亮** 立刻去掉。首屏加载不标记。
## 去重策略
设置里的「同一学号去重」只决定**代表成绩**取哪一份(最新 / 最高):
| 开关 | 成绩明细 | 考试次数 | 点击行内的历史 |
| --- | --- | --- | --- |
| 开启 | 一人一行 | 仍是全部次数 | 仍是全部提交 |
| 关闭 | 一人多行 | 仍是全部次数 | 仍是全部提交 |
`best_score` 字段始终是跨全部尝试的真实最高分。
## 未匹配答卷
提交了、但姓名不在名单中的答卷会单独成面板。常见原因:
- 姓名前后有空格、写了别名、用了繁体字;
- 姓名题选错了(到 **姓名字段** 下拉里手动指定);
- 名单里根本没有这个人。
可直接在面板里点 **加入名单** 补录。
## 缺考名单
名单中有、但未提交的人。支持 **复制学号**(逗号分隔)、**复制姓名**、**打印**。
## 相关
- [试卷设置(满分 / 合格率 / 免考)](./project-config.md)
- [导出与打印](./export-print.md)