diff --git a/api.php b/api.php index 032ffe4..697a32d 100644 --- a/api.php +++ b/api.php @@ -13,11 +13,7 @@ function cfg(): array return ny_load_config(); } -// 会话/安装保护:除 auth_check 外,未安装或未登录一律拒访 -if (!ny_is_installed()) { - ny_json(['ok' => false, 'msg' => '系统尚未安装,请先完成安装向导'], 503); - exit; -} +// 会话保护:除 auth_check 外,未登录一律拒访 if ($action !== 'auth_check' && !ny_is_auth()) { ny_json(['ok' => false, 'msg' => '未登录或会话已过期,请重新登录'], 401); exit; @@ -56,7 +52,7 @@ try { $new[$k] = is_string($in[$k]) ? trim((string)$in[$k]) : $in[$k]; } } - // 密码:与 install.php 一致不做 trim(保留首尾空格的原貌),空 / 占位符 = 不修改 + // 密码:保留首尾空格的原貌,空 / 占位符 = 不修改 if (array_key_exists('password', $in)) { $pwdIn = (string)$in['password']; if ($pwdIn !== '' && $pwdIn !== '******') { diff --git a/bootstrap.php b/bootstrap.php index 7bc030f..52d286f 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -80,17 +80,13 @@ function ny_logout(): void session_destroy(); } -/** 判断是否已安装(有过连接配置且能流转):文件 config 或 DB settings 存在 base_url 且账号非空 */ +/** 判断是否已安装(只看文件 config:base_url 非空即视为已具备连接配置) */ function ny_is_installed(): bool { $cfg = ny_load_config(); if (trim((string)($cfg['base_url'] ?? '')) === '') { return false; } - // 有无可用的后端(文件 config 存在 ,或 DB 连得上) - if (class_exists('Db') && Db::isConnected()) { - return true; - } return is_file(ny_config_file()); } @@ -145,21 +141,7 @@ function ny_load_config(): array { $GLOBALS['__ny_cfg_source'] = 'default'; - // 数据库已初始化且能连上 → 从 DB 读,否则回退文件 - $dbCfg = ny_db_config(); - if ($dbCfg !== null) { - $cfg = ny_default_config(); - foreach ($cfg as $k => $v) { - if (array_key_exists($k, $dbCfg)) { - $cfg[$k] = is_bool($v) ? (bool)$dbCfg[$k] : $dbCfg[$k]; - } - } - $cfg['base_url'] = rtrim(trim((string)$cfg['base_url']), '/'); - $cfg['api_prefix'] = '/' . trim((string)$cfg['api_prefix'], '/'); - $GLOBALS['__ny_cfg_source'] = 'db'; - return $cfg; - } - + // 默认配置 + 文件覆盖(DB 后端本批次已下线,统一走文件存储) $cfg = ny_default_config(); $saved = ny_read_json(ny_config_file(), null); if (is_array($saved)) { @@ -169,15 +151,6 @@ function ny_load_config(): array } } $GLOBALS['__ny_cfg_source'] = 'file'; - // DB 已连上但库里没有配置,而文件里有 —— 迁移一份到 DB,避免两端不一致 - if (class_exists('Db') && Db::pdo() !== null && empty($GLOBALS['__ny_cfg_migrated'])) { - $GLOBALS['__ny_cfg_migrated'] = true; - try { - Db::settingsSet('config', json_encode($cfg, JSON_UNESCAPED_UNICODE)); - } catch (Throwable $e) { - // 迁移失败不影响本次使用 - } - } } $cfg['base_url'] = rtrim(trim((string)$cfg['base_url']), '/'); $cfg['api_prefix'] = '/' . trim((string)$cfg['api_prefix'], '/'); @@ -186,44 +159,8 @@ function ny_load_config(): array function ny_save_config(array $cfg): bool { - // 先写文件快照:DB 万一不可用(连不上 / 未初始化)时不会静默回退到出厂默认配置 - $fileOk = ny_write_json(ny_config_file(), $cfg); - - if (class_exists('Db')) { - $db = Db::pdo(); - if ($db !== null) { - try { - Db::settingsSet('config', json_encode($cfg, JSON_UNESCAPED_UNICODE)); - $GLOBALS['__ny_cfg_source'] = 'db'; - return true; - } catch (Throwable $e) { - // 落库失败则回退文件 - $GLOBALS['__ny_cfg_source'] = 'file'; - return $fileOk; - } - } - } $GLOBALS['__ny_cfg_source'] = 'file'; - return $fileOk; -} - -/** 数据库初始化后从 DB 读取配置(无则返回 null) */ -function ny_db_config(): ?array -{ - if (!class_exists('Db')) { - return null; - } - $db = Db::pdo(); - if ($db === null) { - return null; - } - try { - $raw = (string)(Db::settingsAll()['config'] ?? ''); - } catch (Throwable $e) { - return null; - } - $data = json_decode($raw, true); - return is_array($data) ? $data : null; + return ny_write_json(ny_config_file(), $cfg); } /** 安全的字符串化(避免数组/对象转字符串报错) */ diff --git a/dashboard.php b/dashboard.php index e32de29..a2e733d 100644 --- a/dashboard.php +++ b/dashboard.php @@ -1,11 +1,11 @@ &class=<班级名> 分享与刷新恢复;缺省自动选第一个班级。 + */ declare(strict_types=1); require __DIR__ . '/bootstrap.php'; -if (!ny_is_installed()) { - header('Location: install.php'); - exit; -} if (!ny_is_auth()) { header('Location: login.php'); exit; @@ -16,7 +16,7 @@ if (!ny_is_auth()) { -数据看板 · <?= htmlspecialchars(NY_APP_NAME) ?> +班级看板 · <?= htmlspecialchars(NY_APP_NAME) ?> @@ -27,12 +27,13 @@ if (!ny_is_auth()) {
- NY问卷考试系统 · 数据看板 - 班级整体情况 · 面向班主任与学生 + 班级看板 + 选择一个问卷与班级查看考试整体情况
- + + -

