第 2 阶段:账号与班级,作品保存在服务器
- SQLite(bun:sqlite,编号迁移):班级、用户、会话(只存令牌哈希)、作品 - 登录取代统一访问口令:老师账号由配置设定;接口和 WebSocket 都要登录;按 IP 限制输错次数 - 班级管理页:建班、改名、删除(有学生时不能删);上传 CSV 名单导入(兼容 GBK / UTF-8, 识别表头,标出被 Excel 变成科学计数法的学号),密码留空生成 6 位数字;查看、重置密码, 导出带密码的名单 - 学生作品自动保存到服务器(按账号和实验),显示保存状态;离开页面前立即保存 - 前端加路由:登录页、实验页、班级管理页 - 部署:TEACHER_PASSWORD 必填,数据库放在数据卷 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a00622be15
commit
7cec273d58
31 files changed
+1776
-382
No files matched your search
@@ -0,0 +1,58 @@
|
||||
// SQLite 数据库:账号、班级、登录会话、学生作品。
|
||||
// 表结构用编号迁移:MIGRATIONS 只能在末尾追加,已执行过的不再改动。
|
||||
import { Database } from 'bun:sqlite'
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { config } from './config'
|
||||
|
||||
const MIGRATIONS = [
|
||||
`
|
||||
CREATE TABLE classes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
-- 学生的密码由老师发放、老师可以查看,所以按原样保存(学生不能自己改)
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('student', 'teacher')),
|
||||
class_id INTEGER REFERENCES classes (id),
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX users_class ON users (class_id);
|
||||
-- 只保存令牌的哈希,数据库泄露也拿不到可用的登录令牌
|
||||
CREATE TABLE sessions (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
-- 学生在某个实验上的代码(自动保存),key 如 "sample:p2-t1-LED显示屏"
|
||||
CREATE TABLE works (
|
||||
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
files TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, key)
|
||||
);
|
||||
`,
|
||||
]
|
||||
|
||||
/** 打开数据库并执行未执行过的迁移。path 为 ":memory:" 时用内存数据库(测试用) */
|
||||
export function openDatabase(path = join(config.dataDir, 'iot-sim.db')) {
|
||||
if (path !== ':memory:') mkdirSync(config.dataDir, { recursive: true })
|
||||
const db = new Database(path, { create: true, strict: true })
|
||||
db.run('PRAGMA journal_mode = WAL')
|
||||
db.run('PRAGMA foreign_keys = ON')
|
||||
db.run('PRAGMA busy_timeout = 5000')
|
||||
const version = db.query<{ user_version: number }, []>('PRAGMA user_version').get()!.user_version
|
||||
for (let i = version; i < MIGRATIONS.length; i++) {
|
||||
db.transaction(() => {
|
||||
db.run(MIGRATIONS[i])
|
||||
db.run(`PRAGMA user_version = ${i + 1}`)
|
||||
})()
|
||||
}
|
||||
return db
|
||||
}
|
||||
Reference in new issue
Block a user