Files
selfgroup/group_system/storage/database.py
2026-09-09 16:43:57 +08:00

143 lines
5.7 KiB
Python

"""数据库连接、Schema 和系统级设置。"""
from __future__ import annotations
import sqlite3
import threading
from pathlib import Path
from typing import Any
from group_system.config import GROUP_LIMIT, TEACHER_PASSWORD, TEACHER_USER
from group_system.security import hash_password
class DatabaseCore:
def __init__(self, path: Path):
self.path = path
self.lock = threading.RLock()
self.connection = sqlite3.connect(str(path), check_same_thread=False)
self.connection.row_factory = sqlite3.Row
self.connection.execute("PRAGMA foreign_keys = ON")
self.init_database()
def init_database(self) -> None:
with self.lock, self.connection:
self.connection.executescript(
"""
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
class_name TEXT NOT NULL,
student_no TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
gender TEXT DEFAULT '',
admission_no TEXT DEFAULT '',
remark TEXT DEFAULT '',
password_hash TEXT NOT NULL,
must_change_password INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
class_name TEXT NOT NULL,
name TEXT NOT NULL,
leader_student_id INTEGER REFERENCES students(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS group_members (
group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
student_id INTEGER NOT NULL UNIQUE REFERENCES students(id) ON DELETE CASCADE,
joined_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS class_controls (
class_name TEXT PRIMARY KEY,
locked INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
token_hash TEXT PRIMARY KEY,
role TEXT NOT NULL,
username TEXT NOT NULL,
student_id INTEGER REFERENCES students(id) ON DELETE CASCADE,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
);
"""
)
self.connection.execute(
"INSERT OR IGNORE INTO settings(key,value) VALUES('active_class','1')"
)
self.connection.execute(
"INSERT OR IGNORE INTO settings(key,value) VALUES('teacher_username',?)",
(TEACHER_USER,),
)
self.connection.execute(
"INSERT OR IGNORE INTO settings(key,value) VALUES('teacher_password',?)",
(hash_password(TEACHER_PASSWORD),),
)
self.connection.execute(
"INSERT OR IGNORE INTO settings(key,value) VALUES('group_limit',?)",
(str(GROUP_LIMIT),),
)
def get_setting(self, key: str, conn: sqlite3.Connection | None = None) -> str | None:
conn = conn or self.connection
row = conn.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone()
return None if row is None else row["value"]
def set_setting(self, key: str, value: str) -> None:
with self.lock, self.connection:
self.connection.execute(
"INSERT INTO settings(key,value) VALUES(?,?) "
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
(key, value),
)
def class_list(self) -> list[dict[str, Any]]:
with self.lock:
rows = self.connection.execute(
"""SELECT class_name, COUNT(*) AS student_count
FROM students GROUP BY class_name
ORDER BY CAST(class_name AS REAL), class_name"""
).fetchall()
active = self.get_setting("active_class")
locked = {
row["class_name"]: bool(row["locked"])
for row in self.connection.execute("SELECT class_name,locked FROM class_controls")
}
return [
{
"class_name": row["class_name"],
"student_count": row["student_count"],
"locked": bool(locked.get(row["class_name"], False)),
"active": row["class_name"] == active,
}
for row in rows
]
def active_class(self) -> str:
return self.get_setting("active_class") or ""
def group_limit(self) -> int:
value = self.get_setting("group_limit")
try:
limit = int(value or GROUP_LIMIT)
except ValueError:
limit = GROUP_LIMIT
return max(2, limit)
def set_group_limit(self, limit: int) -> tuple[bool, str]:
if limit < 2 or limit > 100:
return False, "分组上限必须在 2 到 100 人之间"
self.set_setting("group_limit", str(limit))
return True, f"分组上限已调整为 {limit} 人"
def is_locked(self, class_name: str) -> bool:
with self.lock:
row = self.connection.execute(
"SELECT locked FROM class_controls WHERE class_name=?", (class_name,)
).fetchone()
return bool(row and row["locked"])