|
|
@@ -1,1010 +0,0 @@
|
|
|
-# 插件前端自动化部署方案
|
|
|
-
|
|
|
-## 📋 文档信息
|
|
|
-
|
|
|
-- **文档版本**: 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/`
|
|
|
-
|
|
|
----
|
|
|
-
|
|
|
-**文档结束**
|