Files
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.3 KiB
Markdown
Raw Permalink 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.
# 名单管理
名单是平台的"参照系":所有统计都以名单为分母,缺考名单 = 名单中有、但未提交的人。
## 打开方式
主页面右上角 **名单管理 <b>N</b>**(N 为当前人数)。也可以在随机抽取页点击 **名单管理** 跳转回来。
![名单管理](/images/roster.png)
## 导入文件
支持 `.xlsx` / `.xlsm` / `.csv` / `.txt`,单次上限 20 MB。
1. 点击上传区或把文件拖进来;
2. 选择 **覆盖**(替换现有名单)或 **追加**(合并到现有名单);
3. 点 **保存名单**。
> 导入后弹窗内会立即显示解析结果,此时**尚未落库**,检查无误再点保存。
### 列识别规则
程序先找表头行:某一行同时命中「学号类」与「姓名类」关键词,就把它当作表头,从下一行开始读数据。
| 字段 | 识别关键词(正则,容错空格) | 缺失时 |
| --- | --- | --- |
| 学号 | `学号` `编号` `序号` `学员号` `号码` `no` `id` | 取第 0 列 |
| 姓名 | `姓名` `名字` `学生姓名` `姓氏` `成员` | 取第 1 列(若学号占了第 0 列) |
| 班级 | `班级` `class` | 留空,界面不显示该列 |
| 组别 | `组别` `分组` `组次` `组` `group` | 留空 |
| 登陆账号 | `登陆账号` `登录账号` `账号` `手机号` `手机` | 留空 |
其它说明:
- 学号会自动去掉 Excel 数字尾巴(如 `2024012001.0` → `2024012001`);
- 同一学号重复出现时保留首次,其余记为「重复跳过」;
- 学号与姓名均为空的行记为「跳过」;
- CSV 自动识别并剥离 BOM,非 UTF-8 编码(GBK 等)会自动转码。
### 示例文件
| 学号 | 姓名 | 班级 | 组别 | 登陆账号 |
| --- | --- | --- | --- | --- |
| 2024012001 | 张三 | 1班 | A | stu001 |
| 2024012002 | 李四 | 1班 | B | stu002 |
| 2024012003 | 王五 | 2班 | A | stu003 |
**登陆账号** 只用于随机抽取页导出「白名单导入模板 / 白名单列表」,界面任何位置都不显示。
## 手动维护
| 操作 | 说明 |
| --- | --- |
| 新增 | 点 **+ 新增** 弹出录入框,填学号 / 姓名 / 班级 / 组别 / 登陆账号 |
| 编辑 | 直接在表格里改单元格 |
| 删除 | 勾选行 → **删除选中** → 二次确认 |
| 清空 | **清空** → 二次确认,清空全部名单 |
::: warning
**保存名单**是全量替换(后端 `DELETE` + `INSERT`),请确认弹窗内列表无误再保存。
:::
## 从未匹配答卷补入名单
主页面 **未匹配答卷** 面板列出"提交了但姓名不在名单中"的答卷。点 **加入名单**,
填写学号 / 班级 / 组别后即可补入,避免下次仍然匹配不上。
## 试卷级免考
只想让某几个人在**某一张试卷**中不计入统计?用 [试卷设置 → 免考名单](./project-config.md#免考名单),
而不是把他们从名单里删掉——免考只在当前试卷生效,不会影响其它试卷。
## 存储位置
| 环境 | 位置 |
| --- | --- |
| 启用 `pdo_sqlite`(默认) | `data/ny.db` 表 `ny_roster` |
| 未启用 `pdo_sqlite` | `data/roster.json` |
首次在有 SQLite 的环境运行时,`roster.json` 会自动迁移进数据库(仅当库为空时)。
## 相关
- [成绩明细与统计口径](./scores.md)
- [试卷设置](./project-config.md)