Skip to content

Database

dxx114514-stack edited this page Oct 16, 2026 · 5 revisions

数据库

概览

项目 说明
引擎 Node.js 内置 node:sqlite(v1.8.0 起,原生同步直写、零 npm 依赖;此前为 sql.js WASM)
文件 backend/data/NoldOJ.db(路径由 backend/config/config.js 的 database.path 决定)
迁移 启动时 initDB() 自动执行全部 schema 迁移(新增表/列),无需手工改库
驱动要求 Node >= 24(node:sqlite 为内置模块,无需 npm install 数据库驱动)

运行后数据库目录会出现 NoldOJ.db-wal / NoldOJ.db-shm 文件(WAL 模式,崩溃安全、读写更快),属正常现象,请勿手动删除。

数据目录

backend/data/
├── NoldOJ.db          # SQLite 主库(用户/题目/提交/比赛等全部结构化数据)
├── NoldOJ.db-wal      # WAL 日志(勿删)
├── NoldOJ.db-shm      # WAL 共享内存索引(勿删)
└── problems/          # 题目测试数据(.in/.out 等,与库同级便于整体备份)
  • 上传文件等运行时数据在仓库根 data/(自动创建)。
  • 首次启动若根目录存在旧的 problems/ 且 backend/data/problems/ 不存在,start.bat 会自动迁移。

首次初始化

  • 建库并建全部默认表、语言表,预置 10 个算法标签与成就(不含示例题目,题库从空开始)。
  • 创建默认管理员 admin:随机初始口令,仅启动日志 log/<时间戳>/server.log 显示一次;测试/CI 可用 NoldOJ_INIT_ADMIN_PASSWORD 指定(见 配置说明)。
  • config/jwt.txt 缺失时自动生成 JWT 密钥。

组与可见性

用户组与实体可见性由三张新表 + 四张实体表各一列 构成,全部由 initDB() 在启动时建出,老库升级同样自动完成,无需手工改库。

三张新表

表 字段 说明
groups id / name / description / kind / owner_id / created_at 用户组。kind 取 custom(管理员自建)、personal(个人组,每人一个,显示名=用户名)、all_users(全体注册用户系统组,全局唯一一个)
user_groups user_id / group_id / joined_at,主键 (user_id, group_id) 用户与组的成员关系
resource_group_access resource_type / resource_id / group_id,主键三元组 实体与组的可见性绑定。resource_type 取 problem / contest / exam / problem_set,仅在实体 visibility='groups' 时生效

索引(均为 IF NOT EXISTS 幂等建):

索引 定义 作用
idx_groups_personal groups(owner_id) WHERE kind='personal'(唯一) 保证每人至多一个个人组
idx_groups_all_users groups(kind) WHERE kind='all_users'(唯一) 保证全体组全局只有一个
idx_user_groups_group user_groups(group_id) 按组反查成员
idx_rga_resource resource_group_access(resource_type, resource_id) 按实体取绑定组

三张表之间没有外键:删组时由 repositories/groups.js 在一个事务里手工删除 user_groups 与 resource_group_access 的对应行后再删 groups。

实体表新增 visibility 列

problems / contests / exams / problem_sets 四张表各新增同一列:

visibility TEXT NOT NULL DEFAULT 'public' CHECK(visibility IN ('public','groups'))
  • public(默认):所有人可见,匿名访客也算。
  • groups:仅 resource_group_access 中绑定的组成员可见;teacher/admin/su 一律放行(教师必须能看到实体,才能把实体授权给某个组)。
  • 旧的 is_public(题目/题单的公开标记)与 is_hidden(试卷的隐藏开关)原样保留,未被替换或删除。
  • 迁移对四张表逐一检查列是否存在再 ALTER TABLE ... ADD COLUMN(幂等);DEFAULT 'public' 本身就让存量实体保持公开,不需要额外的回填语句。

自动入组触发器

CREATE TRIGGER IF NOT EXISTS trg_users_default_groups AFTER INSERT ON users
BEGIN
  -- 1) 建个人组(name=NEW.username, kind='personal', owner_id=NEW.id)
  -- 2) UPDATE 把复用同号时残留的旧用户名改回新用户名
  -- 3) 把自己加进个人组
  -- 4) 把自己加进全体组(kind='all_users')
END;
  • 触发器挂在 users 的 AFTER INSERT 上,任何建号途径(注册接口、管理员建号、初始 admin、后台脚本、测试直接 INSERT)都会自动加入「所有用户」与个人两个组,不依赖某一条注册代码路径的配合。
  • 用 INSERT OR IGNORE + 按唯一键回查组 id(而不是 last_insert_rowid()):users.id 可被复用(删号后新建会取回同号),重复入组不该让注册语句因唯一索引冲突而失败。
  • 系统组的成员关系因此没有自助变更入口 —— routes/groups.js 对系统组的改名、删除、增减成员一律返回 403 ERR_FORBIDDEN。

启动迁移与回填行为

initDB() 每次启动按固定顺序执行,全部幂等、可反复运行:

  1. tableExists 检查三张新表,缺失则建表(DDL 与 schema.sql 逐字一致;两处都放:新库走 schema.sql 全量建表,老库走这段兜底自愈)。
  2. 无条件重建索引与触发器(CREATE INDEX IF NOT EXISTS / CREATE TRIGGER IF NOT EXISTS),覆盖老库缺表自愈、以及触发器随版本更新的路径。
  3. 补 visibility 列:四张实体表缺列时逐表 ALTER TABLE。
  4. 回填存量成员关系:不存在则建「所有用户」组,再遍历 users 为每个存量用户建个人组并写入两条成员关系(个人组 + 全体组)。触发器只覆盖本次启动之后的新建号,这里负责存量用户(含本次启动创建的 admin)。

对应接口见 API 文档,操作说明见 使用指南。

备份与恢复

备份(务必先停服):

  1. 关闭 start.bat 窗口(或执行 stop.bat)。
  2. 整体复制 backend/data/ 目录(至少包含 NoldOJ.db* 与 problems/)。

恢复:停服后用备份覆盖回 backend/data/,再启动。

单库校验:

node scripts\migrate-sqlite.js

该脚本会自动备份 NoldOJ.db → NoldOJ.db.backup-<时间戳>、执行 PRAGMA integrity_check 完整性校验、统计全部业务表行数。

从 sql.js 迁移(v1.8.0 历史版本升级)

sql.js 导出的文件本身就是标准 SQLite 格式,旧数据无需转换,node:sqlite 可直接打开、原样保留。

  1. 停止服务(务必先停,避免文件占用)。
  2. git pull
  3. cd backend && npm install(无需数据库驱动)
  4. (可选但推荐)node scripts\migrate-sqlite.js —— 备份 + 完整性校验 + 数据统计
  5. start.bat —— 启动时自动完成 schema 迁移

回滚:用第 4 步生成的备份覆盖回 NoldOJ.db 即可。

测试隔离

  • 需要数据库的测试不读写生产库:自动使用 %TEMP%\NoldOJ-test\ 下的独立库,测试中断也不留脏数据。
  • 可用 DB_PATH 环境变量指定其他库路径(见 测试与 CI)。

相关文档

Clone this wiki locally