'SQLite 数据库(PDO)', 'openssl' => 'SMTP / OSS 加密通信']; // 可选扩展:值是说明字符串;也可写成 [说明, 缺失时的状态文案, 缺失时的补救提示] $opt = [ 'curl' => '访问 OSS(可选,没有会退回 PHP 流式请求)', 'mbstring' => '中文字数统计(可选)', 'fileinfo' => [ '上传文件的真实类型识别(推荐开启)', '未启用(仍可用,但上传类型只按扩展名核对)', '建议在 PHP 设置里开启 fileinfo,上传校验会更严格', ], ]; $dirs = [ FB_ROOT => '站点根目录(要写入 config.php)', FB_ROOT . '/data' => '数据库目录 data/', FB_ROOT . '/uploads' => '本机附件目录 uploads/(本机存储模式用)', ]; $rows = []; $ok = true; $verOk = version_compare(PHP_VERSION, '7.4.0', '>='); $rows[] = ['PHP 版本', PHP_VERSION, $verOk, '需要 7.4 及以上(推荐 8.1~8.5,已在 8.5 实测)', $verOk ? '' : '当前版本过低,请切换站点的 PHP 版本']; if (!$verOk) { $ok = false; } foreach ($need as $ext => $why) { $has = extension_loaded($ext); if (!$has) { $ok = false; } $rows[] = ['PHP 扩展:' . $ext, $has ? '已启用' : '未启用', $has, $why, $has ? '' : ('请在 PHP 设置里开启 ' . $ext . ' 扩展')]; } foreach ($opt as $ext => $why) { $note = '未启用(不影响安装)'; $hint = ''; if (is_array($why)) { $note = isset($why[1]) ? $why[1] : $note; $hint = isset($why[2]) ? $why[2] : ''; $why = isset($why[0]) ? $why[0] : ''; } $has = extension_loaded($ext); $rows[] = ['PHP 扩展:' . $ext, $has ? '已启用' : $note, true, $why, $has ? '' : $hint]; } foreach ($dirs as $d => $why) { if (!is_dir($d)) { @mkdir($d, 0755, true); } $w = is_dir($d) && is_writable($d); if (!$w) { $ok = false; } $shown = ($d === FB_ROOT) ? '(站点根)' : str_replace(FB_ROOT, '', $d); $rows[] = ['目录可写 ' . $shown, $w ? '可写' : '不可写', $w, $why, $w ? '' : ('请给 Web 用户写权限,例如 chown www:www ' . $d)]; } // config.php 若已存在(例如从模板复制过),必须可写才能就地更新 if (is_file(FB_ROOT . '/config.php')) { $w = is_writable(FB_ROOT . '/config.php'); if (!$w) { $ok = false; } $rows[] = ['config.php 可写', $w ? '可写' : '不可写', $w, '需要就地更新配置', $w ? '' : '请给该文件写权限,例如 chown www:www config.php']; } else { $rows[] = ['config.php', '尚不存在(将自动生成)', true, '由安装向导创建', '']; } $ex = is_file(FB_ROOT . '/config.example.php'); $rows[] = ['配置模板 config.example.php', $ex ? '存在' : '缺失', $ex, '生成 config.php 的底稿', $ex ? '' : '仓库文件不完整,请重新获取源码']; if (!$ex) { $ok = false; } return [$ok, $rows]; } /* ================================================================== * 生成 config.php(在模板文本上逐项替换,保留注释与其它段落) * ================================================================== */ function fb_install_write_config(array $v) { $example = FB_ROOT . '/config.example.php'; $src = (string)@file_get_contents($example); if ($src === '') { return '读不到 config.example.php。'; } /** 把顶层键(含引号形式)整项替换成新值;找不到就返回原串由调用方兜底 */ $setTop = function ($key, $valueExpr, $txt) { $pat = "/^([ \t]*'" . preg_quote($key, '/') . "'[ \t]*=>[ \t]*)(.*?)(,[ \t]*)(\r?\n)/m"; $rep = '$1' . $valueExpr . '$3$4'; return preg_replace($pat, $rep, $txt, 1); }; $phpStr = function ($s) { return "'" . str_replace(["\\", "'"], ["\\\\", "\\'"], (string)$s) . "'"; }; $dbPath = str_replace('\\', '/', FB_ROOT . '/data/feedback.db'); $src = $setTop('db_path', "'" . $dbPath . "'", $src); $src = $setTop('site_name', $phpStr($v['site_name']), $src); $src = $setTop('site_desc', $phpStr($v['site_desc']), $src); $src = $setTop('admin_email', $phpStr($v['admin_email']), $src); $src = $setTop('base_url', $phpStr($v['base_url']), $src); // 口令不写进文件:此处只把残留的旧明文重置为占位值(正常都是占位值,等价于不动) $src = $setTop('admin_password', "'CHANGE_ME_admin'", $src); $src = $setTop('api_token', $phpStr($v['api_token']), $src); $src = $setTop('ip_salt', $phpStr($v['ip_salt']), $src); // 兜底校验:生成的文本必须是可解析的 PHP。 // 注意必须带 TOKEN_PARSE —— 不加这一位 token_get_all 只做词法切分、不报语法错,等于没校验。 $syntaxOk = true; if (defined('TOKEN_PARSE') && function_exists('token_get_all')) { try { @token_get_all($src, TOKEN_PARSE); } catch (Throwable $e) { $syntaxOk = false; } } if (!$syntaxOk) { return '生成的配置内容语法不正确,已放弃写入(请把 config.php 恢复为模板后再试)。'; } $stamp = date('Y-m-d H:i:s'); $header = " 80) { $err = '站点标题请填写 1~80 个字符。'; } elseif ($admEmail !== '' && !filter_var($admEmail, FILTER_VALIDATE_EMAIL)) { $err = '官方邮箱格式不正确(也可以留空)。'; } elseif ($baseUrl !== '' && !preg_match('~^https?://~i', $baseUrl)) { $err = '站点根地址需要以 http:// 或 https:// 开头(留空则自动探测)。'; } elseif ($setPw && fb_len($pw) < 8) { $err = '管理员口令至少 8 个字符。'; } elseif ($setPw && $pw !== $pw2) { $err = '两次输入的口令不一致。'; } else { // ① 先建库建表 —— 后面写设置才有地方写 $pdo = fb_db(); // ② 写 config.php(口令不入文件) $cfgErr = fb_install_write_config([ 'site_name' => $siteName, 'site_desc' => ($siteDesc !== '' ? $siteDesc : (string)fb_cfg('site_desc', '')), 'admin_email' => $admEmail, 'base_url' => $baseUrl, 'api_token' => fb_install_rand(16), 'ip_salt' => fb_install_rand(16), ]); if ($cfgErr === '') { fb_cfg_flush(); // 让后续取值看到刚落盘的新 config.php } else { // config.php 写不进去不算致命:settings 表里的值同样生效,先把系统跑起来并如实告知 $err = '提示:' . $cfgErr . '(站内设置已写入数据库,系统可用;但建议修好后手工补齐 config.php)'; } // ③ 站点信息入库(优先生效,且不依赖 config.php 是否可写) fb_set_setting('site_name', $siteName); if ($siteDesc !== '') { fb_set_setting('site_desc', $siteDesc); } fb_set_setting('installed_at', time()); // ④ 口令:只写 bcrypt 哈希,绝不落盘明文 $pwSet = false; if ($setPw && fb_len($pw) >= 8 && $pw === $pw2) { $pwSet = fb_admin_set_password($pw); } $done = true; $GLOBALS['__fb_install_pw_set'] = $pwSet; } } ?> 安装 · 轻反馈 FeedLite
轻反馈 FeedLite
安装向导

