Files
ny-exam-platform/docs/dev/api.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

107 lines
4.0 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.
# 接口一览
所有接口统一走 `api.php?action=<name>`,响应为 JSON:
```jsonc
// 成功
{ "ok": true, "data": { ... } }
// 失败
{ "ok": false, "error": "错误信息" }
```
- 未登录时返回 `{ "ok": false, "error": "...", "auth": false }`,前端会清会话并跳登录页;
- **写操作**必须在 JSON 体里带 `_csrf` 字段(或 `X-CSRF-Token` 头),
令牌从 `config_get` 接口获取。
## 写操作白名单
`config_save`、`roster_save`、`roster_import`、`roster_clear`、
`snapshot_save`、`snapshot_delete`、`unmatched_map`、`project_config_save`
## 接口清单
### 认证与配置
| Action | 方法 | 参数 | 说明 |
| --- | --- | --- | --- |
| `auth_check` | GET | — | 是否已登录 |
| `config_get` | GET | — | 返回配置(密码不回显)+ `csrf_token` + `storage` / `db_driver` / `db_connected` |
| `config_save` | POST | JSON 配置对象 | 保存;返回 `conn_changed` 与 `ping` 结果 |
| `config_test` | POST | 连接信息 | 测试连接,不落库 |
| `ping` | GET | — | 探测卷王是否可达 |
| `diag` | GET | — | 诊断信息(存储后端、DB 状态等) |
> **只有连接信息(地址 / 前缀 / 账号 / 密码 / 证书校验)变化时**,`config_save` 才会清除令牌并重新登录。
### 数据
| Action | 方法 | 参数 | 说明 |
| --- | --- | --- | --- |
| `projects` | GET | — | 卷王项目(问卷 / 考试)列表 |
| `data` | GET | `projectId`、`nameQid`、`force` | 主系统数据:统计、成绩行、名单、缺考、未匹配、异常 |
| `dashboard` | GET | `projectId`、`nameQid`、`force` | 按班级 / 组别聚合的看板数据 |
`data` 响应关键字段:
```
stats { total, submitted, absent, rate, avg, pass_rate, max, min, avg_duration }
rows[] { sid, name, cls, grp, score, best_score, attempts, attempts_list[],
submit_ts, submit_at, last_submit_ts, duration, duration_txt }
roster[] { sid, name, cls, grp, login }
absent[] 缺考名单
unmatched[] 未匹配答卷
anomalies { fast_high[], frequent[], identical[] }
project_cfg 本卷配置
_cache { cached, at }
```
### 名单
| Action | 方法 | 参数 | 说明 |
| --- | --- | --- | --- |
| `roster` | GET | — | 名单与元信息 |
| `roster_import` | POST | `multipart`:`file`、`mode=replace\|append` | 导入(扩展名白名单 + 随机名 + 解析后即删) |
| `roster_save` | POST | JSON `{ students: [...] }` | 全量替换(DELETE + INSERT) |
| `roster_clear` | POST | — | 清空 |
| `unmatched_map` | POST | JSON `{ name, sid, cls, grp, login }` | 未匹配答卷补入名单(学号已存在则更新姓名) |
### 每卷配置
| Action | 方法 | 参数 | 说明 |
| --- | --- | --- | --- |
| `project_config` | GET | `projectId` | 返回 `cfg`、`effective`(生效值)、`global`(全局默认) |
| `project_config_save` | POST | JSON `{ full_score, pass_rate, exempt[] }` | 留空/0 = 清除该项,回落全局 |
### 快照
| Action | 方法 | 参数 | 说明 |
| --- | --- | --- | --- |
| `snapshot_save` | POST | `projectId`、JSON `{ label }` | 保存当前成绩快照 |
| `snapshot_list` | GET | `projectId` | 列表 |
| `snapshot_delete` | POST | `projectId`、JSON `{ file }` | 删除 |
| `snapshot_trend` | GET | `projectId`(可逗号分隔多个,跨卷对比) | 趋势 |
### 导出
| Action | 方法 | 参数 | 说明 |
| --- | --- | --- | --- |
| `export` | GET | `projectId`、`cls`、`grp`、`mode=latest`、`split=cls`、`sort`、`cols` | 成绩明细 `.xlsx` |
| `export_absent` | GET | 同上 | 缺考名单 `.xlsx` |
| `draw_export` | GET | `type=import\|list` | 白名单导入模板 / 白名单列表 |
`sort` 取值:`time` / `sid` / `score_desc` / `score_asc`。
## 前端调用约定
```js
// 读
const d = await api('data', { params: { projectId, force: 1 } })
// 写:api() 会自动注入 _csrf
await api('roster_save', { json: { students } })
```
## 相关
- [架构与目录](./architecture.md)
- [测试](./testing.md)