如需修改系统地址请重新打开 install.php

+

如需修改请编辑 data/config.json(保存后清 token 自动重连)

diff --git a/install.php b/install.php deleted file mode 100644 index 0243b25..0000000 --- a/install.php +++ /dev/null @@ -1,203 +0,0 @@ - $backend, - 'host' => trim((string)($_POST['db_host'] ?? '127.0.0.1')), - 'port' => (int)($_POST['db_port'] ?? ($backend === 'pgsql' ? 5432 : 3306)), - 'dbname' => trim((string)($_POST['db_name'] ?? '')), - 'user' => trim((string)($_POST['db_user'] ?? '')), - 'password' => (string)($_POST['db_pass'] ?? ''), - ]; - if ($dbp['dbname'] === '') { - $err = '请填写数据库名。'; - } else { - try { - Db::saveParams($dbp); - // 重新建立连接并建表 - $pdo = Db::connect($dbp); - Db::migrate(); - } catch (Throwable $e) { - $err = '数据库连接/初始化失败:' . $e->getMessage(); - } - } - } - - if ($err === '') { - // 2) 写入系统配置(DB 后端写 settings['config'],文件后端写 config.json) - $cfg = ny_default_config(); - $cfg['base_url'] = $base; - $cfg['api_prefix'] = $prefix; - $cfg['username'] = $user; - $cfg['password'] = $pwd; - $cfg['verify_ssl'] = $verify; - $cfg['full_score'] = $full; - $saved = ny_save_config($cfg); - if ($saved) { - // 清理 token 缓存(连接信息变化) - @unlink(NY_DATA . '/cache/token.json'); - $ok = true; - } else { - if ($backend === 'file') { - $err = '无法写入文件配置,请检查 data/ 目录权限。'; - } else { - $err = '配置写入失败(已建表但写入配置异常)。'; - } - } - } - } -} - -$cfg = ny_load_config(); -?> - - - - - -首次安装 · <?= htmlspecialchars(NY_APP_NAME) ?> - - - - -
-
- - -
- - - -

- 若连接信息有误或未生效,请重新访问本页配置。

- - -
- - -
- -
- - - -
-
- - - -
- - -
-
- - -
-
- - -

用于合格率(≥60%满分),各试卷可在设置中单独调整

-
-
- - -
-
- - -
-
- -
- -
- -
-
- -
-
- - - diff --git a/lib/Db.php b/lib/Db.php index 16ab393..abb4f1f 100644 --- a/lib/Db.php +++ b/lib/Db.php @@ -1,322 +1,83 @@ 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; } } diff --git a/login.php b/login.php index b4e204e..6ca9580 100644 --- a/login.php +++ b/login.php @@ -1,7 +1,8 @@ 登录
-
如系统连接信息已变更,请先完成重新安装/配置
diff --git a/tools/test-config-save.js b/tools/test-config-save.js index 51ea556..a9dce34 100644 --- a/tools/test-config-save.js +++ b/tools/test-config-save.js @@ -37,18 +37,37 @@ fs.writeFileSync(cfgFile, JSON.stringify({ layout: 'default', }, null, 2)); +// 统一收尾钩子:恢复原 config + token + kill 子进程。任何退出路径都会触发 +let cleanedUp = false; +function cleanup() { + if (cleanedUp) return; + cleanedUp = true; + try { srv && srv.kill('SIGKILL'); } catch(e){} + try { mock && mock.kill('SIGKILL'); } catch(e){} + try { + if (origCfg !== null) fs.writeFileSync(cfgFile, origCfg); + else if (fs.existsSync(cfgFile)) fs.unlinkSync(cfgFile); + } catch(e) { console.error('cleanup cfg failed:', e.message); } + try { + if (hasToken && fs.existsSync(tokenBak)) fs.renameSync(tokenBak, tokenFile); + } catch(e) { console.error('cleanup token failed:', e.message); } +} +process.on('exit', cleanup); +process.on('SIGINT', () => { cleanup(); process.exit(1); }); +process.on('uncaughtException', (e) => { console.error('FATAL', e); cleanup(); process.exit(2); }); + // 2) 启动 mock 卷王 -const mock = spawn('node', ['tools/mock-server.js'], { cwd: ROOT, stdio: 'pipe' }); +let mock = null; +let srv = null; +mock = spawn('node', ['tools/mock-server.js'], { cwd: ROOT, stdio: 'pipe' }); mock.stdout.on('data', d => process.stdout.write('[mock] ' + d)); mock.stderr.on('data', d => process.stderr.write('[mock-err] ' + d)); // 3) 启动 PHP 内置服务器(-t 让所有请求走 index.php 路由) const phpArgs = INI.split(/\s+/).filter(Boolean).concat(['-S', '127.0.0.1:' + PORT, '-t', ROOT]); -const srv = spawn(PHP, phpArgs, { cwd: ROOT, stdio: 'pipe' }); +srv = spawn(PHP, phpArgs, { cwd: ROOT, stdio: 'pipe' }); srv.stdout.on('data', d => process.stdout.write('[php] ' + d)); srv.stderr.on('data', d => process.stderr.write('[php-err] ' + d)); -process.on('exit', () => { try { srv.kill('SIGKILL'); mock.kill('SIGKILL'); } catch(e){} }); -process.on('SIGINT', () => { srv.kill('SIGKILL'); mock.kill('SIGKILL'); process.exit(1); }); let pass = 0, fail = 0; function chk(name, cond, extra) { @@ -279,10 +298,7 @@ async function login() { console.log('\n结果:' + pass + ' 通过,' + fail + ' 失败'); - // 清理 - mock.kill('SIGTERM'); - srv.kill('SIGTERM'); - if (origCfg !== null) fs.writeFileSync(cfgFile, origCfg); else if (fs.existsSync(cfgFile)) fs.unlinkSync(cfgFile); - if (hasToken && fs.existsSync(tokenBak)) fs.renameSync(tokenBak, tokenFile); + // 清理 + 退出(统一走 cleanup 钩子,保证 config/token 一定被还原) + cleanup(); setTimeout(() => process.exit(fail === 0 ? 0 : 1), 200); -})().catch(e => { console.error('FATAL', e); try { mock.kill(); srv.kill(); } catch(z){}; process.exit(2); }); +})().catch(e => { console.error('FATAL', e); cleanup(); process.exit(2); });