Files
shuiguoji/install.php
T
2026-07-23 08:58:13 +08:00

124 lines
11 KiB
PHP

<?php
declare(strict_types=1);
$storageDirectory = __DIR__ . '/storage';
$lockFile = $storageDirectory . '/installed.lock';
$configFile = $storageDirectory . '/install-config.php';
if (is_file($lockFile)) {
http_response_code(403);
exit('系统已经安装。如需重装,请先由服务器管理员移除安装锁。');
}
if (session_status() !== PHP_SESSION_ACTIVE) {
session_name('tanwan_install');
session_set_cookie_params(['lifetime' => 0, 'path' => '/', 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', 'httponly' => true, 'samesite' => 'Strict']);
session_start();
}
if (empty($_SESSION['install_csrf'])) {
$_SESSION['install_csrf'] = bin2hex(random_bytes(32));
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
if (!hash_equals((string) $_SESSION['install_csrf'], (string) ($_POST['csrf'] ?? ''))) {
throw new RuntimeException('页面已过期,请刷新后重试。');
}
$host = trim((string) ($_POST['db_host'] ?? 'localhost'));
$port = filter_var($_POST['db_port'] ?? null, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 65535]]);
$database = trim((string) ($_POST['db_database'] ?? ''));
$dbUsername = trim((string) ($_POST['db_username'] ?? ''));
$dbPassword = (string) ($_POST['db_password'] ?? '');
$adminUsername = trim((string) ($_POST['admin_username'] ?? ''));
$adminPassword = (string) ($_POST['admin_password'] ?? '');
$adminConfirm = (string) ($_POST['admin_password_confirm'] ?? '');
$appName = trim((string) ($_POST['app_name'] ?? '欢乐水果机'));
$appUrl = rtrim(trim((string) ($_POST['app_url'] ?? '')), '/');
if ($host === '' || strlen($host) > 255) throw new RuntimeException('数据库主机无效。');
if ($port === false) throw new RuntimeException('数据库端口无效。');
if (!preg_match('/^[A-Za-z0-9_]{1,64}$/', $database)) throw new RuntimeException('数据库名只能包含字母、数字和下划线。');
if ($dbUsername === '' || strlen($dbUsername) > 128 || str_contains($dbUsername, "\0")) throw new RuntimeException('数据库账号无效。');
if (!preg_match('/^[A-Za-z0-9_]{4,32}$/', $adminUsername)) throw new RuntimeException('管理员账号需为 4-32 位字母、数字或下划线。');
if (strlen($adminPassword) < 12 || strlen($adminPassword) > 72) throw new RuntimeException('管理员密码需为 12-72 位。');
if (!hash_equals($adminPassword, $adminConfirm)) throw new RuntimeException('两次输入的管理员密码不一致。');
if ($appName === '' || mb_strlen($appName) > 60) throw new RuntimeException('站点名称无效。');
if (!filter_var($appUrl, FILTER_VALIDATE_URL) || !in_array(parse_url($appUrl, PHP_URL_SCHEME), ['http', 'https'], true)) throw new RuntimeException('站点网址无效。');
if (!in_array('mysql', PDO::getAvailableDrivers(), true)) throw new RuntimeException('服务器未安装 PDO MySQL 扩展。');
$options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false];
try {
$pdo = new PDO(sprintf('mysql:host=%s;port=%d;charset=utf8mb4', $host, $port), $dbUsername, $dbPassword, $options);
$pdo->exec('CREATE DATABASE IF NOT EXISTS `' . $database . '` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');
$pdo->exec('USE `' . $database . '`');
} catch (PDOException) {
$pdo = new PDO(sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $host, $port, $database), $dbUsername, $dbPassword, $options);
}
$schema = file_get_contents(__DIR__ . '/database/schema-mysql.sql');
if ($schema === false) throw new RuntimeException('数据库结构文件不存在。');
$pdo->exec($schema);
if ((int) $pdo->query('SELECT COUNT(*) FROM admins')->fetchColumn() > 0) {
throw new RuntimeException('该数据库已有管理员,安装已停止,未覆盖现有数据。');
}
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare('INSERT INTO admins(username,password_hash,created_at) VALUES(?,?,?)');
$stmt->execute([$adminUsername, password_hash($adminPassword, PASSWORD_DEFAULT), date('Y-m-d H:i:s')]);
$settings = [
'site_name' => $appName, 'announcement' => '理性娱乐,请妥善保管您的账户信息。',
'customer_service' => '', 'recharge_instructions' => '提交后请联系客服完成核验,审核通过后即时到账。',
'register_bonus' => '0', 'invite_reward' => '0', 'direct_commission_rate' => '10',
'second_commission_rate' => '1', 'game_enabled' => '1', 'target_rtp' => '92', 'rtp_window' => '1000', 'lucky_eat_rate' => '2',
'size_win_rate' => '46',
'odds_bar_low' => '50', 'odds_bar_high' => '100',
'odds_77_low' => '2', 'odds_77_high' => '20',
'odds_xx_low' => '2', 'odds_xx_high' => '20',
'odds_xg_low' => '2', 'odds_xg_high' => '20',
'odds_ld_low' => '2', 'odds_ld_high' => '10',
'odds_nm_low' => '2', 'odds_nm_high' => '10',
'odds_jz_low' => '2', 'odds_jz_high' => '10',
'odds_pg_low' => '2', 'odds_pg_high' => '5',
'special_xsy_rate' => '0.7407', 'special_xsy_odds' => '4',
'special_dsy_rate' => '0.1852', 'special_dsy_odds' => '16',
'special_dsx_rate' => '0.3704', 'special_dsx_odds' => '13',
'special_tnsh_rate' => '0.3704', 'special_tnsh_odds' => '8',
'special_tlbb_rate' => '0.1852', 'special_tlbb_odds' => '10',
'special_shuaidao_rate' => '0.3704', 'special_shuaidao_odds' => '8',
'special_huoche_rate' => '0.7407', 'special_huoche_odds' => '5',
'special_dmg_rate' => '0.0370', 'special_dmg_odds' => '80',
];
for ($position = 1; $position <= 24; $position++) $settings['normal_weight_'.$position] = '1';
$upsert = $pdo->prepare('INSERT INTO settings(`key`,`value`) VALUES(?,?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)');
foreach ($settings as $key => $value) $upsert->execute([$key, $value]);
$pdo->commit();
} catch (Throwable $exception) {
if ($pdo->inTransaction()) $pdo->rollBack();
throw $exception;
}
if (!is_dir($storageDirectory) && !mkdir($storageDirectory, 0750, true) && !is_dir($storageDirectory)) throw new RuntimeException('无法创建配置目录。');
$installedConfig = [
'app_name' => $appName, 'app_url' => $appUrl, 'timezone' => 'Asia/Shanghai',
'db' => ['driver' => 'mysql', 'host' => $host, 'port' => (string) $port, 'database' => $database, 'username' => $dbUsername, 'password' => $dbPassword],
];
$contents = "<?php\ndeclare(strict_types=1);\n\nreturn " . var_export($installedConfig, true) . ";\n";
$temporary = $configFile . '.tmp-' . bin2hex(random_bytes(6));
if (file_put_contents($temporary, $contents, LOCK_EX) === false || !chmod($temporary, 0640) || !rename($temporary, $configFile)) throw new RuntimeException('无法写入私有配置。');
if (file_put_contents($lockFile, 'installed=' . date(DATE_ATOM) . "\n", LOCK_EX) === false || !chmod($lockFile, 0640)) throw new RuntimeException('无法写入安装锁。');
session_regenerate_id(true);
header('Location: /admin/login.php?installed=1');
exit;
} catch (Throwable $exception) {
$error = $exception instanceof RuntimeException ? $exception->getMessage() : '安装失败,请检查数据库连接和目录权限。';
}
}
function install_e(mixed $value): string { return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
$suggestedUrl = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
?><!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>安装欢乐水果机</title><style>
*{box-sizing:border-box}body{margin:0;background:#f3f5f8;color:#18212b;font:15px/1.5 system-ui,-apple-system,"Microsoft YaHei",sans-serif}.wrap{width:min(720px,calc(100% - 32px));margin:40px auto;background:#fff;border:1px solid #dde3ea;border-radius:8px;padding:28px}h1{margin:0 0 6px;font-size:26px}p{margin:0 0 24px;color:#657180}.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}label{display:grid;gap:6px;font-weight:600}input{width:100%;padding:11px 12px;border:1px solid #cbd3dc;border-radius:5px;font:inherit}.wide{grid-column:1/-1}button{margin-top:22px;width:100%;padding:12px;border:0;border-radius:5px;background:#1769aa;color:#fff;font:600 16px inherit;cursor:pointer}.error{margin-bottom:18px;padding:11px 13px;background:#fff1f0;border:1px solid #ffc9c5;color:#a82921;border-radius:5px}.note{margin-top:18px;font-size:13px;color:#768392}@media(max-width:620px){.wrap{margin:16px auto;padding:20px}.grid{grid-template-columns:1fr}.wide{grid-column:auto}}
</style></head><body><main class="wrap"><h1>安装欢乐水果机</h1><p>连接 MySQL、创建数据表并设置首个管理员。安装完成后此页面会自动锁定。</p><?php if($error):?><div class="error"><?=install_e($error)?></div><?php endif;?><form method="post" autocomplete="off"><input type="hidden" name="csrf" value="<?=install_e($_SESSION['install_csrf'])?>"><div class="grid"><label>数据库主机<input name="db_host" value="<?=install_e($_POST['db_host']??'localhost')?>" required></label><label>数据库端口<input type="number" name="db_port" value="<?=install_e($_POST['db_port']??'3306')?>" min="1" max="65535" required></label><label>数据库名称<input name="db_database" value="<?=install_e($_POST['db_database']??'tanwan_game')?>" pattern="[A-Za-z0-9_]+" required></label><label>数据库账号<input name="db_username" value="<?=install_e($_POST['db_username']??'')?>" required></label><label class="wide">数据库密码<input type="password" name="db_password" required></label><label>管理员账号<input name="admin_username" value="<?=install_e($_POST['admin_username']??'admin')?>" minlength="4" maxlength="32" required></label><span></span><label>管理员密码<input type="password" name="admin_password" minlength="12" maxlength="72" required></label><label>确认管理员密码<input type="password" name="admin_password_confirm" minlength="12" maxlength="72" required></label><label>站点名称<input name="app_name" value="<?=install_e($_POST['app_name']??'欢乐水果机')?>" required></label><label>站点网址<input type="url" name="app_url" value="<?=install_e($_POST['app_url']??$suggestedUrl)?>" required></label></div><button type="submit">安装并初始化数据库</button></form><div class="note">数据库账号需拥有目标数据库的建表权限。已有管理员的数据不会被覆盖。</div></main></body></html>