Files
nl-jqrl-api/app/Console/Commands/RunNlAdminSql.php

662 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
}