Forráskód Böngészése

refactor(system): 迁移数据库类并优化扩展管理

- 将迁移类从 think\migration\Migrator 替换为 Phinx\Migration\AbstractMigration
- 在多个命令类中移除 SixShop\Core\Helper 的引用
- 更新 ExtensionScaffoldMakeCommand 中的 extension_path 函数调用方式
- 删除废弃的 ExtensionConfig 类
- 更新 ExtensionConfigController 中的 success_response 函数调用
- 从 ExtensionController 中移除未使用的函数导入
- 在 Cron 定时任务中改进日志记录和可调用性检查
- 标记 MacroPageMiddleware 为已弃用
- 移除前端部署服务相关功能代码
- 优化 ExtensionManager 中的配置获取逻辑
- 移除系统日志类中的 debug 开关相关功能
- 更新 Migrate 类中的路径获取方式
- 从配置中移除普通模块加载列表选项
runphp 1 hónapja
szülő
commit
d85668c089

+ 0 - 31
config.php

@@ -18,37 +18,6 @@ return [
         'hidden' => false,
         '_fc_drag_tag' => 'switch'
     ],
-    [
-        'type' => 'checkbox',
-        'field' => 'normal_module_list',
-        'title' => '普通模块加载列表',
-        'info' => '选择状态的模块才会加载',
-        'effect' => [
-            'fetch' => [
-                'action' => '{{API_BASE_URL}}/admin/system/extension/normal',
-                'method' => 'GET',
-                'dataType' => 'json',
-                'headers' => [
-                    "Authorization" => "Bearer {{API_TOKEN}}"
-                ],
-                'query' => [],
-                'data' => [],
-                'parse' => '',
-                'beforeFetch' => '',
-                'onError' => '',
-                'to' => 'options'
-            ]
-        ],
-        '$required' => false,
-        'props' => [
-            '_optionType' => 1
-        ],
-        '_fc_id' => 'id_Ffunmdtoraneacc',
-        'name' => 'ref_Fq8pmdtoraneadc',
-        'display' => true,
-        'hidden' => true,
-        '_fc_drag_tag' => 'checkbox'
-    ],
     [
         'type' => 'group',
         'field' => 'category',

+ 3 - 2
database/migrations/20250624125157_extension_config.php

@@ -1,8 +1,9 @@
 <?php
+declare(strict_types=1);
 
-use think\migration\Migrator;
+use Phinx\Migration\AbstractMigration;
 
-class ExtensionConfig extends Migrator
+class ExtensionConfig extends AbstractMigration
 {
     /**
      * Change Method.

+ 3 - 2
database/migrations/20250627061219_extension.php

@@ -1,8 +1,9 @@
 <?php
+declare(strict_types=1);
 
-use think\migration\Migrator;
+use Phinx\Migration\AbstractMigration;
 
-class Extension extends Migrator
+class Extension extends AbstractMigration
 {
     /**
      * Change Method.

+ 3 - 2
database/migrations/20250702110812_extension_add_category.php

@@ -1,8 +1,9 @@
 <?php
+declare(strict_types=1);
 
-use think\migration\Migrator;
+use Phinx\Migration\AbstractMigration;
 
-class ExtensionAddCategory extends Migrator
+class ExtensionAddCategory extends AbstractMigration
 {
     /**
      * Change Method.

+ 0 - 1
src/Command/CoreExtensionConfigCommand.php

@@ -3,7 +3,6 @@ declare(strict_types=1);
 
 namespace SixShop\System\Command;
 
-use SixShop\Core\Helper;
 use SixShop\System\ExtensionManager;
 use SixShop\System\Migrate;
 use SixShop\System\Model\ExtensionModel;

+ 0 - 1
src/Command/ExtensionManagementCommand.php

@@ -3,7 +3,6 @@ declare(strict_types=1);
 
 namespace SixShop\System\Command;
 
-use SixShop\Core\Helper;
 use SixShop\System\ExtensionManager;
 use think\console\Command;
 use think\console\Input;

+ 2 - 2
src/Command/ExtensionScaffoldMakeCommand.php

@@ -3,7 +3,7 @@ declare(strict_types=1);
 
 namespace SixShop\System\Command;
 
-use SixShop\Core\Helper;
+use function SixShop\Core\extension_path;
 use think\console\Command;
 use think\console\Input;
 use think\console\input\Argument;
@@ -59,7 +59,7 @@ class ExtensionScaffoldMakeCommand extends Command
             // $withFFI 默认为 false,避免环境未开启 FFI 导致构建失败
         }
 
-        $base = rtrim(Helper::extension_path($module), '/');
+        $base = rtrim(extension_path($module), '/');
         if (is_dir($base) && !$force) {
             $output->error("扩展 {$module} 已存在:{$base},可使用 --force 覆盖写入");
             return 1;

+ 0 - 39
src/Config/ExtensionConfig.php

@@ -1,39 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace SixShop\System\Config;
-
-/**
- * 模块配置
- */
-class ExtensionConfig
-{
-    /**
-     * 模块共用的配置
-     * @var array
-     */
-    const array BASE = [
-        [
-            'type' => 'switch',
-            'field' => 'debug',
-            'title' => 'Debug开关',
-            'info' => '您可以开启或关闭debug模式',
-            'required' => false,
-            'props' => [
-                'activeValue' => true,
-                'inactiveValue' => false,
-                'disabled' => false,
-                'activeText' => '开启',
-                'inactiveText' => '关闭',
-                'activeColor' => '#40FB07FF',
-                'inactiveColor' => '#FF0000FF'
-            ],
-            '_fc_id' => 'id_Fakamcartp0rawc',
-            'name' => 'ref_F15kmcartp0raxc',
-            'display' => true,
-            'hidden' => false,
-            '_fc_drag_tag' => 'switch'
-        ]
-    ];
-
-}

+ 4 - 4
src/Controller/ExtensionConfigController.php

@@ -3,7 +3,7 @@ declare(strict_types=1);
 
 namespace SixShop\System\Controller;
 
-use SixShop\Core\Helper;
+use function SixShop\Core\success_response;
 use SixShop\System\ExtensionManager;
 use think\Request;
 use think\Response;
@@ -12,16 +12,16 @@ class ExtensionConfigController
 {
     public function read(string $id, ExtensionManager $extensionManager): Response
     {
-        return Helper::success_response($extensionManager->getExtensionConfig($id));
+        return success_response($extensionManager->getExtensionConfig($id));
     }
 
     public function edit(string $id, ExtensionManager $extensionManager): Response
     {
-        return Helper::success_response($extensionManager->getExtensionConfigForm($id));
+        return success_response($extensionManager->getExtensionConfigForm($id));
     }
 
     public function update(string $id, ExtensionManager $extensionManager, Request $request): Response
     {
-        return Helper::success_response($extensionManager->saveConfig($id, $request->post()));
+        return success_response($extensionManager->saveConfig($id, $request->post()));
     }
 }

+ 0 - 2
src/Controller/ExtensionController.php

@@ -10,8 +10,6 @@ use think\App;
 use think\facade\Event;
 use think\paginator\driver\Bootstrap;
 use think\Response;
-use function SixShop\Core\extension_path;
-use function SixShop\Core\success_response;
 
 class ExtensionController
 {

+ 1 - 32
src/Extension.php

@@ -42,10 +42,7 @@ class Extension extends ExtensionAbstract
         if ($this->isBooted) {
             return;
         }
-        
-        // 注册前端部署事件监听
-        $this->registerFrontendDeployListeners();
-        
+
         foreach (extension_name_list() as $extensionID) {
             /** @var ExtensionAbstract $extension */
             $extension = $this->autoloadService->getExtension($extensionID);
@@ -69,34 +66,6 @@ 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(): true
     {

+ 1 - 2
src/ExtensionManager.php

@@ -8,7 +8,6 @@ use RuntimeException;
 use SixShop\Core\Contracts\ExtensionInterface;
 use SixShop\Core\Service\CoreService;
 use SixShop\Payment\Contracts\PaymentExtensionInterface;
-use SixShop\System\Config\ExtensionConfig;
 use SixShop\System\Enum\ExtensionStatusEnum;
 use SixShop\System\Model\ExtensionConfigModel;
 use SixShop\System\Model\ExtensionModel;
@@ -324,7 +323,7 @@ class ExtensionManager extends Service
 
     private function getLocalConfig(string $extensionID): Collection
     {
-        $config = array_merge(ExtensionConfig::BASE, $this->getExtension($extensionID)->getConfig());
+        $config = $this->getExtension($extensionID)->getConfig();
         $configCollection = new Collection($config);
         Event::trigger('after_read_extension_config', [$configCollection, $extensionID]);
         Event::trigger('after_read_extension_config_' . $extensionID, $configCollection);

+ 0 - 1
src/Hook/ExtensionStatusHook.php

@@ -6,7 +6,6 @@ namespace SixShop\System\Hook;
 use Closure;
 use SixShop\Core\Attribute\Hook;
 use SixShop\Core\Event\BeforeRegisterRouteEvent;
-use SixShop\Core\Helper;
 use SixShop\Core\Request;
 use SixShop\System\Enum\ExtensionStatusEnum;
 use SixShop\System\ExtensionManager;

+ 12 - 2
src/Hook/GatheringCrontabEventHook.php

@@ -7,11 +7,11 @@ use ReflectionClass;
 use ReflectionMethod;
 use SixShop\Core\Attribute\Cron;
 use SixShop\Core\Attribute\Hook;
-use SixShop\Core\Helper;
 use SixShop\System\Event\CrontabWorkerStartEvent;
 use SixShop\System\Event\GetCronJobsEvent;
 use SixShop\System\ExtensionManager;
 use think\App;
+use think\facade\Log;
 use Workerman\Crontab\Crontab;
 use function SixShop\Core\extension_name_list;
 
@@ -41,12 +41,22 @@ class GatheringCrontabEventHook
         $this->app->event->trigger($event);
         foreach ($event as $cronJobClass) {
             $ref = new ReflectionClass($cronJobClass);
+            $objectAttts = $ref->getAttributes(Cron::class);
+            $cronJob = $this->app->make($cronJobClass);
+            foreach ($objectAttts as $attribute) {
+                $cronInstance = $attribute->newInstance();
+                if (is_callable($cronJob)) {
+                    new Crontab($cronInstance->rule, $cronJob, $cronInstance->name?:$cronJobClass);
+                } else {
+                    Log::warning("Cron job {$cronJobClass} is not callable");
+                }
+            }
             foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
                 $attributes = $method->getAttributes(Cron::class);
                 foreach ($attributes as $attribute) {
                     $cronInstance = $attribute->newInstance();
                     $name = $cronInstance->name ?: $cronJobClass . '@' . $method->getName();
-                    new Crontab($cronInstance->rule, [$this->app->make($cronJobClass), $method->getName()], $name);
+                    new Crontab($cronInstance->rule, [$cronJob, $method->getName()], $name);
                 }
             }
         }

+ 0 - 41
src/Log.php

@@ -1,41 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace SixShop\System;
-
-use think\App;
-use think\event\HttpEnd;
-
-class Log extends \think\Log
-{
-    public function __construct(App $app, private ExtensionManager $extensionManager, private string $extensionID)
-    {
-        if (!$app->runningInConsole()) {
-            $app->event->listen(HttpEnd::class, function () {
-                $this->save();
-            });
-        }
-        parent::__construct($app);
-    }
-
-    public function getConfig(?string $name = null, $default = null)
-    {
-        if ($name == 'level') {
-            return $this->getLevelConfig();
-        }
-        return parent::getConfig($name, $default);
-    }
-
-    private function getLevelConfig(): array
-    {
-        $level = parent::getConfig('level', []);
-        if (in_array('debug', $level)) {
-            return $level;
-        }
-        $debug = $this->extensionManager->getExtensionConfig($this->extensionID, 'debug');
-        if ($debug) {
-            $level[] = 'debug';
-        }
-        return $level;
-    }
-}

+ 3 - 0
src/Middleware/MacroPageMiddleware.php

@@ -8,6 +8,9 @@ use SixShop\Core\Request;
 use think\Exception;
 use think\Response;
 
+/**
+ * @deprecated
+ */
 class MacroPageMiddleware
 {
     public function handle(Request $request, Closure $next): Response

+ 2 - 2
src/Migrate.php

@@ -9,10 +9,10 @@ use Phinx\Db\Adapter\AdapterInterface;
 use Phinx\Migration\AbstractMigration;
 use Phinx\Migration\MigrationInterface;
 use Phinx\Util\Util;
-use SixShop\Core\Helper;
 use SixShop\System\Model\MigrationsModel;
 use think\App;
 use think\Model;
+use function SixShop\Core\extension_path;
 
 class Migrate
 {
@@ -28,7 +28,7 @@ class Migrate
     {
         $this->app = $app;
         $this->moduleName = $moduleName;
-        $this->path = Helper::extension_path($this->moduleName) . 'database' . DIRECTORY_SEPARATOR . 'migrations';
+        $this->path = extension_path($this->moduleName) . 'database' . DIRECTORY_SEPARATOR . 'migrations';
         $this->migrations = $this->getMigrations();
         $this->input = null;
         $this->output = null;

+ 0 - 352
src/Service/FrontendDeployService.php

@@ -1,352 +0,0 @@
-<?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)
-        );
-    }
-}

+ 1 - 1
src/Trait/ConfigTrait.php

@@ -12,7 +12,7 @@ trait ConfigTrait
     {
     }
 
-    public function getConfig(string $key = null, mixed $default = null): mixed
+    public function getConfig(?string $key = null, mixed $default = null): mixed
     {
         if (empty($this->options)) {
             if (!method_exists($this, 'getExtensionID')) {

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

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