prepare("SELECT COUNT(*) FROM settings WHERE name = ?"); $stmt->execute([$name]); if ((int)$stmt->fetchColumn() > 0) { $up = $db->prepare("UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE name = ?"); return $up->execute([$value, $name]); } $ins = $db->prepare("INSERT INTO settings (name, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)"); return $ins->execute([$name, $value]); } // 已安装?——只认锁文件。锁在 = 装过了。 // 注意:不拿「库文件存在」当判据 —— 库可能是空壳(建库失败、被人为清空), // 那种情况应该允许重新安装,而不是把用户挡在门外。 function alreadyInstalled() { return is_file(INSTALL_LOCK); } function abort404() { http_response_code(404); header('Content-Type: text/html; charset=utf-8'); echo '
' . '页面不存在。
'; exit; } // ---------- 环境体检 ---------- // 每项:['name'=>显示名, 'ok'=>bool, 'need'=>是否硬性要求, 'hint'=>不通过时的补救说明] function runChecks() { $checks = []; // PHP 版本:代码用了 PASSWORD_DEFAULT、箭头函数等,7.4 起可跑 $checks[] = [ 'name' => 'PHP 版本 ≥ 7.4', 'ok' => version_compare(PHP_VERSION, '7.4.0', '>='), 'need' => true, 'hint' => '当前 ' . PHP_VERSION . ',请升级 PHP。', ]; // 扩展:pdo_sqlite 是硬要求;openssl 用于设置项加密(缺了仍能跑,只是云密钥无法加密); // fileinfo 用于本机存储模式的图片内容校验;mbstring 处理中文更稳。 $exts = [ 'pdo_sqlite' => ['PDO SQLite 驱动', true, '数据库必需。'], 'openssl' => ['OpenSSL', true, '用于加密保存云存储密钥。'], 'fileinfo' => ['Fileinfo', false, '本机存储模式下按内容校验图片类型;缺失则退化为扩展名判断。'], 'mbstring' => ['Mbstring', false, '中文截断/编码处理;缺失一般不影响主流程。'], ]; foreach ($exts as $ext => $cfg) { $checks[] = [ 'name' => '扩展 ' . $cfg[0] . '(' . $ext . ')', 'ok' => extension_loaded($ext), 'need' => $cfg[1], 'hint' => $cfg[2], ]; } // 目录可写:db/(建库+密钥)、uploads/ 四个子目录(存图) $dirs = [ ROOT_DIR . '/db' => '数据库目录 db/', POSTER_DIR => '海报目录 uploads/posters/', TICKET_DIR => '票根目录 uploads/tickets/', PHOTO_DIR => '照片目录 uploads/photos/', AVATAR_DIR => '头像目录 uploads/avatars/', ]; foreach ($dirs as $dir => $label) { $exists = is_dir($dir); $writable = $exists && is_writable($dir); $checks[] = [ 'name' => $label . ' 可写', 'ok' => $writable, 'need' => true, 'hint' => $exists ? '目录存在但不可写,请给 Web 用户写权限(属主 www,或 chmod 775)。' : '目录不存在且无法自动创建,请手动建好并授权。', ]; } return $checks; } // 体检是否放行(硬性项全过) function checksPass(array $checks) { foreach ($checks as $c) { if ($c['need'] && !$c['ok']) return false; } return true; } // 尽力创建运行所需目录 function ensureDirs() { $dirs = [ROOT_DIR . '/db', POSTER_DIR, TICKET_DIR, PHOTO_DIR, AVATAR_DIR]; $failed = []; foreach ($dirs as $d) { if (!is_dir($d)) { if (!@mkdir($d, 0755, true) && !is_dir($d)) { $failed[] = $d; } } } return $failed; } // ---------- 入口 ---------- if (alreadyInstalled()) { abort404(); // 装过了就当它不存在 } $createdDirs = ensureDirs(); $checks = runChecks(); $canInstall = checksPass($checks); $errors = []; // 全局错误 $fieldErrors = []; // 字段级错误 $old = ['site_name' => '', 'username' => '']; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $old['site_name'] = trim((string)($_POST['site_name'] ?? '')); $old['username'] = trim((string)($_POST['username'] ?? '')); $password = (string)($_POST['password'] ?? ''); $password2 = (string)($_POST['password2'] ?? ''); if (!$canInstall) { $errors[] = '环境检查未通过,请先解决下面标红的问题再安装。'; } // 站点名:存进 settings,用于页面标题(可留空,留空用默认「电影日志」) if ($old['site_name'] !== '' && charLen($old['site_name']) > 40) { $fieldErrors['site_name'] = '站点名称最多 40 个字。'; } // 管理员账号 if ($old['username'] === '') { $fieldErrors['username'] = '请填写管理员账号。'; } elseif (!preg_match('/^[A-Za-z0-9_\x{4e00}-\x{9fa5}]{2,20}$/u', $old['username'])) { $fieldErrors['username'] = '账号 2–20 位,可用中文、字母、数字、下划线。'; } elseif (in_array(strtolower($old['username']), ['admin', 'administrator', 'root'], true) && $password === '123456') { // 明确拦住「admin + 123456」这个最容易被扫的弱口令组合 $fieldErrors['username'] = '请勿使用 admin/123456 这类默认弱口令,换一个账号或改密码。'; } // 密码 if ($password === '') { $fieldErrors['password'] = '请设置密码。'; } elseif (charLen($password) < 6) { $fieldErrors['password'] = '密码至少 6 位。'; } elseif ($password !== $password2) { $fieldErrors['password2'] = '两次输入的密码不一致。'; } if (!$errors && !$fieldErrors) { try { // 建表:require lib/db.php 只定义函数,不执行任何副作用。 // initDB(false) 表示「不要种默认管理员」——管理员由本页按用户输入创建。 require_once ROOT_DIR . '/lib/db.php'; initDB(false); $db = getDB(); $db->beginTransaction(); // 双保险:库若已有管理员(例如重复安装到旧库),不再新增,直接报错让人处理 $cnt = (int)$db->query("SELECT COUNT(*) FROM admins")->fetchColumn(); if ($cnt > 0) { $db->rollBack(); throw new RuntimeException('这个数据库里已经有管理员账号了。为避免覆盖,安装已中止。' . '若确实要重装,请先备份并删除 db/movies.db。'); } $hashed = password_hash($password, PASSWORD_DEFAULT); $stmt = $db->prepare("INSERT INTO admins (username, password) VALUES (?, ?)"); $stmt->execute([$old['username'], $hashed]); $adminId = (int)$db->lastInsertId(); // 站点名写入 settings(表由 initDB 建好) if ($old['site_name'] !== '') { putSetting($db, 'site_name', $old['site_name']); } // 存储方式默认 local(与 settings.php 的缺省一致) putSetting($db, 'storage_type', 'local'); // 安装完成标记 putSetting($db, 'installed_at', date('Y-m-d H:i:s')); $db->commit(); // 写锁文件(先写,再尝试自删;顺序反了会留下「已自删但没上锁」的窗口) $lockOk = @file_put_contents( INSTALL_LOCK, "installed_at=" . date('c') . "\nadmin=" . $old['username'] . "\n" ) !== false; if ($lockOk) @chmod(INSTALL_LOCK, 0600); // 自删:成功则以后连文件都不存在;失败也无妨,上面的锁已生效 $selfDeleted = @unlink(__FILE__); // 直接登入,省得用户再输一次 if (session_status() === PHP_SESSION_NONE) { @session_start(); } $_SESSION['admin_id'] = $adminId; $_SESSION['admin_username'] = $old['username']; $_SESSION['admin_theme'] = ''; // 安装成功页 renderDone($old['site_name'], $old['username'], $lockOk, $selfDeleted); exit; } catch (Throwable $e) { $errors[] = '安装失败:' . $e->getMessage(); } } } // ---------- 渲染 ---------- function pageHead($title) { ?>已经可以开始记录观影了
接下来建议先去「系统设置」确认图片来源存储方式(默认存本机)。
首次使用,先做一点简单设置
装好后本页面会自动删除,再次访问返回 404。
数据库与图片都在站点目录内,迁移时记得一起带走。