Files
nxxy335 25a2e8a42c 文档部署到 /ny-exam-platform 子路径 + 移除快照/趋势/异常 + 打印与 UI 全面完善
文档站(#88)
- base 默认改为 /ny-exam-platform/,可用 DOCS_BASE=/ 覆盖本地预览
- 新增 DOCS_OUT 环境变量,避免构建时清空输出目录失败导致产物半截
- 新增 tools/docs-preview.js:按子路径重写路由的本地预览服务
- 删除 docs/guide/snapshot.md,同步清理全部相关文档

移除存快照 / 趋势 / 异常(#89)
- 后端:删除 api.php 的 snapshot_save/list/delete/trend 四个 action 与
  $WRITE_ACTIONS 中的同名条目;删除 lib/Snapshot.php 与自动加载注册;
  DataService 移除 detectAnomalies() 及其调用
- 前端:移除三个按钮与三个弹窗、相关 JS 处理与 state 字段
- 测试:移除对应用例,补充 5 条名单导入列识别断言(17+9+18+32+29+11 全过)

打印完善(#90)
- 改用隐藏 iframe 输出,规避弹窗拦截;@page A4 支持纵向/横向
- 表头每页重复、行内不跨页断开、全表连续名次、可选「是否合格」列
- 分组小计(人数/平均/最高/最低/合格人数与合格率)、可附缺考名单另起一页
- 末尾签字栏;缺考名单打印改为同一套输出

UI / 交互 / Bug 修复(#91)
- dashboard.php:补「← 主系统 / 退出」入口(此前全屏后无法离开);
  修复成员下钻 onclick 里 JSON.stringify 双引号提前闭合属性导致点击报错,
  改为 data-sid 事件委托;补 Esc 关弹窗;后台标签页暂停轮询、切回自动补拉;
  刷新中断原因写入 title;数值字段统一兜底,避免 NaN / undefined%
- draw.php / draw.js:结果表新增「登陆账号」列(导出前可核对,缺失显示 学号 ⚠);
  弹窗只关最上层且支持 Esc;「全部」改为跟随候选池,手工改人数即解除;
  候选池不足时给出明确提示;自定义比例框隐藏时清空,避免旧权重被误读
- app.js / dashboard.php:成绩着色改为按「占本卷满分的百分比」判断,
  修复满分不是 100 时整列误标红
- style.css:数值列右对齐;新增 subbar、draw-stage-sub;
  看板顶栏窄屏下拉与按钮换行,消除横向滚动
2026-09-11 16:23:08 +08:00

113 lines
3.4 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.
# 数据存储(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/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)