getMessage(); } } /** 根据参数建 PDO 连接 */ public static function connect(array $p): PDO { $driver = strtolower((string)($p['driver'] ?? '')); $host = (string)($p['host'] ?? '127.0.0.1'); $port = (int)($p['port'] ?? 0); $db = (string)($p['dbname'] ?? ($p['database'] ?? '')); $user = (string)($p['user'] ?? ($p['username'] ?? '')); $pass = (string)($p['password'] ?? ''); $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; if ($driver === 'sqlite') { $dsn = 'sqlite:' . ($p['path'] ?? ($db ?: (NY_DATA . '/app.sqlite'))); } elseif ($driver === 'pgsql') { $dsn = 'pgsql:host=' . $host; if ($port > 0) { $dsn .= ';port=' . $port; } $dsn .= ';dbname=' . $db; $dsn .= ';user=' . $user; $dsn .= ';password=' . $pass; } else { // mysql / mariadb $dsn = 'mysql:host=' . $host; if ($port > 0) { $dsn .= ';port=' . $port; } $dsn .= ';dbname=' . $db . ';charset=utf8mb4'; $options[PDO::ATTR_PERSISTENT] = false; } $pdo = new PDO($dsn, $user, $pass, $options); if ($driver === 'mysql') { $pdo->exec("SET NAMES utf8mb4"); } return $pdo; } /** 表名前缀 */ public static function prefix(): string { return 'ny_'; } /** * 建表(幂等)。返回已执行语句数量。 * 用通用 SQL,三方言均可执行(表名/列名小写、规范命名)。 */ public static function migrate(): int { $pdo = self::pdo(); if ($pdo === null) { throw new RuntimeException('数据库未连接,无法初始化表结构'); } $driver = self::$driver; $n = 0; // settings(key,value) —— 系统设置键值表 if ($driver === 'sqlite') { $sql = 'CREATE TABLE IF NOT EXISTS ny_settings (' . 'k VARCHAR(128) PRIMARY KEY, ' . 'v TEXT)'; } elseif ($driver === 'pgsql') { $sql = 'CREATE TABLE IF NOT EXISTS ny_settings (' . 'k VARCHAR(128) PRIMARY KEY, ' . 'v TEXT)'; } else { $sql = 'CREATE TABLE IF NOT EXISTS ny_settings (' . '`k` VARCHAR(128) NOT NULL PRIMARY KEY, ' . '`v` TEXT) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'; } $pdo->exec($sql); $n++; // roster —— 名单表(学号/姓名/班级/组别/登陆账号) if ($driver === 'sqlite') { $sql = 'CREATE TABLE IF NOT EXISTS ny_roster (' . 'id INTEGER PRIMARY KEY AUTOINCREMENT, ' . 'sid VARCHAR(64), name VARCHAR(128), cls VARCHAR(64), grp VARCHAR(32), login VARCHAR(64))'; } elseif ($driver === 'pgsql') { $sql = 'CREATE TABLE IF NOT EXISTS ny_roster (' . 'id SERIAL PRIMARY KEY, ' . 'sid VARCHAR(64), name VARCHAR(128), cls VARCHAR(64), grp VARCHAR(32), login VARCHAR(64))'; } else { $sql = 'CREATE TABLE IF NOT EXISTS ny_roster (' . 'id INT AUTO_INCREMENT PRIMARY KEY, ' . 'sid VARCHAR(64), name VARCHAR(128), cls VARCHAR(64), grp VARCHAR(32), login VARCHAR(64)) ' . 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'; } $pdo->exec($sql); $n++; // 旧版本升级:补 login 列 self::ensureRosterLogin(); return $n; } /** 旧表升级:ny_roster 缺 login 列时自动补(三方言兼容) */ public static function ensureRosterLogin(): void { $pdo = self::pdo(); if ($pdo === null) { return; } $has = false; if (self::$driver === 'sqlite') { foreach ($pdo->query('PRAGMA table_info(ny_roster)')->fetchAll() as $c) { if (strtolower((string)$c['name']) === 'login') { $has = true; break; } } } elseif (self::$driver === 'pgsql') { $st = $pdo->prepare("SELECT COUNT(*) FROM information_schema.columns " . "WHERE table_name = 'ny_roster' AND column_name = 'login'"); $st->execute(); $has = (int)$st->fetchColumn() > 0; } else { $has = $pdo->query("SHOW COLUMNS FROM ny_roster LIKE 'login'")->fetch() !== false; } if (!$has) { $pdo->exec('ALTER TABLE ny_roster ADD COLUMN login VARCHAR(64)'); } } /** * 把所有 settings 读成数组 */ public static function settingsAll(): array { $pdo = self::pdo(); if ($pdo === null) { return []; } $out = []; $stmt = $pdo->query('SELECT k, v FROM ny_settings'); foreach ($stmt->fetchAll() as $row) { $out[$row['k']] = $row['v']; } return $out; } public static function settingsSet(string $k, string $v): void { $pdo = self::pdo(); if ($pdo === null) { return; } $drv = self::$driver; if ($drv === 'pgsql') { $sql = 'INSERT INTO ny_settings(k, v) VALUES(?, ?) ' . 'ON CONFLICT (k) DO UPDATE SET v = EXCLUDED.v'; } elseif ($drv === 'sqlite') { $sql = 'INSERT OR REPLACE INTO ny_settings(k, v) VALUES(?, ?)'; } else { $sql = 'INSERT INTO ny_settings(k, v) VALUES(?, ?) ' . 'ON DUPLICATE KEY UPDATE v = VALUES(v)'; } $pdo->prepare($sql)->execute([$k, $v]); } /** 名单:全量读 */ public static function rosterAll(): array { $pdo = self::pdo(); if ($pdo === null) { return []; } self::ensureRosterLogin(); $stmt = $pdo->query('SELECT sid, name, cls, grp, login FROM ny_roster ORDER BY id'); $rows = []; foreach ($stmt->fetchAll() as $r) { $rows[] = [ 'sid' => (string)$r['sid'], 'name' => (string)$r['name'], 'cls' => (string)$r['cls'], 'grp' => (string)$r['grp'], 'login' => (string)($r['login'] ?? ''), ]; } return $rows; } /** 名单:清空后全量写 */ public static function rosterReplace(array $students): void { $pdo = self::pdo(); if ($pdo === null) { return; } self::ensureRosterLogin(); $pdo->beginTransaction(); try { $pdo->exec('DELETE FROM ny_roster'); $st = $pdo->prepare('INSERT INTO ny_roster(sid, name, cls, grp, login) VALUES(?, ?, ?, ?, ?)'); foreach ($students as $s) { $st->execute([ (string)($s['sid'] ?? ''), (string)($s['name'] ?? ''), (string)($s['cls'] ?? ''), (string)($s['grp'] ?? ''), (string)($s['login'] ?? ''), ]); } $pdo->commit(); } catch (Throwable $e) { $pdo->rollBack(); throw $e; } } /** 名单:行数 */ public static function rosterCount(): int { $pdo = self::pdo(); if ($pdo === null) { return 0; } return (int)$pdo->query('SELECT COUNT(*) FROM ny_roster')->fetchColumn(); } }