Files
ny-exam-platform/install.php
T
nxxy335 8686eb99c7 feat: 接入PostgreSQL/MariaDB + 首次安装向导 + 登录页独立 + 设置重构 + 文件夹识别兜底
- 数据库层 lib/Db.php:统一 PDO 支持 pgsql/mysql/sqlite,连接参数存 data/db.json
  (git-ignored),migrate 建 ny_settings/ny_roster 表(三方言兼容 upsert)
- 配置/名单读写:bootstrap ny_load_config/ny_save_config 与 Roster 均改为
  '数据库可用则走 DB、否则回退 data/config.json/roster.json' 的文件降级模式
- 首次安装向导 install.php:选择存储后端(文件/PostgreSQL/MariaDB),测试连接+
  建表+写入卷王连接信息与默认满分;支持 ?force=1 重新配置
- 登录门分离:新增 login.php 独立登录页(仅需密码,账号预填)+logout.php+
  会话(ny_session_start/ny_is_auth/ny_login/ny_logout);index/dashboard 未安装
  跳 install、未登录跳 login;api.php 加会话保护与 auth_check
- 设置重构:删除卷王系统地址/接口前缀/登录账号/登录密码(连接信息由安装向导
  配置);新增布局配置(默认/成绩明细与缺考名单同行)与各试卷独立满分列表(默认
  分数作用于全部);工具栏布局下拉移除
- 文件夹识别兜底:app.js/dashboard.php 双策略 — mode 含 folder 优先,缺省时用
  parent_id 引用反推(应对部分版本无 mode 字段);空文件夹自动隐藏

验证:php -l 全部通过、node --check app.js 通过;SQLite 等价实测 Db 层
(migrate/config upsert/Roster 读写/ny_load_config)与会话辅助函数全部PASS;
e2e(内置服务器)验证路由:未装→install、装完→login、index/dashboard 未登录
跳转、api 401 保护、?force=1 重配、logout 302 均正确。本机无真实 pgsql/mysql
服务器,DB 连接按部署环境实测。
2026-09-09 07:15:56 +08:00

