Browse Source

自动化安装admin 页面,兼容旧逻辑。

mylink 4 tháng trước cách đây
mục cha
commit
142638bd9d
3 tập tin đã thay đổi với 1396 bổ sung0 xóa
  1. 34 0
      src/Extension.php
  2. 352 0
      src/Service/FrontendDeployService.php
  3. 1010 0
      插件前端自动化部署方案.md

+ 34 - 0
src/Extension.php

@@ -9,7 +9,9 @@ use SixShop\System\Cron\SystemCron;
 use SixShop\System\Enum\ExtensionStatusEnum;
 use SixShop\System\Hook\ExtensionStatusHook;
 use SixShop\System\Hook\GatheringCrontabEventHook;
+use SixShop\System\Service\FrontendDeployService;
 use think\db\exception\PDOException;
+use think\facade\Event;
 
 use function SixShop\Core\extension_name_list;
 
@@ -40,6 +42,10 @@ class Extension extends ExtensionAbstract
         if ($this->isBooted) {
             return;
         }
+        
+        // 注册前端部署事件监听
+        $this->registerFrontendDeployListeners();
+        
         foreach (extension_name_list() as $extensionID) {
             /** @var ExtensionAbstract $extension */
             $extension = $this->autoloadService->getExtension($extensionID);
@@ -63,6 +69,34 @@ class Extension extends ExtensionAbstract
         }
         parent::boot();
     }
+    
+    /**
+     * 注册前端部署事件监听器
+     */
+    private function registerFrontendDeployListeners(): void
+    {
+        // 监听安装后事件
+        Event::listen('after_install_extension', function (string $extensionId) {
+            try {
+                $service = app(FrontendDeployService::class);
+                $service->deploy($extensionId);
+            } catch (\Exception $e) {
+                // 记录错误但不影响安装流程
+                \think\facade\Log::error("前端部署失败:{$extensionId},错误:" . $e->getMessage());
+            }
+        });
+        
+        // 监听卸载前事件
+        Event::listen('before_uninstall_extension', function (string $extensionId) {
+            try {
+                $service = app(FrontendDeployService::class);
+                $service->remove($extensionId);
+            } catch (\Exception $e) {
+                // 记录错误但不影响卸载流程
+                \think\facade\Log::error("前端清理失败:{$extensionId},错误:" . $e->getMessage());
+            }
+        });
+    }
 
     public  function available()
     {

+ 352 - 0
src/Service/FrontendDeployService.php

@@ -0,0 +1,352 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\System\Service;
+
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RuntimeException;
+use SixShop\Core\Helper;
+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: 使用 Helper::extension_path()
+        $path = Helper::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)
+        );
+    }
+}

+ 1010 - 0
插件前端自动化部署方案.md

