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

114 lines
3.4 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.
# 数据存储(SQLite)
平台使用 **SQLite 单文件数据库** `data/ny.db`,零配置、无需额外服务。
## 启用条件与降级
```php
Db::isConfigured() // 是否有 pdo_sqlite 扩展
Db::isConnected() // 是否真的连上了
Db::lastError() // 失败原因
```
| 环境 | 行为 |
| --- | --- |
| 有 `pdo_sqlite` | 使用 `data/ny.db`(同时继续双写 JSON 文件作为备份) |
| 无 `pdo_sqlite` | 整体降级为 JSON 文件存储,功能不受影响 |
> 前端可通过 `config_get` 返回的 `storage`(`db` / `file`)判断当前用的是哪种。
## 连接参数
连接建立时施加的 PRAGMA:
```sql
PRAGMA journal_mode = WAL; -- 读写并发
PRAGMA synchronous = NORMAL;
PRAGMA busy_timeout = 5000; -- 写锁等待 5 秒
PRAGMA foreign_keys = ON;
```
连接**每请求只尝试一次**,失败后返回 `null` 而非抛异常,保证上层能优雅降级。
## 表结构
所有建表 / 加列都是**幂等**的(`CREATE TABLE IF NOT EXISTS` + `PRAGMA table_info` 探测后 `ALTER TABLE`),可重复执行。
### `ny_settings` — 全局配置
| 列 | 类型 | 说明 |
| --- | --- | --- |
| `k` | TEXT PK | 键,目前只有 `config` |
| `v` | TEXT | 整份配置的 JSON |
| `updated_at` | — | 更新时间 |
### `ny_roster` — 学号名单
| 列 | 类型 | 说明 |
| --- | --- | --- |
| `sid` | TEXT PK | 学号 |
| `name` | TEXT | 姓名 |
| `cls` | TEXT | 班级 |
| `grp` | TEXT | 组别 |
| `login` | TEXT | 登陆账号(仅用于抽取页白名单导出) |
| `sort` | INTEGER | 排序 |
> `login` 列是后期加入的,旧库会在启动时自动 `ALTER TABLE ADD COLUMN`(幂等)。
### `ny_project_config` — 每卷独立配置
| 列 | 类型 | 说明 |
| --- | --- | --- |
| `project_id` | TEXT PK | 卷王试卷 ID |
| `config_json` | TEXT | `{ full_score, pass_rate, exempt[] }` |
| `updated_at` | — | 更新时间 |
## 迁移策略
首次在有 SQLite 的环境运行时:
1. `ny_settings` 为空 → 从 `data/config.json` 迁入;
2. `ny_roster` 为空 → 从 `data/roster.json` 迁入(`Roster::migrateFileToDbOnce()`);
3. 每卷配置同理从 `data/project_config.json` 迁入。
**迁移只发生在目标表为空时**,不会覆盖已入库的数据;重复执行安全。
## 读写路径
| 数据 | 主存储 | 备份 |
| --- | --- | --- |
| 配置 | `ny_settings['config']` | `data/config.json` |
| 名单 | `ny_roster` | `data/roster.json` |
| 每卷配置 | `ny_project_config` | `data/project_config.json` |
| 成绩快照 | 文件系统 `data/snapshots/` | — |
| 结果缓存 | 文件系统 `data/cache/` | — |
| 卷王令牌 | 文件系统 `data/cache/token.json` | — |
`ny_save_config()` 双写:先写库,再写文件。这样即使数据库突然不可用,也不会静默回落到出厂默认值。
## 维护
```bash
# 备份(推荐:直接复制文件)
cp data/ny.db data/ny.db.bak
# 完整性检查
php -r '$p=new PDO("sqlite:data/ny.db"); var_dump($p->query("PRAGMA integrity_check")->fetchAll());'
# 手动查看名单
php -r '$p=new PDO("sqlite:data/ny.db"); print_r($p->query("SELECT * FROM ny_roster LIMIT 5")->fetchAll(PDO::FETCH_ASSOC));'
```
::: tip 命令行加载扩展
PHP CLI 常常不加载 Web 端已启用的扩展,需要显式指定:
```bash
php -d extension_dir="C:/php/ext" -d extension=pdo_sqlite script.php
```
:::
## 相关
- [架构与目录](./architecture.md)
- [接口一览](./api.md)