更新sql安装器(web版本需要修复)

This commit is contained in:
2025-06-20 09:02:33 +08:00
parent 9f77221bdf
commit b2c9aecfb9
7 changed files with 1782 additions and 58 deletions

View File

@@ -1,61 +1,20 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
# Nl Admin
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## 配置好数据库连接信息后执行
```aiignore
php artisan sql:run-nl-admin
```
### 使用示例
```cmd
# 基本用法
php artisan sql:run-nl-admin
## About Laravel
# 指定SQL文件路径
php artisan sql:run-nl-admin --file=storage/sql/setup.sql
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
# 强制重置已有数据库
php artisan sql:run-nl-admin --force
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
# 创建新数据库
php artisan sql:run-nl-admin --database=my_new_db
```

View File

@@ -0,0 +1,661 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use PDOException;
class RunNlAdminSql extends Command
{
protected $signature = 'sql:run-nl-admin
{--file=public/nl_admin.sql : SQL文件路径}
{--force : 强制执行,跳过确认}
{--database= : 指定数据库名称}
{--charset=utf8mb4 : 数据库字符集}
{--collation=utf8mb4_unicode_ci : 数据库排序规则}';
protected $description = '执行 SQL 文件,自动处理数据库相关事项';
protected $maxFileSize = 50; // 最大文件大小 (MB)
protected $chunkSize = 10; // 分块大小 (MB)
public function handle()
{
$this->info('🚀 开始数据库安装程序');
// 1. 确保基础配置加载
$this->ensureConfigLoaded();
// 2. 优先使用.env配置创建数据库
$filePath = $this->getSqlFilePath();
// 3. 配置数据库连接(如果需要)
$this->ensureDatabaseConnection();
// 4. 处理目标数据库
$this->prepareTargetDatabase();
// 5. 执行SQL文件
$this->executeSql($filePath);
$this->info('✅ 数据库安装成功完成');
return 0;
}
/**
* 确保配置已加载
*/
protected function ensureConfigLoaded(): void
{
$this->call('config:clear');
// 确保数据库配置存在
$envPath = base_path('.env');
if (!file_exists($envPath)) {
File::copy(base_path('.env.example'), $envPath);
$this->info('📄 .env文件已创建基于.example');
}
}
/**
* 获取SQL文件路径
*/
protected function getSqlFilePath(): string
{
$filePath = base_path($this->option('file'));
// 验证文件是否存在
if (!file_exists($filePath)) {
$this->error("❌ SQL 文件未找到:{$filePath}");
$this->info("💡 请使用 --file 选项指定正确的文件路径");
exit(1);
}
// 检查文件大小
$fileSize = filesize($filePath) / 1024 / 1024; // MB
if ($fileSize > $this->maxFileSize) {
$this->warn("⚠️ 注意SQL文件较大 (" . round($fileSize, 2) . "MB),执行可能需要较长时间");
}
$this->info("📁 SQL文件位置: {$filePath}");
$this->info("📏 文件大小: " . round($fileSize, 2) . 'MB');
return $filePath;
}
/**
* 确保数据库连接可用
*/
protected function ensureDatabaseConnection(): void
{
$this->line('🔌 检查数据库连接...');
$retryCount = 0;
while (true) {
try {
DB::connection()->getPdo();
$this->info('✅ 数据库连接正常');
return;
} catch (\Exception $e) {
$this->error("❌ 数据库连接失败: " . $e->getMessage());
if ($retryCount > 0 || !$this->confirm('❓ 是否配置数据库连接?', true)) {
$this->error('🔌 无法连接数据库,操作终止');
exit(1);
}
$this->configureDatabase();
$retryCount++;
}
}
}
/**
* 配置数据库连接信息优先使用env默认值
*/
protected function configureDatabase(): void
{
$this->info('🔧 请提供数据库连接信息(留空使用默认值)');
$currentConfig = Config::get('database.connections.mysql');
$config = [
'DB_HOST' => $this->ask('数据库主机', $currentConfig['host'] ?? '127.0.0.1'),
'DB_PORT' => $this->ask('端口', $currentConfig['port'] ?? '3306'),
'DB_DATABASE' => $this->ask('数据库名', $currentConfig['database'] ?? 'nl_admin'),
'DB_USERNAME' => $this->ask('用户名', $currentConfig['username'] ?? 'root'),
'DB_PASSWORD' => $this->secret('密码') ?: ($currentConfig['password'] ?? ''),
];
// 更新 .env 文件
$this->updateEnvFile($config);
$this->info('📝 .env 文件已更新');
// 刷新配置缓存
$this->call('config:clear');
// 更新运行时配置
foreach ($config as $key => $value) {
$configKey = strtolower(substr($key, 3));
Config::set("database.connections.mysql.{$configKey}", $value);
}
// 测试连接并创建数据库
$this->testDatabaseConnection();
}
/**
* 测试数据库连接并创建数据库(如果需要)
*/
protected function testDatabaseConnection(): void
{
try {
// 测试连接
DB::connection('mysql')->getPdo();
$this->info('✅ 数据库连接测试成功');
} catch (\Exception $e) {
$this->error("❌ 配置后连接测试失败: " . $e->getMessage());
if ($this->confirm('是否尝试创建数据库?', true)) {
$this->createDatabaseFromEnv();
$this->info('✅ 数据库创建成功');
// 再次尝试连接
try {
DB::connection()->getPdo();
$this->info('✅ 数据库连接成功');
} catch (\Exception $e) {
$this->error("❌ 创建数据库后连接失败: " . $e->getMessage());
exit(1);
}
} else {
$this->error('❌ 无法连接数据库,操作终止');
exit(1);
}
}
}
/**
* 使用.env配置创建数据库
*/
protected function createDatabaseFromEnv(): void
{
$dbConfig = [
'driver' => 'mysql',
'host' => config('database.connections.mysql.host'),
'port' => config('database.connections.mysql.port'),
'username' => config('database.connections.mysql.username'),
'password' => config('database.connections.mysql.password'),
'database' => config('database.connections.mysql.database'),
'charset' => config('database.connections.mysql.charset', 'utf8mb4'),
'collation' => config('database.connections.mysql.collation', 'utf8mb4_unicode_ci')
];
$this->createDatabase($dbConfig);
}
/**
* 更新 .env 文件
*/
protected function updateEnvFile(array $config): void
{
$envPath = base_path('.env');
$envContents = file_exists($envPath) ? File::get($envPath) : '';
foreach ($config as $key => $value) {
$escapedValue = str_replace('"', '\\"', $value);
$line = "{$key}=\"{$escapedValue}\"";
if (preg_match("/^{$key}=.*/m", $envContents)) {
$envContents = preg_replace("/^{$key}=.*/m", $line, $envContents);
} else {
$envContents .= PHP_EOL . $line;
}
}
File::put($envPath, $envContents);
}
/**
* 准备目标数据库(基于.env配置
*/
protected function prepareTargetDatabase(): void
{
$this->line('🗄️ 检查目标数据库...');
$connection = config('database.default');
$dbConfig = config("database.connections.{$connection}");
$driver = $dbConfig['driver'];
// 处理数据库名称选项(优先使用命令行)
$databaseName = $this->getDatabaseName($dbConfig);
$this->info("📊 数据库驱动: {$driver}");
$this->info("🗃️ 目标数据库: {$databaseName}");
// 检查数据库是否存在
if (!$this->databaseExists($dbConfig)) {
$this->warn("❓ 数据库 {$databaseName} 不存在");
if ($this->option('force') || $this->confirm("是否创建数据库 {$databaseName}?", true)) {
$this->createDatabase([
'driver' => $driver,
'host' => $dbConfig['host'],
'port' => $dbConfig['port'],
'username' => $dbConfig['username'],
'password' => $dbConfig['password'],
'database' => $databaseName,
'charset' => $this->option('charset'),
'collation' => $this->option('collation')
]);
$this->info("✅ 数据库 {$databaseName} 创建成功");
} else {
$this->error('❌ 操作已取消');
exit(1);
}
}
// 刷新数据库连接确保使用的是目标数据库
$this->ensureUsingTargetDatabase($databaseName);
// 检查数据库表
$this->checkDatabaseTables($databaseName);
}
/**
* 获取数据库名称(优先使用命令行选项)
*/
protected function getDatabaseName(array $defaultConfig): string
{
if ($database = $this->option('database')) {
return $database;
}
return $defaultConfig['database'];
}
/**
* 确保连接到目标数据库
*/
protected function ensureUsingTargetDatabase(string $databaseName): void
{
// 更新配置为当前目标数据库
Config::set("database.connections.".config('database.default').".database", $databaseName);
// 刷新连接
DB::purge(config('database.default'));
DB::reconnect(config('database.default'));
$this->line("🔁 已切换至数据库: {$databaseName}");
}
/**
* 检查数据库表
*/
protected function checkDatabaseTables(string $databaseName): void
{
// 优化的表检查逻辑
$tableCount = $this->getTableCount($databaseName);
if ($tableCount > 0) {
$this->warn("⚠️ 数据库 {$databaseName} 包含 {$tableCount} 张表");
if (!$this->option('force') &&
!$this->confirm("此操作将重置数据库 {$databaseName} 中的所有数据。继续执行吗?", false)) {
$this->error('❌ 操作已取消');
exit(1);
}
} elseif ($tableCount === 0) {
$this->info("📭 数据库 {$databaseName} 为空");
} else {
$this->warn("⚠️ 无法确定数据库表数量,请谨慎操作");
}
}
/**
* 获取数据库中的表数量(优化版)
*/
protected function getTableCount(string $databaseName): int
{
try {
$driver = DB::getDriverName();
if ($driver === 'mysql') {
$result = DB::select("SELECT COUNT(*) AS count
FROM information_schema.tables
WHERE table_schema = ?",
[$databaseName]);
return (int) ($result[0]->count ?? 0);
}
if ($driver === 'sqlite') {
$result = DB::select("SELECT COUNT(*) AS count
FROM sqlite_master
WHERE type='table'");
return (int) ($result[0]->count ?? 0);
}
if ($driver === 'pgsql') {
$result = DB::select("SELECT COUNT(*) AS count
FROM information_schema.tables
WHERE table_schema='public'");
return (int) ($result[0]->count ?? 0);
}
// 其他数据库使用默认方法
$result = DB::select("SHOW TABLES");
return count($result);
} catch (\Exception $e) {
// 出现错误时返回-1表示无法确定
$this->warn("⚠️ 无法确定表数量: " . $e->getMessage());
return -1;
}
}
/**
* 执行SQL
*/
protected function executeSql(string $filePath): void
{
$fileSize = filesize($filePath) / 1024 / 1024; // MB
if ($fileSize > $this->maxFileSize) {
$this->executeSqlFileChunks($filePath);
} else {
$this->executeSingleSqlFile($filePath);
}
}
/**
* 单块执行SQL文件
*/
protected function executeSingleSqlFile(string $filePath): void
{
$this->line('🚀 开始导入SQL文件...');
try {
$sql = file_get_contents($filePath);
$this->smartTransactionHandler(function() use ($sql) {
$this->executeRawSql($sql);
});
$this->info('✅ SQL文件导入成功');
} catch (\Exception $e) {
$this->handleSqlError($e, $sql ?? '');
}
}
/**
* 分块执行SQL文件
*/
protected function executeSqlFileChunks(string $filePath): void
{
$this->line('🚀 开始分块导入SQL文件...');
$fileSize = filesize($filePath);
$chunkSize = $this->chunkSize * 1024 * 1024;
$chunkCount = ceil($fileSize / $chunkSize);
$this->info("📦 文件大小: " . round($fileSize / 1024 / 1024, 2) . "MB");
$this->info("🧩 分块数量: {$chunkCount} (每块 {$this->chunkSize}MB)");
$handle = fopen($filePath, 'r');
$chunkNum = 1;
$position = 0;
try {
while (!feof($handle)) {
// 定位到下一分块
fseek($handle, $position);
// 读取当前分块
$chunkData = fread($handle, $chunkSize);
// 确保完整语句 (找到下一个分号)
$lastSemicolon = strrpos($chunkData, ';');
if ($lastSemicolon !== false) {
$chunkData = substr($chunkData, 0, $lastSemicolon + 1);
}
$chunkSizeActual = strlen($chunkData);
$this->line("🔧 处理分块 {$chunkNum}/{$chunkCount} (" . round($chunkSizeActual / 1024 / 1024, 2) . "MB)...");
$this->smartTransactionHandler(function() use ($chunkData) {
$this->executeRawSql($chunkData);
});
$position += $chunkSizeActual;
$chunkNum++;
}
$this->info('✅ SQL文件导入成功');
} catch (\Exception $e) {
$this->handleSqlError($e, $chunkData ?? '');
} finally {
fclose($handle);
}
}
/**
* 智能事务处理
*/
protected function smartTransactionHandler(callable $callback): void
{
$driver = DB::getDriverName();
$this->prepareDatabaseForExecution($driver);
try {
// 对于支持事务的数据库驱动使用事务
if (in_array($driver, ['mysql', 'pgsql', 'sqlsrv', 'sqlite'])) {
DB::beginTransaction();
$callback();
DB::commit();
} else {
// 不支持事务的数据库直接执行
$callback();
}
} catch (\Exception $e) {
// 回滚事务(如果已经开启)
if (in_array($driver, ['mysql', 'pgsql', 'sqlsrv', 'sqlite']) &&
DB::connection()->transactionLevel() > 0) {
DB::rollBack();
}
throw $e;
} finally {
$this->restoreDatabaseSettings($driver);
}
}
/**
* 准备数据库执行环境
*/
protected function prepareDatabaseForExecution(string $driver): void
{
// MySQL 需要禁用外键检查
if ($driver === 'mysql') {
DB::statement('SET FOREIGN_KEY_CHECKS=0');
DB::statement('SET AUTOCOMMIT=0');
}
// PostgreSQL 需要禁用触发器
if ($driver === 'pgsql') {
DB::statement('SET session_replication_role = replica;');
}
}
/**
* 恢复数据库设置
*/
protected function restoreDatabaseSettings(string $driver): void
{
// MySQL 恢复设置
if ($driver === 'mysql') {
DB::statement('SET FOREIGN_KEY_CHECKS=1');
DB::statement('SET AUTOCOMMIT=1');
}
// PostgreSQL 恢复设置
if ($driver === 'pgsql') {
DB::statement('SET session_replication_role = DEFAULT;');
}
}
/**
* 执行原始SQL语句
*/
protected function executeRawSql(string $sql): void
{
// 使用原生方法执行以避免事务问题
try {
DB::getPdo()->exec($sql);
} catch (PDOException $e) {
// 处理部分SQL的错误记录但不中断
if (str_contains($e->getMessage(), 'already exists') ||
str_contains($e->getMessage(), 'doesn\'t exist')) {
$this->warn("⚠️ SQL警告: " . $e->getMessage());
} else {
throw $e;
}
}
}
/**
* 处理SQL错误
*/
protected function handleSqlError(\Exception $e, string $sqlFragment): void
{
$errorMsg = $e->getMessage();
$this->error("❌ SQL执行错误: {$errorMsg}");
// MySQL错误处理
if ($this->isMysql() && preg_match('/at line (\d+)/i', $errorMsg, $matches)) {
$lineNum = $matches[1];
$this->error("📌 错误位置: 大约第 {$lineNum}");
}
// 创建错误报告
$this->createErrorReport($e, $sqlFragment);
$this->error('❌ 数据库安装失败');
exit(1);
}
/**
* 创建错误报告
*/
protected function createErrorReport(\Exception $e, string $sqlFragment): void
{
$timestamp = date('Ymd-His');
$reportFile = storage_path("logs/sql_error_{$timestamp}.log");
$content = "SQL 错误报告: " . date('Y-m-d H:i:s') . PHP_EOL;
$content .= "错误信息: " . $e->getMessage() . PHP_EOL;
$content .= "错误位置: " . $e->getFile() . ':' . $e->getLine() . PHP_EOL;
$content .= "数据库驱动: " . config('database.default') . PHP_EOL;
$content .= "数据库名称: " . config("database.connections.".config('database.default').".database") . PHP_EOL;
if (!empty($sqlFragment)) {
$content .= PHP_EOL . "相关SQL片段: " . PHP_EOL;
$content .= substr($sqlFragment, -2000) . PHP_EOL; // 最后2000字符
}
file_put_contents($reportFile, $content);
$this->warn("📝 错误报告已保存到: {$reportFile}");
$this->line("💡 请将此文件提供给开发人员以诊断问题");
}
/**
* 检查是否为MySQL驱动
*/
protected function isMysql(): bool
{
return DB::getDriverName() === 'mysql';
}
/**
* 检查数据库是否存在基于env配置
*/
protected function databaseExists(array $config): bool
{
if ($config['driver'] === 'mysql') {
try {
$dbName = $this->getDatabaseName($config);
$result = DB::select("SELECT SCHEMA_NAME
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME = ?",
[$dbName]);
return !empty($result);
} catch (\Exception $e) {
$this->warn("⚠️ 数据库检查错误: " . $e->getMessage());
return false;
}
}
// SQLite 检查
if ($config['driver'] === 'sqlite') {
return file_exists($config['database']);
}
// PostgreSQL 检查
if ($config['driver'] === 'pgsql') {
try {
DB::statement("SELECT 1 FROM pg_database WHERE datname = ?", [$config['database']]);
return true;
} catch (\Exception) {
return false;
}
}
// 默认检查方法
try {
DB::statement("USE {$config['database']}");
return true;
} catch (\Exception) {
return false;
}
}
/**
* 创建数据库基于env配置
*/
protected function createDatabase(array $config): void
{
$dbName = $this->getDatabaseName($config);
$charset = $this->option('charset');
$collation = $this->option('collation');
$this->line("⚙️ 创建 {$config['driver']} 数据库: {$dbName} (字符集: {$charset}, 排序规则: {$collation})");
if ($config['driver'] === 'mysql') {
// 临时连接到系统数据库以创建新数据库
DB::purge('mysql');
Config::set('database.connections.mysql.database', null);
DB::reconnect('mysql');
$query = "CREATE DATABASE IF NOT EXISTS `{$dbName}` ";
$query .= "CHARACTER SET {$charset} COLLATE {$collation}";
DB::statement($query);
// 恢复配置
Config::set('database.connections.mysql.database', $dbName);
return;
}
if ($config['driver'] === 'sqlite') {
file_put_contents($config['database'], '');
return;
}
if ($config['driver'] === 'pgsql') {
DB::statement("CREATE DATABASE \"{$dbName}\"");
return;
}
$this->error("🚫 不支持的数据库驱动: {$config['driver']}");
exit(1);
}
}

View File

@@ -0,0 +1,232 @@
<?php
namespace App\Http\Controllers\core;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class SqlInstallController extends Controller
{
public function testConnection(Request $request)
{
$validated = $request->validate([
'host' => 'required|string',
'port' => 'required|integer',
'database' => 'required|string',
'username' => 'required|string',
'password' => 'required|string',
]);
// 创建临时配置
config([
'database.connections.install' => [
'driver' => 'mysql',
'host' => $validated['host'],
'port' => $validated['port'],
'database' => $validated['database'],
'username' => $validated['username'],
'password' => $validated['password'],
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
]
]);
try {
// 使用临时连接测试
DB::connection('install')->getPdo();
return response()->json([
'success' => true,
'message' => '✅ 成功连接到数据库'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '❌ 连接失败: ' . $e->getMessage()
], 500);
}
}
public function startInstallation(Request $request)
{
// 设置超时时间
set_time_limit(600);
$validated = $request->validate([
'file' => 'required|string',
'host' => 'required|string',
'port' => 'required|integer',
'database' => 'required|string',
'username' => 'required|string',
'password' => 'required|string',
'force' => 'boolean',
'backup' => 'boolean'
]);
$basePath = base_path();
$logs = ["[开始] SQL安装过程启动"];
$logs[] = "[项目目录] {$basePath}";
try {
// 更新环境变量
$config = [
'DB_HOST' => $validated['host'],
'DB_PORT' => $validated['port'],
'DB_DATABASE' => $validated['database'],
'DB_USERNAME' => $validated['username'],
'DB_PASSWORD' => $validated['password'],
];
// 更新 .env 文件
$this->updateEnvFile($config);
$logs[] = "✅ 环境变量已更新";
// 更新运行时配置
foreach ($config as $key => $value) {
$configKey = strtolower(substr($key, 3));
Config::set("database.connections.mysql.{$configKey}", $value);
}
// ds($logs);
$logs[] = "🔧 使用主机: {$validated['host']}";
$logs[] = "🔧 使用端口: {$validated['port']}";
$logs[] = "🔧 使用数据库: {$validated['database']}";
$logs[] = "🔧 SQL文件: {$validated['file']}";
// 检查数据库是否存在,如果不存在则创建
$logs[] = "🔍 检查数据库是否存在";
// $connection = DB::connection('mysql');
// $databaseName = $validated['database'];
// $result = $connection->select("
// SELECT SCHEMA_NAME
// FROM INFORMATION_SCHEMA.SCHEMATA
// WHERE SCHEMA_NAME = ?
// ", [$databaseName]);
//
// if (empty($result)) {
// $logs[] = "🔧 数据库不存在,正在创建...";
// $connection->statement("CREATE DATABASE IF NOT EXISTS `$databaseName`");
// $logs[] = "✅ 数据库已创建";
// }
// 构建 Artisan 命令 - 使用相对路径解决权限问题
$command = [
'php',
'artisan',
'sql:run-nl-admin',
'--file=' . $validated['file'],
'--database=' . $validated['database'],
];
// ds($command);
if ($validated['force'] ?? false) {
$command[] = '--force';
$logs[] = "⚠️ 强制模式已启用 (将覆盖现有数据)";
}
if ($validated['backup'] ?? false) {
$command[] = '--backup';
$logs[] = "📦 数据库备份已启用";
}
$logs[] = "🔄 正在执行: " . implode(' ', $command);
// 改变工作目录解决路径问题
// $process = new Process($command, $basePath);
$process = new Process($command, $basePath);
$process->setTimeout(300);
$process->start();
// 实时捕获输出
$process->wait(function ($type, $buffer) use (&$logs) {
if (!empty(trim($buffer))) {
$bufferLines = array_map('trim', explode("\n", trim($buffer)));
foreach ($bufferLines as $line) {
if (!empty($line)) {
$logs[] = $line;
}
}
}
});
// 检查执行结果
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
// 添加详细成功日志
$logs[] = "✅ SQL文件执行成功";
$logs[] = "📊 执行命令: " . $process->getCommandLine();
$logs[] = "🆗 退出状态: " . $process->getExitCode();
// 获取SQL文件大小
$filePath = base_path($validated['file']);
$fileSize = file_exists($filePath) ? filesize($filePath) : 0;
return response()->json([
'success' => true,
'logs' => $logs,
'file_size' => $fileSize
]);
} catch (\Exception $e) {
$logs[] = "❌ 安装过程中出错: " . $e->getMessage();
// 添加详细的错误分析
if ($e instanceof ProcessFailedException) {
$logs[] = "🛠 退出代码: " . $e->getProcess()->getExitCode();
$logs[] = "📜 错误输出: " . $e->getProcess()->getErrorOutput();
// 分析常见权限问题
$errorOutput = $e->getProcess()->getErrorOutput();
if (strpos($errorOutput, 'Permission denied') !== false) {
$logs[] = "🔑 权限问题检测:";
$logs[] = " 1. 确保Web服务器对扩展目录有读取权限";
$logs[] = " 2. 尝试运行: sudo chmod -R a+r /usr/local/lib/php/extensions/";
$logs[] = " 3. 检查SELinux/AppArmor设置";
}
}
return response()->json([
'success' => false,
'message' => $e->getMessage(),
'logs' => $logs
], 500);
}
}
/**
* 更新 .env 文件
*/
protected function updateEnvFile(array $config): void
{
$envPath = base_path('.env');
$envContents = file_exists($envPath) ? File::get($envPath) : '';
foreach ($config as $key => $value) {
$escapedValue = str_replace('"', '\\"', $value);
$line = "{$key}=\"{$escapedValue}\"";
if (preg_match("/^{$key}=.*/m", $envContents)) {
$envContents = preg_replace("/^{$key}=.*/m", $line, $envContents);
} else {
$envContents .= PHP_EOL . $line;
}
}
File::put($envPath, $envContents);
}
}

View File

@@ -10,7 +10,8 @@
"ext-zip": "*",
"firebase/php-jwt": "^6.11",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1"
"laravel/tinker": "^2.10.1",
"ext-pdo": "*"
},
"require-dev": {
"fakerphp/faker": "^1.23",

View File

@@ -0,0 +1,863 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>SQL 安装器 - Laravel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary-color: #6366f1;
--success-color: #10b981;
--warning-color: #f59e0b;
--danger-color: #ef4444;
--dark-color: #1e293b;
--light-color: #f8fafc;
}
body {
background: linear-gradient(135deg, #f0f4f8, #e2e8f0);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
padding: 2rem 0;
min-height: 100vh;
}
.logo-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 1.5rem;
}
.logo-header i {
font-size: 2.5rem;
color: var(--primary-color);
}
.card {
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
border: none;
overflow: hidden;
margin-bottom: 2rem;
}
.card-header {
background: var(--primary-color);
color: white;
font-weight: 600;
font-size: 1.25rem;
padding: 1.25rem 1.5rem;
border-bottom: none;
}
.card-body {
padding: 2rem;
}
.progress-container {
position: relative;
height: 2.5rem;
margin-bottom: 1.5rem;
border-radius: 12px;
background-color: #e2e8f0;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 12px;
transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
position: absolute;
top: 0;
left: 0;
background: linear-gradient(90deg, var(--primary-color), #4f46e5);
}
.log-container {
background-color: #1e293b;
color: #cbd5e1;
font-family: 'Fira Code', monospace;
border-radius: 12px;
padding: 1.5rem;
height: 320px;
overflow-y: auto;
white-space: pre-wrap;
margin-bottom: 1.5rem;
font-size: 0.9rem;
line-height: 1.6;
}
.log-item {
margin-bottom: 6px;
border-left: 2px solid transparent;
padding-left: 12px;
}
.log-item.info {
border-left-color: #60a5fa;
color: #bfdbfe;
}
.log-item.success {
border-left-color: var(--success-color);
color: #6ee7b7;
}
.log-item.warning {
border-left-color: var(--warning-color);
color: #fcd34d;
}
.log-item.error {
border-left-color: var(--danger-color);
color: #fca5a5;
}
.btn-action {
padding: 0.75rem 1.5rem;
font-weight: 600;
border-radius: 12px;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color), #4f46e5);
box-shadow: 0 4px 6px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
background: linear-gradient(135deg, #4f46e5, #4338ca);
box-shadow: 0 6px 8px rgba(99, 102, 241, 0.4);
transform: translateY(-2px);
}
.status-indicator {
width: 18px;
height: 18px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;
box-shadow: 0 0 8px currentColor;
}
.status-idle {
background-color: #94a3b8;
}
.status-running {
background-color: var(--primary-color);
animation: pulse 1.5s infinite;
}
.status-success {
background-color: var(--success-color);
}
.status-error {
background-color: var(--danger-color);
}
.info-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.info-card h5 {
color: var(--primary-color);
margin-bottom: 1rem;
font-weight: 600;
}
.info-card .info-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid #f1f5f9;
}
.info-card .info-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.6; }
100% { opacity: 1; }
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.1rem;
font-weight: 600;
color: var(--dark-color);
margin-bottom: 1.2rem;
}
.config-section {
background: rgba(241, 245, 249, 0.5);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
font-weight: bold;
margin-right: 12px;
}
.highlight {
background: rgba(99, 102, 241, 0.1);
border-left: 4px solid var(--primary-color);
padding: 0.5rem 1rem;
border-radius: 0 8px 8px 0;
margin: 1rem 0;
}
.floating-message {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1000;
padding: 1rem 1.5rem;
border-radius: 12px;
font-weight: 500;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
opacity: 0;
transform: translateY(20px);
transition: all 0.4s ease;
}
.floating-message.show {
opacity: 1;
transform: translateY(0);
}
.floating-success {
background: var(--success-color);
color: white;
}
.floating-error {
background: var(--danger-color);
color: white;
}
.permission-hint {
background-color: #f8d7da;
border-left: 4px solid #dc3545;
padding: 10px;
border-radius: 4px;
margin: 15px 0;
display: none;
}
.permission-hint-title {
color: #dc3545;
font-weight: bold;
}
.permission-solution {
background-color: #d1ecf1;
border-left: 4px solid #0dcaf0;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="logo-header">
<i class="fas fa-database"></i>
<h1>Laravel SQL 安装器</h1>
</div>
<!-- 权限问题提示 -->
<div class="permission-hint" id="permissionHint">
<div class="permission-hint-title">
<i class="fas fa-exclamation-triangle me-2"></i>检测到权限问题
</div>
<p>您的系统可能遇到了文件权限问题导致无法加载PHP扩展。</p>
<div class="permission-solution">
<h6><i class="fas fa-wrench me-2"></i>解决方法:</h6>
<ol>
<li>检查Web服务器用户如www-data是否有权访问PHP扩展目录</li>
<li>在服务器终端执行:<code>sudo chmod -R a+r /usr/local/lib/php/extensions/</code></li>
<li>如果使用SELinux/AppArmor调整安全策略允许访问</li>
<li>确保PHP进程有权执行<code>artisan</code>命令</li>
</ol>
</div>
</div>
<div class="row">
<!-- 左侧配置面板 -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<i class="fas fa-sliders-h me-2"></i>数据库配置
</div>
<div class="card-body">
<form id="installForm">
<div class="config-section">
<div class="section-title">
<i class="fas fa-server"></i>
<span>数据库连接</span>
</div>
<div class="mb-3">
<label for="host" class="form-label">主机</label>
<input type="text" class="form-control" id="host"
placeholder="数据库主机" value="{{ env('DB_HOST', '127.0.0.1') }}">
</div>
<div class="mb-3">
<label for="port" class="form-label">端口</label>
<input type="number" class="form-control" id="port"
placeholder="端口号" value="{{ env('DB_PORT', 3306) }}">
</div>
<div class="mb-3">
<label for="database" class="form-label">数据库名</label>
<input type="text" class="form-control" id="database"
placeholder="数据库名称" value="{{ env('DB_DATABASE', 'nl_admin') }}">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username"
placeholder="用户名" value="{{ env('DB_USERNAME', 'root') }}">
</div>
<div class="col-md-6 mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password"
placeholder="密码" value="{{ env('DB_PASSWORD', '') }}">
</div>
</div>
<div class="d-grid mt-3">
<button type="button" class="btn btn-primary" id="testConnectionBtn">
<i class="fas fa-plug"></i> 测试连接
</button>
</div>
</div>
<div class="config-section">
<div class="section-title">
<i class="fas fa-file-code"></i>
<span>SQL 文件设置</span>
</div>
<div class="mb-3">
<label for="sqlPath" class="form-label">SQL 文件路径</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-folder"></i></span>
<input type="text" class="form-control" id="sqlPath"
placeholder="输入SQL文件路径" value="public/nl_admin.sql">
</div>
<div class="form-text mt-1">相对于应用根目录的文件路径</div>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="forceInstall">
<label class="form-check-label" for="forceInstall">
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
强制安装(覆盖现有数据)
</label>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="backupDb">
<label class="form-check-label" for="backupDb">
<i class="fas fa-file-archive text-info me-2"></i>
安装前备份数据库
</label>
</div>
<div class="d-grid gap-2 mt-4">
<button type="button" class="btn btn-primary" id="startInstallBtn">
<i class="fas fa-play-circle"></i> 开始安装
</button>
</div>
</div>
</form>
</div>
</div>
<div class="info-card">
<h5><i class="fas fa-info-circle text-primary me-2"></i>使用说明</h5>
<div class="highlight">
<p>1. 配置数据库连接信息</p>
<p>2. 设置SQL文件路径默认为<code>public/nl_admin.sql</code></p>
<p>3. 测试数据库连接是否成功</p>
<p>4. 点击"开始安装"执行SQL文件</p>
</div>
<div class="info-item">
<span><i class="fas fa-exclamation-triangle text-warning me-2"></i>重要提示:</span>
<span class="text-end">强制安装选项会覆盖现有数据,请谨慎使用</span>
</div>
</div>
</div>
<!-- 右侧监控面板 -->
<div class="col-lg-6">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-chart-line me-2"></i>安装进度
</div>
<div id="statusDisplay">
<div class="status-indicator status-idle"></div>
<span id="statusText">准备就绪</span>
</div>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<span id="progressPercentage">0%</span>
<span id="fileSize" class="badge bg-light text-dark">
<i class="fas fa-file-alt me-1"></i> 文件大小: 0 KB
</span>
</div>
<div class="progress-container">
<div id="progressBar" class="progress-bar" style="width: 0%"></div>
</div>
<div class="log-container" id="logContainer">
<div class="log-item info">== 系统日志 ==</div>
<div class="log-item info">等待操作开始...</div>
</div>
<div class="d-grid gap-2 mt-4">
<button type="button" class="btn btn-outline-primary" id="downloadLogBtn" style="display: none;">
<i class="fas fa-download me-2"></i>下载日志报告
</button>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<i class="fas fa-database me-2"></i>数据库信息
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="info-item">
<span>状态:</span>
<span id="dbStatus" class="text-primary">未连接</span>
</div>
<div class="info-item">
<span>驱动:</span>
<span id="dbDriver" class="text-info">{{ env('DB_CONNECTION', '未设置') }}</span>
</div>
<div class="info-item">
<span>字符集:</span>
<span>utf8mb4</span>
</div>
</div>
<div class="col-md-6">
<div class="info-item">
<span>主机:</span>
<span id="dbHost">{{ env('DB_HOST', '未设置') }}</span>
</div>
<div class="info-item">
<span>端口:</span>
<span id="dbPort">{{ env('DB_PORT', '未设置') }}</span>
</div>
<div class="info-item">
<span>数据库名:</span>
<span id="dbName">{{ env('DB_DATABASE', '未设置') }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 操作结果提示 -->
<div id="floatingMessage" class="floating-message">
<i class="fas fa-exclamation-circle me-2"></i> <span id="floatingText"></span>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 获取所有DOM元素引用
const elements = {
logContainer: document.getElementById('logContainer'),
progressBar: document.getElementById('progressBar'),
progressPercentage: document.getElementById('progressPercentage'),
startInstallBtn: document.getElementById('startInstallBtn'),
testConnectionBtn: document.getElementById('testConnectionBtn'),
downloadLogBtn: document.getElementById('downloadLogBtn'),
statusIndicator: document.querySelector('.status-indicator'),
statusText: document.getElementById('statusText'),
dbStatus: document.getElementById('dbStatus'),
dbHost: document.getElementById('dbHost'),
dbPort: document.getElementById('dbPort'),
dbName: document.getElementById('dbName'),
dbDriver: document.getElementById('dbDriver'),
floatingMessage: document.getElementById('floatingMessage'),
floatingText: document.getElementById('floatingText'),
permissionHint: document.getElementById('permissionHint')
};
let installLog = [];
let currentProgress = 0;
// 添加日志函数
function addLog(message, type = 'info') {
const logItem = document.createElement('div');
logItem.className = `log-item ${type}`;
const now = new Date();
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
logItem.textContent = `[${time}] ${message}`;
elements.logContainer.appendChild(logItem);
// 滚动到底部
elements.logContainer.scrollTop = elements.logContainer.scrollHeight;
// 保存日志
installLog.push({
time: new Date().toISOString(),
message,
type
});
// 检测是否是权限错误
if (message.includes('Permission denied')) {
elements.permissionHint.style.display = 'block';
}
// 检测进度信息
detectProgress(message);
}
// 从日志消息中检测进度
function detectProgress(message) {
// 如果消息中包含进度百分比
if (message.includes('%') && /[0-9]{1,3}%/.test(message)) {
const match = message.match(/(\d{1,3})%/);
if (match && match[1]) {
currentProgress = parseInt(match[1]);
updateProgress(currentProgress);
}
}
// 检测文件大小信息
else if (message.includes('文件大小')) {
const match = message.match(/大小: ([0-9.]+)MB/);
if (match && match[1]) {
const size = Math.round(parseFloat(match[1]) * 1024);
elements.fileSize.innerHTML = `<i class="fas fa-file-alt me-1"></i> 文件大小: ${size} KB`;
}
}
}
// 更新状态显示
function updateStatus(status) {
elements.statusIndicator.className = 'status-indicator';
switch(status) {
case 'idle':
elements.statusIndicator.classList.add('status-idle');
elements.statusText.textContent = '准备就绪';
break;
case 'running':
elements.statusIndicator.classList.add('status-running');
elements.statusText.textContent = '安装进行中...';
elements.startInstallBtn.disabled = true;
elements.testConnectionBtn.disabled = true;
break;
case 'success':
elements.statusIndicator.classList.add('status-success');
elements.statusText.textContent = '安装成功';
elements.startInstallBtn.disabled = false;
elements.testConnectionBtn.disabled = false;
break;
case 'error':
elements.statusIndicator.classList.add('status-error');
elements.statusText.textContent = '安装失败';
elements.startInstallBtn.disabled = false;
elements.testConnectionBtn.disabled = false;
elements.downloadLogBtn.style.display = 'block';
break;
}
}
// 更新数据库连接状态
function updateDbStatus(status) {
elements.dbStatus.textContent = status;
elements.dbStatus.className = '';
switch(status) {
case '已连接':
elements.dbStatus.classList.add('text-success');
break;
case '连接失败':
elements.dbStatus.classList.add('text-danger');
break;
default:
elements.dbStatus.classList.add('text-primary');
}
}
// 更新进度条
function updateProgress(percentage, message) {
const percent = Math.min(100, Math.max(0, percentage));
elements.progressBar.style.width = `${percent}%`;
elements.progressPercentage.textContent = `${Math.round(percent)}%`;
if (message) {
addLog(message);
}
}
// 显示浮动消息
function showFloatingMessage(message, type) {
elements.floatingText.textContent = message;
elements.floatingMessage.className = 'floating-message';
elements.floatingMessage.classList.add('show');
if (type === 'success') {
elements.floatingMessage.classList.add('floating-success');
} else {
elements.floatingMessage.classList.add('floating-error');
}
setTimeout(() => {
elements.floatingMessage.classList.remove('show');
setTimeout(() => {
elements.floatingMessage.className = 'floating-message';
}, 300);
}, 5000);
}
// 测试数据库连接
async function testDatabaseConnection() {
const host = document.getElementById('host').value;
const port = document.getElementById('port').value;
const database = document.getElementById('database').value;
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
// 更新数据库信息显示
elements.dbHost.textContent = host || '未设置';
elements.dbPort.textContent = port || '未设置';
elements.dbName.textContent = database || '未设置';
addLog('正在测试数据库连接...', 'info');
try {
const response = await fetch('/api/sql/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
host,
port,
database,
username,
password
})
});
const result = await response.json();
if (result.success) {
addLog('✅ 数据库连接成功!', 'success');
updateDbStatus('已连接');
showFloatingMessage('✅ 数据库连接成功!', 'success');
// 更新数据库信息
elements.dbDriver.textContent = 'MySQL';
} else {
const errorMessage = `❌ ${result.message || '数据库连接失败'}`;
addLog(errorMessage, 'error');
updateDbStatus('连接失败');
showFloatingMessage(errorMessage, 'error');
}
} catch (error) {
const errorMessage = `❌ 连接测试出错: ${error.message}`;
addLog(errorMessage, 'error');
updateDbStatus('连接失败');
showFloatingMessage(errorMessage, 'error');
}
}
// 开始安装过程
async function startInstallation() {
// 重置日志和UI状态
elements.logContainer.innerHTML = '<div class="log-item info">== 安装日志 ==</div>';
installLog = [];
currentProgress = 0;
updateProgress(0);
updateStatus('running');
elements.downloadLogBtn.style.display = 'none';
elements.permissionHint.style.display = 'none';
const sqlPath = document.getElementById('sqlPath').value;
const host = document.getElementById('host').value;
const port = document.getElementById('port').value;
const database = document.getElementById('database').value;
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const forceInstall = document.getElementById('forceInstall').checked;
const backupDb = document.getElementById('backupDb').checked;
addLog('🚀 开始数据库安装流程...', 'info');
addLog(`📁 文件路径: ${sqlPath}`, 'info');
addLog(`🔧 数据库配置: ${username}@${host}:${port}`, 'info');
addLog(`🗃️ 数据库名: ${database}`, 'info');
addLog(`⚡ 强制模式: ${forceInstall ? '是' : '否'}`, forceInstall ? 'warning' : 'info');
try {
const response = await fetch('/api/sql/start-installation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
file: sqlPath,
host,
port,
database,
username,
password,
force: forceInstall,
backup: backupDb
})
});
const result = await response.json();
if (!response.ok) {
// 处理错误情况
const errorMsg = result.message || '安装过程中发生错误';
// 确保日志存在
if (result.logs && Array.isArray(result.logs)) {
result.logs.forEach(log => {
let type = 'info';
if (log.includes('❌') || log.includes('失败')) type = 'error';
if (log.includes('⚠️')) type = 'warning';
if (log.includes('✅')) type = 'success';
addLog(log, type);
});
} else {
addLog(`❌ ${errorMsg}`, 'error');
}
showFloatingMessage(`❌ ${errorMsg}`, 'error');
updateStatus('error');
return;
}
// 处理成功情况
if (result.success && Array.isArray(result.logs)) {
result.logs.forEach(log => {
let type = 'info';
if (log.includes('❌') || log.includes('失败')) type = 'error';
if (log.includes('⚠️')) type = 'warning';
if (log.includes('✅')) type = 'success';
addLog(log, type);
});
// 更新文件大小显示
if (result.file_size) {
const sizeKB = Math.round(result.file_size / 1024);
elements.fileSize.innerHTML =
`<i class="fas fa-file-alt me-1"></i> 文件大小: ${sizeKB} KB`;
}
// 更新进度到100%
updateProgress(100, '✅ 安装完成');
showFloatingMessage('✅ 安装成功!', 'success');
updateStatus('success');
} else {
addLog('❌ 安装完成但返回数据异常', 'error');
showFloatingMessage('❌ 安装完成但返回数据异常', 'error');
updateStatus('error');
}
} catch (error) {
addLog(`❌ 安装过程中出错: ${error.message}`, 'error');
showFloatingMessage(`❌ 安装失败: ${error.message}`, 'error');
updateStatus('error');
}
}
// 下载日志报告
function downloadLogReport() {
const logContent = installLog.map(entry =>
`[${new Date(entry.time).toLocaleTimeString()}] ${entry.message}`
).join('\n');
const blob = new Blob([logContent], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `sql_install_log_${new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')}.log`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 绑定事件监听器
elements.startInstallBtn.addEventListener('click', startInstallation);
elements.testConnectionBtn.addEventListener('click', testDatabaseConnection);
elements.downloadLogBtn.addEventListener('click', downloadLogReport);
// 页面加载完成状态
addLog('系统初始化完毕', 'info');
updateStatus('idle');
updateDbStatus('未连接');
// 初始显示数据库信息
elements.dbHost.textContent = "{{ env('DB_HOST', '未设置') }}";
elements.dbPort.textContent = "{{ env('DB_PORT', '未设置') }}";
elements.dbName.textContent = "{{ env('DB_DATABASE', '未设置') }}";
elements.dbDriver.textContent = "{{ env('DB_CONNECTION', '未设置') }}";
});
</script>
</body>
</html>

View File

@@ -1,11 +1,16 @@
<?php
use App\Http\Controllers\core\SqlInstallController;
use App\Service\common\UtilsService;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::post('/sql/test-connection', [SqlInstallController::class, 'testConnection']);
Route::post('/sql/start-installation', [SqlInstallController::class, 'startInstallation']);
/*
* -----------------------------------以下注释不要删除--------------------------------
* 不需要登录的路由生成地址

View File

@@ -5,3 +5,6 @@ use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/init', function () {
return view('init');
});