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