|
|
@@ -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/`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**文档结束**
|