"""数据库连接、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"])