='), '当前 ' . 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 未能自动删除,请手动删除它。
开始使用
安装未完成,请修正以下问题:
· = e($err) ?>
环境检查
= $ok ? '✓' : '✗' ?>
= e($label) ?>
= e($note) ?>