='), '当前 ' . PHP_VERSION]; foreach ($exts as $ext) { $rows[] = ["扩展 {$ext}", extension_loaded($ext), extension_loaded($ext) ? '已加载' : '未安装']; } $rows[] = ['站点根目录可写(用于生成 config.php)', is_writable($root), is_writable($root) ? '可写' : '不可写']; $rows[] = ['uploads/ 目录可写(用于存放照片)', is_writable($root . '/uploads'), is_writable($root . '/uploads') ? '可写' : '不可写']; $rows[] = ['sql/schema.sql 存在', is_file($root . '/sql/schema.sql'), is_file($root . '/sql/schema.sql') ? '存在' : '缺失']; return $rows; } /** 把 schema.sql 拆成可逐条执行的 SQL(剥离注释行、过滤空段) */ function schema_statements(string $root): array { $raw = (string)file_get_contents($root . '/sql/schema.sql'); $parts = preg_split('/;\s*[\r\n]+/', $raw) ?: []; $out = []; foreach ($parts as $part) { // 去掉每段里以 -- 开头的注释行 $lines = preg_grep('/^\s*--/', explode("\n", $part), PREG_GREP_INVERT) ?: []; $stmt = trim(implode("\n", $lines)); if ($stmt !== '' && stripos($stmt, 'CREATE TABLE') === 0) { $out[] = $stmt . ';'; } } return $out; } // --------------------------------------------------------------------------- // 2) 已安装:直接拒绝 // --------------------------------------------------------------------------- if ($installed) { ?> TripLog 安装
✅

TripLog 已安装

检测到站点已完成安装(config.php 已存在)。
为安全起见,安装向导已停用。
如果 install.php 仍在站点目录中,请手动删除它。