@@ -0,0 +1,1010 @@
+# 插件前端自动化部署方案
+
+## 📋 文档信息
+
+- **文档版本**: v1.0
+- **创建日期**: 2024-11-04
+- **作者**: AI Assistant
+- **项目**: SixShop 电商系统
+- **适用范围**: 所有带前端代码的插件
+
+## 🎯 方案目标
+
+### 当前问题
+
+1. **手动操作繁琐**
+   - 安装插件后需要手动复制前端代码
+   - 需要手动运行 `npm run build` 编译
+   - 卸载插件后需要手动删除前端代码
+
+2. **容易出错**
+   - 忘记复制前端代码导致功能不可用
+   - 忘记删除前端代码导致冗余文件
+   - 手动操作可能导致文件遗漏或错误
+
+3. **开发效率低**
+   - 每次测试都要重复手动操作
+   - 影响开发和测试效率
+
+### 期望目标
+
+1. **完全自动化**
+   - 安装插件时自动部署前端代码
+   - 自动触发编译(可选)
+   - 卸载插件时自动清理前端代码
+
+2. **安全可靠**
+   - 完善的错误处理
+   - 操作日志记录
+   - 支持回滚机制
+
+3. **用户友好**
+   - 实时进度反馈
+   - 清晰的状态提示
+   - 友好的错误信息
+
+## 🏗️ 架构设计
+
+### 用户操作流程
+
+#### 安装插件
+
+```
+用户访问插件管理页面
+http://localhost:3000/plugins/index
+    ↓
+点击插件的"安装"按钮
+    ↓
+前端发送 POST 请求
+POST https://sixshop.ddev.site/admin/system/extension/{plugin-id}/install
+    ↓
+后端处理安装
+    ├─ 1. 检查扩展状态
+    ├─ 2. 运行数据库迁移(创建表)
+    ├─ 3. 调用 Extension::install()
+    ├─ 4. 保存默认配置
+    └─ 5. 更新扩展状态为"已安装"
+    ↓
+触发 after_install_extension 事件
+    ↓
+✨ 自动部署前端代码
+FrontendDeployService::deploy()
+    ├─ 1. 检查插件是否有前端代码
+    ├─ 2. 复制前端代码到 frontend/admin/src/views/
+    ├─ 3. 记录部署日志
+    ├─ 4. 推送部署状态(可选)
+    └─ 5. 触发 npm 编译(可选)
+    ↓
+返回 200 OK
+    ↓
+前端显示"安装成功"
+用户可以立即使用插件功能
+```
+
+#### 卸载插件
+
+```
+用户访问插件管理页面
+http://localhost:3000/plugins/index
+    ↓
+点击插件的"卸载"按钮
+    ↓
+前端发送 POST 请求
+POST https://sixshop.ddev.site/admin/system/extension/{plugin-id}/uninstall
+    ↓
+触发 before_uninstall_extension 事件
+    ↓
+✨ 自动清理前端代码
+FrontendDeployService::remove()
+    ├─ 1. 检查前端代码是否存在
+    ├─ 2. 删除 frontend/admin/src/views/{plugin-id}/
+    ├─ 3. 记录删除日志
+    ├─ 4. 推送删除状态(可选)
+    └─ 5. 触发 npm 编译(可选)
+    ↓
+后端处理卸载
+    ├─ 1. 运行数据库回滚(删除表)
+    ├─ 2. 调用 Extension::uninstall()
+    ├─ 3. 删除配置数据
+    └─ 4. 更新扩展状态为"未安装"
+    ↓
+返回 200 OK
+    ↓
+前端显示"卸载成功"
+插件完全清理,不留痕迹
+```
+
+### 技术流程
+
+```
+ExtensionController::install()
+    ↓
+ExtensionManager::install()
+    ├─ 检查扩展状态
+    ├─ 运行数据库迁移
+    ├─ 调用 Extension::install()
+    └─ 更新扩展状态
+    ↓
+触发 after_install_extension 事件
+    ↓
+FrontendDeployService::deploy()
+    ├─ 检查是否有前端代码
+    ├─ 复制前端代码到 views 目录
+    ├─ 记录部署日志
+    ├─ 推送部署状态(可选)
+    └─ 触发 npm 编译(可选)
+    ↓
+返回安装成功
+```
+
+### 关键特性
+
+#### 1. 一键完成
+
+**安装**:
+- ✅ 用户只需点击一次"安装"按钮
+- ✅ 后端自动完成前端+后端安装
+- ✅ 前端代码自动部署到正确位置
+- ✅ 数据库表自动创建
+- ✅ 默认配置自动保存
+- ✅ 用户无需任何额外操作
+
+**卸载**:
+- ✅ 用户只需点击一次"卸载"按钮
+- ✅ 前端代码自动删除
+- ✅ 数据库表自动清理
+- ✅ 配置数据自动删除
+- ✅ 完全卸载,不留痕迹
+
+#### 2. 完全自动化
+
+**对用户**:
+- 无需手动复制文件
+- 无需手动运行命令
+- 无需手动删除文件
+- 安装即可用,卸载即清理
+
+**对开发者**:
+- 无需修改现有代码
+- 只需添加 frontend 目录
+- 系统自动处理一切
+
+**对系统**:
+- 不侵入现有流程
+- 通过事件监听实现
+- 解耦合,易维护
+
+#### 3. 智能检测
+
+**路径检测**:
+- 自动检测插件位置(runtime/extension 或 vendor)
+- 自动检测前端代码是否存在
+- 无前端代码自动跳过
+
+**状态管理**:
+- 实时记录部署状态
+- 完整的操作日志
+- 可追溯的部署历史
+
+#### 4. 安全可靠
+
+**安全性**:
+- 命令执行安全检查
+- 路径安全验证
+- 权限检查机制
+
+**可靠性**:
+- 完善的错误处理
+- 失败自动回滚
+- 不影响插件安装
+
+### 核心组件
+
+#### 1. FrontendDeployService(前端部署服务)
+
+**职责**:
+- 检查插件是否有前端代码
+- 复制/删除前端代码
+- 触发编译任务
+- 状态管理和日志记录
+
+**位置**:
+```
+backend/runtime/extension/system/src/Service/FrontendDeployService.php
+```
+
+#### 2. BuildFrontendJob(编译队列任务)
+
+**职责**:
+- 异步执行 npm 编译
+- 实时推送编译进度
+- 错误处理和日志记录
+
+**位置**:
+```
+backend/runtime/extension/system/src/Job/BuildFrontendJob.php
+```
+
+#### 3. 事件监听器
+
+**监听事件**:
+- `after_install_extension` - 安装后部署前端
+- `before_uninstall_extension` - 卸载前清理前端
+
+**位置**:
+```
+backend/runtime/extension/system/src/Extension.php
+```
+
+## 📁 文件结构
+
+### 插件安装位置
+
+插件可能安装在两个位置:
+
+#### 1. 本地开发插件(runtime/extension)
+```
+backend/runtime/extension/business-card/
+├── frontend/                  ✨ 新增:前端代码目录
+│   └── business-card/        # 使用插件名称作为子目录
+│       ├── api/              # API 接口
+│       │   └── index.js
+│       ├── components/       # 组件
+│       │   ├── CardPanel.vue
+│       │   ├── CardForm.vue
+│       │   └── ...
+│       ├── index.vue         # 主页面
+│       └── router.js         # 路由配置
+├── src/                      # 后端代码
+├── database/                 # 数据库迁移
+└── ...
+```
+
+#### 2. Composer 安装插件(vendor)
+```
+backend/vendor/sixdec/business-card/
+├── frontend/                  ✨ 新增:前端代码目录
+│   └── business-card/        # 使用插件名称作为子目录
+│       ├── api/
+│       ├── components/
+│       ├── index.vue
+│       └── router.js
+├── src/
+├── database/
+└── ...
+```
+
+**说明**:
+- `runtime/extension/` - 本地开发的插件
+- `vendor/six-shop/` - Six-Shop 官方插件
+- `vendor/sixdec/` - SixDec 第三方插件
+- 系统会自动检测插件的实际位置
+- **前端代码目录结构**:`frontend/{plugin-name}/` - 使用插件名称作为子目录,方便直接复制
+
+### 部署后的位置
+
+```
+frontend/admin/src/views/
+├── {plugin-name}/        ✨ 自动复制到这里
+│   ├── api/
+│   ├── components/
+│   ├── index.vue
+│   └── router.js
+├── other-plugin/
+└── ...
+```
+
+### 部署记录
+
+```
+backend/runtime/
+├── frontend_deploy/      ✨ 新增:部署记录目录
+│   ├── deployed.json     # 已部署的插件列表
+│   └── logs/             # 部署日志
+│       ├── business-card.log
+│       └── ...
+└── ...
+```
+
+## 🔧 核心功能实现
+
+### 1. 插件路径检测
+
+```php
+/**
+ * 获取插件的实际路径
+ * 
+ * 插件可能在两个位置:
+ * 1. runtime/extension/{plugin-name}  - 本地开发
+ * 2. vendor/{vendor}/{plugin-name}    - Composer 安装
+ */
+private function getPluginPath(string $extensionId): string
+{
+    // 方法 1: 使用 Helper::extension_path()
+    // 这个方法已经处理了路径检测
+    $path = Helper::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}");
+}
+
+/**
+ * 获取插件前端代码路径
+ */
+private function getPluginFrontendPath(string $extensionId): string
+{
+    $pluginPath = $this->getPluginPath($extensionId);
+    return $pluginPath . '/frontend';
+}
+```
+
+### 2. 前端代码检测
+
+```php
+/**
+ * 检查插件是否有前端代码
+ */
+private function hasFrontendCode(string $extensionId): bool
+{
+    try {
+        $frontendPath = $this->getPluginFrontendPath($extensionId);
+    } catch (\Exception $e) {
+        Log::warning("获取插件路径失败:{$extensionId},错误:" . $e->getMessage());
+        return false;
+    }
+    
+    // 检查目录是否存在
+    if (!is_dir($frontendPath)) {
+        return false;
+    }
+    
+    // 检查必需文件是否存在
+    $requiredFiles = ['index.vue', 'router.js'];
+    foreach ($requiredFiles as $file) {
+        if (!file_exists($frontendPath . '/' . $file)) {
+            Log::warning("插件 {$extensionId} 缺少必需文件:{$file}");
+            return false;
+        }
+    }
+    
+    return true;
+}
+```
+
+### 3. 文件复制
+
+```php
+/**
+ * 复制前端代码
+ * 
+ * 源目录结构:{plugin-path}/frontend/business-card/
+ * 目标目录:frontend/admin/src/views/business-card/
+ * 
+ * 直接复制 frontend/ 目录下的内容到 views/ 目录
+ */
+private function copyFrontendCode(string $extensionId): void
+{
+    // 1. 获取源目录:{plugin-path}/frontend/
+    $sourcePath = $this->getPluginPath($extensionId) . '/frontend';
+    
+    if (!is_dir($sourcePath)) {
+        throw new RuntimeException("前端代码目录不存在:{$sourcePath}");
+    }
+    
+    // 2. 获取目标目录:frontend/admin/src/views/
+    $targetPath = base_path('frontend/admin/src/views');
+    
+    if (!is_dir($targetPath)) {
+        throw new RuntimeException("目标目录不存在:{$targetPath}");
+    }
+    
+    // 3. 直接复制 frontend/ 目录下的所有内容
+    // 例如:frontend/business-card/ -> views/business-card/
+    $this->copyDirectory($sourcePath, $targetPath);
+    
+    Log::info("前端代码复制成功:{$extensionId}");
+}
+
+/**
+ * 递归复制目录
+ */
+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);
+            }
+            copy($item, $targetPath);
+        }
+    }
+}
+```
+
+**优势**:
+- ✅ 无需重命名,直接复制
+- ✅ 保持目录结构完整
+- ✅ 支持多层嵌套目录
+- ✅ 代码简单,易于维护
+
+**示例**:
+```
+源:backend/runtime/extension/business-card/frontend/business-card/
+目标:frontend/admin/src/views/business-card/
+
+复制结果:
+frontend/admin/src/views/
+└── business-card/
+    ├── api/
+    ├── components/
+    ├── index.vue
+    └── router.js
+```
+
+### 4. 文件删除
+
+```php
+/**
+ * 删除前端代码
+ */
+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);
+        } else {
+            unlink($item);
+        }
+    }
+    
+    rmdir($path);
+}
+```
+
+### 5. 编译触发
+
+```php
+/**
+ * 触发前端编译
+ */
+private function triggerBuild(string $extensionId): void
+{
+    // 开发环境不编译
+    if (app()->isDebug()) {
+        Log::info("开发环境,跳过编译");
+        return;
+    }
+    
+    // 检查 exec 是否可用
+    if (!$this->isExecAvailable()) {
+        Log::warning("exec 函数被禁用,无法自动编译");
+        return;
+    }
+    
+    // 使用队列异步编译
+    Queue::push(new BuildFrontendJob($extensionId));
+}
+```
+
+## 🔒 安全性设计
+
+### 1. 命令执行安全
+
+```php
+/**
+ * 检查 exec 是否可用
+ */
+private function isExecAvailable(): bool
+{
+    $disabled = explode(',', ini_get('disable_functions'));
+    return !in_array('exec', $disabled);
+}
+
+/**
+ * 安全执行命令
+ */
+private function safeExec(string $command): array
+{
+    // 1. 检查函数可用性
+    if (!$this->isExecAvailable()) {
+        throw new RuntimeException('exec 函数被禁用');
+    }
+    
+    // 2. 转义命令
+    $command = escapeshellcmd($command);
+    
+    // 3. 执行并获取输出
+    exec($command, $output, $returnCode);
+    
+    return [
+        'output' => $output,
+        'code' => $returnCode
+    ];
+}
+```
+
+### 2. 路径安全
+
+```php
+/**
+ * 验证路径安全性
+ */
+private function validatePath(string $path): bool
+{
+    // 1. 检查路径是否包含危险字符
+    if (preg_match('/\.\./', $path)) {
+        return false;
+    }
+    
+    // 2. 检查路径是否在允许的范围内
+    $allowedPaths = [
+        base_path('frontend/admin/src/views'),
+        Helper::extension_path()
+    ];
+    
+    $realPath = realpath($path);
+    foreach ($allowedPaths as $allowedPath) {
+        if (strpos($realPath, realpath($allowedPath)) === 0) {
+            return true;
+        }
+    }
+    
+    return false;
+}
+```
+
+### 3. 权限检查
+
+```php
+/**
+ * 检查文件系统权限
+ */
+private function checkPermissions(): array
+{
+    $errors = [];
+    
+    // 检查源目录可读
+    $sourcePath = Helper::extension_path();
+    if (!is_readable($sourcePath)) {
+        $errors[] = "源目录不可读:{$sourcePath}";
+    }
+    
+    // 检查目标目录可写
+    $targetPath = base_path('frontend/admin/src/views');
+    if (!is_writable($targetPath)) {
+        $errors[] = "目标目录不可写:{$targetPath}";
+    }
+    
+    return $errors;
+}
+```
+
+## 📊 进度反馈方案
+
+### 方案对比
+
+| 方案 | 实时性 | 实现难度 | 服务器要求 | 推荐度 |
+|------|--------|----------|-----------|--------|
+| WebSocket | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 需要 WebSocket | ⭐⭐⭐⭐ |
+| 轮询 | ⭐⭐⭐ | ⭐⭐ | 无特殊要求 | ⭐⭐⭐⭐⭐ |
+| 日志文件 | ⭐⭐ | ⭐ | 无特殊要求 | ⭐⭐⭐ |
+
+### 推荐方案:轮询 + 缓存
+
+#### 后端实现
+
+```php
+/**
+ * 设置部署状态
+ */
+private function setDeployStatus(string $extensionId, string $status, string $message, int $progress = 0): void
+{
+    cache('frontend_deploy_' . $extensionId, [
+        'status' => $status,      // idle, copying, building, success, error
+        'message' => $message,
+        'progress' => $progress,  // 0-100
+        'updated_at' => time()
+    ], 3600); // 缓存1小时
+}
+
+/**
+ * 获取部署状态
+ */
+public function getDeployStatus(string $extensionId): array
+{
+    return cache('frontend_deploy_' . $extensionId) ?: [
+        'status' => 'idle',
+        'message' => '',
+        'progress' => 0,
+        'updated_at' => 0
+    ];
+}
+```
+
+#### 前端实现
+
+```javascript
+// 安装插件后开始轮询
+async function installExtension(extensionId) {
+  // 1. 调用安装接口
+  await api.post(`/extension/${extensionId}/install`)
+  
+  // 2. 开始轮询部署状态
+  startPolling(extensionId)
+}
+
+// 轮询部署状态
+function startPolling(extensionId) {
+  const timer = setInterval(async () => {
+    const status = await api.get(`/extension/${extensionId}/deploy-status`)
+    
+    // 更新进度条
+    updateProgress(status.progress, status.message)
+    
+    // 检查是否完成
+    if (status.status === 'success') {
+      ElMessage.success('安装完成!')
+      clearInterval(timer)
+    } else if (status.status === 'error') {
+      ElMessage.error('安装失败:' + status.message)
+      clearInterval(timer)
+    }
+  }, 2000) // 每2秒查询一次
+}
+```
+
+## 🔄 兼容性处理
+
+### 1. 无前端代码的插件
+
+```php
+public function deploy(string $extensionId): bool
+{
+    // 检查是否有前端代码
+    if (!$this->hasFrontendCode($extensionId)) {
+        Log::info("插件 {$extensionId} 没有前端代码,跳过部署");
+        return true; // 返回 true 表示成功(跳过)
+    }
+    
+    // 继续部署流程...
+}
+```
+
+### 2. exec 函数被禁用
+
+```php
+private function triggerBuild(string $extensionId): void
+{
+    if (!$this->isExecAvailable()) {
+        // 记录警告
+        Log::warning("exec 函数被禁用,无法自动编译");
+        
+        // 写入标记文件,提示用户手动编译
+        file_put_contents(
+            runtime_path('need_build.txt'),
+            "请手动执行:cd frontend/admin && npm run build\n",
+            FILE_APPEND
+        );
+        
+        return;
+    }
+    
+    // 正常编译流程...
+}
+```
+
+### 3. 编译失败处理
+
+```php
+class BuildFrontendJob
+{
+    public function handle(): void
+    {
+        try {
+            // 执行编译
+            $result = $this->safeExec('cd frontend/admin && npm run build');
+            
+            if ($result['code'] !== 0) {
+                throw new RuntimeException('编译失败:' . implode("\n", $result['output']));
+            }
+            
+            // 更新状态为成功
+            $this->setStatus('success', '编译完成', 100);
+            
+        } catch (\Exception $e) {
+            // 记录错误
+            Log::error('前端编译失败:' . $e->getMessage());
+            
+            // 更新状态为失败
+            $this->setStatus('error', $e->getMessage(), 0);
+            
+            // 不抛出异常,避免影响其他任务
+        }
+    }
+}
+```
+
+## 📝 部署记录
+
+### 记录格式
+
+```json
+{
+  "deployed": [
+    {
+      "extension_id": "business-card",
+      "deployed_at": "2024-11-04 10:30:00",
+      "source_path": "/path/to/extension/business-card/frontend",
+      "target_path": "/path/to/admin/views/business-card",
+      "files_count": 15,
+      "status": "success"
+    }
+  ]
+}
+```
+
+### 记录管理
+
+```php
+/**
+ * 记录部署信息
+ */
+private function logDeploy(string $extensionId, array $info): void
+{
+    $logFile = runtime_path('frontend_deploy/deployed.json');
+    
+    // 读取现有记录
+    $deployed = [];
+    if (file_exists($logFile)) {
+        $deployed = json_decode(file_get_contents($logFile), true);
+    }
+    
+    // 添加新记录
+    $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));
+}
+```
+
+## 🧪 测试方案
+
+### 测试场景
+
+1. **正常安装**
+   - 有前端代码的插件
+   - 自动复制和编译
+
+2. **无前端代码**
+   - 没有 frontend 目录的插件
+   - 跳过前端部署
+
+3. **重复安装**
+   - 已安装的插件
+   - 覆盖现有文件
+
+4. **卸载清理**
+   - 删除前端代码
+   - 清理部署记录
+
+5. **异常处理**
+   - 权限不足
+   - exec 被禁用
+   - 编译失败
+
+### 测试步骤
+
+```bash
+# 1. 测试安装
+POST /admin/system/extension/business_card/install
+
+# 2. 检查文件是否复制
+ls frontend/admin/src/views/business-card
+
+# 3. 检查部署状态
+GET /admin/system/extension/business_card/deploy-status
+
+# 4. 测试卸载
+POST /admin/system/extension/business_card/uninstall
+
+# 5. 检查文件是否删除
+ls frontend/admin/src/views/business-card  # 应该不存在
+```
+
+## 📈 性能优化
+
+### 1. 异步处理
+
+- 文件复制:同步执行(快速)
+- npm 编译:异步队列(慢速)
+- 状态推送:异步(不阻塞)
+
+### 2. 缓存策略
+
+- 部署状态:缓存 1 小时
+- 部署记录:持久化存储
+- 编译日志:定期清理
+
+### 3. 队列优化
+
+```php
+// 使用延迟队列,避免并发编译
+Queue::later(5, new BuildFrontendJob($extensionId));
+
+// 使用队列优先级
+Queue::push(new BuildFrontendJob($extensionId), '', 'high');
+```
+
+## 🚀 实施计划
+
+### 阶段 1: 核心功能(1-2天)
+
+- [ ] 创建 FrontendDeployService
+- [ ] 实现文件复制/删除
+- [ ] 注册事件监听
+- [ ] 基础测试
+
+### 阶段 2: 编译功能(1天)
+
+- [ ] 创建 BuildFrontendJob
+- [ ] 实现安全的命令执行
+- [ ] 错误处理和日志
+
+### 阶段 3: 进度反馈(1天)
+
+- [ ] 实现状态管理
+- [ ] 添加 API 接口
+- [ ] 前端轮询实现
+
+### 阶段 4: 完善和测试(1天)
+
+- [ ] 完整测试所有场景
+- [ ] 优化性能
+- [ ] 编写文档
+
+## 📚 使用文档
+
+### 插件开发者
+
+#### 1. 创建前端代码目录
+
+```bash
+cd backend/runtime/extension/your-plugin
+mkdir frontend
+```
+
+#### 2. 组织前端代码
+
+```
+frontend/
+├── api/
+│   └── index.js
+├── components/
+│   └── YourComponent.vue
+├── index.vue
+└── router.js
+```
+
+#### 3. 测试安装
+
+```bash
+# 安装插件
+POST /admin/system/extension/your-plugin/install
+
+# 检查前端代码
+ls frontend/admin/src/views/your-plugin
+```
+
+### 系统管理员
+
+#### 1. 检查权限
+
+```bash
+# 检查目录权限
+ls -la frontend/admin/src/views
+ls -la backend/runtime/extension
+```
+
+#### 2. 查看部署日志
+
+```bash
+# 查看部署记录
+cat backend/runtime/frontend_deploy/deployed.json
+
+# 查看编译日志
+cat backend/runtime/frontend_deploy/logs/your-plugin.log
+```
+
+#### 3. 手动编译
+
+```bash
+# 如果自动编译失败,可以手动编译
+cd frontend/admin
+npm run build
+```
+
+## ❓ 常见问题
+
+### Q1: exec 函数被禁用怎么办?
+
+**A**: 系统会自动检测并跳过编译,生成提示文件 `runtime/need_build.txt`,管理员可以手动执行编译。
+
+### Q2: 编译很慢怎么办?
+
+**A**: 编译是异步执行的,不会阻塞安装流程。可以通过轮询查看编译进度。
+
+### Q3: 安装失败如何回滚?
+
+**A**: 系统会自动删除已复制的文件,不会留下垃圾文件。
+
+### Q4: 开发时需要每次编译吗?
+
+**A**: 不需要。开发环境会跳过编译,直接使用 Vite 热更新。
+
+### Q5: 如何查看部署状态?
+
+**A**: 调用 API:`GET /admin/system/extension/{id}/deploy-status`
+
+## 📞 技术支持
+
+如有问题,请联系:
+- 邮箱:support@sixdec.com
+- 文档:查看本文档
+- 日志:`backend/runtime/frontend_deploy/logs/`
+
+---
+
+**文档结束**