From ae75058de10462d1497913e09202cf3ebbe346e4 Mon Sep 17 00:00:00 2001 From: laoyang Date: Tue, 3 Nov 2026 22:07:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AA=E5=AE=89=E8=A3=85=E6=97=B6=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=B7=B3=E8=BD=AC=E5=AE=89=E8=A3=85=E5=90=91=E5=AF=BC?= =?UTF-8?q?=EF=BC=88=E4=BF=AE=E3=80=8C=E6=89=93=E5=BC=80=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E4=B8=8D=E8=B7=B3=E8=BD=AC=E3=80=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 现象:全新部署后打开域名,看到的是一个空导航站(没有任何提示), 必须自己猜 /install.php 这个网址才能安装。 原因:index.php 一进来就 db() 建库并渲染前台,全站没有任何 「还没装」的引导逻辑;install.php 只能靠手输网址触达。 改动: - functions.php 新增 app_installed():全站唯一的「已安装」判断源 (有管理员账号 = 已安装),内部 try/catch 兜住建库失败 - index.php 开头:未安装且 install.php 存在 → 302 到安装页 · 只在 install.php 真存在时跳,用户装完删掉后自动失效,不会 404 - install.php 复用 app_installed(),删掉自己那套 is_file(DB_PATH) 推断, 避免「首页说没装、install 说装了」的口径分裂 测试(都在 _tooling/): - _sn_install_redirect_e2e.py 19 项:未安装跳转 / 已安装不跳 / install.php 删了不跳 / 已装拒重装,四态全覆盖 - _sn_install_redirect_falsify.py 6 变体全精确命中(含「无条件跳转→死循环」) - _sn_install_e2e.py 41 项回归全绿(顺手修 6 条因新逻辑过时的断言) --- functions.php | 25 +++++++++++++++++++++++++ index.php | 14 ++++++++++++++ install.php | 13 +++---------- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/functions.php b/functions.php index 163d9eb..b477e4b 100644 --- a/functions.php +++ b/functions.php @@ -7,6 +7,31 @@ declare(strict_types=1); require_once __DIR__ . '/config.php'; require_once __DIR__ . '/db.php'; +/** + * 是否已完成安装(= 至少存在一个管理员账号)。 + * + * **这是全站唯一的「已安装」判断源**,前台跳转、install.php 守卫、登录页文案都走这里。 + * 不要各处自己写 `is_file(DB_PATH)` 或 `SELECT COUNT(*) FROM users` —— 判断口径不一会出现 + * 「首页说装了、install 说没装」这类互相矛盾的状态。 + * + * 为什么要 try/catch:库还建不出来(data/ 不可写、SQLite 扩展缺失)时, + * db() 会抛异常。此时应视为「未安装」,让用户走到 install.php 看到环境检查的报错, + * 而不是在前台白屏或 500。 + */ +function app_installed(): bool +{ + static $cached = null; + if ($cached !== null) { + return $cached; + } + try { + $cached = (int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn() > 0; + } catch (Throwable $e) { + $cached = false; + } + return $cached; +} + /** HTML 转义 */ function e(?string $s): string { diff --git a/index.php b/index.php index 8af5a66..de150ac 100644 --- a/index.php +++ b/index.php @@ -9,6 +9,20 @@ declare(strict_types=1); require_once __DIR__ . '/functions.php'; send_no_cache(); +/** + * 未安装就先去装:库里还没有任何一个管理员账号时,前台没有意义 + * (打开只会看到一个空导航,既没有提示也不知道去哪)。 + * + * ⚠️ 只在 install.php 真的存在时才跳。用户装完删掉 install.php 后, + * 这个分支自动失效,不会跳到 404。 + * ⚠️ 必须放在 db() 之前判断的等价位置:app_installed() 内部会调 db(), + * 但那边有 try/catch 兜住建库失败的情况,不会把异常抛到前台。 + */ +if (!app_installed() && is_file(__DIR__ . '/install.php')) { + header('Location: ' . url('install.php')); + exit; +} + $pdo = db(); // 前台数据 diff --git a/install.php b/install.php index 6bcecae..f33d2cb 100755 --- a/install.php +++ b/install.php @@ -90,16 +90,9 @@ foreach ($checks as $ck) { * 安装状态 * ========================================================================= */ -$lockFile = DATA_PATH . '/install.lock'; -$dbExists = is_file(DB_PATH); -$hasAdmin = false; -if ($dbExists) { - try { - $hasAdmin = (int)db()->query('SELECT COUNT(*) FROM users')->fetchColumn() > 0; - } catch (Throwable $e) { - // 库文件存在但读不出来(损坏 / 不可写)—— 环境检查会报,这里不重复喊 - } -} +$lockFile = DATA_PATH . '/install.lock'; +// 与前台共用同一个判断源(functions.php 的 app_installed),避免两边口径不一致 +$hasAdmin = app_installed(); $installed = is_file($lockFile) || $hasAdmin; $err = '';