进入首页
'', 'family_name' => '我的家庭', 'db_host' => '127.0.0.1', 'db_port' => '3306', 'db_name' => '', 'db_user' => '', 'db_pass' => '', 'amap_key' => '', 'amap_sec' => '', 'amap_nav_src' => 'trip.example.com', 'admin_name' => '', 'admin_phone' => '', ]; $done = false; $removed = false; // install.php 是否自删成功 if ($_SERVER['REQUEST_METHOD'] === 'POST') { $token = (string)($_POST['_csrf'] ?? ''); if (!hash_equals($_SESSION['install_csrf'], $token)) { $errors[] = '页面已过期,请刷新后重试。'; } else { // ---- 收集(密码类不回填,单独处理)---- foreach ($values as $k => $_) { if (isset($_POST[$k]) && is_string($_POST[$k])) { $values[$k] = trim($_POST[$k]); } } $adminPass = (string)($_POST['admin_pass'] ?? ''); $adminPass2 = (string)($_POST['admin_pass2'] ?? ''); $dbPass = $values['db_pass']; // ---- 校验 ---- if ($values['site_name'] === '') { $errors[] = '网站标题不能为空'; } elseif (mb_strlen($values['site_name']) > 50) { $errors[] = '网站标题不能超过 50 个字符'; } if ($values['family_name'] === '') { $values['family_name'] = '我的家庭'; } elseif (mb_strlen($values['family_name']) > 30) { $errors[] = '家庭名称不能超过 30 个字符'; } if (!preg_match('/^\d{1,5}$/', $values['db_port']) || (int)$values['db_port'] < 1 || (int)$values['db_port'] > 65535) { $errors[] = '数据库端口不正确'; } if (!preg_match('/^[A-Za-z0-9_]+$/', $values['db_name'])) { $errors[] = '数据库名只能包含字母、数字和下划线'; } if ($values['db_user'] === '') { $errors[] = '数据库用户名不能为空'; } if ($values['admin_name'] === '') { $errors[] = '管理员姓名不能为空'; } if (!preg_match('/^1[3-9]\d{9}$/', $values['admin_phone'])) { $errors[] = '管理员手机号格式不正确(11 位,1 开头)'; } if (strlen($adminPass) < 6) { $errors[] = '管理员密码至少 6 位'; } if ($adminPass !== $adminPass2) { $errors[] = '两次输入的管理员密码不一致'; } if ($values['amap_nav_src'] === '') { $values['amap_nav_src'] = 'trip.example.com'; } // ---- 执行安装 ---- if (!$errors) { $pdo = null; try { $dsn = sprintf( 'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $values['db_host'], (int)$values['db_port'], $values['db_name'] ); $pdo = new PDO($dsn, $values['db_user'], $dbPass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, ]); } catch (PDOException $ex) { $code = (int)$ex->getCode(); $msg = $ex->getMessage(); if (strpos($msg, '1049') !== false || $code === 1049 || strpos($msg, '1044') !== false || $code === 1044) { // MySQL 8 对不存在的库报 1049,MariaDB 报 1044(Access denied)—— // 对用户来说两种情况都是「库没建好或账号不对」,合并成一条可操作的提示 $errors[] = '数据库不存在,或该用户无权访问「' . $values['db_name'] . '」:请先在 MySQL 里创建名为「' . $values['db_name'] . '」的空数据库(CREATE DATABASE ' . $values['db_name'] . ' DEFAULT CHARACTER SET utf8mb4;)并把它的全部权限授予刚才填写的用户,再回到本页重试。'; } elseif (strpos($msg, '1045') !== false || $code === 1045) { $errors[] = '无法连接数据库主机「' . $values['db_host'] . '」,请确认主机地址与端口。'; } else { $errors[] = '数据库连接失败:' . $msg; } } if ($pdo) { try { // ---- 建表(schema.sql 全量 + Migrate 增量,均幂等)---- $stmts = schema_statements($ROOT); if (count($stmts) < 10) { throw new RuntimeException('sql/schema.sql 内容异常(仅解析出 ' . count($stmts) . ' 条建表语句)'); } foreach ($stmts as $stmt) { $pdo->exec($stmt); } require $ROOT . '/src/Migrate.php'; \App\Migrate::run($pdo); // ---- 种子数据(一个事务;均只在空表时插入,重复执行不重复)---- $pdo->beginTransaction(); $familyName = $values['family_name']; $pdo->prepare('INSERT INTO families (name) VALUES (?)')->execute([$familyName]); $familyId = (int)$pdo->lastInsertId(); $pdo->prepare('INSERT INTO users (family_id, name, phone_encrypted, phone_hash, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, ?)') ->execute([ $familyId, $values['admin_name'], install_encrypt($values['admin_phone']), hash('sha256', $values['admin_phone']), password_hash($adminPass, PASSWORD_DEFAULT), 'admin', 'active', ]); // 节点类型(内置,不可删除) $insType = $pdo->prepare('INSERT IGNORE INTO node_types (family_id, name, icon, is_system) VALUES (?, ?, ?, 1)'); foreach ([ ['游玩', '🎡'], ['用餐', '🍜'], ['住宿', '🏨'], ['赶路', '🚄'], ['休息', '☕'], ] as $t) { $insType->execute([$familyId, $t[0], $t[1]]); } // 角色标签 $insTag = $pdo->prepare('INSERT IGNORE INTO role_tags (family_id, name, icon) VALUES (?, ?, ?)'); foreach ([ ['司机', '🚗'], ['摄影担当', '📷'], ['向导', '🗺️'], ['财务', '💰'], ] as $t) { $insTag->execute([$familyId, $t[0], $t[1]]); } // 出行方式:不含自驾两种(自驾需先在「车辆管理」里添加车辆, // travel_modes.vehicle_id 外键指向车辆,空挂没有意义) $insMode = $pdo->prepare('INSERT IGNORE INTO travel_modes (family_id, mode_type) VALUES (?, ?)'); foreach (['train', 'plane', 'bus', 'subway', 'taxi', 'bike', 'walk', 'ship', 'other'] as $m) { $insMode->execute([$familyId, $m]); } $pdo->commit(); // ---- 生成 config.php(临时文件 → include 验证 → rename)---- $configArr = [ 'db' => [ 'host' => $values['db_host'], 'port' => (int)$values['db_port'], 'name' => $values['db_name'], 'user' => $values['db_user'], 'pass' => $dbPass, 'charset' => 'utf8mb4', ], 'app' => [ 'name' => $values['site_name'], 'timezone' => 'Asia/Shanghai', ], 'amap' => [ 'key' => $values['amap_key'], 'security_js' => $values['amap_sec'], 'nav_src' => $values['amap_nav_src'], ], ]; $php = "inTransaction()) { $pdo->rollBack(); } $errors[] = $ex instanceof PDOException ? '安装执行失败:' . $ex->getMessage() : $ex->getMessage(); } } } } } // --------------------------------------------------------------------------- // 4) 结果页 / 表单页 // --------------------------------------------------------------------------- $checks = env_checks($ROOT); $envOk = true; foreach ($checks as $c) { if (!$c[1]) { $envOk = false; break; } } ?> TripLog 安装向导

TripLog 安装向导

家庭旅行日记 · 首次部署配置

🎉

安装完成!

数据表已创建,基本数据已就绪(节点类型、角色标签、出行方式)。
请使用刚才设置的管理员手机号和密码登录。
⚠️ 安装文件 install.php 未能自动删除,请手动删除它。

开始使用
安装未完成,请修正以下问题:
·

环境检查

站点信息

所有成员、行程、花销都归属这个家庭;之后还可以在后台调整。

数据库(MySQL)

请先在 MySQL 中创建一个空的数据库,表结构由安装向导自动生成。

高德开放平台(可稍后在 config.php 中补充)

用于地图展示与地点搜索。lbs.amap.com → 控制台 → 应用管理 → 创建应用(Web端(JS API) + Web服务)后填写。
仅用于高德侧的调用来源统计,填任意英数字串均可,通常填你的域名。

管理员账号