204 lines
9.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 首次安装向导:配置数据存储后端(文件 / PostgreSQL / MariaDB)与卷王系统连接信息。
* 访问本页时若已安装则自动跳转 /login。
*/
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
// 已安装:默认跳转登录页;仅当带 ?force=1 或提交 POST 时才允许重新配置
if (ny_is_installed() && empty($_GET['force']) && $_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: login.php');
exit;
}
$err = '';
$ok = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$backend = (string)($_POST['backend'] ?? 'file');
$base = rtrim(trim((string)($_POST['base_url'] ?? '')), '/');
$prefix = '/' . trim((string)($_POST['api_prefix'] ?? '/api'), '/');
$user = trim((string)($_POST['username'] ?? ''));
$pwd = (string)($_POST['password'] ?? '');
$verify = !empty($_POST['verify_ssl']);
$full = (int)($_POST['full_score'] ?? 100);
if ($full < 1) {
$full = 100;
}
if ($base === '' || $user === '') {
$err = '请填写卷王系统地址与登录账号。';
} else {
// 1) 若是数据库后端,先保存连接参数并测试+建表
if ($backend === 'pgsql' || $backend === 'mysql') {
$dbp = [
'driver' => $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();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>首次安装 · <?= htmlspecialchars(NY_APP_NAME) ?></title>
<link rel="stylesheet" href="assets/style.css">
<style>
.install-wrap { position:fixed; inset:0; z-index:200; display:flex; align-items:center; justify-content:center; padding:20px; background:linear-gradient(135deg,#1b2440,#2f6bff); overflow:auto; }
.install-box { width:640px; max-width:100%; background:#fff; border-radius:18px; padding:28px 28px 30px; box-shadow:0 24px 64px rgba(15,23,42,.35); }
.install-box::before { content:''; position:absolute; }
.steps { display:flex; gap:8px; margin:6px 0 20px; }
.steps i { flex:1; height:4px; border-radius:999px; background:#e2e8f2; }
.steps i.on { background:linear-gradient(90deg,#2f6bff,#6a4bff); }
.opt-seg { display:flex; gap:10px; }
.opt-seg label { flex:1; display:flex; align-items:center; justify-content:center; gap:8px; border:1px solid var(--border); border-radius:10px; padding:12px 8px; cursor:pointer; font-size:13px; font-weight:600; color:var(--text-2); transition:.15s; }
.opt-seg label.on { border-color:var(--primary); background:var(--primary-weak); color:var(--primary); }
.opt-seg input { display:none; }
.db-box { margin-top:14px; padding:14px; border:1px dashed #cfd8ea; border-radius:10px; background:#fbfcfe; }
.db-box.hidden { display:none; }
.form-grid2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.fi.full { grid-column:1 / -1; }
.banner { padding:12px 14px; border-radius:10px; font-size:12.5px; margin-bottom:16px; line-height:1.6; }
.banner.err { background:var(--danger-weak); color:#a3342c; border:1px solid #f5d3cf; }
.banner.ok { background:var(--ok-weak); color:#0d7a4d; border:1px solid #c8ecda; }
.forgot { display:flex; justify-content:flex-end; margin-top:6px; font-size:12px; color:var(--text-3); }
</style>
</head>
<body class="dash">
<div class="install-wrap">
<div class="install-box">
<div class="login-brand">
<span class="logo">NY</span>
<div class="brand-text">
<strong><?= htmlspecialchars(NY_APP_NAME) ?></strong>
<small>首次安装 · 配置数据存储与系统连接</small>
</div>
</div>
<div class="steps"><i class="on"></i><i class="on"></i></div>
<?php if ($ok): ?>
<div class="banner ok">✅ 安装完成!即将进入登录页…</div>
<p style="font-size:13px;color:var(--text-2);text-align:center;margin:24px 0 6px">
若连接信息有误或未生效,请重新访问本页配置。</p>
<div class="login-actions"><button class="btn btn-primary" onclick="location.href='login.php'">前往登录</button></div>
<?php else: ?>
<form method="post" class="form-grid" id="f">
<?php if ($err !== ''): ?><div class="banner err full"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<div class="fi full">
<label>数据存储后端</label>
<div class="opt-seg" id="seg">
<label><input type="radio" name="backend" value="file" checked><span>✅ 文件存储</span><small style="font-weight:500;color:var(--text-3)">无需数据库,开箱即用</small></label>
<label><input type="radio" name="backend" value="pgsql"><span>🐘 PostgreSQL</span><small style="font-weight:500;color:var(--text-3)">生产环境推荐</small></label>
<label><input type="radio" name="backend" value="mysql"><span>🍀 MariaDB / MySQL</span><small style="font-weight:500;color:var(--text-3)">兼容 MySQL 协议</small></label>
</div>
</div>
<div class="db-box hidden full" id="dbBox">
<div class="form-grid2">
<div class="fi"><label>主机</label><input type="text" name="db_host" value="127.0.0.1"></div>
<div class="fi"><label>端口</label><input type="number" id="db_port" name="db_port" value="5432"></div>
<div class="fi"><label>数据库名</label><input type="text" name="db_name"></div>
<div class="fi"><label>账号</label><input type="text" name="db_user"></div>
<div class="fi full"><label>密码</label><input type="password" name="db_pass"></div>
</div>
</div>
<div class="fi full">
<label>卷王系统地址</label>
<input type="text" name="base_url" value="<?= htmlspecialchars($cfg['base_url']) ?>" placeholder="https://exam.nxxy335.top">
</div>
<div class="fi">
<label>接口前缀</label>
<input type="text" name="api_prefix" value="<?= htmlspecialchars($cfg['api_prefix']) ?>" placeholder="/api">
</div>
<div class="fi">
<label>默认满分</label>
<input type="number" name="full_score" min="1" max="1000" value="<?= (int)$cfg['full_score'] ?>">
<p class="hint">用于合格率(≥60%满分),各试卷可在设置中单独调整</p>
</div>
<div class="fi">
<label>登录账号</label>
<input type="text" name="username" value="<?= htmlspecialchars($cfg['username']) ?>" placeholder="admin">
</div>
<div class="fi">
<label>登录密码</label>
<input type="password" name="password" placeholder="请输入密码">
</div>
<div class="fi full checks">
<label><input type="checkbox" name="verify_ssl" <?= $cfg['verify_ssl'] ? 'checked' : '' ?>> 校验 HTTPS 证书</label>
</div>
<div class="full" style="margin-top:10px">
<button class="btn btn-primary" id="btnInstall" style="width:100%;height:44px;font-size:15px">⚙️ 完成安装</button>
</div>
</form>
<?php endif; ?>
</div>
</div>
<script>
(function(){
var seg = document.getElementById('seg');
var dbBox = document.getElementById('dbBox');
var dbPort = document.getElementById('db_port');
function sync(){
var v = (document.querySelector('input[name=backend]:checked')||{}).value || 'file';
var labels = seg.querySelectorAll('label');
labels.forEach(function(l){ l.classList.toggle('on', l.querySelector('input').value === v); });
dbBox.classList.toggle('hidden', v === 'file');
if (v === 'pgsql') dbPort.value = 5432;
if (v === 'mysql') dbPort.value = 3306;
}
seg.addEventListener('change', sync);
sync();
})();
</script>
</body>
</html>