已经装好了

检测到数据库已存在,无需重复安装。

进入问题列表 管理后台

如果你确实想重装(例如换了数据库位置),请先手动移走 data/feedback.db 再刷新本页。本向导不会替你删除任何数据。

安装完成 🎉

已生成 config.php、建好数据库表。下面是接下来要做的两件事:

① 删除安装文件 —— 请把这个 install.php 从服务器上删掉(已安装态下它会拒绝重装,但删掉更干净)。 ② 设置口令 —— 管理员口令已设成你刚才填的那个,并以 bcrypt 哈希存进数据库;config.php 里没有任何明文口令。 你这次没设口令。在设好之前后台无法登录:请进入 管理后台,在「站点设置 → 登录口令」里设定(或重新运行本向导)。

进入问题列表 管理后台

安装向导

数据库目录 data/ 下还没有数据,先完成这几步设置,装完即可使用。

一、环境体检

: ()
↳

二、填写站点信息

三、设置管理员口令

口令用来登录管理后台、以及以「官方」身份回复。它以 bcrypt 单向哈希写入数据库,明文不会保存在 config.php 或任何文件里。 留空则暂不设定,装完后可在后台补设(补设之前后台不可登录)。

环境体检未通过前无法提交,请先处理标红项。

安装后再删掉本文件即可。
邮件通知(SMTP)与附件上传(阿里云 OSS)都在安装完成后、在后台「站点设置」里配置,这里不需要填。