<?php
declare(strict_types=1);

$root = dirname(__DIR__);
$lockFile = $root . '/storage/install.lock';
$permanentLockFile = $root . '/storage/install.permanent.lock';
$dbConfigPath = $root . '/config/database.php';
$schemaPath = $root . '/config/schema.sql';
$appConfigPath = $root . '/config/config.php';
$securityConfigPath = $root . '/config/security.php';

function h(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

function app_route_url(string $appConfigPath, string $path = ''): string
{
    $path = ltrim($path, '/');
    if (!is_file($appConfigPath)) {
        if ($path === '') {
            return '/';
        }
        return '/index.php?r=' . rawurlencode($path);
    }

    $cfg = require $appConfigPath;
    if (!is_array($cfg)) {
        if ($path === '') {
            return '/';
        }
        return '/index.php?r=' . rawurlencode($path);
    }

    $base = rtrim((string)($cfg['base_url'] ?? '/'), '/');
    $basePrefix = $base === '' ? '' : $base;
    $entryScript = trim((string)($cfg['entry_script'] ?? ''));
    $entryMode = trim((string)($cfg['entry_mode'] ?? 'path'));
    $routeKey = trim((string)($cfg['entry_route_key'] ?? 'r'));
    if ($routeKey === '') {
        $routeKey = 'r';
    }

    if ($path === '') {
        return $basePrefix === '' ? '/' : ($basePrefix . '/');
    }

    if ($entryScript !== '') {
        $entry = '/' . trim($entryScript, '/');
        if ($entryMode === 'query') {
            return $basePrefix . $entry . '?' . rawurlencode($routeKey) . '=' . rawurlencode($path);
        }
        return $basePrefix . $entry . '/' . $path;
    }

    return $basePrefix . '/' . $path;
}

function write_install_locks(string $root, string $lockFile, string $permanentLockFile): bool
{
    $storageDir = $root . '/storage';
    if (!is_dir($storageDir) && !@mkdir($storageDir, 0755, true)) {
        return false;
    }

    $time = (string)time();
    $ok1 = @file_put_contents($lockFile, $time, LOCK_EX) !== false;
    $ok2 = @file_put_contents($permanentLockFile, $time, LOCK_EX) !== false;
    return $ok1 && $ok2;
}

function write_db_config(string $dbConfigPath, string $host, int $port, string $name, string $user, string $pass): bool
{
    $config = "<?php\nreturn [\n"
        . "    'host' => '" . addslashes($host) . "',\n"
        . "    'port' => " . $port . ",\n"
        . "    'database' => '" . addslashes($name) . "',\n"
        . "    'username' => '" . addslashes($user) . "',\n"
        . "    'password' => '" . addslashes($pass) . "',\n"
        . "    'charset' => 'utf8mb4',\n"
        . "];\n";

    return @file_put_contents($dbConfigPath, $config, LOCK_EX) !== false;
}

function initialize_app_secrets(string $appConfigPath): bool
{
    if (!is_file($appConfigPath)) {
        return true;
    }

    $cfg = require $appConfigPath;
    if (!is_array($cfg)) {
        return false;
    }

    $changed = false;

    $tokenSalt = trim((string)($cfg['token_salt'] ?? ''));
    if ($tokenSalt === '' || $tokenSalt === 'change_this_to_a_random_string') {
        $cfg['token_salt'] = bin2hex(random_bytes(32));
        $changed = true;
    }

    $apiSecret = trim((string)($cfg['api_secret'] ?? ''));
    if ($apiSecret === '' || $apiSecret === 'change_this_to_a_random_api_secret') {
        $cfg['api_secret'] = bin2hex(random_bytes(32));
        $changed = true;
    }

    if (!isset($cfg['session_cookie_samesite'])) {
        $cfg['session_cookie_samesite'] = 'Strict';
        $changed = true;
    }

    if (!$changed) {
        return true;
    }

    $content = "<?php\nreturn " . var_export($cfg, true) . ";\n";
    return @file_put_contents($appConfigPath, $content, LOCK_EX) !== false;
}

function initialize_security_config(string $securityConfigPath, string $appConfigPath): bool
{
    $cfg = [];
    if (is_file($securityConfigPath)) {
        $loaded = require $securityConfigPath;
        if (is_array($loaded)) {
            $cfg = $loaded;
        }
    }

    if (!isset($cfg['auth']) || !is_array($cfg['auth'])) {
        $cfg['auth'] = [];
    }
    if (!isset($cfg['local']) || !is_array($cfg['local'])) {
        $cfg['local'] = [];
    }
    if (!isset($cfg['update']) || !is_array($cfg['update'])) {
        $cfg['update'] = [];
    }

    if (trim((string)($cfg['local']['install_id'] ?? '')) === '') {
        $cfg['local']['install_id'] = 'inst_' . bin2hex(random_bytes(16));
    }
    if (trim((string)($cfg['local']['cache_key'] ?? '')) === '') {
        $cfg['local']['cache_key'] = bin2hex(random_bytes(32));
    }
    if ((int)($cfg['local']['offline_grace_seconds'] ?? 0) < 300) {
        $cfg['local']['offline_grace_seconds'] = 86400;
    }

    $cfg['update']['require_signature'] = false;
    if ((int)($cfg['update']['connect_timeout'] ?? 0) < 3) {
        $cfg['update']['connect_timeout'] = 5;
    }
    if ((int)($cfg['update']['timeout'] ?? 0) < 6) {
        $cfg['update']['timeout'] = 15;
    }
    if (!array_key_exists('retries', $cfg['update'])) {
        $cfg['update']['retries'] = 1;
    }
    if ((int)($cfg['update']['rollback_keep'] ?? 0) < 1) {
        $cfg['update']['rollback_keep'] = 3;
    }

    if (is_file($appConfigPath)) {
        $appCfg = require $appConfigPath;
        if (is_array($appCfg) && trim((string)($cfg['auth']['api_base_url'] ?? '')) === '') {
            $cfg['auth']['api_base_url'] = rtrim((string)($appCfg['update_api_url'] ?? ''), '/');
        }
    }

    $content = "<?php\ndeclare(strict_types=1);\nreturn " . var_export($cfg, true) . ";\n";
    return @file_put_contents($securityConfigPath, $content, LOCK_EX) !== false;
}

function connect_database(string $host, int $port, string $name, string $user, string $pass): array
{
    try {
        $dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $host, $port, $name);
        $pdo = new PDO($dsn, $user, $pass, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
        return ['ok' => true, 'pdo' => $pdo, 'error' => ''];
    } catch (Throwable $e) {
        return ['ok' => false, 'pdo' => null, 'error' => $e->getMessage()];
    }
}

function users_table_exists(PDO $pdo): bool
{
    $stmt = $pdo->query("SHOW TABLES LIKE 'users'");
    return (bool)$stmt->fetch();
}

function detect_existing_install(string $dbConfigPath): bool
{
    if (!is_file($dbConfigPath)) {
        return false;
    }

    $cfg = require $dbConfigPath;
    if (!is_array($cfg)) {
        return false;
    }

    $res = connect_database(
        (string)($cfg['host'] ?? ''),
        (int)($cfg['port'] ?? 3306),
        (string)($cfg['database'] ?? ''),
        (string)($cfg['username'] ?? ''),
        (string)($cfg['password'] ?? '')
    );

    if (empty($res['ok']) || !($res['pdo'] instanceof PDO)) {
        return false;
    }

    return users_table_exists($res['pdo']);
}

function load_existing_db_values(string $dbConfigPath): array
{
    if (!is_file($dbConfigPath)) {
        return [];
    }

    $cfg = require $dbConfigPath;
    if (!is_array($cfg)) {
        return [];
    }

    return [
        'db_host' => (string)($cfg['host'] ?? ''),
        'db_port' => (string)($cfg['port'] ?? ''),
        'db_name' => (string)($cfg['database'] ?? ''),
        'db_user' => (string)($cfg['username'] ?? ''),
        'db_pass' => (string)($cfg['password'] ?? ''),
    ];
}

function split_sql_statements(string $sql): array
{
    $lines = preg_split('/\r?\n/', $sql);
    if (!is_array($lines)) {
        return [];
    }

    $filtered = '';
    foreach ($lines as $line) {
        $trim = trim($line);
        if ($trim === '' || strncmp($trim, '--', 2) === 0) {
            continue;
        }
        $filtered .= $line . "\n";
    }

    return array_values(array_filter(array_map('trim', explode(';', $filtered))));
}

function preflight_checks(string $root, string $schemaPath, string $dbConfigPath, string $appConfigPath): array
{
    $configDir = dirname($dbConfigPath);
    $storageDir = $root . '/storage';

    return [
        [
            'label' => 'PHP 版本 >= 8.0',
            'ok' => version_compare(PHP_VERSION, '8.0.0', '>='),
            'detail' => PHP_VERSION,
        ],
        [
            'label' => 'PDO 扩展',
            'ok' => extension_loaded('pdo'),
            'detail' => extension_loaded('pdo') ? '已安装' : '缺失',
        ],
        [
            'label' => 'PDO_MySQL 扩展',
            'ok' => extension_loaded('pdo_mysql'),
            'detail' => extension_loaded('pdo_mysql') ? '已安装' : '缺失',
        ],
        [
            'label' => 'OpenSSL 扩展',
            'ok' => extension_loaded('openssl'),
            'detail' => extension_loaded('openssl') ? '已安装' : '缺失',
        ],
        [
            'label' => '数据库结构文件可读',
            'ok' => is_file($schemaPath) && is_readable($schemaPath),
            'detail' => $schemaPath,
        ],
        [
            'label' => 'config 目录可写',
            'ok' => is_dir($configDir) && is_writable($configDir),
            'detail' => $configDir,
        ],
        [
            'label' => 'storage 目录可写',
            'ok' => is_dir($storageDir) ? is_writable($storageDir) : is_writable($root),
            'detail' => $storageDir,
        ],
        [
            'label' => '应用配置可写',
            'ok' => !is_file($appConfigPath) || is_writable($appConfigPath),
            'detail' => $appConfigPath,
        ],
    ];
}

function render_page(string $title, string $content): void
{
    echo '<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">';
    echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
    echo '<title>' . h($title) . '</title>';
    echo '<style>
      :root {
        --bg: #eef3ff;
        --panel: #ffffff;
        --text: #0f172a;
        --muted: #64748b;
        --line: #e2e8f0;
        --ok-bg: #ecfdf3;
        --ok-border: #86efac;
        --ok-text: #166534;
        --err-bg: #fff1f2;
        --err-border: #fda4af;
        --err-text: #9f1239;
        --primary: #2563eb;
      }
      * { box-sizing: border-box; }
      body {
        margin: 0;
        background: radial-gradient(circle at top right, #dbeafe 0%, var(--bg) 45%, #f8fafc 100%);
        color: var(--text);
        font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
      }
      .wrap { max-width: 900px; margin: 28px auto; padding: 0 16px; }
      .panel {
        background: var(--panel);
        border: 1px solid var(--line);
        border-radius: 14px;
        box-shadow: 0 10px 24px rgba(15, 23, 42, .06);
        overflow: hidden;
      }
      .head {
        padding: 18px 22px;
        border-bottom: 1px solid var(--line);
        background: linear-gradient(90deg, #f8fbff, #f1f5ff);
      }
      .head h1 { margin: 0; font-size: 20px; }
      .head p { margin: 6px 0 0; color: var(--muted); font-size: 13px; }
      .body { padding: 20px 22px 24px; }
      .msg {
        border-radius: 10px;
        padding: 12px 14px;
        margin-bottom: 14px;
        font-size: 14px;
      }
      .msg.ok { background: var(--ok-bg); border: 1px solid var(--ok-border); color: var(--ok-text); }
      .msg.err { background: var(--err-bg); border: 1px solid var(--err-border); color: var(--err-text); }
      .grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
      @media (max-width: 760px) { .grid { grid-template-columns: 1fr; } }
      .field { margin-bottom: 10px; }
      .field label { display: block; font-size: 13px; margin-bottom: 6px; color: #334155; font-weight: 600; }
      .field input {
        width: 100%;
        height: 40px;
        border: 1px solid #cbd5e1;
        border-radius: 9px;
        padding: 0 12px;
        outline: none;
        transition: border-color .18s ease;
      }
      .field input:focus { border-color: var(--primary); }
      .checks { margin: 0 0 16px; padding: 0; list-style: none; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
      .checks li {
        display: flex;
        justify-content: space-between;
        gap: 10px;
        padding: 10px 12px;
        border-bottom: 1px solid var(--line);
        font-size: 13px;
      }
      .checks li:last-child { border-bottom: 0; }
      .tag-ok { color: #15803d; font-weight: 700; }
      .tag-no { color: #b91c1c; font-weight: 700; }
      .muted { color: var(--muted); }
      .btns { display: flex; gap: 10px; margin-top: 8px; flex-wrap: wrap; }
      .btn {
        border: 0;
        border-radius: 9px;
        height: 40px;
        padding: 0 16px;
        cursor: pointer;
        font-size: 14px;
      }
      .btn.primary { background: var(--primary); color: #fff; }
      .btn.ghost { background: #e2e8f0; color: #0f172a; }
      .jump {
        display: inline-block;
        margin-top: 8px;
        text-decoration: none;
        background: var(--primary);
        color: #fff;
        border-radius: 9px;
        padding: 10px 14px;
      }
      code {
        background: #f1f5f9;
        padding: 2px 6px;
        border-radius: 6px;
      }
    </style></head><body><div class="wrap"><div class="panel">' . $content . '</div></div></body></html>';
}

function render_checks(array $checks): string
{
    $html = '<ul class="checks">';
    foreach ($checks as $check) {
        $label = h((string)($check['label'] ?? ''));
        $detail = h((string)($check['detail'] ?? ''));
        $ok = !empty($check['ok']);
        $html .= '<li><span>' . $label . ' <span class="muted">(' . $detail . ')</span></span>';
        $html .= '<span class="' . ($ok ? 'tag-ok' : 'tag-no') . '">' . ($ok ? '通过' : '失败') . '</span></li>';
    }
    $html .= '</ul>';
    return $html;
}

function render_form(array $values, array $checks, string $error = '', string $success = ''): void
{
    $html = '<div class="head"><h1>快速安装向导</h1><p>先完成环境检查和数据库测试，再一键安装。</p></div><div class="body">';

    if ($success !== '') {
        $html .= '<div class="msg ok">' . h($success) . '</div>';
    }
    if ($error !== '') {
        $html .= '<div class="msg err">' . h($error) . '</div>';
    }

    $html .= '<h3 style="margin:0 0 8px;font-size:15px;">环境检查</h3>';
    $html .= render_checks($checks);
    $html .= '<form method="post">';
    $html .= '<div class="grid">';
    $html .= '<div class="field"><label>数据库主机</label><input name="db_host" required value="' . h((string)($values['db_host'] ?? '127.0.0.1')) . '"></div>';
    $html .= '<div class="field"><label>数据库端口</label><input name="db_port" required value="' . h((string)($values['db_port'] ?? '3306')) . '"></div>';
    $html .= '<div class="field"><label>数据库名</label><input name="db_name" required value="' . h((string)($values['db_name'] ?? 'lyapp')) . '"></div>';
    $html .= '<div class="field"><label>数据库用户名</label><input name="db_user" required value="' . h((string)($values['db_user'] ?? 'root')) . '"></div>';
    $html .= '<div class="field"><label>数据库密码</label><input type="password" name="db_pass" value="' . h((string)($values['db_pass'] ?? '')) . '"></div>';
    $html .= '<div class="field"><label>管理员账号</label><input name="admin_user" required value="' . h((string)($values['admin_user'] ?? 'admin')) . '"></div>';
    $html .= '<div class="field"><label>管理员密码</label><input type="password" name="admin_pass" placeholder="至少 8 位"></div>';
    $html .= '<div class="field"><label>管理员昵称</label><input name="admin_name" value="' . h((string)($values['admin_name'] ?? 'Administrator')) . '"></div>';
    $html .= '</div>';
    $html .= '<div class="btns">';
    $html .= '<button class="btn ghost" type="submit" name="action" value="test_db">测试数据库连接</button>';
    $html .= '<button class="btn primary" type="submit" name="action" value="install">立即安装</button>';
    $html .= '</div>';
    $html .= '</form>';
    $html .= '<p class="muted" style="margin:12px 0 0 6px;font-size:12px;">安装器会自动创建或修复数据库表、写入配置，并生成随机安全密钥。</p>';
    $html .= '<p class="muted" style="margin:0;font-size:12px;">默认推荐零配置部署：无需把站点目录改到 public，也无需配置伪静态。安装完成后可直接使用 <code>/index.php?r=login</code> 进入后台。</p>';
    $html .= '</div>';

    render_page('安装向导', $html);
}

function render_installed(string $message, string $loginUrl): void
{
    $html = '<div class="head"><h1>安装状态</h1><p>初始化已完成。</p></div><div class="body">';
    $html .= '<div class="msg ok">' . h($message) . '</div>';
    $html .= '<p class="muted" style="margin:0 0 10px;">如果你没有配置伪静态，请继续使用系统默认入口访问后台。</p>';
    $html .= '<p class="muted" style="margin:0 0 10px;">默认后台地址：<code>' . h($loginUrl) . '</code></p>';
    $html .= '<a class="jump" href="' . h($loginUrl) . '">前往登录</a>';
    $html .= '</div>';
    render_page('安装完成', $html);
}

$defaults = [
    'db_host' => '127.0.0.1',
    'db_port' => '3306',
    'db_name' => 'lyapp',
    'db_user' => 'root',
    'db_pass' => '',
    'admin_user' => 'admin',
    'admin_name' => 'Administrator',
];
$defaults = array_merge($defaults, load_existing_db_values($dbConfigPath));
$checks = preflight_checks($root, $schemaPath, $dbConfigPath, $appConfigPath);
$repairModeHint = '';

$alreadyInstalled = detect_existing_install($dbConfigPath);
if ($alreadyInstalled) {
    @write_install_locks($root, $lockFile, $permanentLockFile);
    render_installed('系统已经安装完成。', app_route_url($appConfigPath, 'login'));
    exit;
}

$hasConfigFile = is_file($dbConfigPath);
$hasAnyInstallLock = is_file($lockFile) || is_file($permanentLockFile);
if (is_file($permanentLockFile)) {
    $html = '<div class="head"><h1>安装器已禁用</h1><p>出于安全考虑，已阻止自动重新安装。</p></div><div class="body">';
    $html .= '<div class="msg err">检测到永久安装锁，安装器不会自动删除锁文件，也不会重新开放安装。</div>';
    $html .= '<p class="muted">如果你确实需要全新安装，请手动删除 <code>config/database.php</code>、<code>storage/install.lock</code> 和 <code>storage/install.permanent.lock</code> 后再重新访问本页。</p>';
    $html .= '</div>';
    http_response_code(403);
    render_page('安装器已禁用', $html);
    exit;
}

if ($hasConfigFile || $hasAnyInstallLock) {
    $repairModeHint = '检测到旧配置或安装锁，已进入安装修复模式。你可以直接测试数据库并重新安装，提交后将覆盖 config/database.php 并重置管理员账号。';
}

if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
    render_form($defaults, $checks, '', $repairModeHint);
    exit;
}

$action = trim((string)($_POST['action'] ?? 'install'));
$dbHost = trim((string)($_POST['db_host'] ?? ''));
$dbPort = (int)($_POST['db_port'] ?? 3306);
$dbName = trim((string)($_POST['db_name'] ?? ''));
$dbUser = trim((string)($_POST['db_user'] ?? ''));
$dbPass = (string)($_POST['db_pass'] ?? '');
$adminUser = trim((string)($_POST['admin_user'] ?? ''));
$adminPass = (string)($_POST['admin_pass'] ?? '');
$adminName = trim((string)($_POST['admin_name'] ?? 'Administrator'));

$values = [
    'db_host' => $dbHost,
    'db_port' => (string)$dbPort,
    'db_name' => $dbName,
    'db_user' => $dbUser,
    'db_pass' => $dbPass,
    'admin_user' => $adminUser,
    'admin_name' => $adminName,
];

if ($dbHost === '' || $dbPort <= 0 || $dbName === '' || $dbUser === '') {
    render_form($values, $checks, '请完整填写数据库连接信息。');
    exit;
}

if ($action === 'install' && ($adminUser === '' || strlen($adminPass) < 8)) {
    render_form($values, $checks, '请填写管理员账号和密码，且密码至少 8 位。');
    exit;
}

$dbRes = connect_database($dbHost, $dbPort, $dbName, $dbUser, $dbPass);
if (empty($dbRes['ok']) || !($dbRes['pdo'] instanceof PDO)) {
    render_form($values, $checks, '数据库连接失败：' . (string)$dbRes['error']);
    exit;
}

if ($action === 'test_db') {
    render_form($values, $checks, '', '数据库连接成功，现在可以开始安装。');
    exit;
}

try {
    $pdo = $dbRes['pdo'];

    if (users_table_exists($pdo)) {
        if (!write_db_config($dbConfigPath, $dbHost, $dbPort, $dbName, $dbUser, $dbPass)) {
            render_form($values, $checks, '检测到已有数据表，但写入 config/database.php 失败。');
            exit;
        }
        if (!initialize_app_secrets($appConfigPath)) {
            render_form($values, $checks, '检测到已有数据表，但写入应用安全密钥失败。');
            exit;
        }
        if (!initialize_security_config($securityConfigPath, $appConfigPath)) {
            render_form($values, $checks, '检测到已有数据表，但写入安全配置失败。');
            exit;
        }
        if (!write_install_locks($root, $lockFile, $permanentLockFile)) {
            render_form($values, $checks, '检测到已有数据表，但写入安装锁失败。');
            exit;
        }

        render_installed('检测到已有数据表，初始化已完成。', app_route_url($appConfigPath, 'login'));
        exit;
    }

    if (!is_file($schemaPath) || !is_readable($schemaPath)) {
        render_form($values, $checks, '找不到或无法读取结构文件：config/schema.sql');
        exit;
    }

    $sql = file_get_contents($schemaPath);
    if (!is_string($sql) || $sql === '') {
        render_form($values, $checks, '读取结构文件失败。');
        exit;
    }
    if (strncmp($sql, "\xEF\xBB\xBF", 3) === 0) {
        $sql = substr($sql, 3);
    }

    foreach (split_sql_statements($sql) as $stmt) {
        try {
            $pdo->exec($stmt);
        } catch (PDOException $e) {
            $code = (string)$e->getCode();
            $msg = $e->getMessage();
            if ($code === '42S01' || stripos($msg, 'already exists') !== false) {
                continue;
            }
            throw $e;
        }
    }

    $hash = password_hash($adminPass, PASSWORD_DEFAULT, ['cost' => 10]);
    $stmt = $pdo->prepare('SELECT id FROM users WHERE username = ? LIMIT 1');
    $stmt->execute([$adminUser]);
    $row = $stmt->fetch();

    if ($row) {
        $upd = $pdo->prepare('UPDATE users SET password_hash = ?, full_name = ?, role = "admin", status = "active", updated_at = NOW() WHERE id = ?');
        $upd->execute([$hash, $adminName, (int)$row['id']]);
    } else {
        $ins = $pdo->prepare('INSERT INTO users (username, password_hash, full_name, role, status, balance, created_at, updated_at) VALUES (?, ?, ?, "admin", "active", 0, NOW(), NOW())');
        $ins->execute([$adminUser, $hash, $adminName]);
    }

    if (!write_db_config($dbConfigPath, $dbHost, $dbPort, $dbName, $dbUser, $dbPass)) {
        render_form($values, $checks, '安装完成，但写入 config/database.php 失败。');
        exit;
    }
    if (!initialize_app_secrets($appConfigPath)) {
        render_form($values, $checks, '安装完成，但写入应用安全密钥失败。');
        exit;
    }
    if (!initialize_security_config($securityConfigPath, $appConfigPath)) {
        render_form($values, $checks, '安装完成，但写入安全配置失败。');
        exit;
    }
    if (!write_install_locks($root, $lockFile, $permanentLockFile)) {
        render_form($values, $checks, '安装完成，但写入安装锁失败。');
        exit;
    }

    render_installed('安装完成。', app_route_url($appConfigPath, 'login'));
} catch (Throwable $e) {
    render_form($values, $checks, '安装失败：' . $e->getMessage());
}
