Files
MrleeAdmin/backend/app/service/core/install/InstallService.php
T
2026-08-31 11:33:59 +08:00

1805 lines
62 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
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: https://madong.tech
*/
namespace app\service\core\install;
use app\service\core\terminal\Terminal;
use app\service\core\install\traits\InstallDatabaseTrait;
use app\service\core\install\traits\MenuTrait;
use app\service\core\install\traits\DictTrait;
use app\service\core\install\traits\ConfigTrait;
use app\service\core\install\traits\AdminTrait;
use core\tool\Sse;
use core\tool\Util;
use Exception;
use Throwable;
use core\db\DataImporterService;
/**
* 安装服务类
* 基于SSE和生成器模式实现简化安装流程
*/
final class InstallService
{
use InstallDatabaseTrait;
use MenuTrait;
use DictTrait;
use ConfigTrait;
use AdminTrait;
protected string $lock_file;
protected ?DataImporterService $dataImporter = null;
protected ?Terminal $terminalService = null;
public function __construct()
{
$this->lock_file = base_path() . '/install.lock';
// 不在构造函数中实例化依赖,延迟加载
}
/**
* 获取 DataImporterService 实例
*/
protected function getDataImporter(): DataImporterService
{
if ($this->dataImporter === null) {
$this->dataImporter = new DataImporterService();
}
return $this->dataImporter;
}
/**
* 获取 Terminal 实例
*/
protected function getTerminal(): Terminal
{
if ($this->terminalService === null) {
$this->terminalService = Terminal::create();
}
return $this->terminalService;
}
/**
* 检查是否已安装
*
* @return bool
*/
public function checkInstalled(): bool
{
// 输出调试信息
echo "Checking for install.lock at: {$this->lock_file}\n";
echo "File exists: " . (file_exists($this->lock_file) ? "Yes" : "No") . "\n";
return file_exists($this->lock_file);
}
/**
* 获取许可协议数据
*
* @return array
*/
public function getAgreementData(): array
{
return [
'title' => 'MDAdmin 软件许可协议',
'content' => $this->getLicenseAgreement(),
'version' => '1.0.0',
'update_time' => date('Y-m-d H:i:s'),
];
}
/**
* 检查系统环境
*
* @return array
*/
public function checkEnvironment(): array
{
$system_variables = $this->checkSystemRequirements();
$dirs_list = $this->checkDirectoryPermissions();
$all_ok = $this->checkAllRequirements($system_variables, $dirs_list);
return [
'check_items' => $system_variables,
'directory_check_items' => $dirs_list,
'all_requirements_met' => $all_ok,
'php_version' => PHP_VERSION,
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? '未知',
'operating_system' => PHP_OS,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
];
}
/**
* 验证数据库配置
*
* @param array $config
*
* @return array
*/
public function validateDatabaseConfig(array $config): array
{
$errors = [];
// 必填字段验证
$required = ['host', 'port', 'username', 'password', 'database'];
foreach ($required as $field) {
if (empty($config[$field])) {
$errors[] = "数据库{$field}不能为空";
}
}
// 端口验证
if (!is_numeric($config['port']) || $config['port'] <= 0 || $config['port'] > 65535) {
$errors[] = "数据库端口必须为1-65535之间的数字";
}
// 数据库名称验证
if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $config['database'])) {
$errors[] = "数据库名称格式不正确";
}
return $errors;
}
/**
* 验证管理员配置
*
* @param array $config
*
* @return array
*/
public function validateAdminConfig(array $config): array
{
$errors = [];
// 必填字段验证
$required = ['username', 'password', 'email'];
foreach ($required as $field) {
if (empty($config[$field])) {
$errors[] = "管理员{$field}不能为空";
}
}
// 邮箱格式验证
if (!filter_var($config['email'], FILTER_VALIDATE_EMAIL)) {
$errors[] = "邮箱格式不正确";
}
// 密码强度验证
if (strlen($config['password']) < 6) {
$errors[] = "密码长度不能少于6位";
}
return $errors;
}
/**
* 测试数据库连接
*
* @param array $config
*
* @return array
* @throws Exception
*/
public function testDatabaseConnection(array $config): array
{
try {
$dsn = "mysql:host={$config['host']};port={$config['port']}";
$pdo = new \PDO($dsn, $config['username'], $config['password']);
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
return [
'connected' => true,
'message' => '数据库连接成功',
];
} catch (Exception $e) {
throw new Exception('数据库连接失败: ' . $e->getMessage());
}
}
/**
* 测试 Redis 连接
*
* @param array $config Redis 配置(host, port, password)
*
* @return array
* @throws Exception
*/
public function testRedisConnection(array $config): array
{
try {
$host = $config['host'] ?? '127.0.0.1';
$port = (int)($config['port'] ?? 6379);
$password = $config['password'] ?? '';
$timeout = 3.0;
if (!extension_loaded('redis')) {
throw new Exception('Redis 扩展未安装');
}
$redis = new \Redis();
if (!$redis->connect($host, $port, $timeout)) {
throw new Exception('无法连接到 Redis 服务器');
}
// 密码认证
if (!empty($password)) {
if (!$redis->auth($password)) {
throw new Exception('Redis 密码认证失败');
}
}
// 执行 ping 验证连接可用
// phpredis 不同版本返回值不同:高版本返回 true,低版本返回 '+PONG'
$pong = $redis->ping();
if ($pong !== true && stripos((string)$pong, 'PONG') === false) {
throw new Exception('Redis PING 测试失败');
}
$redis->close();
return [
'connected' => true,
'message' => 'Redis 连接成功',
];
} catch (Exception $e) {
throw new Exception('Redis 连接失败: ' . $e->getMessage());
}
}
/**
* 安装-SSE模式
*
* 流程:1.验证安装状态 -> 2.环境检测 -> 3.数据库配置 -> 4.参数配置 -> 5.执行安装 -> 6.创建.env(最后)
*
* @param array $params
*
* @return \Generator
* @throws \Exception
*/
public function install(array $params): \Generator
{
$sessionUuid = uniqid('install_', true);
yield Sse::progress('> 执行安装', 1, [], $sessionUuid);
try {
// 1. 验证安装状态 (1-10%)
yield Sse::progress('> 1/6 验证安装状态', 5, [], $sessionUuid);
if ($this->checkInstalled()) {
throw new Exception('系统已安装,请勿重复安装');
}
// 2. 环境检测 (10-15%)
yield Sse::progress('> 2/6 环境检测...', 12, [], $sessionUuid);
if (!$this->checkEnvironment()['all_requirements_met']) {
throw new Exception('系统环境不满足安装要求');
}
// 3. 数据库配置验证 (15-20%)
yield Sse::progress('> 3/6 数据库配置验证', 17, [], $sessionUuid);
$dbParams = $this->extractPrefixedData($params, 'db_');
$errors = $this->validateDatabaseConfig($dbParams);
if (!empty($errors)) {
throw new Exception('安装参数验证失败: ' . implode(', ', $errors));
}
// 4. 参数配置验证 (20-25%)
yield Sse::progress('> 4/6 参数配置验证', 22, [], $sessionUuid);
$adminParams = $this->extractPrefixedData($params, 'admin_');
$errors = $this->validateAdminConfig($adminParams);
if (!empty($errors)) {
throw new Exception('安装参数验证失败: ' . implode(', ', $errors));
}
// 提取 Redis 配置参数
$redisParams = [
'host' => $params['redis_host'] ?? '127.0.0.1',
'port' => $params['redis_port'] ?? '6379',
'password' => $params['redis_password'] ?? '',
];
// 测试数据库连接 (25-30%)
yield Sse::progress('> 测试数据库连接', 26, [], $sessionUuid);
$this->testDatabaseConnection($dbParams);
// 测试 Redis 连接 (30-33%)
yield Sse::progress('> 测试 Redis 连接', 30, [], $sessionUuid);
$this->testRedisConnection($redisParams);
// 5. 执行安装 (33-63%)
if ($params['install_database'] == 1) {
yield Sse::progress('> 5/6 执行数据库安装', 30, [], $sessionUuid);
yield from $this->installDatabaseTables($dbParams, $adminParams, $sessionUuid);
}
if ($params['install_database'] == 0) {
yield Sse::progress('> 5/6 创建系统管理员', 30, [], $sessionUuid);
yield from $this->initializeAdmin($dbParams, $adminParams, $sessionUuid);
}
// 前端构建 (60-80%)
if ($params['build_project'] == 1) {
yield Sse::progress('> 检查前端项目', 60, [], $sessionUuid);
yield from $this->processFrontendProjects($sessionUuid);
}
// 创建安装锁 (80-90%)
yield Sse::progress('> 创建安装锁', 85, [], $sessionUuid);
$this->createInstallLock();
// 6. 创建 .env 文件 (90-99%)
yield Sse::progress('> 6/6 创建.env配置文件', 93, [], $sessionUuid);
yield Sse::progress('> 写入数据库配置', 94, [], $sessionUuid);
yield Sse::progress('> 写入 Redis 配置', 95, [], $sessionUuid);
$this->copyDatabaseTemplateAndCreateEnv($dbParams, $redisParams);
// 完成安装 (100%)
yield Sse::completed('安装完成', [], $sessionUuid);
Util::reloadWebman();
return;
} catch (Throwable $e) {
yield Sse::runtimeError($e->getMessage(), [], $sessionUuid);
return;
}
}
/**
* 检查前端项目目录是否存在
* 前端代码统一在 frontend 目录下
*
* @param string $projectType
*
* @return bool
*/
private function checkFrontendProjectExists(string $projectType): bool
{
$baseDir = dirname(base_path());
$projectDir = $baseDir . '/frontend/' . $projectType;
$packageJson = $projectDir . '/package.json';
return is_dir($projectDir) && file_exists($packageJson);
}
/**
* 检查构建目录是否存在
*
* @param string $projectType
*
* @return bool
*/
private function checkBuildDirectoryExists(string $projectType): bool
{
$config = config("terminal.frontend_programs.{$projectType}");
if (!$config || !$config['enabled']) {
return false;
}
// 处理不同项目的构建目录路径
$sourceDir = $config['source_dir'] ?? '';
// 替换路径变量
$sourceDir = $this->replacePathVariables($sourceDir);
// 标准化路径分隔符(Windows兼容)
$sourceDir = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $sourceDir);
// 检查构建目录是否存在
if (!file_exists($sourceDir)) {
error_log("构建目录不存在: " . $sourceDir);
return false;
}
// 检查构建目录是否为空
$files = glob($sourceDir . DIRECTORY_SEPARATOR . '*');
if (empty($files)) {
error_log("构建目录为空: " . $sourceDir);
return false;
}
return true;
}
/**
* 等待构建完成-带重试机制
*
* @param string $projectType
* @param int $maxRetries
* @param int $retryInterval
*
* @return bool
*/
private function waitForBuildCompletion(string $projectType, int $maxRetries = 5, int $retryInterval = 2000000): bool
{
$config = config("terminal.frontend_programs.{$projectType}");
if (!$config || !$config['enabled']) {
return false;
}
$sourceDir = $config['source_dir'] ?? '';
// 替换路径变量
$sourceDir = $this->replacePathVariables($sourceDir);
$sourceDir = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $sourceDir);
// 等待构建目录创建
for ($i = 0; $i < $maxRetries; $i++) {
if (file_exists($sourceDir)) {
// 检查目录是否包含文件(非空)
$files = glob($sourceDir . DIRECTORY_SEPARATOR . '*');
if (!empty($files)) {
// 额外等待1000ms确保文件完全写入
usleep(1000000);
return true;
}
}
usleep($retryInterval);
}
error_log("等待构建完成超时: " . $sourceDir);
return false;
}
/**
* 检查并等待构建结果
*
* @param string $projectType
* @param string $sessionUuid
*
* @return \Generator
* @throws \Exception
*/
private function checkAndWaitForBuildResult(string $projectType, string $sessionUuid): \Generator
{
yield Sse::progress("检查{$projectType}构建结果...", 58, [], $sessionUuid);
// 等待构建完成
if (!$this->waitForBuildCompletion($projectType)) {
yield Sse::progress("{$projectType} 构建超时或失败", 59, [], $sessionUuid);
return false;
}
// 最终检查构建目录
if ($this->checkBuildDirectoryExists($projectType)) {
yield Sse::progress("{$projectType} 构建成功", 59, [], $sessionUuid);
return true;
} else {
yield Sse::progress("{$projectType} 构建失败或目录为空", 59, [], $sessionUuid);
return false;
}
}
/**
* 智能处理前端项目
*
* @param string $sessionUuid
*
* @return \Generator
* @throws \Exception|\Throwable
*/
private function processFrontendProjects(string $sessionUuid): \Generator
{
$frontendProjects = ['admin', 'web', 'uni-app'];
$availableProjects = [];
// 检查可用的前端项目
foreach ($frontendProjects as $project) {
if ($this->checkFrontendProjectExists($project)) {
$availableProjects[] = $project;
yield Sse::progress("检测到 {$project} 前端项目", 36, [], $sessionUuid);
} else {
yield Sse::progress("跳过 {$project} 前端项目(目录不存在)", 36, [], $sessionUuid);
}
}
if (empty($availableProjects)) {
yield Sse::progress('未检测到任何前端项目,跳过前端处理', 37, [], $sessionUuid);
return;
}
// 安装依赖 (60-65%)
yield Sse::progress('安装前端依赖...', 62, [], $sessionUuid);
foreach ($availableProjects as $project) {
yield from $this->installFrontendDependencies($project, $sessionUuid);
}
// 构建前端 (65-70%)
yield Sse::progress('构建前端项目...', 67, [], $sessionUuid);
foreach ($availableProjects as $project) {
yield from $this->buildFrontend($project, $sessionUuid);
}
// 检查构建结果 (70-75%)
yield Sse::progress('检查构建结果...', 72, [], $sessionUuid);
$builtProjects = [];
foreach ($availableProjects as $project) {
if (yield from $this->checkAndWaitForBuildResult($project, $sessionUuid)) {
$builtProjects[] = $project;
}
}
if (empty($builtProjects)) {
yield Sse::progress('所有前端项目构建失败,跳过文件迁移', 75, [], $sessionUuid);
return;
}
// 迁移构建文件 (75-80%)
yield Sse::progress('迁移前端构建文件...', 77, [], $sessionUuid);
yield from $this->migrateFrontendBuildFiles($sessionUuid, $builtProjects);
}
/**
* 安装前端依赖(通用方法)
*
* @param string $projectType
* @param string $sessionUuid
*
* @return \Generator
* @throws \Throwable
*/
private function installFrontendDependencies(string $projectType, string $sessionUuid): \Generator
{
try {
// 获取配置的包管理器
$packageManager = config('terminal.npm_package_manager', 'pnpm');
$command = "install.{$projectType}";
// 使用异步执行方法提供实时输出
$progressStep = $projectType === 'admin' ? 42 : ($projectType === 'web' ? 44 : 46);
yield from $this->executeCommandWithRealTimeOutput($command, "{$projectType}前端依赖安装", $sessionUuid, $progressStep);
} catch (Exception $e) {
throw new Exception("{$projectType}前端依赖安装失败: " . $e->getMessage());
}
}
/**
* 构建前端-异步执行方法
*
* @param string $projectType
* @param string $sessionUuid
*
* @return \Generator
* @throws \Throwable
*/
private function buildFrontend(string $projectType, string $sessionUuid): \Generator
{
try {
// 获取配置的包管理器
$packageManager = config('terminal.npm_package_manager', 'pnpm');
$command = "build.{$projectType}.{$packageManager}";
// 使用异步执行方法提供实时输出
$progressStep = $projectType === 'admin' ? 52 : ($projectType === 'web' ? 54 : 56);
yield from $this->executeCommandWithRealTimeOutput($command, "{$projectType}前端构建", $sessionUuid, $progressStep);
} catch (Exception $e) {
throw new Exception("{$projectType}前端构建失败: " . $e->getMessage());
}
}
/**
* 迁移前端构建文件-仅迁移存在的项目
*
* @param string $sessionUuid
* @param array $availableProjects
*
* @return \Generator
* @throws \Exception
*/
private function migrateFrontendBuildFiles(string $sessionUuid, array $availableProjects): \Generator
{
try {
$progressStep = 61;
foreach ($availableProjects as $project) {
yield Sse::progress("迁移{$project}前端构建文件...", $progressStep, [], $sessionUuid);
$this->migrateFrontendProgram($project);
$progressStep++;
}
yield Sse::progress('前端构建文件迁移完成', $progressStep, [], $sessionUuid);
} catch (Exception $e) {
throw new Exception('前端构建文件迁移失败: ' . $e->getMessage());
}
}
/**
* 迁移前端构建文件
*
* @param string $frontendType 前端类型(admin/web/uni-app)
* @return bool
*/
private function migrateFrontendProgram(string $frontendType): bool
{
$config = config('terminal.frontend_programs', []);
if (!isset($config[$frontendType]) || !$config[$frontendType]['enabled']) {
return false;
}
// 处理uni-app等多平台程序
$programConfig = $config[$frontendType];
$sourceDir = $programConfig['source_dir'] ?? '';
$targetDir = $programConfig['target_dir'] ?? '';
$copyMappings = $programConfig['copy_mappings'] ?? [];
$cleanTarget = $programConfig['clean_target'] ?? true;
$preserveFiles = $programConfig['preserve_files'] ?? [];
$copyOptions = $programConfig['copy_options'] ?? [];
// 替换路径变量
$sourceDir = $this->replacePathVariables($sourceDir);
$targetDir = $this->replacePathVariables($targetDir);
if (empty($sourceDir) || empty($targetDir)) {
return false;
}
try {
// 复制构建文件
return $this->copyBuildFiles($sourceDir, $targetDir, $copyMappings, $cleanTarget, $preserveFiles, $copyOptions);
} catch (\Exception $e) {
return false;
}
}
/**
* 替换路径中的变量
*
* @param string $path 路径
*
* @return string
*/
private function replacePathVariables(string $path): string
{
// 后端根目录(server目录)
$backendRoot = base_path();
// 项目根目录(包含server、frontend等目录的父目录)
$projectRoot = dirname($backendRoot);
// 前端根目录(frontend目录)
$frontendRoot = $projectRoot . DIRECTORY_SEPARATOR . 'frontend';
$variables = [
'{backend_root}' => $backendRoot,
'{project_root}' => $projectRoot,
'{frontend_root}' => $frontendRoot,
];
foreach ($variables as $variable => $value) {
$path = str_replace($variable, $value, $path);
}
return $path;
}
/**
* 复制构建文件
*
* @param string $sourceDir 源目录
* @param string $targetDir 目标目录
* @param array $copyMappings 复制映射
* @param bool $cleanTarget 是否清理目标目录
* @param array $preserveFiles 保留的文件
* @param array $copyOptions 复制选项
*
* @return bool
*/
private function copyBuildFiles(string $sourceDir, string $targetDir, array $copyMappings = [], bool $cleanTarget = true, array $preserveFiles = [], array $copyOptions = []): bool
{
if (!is_dir($sourceDir)) {
return false;
}
// 确保目标目录存在
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
// 清理目标目录(如果需要)
if ($cleanTarget && is_dir($targetDir)) {
$this->cleanDirectory($targetDir, $preserveFiles);
}
// 如果没有配置复制映射,默认复制所有文件
if (empty($copyMappings)) {
$copyMappings = ['*' => '.'];
}
// 执行复制
foreach ($copyMappings as $source => $target) {
$sourcePath = $sourceDir . DIRECTORY_SEPARATOR . $source;
$targetPath = $targetDir . DIRECTORY_SEPARATOR . $target;
// 确保目标路径存在
if (!is_dir($targetPath)) {
mkdir($targetPath, 0755, true);
}
// 处理通配符复制
if (str_contains($source, '*')) {
// 转换路径分隔符为正斜杠,确保 glob 函数在 Windows 上正常工作
$globPath = str_replace(DIRECTORY_SEPARATOR, '/', $sourcePath);
$files = glob($globPath);
if (!empty($files)) {
foreach ($files as $file) {
$destFile = $targetPath . DIRECTORY_SEPARATOR . basename($file);
if (is_dir($file)) {
// 递归复制目录
$this->copyDirectory($file, $destFile);
} else {
// 复制文件
copy($file, $destFile);
}
}
}
} elseif (is_dir($sourcePath)) {
// 复制目录
$this->copyDirectory($sourcePath, $targetPath);
} else {
// 复制单个文件
if (file_exists($sourcePath)) {
copy($sourcePath, $targetPath);
}
}
}
return true;
}
/**
* 复制目录
*
* @param string $source 源目录
* @param string $target 目标目录
*
* @return bool
*/
private function copyDirectory(string $source, string $target): bool
{
if (!is_dir($source)) {
return false;
}
// 创建目标目录
if (!is_dir($target)) {
mkdir($target, 0755, true);
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
$sourcePath = $item->getPathname();
$targetPath = $target . DIRECTORY_SEPARATOR . $iterator->getSubPathName();
if ($item->isDir()) {
if (!is_dir($targetPath)) {
mkdir($targetPath, 0755, true);
}
} else {
copy($sourcePath, $targetPath);
}
}
return true;
}
/**
* 清理目录
*
* @param string $directory 目录路径
* @param array $preserveFiles 保留的文件
*/
private function cleanDirectory(string $directory, array $preserveFiles = []): void
{
if (!is_dir($directory)) {
return;
}
$files = scandir($directory);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $directory . DIRECTORY_SEPARATOR . $file;
if (in_array($file, $preserveFiles)) {
continue;
}
if (is_dir($path)) {
$this->deleteDirectory($path);
} else {
unlink($path);
}
}
}
/**
* 删除目录
*
* @param string $directory 目录路径
*/
private function deleteDirectory(string $directory): void
{
if (!is_dir($directory)) {
return;
}
$files = scandir($directory);
foreach ($files as $file) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $directory . DIRECTORY_SEPARATOR . $file;
if (is_dir($path)) {
$this->deleteDirectory($path);
} else {
unlink($path);
}
}
rmdir($directory);
}
/**
* 异步执行命令并提供实时输出
*
* @param string $commandKey 命令键值
* @param string $description 命令描述
* @param string $sessionUuid 会话UUID
* @param int $progressStep 进度步骤
*
* @return \Generator
* @throws \Throwable
*/
private function executeCommandWithRealTimeOutput(string $commandKey, string $description, string $sessionUuid, int $progressStep): \Generator
{
// 检查命令是否存在
$command = \app\service\core\terminal\Terminal::getCommand($commandKey);
if (!$command) {
throw new Exception('错误:命令不允许执行或不存在: ' . $commandKey);
}
// 检查服务是否启用
if (!config('terminal.enabled', false)) {
throw new Exception('Terminal服务未启用');
}
// 开始执行
yield Sse::progress("开始{$description}...", $progressStep - 1, [], $sessionUuid);
// 显示命令描述(如果有)
if (!empty($command['description'])) {
yield Sse::progress('> ' . $command['description'], $progressStep - 1, [], $sessionUuid);
}
// 替换命令变量
$terminal = Terminal::create();
$finalCommand = $terminal->replaceCommandVariables($command['command'], $command['variables'] ?? []);
// 替换 cwd 中的变量
$finalCwd = $terminal->replaceCommandVariables($command['cwd'], $command['variables'] ?? []);
yield Sse::progress('> ' . $finalCommand, $progressStep - 1, [], $sessionUuid);
// 执行命令
if (!$terminal->executeCommand($finalCommand, $finalCwd)) {
throw new Exception('执行失败');
}
// 处理实时输出
$config = config('terminal.execution', []);
$pollInterval = $config['poll_interval'] ?? 500000;
// 获取输出文件路径
$outputFile = $terminal->getOutputFile();
$outputContent = file_get_contents($outputFile);
// 获取进程状态
while ($terminal->getProcStatus()) {
$contents = file_get_contents($outputFile);
if ($contents !== $outputContent) {
$newOutput = substr($contents, strlen($outputContent));
// 发送实时输出,无论是否包含换行符
yield Sse::progress($newOutput, $progressStep - 1, [], $sessionUuid);
$outputContent = $contents;
}
usleep($pollInterval);
}
// 检查是否有剩余输出
$contents = file_get_contents($outputFile);
if ($contents !== $outputContent) {
$newOutput = substr($contents, strlen($outputContent));
yield Sse::progress($newOutput, $progressStep - 1, [], $sessionUuid);
}
// 获取执行结果
$exitCode = $terminal->getProcessExitCode();
if ($exitCode === 0) {
yield Sse::progress("{$description}完成", $progressStep, [], $sessionUuid);
} else {
throw new Exception("{$description}失败,退出代码: " . $exitCode);
}
// 清理资源
$terminal->cleanupResources();
}
/**
* 提取带前缀的数据
*
* @param array $data
* @param string $prefix
*
* @return array
*/
private function extractPrefixedData(array $data, string $prefix): array
{
$result = [];
foreach ($data as $key => $value) {
if (str_starts_with($key, $prefix)) {
$result[substr($key, strlen($prefix))] = $value;
}
}
return $result;
}
/**
* 检查系统要求
*
* @return array
*/
private function checkSystemRequirements(): array
{
$system_variables = [];
// PHP版本检查
$min_php_version = '8.2.0';
$current_php_version = PHP_VERSION;
$php_version_ok = version_compare($current_php_version, $min_php_version) >= 0;
$system_variables[] = [
"name" => "PHP版本",
"current" => $current_php_version,
"need" => ">= " . $min_php_version,
"status" => $php_version_ok,
"status_text" => $php_version_ok ? '符合' : '不符合',
'message' => $php_version_ok ?
"PHP版本符合要求 ({$current_php_version})" :
"PHP版本过低,当前版本 {$current_php_version},需要 {$min_php_version} 或更高版本",
"is_required" => true,
];
// 内存限制检查
$memory_limit = ini_get('memory_limit');
$min_memory = '128M';
$memory_ok = $this->convertMemoryToBytes($memory_limit) >= $this->convertMemoryToBytes($min_memory);
$system_variables[] = [
"name" => "内存限制",
"current" => $memory_limit,
"need" => ">= " . $min_memory,
"status" => $memory_ok,
"status_text" => $memory_ok ? '符合' : '建议提升',
'message' => $memory_ok ?
"内存限制符合要求 ({$memory_limit})" :
"建议将内存限制提升到 {$min_memory} 或更高,当前为 {$memory_limit}",
"is_required" => false,
];
// 最大执行时间检查
$max_execution_time = ini_get('max_execution_time');
$min_execution_time = 30;
$time_ok = intval($max_execution_time) >= $min_execution_time || intval($max_execution_time) === 0;
$system_variables[] = [
"name" => "最大执行时间",
"current" => $max_execution_time . '秒',
"need" => ">= " . $min_execution_time . '秒',
"status" => $time_ok,
"status_text" => $time_ok ? '符合' : '建议提升',
'message' => $time_ok ?
"执行时间符合要求 ({$max_execution_time}秒)" :
"建议将最大执行时间提升到 {$min_execution_time} 秒或更高,当前为 {$max_execution_time} 秒",
"is_required" => false,
];
// 文件上传大小限制检查
$upload_max_filesize = ini_get('upload_max_filesize');
$post_max_size = ini_get('post_max_size');
$min_upload_size = '2M';
$upload_ok = $this->convertMemoryToBytes($upload_max_filesize) >= $this->convertMemoryToBytes($min_upload_size);
$system_variables[] = [
"name" => "文件上传限制",
"current" => $upload_max_filesize,
"need" => ">= " . $min_upload_size,
"status" => $upload_ok,
"status_text" => $upload_ok ? '符合' : '建议提升',
'message' => $upload_ok ?
"文件上传限制符合要求 ({$upload_max_filesize})" :
"建议将文件上传限制提升到 {$min_upload_size} 或更高,当前为 {$upload_max_filesize}",
"is_required" => false,
];
// 扩展检查
$extensions = [
'pdo' => ['name' => 'PDO扩展', 'required' => true, 'description' => '数据库连接必需'],
'pdo_mysql' => ['name' => 'PDO MySQL扩展', 'required' => true, 'description' => 'MySQL数据库连接必需'],
'curl' => ['name' => 'CURL扩展', 'required' => true, 'description' => 'HTTP请求处理必需'],
'openssl' => ['name' => 'OpenSSL扩展', 'required' => true, 'description' => '加密和安全通信必需'],
'gd' => ['name' => 'GD库', 'required' => true, 'description' => '图像处理必需'],
'fileinfo' => ['name' => 'Fileinfo扩展', 'required' => true, 'description' => '文件类型检测必需'],
'json' => ['name' => 'JSON扩展', 'required' => true, 'description' => 'JSON数据处理必需'],
'mbstring' => ['name' => 'MBString扩展', 'required' => true, 'description' => '多字节字符串处理必需'],
'sodium' => ['name' => 'Sodium扩展', 'required' => false, 'description' => '现代加密算法(可选)'],
'imagick' => ['name' => 'Imagick扩展', 'required' => false, 'description' => '高级图像处理(可选)'],
'zip' => ['name' => 'Zip扩展', 'required' => false, 'description' => '压缩文件处理(可选)'],
];
foreach ($extensions as $ext => $info) {
$status = extension_loaded($ext);
$system_variables[] = [
"name" => $info['name'],
"current" => $status ? '已安装' : '未安装',
"need" => $info['required'] ? "必需" : "可选",
"status" => $status,
"status_text" => $status ? '已安装' : ($info['required'] ? '缺失' : '未安装'),
"message" => $status ?
"{$info['name']}已安装" :
($info['required'] ?
"{$info['name']}未安装 - {$info['description']}" :
"{$info['name']}未安装 - {$info['description']}(可选)"),
"is_required" => $info['required'],
];
}
return $system_variables;
}
/**
* 检查目录权限
*
* @return array
*/
private function checkDirectoryPermissions(): array
{
$root_path = base_path();
$dirs_list = [
// 必需读写目录
[
"path" => $root_path,
"path_name" => "网站根目录",
"name" => "root_dir",
"required" => true,
"permission_type" => "read_write", // 新增:明确权限类型
"description" => "系统根目录,需要读写权限",
],
// 必需写权限文件
[
"path" => $root_path . "/.env",
"path_name" => ".env文件",
"name" => "env_file",
"required" => true,
"permission_type" => "write_only", // 新增:明确权限类型
"description" => "环境配置文件,需要写入权限",
],
// 必需读写目录
[
"path" => $root_path . '/runtime',
"path_name" => "runtime目录",
"name" => "runtime_dir",
"required" => true,
"permission_type" => "read_write",
"description" => "运行时目录,需要读写权限",
],
[
"path" => $root_path . '/public/upload',
"path_name" => "upload目录",
"name" => "upload_dir",
"required" => true,
"permission_type" => "read_write",
"description" => "文件上传目录,需要读写权限",
],
[
"path" => $root_path . '/storage',
"path_name" => "storage目录",
"name" => "storage_dir",
"required" => true,
"permission_type" => "read_write",
"description" => "存储目录,需要读写权限",
],
[
"path" => $root_path . '/public/install',
"path_name" => "install目录",
"name" => "install_dir",
"required" => true,
"permission_type" => "read_write",
"description" => "安装目录,需要读写权限",
],
// 可选读权限目录
[
"path" => $root_path . '/vendor',
"path_name" => "vendor目录",
"name" => "vendor_dir",
"required" => false,
"permission_type" => "read_only", // 新增:明确权限类型
"description" => "依赖包目录,需要读取权限",
],
[
"path" => $root_path . '/config',
"path_name" => "config目录",
"name" => "config_dir",
"required" => false,
"permission_type" => "read_only",
"description" => "配置目录,需要读取权限",
],
];
foreach ($dirs_list as $k => $v) {
// 如果是文件,检查文件权限
if (str_contains($v["path"], '.') && !is_dir($v["path"])) {
// 文件不存在,尝试创建
if (!file_exists($v["path"])) {
$dir = dirname($v["path"]);
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
@touch($v["path"]);
}
} else {
// 目录不存在,尝试创建
if (!file_exists($v["path"])) {
@mkdir($v["path"], 0755, true);
}
}
$is_readable = is_readable($v["path"]);
$is_writable = is_writable($v["path"]);
$exists = file_exists($v["path"]);
$dirs_list[$k]["exists"] = $exists;
$dirs_list[$k]["is_readable"] = $is_readable;
$dirs_list[$k]["is_writable"] = $is_writable;
// 根据权限类型判断状态
switch ($v["permission_type"]) {
case "read_write":
$dirs_list[$k]["status"] = $exists && $is_readable && $is_writable;
$dirs_list[$k]["status_text"] = $dirs_list[$k]["status"] ? '正常' : '异常';
$dirs_list[$k]["message"] = $dirs_list[$k]["status"] ?
"{$v['path_name']}权限正常" :
(!$exists ? "{$v['path_name']}不存在" :
(!$is_readable ? "{$v['path_name']}不可读" : "{$v['path_name']}不可写"));
break;
case "write_only":
$dirs_list[$k]["status"] = $exists && $is_writable;
$dirs_list[$k]["status_text"] = $dirs_list[$k]["status"] ? '正常' : '异常';
$dirs_list[$k]["message"] = $dirs_list[$k]["status"] ?
"{$v['path_name']}权限正常" :
(!$exists ? "{$v['path_name']}不存在" : "{$v['path_name']}不可写");
break;
case "read_only":
$dirs_list[$k]["status"] = $exists && $is_readable;
$dirs_list[$k]["status_text"] = $dirs_list[$k]["status"] ? '正常' : '警告';
$dirs_list[$k]["message"] = $dirs_list[$k]["status"] ?
"{$v['path_name']}权限正常" :
(!$exists ? "{$v['path_name']}不存在" : "{$v['path_name']}不可读");
break;
}
}
return $dirs_list;
}
/**
* 检查所有要求
*
* @param array $system_variables
* @param array $dirs_list
*
* @return bool
*/
private function checkAllRequirements(array $system_variables, array $dirs_list): bool
{
// 检查系统变量
foreach ($system_variables as $variable) {
if ($variable['is_required'] && $variable['status'] !== true) {
return false;
}
}
// 检查目录权限
foreach ($dirs_list as $dir) {
if ($dir['required'] && !$dir['status']) {
return false;
}
}
return true;
}
/**
* 初始化管理员(不安装数据库时使用)
*
* 使用 PDO 直接创建管理员,不依赖框架 Db
*
* @throws \Exception
*/
private function initializeAdmin(array $dbParams, array $adminParams, string $sessionUuid): \Generator
{
yield Sse::progress('📝 创建管理员账户', 50, [], $sessionUuid);
// 使用 Trait 创建管理员
$this->initDbConfig([
'host' => $dbParams['host'],
'port' => $dbParams['port'],
'database' => $dbParams['database'],
'username' => $dbParams['username'],
'password' => $dbParams['password'],
'prefix' => $dbParams['prefix'] ?? 'ma_',
]);
$this->createAdmin($adminParams);
yield Sse::progress('✅ 管理员账户创建完成', 55, [], $sessionUuid);
}
/**
* 安装数据库表(直接通过 PDO 执行迁移)
*
* 每次安装时:
* 1. 创建数据库
* 2. 通过 PDO 执行迁移创建表
* 3. 运行种子数据(通过 Trait)
*
* @param array $dbParams
* @param array $adminParams
* @param string $sessionUuid 会话UUID
*
* @return \Generator
* @throws \Exception
*/
private function installDatabaseTables(array $dbParams, array $adminParams, string $sessionUuid): \Generator
{
try {
// 1. 连接数据库并执行安装
$pdo = $this->getDataImporter()->getPdo(
$dbParams['host'],
$dbParams['username'],
$dbParams['password'],
$dbParams['port']
);
$database = $dbParams['database'];
// 2. 检查数据库是否存在
$stmt = $pdo->query("SHOW DATABASES LIKE '{$database}'");
$dbExists = $stmt->rowCount() > 0;
if ($dbExists) {
// 删除现有数据库
yield Sse::progress('> 删除旧数据库', 31, [], $sessionUuid);
$pdo->exec("DROP DATABASE `{$database}`");
}
// 3. 创建数据库
yield Sse::progress('> 创建数据库', 32, [], $sessionUuid);
$pdo->exec("CREATE DATABASE `{$database}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$pdo->exec("USE `{$database}`");
// 4. 执行迁移创建表 (30-40%)
yield Sse::progress('> 执行数据库结构安装', 33, [], $sessionUuid);
yield from $this->runMigrations($dbParams, $sessionUuid);
yield Sse::progress('> 数据库结构安装完成', 40, [], $sessionUuid);
// 5. 运行种子数据 (40-50%)
yield from $this->runAllSeeders($dbParams, $adminParams, $sessionUuid);
} catch (\Exception $e) {
throw new Exception('数据库安装失败: ' . $e->getMessage());
}
}
/**
* 执行迁移创建表
*
* 使用框架 Db 执行迁移
*
* @param array $dbParams 数据库参数
* @param string $sessionUuid
* @return \Generator
*/
private function runMigrations(array $dbParams, string $sessionUuid): \Generator
{
// 获取迁移文件目录
$migrationDir = base_path('resource/database/migrations');
if (!is_dir($migrationDir)) {
yield Sse::progress('⚠️ 迁移目录不存在', 33, [], $sessionUuid);
return;
}
$files = glob($migrationDir . '/*.php');
if (empty($files)) {
yield Sse::progress('⚠️ 没有找到迁移文件', 33, [], $sessionUuid);
return;
}
// 排序文件
sort($files);
// 创建 PDO 连接
try {
$pdo = new \PDO(
"mysql:host={$dbParams['host']};port={$dbParams['port']};dbname={$dbParams['database']};charset=utf8mb4",
$dbParams['username'],
$dbParams['password'],
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
);
} catch (\Exception $e) {
yield Sse::progress('⚠️ PDO 连接失败: ' . $e->getMessage(), 33, [], $sessionUuid);
return;
}
$total = count($files);
$current = 0;
foreach ($files as $file) {
$basename = basename($file, '.php');
$current++;
// 迁移进度 30-40%
$progress = 30 + ($current / $total) * 10;
try {
// 加载迁移文件
$migrationClass = require $file;
// 捕获 echo 输出
ob_start();
// 执行迁移的 up 方法
if (method_exists($migrationClass, 'up')) {
// 获取 Schema Builder
$schema = $this->createSchemaBuilder($pdo, $dbParams['prefix'] ?? 'ma_', $dbParams['database'] ?? 'madong');
$migrationClass->up($schema);
}
// 获取捕获的输出
$output = ob_get_clean();
// 通过 SSE 返回迁移输出
if (!empty($output)) {
yield Sse::progress($output, (int)$progress, [], $sessionUuid);
}
yield Sse::progress("> 迁移: {$basename}", (int)$progress, [], $sessionUuid);
} catch (\Exception $e) {
ob_end_clean(); // 清理捕获的输出
yield Sse::progress("⚠️ 迁移失败 {$basename}: " . $e->getMessage(), (int)$progress, [], $sessionUuid);
// 记录详细错误日志
error_log("Migration error in {$basename}: " . $e->getMessage() . "\n" . $e->getTraceAsString());
}
}
yield Sse::progress('✅ 迁移执行完成', 40, [], $sessionUuid);
}
/**
* 创建 Schema Builder 实例
*/
private function createSchemaBuilder(\PDO $pdo, string $prefix, string $database): \Illuminate\Database\Schema\Builder
{
// 创建连接
$capsule = new \Illuminate\Database\Capsule\Manager();
$capsule->addConnection([
'driver' => 'mysql',
'host' => '127.0.0.1',
'port' => 3306,
'database' => $database,
'username' => '',
'password' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => $prefix,
'strict' => false,
'engine' => 'InnoDB',
]);
// 获取连接并注入 PDO
$connection = $capsule->getConnection('default');
$connection->setPdo($pdo);
$capsule->setAsGlobal();
$capsule->bootEloquent();
return $capsule->schema();
}
/**
* 配置数据库连接用于迁移(使用 .env 文件)
*
* 保存数据库和 Redis 配置到 .env 文件
* 用于测试成功后,为后续安装步骤准备配置
*
* @param array $dbParams 数据库参数
* @param array $redisParams Redis 参数(可选)
*/
public function saveDatabaseConfig(array $dbParams, array $redisParams = []): void
{
$envFile = base_path('.env');
$envTemplateFile = base_path('.example.env');
// 如果 .env 不存在或为空,从模板创建
$envContent = '';
if (file_exists($envFile) && filesize($envFile) > 0) {
$envContent = file_get_contents($envFile);
} elseif (file_exists($envTemplateFile)) {
$envContent = file_get_contents($envTemplateFile);
} else {
// 创建一个基本的 .env 内容
$envContent = $this->getDefaultEnvContent();
}
// 更新 .env 中的数据库配置
$patterns = [
'/^DB_HOST=.*/m' => 'DB_HOST=' . $dbParams['host'],
'/^DB_PORT=.*/m' => 'DB_PORT=' . $dbParams['port'],
'/^DB_DATABASE=.*/m' => 'DB_DATABASE=' . $dbParams['database'],
'/^DB_USERNAME=.*/m' => 'DB_USERNAME=' . $dbParams['username'],
'/^DB_PASSWORD=.*/m' => 'DB_PASSWORD=' . $dbParams['password'],
'/^DB_PREFIX=.*/m' => 'DB_PREFIX=' . ($dbParams['prefix'] ?? 'ma_'),
];
foreach ($patterns as $pattern => $replacement) {
$envContent = preg_replace($pattern, $replacement, $envContent);
}
// 如果传入了 Redis 参数,也更新 Redis 配置
if (!empty($redisParams)) {
$envContent = $this->replaceEnvRedisParams($envContent, $redisParams);
}
file_put_contents($envFile, $envContent);
}
/**
* 获取默认的 .env 内容
*/
private function getDefaultEnvContent(): string
{
return <<<ENV
APP_DEBUG = false
DB_CONNECTION = mysql
DB_HOST = 127.0.0.1
DB_PORT = 3306
DB_DATABASE = md_admin
DB_USERNAME = root
DB_PASSWORD =
DB_PREFIX = ma_
REDIS_HOST = 127.0.0.1
REDIS_PORT = 6379
REDIS_PASSWORD =
REDIS_PREFIX = md:
CACHE_DRIVER = redis
SESSION_DRIVER = redis
LOG_CHANNEL = daily
ENV;
}
/**
* 运行所有种子数据
*
* 使用 Trait 处理菜单、字典、配置、管理员数据
*
* @param array $dbParams 数据库参数
* @param array $adminParams 管理员参数
* @param string $sessionUuid 会话UUID
* @return \Generator
*/
private function runAllSeeders(array $dbParams, array $adminParams, string $sessionUuid): \Generator
{
// 初始化数据库配置
$this->initDbConfig([
'host' => $dbParams['host'],
'port' => $dbParams['port'],
'database' => $dbParams['database'],
'username' => $dbParams['username'],
'password' => $dbParams['password'],
'prefix' => $dbParams['prefix'] ?? 'ma_',
]);
// 菜单 40%
yield Sse::progress("📝 导入菜单数据", 42, [], $sessionUuid);
$this->runMenu();
// 字典 45%
yield Sse::progress("📝 导入字典数据", 45, [], $sessionUuid);
$this->runDict();
// 配置 48%
yield Sse::progress("📝 导入配置数据", 48, [], $sessionUuid);
$this->runConfig();
// 管理员 50%
yield Sse::progress("📝 创建管理员账户", 50, [], $sessionUuid);
$this->createAdmin($adminParams);
yield Sse::progress("✅ 所有种子数据执行完成", 52, [], $sessionUuid);
}
/**
* 获取安装协议
*
* @return string
*/
private function getLicenseAgreement(): string
{
$templatePath = base_path('resource/license.html');
return file_get_contents($templatePath);
}
/**
* 将内存大小字符串转换为字节数
*
* @param string $size
*
* @return int
*/
private function convertMemoryToBytes(string $size): int
{
$unit = preg_replace('/[^bkmgtpezy]/i', '', $size);
$size = preg_replace('/[^0-9\.]/', '', $size);
if ($unit) {
$size = (int)$size * pow(1024, stripos('bkmgtpezy', $unit[0]));
}
return (int)$size;
}
/**
* 复制数据库模板并创建.env文件
*
* @param array $dbParams 数据库参数
*
* @return void
* @throws \Exception
*/
private function copyDatabaseTemplateAndCreateEnv(array $dbParams, array $redisParams = []): void
{
try {
$databaseTemplatePath = base_path('resource/data/template/database.php');
$databaseConfigPath = base_path('config/database.php');
if (file_exists($databaseTemplatePath)) {
if (!copy($databaseTemplatePath, $databaseConfigPath)) {
throw new Exception('复制数据库模板文件失败');
}
error_log("数据库模板文件已复制到: " . $databaseConfigPath);
} else {
throw new Exception('数据库模板文件不存在: ' . $databaseTemplatePath);
}
// 2. 创建.env文件
$envTemplatePath = base_path('.example.env');
$envFilePath = base_path('.env');
if (!file_exists($envTemplatePath)) {
throw new Exception('.example.env模板文件不存在: ' . $envTemplatePath);
}
// 读取模板内容
$envContent = file_get_contents($envTemplatePath);
if ($envContent === false) {
throw new Exception('读取.env模板文件失败');
}
// 替换数据库配置参数
$envContent = $this->replaceEnvDatabaseParams($envContent, $dbParams);
// 替换 Redis 配置参数
$envContent = $this->replaceEnvRedisParams($envContent, $redisParams);
// 写入.env文件
if (file_put_contents($envFilePath, $envContent) === false) {
throw new Exception('创建.env文件失败');
}
error_log(".env文件已创建: " . $envFilePath);
} catch (Exception $e) {
error_log("配置环境文件失败: " . $e->getMessage());
throw $e;
}
}
/**
* 替换.env文件中的数据库参数
*
* @param string $envContent .env文件内容
* @param array $dbParams 数据库参数
*
* @return string 替换后的内容
*/
private function replaceEnvDatabaseParams(string $envContent, array $dbParams): string
{
// 定义需要替换的参数映射
$replacements = [
'DB_HOST' => $dbParams['host'] ?? '127.0.0.1',
'DB_PORT' => $dbParams['port'] ?? '3306',
'DB_DATABASE' => $dbParams['database'] ?? 'md_admin',
'DB_USERNAME' => $dbParams['username'] ?? 'root',
'DB_PASSWORD' => $dbParams['password'] ?? 'root',
'DB_PREFIX' => $dbParams['prefix'] ?? 'ma_',
];
foreach ($replacements as $key => $value) {
$pattern = '/^' . preg_quote($key) . '=.*$/m';
$replacement = $key . '=' . $value;
if (preg_match($pattern, $envContent)) {
$envContent = preg_replace($pattern, $replacement, $envContent);
} else {
$envContent .= "\n" . $replacement;
}
}
return $envContent;
}
/**
* 替换.env文件中的 Redis 参数
*
* @param string $envContent .env文件内容
* @param array $redisParams Redis 参数(host, port, password)
*
* @return string 替换后的内容
*/
private function replaceEnvRedisParams(string $envContent, array $redisParams): string
{
$host = $redisParams['host'] ?? '127.0.0.1';
$port = $redisParams['port'] ?? '6379';
$password = $redisParams['password'] ?? '';
// 主 Redis 配置
$replacements = [
'REDIS_HOST' => $host,
'REDIS_PORT' => $port,
'REDIS_PASSWORD' => $password,
];
// 队列 Redis 配置(复用连接信息,使用 redis:// DSN 格式)
$queueHost = ($password ? "redis://:{$password}@" : 'redis://') . $host . ':' . $port;
$queueReplacements = [
'QUEUE_REDIS_HOST' => $queueHost,
'QUEUE_REDIS_PORT' => $port,
'QUEUE_REDIS_PASSWORD' => $password,
];
// 缓存 Redis 配置(复用连接信息)
$cacheReplacements = [
'CACHE_CUSTOM_REDIS_HOST' => $host,
'CACHE_CUSTOM_REDIS_PORT' => $port,
'CACHE_CUSTOM_REDIS_PASSWORD' => $password,
];
// 合并所有替换项
$replacements = array_merge($replacements, $queueReplacements, $cacheReplacements);
foreach ($replacements as $key => $value) {
$pattern = '/^' . preg_quote($key) . '=.*$/m';
$replacement = $key . '=' . $value;
if (preg_match($pattern, $envContent)) {
$envContent = preg_replace($pattern, $replacement, $envContent);
} else {
$envContent .= "\n" . $replacement;
}
}
return $envContent;
}
/**
* 创建安装锁
*
* @return void
*/
private function createInstallLock(): void
{
file_put_contents($this->lock_file, date('Y-m-d H:i:s'));
}
/**
* 分割SQL语句
*
* @param string $sql
*
* @return array
*/
private function splitSqlStatements(string $sql): array
{
$statements = [];
$currentStatement = '';
$delimiter = ';';
$delimiterLength = strlen($delimiter);
$inString = false;
$stringChar = '';
$escaped = false;
$lines = explode("\n", $sql);
foreach ($lines as $line) {
$trimmedLine = trim($line);
// 处理 DELIMITER 命令
if (stripos($trimmedLine, 'DELIMITER') === 0) {
// 保存之前的语句
if (!empty(trim($currentStatement))) {
$statements[] = trim($currentStatement);
$currentStatement = '';
}
// 设置新的分隔符
$parts = preg_split('/\s+/', $trimmedLine, 2);
if (isset($parts[1])) {
$delimiter = trim($parts[1]);
$delimiterLength = strlen($delimiter);
}
continue;
}
$currentStatement .= $line . "\n";
// 分析字符,处理字符串中的特殊字符
for ($i = 0; $i < strlen($line); $i++) {
$char = $line[$i];
if ($escaped) {
$escaped = false;
continue;
}
if ($char === '\\') {
$escaped = true;
continue;
}
if (!$inString && ($char === '"' || $char === "'" || $char === '`')) {
$inString = true;
$stringChar = $char;
continue;
}
if ($inString && $char === $stringChar) {
$inString = false;
continue;
}
if (!$inString) {
// 检查是否到达分隔符
if (substr($line, $i, $delimiterLength) === $delimiter) {
$statement = substr($currentStatement, 0, -($delimiterLength - strlen($line) + $i));
if (!empty(trim($statement))) {
$statements[] = trim($statement);
}
$currentStatement = '';
$i += $delimiterLength - 1;
}
}
}
}
// 添加最后一个语句
if (!empty(trim($currentStatement))) {
$statements[] = trim($currentStatement);
}
return $statements;
}
}