refactor: 取消 install.php 安装向导 + 下线 pgsql/mysql 支持 + 班级看板独立
- 删除 install.php:所有入口(index/dashboard/draw/login/api)不再跳安装,
未登录统一跳 login.php;连接信息变更在【设置】里直接修改 data/config.json
- 下线 pgsql/mysql:
* bootstrap.php 简化为单文件存储路径(ny_load_config/ny_save_config 直接读写 data/config.json),
删除 ny_db_config、配置迁移、配置双写分支
* lib/Db.php 改为纯 stub:pdo() 恒返回 null,避免上层误连;migrate/ensureRosterLogin 等为 no-op
* Roster.php 经 useDb 永远 false,自动走 roster.json
* api.php 不再返回 db_driver/db_connected/db_error 诊断
- 班级看板独立(dashboard.php 重写):
* 删除"← 返回总览"按钮 + 整体概况 #overview 区
* 进入即展示单班看板:6 张汇总卡 + 分组对比/成员榜
* 顶部 hero 区动态显示当前班级名 + 问卷名
* 工具栏新增 #classSel 班级选择器(切换班级不离开本页)
* URL 支持 ?pid=&class=&auto=1|0 分享与刷新恢复
* 下拉班级选项随数据动态刷新;保留自动刷新 + 全屏 + 缓存
- e2e 测试(test-config-save)加固 cleanup 钩子:
* 统一 cleanup() 函数接管所有退出路径
* 解决之前 e2e 留 residue 把 data/config.json 改成 mock 地址的问题(导致本机登录失败)
- php -l 全文件通过;node --check 内嵌 JS 通过;e2e test-config-save 27/27 + test-draw-alloc 29/29 全过
This commit is contained in:
+28
-267
@@ -1,322 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* 数据库连接层(统一 PDO)
|
||||
*
|
||||
* 支持 PostgreSQL / MariaDB(MySQL) / SQLite(主要用于本地无 DB 时的降级与测试)。
|
||||
* 连接参数存于 data/db.json;通过 Db::instance() 取单例连接。
|
||||
* 未配置数据库时返回 null,上层回退到文件存储(config.json / roster.json)。
|
||||
* 数据库适配层(旧:pgsql/mysql/sqlite 三方言支持)
|
||||
* 本批次已下线,仅保留文件存储。如未来需要接入 DB,请在此文件实现 PDO 单例。
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
class Db
|
||||
{
|
||||
/** @var PDO|null */
|
||||
private static $pdo = null;
|
||||
private static string $driver = '';
|
||||
private static bool $tried = false;
|
||||
private static bool $failed = false;
|
||||
private static string $lastError = '';
|
||||
/** @var PDO|null 当前不可用,始终返回 null */
|
||||
private static ?PDO $pdo = null;
|
||||
|
||||
/** 数据库连接参数文件 */
|
||||
public static function dbFile(): string
|
||||
{
|
||||
return NY_DATA . '/db.json';
|
||||
}
|
||||
|
||||
/** 是否已配置数据库(db.json 存在且有 driver/host 等) */
|
||||
public static function isConfigured(): bool
|
||||
{
|
||||
$d = self::params();
|
||||
return !empty($d['driver']);
|
||||
}
|
||||
|
||||
/** 读取连接参数 */
|
||||
public static function params(): array
|
||||
{
|
||||
$d = ny_read_json(self::dbFile(), null);
|
||||
if (!is_array($d)) {
|
||||
return [];
|
||||
}
|
||||
return $d;
|
||||
}
|
||||
|
||||
/** 写入连接参数 */
|
||||
public static function saveParams(array $p): bool
|
||||
{
|
||||
return ny_write_json(self::dbFile(), $p);
|
||||
}
|
||||
|
||||
/** 当前驱动名(用于 SQL 方言):mysql | pgsql | sqlite | '' */
|
||||
public static function driver(): string
|
||||
{
|
||||
self::check();
|
||||
return self::$driver;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function isConnected(): bool
|
||||
{
|
||||
self::check();
|
||||
return self::$pdo !== null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function driver(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/** 上一次连接失败的原因(连接正常时为空) */
|
||||
public static function lastError(): string
|
||||
{
|
||||
self::check();
|
||||
return self::$lastError;
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 PDO 单例;未配置或连不上返回 null。
|
||||
*/
|
||||
public static function pdo(): ?PDO
|
||||
public static function params(): array
|
||||
{
|
||||
self::check();
|
||||
return self::$pdo;
|
||||
return [];
|
||||
}
|
||||
|
||||
private static function check(): void
|
||||
public static function saveParams(array $p): bool
|
||||
{
|
||||
if (self::$tried) {
|
||||
return;
|
||||
}
|
||||
self::$tried = true;
|
||||
$p = self::params();
|
||||
$driver = strtolower((string)($p['driver'] ?? ''));
|
||||
if ($driver === '') {
|
||||
return;
|
||||
}
|
||||
self::$driver = $driver;
|
||||
try {
|
||||
self::$pdo = self::connect($p);
|
||||
} catch (Throwable $e) {
|
||||
self::$failed = true;
|
||||
self::$pdo = null;
|
||||
self::$lastError = $e->getMessage();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 根据参数建 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'] ?? '');
|
||||
throw new RuntimeException('数据库后端已下线,请使用文件存储(data/config.json / data/roster.json)');
|
||||
}
|
||||
|
||||
$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 pdo(): ?PDO
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 表名前缀 */
|
||||
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;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 旧表升级: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)');
|
||||
}
|
||||
}
|
||||
public static function ensureRosterLogin(): void {}
|
||||
|
||||
/**
|
||||
* 把所有 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;
|
||||
return [];
|
||||
}
|
||||
|
||||
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 settingsSet(string $k, string $v): void {}
|
||||
|
||||
/** 名单:全量读 */
|
||||
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;
|
||||
return [];
|
||||
}
|
||||
|
||||
/** 名单:清空后全量写 */
|
||||
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 rosterReplace(array $students): void {}
|
||||
|
||||
/** 名单:行数 */
|
||||
public static function rosterCount(): int
|
||||
{
|
||||
$pdo = self::pdo();
|
||||
if ($pdo === null) {
|
||||
return 0;
|
||||
}
|
||||
return (int)$pdo->query('SELECT COUNT(*) FROM ny_roster')->fetchColumn();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user