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 连接按部署环境实测。
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user