662 lines
21 KiB
PHP
662 lines
21 KiB
PHP
|
|
<?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);
|
|||
|
|
}
|
|||
|
|
}
|