| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- <?php
- declare(strict_types=1);
- namespace SixShop\System\Service;
- use RecursiveDirectoryIterator;
- use RecursiveIteratorIterator;
- use RuntimeException;
- use function SixShop\Core\extension_path;
- use SixShop\Core\Service\CoreService;
- use think\facade\Log;
- /**
- * 前端代码部署服务
- *
- * 负责插件前端代码的自动部署和清理
- */
- class FrontendDeployService
- {
- /**
- * 部署前端代码
- *
- * @param string $extensionId 插件ID
- * @return bool 是否部署成功
- */
- public function deploy(string $extensionId): bool
- {
- try {
- // 1. 检查前端代码是否存在
- if (!$this->hasFrontendCode($extensionId)) {
- Log::info("插件 {$extensionId} 没有前端代码,跳过部署");
- return true; // 跳过,不算失败
- }
- // 2. 获取源目录和目标目录
- $sourcePath = $this->getFrontendSourcePath($extensionId);
- $targetPath = $this->getFrontendTargetPath();
- // 3. 检查目标目录是否已存在
- $pluginTargetPath = $targetPath . '/' . $extensionId;
- if (is_dir($pluginTargetPath)) {
- Log::warning("目标目录已存在,将覆盖:{$pluginTargetPath}");
- $this->removeDirectory($pluginTargetPath);
- }
- // 4. 复制前端代码
- $this->copyDirectory($sourcePath, $targetPath);
- // 5. 记录部署信息
- $this->logDeploy($extensionId, [
- 'source_path' => $sourcePath,
- 'target_path' => $targetPath,
- 'status' => 'success'
- ]);
- Log::info("前端代码部署成功:{$extensionId}");
- return true;
- } catch (\Exception $e) {
- Log::error("前端代码部署失败:{$extensionId},错误:" . $e->getMessage());
- // 回滚:删除已复制的文件
- if (isset($pluginTargetPath) && is_dir($pluginTargetPath)) {
- $this->removeDirectory($pluginTargetPath);
- }
- // 不抛出异常,允许安装继续
- return false;
- }
- }
- /**
- * 移除前端代码
- *
- * @param string $extensionId 插件ID
- * @return bool 是否移除成功
- */
- public function remove(string $extensionId): bool
- {
- try {
- // 兼容性处理:检查插件是否有前端代码
- // 如果插件没有前端代码(旧项目),则不删除 admin 的前端代码
- if (!$this->hasFrontendCode($extensionId)) {
- Log::info("插件 {$extensionId} 没有前端代码,跳过删除(兼容旧项目)");
- return true;
- }
-
- $targetPath = $this->getFrontendTargetPath();
- $pluginTargetPath = $targetPath . '/' . $extensionId;
- if (!is_dir($pluginTargetPath)) {
- Log::info("前端代码不存在,无需删除:{$extensionId}");
- return true;
- }
- // 删除目录
- $this->removeDirectory($pluginTargetPath);
- // 记录删除信息
- $this->logRemove($extensionId);
- Log::info("前端代码删除成功:{$extensionId}");
- return true;
- } catch (\Exception $e) {
- Log::error("前端代码删除失败:{$extensionId},错误:" . $e->getMessage());
- return false;
- }
- }
- /**
- * 检查插件是否有前端代码
- *
- * @param string $extensionId 插件ID
- * @return bool
- */
- private function hasFrontendCode(string $extensionId): bool
- {
- try {
- $frontendPath = $this->getFrontendSourcePath($extensionId);
- } catch (\Exception $e) {
- Log::warning("获取插件路径失败:{$extensionId},错误:" . $e->getMessage());
- return false;
- }
- // 检查目录是否存在
- if (!is_dir($frontendPath)) {
- return false;
- }
- // 检查是否有插件名称的子目录
- $pluginFrontendPath = $frontendPath . '/' . $extensionId;
- if (!is_dir($pluginFrontendPath)) {
- Log::warning("插件 {$extensionId} 前端代码目录结构不正确,应为:frontend/{$extensionId}/");
- return false;
- }
- return true;
- }
- /**
- * 获取插件的实际路径
- *
- * @param string $extensionId 插件ID
- * @return string
- * @throws RuntimeException
- */
- private function getPluginPath(string $extensionId): string
- {
- // 方法 1: 使用 extension_path()
- $path = extension_path($extensionId);
- if (is_dir($path)) {
- return $path;
- }
- // 方法 2: 从 CoreService 获取 Composer 映射
- if (isset(CoreService::$extensionComposerMap[$extensionId])) {
- $composerInfo = CoreService::$extensionComposerMap[$extensionId];
- $vendorPath = base_path('vendor/' . $composerInfo['name']);
- if (is_dir($vendorPath)) {
- return $vendorPath;
- }
- }
- // 方法 3: 手动检测常见路径
- $possiblePaths = [
- base_path('runtime/extension/' . $extensionId),
- base_path('vendor/six-shop/' . $extensionId),
- base_path('vendor/sixdec/' . $extensionId),
- ];
- foreach ($possiblePaths as $path) {
- if (is_dir($path)) {
- return $path;
- }
- }
- throw new RuntimeException("找不到插件路径:{$extensionId}");
- }
- /**
- * 获取前端代码源路径
- *
- * @param string $extensionId 插件ID
- * @return string
- */
- private function getFrontendSourcePath(string $extensionId): string
- {
- $pluginPath = $this->getPluginPath($extensionId);
- return $pluginPath . '/frontend';
- }
- /**
- * 获取前端代码目标路径
- *
- * @return string
- */
- private function getFrontendTargetPath(): string
- {
- // base_path() 返回 backend 目录
- // 需要回到项目根目录
- $backendPath = base_path();
- $projectRoot = dirname($backendPath);
- $realRoot = dirname($projectRoot);
- return $realRoot . '/frontend/admin/src/views';
- }
- /**
- * 递归复制目录
- *
- * @param string $source 源目录
- * @param string $target 目标目录
- * @return void
- */
- private function copyDirectory(string $source, string $target): void
- {
- // 递归复制文件
- $iterator = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS),
- RecursiveIteratorIterator::SELF_FIRST
- );
- foreach ($iterator as $item) {
- // 计算目标路径
- $subPath = $iterator->getSubPathName();
- $targetPath = $target . DIRECTORY_SEPARATOR . $subPath;
- if ($item->isDir()) {
- // 创建目录
- if (!is_dir($targetPath)) {
- mkdir($targetPath, 0755, true);
- }
- } else {
- // 复制文件
- $targetDir = dirname($targetPath);
- if (!is_dir($targetDir)) {
- mkdir($targetDir, 0755, true);
- }
- // 转换 SplFileInfo 对象为字符串路径
- copy($item->getPathname(), $targetPath);
- }
- }
- }
- /**
- * 递归删除目录
- *
- * @param string $path 目录路径
- * @return void
- */
- private function removeDirectory(string $path): void
- {
- if (!is_dir($path)) {
- return;
- }
- $iterator = new RecursiveIteratorIterator(
- new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
- RecursiveIteratorIterator::CHILD_FIRST
- );
- foreach ($iterator as $item) {
- if ($item->isDir()) {
- rmdir($item->getPathname());
- } else {
- unlink($item->getPathname());
- }
- }
- rmdir($path);
- }
- /**
- * 记录部署信息
- *
- * @param string $extensionId 插件ID
- * @param array $info 部署信息
- * @return void
- */
- private function logDeploy(string $extensionId, array $info): void
- {
- $logDir = runtime_path('frontend_deploy');
- if (!is_dir($logDir)) {
- mkdir($logDir, 0755, true);
- }
- $logFile = $logDir . '/deployed.json';
- // 读取现有记录
- $deployed = [];
- if (file_exists($logFile)) {
- $content = file_get_contents($logFile);
- $deployed = json_decode($content, true) ?: [];
- }
- // 添加新记录
- if (!isset($deployed['deployed'])) {
- $deployed['deployed'] = [];
- }
- $deployed['deployed'][] = array_merge([
- 'extension_id' => $extensionId,
- 'deployed_at' => date('Y-m-d H:i:s'),
- ], $info);
- // 写入文件
- file_put_contents(
- $logFile,
- json_encode($deployed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
- );
- }
- /**
- * 记录删除信息
- *
- * @param string $extensionId 插件ID
- * @return void
- */
- private function logRemove(string $extensionId): void
- {
- $logDir = runtime_path('frontend_deploy');
- if (!is_dir($logDir)) {
- mkdir($logDir, 0755, true);
- }
- $logFile = $logDir . '/deployed.json';
- // 读取现有记录
- $deployed = [];
- if (file_exists($logFile)) {
- $content = file_get_contents($logFile);
- $deployed = json_decode($content, true) ?: [];
- }
- // 添加删除记录
- if (!isset($deployed['removed'])) {
- $deployed['removed'] = [];
- }
- $deployed['removed'][] = [
- 'extension_id' => $extensionId,
- 'removed_at' => date('Y-m-d H:i:s'),
- ];
- // 写入文件
- file_put_contents(
- $logFile,
- json_encode($deployed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
- );
- }
- }
|